[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)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def simplified_format(account)
 | 
			
		||||
    return reformat(account.note).html_safe unless account.local? # rubocop:disable Rails/OutputSafety
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue