16909443843c9a37932f6a7ecf700a278faabd24
[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 emoji_dir_path =
85 Path.join(
86 Pleroma.Config.get!([:instance, :static_dir]),
87 "emoji"
88 )
89
90 case File.ls(emoji_dir_path) do
91 {:error, :enoent} ->
92 # The custom emoji directory doesn't exist,
93 # don't do anything
94 nil
95
96 {:error, e} ->
97 # There was some other error
98 Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
99
100 {:ok, packs} ->
101 # Print the packs we've found
102 Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
103
104 # compat thing for old custom emoji handling
105 shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
106
107 emojis =
108 (Enum.flat_map(
109 packs,
110 fn pack -> load_pack(Path.join(emoji_dir_path, pack)) end
111 ) ++
112 load_from_file("config/emoji.txt") ++
113 load_from_file("config/custom_emoji.txt") ++
114 load_from_globs(shortcode_globs))
115 |> Enum.reject(fn value -> value == nil end)
116
117 true = :ets.insert(@ets, emojis)
118 end
119
120 :ok
121 end
122
123 defp load_pack(pack_dir) do
124 pack_name = Path.basename(pack_dir)
125
126 emoji_txt = Path.join(pack_dir, "emoji.txt")
127
128 if File.exists?(emoji_txt) do
129 load_from_file(emoji_txt)
130 else
131 Logger.info(
132 "No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
133 )
134
135 make_shortcode_to_file_map(pack_dir, [".png"]) |>
136 Enum.map(fn {shortcode, rel_file} ->
137 filename = Path.join("/emoji/#{pack_name}", rel_file)
138
139 {shortcode, filename, [to_string(match_extra(@groups, filename))]}
140 end)
141 end
142 end
143
144 def make_shortcode_to_file_map(pack_dir, exts) do
145 find_all_emoji(pack_dir, exts)
146 |> Enum.map(&Path.relative_to(&1, pack_dir))
147 |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
148 |> Enum.into(%{})
149 end
150
151 def find_all_emoji(dir, exts) do
152 Enum.reduce(
153 File.ls!(dir),
154 [],
155 fn f, acc ->
156 filepath = Path.join(dir, f)
157
158 if File.dir?(filepath) do
159 acc ++ find_all_emoji(filepath, exts)
160 else
161 acc ++ [filepath]
162 end
163 end
164 )
165 |> Enum.filter(fn f -> Path.extname(f) in exts end)
166 end
167
168 defp load_from_file(file) do
169 if File.exists?(file) do
170 load_from_file_stream(File.stream!(file))
171 else
172 []
173 end
174 end
175
176 defp load_from_file_stream(stream) do
177 stream
178 |> Stream.map(&String.trim/1)
179 |> Stream.map(fn line ->
180 case String.split(line, ~r/,\s*/) do
181 [name, file] ->
182 {name, file, [to_string(match_extra(@groups, file))]}
183
184 [name, file | tags] ->
185 {name, file, tags}
186
187 _ ->
188 nil
189 end
190 end)
191 |> Enum.to_list()
192 end
193
194 defp load_from_globs(globs) do
195 static_path = Path.join(:code.priv_dir(:pleroma), "static")
196
197 paths =
198 Enum.map(globs, fn glob ->
199 Path.join(static_path, glob)
200 |> Path.wildcard()
201 end)
202 |> Enum.concat()
203
204 Enum.map(paths, fn path ->
205 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
206 shortcode = Path.basename(path, Path.extname(path))
207 external_path = Path.join("/", Path.relative_to(path, static_path))
208 {shortcode, external_path, [to_string(tag)]}
209 end)
210 end
211
212 @doc """
213 Finds a matching group for the given emoji filename
214 """
215 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
216 def match_extra(group_patterns, filename) do
217 match_group_patterns(group_patterns, fn pattern ->
218 case pattern do
219 %Regex{} = regex -> Regex.match?(regex, filename)
220 string when is_binary(string) -> filename == string
221 end
222 end)
223 end
224
225 defp match_group_patterns(group_patterns, matcher) do
226 Enum.find_value(group_patterns, fn {group, patterns} ->
227 patterns =
228 patterns
229 |> List.wrap()
230 |> Enum.map(fn pattern ->
231 if String.contains?(pattern, "*") do
232 ~r(#{String.replace(pattern, "*", ".*")})
233 else
234 pattern
235 end
236 end)
237
238 Enum.any?(patterns, matcher) && group
239 end)
240 end
241 end