[WIP] Enable custom emoji on account pages and in the sidebar (#6124)
Federate custom emojis with accounts
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							f464f98fd3
						
					
				
				
					commit
					123a343d11
				
			| @@ -51,9 +51,14 @@ class Formatter | ||||
|     strip_tags(text) | ||||
|   end | ||||
|  | ||||
|   def simplified_format(account) | ||||
|     return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety | ||||
|     linkify(account.note) | ||||
|   def simplified_format(account, **options) | ||||
|     html = if account.local? | ||||
|              linkify(account.note) | ||||
|            else | ||||
|              reformat(account.note) | ||||
|            end | ||||
|     html = encode_custom_emojis(html, CustomEmoji.from_text(account.note, account.domain)) if options[:custom_emojify] | ||||
|     html.html_safe # rubocop:disable Rails/OutputSafety | ||||
|   end | ||||
|  | ||||
|   def sanitize(html, config) | ||||
|   | ||||
| @@ -26,6 +26,9 @@ class OStatus::AtomSerializer | ||||
|     append_element(author, 'link', nil, rel: :alternate, type: 'text/html', href: ::TagManager.instance.url_for(account)) | ||||
|     append_element(author, 'link', nil, rel: :avatar, type: account.avatar_content_type, 'media:width': 120, 'media:height': 120, href: full_asset_url(account.avatar.url(:original))) if account.avatar? | ||||
|     append_element(author, 'link', nil, rel: :header, type: account.header_content_type, 'media:width': 700, 'media:height': 335, href: full_asset_url(account.header.url(:original))) if account.header? | ||||
|     account.emojis.each do |emoji| | ||||
|       append_element(author, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode) | ||||
|     end | ||||
|     append_element(author, 'poco:preferredUsername', account.username) | ||||
|     append_element(author, 'poco:displayName', account.display_name) if account.display_name? | ||||
|     append_element(author, 'poco:note', account.local? ? account.note : strip_tags(account.note)) if account.note? | ||||
|   | ||||
| @@ -350,6 +350,10 @@ class Account < ApplicationRecord | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def emojis | ||||
|     CustomEmoji.from_text(note, domain) | ||||
|   end | ||||
|  | ||||
|   before_create :generate_keys | ||||
|   before_validation :normalize_domain | ||||
|   before_validation :prepare_contents, if: :local? | ||||
|   | ||||
| @@ -41,6 +41,10 @@ class RemoteProfile | ||||
|     @header ||= link_href_from_xml(author, 'header') | ||||
|   end | ||||
|  | ||||
|   def emojis | ||||
|     @emojis ||= author.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS) | ||||
|   end | ||||
|  | ||||
|   def locked? | ||||
|     scope == 'private' | ||||
|   end | ||||
|   | ||||
| @@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer | ||||
|  | ||||
|   has_one :public_key, serializer: ActivityPub::PublicKeySerializer | ||||
|  | ||||
|   has_many :virtual_tags, key: :tag | ||||
|  | ||||
|   attribute :moved_to, if: :moved? | ||||
|  | ||||
|   class EndpointsSerializer < ActiveModel::Serializer | ||||
| @@ -101,7 +103,14 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer | ||||
|     object.locked | ||||
|   end | ||||
|  | ||||
|   def virtual_tags | ||||
|     object.emojis | ||||
|   end | ||||
|  | ||||
|   def moved_to | ||||
|     ActivityPub::TagManager.instance.uri_for(object.moved_to_account) | ||||
|   end | ||||
|  | ||||
|   class CustomEmojiSerializer < ActivityPub::EmojiSerializer | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class REST::AccountSerializer < ActiveModel::Serializer | ||||
|   end | ||||
|  | ||||
|   def note | ||||
|     Formatter.instance.simplified_format(object) | ||||
|     Formatter.instance.simplified_format(object, custom_emojify: true) | ||||
|   end | ||||
|  | ||||
|   def url | ||||
|   | ||||
| @@ -22,6 +22,7 @@ class ActivityPub::ProcessAccountService < BaseService | ||||
|  | ||||
|         create_account if @account.nil? | ||||
|         update_account | ||||
|         process_tags(@account) | ||||
|       end | ||||
|     end | ||||
|  | ||||
| @@ -187,4 +188,31 @@ class ActivityPub::ProcessAccountService < BaseService | ||||
|   def lock_options | ||||
|     { redis: Redis.current, key: "process_account:#{@uri}" } | ||||
|   end | ||||
|  | ||||
|   def process_tags(account) | ||||
|     return if @json['tag'].blank? | ||||
|     as_array(@json['tag']).each do |tag| | ||||
|       case tag['type'] | ||||
|       when 'Emoji' | ||||
|         process_emoji tag, account | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def process_emoji(tag, _account) | ||||
|     return if skip_download? | ||||
|     return if tag['name'].blank? || tag['icon'].blank? || tag['icon']['url'].blank? | ||||
|  | ||||
|     shortcode = tag['name'].delete(':') | ||||
|     image_url = tag['icon']['url'] | ||||
|     uri       = tag['id'] | ||||
|     updated   = tag['updated'] | ||||
|     emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain) | ||||
|  | ||||
|     return unless emoji.nil? || emoji.updated_at >= updated | ||||
|  | ||||
|     emoji ||= CustomEmoji.new(domain: @account.domain, shortcode: shortcode, uri: uri) | ||||
|     emoji.image_remote_url = image_url | ||||
|     emoji.save | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -40,6 +40,27 @@ class UpdateRemoteProfileService < BaseService | ||||
|         account.header_remote_url = '' | ||||
|         account.header.destroy | ||||
|       end | ||||
|  | ||||
|       save_emojis(account) if remote_profile.emojis.present? | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def save_emojis(parent) | ||||
|     do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? | ||||
|  | ||||
|     return if do_not_download | ||||
|  | ||||
|     remote_account.emojis.each do |link| | ||||
|       next unless link['href'] && link['name'] | ||||
|  | ||||
|       shortcode = link['name'].delete(':') | ||||
|       emoji     = CustomEmoji.find_by(shortcode: shortcode, domain: parent.account.domain) | ||||
|  | ||||
|       next unless emoji.nil? | ||||
|  | ||||
|       emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain) | ||||
|       emoji.image_remote_url = link['href'] | ||||
|       emoji.save | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|             = t 'accounts.roles.moderator' | ||||
|  | ||||
|     .bio | ||||
|       .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account) | ||||
|       .account__header__content.p-note.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) | ||||
|  | ||||
|     .details-counters | ||||
|       .counter{ class: active_nav_class(short_account_url(account)) } | ||||
|   | ||||
| @@ -394,6 +394,45 @@ RSpec.describe Formatter do | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       context 'with custom_emojify option' do | ||||
|         let!(:emoji) { Fabricate(:custom_emoji) } | ||||
|  | ||||
|         before { account.note = text } | ||||
|         subject { Formatter.instance.simplified_format(account, custom_emojify: true) } | ||||
|  | ||||
|         context 'with emoji at the start' do | ||||
|           let(:text) { ':coolcat: Beep boop' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with emoji in the middle' do | ||||
|           let(:text) { 'Beep :coolcat: boop' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with concatenated emoji' do | ||||
|           let(:text) { ':coolcat::coolcat:' } | ||||
|  | ||||
|           it 'does not touch the shortcodes' do | ||||
|             is_expected.to match(/:coolcat::coolcat:/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with emoji at the end' do | ||||
|           let(:text) { 'Beep boop :coolcat:' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/boop <img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|  | ||||
|       include_examples 'encode and link URLs' | ||||
|     end | ||||
|  | ||||
| @@ -404,6 +443,46 @@ RSpec.describe Formatter do | ||||
|       it 'reformats' do | ||||
|         is_expected.to_not include '<script>alert("Hello")</script>' | ||||
|       end | ||||
|  | ||||
|       context 'with custom_emojify option' do | ||||
|         let!(:emoji) { Fabricate(:custom_emoji, domain: remote_account.domain) } | ||||
|  | ||||
|         before { remote_account.note = text } | ||||
|  | ||||
|         subject { Formatter.instance.simplified_format(remote_account, custom_emojify: true) } | ||||
|  | ||||
|         context 'with emoji at the start' do | ||||
|           let(:text) { '<p>:coolcat: Beep boop<br />' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/<p><img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with emoji in the middle' do | ||||
|           let(:text) { '<p>Beep :coolcat: boop</p>' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/Beep <img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with concatenated emoji' do | ||||
|           let(:text) { '<p>:coolcat::coolcat:</p>' } | ||||
|  | ||||
|           it 'does not touch the shortcodes' do | ||||
|             is_expected.to match(/<p>:coolcat::coolcat:<\/p>/) | ||||
|           end | ||||
|         end | ||||
|  | ||||
|         context 'with emoji at the end' do | ||||
|           let(:text) { '<p>Beep boop<br />:coolcat:</p>' } | ||||
|  | ||||
|           it 'converts shortcode to image tag' do | ||||
|             is_expected.to match(/<br><img draggable="false" class="emojione" alt=":coolcat:"/) | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user