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(/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(/
Beep :coolcat: boop
' }
+
+ it 'converts shortcode to image tag' do
+ is_expected.to match(/Beep :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(/