logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: 265746b21f5a58f49efbdbe9d9eecd697781b93b
parent: 423ea497bb2a7225f4f0d0e1ebff93466b3ec124
Author: lain <lain@soykaf.club>
Date:   Wed, 20 May 2020 12:08:28 +0000

Merge branch 'openapi/pleroma-api/emojis' into 'develop'

Add OpenAPI spec for EmojiPackController

See merge request pleroma/pleroma!2549

Diffstat:

Mdocs/API/pleroma_api.md2+-
Mlib/pleroma/emoji/pack.ex688+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Alib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex390+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dlib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex304-------------------------------------------------------------------------------
Alib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex304+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/router.ex25+++++++++++++------------
Mtest/web/api_spec/schema_examples_test.exs2+-
Dtest/web/pleroma_api/controllers/emoji_api_controller_test.exs730-------------------------------------------------------------------------------
Atest/web/pleroma_api/controllers/emoji_pack_controller_test.exs780+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 1850 insertions(+), 1375 deletions(-)

diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md @@ -426,7 +426,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Authentication: required * Params: * `file`: file needs to be uploaded with the multipart request or link to remote file. - * `shortcode`: (*optional*) shortcode for new emoji, must be uniq for all emoji. If not sended, shortcode will be taken from original filename. + * `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename. * `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename. * Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message. diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex @@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do alias Pleroma.Emoji - @spec emoji_path() :: Path.t() - def emoji_path do - static = Pleroma.Config.get!([:instance, :static_dir]) - Path.join(static, "emoji") - end - @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values} - def create(name) when byte_size(name) > 0 do - dir = Path.join(emoji_path(), name) - - with :ok <- File.mkdir(dir) do - %__MODULE__{ - pack_file: Path.join(dir, "pack.json") - } + def create(name) do + with :ok <- validate_not_empty([name]), + dir <- Path.join(emoji_path(), name), + :ok <- File.mkdir(dir) do + %__MODULE__{pack_file: Path.join(dir, "pack.json")} |> save_pack() end end - def create(_), do: {:error, :empty_values} - - @spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values} - def show(name) when byte_size(name) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, pack} <- validate_pack(pack) do - {:ok, pack} + @spec show(String.t()) :: {:ok, t()} | {:error, atom()} + def show(name) do + with :ok <- validate_not_empty([name]), + {:ok, pack} <- load_pack(name) do + {:ok, validate_pack(pack)} end end - def show(_), do: {:error, :empty_values} - @spec delete(String.t()) :: {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} - def delete(name) when byte_size(name) > 0 do - emoji_path() - |> Path.join(name) - |> File.rm_rf() - end - - def delete(_), do: {:error, :empty_values} - - @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def add_file(name, shortcode, filename, file) - when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do - with {_, nil} <- {:exists, Emoji.get(shortcode)}, - {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do - file_path = Path.join(pack.path, filename) - - create_subdirs(file_path) - - case file do - %Plug.Upload{path: upload_path} -> - # Copy the uploaded file from the temporary directory - File.copy!(upload_path, file_path) - - url when is_binary(url) -> - # Download and write the file - file_contents = Tesla.get!(url).body - File.write!(file_path, file_contents) - end - - files = Map.put(pack.files, shortcode, filename) - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + def delete(name) do + with :ok <- validate_not_empty([name]) do + emoji_path() + |> Path.join(name) + |> File.rm_rf() end end - def add_file(_, _, _, _), do: {:error, :empty_values} - - defp create_subdirs(file_path) do - if String.contains?(file_path, "/") do - file_path - |> Path.dirname() - |> File.mkdir_p!() + @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: + {:ok, t()} | {:error, File.posix() | atom()} + def add_file(name, shortcode, filename, file) do + with :ok <- validate_not_empty([name, shortcode, filename]), + :ok <- validate_emoji_not_exists(shortcode), + {:ok, pack} <- load_pack(name), + :ok <- save_file(file, pack, filename), + {:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end @spec delete_file(String.t(), String.t()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, {filename, files}} when not is_nil(filename) <- - {:exists, Map.pop(pack.files, shortcode)}, - emoji <- Path.join(pack.path, filename), - {_, true} <- {:exists, File.exists?(emoji)} do - emoji_dir = Path.dirname(emoji) - - File.rm!(emoji) - - if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do - File.rmdir!(emoji_dir) - end - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + {:ok, t()} | {:error, File.posix() | atom()} + def delete_file(name, shortcode) do + with :ok <- validate_not_empty([name, shortcode]), + {:ok, pack} <- load_pack(name), + :ok <- remove_file(pack, shortcode), + {:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end - def delete_file(_, _), do: {:error, :empty_values} - @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) :: - {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def update_file(name, shortcode, new_shortcode, new_filename, force) - when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and - byte_size(new_filename) > 0 do - with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, - {_, {filename, files}} when not is_nil(filename) <- - {:exists, Map.pop(pack.files, shortcode)}, - {_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do - old_path = Path.join(pack.path, filename) - old_dir = Path.dirname(old_path) - new_path = Path.join(pack.path, new_filename) - - create_subdirs(new_path) - - :ok = File.rename(old_path, new_path) - - if String.contains?(filename, "/") and File.ls!(old_dir) == [] do - File.rmdir!(old_dir) - end - - files = Map.put(files, new_shortcode, new_filename) - - updated_pack = %{pack | files: files} - - case save_pack(updated_pack) do - :ok -> - Emoji.reload() - {:ok, updated_pack} - - e -> - e - end + {:ok, t()} | {:error, File.posix() | atom()} + def update_file(name, shortcode, new_shortcode, new_filename, force) do + with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]), + {:ok, pack} <- load_pack(name), + {:ok, filename} <- get_filename(pack, shortcode), + :ok <- validate_emoji_not_exists(new_shortcode, force), + :ok <- rename_file(pack, filename, new_filename), + {:ok, updated_pack} <- + pack + |> delete_emoji(shortcode) + |> put_emoji(new_shortcode, new_filename) + |> save_pack() do + Emoji.reload() + {:ok, updated_pack} end end - def update_file(_, _, _, _, _), do: {:error, :empty_values} - - @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()} + @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()} def import_from_filesystem do emoji_path = emoji_path() @@ -184,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do File.dir?(path) and File.exists?(Path.join(path, "pack.json")) end) |> Enum.map(&write_pack_contents/1) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) {:ok, names} else @@ -193,6 +109,117 @@ defmodule Pleroma.Emoji.Pack do end end + @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()} + def list_remote(url) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri) do + uri + |> URI.merge("/api/pleroma/emoji/packs") + |> http_get() + end + end + + @spec list_local() :: {:ok, map()} + def list_local do + with {:ok, results} <- list_packs_dir() do + packs = + results + |> Enum.map(fn name -> + case load_pack(name) do + {:ok, pack} -> pack + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) + + {:ok, packs} + end + end + + @spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()} + def get_archive(name) do + with {:ok, pack} <- load_pack(name), + :ok <- validate_downloadable(pack) do + {:ok, fetch_archive(pack)} + end + end + + @spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()} + def download(name, url, as) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri), + {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(), + {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), + {:ok, archive} <- download_archive(url, sha), + pack <- copy_as(remote_pack, as || name), + {:ok, _} = unzip(archive, pack_info, remote_pack, pack) do + # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 + # in it to depend on itself + if pack_info[:fallback] do + save_pack(pack) + else + {:ok, pack} + end + end + end + + @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} + def save_metadata(metadata, %__MODULE__{} = pack) do + pack + |> Map.put(:pack, metadata) + |> save_pack() + end + + @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} + def update_metadata(name, data) do + with {:ok, pack} <- load_pack(name) do + if fallback_sha_changed?(pack, data) do + update_sha_and_save_metadata(pack, data) + else + save_metadata(data, pack) + end + end + end + + @spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found} + def load_pack(name) do + pack_file = Path.join([emoji_path(), name, "pack.json"]) + + if File.exists?(pack_file) do + pack = + pack_file + |> File.read!() + |> from_json() + |> Map.put(:pack_file, pack_file) + |> Map.put(:path, Path.dirname(pack_file)) + |> Map.put(:name, name) + + {:ok, pack} + else + {:error, :not_found} + end + end + + @spec emoji_path() :: Path.t() + defp emoji_path do + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") + end + + defp validate_emoji_not_exists(shortcode, force \\ false) + defp validate_emoji_not_exists(_shortcode, true), do: :ok + + defp validate_emoji_not_exists(shortcode, _) do + case Emoji.get(shortcode) do + nil -> :ok + _ -> {:error, :already_exists} + end + end + defp write_pack_contents(path) do pack = %__MODULE__{ files: files_from_path(path), @@ -201,7 +228,7 @@ defmodule Pleroma.Emoji.Pack do } case save_pack(pack) do - :ok -> Path.basename(path) + {:ok, _pack} -> Path.basename(path) _ -> nil end end @@ -216,7 +243,8 @@ defmodule Pleroma.Emoji.Pack do # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2 # Create a map of shortcodes to filenames from emoji.txt - File.read!(txt_path) + txt_path + |> File.read!() |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.map(fn line -> @@ -226,21 +254,18 @@ defmodule Pleroma.Emoji.Pack do [name, file | _] -> file_dir_name = Path.dirname(file) - file = - if String.ends_with?(path, file_dir_name) do - Path.basename(file) - else - file - end - - {name, file} + if String.ends_with?(path, file_dir_name) do + {name, Path.basename(file)} + else + {name, file} + end _ -> nil end end) - |> Enum.filter(& &1) - |> Enum.into(%{}) + |> Enum.reject(&is_nil/1) + |> Map.new() else # If there's no emoji.txt, assume all files # that are of certain extensions from the config are emojis and import them all @@ -249,60 +274,20 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_remote(String.t()) :: {:ok, map()} - def list_remote(url) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - packs = - uri - |> URI.merge("/api/pleroma/emoji/packs") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - {:ok, packs} - end - end - - @spec list_local() :: {:ok, map()} - def list_local do - emoji_path = emoji_path() - - # Create the directory first if it does not exist. This is probably the first request made - # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, - {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do - packs = - results - |> Enum.map(&load_pack/1) - |> Enum.filter(& &1) - |> Enum.map(&validate_pack/1) - |> Map.new() - - {:ok, packs} - end - end - defp validate_pack(pack) do - if downloadable?(pack) do - archive = fetch_archive(pack) - archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() + info = + if downloadable?(pack) do + archive = fetch_archive(pack) + archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() - info = pack.pack |> Map.put("can-download", true) |> Map.put("download-sha256", archive_sha) + else + Map.put(pack.pack, "can-download", false) + end - {pack.name, Map.put(pack, :pack, info)} - else - info = Map.put(pack.pack, "can-download", false) - {pack.name, Map.put(pack, :pack, info)} - end + Map.put(pack, :pack, info) end defp downloadable?(pack) do @@ -315,26 +300,6 @@ defmodule Pleroma.Emoji.Pack do end) end - @spec get_archive(String.t()) :: {:ok, binary()} - def get_archive(name) do - with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)}, - {_, true} <- {:can_download?, downloadable?(pack)} do - {:ok, fetch_archive(pack)} - end - end - - defp fetch_archive(pack) do - hash = :crypto.hash(:md5, File.read!(pack.pack_file)) - - case Cachex.get!(:emoji_packs_cache, pack.name) do - %{hash: ^hash, pack_data: archive} -> - archive - - _ -> - create_archive_and_cache(pack, hash) - end - end - defp create_archive_and_cache(pack, hash) do files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] @@ -356,152 +321,221 @@ defmodule Pleroma.Emoji.Pack do result end - @spec download(String.t(), String.t(), String.t()) :: :ok - def download(name, url, as) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - remote_pack = - uri - |> URI.merge("/api/pleroma/emoji/packs/#{name}") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - result = - case remote_pack["pack"] do - %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> - {:ok, - %{ - sha: sha, - url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() - }} - - %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> - {:ok, - %{ - sha: sha, - url: src, - fallback: true - }} + defp save_pack(pack) do + with {:ok, json} <- Jason.encode(pack, pretty: true), + :ok <- File.write(pack.pack_file, json) do + {:ok, pack} + end + end - _ -> - {:error, - "The pack was not set as shared and there is no fallback src to download from"} - end + defp from_json(json) do + map = Jason.decode!(json) - with {:ok, %{sha: sha, url: url} = pinfo} <- result, - %{body: archive} <- Tesla.get!(url), - {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do - local_name = as || name + struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + end - path = Path.join(emoji_path(), local_name) + defp validate_shareable_packs_available(uri) do + with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(), + # Get the actual nodeinfo address and fetch it + {:ok, %{"metadata" => %{"features" => features}}} <- + links |> List.last() |> Map.get("href") |> http_get() do + if Enum.member?(features, "shareable_emoji_packs") do + :ok + else + {:error, :not_shareable} + end + end + end - pack = %__MODULE__{ - name: local_name, - path: path, - files: remote_pack["files"], - pack_file: Path.join(path, "pack.json") - } + defp validate_not_empty(list) do + if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do + :ok + else + {:error, :empty_values} + end + end - File.mkdir_p!(pack.path) + defp save_file(file, pack, filename) do + file_path = Path.join(pack.path, filename) + create_subdirs(file_path) - files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) - # Fallback cannot contain a pack.json file - files = if pinfo[:fallback], do: files, else: ['pack.json' | files] + case file do + %Plug.Upload{path: upload_path} -> + # Copy the uploaded file from the temporary directory + with {:ok, _} <- File.copy(upload_path, file_path), do: :ok - {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files) + url when is_binary(url) -> + # Download and write the file + file_contents = Tesla.get!(url).body + File.write(file_path, file_contents) + end + end - # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 - # in it to depend on itself - if pinfo[:fallback] do - save_pack(pack) - end + defp put_emoji(pack, shortcode, filename) do + files = Map.put(pack.files, shortcode, filename) + %{pack | files: files} + end - :ok - end + defp delete_emoji(pack, shortcode) do + files = Map.delete(pack.files, shortcode) + %{pack | files: files} + end + + defp rename_file(pack, filename, new_filename) do + old_path = Path.join(pack.path, filename) + new_path = Path.join(pack.path, new_filename) + create_subdirs(new_path) + + with :ok <- File.rename(old_path, new_path) do + remove_dir_if_empty(old_path, filename) end end - defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true)) + defp create_subdirs(file_path) do + if String.contains?(file_path, "/") do + file_path + |> Path.dirname() + |> File.mkdir_p!() + end + end - @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} - def save_metadata(metadata, %__MODULE__{} = pack) do - pack = Map.put(pack, :pack, metadata) + defp remove_file(pack, shortcode) do + with {:ok, filename} <- get_filename(pack, shortcode), + emoji <- Path.join(pack.path, filename), + :ok <- File.rm(emoji) do + remove_dir_if_empty(emoji, filename) + end + end - with :ok <- save_pack(pack) do - {:ok, pack} + defp remove_dir_if_empty(emoji, filename) do + dir = Path.dirname(emoji) + + if String.contains?(filename, "/") and File.ls!(dir) == [] do + File.rmdir!(dir) + else + :ok end end - @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} - def update_metadata(name, data) do - pack = load_pack(name) + defp get_filename(pack, shortcode) do + with %{^shortcode => filename} when is_binary(filename) <- pack.files, + true <- pack.path |> Path.join(filename) |> File.exists?() do + {:ok, filename} + else + _ -> {:error, :doesnt_exist} + end + end - fb_sha_changed? = - not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"] + defp http_get(%URI{} = url), do: url |> to_string() |> http_get() - with {_, true} <- {:update?, fb_sha_changed?}, - {:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]), - {:ok, f_list} <- :zip.unzip(zip, [:memory]), - {_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do - fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16() + defp http_get(url) do + with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do + Jason.decode(body) + end + end - data - |> Map.put("fallback-src-sha256", fallback_sha) - |> save_metadata(pack) + defp list_packs_dir do + emoji_path = emoji_path() + # Create the directory first if it does not exist. This is probably the first request made + # with the API so it should be sufficient + with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do + {:ok, results} else - {:update?, _} -> save_metadata(data, pack) - e -> e + {:create_dir, {:error, e}} -> {:error, :create_dir, e} + {:ls, {:error, e}} -> {:error, :ls, e} end end - # Check if all files from the pack.json are in the archive - defp has_all_files?(files, f_list) do - Enum.all?(files, fn {_, from_manifest} -> - List.keyfind(f_list, to_charlist(from_manifest), 0) - end) + defp validate_downloadable(pack) do + if downloadable?(pack), do: :ok, else: {:error, :cant_download} end - @spec load_pack(String.t()) :: t() | nil - def load_pack(name) do - pack_file = Path.join([emoji_path(), name, "pack.json"]) + defp copy_as(remote_pack, local_name) do + path = Path.join(emoji_path(), local_name) - if File.exists?(pack_file) do - pack_file - |> File.read!() - |> from_json() - |> Map.put(:pack_file, pack_file) - |> Map.put(:path, Path.dirname(pack_file)) - |> Map.put(:name, name) + %__MODULE__{ + name: local_name, + path: path, + files: remote_pack["files"], + pack_file: Path.join(path, "pack.json") + } + end + + defp unzip(archive, pack_info, remote_pack, local_pack) do + with :ok <- File.mkdir_p!(local_pack.path) do + files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + # Fallback cannot contain a pack.json file + files = if pack_info[:fallback], do: files, else: ['pack.json' | files] + + :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) end end - defp from_json(json) do - map = Jason.decode!(json) + defp fetch_pack_info(remote_pack, uri, name) do + case remote_pack["pack"] do + %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> + {:ok, + %{ + sha: sha, + url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() + }} + + %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> + {:ok, + %{ + sha: sha, + url: src, + fallback: true + }} - struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + _ -> + {:error, "The pack was not set as shared and there is no fallback src to download from"} + end + end + + defp download_archive(url, sha) do + with {:ok, %{body: archive}} <- Tesla.get(url) do + if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do + {:ok, archive} + else + {:error, :imvalid_checksum} + end + end + end + + defp fetch_archive(pack) do + hash = :crypto.hash(:md5, File.read!(pack.pack_file)) + + case Cachex.get!(:emoji_packs_cache, pack.name) do + %{hash: ^hash, pack_data: archive} -> archive + _ -> create_archive_and_cache(pack, hash) + end + end + + defp fallback_sha_changed?(pack, data) do + is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"] + end + + defp update_sha_and_save_metadata(pack, data) do + with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]), + :ok <- validate_has_all_files(pack, zip) do + fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16() + + data + |> Map.put("fallback-src-sha256", fallback_sha) + |> save_metadata(pack) + end end - defp shareable_packs_available?(uri) do - uri - |> URI.merge("/.well-known/nodeinfo") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> Map.get("links") - |> List.last() - |> Map.get("href") - # Get the actual nodeinfo address and fetch it - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> get_in(["metadata", "features"]) - |> Enum.member?("shareable_emoji_packs") + defp validate_has_all_files(pack, zip) do + with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do + # Check if all files from the pack.json are in the archive + pack.files + |> Enum.all?(fn {_, from_manifest} -> + List.keyfind(f_list, to_charlist(from_manifest), 0) + end) + |> if(do: :ok, else: {:error, :incomplete}) + end end end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -0,0 +1,390 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def remote_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Make request to another instance for emoji packs list", + security: [%{"oAuth" => ["write"]}], + parameters: [url_param()], + operationId: "PleromaAPI.EmojiPackController.remote", + responses: %{ + 200 => emoji_packs_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def index_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Lists local custom emoji packs", + operationId: "PleromaAPI.EmojiPackController.index", + responses: %{ + 200 => emoji_packs_response() + } + } + end + + def show_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Show emoji pack", + operationId: "PleromaAPI.EmojiPackController.show", + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def archive_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Requests a local pack archive from the instance", + operationId: "PleromaAPI.EmojiPackController.archive", + parameters: [name_param()], + responses: %{ + 200 => + Operation.response("Archive file", "application/octet-stream", %Schema{ + type: :string, + format: :binary + }), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def download_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Download pack from another instance", + operationId: "PleromaAPI.EmojiPackController.download", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", download_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp download_request do + %Schema{ + type: :object, + required: [:url, :name], + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + description: "URL of the instance to download from" + }, + name: %Schema{type: :string, format: :uri, description: "Pack Name"}, + as: %Schema{type: :string, format: :uri, description: "Save as"} + } + } + end + + def create_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Create an empty pack", + operationId: "PleromaAPI.EmojiPackController.create", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Not Found", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete a custom emoji pack", + operationId: "PleromaAPI.EmojiPackController.delete", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Updates (replaces) pack metadata", + operationId: "PleromaAPI.EmojiPackController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Metadata", "application/json", metadata()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def add_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.add_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", add_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp add_file_request do + %Schema{ + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + description: + "File needs to be uploaded with the multipart request or link to remote file", + anyOf: [ + %Schema{type: :string, format: :binary}, + %Schema{type: :string, format: :uri} + ] + }, + shortcode: %Schema{ + type: :string, + description: + "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename." + }, + filename: %Schema{ + type: :string, + description: + "New emoji file name. If not specified will be taken from original filename." + } + } + } + end + + def update_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.update_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp update_file_request do + %Schema{ + type: :object, + required: [:shortcode, :new_shortcode, :new_filename], + properties: %{ + shortcode: %Schema{ + type: :string, + description: "Emoji file shortcode" + }, + new_shortcode: %Schema{ + type: :string, + description: "New emoji file shortcode" + }, + new_filename: %Schema{ + type: :string, + description: "New filename for emoji file" + }, + force: %Schema{ + type: :boolean, + description: "With true value to overwrite existing emoji with new shortcode", + default: false + } + } + } + end + + def delete_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete emoji file from pack", + operationId: "PleromaAPI.EmojiPackController.delete_file", + security: [%{"oAuth" => ["write"]}], + parameters: [ + name_param(), + Operation.parameter(:shortcode, :query, :string, "File shortcode", + example: "cofe", + required: true + ) + ], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def import_from_filesystem_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Imports packs from filesystem", + operationId: "PleromaAPI.EmojiPackController.import", + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => + Operation.response("Array of imported pack names", "application/json", %Schema{ + type: :array, + items: %Schema{type: :string} + }) + } + } + end + + defp name_param do + Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + end + + defp url_param do + Operation.parameter( + :url, + :query, + %Schema{type: :string, format: :uri}, + "URL of the instance", + required: true + ) + end + + defp ok_response do + Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"}) + end + + defp emoji_packs_response do + Operation.response( + "Object with pack names as keys and pack contents as values", + "application/json", + %Schema{ + type: :object, + additionalProperties: emoji_pack(), + example: %{ + "emojos" => emoji_pack().example + } + } + ) + end + + defp emoji_pack do + %Schema{ + title: "EmojiPack", + type: :object, + properties: %{ + files: files_object(), + pack: %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "can-download": %Schema{type: :boolean}, + "share-files": %Schema{type: :boolean}, + "download-sha256": %Schema{type: :string} + } + } + }, + example: %{ + "files" => %{"emacs" => "emacs.png", "guix" => "guix.png"}, + "pack" => %{ + "license" => "Test license", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "can-download" => true, + "share-files" => true, + "download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238" + } + } + } + end + + defp files_object do + %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: "Object with emoji names as keys and filenames as values" + } + end + + defp update_request do + %Schema{ + type: :object, + properties: %{ + metadata: %Schema{ + type: :object, + description: "Metadata to replace the old one", + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + } + } + end + + defp metadata do + %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,304 +0,0 @@ -defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do - use Pleroma.Web, :controller - - alias Pleroma.Emoji.Pack - - plug( - Pleroma.Plugs.OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [ - :import_from_filesystem, - :remote, - :download, - :create, - :update, - :delete, - :add_file, - :update_file, - :delete_file - ] - ) - - plug( - :skip_plug, - [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] - when action in [:archive, :show, :list] - ) - - def remote(conn, %{"url" => url}) do - with {:ok, packs} <- Pack.list_remote(url) do - json(conn, packs) - else - {:shareable, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) - end - end - - def list(conn, _params) do - emoji_path = - Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - - with {:ok, packs} <- Pack.list_local() do - json(conn, packs) - else - {:create_dir, {:error, e}} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) - - {:ls, {:error, e}} -> - conn - |> put_status(:internal_server_error) - |> json(%{ - error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}" - }) - end - end - - def show(conn, %{"name" => name}) do - name = String.trim(name) - - with {:ok, pack} <- Pack.show(name) do - json(conn, pack) - else - {:loaded, _} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - end - end - - def archive(conn, %{"name" => name}) do - with {:ok, archive} <- Pack.get_archive(name) do - send_download(conn, {:binary, archive}, filename: "#{name}.zip") - else - {:can_download?, _} -> - conn - |> put_status(:forbidden) - |> json(%{ - error: - "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" - }) - - {:exists?, _} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - end - end - - def download(conn, %{"url" => url, "name" => name} = params) do - with :ok <- Pack.download(name, url, params["as"]) do - json(conn, "ok") - else - {:shareable, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) - - {:checksum, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) - - {:error, e} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: e}) - end - end - - def create(conn, %{"name" => name}) do - name = String.trim(name) - - with :ok <- Pack.create(name) do - json(conn, "ok") - else - {:error, :eexist} -> - conn - |> put_status(:conflict) - |> json(%{error: "A pack named \"#{name}\" already exists"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while creating pack." - ) - end - end - - def delete(conn, %{"name" => name}) do - name = String.trim(name) - - with {:ok, deleted} when deleted != [] <- Pack.delete(name) do - json(conn, "ok") - else - {:ok, []} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - - {:error, _, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Couldn't delete the pack #{name}"}) - end - end - - def update(conn, %{"name" => name, "metadata" => metadata}) do - with {:ok, pack} <- Pack.update_metadata(name, metadata) do - json(conn, pack.pack) - else - {:has_all_files?, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating pack metadata." - ) - end - end - - def add_file(conn, %{"name" => name} = params) do - filename = params["filename"] || get_filename(params["file"]) - shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename)) - - with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do - json(conn, pack.files) - else - {:exists, _} -> - conn - |> put_status(:conflict) - |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - - {:loaded, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name, shortcode or filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while adding file to pack." - ) - end - end - - def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do - new_shortcode = params["new_shortcode"] - new_filename = params["new_filename"] - force = params["force"] == true - - with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do - json(conn, pack.files) - else - {:exists, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:not_used, _} -> - conn - |> put_status(:conflict) - |> json(%{ - error: - "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" - }) - - {:loaded, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating file in pack." - ) - end - end - - def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do - with {:ok, pack} <- Pack.delete_file(name, shortcode) do - json(conn, pack.files) - else - {:exists, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:loaded, _} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name or shortcode cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while removing file from pack." - ) - end - end - - def import_from_filesystem(conn, _params) do - with {:ok, names} <- Pack.import_from_filesystem() do - json(conn, names) - else - {:error, :no_read_write} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Error: emoji pack directory must be writable"}) - - {:error, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Error accessing emoji pack directory"}) - end - end - - defp get_filename(%Plug.Upload{filename: filename}), do: filename - defp get_filename(url) when is_binary(url), do: Path.basename(url) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -0,0 +1,304 @@ +defmodule Pleroma.Web.PleromaAPI.EmojiPackController do + use Pleroma.Web, :controller + + alias Pleroma.Emoji.Pack + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + Pleroma.Plugs.OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [ + :import_from_filesystem, + :remote, + :download, + :create, + :update, + :delete, + :add_file, + :update_file, + :delete_file + ] + ) + + @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] + plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list]) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation + + def remote(conn, %{url: url}) do + with {:ok, packs} <- Pack.list_remote(url) do + json(conn, packs) + else + {:error, :not_shareable} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) + end + end + + def index(conn, _params) do + emoji_path = + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") + + with {:ok, packs} <- Pack.list_local() do + json(conn, packs) + else + {:error, :create_dir, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) + + {:error, :ls, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{ + error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}" + }) + end + end + + def show(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, pack} <- Pack.show(name) do + json(conn, pack) + else + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + end + end + + def archive(conn, %{name: name}) do + with {:ok, archive} <- Pack.get_archive(name) do + send_download(conn, {:binary, archive}, filename: "#{name}.zip") + else + {:error, :cant_download} -> + conn + |> put_status(:forbidden) + |> json(%{ + error: + "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" + }) + + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + end + end + + def download(%{body_params: %{url: url, name: name} = params} = conn, _) do + with {:ok, _pack} <- Pack.download(name, url, params[:as]) do + json(conn, "ok") + else + {:error, :not_shareable} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) + + {:error, :imvalid_checksum} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) + + {:error, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: e}) + end + end + + def create(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, _pack} <- Pack.create(name) do + json(conn, "ok") + else + {:error, :eexist} -> + conn + |> put_status(:conflict) + |> json(%{error: "A pack named \"#{name}\" already exists"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while creating pack." + ) + end + end + + def delete(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, deleted} when deleted != [] <- Pack.delete(name) do + json(conn, "ok") + else + {:ok, []} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + + {:error, _, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Couldn't delete the pack #{name}"}) + end + end + + def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do + with {:ok, pack} <- Pack.update_metadata(name, metadata) do + json(conn, pack.pack) + else + {:error, :incomplete} -> + conn + |> put_status(:bad_request) + |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating pack metadata." + ) + end + end + + def add_file(%{body_params: params} = conn, %{name: name}) do + filename = params[:filename] || get_filename(params[:file]) + shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) + + with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do + json(conn, pack.files) + else + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name, shortcode or filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while adding file to pack." + ) + end + end + + def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do + new_shortcode = params[:new_shortcode] + new_filename = params[:new_filename] + force = params[:force] + + with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{ + error: + "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" + }) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating file in pack." + ) + end + end + + def delete_file(conn, %{name: name, shortcode: shortcode}) do + with {:ok, pack} <- Pack.delete_file(name, shortcode) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name or shortcode cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while removing file from pack." + ) + end + end + + def import_from_filesystem(conn, _params) do + with {:ok, names} <- Pack.import_from_filesystem() do + json(conn, names) + else + {:error, :no_read_write} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Error: emoji pack directory must be writable"}) + + {:error, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Error accessing emoji pack directory"}) + end + end + + defp get_filename(%Plug.Upload{filename: filename}), do: filename + defp get_filename(url) when is_binary(url), do: Path.basename(url) +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex @@ -216,24 +216,25 @@ defmodule Pleroma.Web.Router do scope "/packs" do pipe_through(:admin_api) - get("/import", EmojiAPIController, :import_from_filesystem) - get("/remote", EmojiAPIController, :remote) - post("/download", EmojiAPIController, :download) + get("/import", EmojiPackController, :import_from_filesystem) + get("/remote", EmojiPackController, :remote) + post("/download", EmojiPackController, :download) - post("/:name", EmojiAPIController, :create) - patch("/:name", EmojiAPIController, :update) - delete("/:name", EmojiAPIController, :delete) + post("/:name", EmojiPackController, :create) + patch("/:name", EmojiPackController, :update) + delete("/:name", EmojiPackController, :delete) - post("/:name/files", EmojiAPIController, :add_file) - patch("/:name/files", EmojiAPIController, :update_file) - delete("/:name/files", EmojiAPIController, :delete_file) + post("/:name/files", EmojiPackController, :add_file) + patch("/:name/files", EmojiPackController, :update_file) + delete("/:name/files", EmojiPackController, :delete_file) end # Pack info / downloading scope "/packs" do - get("/", EmojiAPIController, :list) - get("/:name", EmojiAPIController, :show) - get("/:name/archive", EmojiAPIController, :archive) + pipe_through(:api) + get("/", EmojiPackController, :index) + get("/:name", EmojiPackController, :show) + get("/:name/archive", EmojiPackController, :archive) end end diff --git a/test/web/api_spec/schema_examples_test.exs b/test/web/api_spec/schema_examples_test.exs @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ApiSpec.SchemaExamplesTest do end end - for {status, response} <- operation.responses do + for {status, response} <- operation.responses, is_map(response.content[@content_type]) do describe "#{operation.operationId} - #{status} Response" do @schema resolve_schema(response.content[@content_type].schema) diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -1,730 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do - use Pleroma.Web.ConnCase - - import Tesla.Mock - import Pleroma.Factory - - @emoji_path Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) - - setup do - admin = insert(:user, is_admin: true) - token = insert(:oauth_admin_token, user: admin) - - admin_conn = - build_conn() - |> assign(:user, admin) - |> assign(:token, token) - - Pleroma.Emoji.reload() - {:ok, %{admin_conn: admin_conn}} - end - - test "GET /api/pleroma/emoji/packs", %{conn: conn} do - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200) - - shared = resp["test_pack"] - assert shared["files"] == %{"blank" => "blank.png"} - assert Map.has_key?(shared["pack"], "download-sha256") - assert shared["pack"]["can-download"] - assert shared["pack"]["share-files"] - - non_shared = resp["test_pack_nonshared"] - assert non_shared["pack"]["share-files"] == false - assert non_shared["pack"]["can-download"] == false - end - - describe "GET /api/pleroma/emoji/packs/remote" do - test "shareable instance", %{admin_conn: admin_conn, conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs") - |> json_response(200) - - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} -> - json(resp) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote", %{ - url: "https://example.com" - }) - |> json_response(200) == resp - end - - test "non shareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/remote", %{url: "https://example.com"}) - |> json_response(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - end - - describe "GET /api/pleroma/emoji/packs/:name/archive" do - test "download shared pack", %{conn: conn} do - resp = - conn - |> get("/api/pleroma/emoji/packs/test_pack/archive") - |> response(200) - - {:ok, arch} = :zip.unzip(resp, [:memory]) - - assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) - assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/test_pack_for_import/archive") - |> json_response(:not_found) == %{ - "error" => "Pack test_pack_for_import does not exist" - } - end - - test "non downloadable pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/test_pack_nonshared/archive") - |> json_response(:forbidden) == %{ - "error" => - "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" - } - end - end - - describe "POST /api/pleroma/emoji/packs/download" do - test "shared pack from remote and non shared from fallback-src", %{ - admin_conn: admin_conn, - conn: conn - } do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack" - } -> - conn - |> get("/api/pleroma/emoji/packs/test_pack") - |> json_response(200) - |> json() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack/archive" - } -> - conn - |> get("/api/pleroma/emoji/packs/test_pack/archive") - |> response(200) - |> text() - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack_nonshared" - } -> - conn - |> get("/api/pleroma/emoji/packs/test_pack_nonshared") - |> json_response(200) - |> json() - - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "test_pack", - as: "test_pack2" - }) - |> json_response(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack2") - |> json_response(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack2") - - assert admin_conn - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://example.com", - name: "test_pack_nonshared", - as: "test_pack_nonshared2" - } - ) - |> json_response(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") - assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack_nonshared2") - |> json_response(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_pack_nonshared2") - end - - test "nonshareable instance", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: []}}) - end) - - assert admin_conn - |> post( - "/api/pleroma/emoji/packs/download", - %{ - url: "https://old-instance", - name: "test_pack", - as: "test_pack2" - } - ) - |> json_response(500) == %{ - "error" => "The requested instance does not support sharing emoji packs" - } - end - - test "checksum fail", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha" - } -> - %Tesla.Env{ - status: 200, - body: Pleroma.Emoji.Pack.load_pack("pack_bad_sha") |> Jason.encode!() - } - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha/archive" - } -> - %Tesla.Env{ - status: 200, - body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") - } - end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "pack_bad_sha", - as: "pack_bad_sha2" - }) - |> json_response(:internal_server_error) == %{ - "error" => "SHA256 for the pack doesn't match the one sent by the server" - } - end - - test "other error", %{admin_conn: admin_conn} do - mock(fn - %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> - json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) - - %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> - json(%{metadata: %{features: ["shareable_emoji_packs"]}}) - - %{ - method: :get, - url: "https://example.com/api/pleroma/emoji/packs/test_pack" - } -> - %Tesla.Env{ - status: 200, - body: Pleroma.Emoji.Pack.load_pack("test_pack") |> Jason.encode!() - } - end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/download", %{ - url: "https://example.com", - name: "test_pack", - as: "test_pack2" - }) - |> json_response(:internal_server_error) == %{ - "error" => - "The pack was not set as shared and there is no fallback src to download from" - } - end - end - - describe "PATCH /api/pleroma/emoji/packs/:name" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - {:ok, - pack_file: pack_file, - new_data: %{ - "license" => "Test license changed", - "homepage" => "https://pleroma.social", - "description" => "Test description", - "share-files" => false - }} - end - - test "for a pack without a fallback source", ctx do - assert ctx[:admin_conn] - |> patch("/api/pleroma/emoji/packs/test_pack", %{"metadata" => ctx[:new_data]}) - |> json_response(200) == ctx[:new_data] - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] - end - - test "for a pack with a fallback source", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - new_data_with_sha = - Map.put( - new_data, - "fallback-src-sha256", - "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF" - ) - - assert ctx[:admin_conn] - |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) - |> json_response(200) == new_data_with_sha - - assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha - end - - test "when the fallback source doesn't have all the files", ctx do - mock(fn - %{ - method: :get, - url: "https://nonshared-pack" - } -> - {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) - text(empty_arch) - end) - - new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - - assert ctx[:admin_conn] - |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) - |> json_response(:bad_request) == %{ - "error" => "The fallback archive does not have all files specified in pack.json" - } - end - end - - describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do - setup do - pack_file = "#{@emoji_path}/test_pack/pack.json" - original_content = File.read!(pack_file) - - on_exit(fn -> - File.write!(pack_file, original_content) - end) - - :ok - end - - test "create shortcode exists", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(:conflict) == %{ - "error" => "An emoji with the \"blank\" shortcode already exists" - } - end - - test "don't rewrite old emoji", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/blank.png"} - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - new_shortcode: "blank2", - new_filename: "dir_2/blank_3.png" - }) - |> json_response(:conflict) == %{ - "error" => - "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" - } - end - - test "rewrite old emoji with force option", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/blank.png"} - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png", - force: true - }) - |> json_response(200) == %{ - "blank" => "blank.png", - "blank3" => "dir_2/blank_3.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - end - - test "with empty filename", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - filename: "", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(:bad_request) == %{ - "error" => "pack name, shortcode or filename cannot be empty" - } - end - - test "add file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ - shortcode: "blank2", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(:bad_request) == %{ - "error" => "pack \"not_loaded\" is not found" - } - end - - test "remove file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files", %{shortcode: "blank3"}) - |> json_response(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"} - end - - test "remove file with empty shortcode", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/not_loaded/files", %{shortcode: ""}) - |> json_response(:bad_request) == %{ - "error" => "pack name or shortcode cannot be empty" - } - end - - test "update file with not loaded pack", %{admin_conn: admin_conn} do - assert admin_conn - |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"} - end - - test "new with shortcode as file with update", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank4", - filename: "dir/blank.png", - file: %Plug.Upload{ - filename: "blank.png", - path: "#{@emoji_path}/test_pack/blank.png" - } - }) - |> json_response(200) == %{"blank" => "blank.png", "blank4" => "dir/blank.png"} - - assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") - - assert admin_conn - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank4", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response(200) == %{"blank3" => "dir_2/blank_3.png", "blank" => "blank.png"} - - refute File.exists?("#{@emoji_path}/test_pack/dir/") - assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files", %{shortcode: "blank3"}) - |> json_response(200) == %{"blank" => "blank.png"} - - refute File.exists?("#{@emoji_path}/test_pack/dir_2/") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) - end - - test "new with shortcode from url", %{admin_conn: admin_conn} do - mock(fn - %{ - method: :get, - url: "https://test-blank/blank_url.png" - } -> - text(File.read!("#{@emoji_path}/test_pack/blank.png")) - end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank_url", - file: "https://test-blank/blank_url.png" - }) - |> json_response(200) == %{ - "blank_url" => "blank_url.png", - "blank" => "blank.png" - } - - assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") - - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) - end - - test "new without shortcode", %{admin_conn: admin_conn} do - on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_pack/files", %{ - file: %Plug.Upload{ - filename: "shortcode.png", - path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" - } - }) - |> json_response(200) == %{"shortcode" => "shortcode.png", "blank" => "blank.png"} - end - - test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_pack/files", %{shortcode: "blank2"}) - |> json_response(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"} - end - - test "update non existing emoji", %{admin_conn: admin_conn} do - assert admin_conn - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank2", - new_shortcode: "blank3", - new_filename: "dir_2/blank_3.png" - }) - |> json_response(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"} - end - - test "update with empty shortcode", %{admin_conn: admin_conn} do - assert admin_conn - |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ - shortcode: "blank", - new_filename: "dir_2/blank_3.png" - }) - |> json_response(:bad_request) == %{ - "error" => "new_shortcode or new_filename cannot be empty" - } - end - end - - describe "POST/DELETE /api/pleroma/emoji/packs/:name" do - test "creating and deleting a pack", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_created") - |> json_response(200) == "ok" - - assert File.exists?("#{@emoji_path}/test_created/pack.json") - - assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ - "pack" => %{}, - "files" => %{} - } - - assert admin_conn - |> delete("/api/pleroma/emoji/packs/test_created") - |> json_response(200) == "ok" - - refute File.exists?("#{@emoji_path}/test_created/pack.json") - end - - test "if pack exists", %{admin_conn: admin_conn} do - path = Path.join(@emoji_path, "test_created") - File.mkdir(path) - pack_file = Jason.encode!(%{files: %{}, pack: %{}}) - File.write!(Path.join(path, "pack.json"), pack_file) - - assert admin_conn - |> post("/api/pleroma/emoji/packs/test_created") - |> json_response(:conflict) == %{ - "error" => "A pack named \"test_created\" already exists" - } - - on_exit(fn -> File.rm_rf(path) end) - end - - test "with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> post("/api/pleroma/emoji/packs/ ") - |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"} - end - end - - test "deleting nonexisting pack", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/non_existing") - |> json_response(:not_found) == %{"error" => "Pack non_existing does not exist"} - end - - test "deleting with empty name", %{admin_conn: admin_conn} do - assert admin_conn - |> delete("/api/pleroma/emoji/packs/ ") - |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"} - end - - test "filesystem import", %{admin_conn: admin_conn, conn: conn} do - on_exit(fn -> - File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - end) - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200) - - refute Map.has_key?(resp, "test_pack_for_import") - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200) - assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} - - File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") - refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") - - emoji_txt_content = """ - blank, blank.png, Fun - blank2, blank.png - foo, /emoji/test_pack_for_import/blank.png - bar - """ - - File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) - - assert admin_conn - |> get("/api/pleroma/emoji/packs/import") - |> json_response(200) == ["test_pack_for_import"] - - resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200) - - assert resp["test_pack_for_import"]["files"] == %{ - "blank" => "blank.png", - "blank2" => "blank.png", - "foo" => "blank.png" - } - end - - describe "GET /api/pleroma/emoji/packs/:name" do - test "shows pack.json", %{conn: conn} do - assert %{ - "files" => %{"blank" => "blank.png"}, - "pack" => %{ - "can-download" => true, - "description" => "Test description", - "download-sha256" => _, - "homepage" => "https://pleroma.social", - "license" => "Test license", - "share-files" => true - } - } = - conn - |> get("/api/pleroma/emoji/packs/test_pack") - |> json_response(200) - end - - test "non existing pack", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/non_existing") - |> json_response(:not_found) == %{"error" => "Pack non_existing does not exist"} - end - - test "error name", %{conn: conn} do - assert conn - |> get("/api/pleroma/emoji/packs/ ") - |> json_response(:bad_request) == %{"error" => "pack name cannot be empty"} - end - end -end diff --git a/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs @@ -0,0 +1,780 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do + use Pleroma.Web.ConnCase + + import Tesla.Mock + import Pleroma.Factory + + @emoji_path Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false) + + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + admin_conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + Pleroma.Emoji.reload() + {:ok, %{admin_conn: admin_conn}} + end + + test "GET /api/pleroma/emoji/packs", %{conn: conn} do + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + shared = resp["test_pack"] + assert shared["files"] == %{"blank" => "blank.png"} + assert Map.has_key?(shared["pack"], "download-sha256") + assert shared["pack"]["can-download"] + assert shared["pack"]["share-files"] + + non_shared = resp["test_pack_nonshared"] + assert non_shared["pack"]["share-files"] == false + assert non_shared["pack"]["can-download"] == false + end + + describe "GET /api/pleroma/emoji/packs/remote" do + test "shareable instance", %{admin_conn: admin_conn, conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs") + |> json_response_and_validate_schema(200) + + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} -> + json(resp) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> json_response_and_validate_schema(200) == resp + end + + test "non shareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/remote?url=https://example.com") + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + end + + describe "GET /api/pleroma/emoji/packs/:name/archive" do + test "download shared pack", %{conn: conn} do + resp = + conn + |> get("/api/pleroma/emoji/packs/test_pack/archive") + |> response(200) + + {:ok, arch} = :zip.unzip(resp, [:memory]) + + assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end) + assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/test_pack_for_import/archive") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack test_pack_for_import does not exist" + } + end + + test "non downloadable pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/test_pack_nonshared/archive") + |> json_response_and_validate_schema(:forbidden) == %{ + "error" => + "Pack test_pack_nonshared cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" + } + end + end + + describe "POST /api/pleroma/emoji/packs/download" do + test "shared pack from remote and non shared from fallback-src", %{ + admin_conn: admin_conn, + conn: conn + } do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/test_pack" + } -> + conn + |> get("/api/pleroma/emoji/packs/test_pack") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/test_pack/archive" + } -> + conn + |> get("/api/pleroma/emoji/packs/test_pack/archive") + |> response(200) + |> text() + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/test_pack_nonshared" + } -> + conn + |> get("/api/pleroma/emoji/packs/test_pack_nonshared") + |> json_response_and_validate_schema(200) + |> json() + + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "test_pack", + as: "test_pack2" + }) + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack2") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://example.com", + name: "test_pack_nonshared", + as: "test_pack_nonshared2" + } + ) + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/pack.json") + assert File.exists?("#{@emoji_path}/test_pack_nonshared2/blank.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack_nonshared2") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_pack_nonshared2") + end + + test "nonshareable instance", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: []}}) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post( + "/api/pleroma/emoji/packs/download", + %{ + url: "https://old-instance", + name: "test_pack", + as: "test_pack2" + } + ) + |> json_response_and_validate_schema(500) == %{ + "error" => "The requested instance does not support sharing emoji packs" + } + end + + test "checksum fail", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha" + } -> + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha") + %Tesla.Env{status: 200, body: Jason.encode!(pack)} + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/pack_bad_sha/archive" + } -> + %Tesla.Env{ + status: 200, + body: File.read!("test/instance_static/emoji/pack_bad_sha/pack_bad_sha.zip") + } + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "pack_bad_sha", + as: "pack_bad_sha2" + }) + |> json_response_and_validate_schema(:internal_server_error) == %{ + "error" => "SHA256 for the pack doesn't match the one sent by the server" + } + end + + test "other error", %{admin_conn: admin_conn} do + mock(fn + %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> + json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]}) + + %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> + json(%{metadata: %{features: ["shareable_emoji_packs"]}}) + + %{ + method: :get, + url: "https://example.com/api/pleroma/emoji/packs/test_pack" + } -> + {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack") + %Tesla.Env{status: 200, body: Jason.encode!(pack)} + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/download", %{ + url: "https://example.com", + name: "test_pack", + as: "test_pack2" + }) + |> json_response_and_validate_schema(:internal_server_error) == %{ + "error" => + "The pack was not set as shared and there is no fallback src to download from" + } + end + end + + describe "PATCH /api/pleroma/emoji/packs/:name" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + {:ok, + pack_file: pack_file, + new_data: %{ + "license" => "Test license changed", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "share-files" => false + }} + end + + test "for a pack without a fallback source", ctx do + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack", %{"metadata" => ctx[:new_data]}) + |> json_response_and_validate_schema(200) == ctx[:new_data] + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data] + end + + test "for a pack with a fallback source", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + text(File.read!("#{@emoji_path}/test_pack_nonshared/nonshared.zip")) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + new_data_with_sha = + Map.put( + new_data, + "fallback-src-sha256", + "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF" + ) + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(200) == new_data_with_sha + + assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha + end + + test "when the fallback source doesn't have all the files", ctx do + mock(fn + %{ + method: :get, + url: "https://nonshared-pack" + } -> + {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory]) + text(empty_arch) + end) + + new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") + + assert ctx[:admin_conn] + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack", %{metadata: new_data}) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "The fallback archive does not have all files specified in pack.json" + } + end + end + + describe "POST/PATCH/DELETE /api/pleroma/emoji/packs/:name/files" do + setup do + pack_file = "#{@emoji_path}/test_pack/pack.json" + original_content = File.read!(pack_file) + + on_exit(fn -> + File.write!(pack_file, original_content) + end) + + :ok + end + + test "create shortcode exists", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "An emoji with the \"blank\" shortcode already exists" + } + end + + test "don't rewrite old emoji", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + new_shortcode: "blank2", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:conflict) == %{ + "error" => + "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option" + } + end + + test "rewrite old emoji with force option", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir_2/") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank2" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png", + force: true + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank3" => "dir_2/blank_3.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + end + + test "with empty filename", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + filename: "", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name, shortcode or filename cannot be empty" + } + end + + test "add file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/not_loaded/files", %{ + shortcode: "blank2", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=blank3") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "remove file with empty shortcode", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/not_loaded/files?shortcode=") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name or shortcode cannot be empty" + } + end + + test "update file with not loaded pack", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/not_loaded/files", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack \"not_loaded\" is not found" + } + end + + test "new with shortcode as file with update", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank4", + filename: "dir/blank.png", + file: %Plug.Upload{ + filename: "blank.png", + path: "#{@emoji_path}/test_pack/blank.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "blank" => "blank.png", + "blank4" => "dir/blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/dir/blank.png") + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank4", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank3" => "dir_2/blank_3.png", + "blank" => "blank.png" + } + + refute File.exists?("#{@emoji_path}/test_pack/dir/") + assert File.exists?("#{@emoji_path}/test_pack/dir_2/blank_3.png") + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank3") + |> json_response_and_validate_schema(200) == %{"blank" => "blank.png"} + + refute File.exists?("#{@emoji_path}/test_pack/dir_2/") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/dir") end) + end + + test "new with shortcode from url", %{admin_conn: admin_conn} do + mock(fn + %{ + method: :get, + url: "https://test-blank/blank_url.png" + } -> + text(File.read!("#{@emoji_path}/test_pack/blank.png")) + end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank_url", + file: "https://test-blank/blank_url.png" + }) + |> json_response_and_validate_schema(200) == %{ + "blank_url" => "blank_url.png", + "blank" => "blank.png" + } + + assert File.exists?("#{@emoji_path}/test_pack/blank_url.png") + + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/blank_url.png") end) + end + + test "new without shortcode", %{admin_conn: admin_conn} do + on_exit(fn -> File.rm_rf!("#{@emoji_path}/test_pack/shortcode.png") end) + + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> post("/api/pleroma/emoji/packs/test_pack/files", %{ + file: %Plug.Upload{ + filename: "shortcode.png", + path: "#{Pleroma.Config.get([:instance, :static_dir])}/add/shortcode.png" + } + }) + |> json_response_and_validate_schema(200) == %{ + "shortcode" => "shortcode.png", + "blank" => "blank.png" + } + end + + test "remove non existing shortcode in pack.json", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_pack/files?shortcode=blank2") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank2\" does not exist" + } + end + + test "update non existing emoji", %{admin_conn: admin_conn} do + assert admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank2", + new_shortcode: "blank3", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "Emoji \"blank2\" does not exist" + } + end + + test "update with empty shortcode", %{admin_conn: admin_conn} do + assert %{ + "error" => "Missing field: new_shortcode." + } = + admin_conn + |> put_req_header("content-type", "multipart/form-data") + |> patch("/api/pleroma/emoji/packs/test_pack/files", %{ + shortcode: "blank", + new_filename: "dir_2/blank_3.png" + }) + |> json_response_and_validate_schema(:bad_request) + end + end + + describe "POST/DELETE /api/pleroma/emoji/packs/:name" do + test "creating and deleting a pack", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/packs/test_created") + |> json_response_and_validate_schema(200) == "ok" + + assert File.exists?("#{@emoji_path}/test_created/pack.json") + + assert Jason.decode!(File.read!("#{@emoji_path}/test_created/pack.json")) == %{ + "pack" => %{}, + "files" => %{} + } + + assert admin_conn + |> delete("/api/pleroma/emoji/packs/test_created") + |> json_response_and_validate_schema(200) == "ok" + + refute File.exists?("#{@emoji_path}/test_created/pack.json") + end + + test "if pack exists", %{admin_conn: admin_conn} do + path = Path.join(@emoji_path, "test_created") + File.mkdir(path) + pack_file = Jason.encode!(%{files: %{}, pack: %{}}) + File.write!(Path.join(path, "pack.json"), pack_file) + + assert admin_conn + |> post("/api/pleroma/emoji/packs/test_created") + |> json_response_and_validate_schema(:conflict) == %{ + "error" => "A pack named \"test_created\" already exists" + } + + on_exit(fn -> File.rm_rf(path) end) + end + + test "with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> post("/api/pleroma/emoji/packs/ ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end + + test "deleting nonexisting pack", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "deleting with empty name", %{admin_conn: admin_conn} do + assert admin_conn + |> delete("/api/pleroma/emoji/packs/ ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + + test "filesystem import", %{admin_conn: admin_conn, conn: conn} do + on_exit(fn -> + File.rm!("#{@emoji_path}/test_pack_for_import/emoji.txt") + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + end) + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + refute Map.has_key?(resp, "test_pack_for_import") + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"} + + File.rm!("#{@emoji_path}/test_pack_for_import/pack.json") + refute File.exists?("#{@emoji_path}/test_pack_for_import/pack.json") + + emoji_txt_content = """ + blank, blank.png, Fun + blank2, blank.png + foo, /emoji/test_pack_for_import/blank.png + bar + """ + + File.write!("#{@emoji_path}/test_pack_for_import/emoji.txt", emoji_txt_content) + + assert admin_conn + |> get("/api/pleroma/emoji/packs/import") + |> json_response_and_validate_schema(200) == ["test_pack_for_import"] + + resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200) + + assert resp["test_pack_for_import"]["files"] == %{ + "blank" => "blank.png", + "blank2" => "blank.png", + "foo" => "blank.png" + } + end + + describe "GET /api/pleroma/emoji/packs/:name" do + test "shows pack.json", %{conn: conn} do + assert %{ + "files" => %{"blank" => "blank.png"}, + "pack" => %{ + "can-download" => true, + "description" => "Test description", + "download-sha256" => _, + "homepage" => "https://pleroma.social", + "license" => "Test license", + "share-files" => true + } + } = + conn + |> get("/api/pleroma/emoji/packs/test_pack") + |> json_response_and_validate_schema(200) + end + + test "non existing pack", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/non_existing") + |> json_response_and_validate_schema(:not_found) == %{ + "error" => "Pack non_existing does not exist" + } + end + + test "error name", %{conn: conn} do + assert conn + |> get("/api/pleroma/emoji/packs/ ") + |> json_response_and_validate_schema(:bad_request) == %{ + "error" => "pack name cannot be empty" + } + end + end +end