c7620b57358e6a6b75c53c07f368310d5ab99943
[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 if File.exists?(emoji_txt) do
131 load_from_file(emoji_txt)
132 else
133 Logger.info("No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji")
134
135 common_pack_path = Path.join([
136 "/", Pleroma.Config.get!([:instance, :static_dir]), "emoji", pack_name
137 ])
138 make_shortcode_to_file_map(pack_dir, [".png"]) |>
139 Enum.map(fn {shortcode, rel_file} ->
140 filename = Path.join(common_pack_path, rel_file)
141
142 # If no tag matches, use the pack name as a tag
143 {shortcode, filename, to_string(match_extra(@groups, filename))}
144 end)
145 end
146 end
147
148 def make_shortcode_to_file_map(pack_dir, exts) do
149 find_all_emoji(pack_dir, exts) |>
150 Enum.map(&Path.relative_to(&1, pack_dir)) |>
151 Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end) |>
152 Enum.into(%{})
153 end
154
155 def find_all_emoji(dir, exts) do
156 Enum.reduce(
157 File.ls!(dir),
158 [],
159 fn f, acc ->
160 filepath = Path.join(dir, f)
161 if File.dir?(filepath) do
162 acc ++ find_all_emoji(filepath, exts)
163 else
164 acc ++ [filepath]
165 end
166 end
167 ) |> Enum.filter(fn f -> Path.extname(f) in exts end)
168 end
169
170 defp load_from_file(file) do
171 if File.exists?(file) do
172 load_from_file_stream(File.stream!(file))
173 else
174 []
175 end
176 end
177
178 defp load_from_file_stream(stream) do
179 stream
180 |> Stream.map(&String.trim/1)
181 |> Stream.map(fn line ->
182 case String.split(line, ~r/,\s*/) do
183 [name, file, tags] ->
184 {name, file, tags}
185
186 [name, file] ->
187 {name, file, to_string(match_extra(@groups, file))}
188
189 _ ->
190 nil
191 end
192 end)
193 |> Enum.to_list()
194 end
195
196 defp load_from_globs(globs) do
197 static_path = Path.join(:code.priv_dir(:pleroma), "static")
198
199 paths =
200 Enum.map(globs, fn glob ->
201 Path.join(static_path, glob)
202 |> Path.wildcard()
203 end)
204 |> Enum.concat()
205
206 Enum.map(paths, fn path ->
207 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
208 shortcode = Path.basename(path, Path.extname(path))
209 external_path = Path.join("/", Path.relative_to(path, static_path))
210 {shortcode, external_path, to_string(tag)}
211 end)
212 end
213
214 @doc """
215 Finds a matching group for the given emoji filename
216 """
217 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
218 def match_extra(group_patterns, filename) do
219 match_group_patterns(group_patterns, fn pattern ->
220 case pattern do
221 %Regex{} = regex -> Regex.match?(regex, filename)
222 string when is_binary(string) -> filename == string
223 end
224 end)
225 end
226
227 defp match_group_patterns(group_patterns, matcher) do
228 Enum.find_value(group_patterns, fn {group, patterns} ->
229 patterns =
230 patterns
231 |> List.wrap()
232 |> Enum.map(fn pattern ->
233 if String.contains?(pattern, "*") do
234 ~r(#{String.replace(pattern, "*", ".*")})
235 else
236 pattern
237 end
238 end)
239
240 Enum.any?(patterns, matcher) && group
241 end)
242 end
243 end