Class: Emoji

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::SerializerSupport
Defined in:
app/models/emoji.rb

Constant Summary collapse

EMOJI_VERSION =

update this to clear the cache

"12"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#groupObject

Returns the value of attribute group.



13
14
15
# File 'app/models/emoji.rb', line 13

def group
  @group
end

#nameObject

Returns the value of attribute name.



13
14
15
# File 'app/models/emoji.rb', line 13

def name
  @name
end

#search_aliasesObject

Returns the value of attribute search_aliases.



13
14
15
# File 'app/models/emoji.rb', line 13

def search_aliases
  @search_aliases
end

#tonableObject

Returns the value of attribute tonable.



13
14
15
# File 'app/models/emoji.rb', line 13

def tonable
  @tonable
end

#urlObject

Returns the value of attribute url.



13
14
15
# File 'app/models/emoji.rb', line 13

def url
  @url
end

Class Method Details

.[](name) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/emoji.rb', line 68

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

.aliasesObject



39
40
41
# File 'app/models/emoji.rb', line 39

def self.aliases
  db["aliases"]
end

.allObject



23
24
25
# File 'app/models/emoji.rb', line 23

def self.all
  Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom }
end

.allowedObject



31
32
33
# File 'app/models/emoji.rb', line 31

def self.allowed
  Discourse.cache.fetch(cache_key("allowed_emojis")) { load_allowed }
end

.base_directoryObject



215
216
217
# File 'app/models/emoji.rb', line 215

def self.base_directory
  "public#{base_url}"
end

.base_urlObject



219
220
221
222
# File 'app/models/emoji.rb', line 219

def self.base_url
  db = RailsMultisite::ConnectionManagement.current_db
  "#{Discourse.base_path}/uploads/#{db}/_emoji"
end

.cache_key(name) ⇒ Object



116
117
118
# File 'app/models/emoji.rb', line 116

def self.cache_key(name)
  "#{name}#{cache_postfix}"
end

.cache_postfixObject



120
121
122
# File 'app/models/emoji.rb', line 120

def self.cache_postfix
  ":v#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}"
end

.clear_cacheObject



124
125
126
127
128
129
130
# File 'app/models/emoji.rb', line 124

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

.codes_to_img(str) ⇒ Object



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/emoji.rb', line 310

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

.create_from_db_item(emoji) ⇒ Object



93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'app/models/emoji.rb', line 93

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

.customObject



51
52
53
# File 'app/models/emoji.rb', line 51

def self.custom
  Discourse.cache.fetch(cache_key("custom_emojis")) { load_custom }
end

.custom?(name) ⇒ Boolean

Returns:

  • (Boolean)


59
60
61
62
# File 'app/models/emoji.rb', line 59

def self.custom?(name)
  name = name.delete_prefix(":").delete_suffix(":")
  Emoji.custom.detect { |e| e.name == name }.present?
end

.dbObject



153
154
155
# File 'app/models/emoji.rb', line 153

def self.db
  @db ||= File.open(db_file, "r:UTF-8") { |f| JSON.parse(f.read) }
end

.db_fileObject



149
150
151
# File 'app/models/emoji.rb', line 149

def self.db_file
  @db_file ||= "#{Rails.root}/lib/emoji/db.json"
end

.deniedObject



35
36
37
# File 'app/models/emoji.rb', line 35

def self.denied
  Discourse.cache.fetch(cache_key("denied_emojis")) { load_denied }
end

.exists?(name) ⇒ Boolean

Returns:

  • (Boolean)


64
65
66
# File 'app/models/emoji.rb', line 64

def self.exists?(name)
  Emoji[name].present?
end

.global_emoji_cacheObject



15
16
17
# File 'app/models/emoji.rb', line 15

def self.global_emoji_cache
  @global_emoji_cache ||= DistributedCache.new("global_emoji_cache", namespace: false)
end

.groupsObject



136
137
138
139
140
141
142
143
144
145
146
147
# File 'app/models/emoji.rb', line 136

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

.groups_fileObject



132
133
134
# File 'app/models/emoji.rb', line 132

def self.groups_file
  @groups_file ||= "#{Rails.root}/lib/emoji/groups.json"
end

.gsub_emoji_to_unicode(str) ⇒ Object



269
270
271
# File 'app/models/emoji.rb', line 269

def self.gsub_emoji_to_unicode(str)
  str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } if str
end

.load_allowedObject



161
162
163
164
165
166
167
168
169
170
# File 'app/models/emoji.rb', line 161

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

.load_customObject



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# File 'app/models/emoji.rb', line 181

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
        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

.load_deniedObject



172
173
174
175
176
177
178
179
# File 'app/models/emoji.rb', line 172

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

.load_standardObject



157
158
159
# File 'app/models/emoji.rb', line 157

def self.load_standard
  db["emojis"].map { |e| Emoji.create_from_db_item(e) }.compact
end

.load_translationsObject



211
212
213
# File 'app/models/emoji.rb', line 211

def self.load_translations
  db["translations"]
end

.lookup_unicode(name) ⇒ Object



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/models/emoji.rb', line 273

def self.lookup_unicode(name)
  return "" if denied&.include?(name)

  @reverse_map ||=
    begin
      map = {}
      is_tonable_emojis = Emoji.tonable_emojis

      db["emojis"].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

.replacement_code(code) ⇒ Object



224
225
226
# File 'app/models/emoji.rb', line 224

def self.replacement_code(code)
  code.split("-").map!(&:hex).pack("U*")
end

.search_aliasesObject



43
44
45
# File 'app/models/emoji.rb', line 43

def self.search_aliases
  db["searchAliases"]
end

.site_emoji_cacheObject



19
20
21
# File 'app/models/emoji.rb', line 19

def self.site_emoji_cache
  @site_emoji_cache ||= DistributedCache.new("site_emoji_cache")
end

.standardObject



27
28
29
# File 'app/models/emoji.rb', line 27

def self.standard
  Discourse.cache.fetch(cache_key("standard_emojis")) { load_standard }
end

.tonable_emojisObject



55
56
57
# File 'app/models/emoji.rb', line 55

def self.tonable_emojis
  db["tonableEmojis"]
end

.translationsObject



47
48
49
# File 'app/models/emoji.rb', line 47

def self.translations
  Discourse.cache.fetch(cache_key("translations_emojis")) { load_translations }
end

.unicode_replacementsObject



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'app/models/emoji.rb', line 228

def self.unicode_replacements
  @unicode_replacements ||=
    begin
      replacements = {}
      is_tonable_emojis = Emoji.tonable_emojis
      fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) }

      db["emojis"].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

.unicode_replacements_jsonObject



306
307
308
# File 'app/models/emoji.rb', line 306

def self.unicode_replacements_json
  @unicode_replacements_json ||= unicode_replacements.to_json
end

.unicode_unescape(string) ⇒ Object



265
266
267
# File 'app/models/emoji.rb', line 265

def self.unicode_unescape(string)
  PrettyText.escape_emoji(string)
end

.url_for(name) ⇒ Object



107
108
109
110
111
112
113
114
# File 'app/models/emoji.rb', line 107

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