9a23bd81b3548ff35bc942118ea1b0bd107c23f6
[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 require Logger
18
19 @type pattern :: Regex.t() | module() | String.t()
20 @type patterns :: pattern() | [pattern()]
21 @type group_patterns :: keyword(patterns())
22
23 @ets __MODULE__.Ets
24 @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
25 @groups Application.get_env(:pleroma, :emoji)[:groups]
26
27 @doc false
28 def start_link do
29 GenServer.start_link(__MODULE__, [], name: __MODULE__)
30 end
31
32 @doc "Reloads the emojis from disk."
33 @spec reload() :: :ok
34 def reload do
35 GenServer.call(__MODULE__, :reload)
36 end
37
38 @doc "Returns the path of the emoji `name`."
39 @spec get(String.t()) :: String.t() | nil
40 def get(name) do
41 case :ets.lookup(@ets, name) do
42 [{_, path}] -> path
43 _ -> nil
44 end
45 end
46
47 @doc "Returns all the emojos!!"
48 @spec get_all() :: [{String.t(), String.t()}, ...]
49 def get_all do
50 :ets.tab2list(@ets)
51 end
52
53 @doc false
54 def init(_) do
55 @ets = :ets.new(@ets, @ets_options)
56 GenServer.cast(self(), :reload)
57 {:ok, nil}
58 end
59
60 @doc false
61 def handle_cast(:reload, state) do
62 load()
63 {:noreply, state}
64 end
65
66 @doc false
67 def handle_call(:reload, _from, state) do
68 load()
69 {:reply, :ok, state}
70 end
71
72 @doc false
73 def terminate(_, _) do
74 :ok
75 end
76
77 @doc false
78 def code_change(_old_vsn, state, _extra) do
79 load()
80 {:ok, state}
81 end
82
83 defp load do
84 static_path = Path.join(:code.priv_dir(:pleroma), "static")
85
86 emoji_dir_path =
87 Path.join([
88 static_path,
89 Pleroma.Config.get!([:instance, :static_dir]),
90 "emoji"
91 ])
92
93 case File.ls(emoji_dir_path) do
94 {:error, :enoent} ->
95 # The custom emoji directory doesn't exist,
96 # don't do anything
97 nil
98
99 {:error, e} ->
100 # There was some other error
101 Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
102
103 {:ok, packs} ->
104 # Print the packs we've found
105 Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
106
107 # compat thing for old custom emoji handling
108 shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
109
110 emojis =
111 (Enum.flat_map(
112 packs,
113 fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
114 ) ++
115 load_from_file("config/emoji.txt") ++
116 load_from_file("config/custom_emoji.txt") ++
117 load_from_globs(shortcode_globs))
118 |> Enum.reject(fn value -> value == nil end)
119
120 true = :ets.insert(@ets, emojis)
121 end
122
123 :ok
124 end
125
126 defp load_pack(pack_dir) do
127 pack_name = Path.basename(pack_dir)
128
129 emoji_txt = Path.join(pack_dir, "emoji.txt")
130
131 if File.exists?(emoji_txt) do
132 load_from_file(emoji_txt)
133 else
134 Logger.info(
135 "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
136 )
137
138 common_pack_path =
139 Path.join([
140 "/",
141 Pleroma.Config.get!([:instance, :static_dir]),
142 "emoji",
143 pack_name
144 ])
145
146 make_shortcode_to_file_map(pack_dir, [".png"])
147 |> Enum.map(fn {shortcode, rel_file} ->
148 filename = Path.join(common_pack_path, rel_file)
149
150 # If no tag matches, use the pack name as a tag
151 {shortcode, filename, to_string(match_extra(@groups, filename))}
152 end)
153 end
154 end
155
156 def make_shortcode_to_file_map(pack_dir, exts) do
157 find_all_emoji(pack_dir, exts)
158 |> Enum.map(&Path.relative_to(&1, pack_dir))
159 |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
160 |> Enum.into(%{})
161 end
162
163 def find_all_emoji(dir, exts) do
164 Enum.reduce(
165 File.ls!(dir),
166 [],
167 fn f, acc ->
168 filepath = Path.join(dir, f)
169
170 if File.dir?(filepath) do
171 acc ++ find_all_emoji(filepath, exts)
172 else
173 acc ++ [filepath]
174 end
175 end
176 )
177 |> Enum.filter(fn f -> Path.extname(f) in exts end)
178 end
179
180 defp load_from_file(file) do
181 if File.exists?(file) do
182 load_from_file_stream(File.stream!(file))
183 else
184 []
185 end
186 end
187
188 defp load_from_file_stream(stream) do
189 stream
190 |> Stream.map(&String.trim/1)
191 |> Stream.map(fn line ->
192 case String.split(line, ~r/,\s*/) do
193 [name, file, tags] ->
194 {name, file, tags}
195
196 [name, file] ->
197 {name, file, to_string(match_extra(@groups, file))}
198
199 _ ->
200 nil
201 end
202 end)
203 |> Enum.to_list()
204 end
205
206 defp load_from_globs(globs) do
207 static_path = Path.join(:code.priv_dir(:pleroma), "static")
208
209 paths =
210 Enum.map(globs, fn glob ->
211 Path.join(static_path, glob)
212 |> Path.wildcard()
213 end)
214 |> Enum.concat()
215
216 Enum.map(paths, fn path ->
217 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
218 shortcode = Path.basename(path, Path.extname(path))
219 external_path = Path.join("/", Path.relative_to(path, static_path))
220 {shortcode, external_path, to_string(tag)}
221 end)
222 end
223
224 @doc """
225 Finds a matching group for the given emoji filename
226 """
227 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
228 def match_extra(group_patterns, filename) do
229 match_group_patterns(group_patterns, fn pattern ->
230 case pattern do
231 %Regex{} = regex -> Regex.match?(regex, filename)
232 string when is_binary(string) -> filename == string
233 end
234 end)
235 end
236
237 defp match_group_patterns(group_patterns, matcher) do
238 Enum.find_value(group_patterns, fn {group, patterns} ->
239 patterns =
240 patterns
241 |> List.wrap()
242 |> Enum.map(fn pattern ->
243 if String.contains?(pattern, "*") do
244 ~r(#{String.replace(pattern, "*", ".*")})
245 else
246 pattern
247 end
248 end)
249
250 Enum.any?(patterns, matcher) && group
251 end)
252 end
253 end