discourse/app/models/emoji.rb
Joffrey JAFFEUX 9dd26a3954
DEV: discourse-emojis 1.0.32 (#31669)
This gems bring a whole improved fluentui emoji set. Each image has been
recomputed to optimise the size of the emoji and remove useless margins.

For this reason the version has been bumped.

Other changes:
- not loading rake task dependencies in production
- renamed yo_yo/yo-yo to yoyo
2025-03-06 10:35:04 +01:00

373 lines
9.3 KiB
Ruby

# frozen_string_literal: true
class Emoji
# update this to clear the cache
EMOJI_VERSION = "14"
FITZPATRICK_SCALE = %w[1f3fb 1f3fc 1f3fd 1f3fe 1f3ff]
DEFAULT_GROUP = "default"
include ActiveModel::SerializerSupport
attr_accessor :name, :url, :tonable, :group, :search_aliases, :created_by
def self.global_emoji_cache
@global_emoji_cache ||= DistributedCache.new("global_emoji_cache", namespace: false)
end
def self.site_emoji_cache
@site_emoji_cache ||= DistributedCache.new("site_emoji_cache")
end
def self.all
Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
end
def self.standard
Discourse.cache.fetch(cache_key("standard_emojis")) { load_standard }
end
def self.allowed
Discourse.cache.fetch(cache_key("allowed_emojis")) { load_allowed }
end
def self.denied
Discourse.cache.fetch(cache_key("denied_emojis")) { load_denied }
end
def self.aliases
aliases_db
end
def self.search_aliases
search_aliases_db
end
def self.translations
Discourse.cache.fetch(cache_key("translations_emojis")) { load_translations }
end
def self.custom
Discourse.cache.fetch(cache_key("custom_emojis")) { load_custom }
end
def self.tonable_emojis
tonable_emojis_db
end
def self.custom?(name)
name = name.delete_prefix(":").delete_suffix(":")
Emoji.custom.detect { |e| e.name == name }.present?
end
def self.exists?(name)
Emoji[name].present? || Emoji.aliases_values.include?(name)
end
def self.[](name)
name = name.delete_prefix(":").delete_suffix(":")
is_toned = name.match?(/\A.+:t[1-6]\z/)
normalized_name = name.gsub(/\A(.+):t[1-6]\z/, '\1')
found_emoji = nil
[[global_emoji_cache, :standard], [site_emoji_cache, :custom]].each do |cache, list_key|
found_emoji =
cache.defer_get_set(normalized_name) do
[
Emoji
.public_send(list_key)
.detect { |e| e.name == normalized_name && (!is_toned || (is_toned && e.tonable)) },
]
end[
0
]
break if found_emoji
end
found_emoji
end
def self.create_from_db_item(emoji)
name = emoji["name"]
return unless group = groups[name]
filename = emoji["filename"] || name
Emoji.new.tap do |e|
e.name = name
e.tonable = Emoji.tonable_emojis.include?(name)
e.url = Emoji.url_for(filename)
e.group = group
e.search_aliases = search_aliases[name] || []
end
end
def self.url_for(name)
name = name.delete_prefix(":").delete_suffix(":").gsub(/(.+):t([1-6])/, '\1/\2')
if SiteSetting.external_emoji_url.blank?
"#{Discourse.base_path}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}"
else
"#{SiteSetting.external_emoji_url}/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}"
end
end
def self.cache_key(name)
"#{name}#{cache_postfix}"
end
def self.cache_postfix
":v#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
end
def self.clear_cache
%w[custom standard translations allowed denied all].each do |key|
Discourse.cache.delete(cache_key("#{key}_emojis"))
end
global_emoji_cache.clear
site_emoji_cache.clear
end
def self.groups_file
@groups_file ||= DiscourseEmojis.paths[:groups]
end
def self.groups
@groups ||=
begin
groups = {}
File
.open(groups_file, "r:UTF-8") { |f| JSON.parse(f.read) }
.each { |group| group["icons"].each { |icon| groups[icon["name"]] = group["name"] } }
groups
end
end
def self.emojis_db_file
@emojis_db_file ||= DiscourseEmojis.paths[:emojis]
end
def self.emojis_db
@emojis_db ||= Emoji.parse_emoji_file(emojis_db_file)
end
def self.translations_db_file
@translations_db_file ||= DiscourseEmojis.paths[:translations]
end
def self.translations_db
@translations_db ||= Emoji.parse_emoji_file(translations_db_file)
end
def self.tonable_emojis_db_file
@tonable_emojis_db_file ||= DiscourseEmojis.paths[:tonable_emojis]
end
def self.tonable_emojis_db
@tonable_emojis_db ||= Emoji.parse_emoji_file(tonable_emojis_db_file)
end
def self.aliases_db_file
@aliases_db_file ||= DiscourseEmojis.paths[:aliases]
end
def self.aliases_db
@aliases_db ||= Emoji.parse_emoji_file(aliases_db_file)
end
def self.aliases_values
@aliases_values ||= Set.new(Emoji.aliases_db.values.flatten)
end
def self.search_aliases_db_file
@search_aliases_db_file ||= DiscourseEmojis.paths[:search_aliases]
end
def self.search_aliases_db
@search_aliases_db ||= Emoji.parse_emoji_file(search_aliases_db_file)
end
def self.load_standard
emojis_db.map { |e| Emoji.create_from_db_item(e) }.compact
end
def self.load_allowed
denied_emojis = denied
all_emojis = load_standard + load_custom
if denied_emojis.present?
all_emojis.reject { |e| denied_emojis.include?(e.name) }
else
all_emojis
end
end
def self.load_denied
if SiteSetting.emoji_deny_list.present?
denied_emoji = SiteSetting.emoji_deny_list.split("|")
if denied_emoji.size > 0
denied_emoji.concat(denied_emoji.flat_map { |e| Emoji.aliases[e] }.compact)
end
end
end
def self.load_custom
result = []
if !GlobalSetting.skip_db?
CustomEmoji
.includes(:upload)
.order(:name)
.each do |emoji|
result << Emoji.new.tap do |e|
e.name = emoji.name
e.url = emoji.upload&.url
e.group = emoji.group || DEFAULT_GROUP
e.created_by = User.where(id: emoji.user_id).pick(:username)
end
end
end
Plugin::CustomEmoji.emojis.each do |group, emojis|
emojis.each do |name, url|
result << Emoji.new.tap do |e|
e.name = name
url = (Discourse.base_path + url) if url[%r{\A/[^/]}]
e.url = url
e.group = group || DEFAULT_GROUP
end
end
end
result
end
def self.load_translations
translations_db
end
def self.base_directory
"public#{base_url}"
end
def self.base_url
db = RailsMultisite::ConnectionManagement.current_db
"#{Discourse.base_path}/uploads/#{db}/_emoji"
end
def self.replacement_code(code)
code.split("-").map!(&:hex).pack("U*")
end
def self.unicode_replacements
@unicode_replacements ||=
begin
replacements = {}
is_tonable_emojis = Emoji.tonable_emojis
fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) }
emojis_db.each do |e|
name = e["name"]
# special cased as we prefer to keep these as symbols
next if name == "registered"
next if name == "copyright"
next if name == "tm"
next if name == "left_right_arrow"
code = replacement_code(e["code"])
next unless code
replacements[code] = name
if is_tonable_emojis.include?(name)
fitzpatrick_scales.each_with_index do |scale, index|
toned_code = code.codepoints.insert(1, scale).pack("U*")
replacements[toned_code] = "#{name}:t#{index + 2}"
end
end
end
replacements["\u{2639}"] = "frowning"
replacements["\u{263B}"] = "slight_smile"
replacements["\u{2661}"] = "heart"
replacements["\u{2665}"] = "heart"
replacements
end
end
def self.unicode_unescape(string)
PrettyText.escape_emoji(string)
end
def self.gsub_emoji_to_unicode(str)
str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } if str
end
def self.lookup_unicode(name)
return "" if denied&.include?(name)
@reverse_map ||=
begin
map = {}
is_tonable_emojis = Emoji.tonable_emojis
emojis_db.each do |e|
next if e["name"] == "tm"
code = replacement_code(e["code"])
next unless code
map[e["name"]] = code
if is_tonable_emojis.include?(e["name"])
FITZPATRICK_SCALE.each_with_index do |scale, index|
toned_code = (code.codepoints.insert(1, scale.to_i(16))).pack("U*")
map["#{e["name"]}:t#{index + 2}"] = toned_code
end
end
end
Emoji.aliases.each do |key, alias_names|
next unless alias_code = map[key]
alias_names.each { |alias_name| map[alias_name] = alias_code }
end
map
end
@reverse_map[name]
end
def self.unicode_replacements_json
@unicode_replacements_json ||= unicode_replacements.to_json
end
def self.codes_to_img(str)
return if str.blank?
str =
str.gsub(/:([\w\-+]*(?::t\d)?):/) do |name|
code = $1
if code && Emoji.custom?(code)
emoji = Emoji[code]
"<img src=\"#{emoji.url}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
elsif code && Emoji.exists?(code)
"<img src=\"#{Emoji.url_for(code)}\" title=\"#{code}\" class=\"emoji\" alt=\"#{code}\" loading=\"lazy\" width=\"20\" height=\"20\">"
else
name
end
end
end
def self.sanitize_emoji_name(name)
name.gsub(/[^a-z0-9\+\-]+/i, "_").gsub(/_{2,}/, "_").downcase
end
def self.parse_emoji_file(file)
File.open(file, "r:UTF-8") { |f| JSON.parse(f.read) }
end
end