[WIP] Enable custom emoji on account pages and in the sidebar (#6124)
Federate custom emojis with accountspull/410/head
parent
f464f98fd3
commit
123a343d11
|
@ -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 New Issue