15455b77984144a751d8111be8aedf4ce7fae8df
[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 {shortcode, filename, [to_string(match_extra(@groups, filename))]}
151 end)
152 end
153 end
154
155 def make_shortcode_to_file_map(pack_dir, exts) do
156 find_all_emoji(pack_dir, exts)
157 |> Enum.map(&Path.relative_to(&1, pack_dir))
158 |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
159 |> Enum.into(%{})
160 end
161
162 def find_all_emoji(dir, exts) do
163 Enum.reduce(
164 File.ls!(dir),
165 [],
166 fn f, acc ->
167 filepath = Path.join(dir, f)
168
169 if File.dir?(filepath) do
170 acc ++ find_all_emoji(filepath, exts)
171 else
172 acc ++ [filepath]
173 end
174 end
175 )
176 |> Enum.filter(fn f -> Path.extname(f) in exts end)
177 end
178
179 defp load_from_file(file) do
180 if File.exists?(file) do
181 load_from_file_stream(File.stream!(file))
182 else
183 []
184 end
185 end
186
187 defp load_from_file_stream(stream) do
188 stream
189 |> Stream.map(&String.trim/1)
190 |> Stream.map(fn line ->
191 case String.split(line, ~r/,\s*/) do
192 [name, file] ->
193 {name, file, [to_string(match_extra(@groups, file))]}
194
195 [name, file | tags] ->
196 {name, file, tags}
197
198 _ ->
199 nil
200 end
201 end)
202 |> Enum.to_list()
203 end
204
205 defp load_from_globs(globs) do
206 static_path = Path.join(:code.priv_dir(:pleroma), "static")
207
208 paths =
209 Enum.map(globs, fn glob ->
210 Path.join(static_path, glob)
211 |> Path.wildcard()
212 end)
213 |> Enum.concat()
214
215 Enum.map(paths, fn path ->
216 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
217 shortcode = Path.basename(path, Path.extname(path))
218 external_path = Path.join("/", Path.relative_to(path, static_path))
219 {shortcode, external_path, [to_string(tag)]}
220 end)
221 end
222
223 @doc """
224 Finds a matching group for the given emoji filename
225 """
226 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
227 def match_extra(group_patterns, filename) do
228 match_group_patterns(group_patterns, fn pattern ->
229 case pattern do
230 %Regex{} = regex -> Regex.match?(regex, filename)
231 string when is_binary(string) -> filename == string
232 end
233 end)
234 end
235
236 defp match_group_patterns(group_patterns, matcher) do
237 Enum.find_value(group_patterns, fn {group, patterns} ->
238 patterns =
239 patterns
240 |> List.wrap()
241 |> Enum.map(fn pattern ->
242 if String.contains?(pattern, "*") do
243 ~r(#{String.replace(pattern, "*", ".*")})
244 else
245 pattern
246 end
247 end)
248
249 Enum.any?(patterns, matcher) && group
250 end)
251 end
252 end