9fa8574749a12383c9be91a729508a3c83098c96
[akkoma] / lib / pleroma / web / pleroma_api / controllers / emoji_api_controller.ex
1 defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
2 use Pleroma.Web, :controller
3
4 alias Pleroma.Emoji.Pack
5
6 plug(
7 Pleroma.Plugs.OAuthScopesPlug,
8 %{scopes: ["write"], admin: true}
9 when action in [
10 :create,
11 :delete,
12 :download_from,
13 :import_from_fs,
14 :update_file,
15 :update_metadata
16 ]
17 )
18
19 plug(
20 :skip_plug,
21 [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
22 when action in [:download_shared, :list_packs, :list_from]
23 )
24
25 @doc """
26 Lists packs from the remote instance.
27
28 Since JS cannot ask remote instances for their packs due to CPS, it has to
29 be done by the server
30 """
31 def list_from(conn, %{"instance_address" => address}) do
32 with {:ok, packs} <- Pack.list_remote_packs(address) do
33 json(conn, packs)
34 else
35 {:shareable, _} ->
36 conn
37 |> put_status(:internal_server_error)
38 |> json(%{error: "The requested instance does not support sharing emoji packs"})
39 end
40 end
41
42 @doc """
43 Lists the packs available on the instance as JSON.
44
45 The information is public and does not require authentication. The format is
46 a map of "pack directory name" to pack.json contents.
47 """
48 def list_packs(conn, _params) do
49 emoji_path =
50 Path.join(
51 Pleroma.Config.get!([:instance, :static_dir]),
52 "emoji"
53 )
54
55 with {:ok, packs} <- Pack.list_local_packs() do
56 json(conn, packs)
57 else
58 {:create_dir, {:error, e}} ->
59 conn
60 |> put_status(:internal_server_error)
61 |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
62
63 {:ls, {:error, e}} ->
64 conn
65 |> put_status(:internal_server_error)
66 |> json(%{
67 error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}"
68 })
69 end
70 end
71
72 def show(conn, %{"name" => name}) do
73 name = String.trim(name)
74
75 with {:ok, pack} <- Pack.show(name) do
76 json(conn, pack)
77 else
78 {:loaded, _} ->
79 conn
80 |> put_status(:not_found)
81 |> json(%{error: "Pack #{name} does not exist"})
82
83 {:error, :empty_values} ->
84 conn
85 |> put_status(:bad_request)
86 |> json(%{error: "pack name cannot be empty"})
87 end
88 end
89
90 @doc """
91 An endpoint for other instances (via admin UI) or users (via browser)
92 to download packs that the instance shares.
93 """
94 def download_shared(conn, %{"name" => name}) do
95 with {:ok, archive} <- Pack.download(name) do
96 send_download(conn, {:binary, archive}, filename: "#{name}.zip")
97 else
98 {:can_download?, _} ->
99 conn
100 |> put_status(:forbidden)
101 |> json(%{
102 error:
103 "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
104 })
105
106 {:exists?, _} ->
107 conn
108 |> put_status(:not_found)
109 |> json(%{error: "Pack #{name} does not exist"})
110 end
111 end
112
113 @doc """
114 An admin endpoint to request downloading and storing a pack named `pack_name` from the instance
115 `instance_address`.
116
117 If the requested instance's admin chose to share the pack, it will be downloaded
118 from that instance, otherwise it will be downloaded from the fallback source, if there is one.
119 """
120 def download_from(conn, %{"instance_address" => address, "pack_name" => name} = params) do
121 with :ok <- Pack.download_from_source(name, address, params["as"]) do
122 json(conn, "ok")
123 else
124 {:shareable, _} ->
125 conn
126 |> put_status(:internal_server_error)
127 |> json(%{error: "The requested instance does not support sharing emoji packs"})
128
129 {:checksum, _} ->
130 conn
131 |> put_status(:internal_server_error)
132 |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
133
134 {:error, e} ->
135 conn
136 |> put_status(:internal_server_error)
137 |> json(%{error: e})
138 end
139 end
140
141 @doc """
142 Creates an empty pack named `name` which then can be updated via the admin UI.
143 """
144 def create(conn, %{"name" => name}) do
145 name = String.trim(name)
146
147 with :ok <- Pack.create(name) do
148 json(conn, "ok")
149 else
150 {:error, :eexist} ->
151 conn
152 |> put_status(:conflict)
153 |> json(%{error: "A pack named \"#{name}\" already exists"})
154
155 {:error, :empty_values} ->
156 conn
157 |> put_status(:bad_request)
158 |> json(%{error: "pack name cannot be empty"})
159
160 {:error, _} ->
161 render_error(
162 conn,
163 :internal_server_error,
164 "Unexpected error occurred while creating pack."
165 )
166 end
167 end
168
169 @doc """
170 Deletes the pack `name` and all it's files.
171 """
172 def delete(conn, %{"name" => name}) do
173 name = String.trim(name)
174
175 with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
176 json(conn, "ok")
177 else
178 {:ok, []} ->
179 conn
180 |> put_status(:not_found)
181 |> json(%{error: "Pack #{name} does not exist"})
182
183 {:error, :empty_values} ->
184 conn
185 |> put_status(:bad_request)
186 |> json(%{error: "pack name cannot be empty"})
187
188 {:error, _, _} ->
189 conn
190 |> put_status(:internal_server_error)
191 |> json(%{error: "Couldn't delete the pack #{name}"})
192 end
193 end
194
195 @doc """
196 An endpoint to update `pack_names`'s metadata.
197
198 `new_data` is the new metadata for the pack, that will replace the old metadata.
199 """
200 def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
201 with {:ok, pack} <- Pack.update_metadata(name, new_data) do
202 json(conn, pack.pack)
203 else
204 {:has_all_files?, _} ->
205 conn
206 |> put_status(:bad_request)
207 |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
208
209 {:error, _} ->
210 render_error(
211 conn,
212 :internal_server_error,
213 "Unexpected error occurred while updating pack metadata."
214 )
215 end
216 end
217
218 @doc """
219 Updates a file in a pack.
220
221 Updating can mean three things:
222
223 - `add` adds an emoji named `shortcode` to the pack `pack_name`,
224 that means that the emoji file needs to be uploaded with the request
225 (thus requiring it to be a multipart request) and be named `file`.
226 There can also be an optional `filename` that will be the new emoji file name
227 (if it's not there, the name will be taken from the uploaded file).
228 - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
229 (from the current filename to `new_filename`)
230 - `remove` removes the emoji named `shortcode` and it's associated file
231 """
232
233 # Add
234 def update_file(
235 conn,
236 %{"pack_name" => pack_name, "action" => "add"} = params
237 ) do
238 filename = params["filename"] || get_filename(params["file"])
239 shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
240
241 with {:ok, pack} <- Pack.add_file(pack_name, shortcode, filename, params["file"]) do
242 json(conn, pack.files)
243 else
244 {:exists, _} ->
245 conn
246 |> put_status(:conflict)
247 |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
248
249 {:loaded, _} ->
250 conn
251 |> put_status(:bad_request)
252 |> json(%{error: "pack \"#{pack_name}\" is not found"})
253
254 {:error, :empty_values} ->
255 conn
256 |> put_status(:bad_request)
257 |> json(%{error: "pack name, shortcode or filename cannot be empty"})
258
259 {:error, _} ->
260 render_error(
261 conn,
262 :internal_server_error,
263 "Unexpected error occurred while adding file to pack."
264 )
265 end
266 end
267
268 # Remove
269 def update_file(conn, %{
270 "pack_name" => pack_name,
271 "action" => "remove",
272 "shortcode" => shortcode
273 }) do
274 with {:ok, pack} <- Pack.remove_file(pack_name, shortcode) do
275 json(conn, pack.files)
276 else
277 {:exists, _} ->
278 conn
279 |> put_status(:bad_request)
280 |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
281
282 {:loaded, _} ->
283 conn
284 |> put_status(:bad_request)
285 |> json(%{error: "pack \"#{pack_name}\" is not found"})
286
287 {:error, :empty_values} ->
288 conn
289 |> put_status(:bad_request)
290 |> json(%{error: "pack name or shortcode cannot be empty"})
291
292 {:error, _} ->
293 render_error(
294 conn,
295 :internal_server_error,
296 "Unexpected error occurred while removing file from pack."
297 )
298 end
299 end
300
301 # Update
302 def update_file(
303 conn,
304 %{"pack_name" => name, "action" => "update", "shortcode" => shortcode} = params
305 ) do
306 new_shortcode = params["new_shortcode"]
307 new_filename = params["new_filename"]
308 force = params["force"] == true
309
310 with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
311 json(conn, pack.files)
312 else
313 {:exists, _} ->
314 conn
315 |> put_status(:bad_request)
316 |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
317
318 {:not_used, _} ->
319 conn
320 |> put_status(:conflict)
321 |> json(%{
322 error:
323 "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
324 })
325
326 {:loaded, _} ->
327 conn
328 |> put_status(:bad_request)
329 |> json(%{error: "pack \"#{name}\" is not found"})
330
331 {:error, :empty_values} ->
332 conn
333 |> put_status(:bad_request)
334 |> json(%{error: "new_shortcode or new_filename cannot be empty"})
335
336 {:error, _} ->
337 render_error(
338 conn,
339 :internal_server_error,
340 "Unexpected error occurred while updating file in pack."
341 )
342 end
343 end
344
345 def update_file(conn, %{"action" => action}) do
346 conn
347 |> put_status(:bad_request)
348 |> json(%{error: "Unknown action: #{action}"})
349 end
350
351 @doc """
352 Imports emoji from the filesystem.
353
354 Importing means checking all the directories in the
355 `$instance_static/emoji/` for directories which do not have
356 `pack.json`. If one has an emoji.txt file, that file will be used
357 to create a `pack.json` file with it's contents. If the directory has
358 neither, all the files with specific configured extenstions will be
359 assumed to be emojis and stored in the new `pack.json` file.
360 """
361
362 def import_from_fs(conn, _params) do
363 with {:ok, names} <- Pack.import_from_filesystem() do
364 json(conn, names)
365 else
366 {:error, :not_writable} ->
367 conn
368 |> put_status(:internal_server_error)
369 |> json(%{error: "Error: emoji pack directory must be writable"})
370
371 {:error, _} ->
372 conn
373 |> put_status(:internal_server_error)
374 |> json(%{error: "Error accessing emoji pack directory"})
375 end
376 end
377
378 defp get_filename(%Plug.Upload{filename: filename}), do: filename
379 defp get_filename(url) when is_binary(url), do: Path.basename(url)
380 end