logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe git clone https://hacktivis.me/git/mastofe.git
commit: 123a343d116d3e83cbd04460cc7bd0b6f3d208c4
parent f464f98fd3b8ef33b3afa5acf09e829c046134de
Author: David Underwood <davefp@gmail.com>
Date:   Sun,  1 Apr 2018 17:55:42 -0400

[WIP] Enable custom emoji on account pages and in the sidebar (#6124)

Federate custom emojis with accounts

Diffstat:

Mapp/lib/formatter.rb11++++++++---
Mapp/lib/ostatus/atom_serializer.rb3+++
Mapp/models/account.rb4++++
Mapp/models/remote_profile.rb4++++
Mapp/serializers/activitypub/actor_serializer.rb9+++++++++
Mapp/serializers/rest/account_serializer.rb2+-
Mapp/services/activitypub/process_account_service.rb28++++++++++++++++++++++++++++
Mapp/services/update_remote_profile_service.rb21+++++++++++++++++++++
Mapp/views/accounts/_header.html.haml2+-
Mspec/lib/formatter_spec.rb79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 158 insertions(+), 5 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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