Remove finmoji and add a way to download emojis in packs
[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 # Add the things fro
112 # Deprecated?
113 (Enum.flat_map(
114 packs,
115 fn pack -> load_from_file(Path.join([emoji_dir_path, pack, "emoji.txt"])) end
116 ) ++
117 load_from_file("config/emoji.txt") ++
118 load_from_file("config/custom_emoji.txt") ++
119 load_from_globs(shortcode_globs))
120 |> Enum.reject(fn value -> value == nil end)
121
122 true = :ets.insert(@ets, emojis)
123 end
124
125 :ok
126 end
127
128 defp load_from_file(file) do
129 if File.exists?(file) do
130 load_from_file_stream(File.stream!(file))
131 else
132 []
133 end
134 end
135
136 defp load_from_file_stream(stream) do
137 stream
138 |> Stream.map(&String.trim/1)
139 |> Stream.map(fn line ->
140 case String.split(line, ~r/,\s*/) do
141 [name, file, tags] ->
142 {name, file, tags}
143
144 [name, file] ->
145 {name, file, to_string(match_extra(@groups, file))}
146
147 _ ->
148 nil
149 end
150 end)
151 |> Enum.to_list()
152 end
153
154 defp load_from_globs(globs) do
155 static_path = Path.join(:code.priv_dir(:pleroma), "static")
156
157 paths =
158 Enum.map(globs, fn glob ->
159 Path.join(static_path, glob)
160 |> Path.wildcard()
161 end)
162 |> Enum.concat()
163
164 Enum.map(paths, fn path ->
165 tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
166 shortcode = Path.basename(path, Path.extname(path))
167 external_path = Path.join("/", Path.relative_to(path, static_path))
168 {shortcode, external_path, to_string(tag)}
169 end)
170 end
171
172 @doc """
173 Finds a matching group for the given emoji filename
174 """
175 @spec match_extra(group_patterns(), String.t()) :: atom() | nil
176 def match_extra(group_patterns, filename) do
177 match_group_patterns(group_patterns, fn pattern ->
178 case pattern do
179 %Regex{} = regex -> Regex.match?(regex, filename)
180 string when is_binary(string) -> filename == string
181 end
182 end)
183 end
184
185 defp match_group_patterns(group_patterns, matcher) do
186 Enum.find_value(group_patterns, fn {group, patterns} ->
187 patterns =
188 patterns
189 |> List.wrap()
190 |> Enum.map(fn pattern ->
191 if String.contains?(pattern, "*") do
192 ~r(#{String.replace(pattern, "*", ".*")})
193 else
194 pattern
195 end
196 end)
197
198 Enum.any?(patterns, matcher) && group
199 end)
200 end
201 end