[WIP] Enable custom emoji on account pages and in the sidebar (#6124)
Federate custom emojis with accounts
This commit is contained in:
		
							parent
							
								
									f464f98fd3
								
							
						
					
					
						commit
						123a343d11
					
				
					 10 changed files with 158 additions and 5 deletions
				
			
		| 
						 | 
					@ -51,9 +51,14 @@ class Formatter
 | 
				
			||||||
    strip_tags(text)
 | 
					    strip_tags(text)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def simplified_format(account)
 | 
					  def simplified_format(account, **options)
 | 
				
			||||||
    return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety
 | 
					    html = if account.local?
 | 
				
			||||||
    linkify(account.note)
 | 
					             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
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def sanitize(html, config)
 | 
					  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: :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: :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?
 | 
					    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:preferredUsername', account.username)
 | 
				
			||||||
    append_element(author, 'poco:displayName', account.display_name) if account.display_name?
 | 
					    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?
 | 
					    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
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def emojis
 | 
				
			||||||
 | 
					    CustomEmoji.from_text(note, domain)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_create :generate_keys
 | 
					  before_create :generate_keys
 | 
				
			||||||
  before_validation :normalize_domain
 | 
					  before_validation :normalize_domain
 | 
				
			||||||
  before_validation :prepare_contents, if: :local?
 | 
					  before_validation :prepare_contents, if: :local?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,10 @@ class RemoteProfile
 | 
				
			||||||
    @header ||= link_href_from_xml(author, 'header')
 | 
					    @header ||= link_href_from_xml(author, 'header')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def emojis
 | 
				
			||||||
 | 
					    @emojis ||= author.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def locked?
 | 
					  def locked?
 | 
				
			||||||
    scope == 'private'
 | 
					    scope == 'private'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,8 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 | 
					  has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  has_many :virtual_tags, key: :tag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attribute :moved_to, if: :moved?
 | 
					  attribute :moved_to, if: :moved?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class EndpointsSerializer < ActiveModel::Serializer
 | 
					  class EndpointsSerializer < ActiveModel::Serializer
 | 
				
			||||||
| 
						 | 
					@ -101,7 +103,14 @@ class ActivityPub::ActorSerializer < ActiveModel::Serializer
 | 
				
			||||||
    object.locked
 | 
					    object.locked
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def virtual_tags
 | 
				
			||||||
 | 
					    object.emojis
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def moved_to
 | 
					  def moved_to
 | 
				
			||||||
    ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
 | 
					    ActivityPub::TagManager.instance.uri_for(object.moved_to_account)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  class CustomEmojiSerializer < ActivityPub::EmojiSerializer
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def note
 | 
					  def note
 | 
				
			||||||
    Formatter.instance.simplified_format(object)
 | 
					    Formatter.instance.simplified_format(object, custom_emojify: true)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def url
 | 
					  def url
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ class ActivityPub::ProcessAccountService < BaseService
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        create_account if @account.nil?
 | 
					        create_account if @account.nil?
 | 
				
			||||||
        update_account
 | 
					        update_account
 | 
				
			||||||
 | 
					        process_tags(@account)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -187,4 +188,31 @@ class ActivityPub::ProcessAccountService < BaseService
 | 
				
			||||||
  def lock_options
 | 
					  def lock_options
 | 
				
			||||||
    { redis: Redis.current, key: "process_account:#{@uri}" }
 | 
					    { redis: Redis.current, key: "process_account:#{@uri}" }
 | 
				
			||||||
  end
 | 
					  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
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,27 @@ class UpdateRemoteProfileService < BaseService
 | 
				
			||||||
        account.header_remote_url = ''
 | 
					        account.header_remote_url = ''
 | 
				
			||||||
        account.header.destroy
 | 
					        account.header.destroy
 | 
				
			||||||
      end
 | 
					      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
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@
 | 
				
			||||||
            = t 'accounts.roles.moderator'
 | 
					            = t 'accounts.roles.moderator'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .bio
 | 
					    .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
 | 
					    .details-counters
 | 
				
			||||||
      .counter{ class: active_nav_class(short_account_url(account)) }
 | 
					      .counter{ class: active_nav_class(short_account_url(account)) }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -394,6 +394,45 @@ RSpec.describe Formatter do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      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'
 | 
					      include_examples 'encode and link URLs'
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -404,6 +443,46 @@ RSpec.describe Formatter do
 | 
				
			||||||
      it 'reformats' do
 | 
					      it 'reformats' do
 | 
				
			||||||
        is_expected.to_not include '<script>alert("Hello")</script>'
 | 
					        is_expected.to_not include '<script>alert("Hello")</script>'
 | 
				
			||||||
      end
 | 
					      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
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue