Merge pull request 'Remove reference to pleroma-fox-tan-shy.png in COPYING' (#298...
[akkoma] / lib / pleroma / emoji.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Emoji do
6 @moduledoc """
7 This GenServer stores in an ETS table the list of the loaded emojis,
8 and also allows to reload the list at runtime.
9 """
10 use GenServer
11
12 alias Pleroma.Emoji.Combinations
13 alias Pleroma.Emoji.Loader
14
15 require Logger
16
17 @ets __MODULE__.Ets
18 @ets_options [
19 :ordered_set,
20 :protected,
21 :named_table,
22 {:read_concurrency, true}
23 ]
24
25 defstruct [:code, :file, :tags, :safe_code, :safe_file]
26
27 @doc "Build emoji struct"
28 def build({code, file, tags}) do
29 %__MODULE__{
30 code: code,
31 file: file,
32 tags: tags,
33 safe_code: Pleroma.HTML.strip_tags(code),
34 safe_file: Pleroma.HTML.strip_tags(file)
35 }
36 end
37
38 def build({code, file}), do: build({code, file, []})
39
40 @doc false
41 def start_link(_) do
42 GenServer.start_link(__MODULE__, [], name: __MODULE__)
43 end
44
45 @doc "Reloads the emojis from disk."
46 @spec reload() :: :ok
47 def reload do
48 GenServer.call(__MODULE__, :reload)
49 end
50
51 @doc "Returns the path of the emoji `name`."
52 @spec get(String.t()) :: String.t() | nil
53 def get(name) do
54 name =
55 if String.starts_with?(name, ":") do
56 name
57 |> String.replace_leading(":", "")
58 |> String.replace_trailing(":", "")
59 else
60 name
61 end
62
63 case :ets.lookup(@ets, name) do
64 [{_, path}] -> path
65 _ -> nil
66 end
67 end
68
69 @spec exist?(String.t()) :: boolean()
70 def exist?(name), do: not is_nil(get(name))
71
72 @doc "Returns all the emojos!!"
73 @spec get_all() :: list({String.t(), String.t(), String.t()})
74 def get_all do
75 :ets.tab2list(@ets)
76 end
77
78 @doc "Clear out old emojis"
79 def clear_all, do: :ets.delete_all_objects(@ets)
80
81 @doc false
82 def init(_) do
83 @ets = :ets.new(@ets, @ets_options)
84 GenServer.cast(self(), :reload)
85 {:ok, nil}
86 end
87
88 @doc false
89 def handle_cast(:reload, state) do
90 update_emojis(Loader.load())
91 {:noreply, state}
92 end
93
94 @doc false
95 def handle_call(:reload, _from, state) do
96 update_emojis(Loader.load())
97 {:reply, :ok, state}
98 end
99
100 @doc false
101 def terminate(_, _) do
102 :ok
103 end
104
105 @doc false
106 def code_change(_old_vsn, state, _extra) do
107 update_emojis(Loader.load())
108 {:ok, state}
109 end
110
111 defp update_emojis(emojis) do
112 :ets.insert(@ets, emojis)
113 end
114
115 @external_resource "lib/pleroma/emoji-test.txt"
116
117 regional_indicators =
118 Enum.map(127_462..127_487, fn codepoint ->
119 <<codepoint::utf8>>
120 end)
121
122 emojis =
123 @external_resource
124 |> File.read!()
125 |> String.split("\n")
126 |> Enum.filter(fn line ->
127 line != "" and not String.starts_with?(line, "#") and
128 String.contains?(line, "fully-qualified")
129 end)
130 |> Enum.map(fn line ->
131 line
132 |> String.split(";", parts: 2)
133 |> hd()
134 |> String.trim()
135 |> String.split()
136 |> Enum.map(fn codepoint ->
137 <<String.to_integer(codepoint, 16)::utf8>>
138 end)
139 |> Enum.join()
140 end)
141 |> Enum.uniq()
142
143 emojis = emojis ++ regional_indicators
144
145 for emoji <- emojis do
146 def is_unicode_emoji?(unquote(emoji)), do: true
147 end
148
149 def is_unicode_emoji?(_), do: false
150
151 def stripped_name(name) when is_binary(name) do
152 name
153 |> String.replace_leading(":", "")
154 |> String.replace_trailing(":", "")
155 end
156
157 def stripped_name(name), do: name
158
159 def maybe_quote(name) when is_binary(name) do
160 if is_unicode_emoji?(name) do
161 name
162 else
163 if String.starts_with?(name, ":") do
164 name
165 else
166 ":#{name}:"
167 end
168 end
169 end
170
171 def maybe_quote(name), do: name
172
173 def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
174
175 def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
176 tag =
177 tags
178 |> Enum.find(fn tag -> tag["type"] == "Emoji" && tag["name"] == stripped_name(emoji) end)
179
180 if is_nil(tag) do
181 nil
182 else
183 tag
184 |> Map.get("icon")
185 |> Map.get("url")
186 end
187 end
188
189 def emoji_url(_), do: nil
190
191 def emoji_name_with_instance(name, url) do
192 url = url |> URI.parse() |> Map.get(:host)
193 "#{name}@#{url}"
194 end
195
196 emoji_qualification_map =
197 emojis
198 |> Enum.filter(&String.contains?(&1, "\uFE0F"))
199 |> Combinations.variate_emoji_qualification()
200
201 for {qualified, unqualified_list} <- emoji_qualification_map do
202 for unqualified <- unqualified_list do
203 def fully_qualify_emoji(unquote(unqualified)), do: unquote(qualified)
204 end
205 end
206
207 def fully_qualify_emoji(emoji), do: emoji
208 end