Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement emoji import feature #5145

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
92 changes: 85 additions & 7 deletions app/controllers/admin/custom_emojis_controller.rb
Expand Up @@ -4,31 +4,109 @@ module Admin
class CustomEmojisController < BaseController
def index
@custom_emojis = CustomEmoji.local
@status = Status.new
end

def new
def import_form
@custom_emoji = CustomEmoji.new
render_import_form_with_status_url import_form_params.require(:url)
rescue ActionController::ParameterMissing
redirect_to action: :index, flash: { error: I18n.t('admin.custom_emojis.status_unspecified_msg') }
end

def create
@custom_emoji = CustomEmoji.new(resource_params)
def upload_form
@custom_emoji = CustomEmoji.new
@custom_emoji.build_custom_emoji_icon
end

def import
super_custom_emoji = CustomEmoji.find(import_params.require(:super_id))
rescue ActionController::ParameterMissing
@custom_emoji = CustomEmoji.new
flash.now[:error] = I18n.t('admin.custom_emojis.emoji_unspecified_msg')
render_import_form_with_status_id params.require(:status).require(:id)
rescue ActiveRecord::RecordNotFound
@custom_emoji = CustomEmoji.new
flash.now[:error] = I18n.t('admin.custom_emojis.emoji_not_found_msg')
render_import_form_with_status_id params.require(:status).require(:id)
else
@custom_emoji = CustomEmoji.new(
custom_emoji_icon: super_custom_emoji.custom_emoji_icon,
shortcode: import_params[:shortcode] || super_custom_emoji.shortcode
)

if @custom_emoji.save
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :new
render_import_form_with_status_id params.require(:status).require(:id)
end
end

def upload
@custom_emoji = CustomEmoji.new(
custom_emoji_icon: CustomEmojiIcon.new(image: upload_params.dig(:custom_emoji_icon, :image)),
shortcode: upload_params[:shortcode]
)

saved = ApplicationRecord.transaction do
@custom_emoji.custom_emoji_icon.save && @custom_emoji.save
end

if saved
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else
render :upload_form
end
end

def destroy
CustomEmoji.find(params[:id]).destroy
custom_emoji = CustomEmoji.local.find(params[:id])
custom_emoji.destroy!

ApplicationRecord.transaction do
icon = custom_emoji.custom_emoji_icon
icon.destroy! if icon.local?
end

redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end

private

def resource_params
params.require(:custom_emoji).permit(:shortcode, :image)
def render_import_form_with_status_url(url)
status = FetchRemoteResourceService.new.call(url)
if status&.respond_to? :emojis
render_import_form_with_status status
else
redirect_to action: :index, flash: { error: I18n.t('admin.custom_emojis.status_not_found_msg') }
end
end

def render_import_form_with_status_id(id)
render_import_form_with_status Status.find(id)
rescue ActiveRecord::RecordNotFound
redirect_to action: :index, flash: { error: I18n.t('admin.custom_emojis.status_not_found_msg') }
end

def render_import_form_with_status(status)
@status = status
@remote_custom_emojis = status.emojis.reject do |custom_emoji|
custom_emoji.custom_emoji_icon.custom_emojis.local.exists?
end

render :import_form
end

def import_form_params
params.require(:status).permit(:url)
end

def import_params
params.require(:custom_emoji).permit([:shortcode, :super_id])
end

def upload_params
params.require(:custom_emoji).permit([:shortcode, { custom_emoji_icon: [:image] }])
end
end
end
23 changes: 23 additions & 0 deletions app/controllers/emojis_controller.rb
@@ -0,0 +1,23 @@
# frozen_string_literal: true

class EmojisController < ApplicationController
before_action :set_emoji

def show
respond_to do |format|
format.json do
render json: @emoji, serializer: ActivityPub::CustomEmojiIconSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end

format.atom do
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.emoji_icon(@emoji, true))
end
end
end

private

def set_emoji
@emoji = CustomEmojiIcon.local.find(params.require(:id))
end
end
4 changes: 4 additions & 0 deletions app/lib/activitypub/activity.rb
Expand Up @@ -49,6 +49,10 @@ def klass

protected

def emoji_from_uri(uri)
ActivityPub::TagManager.instance.uri_to_resource(uri, CustomEmojiIcon)
end

def status_from_uri(uri)
ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
end
Expand Down
32 changes: 26 additions & 6 deletions app/lib/activitypub/activity/create.rb
Expand Up @@ -86,16 +86,36 @@ def process_mention(tag, status)
end

def process_emoji(tag, _status)
return if tag['name'].blank? || tag['href'].blank?
return if tag['name'].blank? || tag['icon'].blank?

uri = value_or_id(tag['icon'])
shortcode = tag['name'].delete(':')
emoji = CustomEmoji.find_by(shortcode: shortcode, domain: @account.domain)

return if !emoji.nil? || skip_download?
return if uri.blank?

emoji = CustomEmoji.new(domain: @account.domain, shortcode: shortcode)
emoji.image_remote_url = tag['href']
emoji.save
emoji = CustomEmoji.find_by(domain: @account.domain, shortcode: shortcode)

return unless emoji.nil?

icon = emoji_from_uri(uri)
if icon.nil?
parsed_uri = Addressable::URI.parse(uri)
domain_block = DomainBlock.find_by(domain: parsed_uri.normalized_host)

unless domain_block&.reject_media?
icon = ActivityPub::FetchRemoteCustomEmojiIconService.new.call(uri) if icon.nil?
end
end

if icon.present?
CustomEmoji.create!(
domain: @account.domain,
custom_emoji_icon: icon,
shortcode: shortcode
)
end
rescue Addressable::URI::InvalidURIError => e
Rails.logger.debug e
end

def process_attachments(status)
Expand Down
4 changes: 4 additions & 0 deletions app/lib/activitypub/tag_manager.rb
Expand Up @@ -28,6 +28,8 @@ def uri_for(target)
return target.uri if target.respond_to?(:local?) && !target.local?

case target.object_type
when :emoji
emoji_url(target)
when :person
account_url(target)
when :note, :comment, :activity
Expand Down Expand Up @@ -95,6 +97,8 @@ def uri_to_resource(uri, klass)
case klass.name
when 'Account'
klass.find_local(uri_to_local_id(uri, :username))
when 'CustomEmojiIcon'
klass.find_local(uri_to_local_id(uri))
else
StatusFinder.new(uri).status
end
Expand Down
2 changes: 1 addition & 1 deletion app/lib/formatter.rb
Expand Up @@ -92,7 +92,7 @@ def encode_and_link_urls(html, accounts = nil)
def encode_custom_emojis(html, emojis)
return html if emojis.empty?

emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url)] }.to_h
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.custom_emoji_icon.image.url)] }.to_h

i = -1
inside_tag = false
Expand Down
29 changes: 23 additions & 6 deletions app/lib/ostatus/activity/creation.rb
Expand Up @@ -160,10 +160,6 @@ def save_media(parent)
end

def save_emojis(parent)
do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media?

return if do_not_download

@xml.xpath('./xmlns:link[@rel="emoji"]', xmlns: OStatus::TagManager::XMLNS).each do |link|
next unless link['href'] && link['name']

Expand All @@ -172,8 +168,29 @@ def save_emojis(parent)

next unless emoji.nil?

emoji = CustomEmoji.new(shortcode: shortcode, domain: parent.account.domain)
emoji.image_remote_url = link['href']
icon = ActivityPub::TagManager.instance.uri_to_resource(link['href'], CustomEmojiIcon)
if icon.nil?
begin
parsed_href = Addressable::URI.parse(link['href'])
rescue Addressable::URI::InvalidURIError => e
Rails.logger.debug e
next
end

do_not_download = DomainBlock.find_by(domain: parsed_href.normalized_host)&.reject_media?

next if do_not_download

icon = FetchRemoteCustomEmojiIconService.new.call(link['href'])
end

next if icon.nil?

emoji = CustomEmoji.new(
custom_emoji_icon: icon,
shortcode: shortcode,
domain: parent.account.domain
)
emoji.save
end
end
Expand Down
18 changes: 17 additions & 1 deletion app/lib/ostatus/atom_serializer.rb
Expand Up @@ -34,6 +34,22 @@ def author(account)
author
end

def emoji_icon(icon, root = false)
image = Ox::Element.new('entry')

add_namespaces(image) if root
append_element(image, 'id', OStatus::TagManager.instance.uri_for(icon))
append_element(
image,
'link',
nil,
rel: :enclosure,
href: icon.image_remote_url || full_asset_url(icon.image.url(:original, false))
)

image
end

def feed(account, stream_entries)
feed = Ox::Element.new('feed')

Expand Down Expand Up @@ -370,7 +386,7 @@ def serialize_status_attributes(entry, status)
append_element(entry, 'mastodon:scope', status.visibility)

status.emojis.each do |emoji|
append_element(entry, 'link', nil, rel: :emoji, href: full_asset_url(emoji.image.url), name: emoji.shortcode)
append_element(entry, 'link', nil, rel: :emoji, href: OStatus::TagManager.instance.uri_for(emoji.custom_emoji_icon), name: emoji.shortcode)
end
end
end
2 changes: 2 additions & 0 deletions app/lib/ostatus/tag_manager.rb
Expand Up @@ -64,6 +64,8 @@ def uri_for(target)
return target.uri if target.respond_to?(:local?) && !target.local?

case target.object_type
when :emoji
emoji_url(target)
when :person
account_url(target)
when :note, :comment, :activity
Expand Down
3 changes: 2 additions & 1 deletion app/lib/tag_manager.rb
Expand Up @@ -29,7 +29,8 @@ def same_acct?(canonical, needle)
end

def local_url?(url)
uri = Addressable::URI.parse(url).normalize
uri = Addressable::URI.parse(url).normalize
return false if uri.host.nil?
domain = uri.host + (uri.port ? ":#{uri.port}" : '')
TagManager.instance.web_domain?(domain)
end
Expand Down
21 changes: 7 additions & 14 deletions app/models/custom_emoji.rb
Expand Up @@ -3,15 +3,12 @@
#
# Table name: custom_emojis
#
# id :integer not null, primary key
# shortcode :string default(""), not null
# domain :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# id :integer not null, primary key
# shortcode :string default(""), not null
# domain :string
# created_at :datetime not null
# updated_at :datetime not null
# custom_emoji_icon_id :integer not null
#

class CustomEmoji < ApplicationRecord
Expand All @@ -21,15 +18,11 @@ class CustomEmoji < ApplicationRecord
:(#{SHORTCODE_RE_FRAGMENT}):
(?=[^[:alnum:]:]|$)/x

has_attached_file :image

validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }
belongs_to :custom_emoji_icon, inverse_of: :custom_emojis
validates :shortcode, uniqueness: { scope: :domain }, format: { with: /\A#{SHORTCODE_RE_FRAGMENT}\z/ }, length: { minimum: 2 }

scope :local, -> { where(domain: nil) }

include Remotable

class << self
def from_text(text, domain)
return [] if text.blank?
Expand Down
39 changes: 39 additions & 0 deletions app/models/custom_emoji_icon.rb
@@ -0,0 +1,39 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: custom_emoji_icons
#
# id :integer not null, primary key
# uri :string
# image_remote_url :string
# image_file_name :string
# image_content_type :string
# image_file_size :integer
# image_updated_at :datetime
#

class CustomEmojiIcon < ApplicationRecord
has_many :custom_emojis, inverse_of: :custom_emoji_icon

has_attached_file :image
validates_attachment :image, content_type: { content_type: 'image/png' }, presence: true, size: { in: 0..50.kilobytes }

scope :local, -> { where(uri: nil) }
scope :remote, -> { where.not(uri: nil) }

include Remotable

def self.find_local(id)
local.find(id)
rescue ActiveRecord::RecordNotFound
nil
end

def local?
uri.nil?
end

def object_type
:emoji
end
end