b60d19e8949dfa044c99feb0d5c9453872f60fb7
[akkoma] / lib / pleroma / emoji.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Emoji do
6 @moduledoc """
7 The emojis are loaded from:
8
9 * the built-in Finmojis (if enabled in configuration),
10 * the files: `config/emoji.txt` and `config/custom_emoji.txt`
11 * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
12
13 This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
14 """
15 use GenServer
16
17 @type pattern :: Regex.t() | module() | String.t()
18 @type patterns :: pattern | [pattern]
19 @type group_patterns :: keyword(patterns)
20
21 @ets __MODULE__.Ets
22 @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
23 @groups Application.get_env(:pleroma, :emoji)[:groups]
24
25 @doc false
26 def start_link do
27 GenServer.start_link(__MODULE__, [], name: __MODULE__)
28 end
29
30 @doc "Reloads the emojis from disk."
31 @spec reload() :: :ok
32 def reload do
33 GenServer.call(__MODULE__, :reload)
34 end
35
36 @doc "Returns the path of the emoji `name`."
37 @spec get(String.t()) :: String.t() | nil
38 def get(name) do
39 case :ets.lookup(@ets, name) do
40 [{_, path}] -> path
41 _ -> nil
42 end
43 end
44
45 @doc "Returns all the emojos!!"
46 @spec get_all() :: [{String.t(), String.t()}, ...]
47 def get_all do
48 :ets.tab2list(@ets)
49 end
50
51 @doc false
52 def init(_) do
53 @ets = :ets.new(@ets, @ets_options)
54 GenServer.cast(self(), :reload)
55 {:ok, nil}
56 end
57
58 @doc false
59 def handle_cast(:reload, state) do
60 load()
61 {:noreply, state}
62 end
63
64 @doc false
65 def handle_call(:reload, _from, state) do
66 load()
67 {:reply, :ok, state}
68 end
69
70 @doc false
71 def terminate(_, _) do
72 :ok
73 end
74
75 @doc false
76 def code_change(_old_vsn, state, _extra) do
77 load()
78 {:ok, state}
79 end
80
81 defp load do
82 finmoji_enabled = Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)
83 shortcode_globs = Keyword.get(Application.get_env(:pleroma, :emoji, []), :shortcode_globs, [])
84
85 emojis =
86 (load_finmoji(finmoji_enabled) ++
87 load_from_file("config/emoji.txt") ++
88 load_from_file("config/custom_emoji.txt") ++
89 load_from_globs(shortcode_globs))
90 |> Enum.reject(fn value -> value == nil end)
91
92 true = :ets.insert(@ets, emojis)
93 :ok
94 end
95
96 @finmoji [
97 "a_trusted_friend",
98 "alandislands",
99 "association",
100 "auroraborealis",
101 "baby_in_a_box",
102 "bear",
103 "black_gold",
104 "christmasparty",
105 "crosscountryskiing",
106 "cupofcoffee",
107 "education",
108 "fashionista_finns",
109 "finnishlove",
110 "flag",
111 "forest",
112 "four_seasons_of_bbq",
113 "girlpower",
114 "handshake",
115 "happiness",
116 "headbanger",
117 "icebreaker",
118 "iceman",
119 "joulutorttu",
120 "kaamos",
121 "kalsarikannit_f",
122 "kalsarikannit_m",
123 "karjalanpiirakka",
124 "kicksled",
125 "kokko",
126 "lavatanssit",
127 "losthopes_f",
128 "losthopes_m",
129 "mattinykanen",
130 "meanwhileinfinland",
131 "moominmamma",
132 "nordicfamily",
133 "out_of_office",
134 "peacemaker",
135 "perkele",
136 "pesapallo",
137 "polarbear",
138 "pusa_hispida_saimensis",
139 "reindeer",
140 "sami",
141 "sauna_f",
142 "sauna_m",
143 "sauna_whisk",
144 "sisu",
145 "stuck",
146 "suomimainittu",
147 "superfood",
148 "swan",
149 "the_cap",
150 "the_conductor",
151 "the_king",
152 "the_voice",
153 "theoriginalsanta",
154 "tomoffinland",
155 "torillatavataan",
156 "unbreakable",
157 "waiting",
158 "white_nights",
159 "woollysocks"
160 ]
161
162 defp load_finmoji(true) do
163 Enum.map(@finmoji, fn finmoji ->
164 file_name = "/finmoji/128px/#{finmoji}-128.png"
165 group = match_extra(@groups, file_name)
166 {finmoji, file_name, to_string(group)}
167 end)
168 end
169
170 defp load_finmoji(_), do: []
171
172 defp load_from_file(file) do
173 if File.exists?(file) do
174 load_from_file_stream(File.stream!(file))
175 else
176 []
177 end
178 end
179
180 defp load_from_file_stream(stream) do
181 stream
182 |> Stream.map(&String.trim/1)
183 |> Stream.map(fn line ->
184 case String.split(line, ~r/,\s*/) do
185 [name, file, tags] ->
186 {name, file, tags}
187
188 [name, file] ->
189 {name, file, to_string(match_extra(@groups, file))}
190
191 _ ->
192 nil
193 end
194 end)
195 |> Enum.to_list()
196 end
197
198 defp load_from_globs(globs) do
199 static_path = Path.join(:code.priv_dir(:pleroma), "static")
200
201 paths =
202 Enum.map(globs, fn glob ->
203 Path.join(static_path, glob)
204 |> Path.wildcard()
205 end)
206 |> Enum.concat()
207
208 Enum.map(paths, fn path ->
209 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
210 shortcode = Path.basename(path, Path.extname(path))
211 external_path = Path.join("/", Path.relative_to(path, static_path))
212 {shortcode, external_path, to_string(tag)}
213 end)
214 end
215
216 @doc """
217 Finds a matching group for the given extra filename
218 """
219 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
220 def match_extra(group_patterns, filename) do
221 match_group_patterns(group_patterns, fn pattern ->
222 case pattern do
223 %Regex{} = regex -> Regex.match?(regex, filename)
224 string when is_binary(string) -> filename == string
225 end
226 end)
227 end
228
229 defp match_group_patterns(group_patterns, matcher) do
230 Enum.find_value(group_patterns, fn {group, patterns} ->
231 patterns =
232 patterns
233 |> List.wrap()
234 |> Enum.map(fn pattern ->
235 if String.contains?(pattern, "*") do
236 ~r(#{String.replace(pattern, "*", ".*")})
237 else
238 pattern
239 end
240 end)
241
242 Enum.any?(patterns, matcher) && group
243 end)
244 end
245 end