From 123a343d116d3e83cbd04460cc7bd0b6f3d208c4 Mon Sep 17 00:00:00 2001 From: David Underwood Date: Sun, 1 Apr 2018 17:55:42 -0400 Subject: [PATCH] [WIP] Enable custom emoji on account pages and in the sidebar (#6124) Federate custom emojis with accounts --- app/lib/formatter.rb | 11 ++- app/lib/ostatus/atom_serializer.rb | 3 + app/models/account.rb | 4 + app/models/remote_profile.rb | 4 + .../activitypub/actor_serializer.rb | 9 +++ app/serializers/rest/account_serializer.rb | 2 +- .../activitypub/process_account_service.rb | 28 +++++++ app/services/update_remote_profile_service.rb | 21 +++++ app/views/accounts/_header.html.haml | 2 +- spec/lib/formatter_spec.rb | 79 +++++++++++++++++++ 10 files changed, 158 insertions(+), 5 deletions(-) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 1df4ff8d47a..f7e7a3c2381 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -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) diff --git a/app/lib/ostatus/atom_serializer.rb b/app/lib/ostatus/atom_serializer.rb index 46d0a8b37cd..055b4649c41 100644 --- a/app/lib/ostatus/atom_serializer.rb +++ b/app/lib/ostatus/atom_serializer.rb @@ -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? diff --git a/app/models/account.rb b/app/models/account.rb index 25e7d7436f6..a34b6a2d3cc 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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? diff --git a/app/models/remote_profile.rb b/app/models/remote_profile.rb index 613911c572c..742d2b56f4c 100644 --- a/app/models/remote_profile.rb +++ b/app/models/remote_profile.rb @@ -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 diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index afcd3777119..df309072643 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -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 diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 19b746520f2..6097acda5bf 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -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 diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 7d8dc136912..cf846282147 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -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 diff --git a/app/services/update_remote_profile_service.rb b/app/services/update_remote_profile_service.rb index 49a907682df..aca1185de2c 100644 --- a/app/services/update_remote_profile_service.rb +++ b/app/services/update_remote_profile_service.rb @@ -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 diff --git a/app/views/accounts/_header.html.haml b/app/views/accounts/_header.html.haml index b3c91b8696b..b78998e9e4b 100644 --- a/app/views/accounts/_header.html.haml +++ b/app/views/accounts/_header.html.haml @@ -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)) } diff --git a/spec/lib/formatter_spec.rb b/spec/lib/formatter_spec.rb index 67fbfe92de3..6e849f3794d 100644 --- a/spec/lib/formatter_spec.rb +++ b/spec/lib/formatter_spec.rb @@ -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(/

:coolcat:alert("Hello")' 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) { '

:coolcat: Beep boop
' } + + it 'converts shortcode to image tag' do + is_expected.to match(/

:coolcat:Beep :coolcat: boop

' } + + it 'converts shortcode to image tag' do + is_expected.to match(/Beep :coolcat::coolcat::coolcat:

' } + + it 'does not touch the shortcodes' do + is_expected.to match(/

:coolcat::coolcat:<\/p>/) + 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(/