1 defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
2 use Pleroma.Web, :controller
6 def reload(conn, _params) do
12 @emoji_dir_path Path.join(
13 Pleroma.Config.get!([:instance, :static_dir]),
17 @cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
19 def list_packs(conn, _params) do
21 case File.ls(@emoji_dir_path) do
27 |> Enum.filter(fn file ->
28 dir_path = Path.join(@emoji_dir_path, file)
29 # Filter to only use the pack.json packs
30 File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
32 |> Enum.map(fn pack_name ->
33 pack_path = Path.join(@emoji_dir_path, pack_name)
34 pack_file = Path.join(pack_path, "pack.json")
36 {pack_name, Jason.decode!(File.read!(pack_file))}
38 # Transform into a map of pack-name => pack-data
39 # Check if all the files are in place and can be sent
40 |> Enum.map(fn {name, pack} ->
41 pack_path = Path.join(@emoji_dir_path, name)
43 if can_download?(pack, pack_path) do
44 archive_for_sha = make_archive(name, pack, pack_path)
45 archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
49 |> put_in(["pack", "can-download"], true)
50 |> put_in(["pack", "download-sha256"], archive_sha)}
54 |> put_in(["pack", "can-download"], false)}
60 conn |> json(pack_infos)
63 defp can_download?(pack, pack_path) do
64 # If the pack is set as shared, check if it can be downloaded
65 # That means that when asked, the pack can be packed and sent to the remote
66 # Otherwise, they'd have to download it from external-src
67 pack["pack"]["share-files"] and
68 Enum.all?(pack["files"], fn {_, path} ->
69 File.exists?(Path.join(pack_path, path))
73 defp create_archive_and_cache(name, pack, pack_dir, md5) do
76 (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
78 {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
80 cache_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files))
85 # if pack.json MD5 changes, the cache is not valid anymore
86 %{pack_json_md5: md5, pack_data: zip_result},
87 # Add a minute to cache time for every file in the pack
91 Logger.debug("Create an archive for the '#{name}' emoji pack, \
92 keeping it in cache for #{div(cache_ms, 1000)}s")
97 defp make_archive(name, pack, pack_dir) do
98 # Having a different pack.json md5 invalidates cache
99 pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
101 maybe_cached_pack = Cachex.get!(:emoji_packs_cache, name)
104 if is_nil(maybe_cached_pack) do
105 create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
107 if maybe_cached_pack[:pack_file_md5] == pack_file_md5 do
108 Logger.debug("Using cache for the '#{name}' shared emoji pack")
110 maybe_cached_pack[:pack_data]
112 create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
119 def download_shared(conn, %{"name" => name}) do
120 pack_dir = Path.join(@emoji_dir_path, name)
121 pack_file = Path.join(pack_dir, "pack.json")
123 if File.exists?(pack_file) do
124 pack = Jason.decode!(File.read!(pack_file))
126 if can_download?(pack, pack_dir) do
127 zip_result = make_archive(name, pack, pack_dir)
130 |> send_download({:binary, zip_result}, filename: "#{name}.zip")
134 |> put_status(:forbidden)
135 |> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
136 was disabled for this pack or some files are missing")}
141 |> put_status(:not_found)
142 |> text("Pack #{name} does not exist")}
146 def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
147 list_uri = "#{address}/api/pleroma/emoji/packs/list"
149 list = Tesla.get!(list_uri).body |> Jason.decode!()
150 full_pack = list[name]
151 pfiles = full_pack["files"]
152 pack = full_pack["pack"]
156 pack["share-files"] && pack["can-download"] ->
159 sha: pack["download-sha256"],
160 uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
163 pack["fallback-src"] ->
166 sha: pack["fallback-src-sha256"],
167 uri: pack["fallback-src"],
172 {:error, "The pack was not set as shared and there is no fallback src to download from"}
175 case pack_info_res do
176 {:ok, %{sha: sha, uri: uri} = pinfo} ->
177 sha = Base.decode16!(sha)
178 emoji_archive = Tesla.get!(uri).body
180 got_sha = :crypto.hash(:sha256, emoji_archive)
183 local_name = data["as"] || name
184 pack_dir = Path.join(@emoji_dir_path, local_name)
185 File.mkdir_p!(pack_dir)
187 # Fallback cannot contain a pack.json file
189 unless(pinfo[:fallback], do: ['pack.json'], else: []) ++
190 (pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
192 {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
194 # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
195 # in it to depend on itself
196 if pinfo[:fallback] do
197 pack_file_path = Path.join(pack_dir, "pack.json")
199 File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
205 |> put_status(:internal_server_error)
206 |> text("SHA256 for the pack doesn't match the one sent by the server")
210 conn |> put_status(:internal_server_error) |> text(e)
214 def delete(conn, %{"name" => name}) do
215 pack_dir = Path.join(@emoji_dir_path, name)
217 case File.rm_rf(pack_dir) do
222 conn |> put_status(:internal_server_error) |> text("Couldn't delete the pack #{name}")
226 def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
227 pack_dir = Path.join(@emoji_dir_path, name)
228 pack_file_p = Path.join(pack_dir, "pack.json")
230 full_pack = Jason.decode!(File.read!(pack_file_p))
232 # The new fallback-src is in the new data and it's not the same as it was in the old data
233 should_update_fb_sha =
234 not is_nil(new_data["fallback-src"]) and
235 new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
238 if should_update_fb_sha do
239 pack_arch = Tesla.get!(new_data["fallback-src"]).body
241 {:ok, flist} = :zip.unzip(pack_arch, [:memory])
243 # Check if all files from the pack.json are in the archive
245 Enum.all?(full_pack["files"], fn {_, from_manifest} ->
246 Enum.find(flist, fn {from_archive, _} ->
247 to_string(from_archive) == from_manifest
251 unless has_all_files do
254 |> put_status(:bad_request)
255 |> text("The fallback archive does not have all files specified in pack.json")}
257 fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
259 {:ok, new_data |> Map.put("fallback-src-sha256", fallback_sha)}
267 full_pack = Map.put(full_pack, "pack", new_data)
268 File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
270 # Send new data back with fallback sha filled
271 conn |> json(new_data)
280 %{"pack_name" => pack_name, "action" => action, "shortcode" => shortcode} = params
282 pack_dir = Path.join(@emoji_dir_path, pack_name)
283 pack_file_p = Path.join(pack_dir, "pack.json")
285 full_pack = Jason.decode!(File.read!(pack_file_p))
290 unless Map.has_key?(full_pack["files"], shortcode) do
291 with %{"file" => %Plug.Upload{filename: filename, path: upload_path}} <- params do
292 # If there was a file name provided with the request, use it, otherwise just use the
295 if Map.has_key?(params, "filename") do
301 file_path = Path.join(pack_dir, filename)
303 # If the name contains directories, create them
304 if String.contains?(file_path, "/") do
305 File.mkdir_p!(Path.dirname(file_path))
308 # Copy the uploaded file from the temporary directory
309 File.copy!(upload_path, file_path)
311 updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
313 {:ok, updated_full_pack}
315 _ -> {:error, conn |> put_status(:bad_request) |> text("\"file\" not provided")}
320 |> put_status(:conflict)
321 |> text("An emoji with the \"#{shortcode}\" shortcode already exists")}
325 if Map.has_key?(full_pack["files"], shortcode) do
326 {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
328 emoji_file_path = Path.join(pack_dir, emoji_file_path)
330 # Delete the emoji file
331 File.rm!(emoji_file_path)
333 # If the old directory has no more files, remove it
334 if String.contains?(emoji_file_path, "/") do
335 dir = Path.dirname(emoji_file_path)
337 if Enum.empty?(File.ls!(dir)) do
342 {:ok, updated_full_pack}
345 conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
349 if Map.has_key?(full_pack["files"], shortcode) do
350 with %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params do
351 # First, remove the old shortcode, saving the old path
352 {old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
353 old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
354 new_emoji_file_path = Path.join(pack_dir, new_filename)
356 # If the name contains directories, create them
357 if String.contains?(new_emoji_file_path, "/") do
358 File.mkdir_p!(Path.dirname(new_emoji_file_path))
361 # Move/Rename the old filename to a new filename
362 # These are probably on the same filesystem, so just rename should work
363 :ok = File.rename(old_emoji_file_path, new_emoji_file_path)
365 # If the old directory has no more files, remove it
366 if String.contains?(old_emoji_file_path, "/") do
367 dir = Path.dirname(old_emoji_file_path)
369 if Enum.empty?(File.ls!(dir)) do
374 # Then, put in the new shortcode with the new path
376 put_in(updated_full_pack, ["files", new_shortcode], new_filename)
378 {:ok, updated_full_pack}
383 |> put_status(:bad_request)
384 |> text("new_shortcode or new_file were not specified")}
388 conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
392 {:error, conn |> put_status(:bad_request) |> text("Unknown action: #{action}")}
396 {:ok, updated_full_pack} ->
397 # Write the emoji pack file
398 File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
400 # Return the modified file list
401 conn |> json(updated_full_pack["files"])