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