From 6e4de715b3ae2523fc90c2f5660a47fdda03bd6b Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Thu, 14 May 2020 19:21:51 +0400
Subject: [PATCH 1/4] Add OpenAPI spec for PleromaAPI.EmojiAPIController

---
 lib/pleroma/emoji/pack.ex                     |   4 +-
 .../operations/pleroma_emoji_operation.ex     | 390 ++++++++++++++++++
 .../controllers/emoji_api_controller.ex       |  54 +--
 lib/pleroma/web/router.ex                     |   3 +-
 test/web/api_spec/schema_examples_test.exs    |   2 +-
 .../controllers/emoji_api_controller_test.exs | 184 ++++++---
 6 files changed, 541 insertions(+), 96 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex

diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index 242344374..c7b423fbd 100644
--- a/lib/pleroma/emoji/pack.ex
+++ b/lib/pleroma/emoji/pack.ex
@@ -443,10 +443,10 @@ def update_metadata(name, data) do
     pack = load_pack(name)
 
     fb_sha_changed? =
-      not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
+      not is_nil(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack[:"fallback-src"]
 
     with {_, true} <- {:update?, fb_sha_changed?},
-         {:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]),
+         {: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()
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex
new file mode 100644
index 000000000..fc881e657
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_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.PleromaEmojiOperation 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.EmojiAPIController.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.EmojiAPIController.index",
+      responses: %{
+        200 => emoji_packs_response()
+      }
+    }
+  end
+
+  def show_operation do
+    %Operation{
+      tags: ["Emoji Packs"],
+      summary: "Show emoji pack",
+      operationId: "PleromaAPI.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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 uniq 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.EmojiAPIController.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.EmojiAPIController.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.EmojiAPIController.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
index d276b96a4..e20c11860 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -3,6 +3,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
 
   alias Pleroma.Emoji.Pack
 
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
   plug(
     Pleroma.Plugs.OAuthScopesPlug,
     %{scopes: ["write"], admin: true}
@@ -19,13 +21,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
          ]
   )
 
-  plug(
-    :skip_plug,
-    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
-    when action in [:archive, :show, :list]
-  )
+  @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
+  plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
 
-  def remote(conn, %{"url" => url}) do
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiOperation
+
+  def remote(conn, %{url: url}) do
     with {:ok, packs} <- Pack.list_remote(url) do
       json(conn, packs)
     else
@@ -36,12 +37,11 @@ def remote(conn, %{"url" => url}) do
     end
   end
 
-  def list(conn, _params) do
+  def index(conn, _params) do
     emoji_path =
-      Path.join(
-        Pleroma.Config.get!([:instance, :static_dir]),
-        "emoji"
-      )
+      [:instance, :static_dir]
+      |> Pleroma.Config.get!()
+      |> Path.join("emoji")
 
     with {:ok, packs} <- Pack.list_local() do
       json(conn, packs)
@@ -60,7 +60,7 @@ def list(conn, _params) do
     end
   end
 
-  def show(conn, %{"name" => name}) do
+  def show(conn, %{name: name}) do
     name = String.trim(name)
 
     with {:ok, pack} <- Pack.show(name) do
@@ -78,7 +78,7 @@ def show(conn, %{"name" => name}) do
     end
   end
 
-  def archive(conn, %{"name" => name}) do
+  def archive(conn, %{name: name}) do
     with {:ok, archive} <- Pack.get_archive(name) do
       send_download(conn, {:binary, archive}, filename: "#{name}.zip")
     else
@@ -97,8 +97,8 @@ def archive(conn, %{"name" => name}) do
     end
   end
 
-  def download(conn, %{"url" => url, "name" => name} = params) do
-    with :ok <- Pack.download(name, url, params["as"]) do
+  def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
+    with :ok <- Pack.download(name, url, params[:as]) do
       json(conn, "ok")
     else
       {:shareable, _} ->
@@ -118,7 +118,7 @@ def download(conn, %{"url" => url, "name" => name} = params) do
     end
   end
 
-  def create(conn, %{"name" => name}) do
+  def create(conn, %{name: name}) do
     name = String.trim(name)
 
     with :ok <- Pack.create(name) do
@@ -143,7 +143,7 @@ def create(conn, %{"name" => name}) do
     end
   end
 
-  def delete(conn, %{"name" => name}) do
+  def delete(conn, %{name: name}) do
     name = String.trim(name)
 
     with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
@@ -166,7 +166,7 @@ def delete(conn, %{"name" => name}) do
     end
   end
 
-  def update(conn, %{"name" => name, "metadata" => metadata}) do
+  def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
     with {:ok, pack} <- Pack.update_metadata(name, metadata) do
       json(conn, pack.pack)
     else
@@ -184,11 +184,11 @@ def update(conn, %{"name" => name, "metadata" => metadata}) do
     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))
+  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
+    with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do
       json(conn, pack.files)
     else
       {:exists, _} ->
@@ -215,10 +215,10 @@ def add_file(conn, %{"name" => name} = params) do
     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
+  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)
@@ -255,7 +255,7 @@ def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
     end
   end
 
-  def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
+  def delete_file(conn, %{name: name, shortcode: shortcode}) do
     with {:ok, pack} <- Pack.delete_file(name, shortcode) do
       json(conn, pack.files)
     else
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index d77a61361..0d4ebf4ce 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -231,7 +231,8 @@ defmodule Pleroma.Web.Router do
 
     # Pack info / downloading
     scope "/packs" do
-      get("/", EmojiAPIController, :list)
+      pipe_through(:api)
+      get("/", EmojiAPIController, :index)
       get("/:name", EmojiAPIController, :show)
       get("/:name/archive", EmojiAPIController, :archive)
     end
diff --git a/test/web/api_spec/schema_examples_test.exs b/test/web/api_spec/schema_examples_test.exs
index 88b6f07cb..f00e834fc 100644
--- a/test/web/api_spec/schema_examples_test.exs
+++ b/test/web/api_spec/schema_examples_test.exs
@@ -28,7 +28,7 @@ test "request body schema example matches schema" 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
index d343256fe..c625a5c43 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
   end
 
   test "GET /api/pleroma/emoji/packs", %{conn: conn} do
-    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
+    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
 
     shared = resp["test_pack"]
     assert shared["files"] == %{"blank" => "blank.png"}
@@ -46,7 +46,7 @@ test "shareable instance", %{admin_conn: admin_conn, conn: conn} do
       resp =
         conn
         |> get("/api/pleroma/emoji/packs")
-        |> json_response(200)
+        |> json_response_and_validate_schema(200)
 
       mock(fn
         %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
@@ -60,10 +60,8 @@ test "shareable instance", %{admin_conn: admin_conn, conn: conn} do
       end)
 
       assert admin_conn
-             |> get("/api/pleroma/emoji/packs/remote", %{
-               url: "https://example.com"
-             })
-             |> json_response(200) == resp
+             |> 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
@@ -76,8 +74,8 @@ test "non shareable instance", %{admin_conn: admin_conn} do
       end)
 
       assert admin_conn
-             |> get("/api/pleroma/emoji/packs/remote", %{url: "https://example.com"})
-             |> json_response(500) == %{
+             |> 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
@@ -99,7 +97,7 @@ test "download shared pack", %{conn: conn} do
     test "non existing pack", %{conn: conn} do
       assert conn
              |> get("/api/pleroma/emoji/packs/test_pack_for_import/archive")
-             |> json_response(:not_found) == %{
+             |> json_response_and_validate_schema(:not_found) == %{
                "error" => "Pack test_pack_for_import does not exist"
              }
     end
@@ -107,7 +105,7 @@ test "non existing pack", %{conn: conn} do
     test "non downloadable pack", %{conn: conn} do
       assert conn
              |> get("/api/pleroma/emoji/packs/test_pack_nonshared/archive")
-             |> json_response(:forbidden) == %{
+             |> 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"
              }
@@ -132,7 +130,7 @@ test "shared pack from remote and non shared from fallback-src", %{
         } ->
           conn
           |> get("/api/pleroma/emoji/packs/test_pack")
-          |> json_response(200)
+          |> json_response_and_validate_schema(200)
           |> json()
 
         %{
@@ -150,7 +148,7 @@ test "shared pack from remote and non shared from fallback-src", %{
         } ->
           conn
           |> get("/api/pleroma/emoji/packs/test_pack_nonshared")
-          |> json_response(200)
+          |> json_response_and_validate_schema(200)
           |> json()
 
         %{
@@ -161,23 +159,25 @@ test "shared pack from remote and non shared from fallback-src", %{
       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(200) == "ok"
+             |> 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(200) == "ok"
+             |> 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",
                %{
@@ -186,14 +186,14 @@ test "shared pack from remote and non shared from fallback-src", %{
                  as: "test_pack_nonshared2"
                }
              )
-             |> json_response(200) == "ok"
+             |> 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(200) == "ok"
+             |> json_response_and_validate_schema(200) == "ok"
 
       refute File.exists?("#{@emoji_path}/test_pack_nonshared2")
     end
@@ -208,6 +208,7 @@ test "nonshareable instance", %{admin_conn: admin_conn} do
       end)
 
       assert admin_conn
+             |> put_req_header("content-type", "multipart/form-data")
              |> post(
                "/api/pleroma/emoji/packs/download",
                %{
@@ -216,7 +217,7 @@ test "nonshareable instance", %{admin_conn: admin_conn} do
                  as: "test_pack2"
                }
              )
-             |> json_response(500) == %{
+             |> json_response_and_validate_schema(500) == %{
                "error" => "The requested instance does not support sharing emoji packs"
              }
     end
@@ -249,12 +250,13 @@ test "checksum fail", %{admin_conn: admin_conn} do
       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(:internal_server_error) == %{
+             |> json_response_and_validate_schema(:internal_server_error) == %{
                "error" => "SHA256 for the pack doesn't match the one sent by the server"
              }
     end
@@ -278,12 +280,13 @@ test "other error", %{admin_conn: admin_conn} do
       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(:internal_server_error) == %{
+             |> 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"
              }
@@ -311,8 +314,9 @@ test "other error", %{admin_conn: admin_conn} do
 
     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(200) == 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
@@ -336,8 +340,9 @@ test "for a pack with 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: new_data})
-             |> json_response(200) == new_data_with_sha
+             |> json_response_and_validate_schema(200) == new_data_with_sha
 
       assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
     end
@@ -355,8 +360,9 @@ test "when the fallback source doesn't have all the files", ctx do
       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(:bad_request) == %{
+             |> json_response_and_validate_schema(:bad_request) == %{
                "error" => "The fallback archive does not have all files specified in pack.json"
              }
     end
@@ -376,6 +382,7 @@ test "when the fallback source doesn't have all the files", ctx do
 
     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",
@@ -384,7 +391,7 @@ test "create shortcode exists", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(:conflict) == %{
+             |> json_response_and_validate_schema(:conflict) == %{
                "error" => "An emoji with the \"blank\" shortcode already exists"
              }
     end
@@ -393,6 +400,7 @@ 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",
@@ -401,17 +409,21 @@ test "don't rewrite old emoji", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/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(:conflict) == %{
+             |> json_response_and_validate_schema(:conflict) == %{
                "error" =>
                  "New shortcode \"blank2\" is already used. If you want to override emoji use 'force' option"
              }
@@ -421,6 +433,7 @@ 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",
@@ -429,18 +442,22 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(200) == %{"blank" => "blank.png", "blank2" => "dir/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(200) == %{
+             |> json_response_and_validate_schema(200) == %{
                "blank" => "blank.png",
                "blank3" => "dir_2/blank_3.png"
              }
@@ -450,6 +467,7 @@ test "rewrite old emoji with force option", %{admin_conn: admin_conn} do
 
     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: "",
@@ -458,13 +476,14 @@ test "with empty filename", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(:bad_request) == %{
+             |> 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",
@@ -473,37 +492,43 @@ test "add file with not loaded pack", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(:bad_request) == %{
+             |> 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(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"}
+             |> 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(:bad_request) == %{
+             |> 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(:bad_request) == %{"error" => "pack \"not_loaded\" is not found"}
+             |> 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",
@@ -512,24 +537,31 @@ test "new with shortcode as file with update", %{admin_conn: admin_conn} do
                  path: "#{@emoji_path}/test_pack/blank.png"
                }
              })
-             |> json_response(200) == %{"blank" => "blank.png", "blank4" => "dir/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(200) == %{"blank3" => "dir_2/blank_3.png", "blank" => "blank.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(200) == %{"blank" => "blank.png"}
+             |> 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/")
 
@@ -546,11 +578,12 @@ test "new with shortcode from url", %{admin_conn: admin_conn} do
       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(200) == %{
+             |> json_response_and_validate_schema(200) == %{
                "blank_url" => "blank_url.png",
                "blank" => "blank.png"
              }
@@ -564,40 +597,51 @@ 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(200) == %{"shortcode" => "shortcode.png", "blank" => "blank.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(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"}
+             |> 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(:bad_request) == %{"error" => "Emoji \"blank2\" does not exist"}
+             |> 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 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"
-             }
+      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
 
@@ -605,7 +649,7 @@ test "update with empty shortcode", %{admin_conn: admin_conn} 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"
+             |> json_response_and_validate_schema(200) == "ok"
 
       assert File.exists?("#{@emoji_path}/test_created/pack.json")
 
@@ -616,7 +660,7 @@ test "creating and deleting a pack", %{admin_conn: admin_conn} do
 
       assert admin_conn
              |> delete("/api/pleroma/emoji/packs/test_created")
-             |> json_response(200) == "ok"
+             |> json_response_and_validate_schema(200) == "ok"
 
       refute File.exists?("#{@emoji_path}/test_created/pack.json")
     end
@@ -629,7 +673,7 @@ test "if pack exists", %{admin_conn: admin_conn} do
 
       assert admin_conn
              |> post("/api/pleroma/emoji/packs/test_created")
-             |> json_response(:conflict) == %{
+             |> json_response_and_validate_schema(:conflict) == %{
                "error" => "A pack named \"test_created\" already exists"
              }
 
@@ -639,20 +683,26 @@ test "if pack exists", %{admin_conn: admin_conn} do
     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"}
+             |> 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(:not_found) == %{"error" => "Pack non_existing does not exist"}
+           |> 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(:bad_request) == %{"error" => "pack name cannot be empty"}
+           |> json_response_and_validate_schema(:bad_request) == %{
+             "error" => "pack name cannot be empty"
+           }
   end
 
   test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
@@ -661,15 +711,15 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
       File.rm!("#{@emoji_path}/test_pack_for_import/pack.json")
     end)
 
-    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
+    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(200) == ["test_pack_for_import"]
+           |> json_response_and_validate_schema(200) == ["test_pack_for_import"]
 
-    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
+    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")
@@ -686,9 +736,9 @@ test "filesystem import", %{admin_conn: admin_conn, conn: conn} do
 
     assert admin_conn
            |> get("/api/pleroma/emoji/packs/import")
-           |> json_response(200) == ["test_pack_for_import"]
+           |> json_response_and_validate_schema(200) == ["test_pack_for_import"]
 
-    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response(200)
+    resp = conn |> get("/api/pleroma/emoji/packs") |> json_response_and_validate_schema(200)
 
     assert resp["test_pack_for_import"]["files"] == %{
              "blank" => "blank.png",
@@ -712,19 +762,23 @@ test "shows pack.json", %{conn: conn} do
              } =
                conn
                |> get("/api/pleroma/emoji/packs/test_pack")
-               |> json_response(200)
+               |> 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(:not_found) == %{"error" => "Pack non_existing does not exist"}
+             |> 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(:bad_request) == %{"error" => "pack name cannot be empty"}
+             |> json_response_and_validate_schema(:bad_request) == %{
+               "error" => "pack name cannot be empty"
+             }
     end
   end
 end

From 8bde8dfec21dbc83bc73ea6f7ea43a432eea116b Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 18 May 2020 19:43:23 +0400
Subject: [PATCH 2/4] Cleanup Pleroma.Emoji.Pack

---
 lib/pleroma/emoji/pack.ex                     | 734 +++++++++---------
 .../controllers/emoji_api_controller.ex       |  36 +-
 .../controllers/emoji_api_controller_test.exs |  12 +-
 3 files changed, 406 insertions(+), 376 deletions(-)

diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
index c7b423fbd..eb7d598c6 100644
--- 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 @@ def import_from_filesystem 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 @@ def import_from_filesystem 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 @@ defp write_pack_contents(path) do
     }
 
     case save_pack(pack) do
-      :ok -> Path.basename(path)
+      {:ok, _pack} -> Path.basename(path)
       _ -> nil
     end
   end
@@ -216,7 +243,8 @@ defp files_from_path(path) 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 @@ defp files_from_path(path) 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 @@ defp files_from_path(path) 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 @@ defp downloadable?(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 @@ defp create_archive_and_cache(pack, hash) 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
-             }}
-
-          _ ->
-            {:error,
-             "The pack was not set as shared and there is no fallback src to download from"}
-        end
-
-      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
-
-        path = Path.join(emoji_path(), local_name)
-
-        pack = %__MODULE__{
-          name: local_name,
-          path: path,
-          files: remote_pack["files"],
-          pack_file: Path.join(path, "pack.json")
-        }
-
-        File.mkdir_p!(pack.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]
-
-        {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
-
-        # 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
-
-        :ok
-      end
-    end
-  end
-
-  defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
-
-  @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
-  def save_metadata(metadata, %__MODULE__{} = pack) do
-    pack = Map.put(pack, :pack, metadata)
-
-    with :ok <- save_pack(pack) do
+  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
 
-  @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
-  def update_metadata(name, data) do
-    pack = load_pack(name)
-
-    fb_sha_changed? =
-      not is_nil(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack[:"fallback-src"]
-
-    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()
-
-      data
-      |> Map.put("fallback-src-sha256", fallback_sha)
-      |> save_metadata(pack)
-    else
-      {:update?, _} -> save_metadata(data, pack)
-      e -> 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)
-  end
-
-  @spec load_pack(String.t()) :: t() | nil
-  def load_pack(name) do
-    pack_file = Path.join([emoji_path(), name, "pack.json"])
-
-    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)
-    end
-  end
-
   defp from_json(json) do
     map = Jason.decode!(json)
 
     struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
   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_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
+
+  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
+
+  defp save_file(file, pack, filename) 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
+        with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
+
+      url when is_binary(url) ->
+        # Download and write the file
+        file_contents = Tesla.get!(url).body
+        File.write(file_path, file_contents)
+    end
+  end
+
+  defp put_emoji(pack, shortcode, filename) do
+    files = Map.put(pack.files, shortcode, filename)
+    %{pack | files: files}
+  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 create_subdirs(file_path) do
+    if String.contains?(file_path, "/") do
+      file_path
+      |> Path.dirname()
+      |> File.mkdir_p!()
+    end
+  end
+
+  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
+
+  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
+
+  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
+
+  defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
+
+  defp http_get(url) do
+    with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
+      Jason.decode(body)
+    end
+  end
+
+  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
+      {:create_dir, {:error, e}} -> {:error, :create_dir, e}
+      {:ls, {:error, e}} -> {:error, :ls, e}
+    end
+  end
+
+  defp validate_downloadable(pack) do
+    if downloadable?(pack), do: :ok, else: {:error, :cant_download}
+  end
+
+  defp copy_as(remote_pack, local_name) do
+    path = Path.join(emoji_path(), local_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 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
+         }}
+
+      _ ->
+        {: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 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/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
index e20c11860..834fc717e 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -30,7 +30,7 @@ def remote(conn, %{url: url}) do
     with {:ok, packs} <- Pack.list_remote(url) do
       json(conn, packs)
     else
-      {:shareable, _} ->
+      {:error, :not_shareable} ->
         conn
         |> put_status(:internal_server_error)
         |> json(%{error: "The requested instance does not support sharing emoji packs"})
@@ -46,12 +46,12 @@ def index(conn, _params) do
     with {:ok, packs} <- Pack.list_local() do
       json(conn, packs)
     else
-      {:create_dir, {:error, e}} ->
+      {:error, :create_dir, e} ->
         conn
         |> put_status(:internal_server_error)
         |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
 
-      {:ls, {:error, e}} ->
+      {:error, :ls, e} ->
         conn
         |> put_status(:internal_server_error)
         |> json(%{
@@ -66,7 +66,7 @@ def show(conn, %{name: name}) do
     with {:ok, pack} <- Pack.show(name) do
       json(conn, pack)
     else
-      {:loaded, _} ->
+      {:error, :not_found} ->
         conn
         |> put_status(:not_found)
         |> json(%{error: "Pack #{name} does not exist"})
@@ -82,7 +82,7 @@ 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?, _} ->
+      {:error, :cant_download} ->
         conn
         |> put_status(:forbidden)
         |> json(%{
@@ -90,7 +90,7 @@ def archive(conn, %{name: name}) do
             "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
         })
 
-      {:exists?, _} ->
+      {:error, :not_found} ->
         conn
         |> put_status(:not_found)
         |> json(%{error: "Pack #{name} does not exist"})
@@ -98,15 +98,15 @@ def archive(conn, %{name: name}) do
   end
 
   def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
-    with :ok <- Pack.download(name, url, params[:as]) do
+    with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
       json(conn, "ok")
     else
-      {:shareable, _} ->
+      {:error, :not_shareable} ->
         conn
         |> put_status(:internal_server_error)
         |> json(%{error: "The requested instance does not support sharing emoji packs"})
 
-      {:checksum, _} ->
+      {:error, :imvalid_checksum} ->
         conn
         |> put_status(:internal_server_error)
         |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
@@ -121,7 +121,7 @@ def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
   def create(conn, %{name: name}) do
     name = String.trim(name)
 
-    with :ok <- Pack.create(name) do
+    with {:ok, _pack} <- Pack.create(name) do
       json(conn, "ok")
     else
       {:error, :eexist} ->
@@ -170,7 +170,7 @@ def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
     with {:ok, pack} <- Pack.update_metadata(name, metadata) do
       json(conn, pack.pack)
     else
-      {:has_all_files?, _} ->
+      {:error, :incomplete} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
@@ -191,12 +191,12 @@ def add_file(%{body_params: params} = conn, %{name: name}) do
     with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do
       json(conn, pack.files)
     else
-      {:exists, _} ->
+      {:error, :already_exists} ->
         conn
         |> put_status(:conflict)
         |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
 
-      {:loaded, _} ->
+      {:error, :not_found} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "pack \"#{name}\" is not found"})
@@ -223,12 +223,12 @@ def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name:
     with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
       json(conn, pack.files)
     else
-      {:exists, _} ->
+      {:error, :doesnt_exist} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
 
-      {:not_used, _} ->
+      {:error, :already_exists} ->
         conn
         |> put_status(:conflict)
         |> json(%{
@@ -236,7 +236,7 @@ def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name:
             "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
         })
 
-      {:loaded, _} ->
+      {:error, :not_found} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "pack \"#{name}\" is not found"})
@@ -259,12 +259,12 @@ def delete_file(conn, %{name: name, shortcode: shortcode}) do
     with {:ok, pack} <- Pack.delete_file(name, shortcode) do
       json(conn, pack.files)
     else
-      {:exists, _} ->
+      {:error, :doesnt_exist} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
 
-      {:loaded, _} ->
+      {:error, :not_found} ->
         conn
         |> put_status(:bad_request)
         |> json(%{error: "pack \"#{name}\" is not found"})
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
index c625a5c43..6871111d7 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
@@ -234,10 +234,8 @@ test "checksum fail", %{admin_conn: admin_conn} do
           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!()
-          }
+          {:ok, pack} = Pleroma.Emoji.Pack.load_pack("pack_bad_sha")
+          %Tesla.Env{status: 200, body: Jason.encode!(pack)}
 
         %{
           method: :get,
@@ -273,10 +271,8 @@ test "other error", %{admin_conn: admin_conn} do
           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!()
-          }
+          {:ok, pack} = Pleroma.Emoji.Pack.load_pack("test_pack")
+          %Tesla.Env{status: 200, body: Jason.encode!(pack)}
       end)
 
       assert admin_conn

From aef31c69df0424491a3c0bf45fbf46e2da132580 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 18 May 2020 19:38:22 +0400
Subject: [PATCH 3/4] Rename EmojiAPIController to EmojiPackController

---
 ...ion.ex => pleroma_emoji_pack_operation.ex} | 26 +++++++++----------
 ...controller.ex => emoji_pack_controller.ex} |  4 +--
 lib/pleroma/web/router.ex                     | 24 ++++++++---------
 ...est.exs => emoji_pack_controller_test.exs} |  2 +-
 4 files changed, 28 insertions(+), 28 deletions(-)
 rename lib/pleroma/web/api_spec/operations/{pleroma_emoji_operation.ex => pleroma_emoji_pack_operation.ex} (93%)
 rename lib/pleroma/web/pleroma_api/controllers/{emoji_api_controller.ex => emoji_pack_controller.ex} (99%)
 rename test/web/pleroma_api/controllers/{emoji_api_controller_test.exs => emoji_pack_controller_test.exs} (99%)

diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
similarity index 93%
rename from lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex
rename to lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index fc881e657..439127935 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -2,7 +2,7 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.ApiSpec.PleromaEmojiOperation do
+defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
   alias OpenApiSpex.Operation
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.Schemas.ApiError
@@ -20,7 +20,7 @@ def remote_operation do
       summary: "Make request to another instance for emoji packs list",
       security: [%{"oAuth" => ["write"]}],
       parameters: [url_param()],
-      operationId: "PleromaAPI.EmojiAPIController.remote",
+      operationId: "PleromaAPI.EmojiPackController.remote",
       responses: %{
         200 => emoji_packs_response(),
         500 => Operation.response("Error", "application/json", ApiError)
@@ -32,7 +32,7 @@ def index_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Lists local custom emoji packs",
-      operationId: "PleromaAPI.EmojiAPIController.index",
+      operationId: "PleromaAPI.EmojiPackController.index",
       responses: %{
         200 => emoji_packs_response()
       }
@@ -43,7 +43,7 @@ def show_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Show emoji pack",
-      operationId: "PleromaAPI.EmojiAPIController.show",
+      operationId: "PleromaAPI.EmojiPackController.show",
       parameters: [name_param()],
       responses: %{
         200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
@@ -57,7 +57,7 @@ def archive_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Requests a local pack archive from the instance",
-      operationId: "PleromaAPI.EmojiAPIController.archive",
+      operationId: "PleromaAPI.EmojiPackController.archive",
       parameters: [name_param()],
       responses: %{
         200 =>
@@ -75,7 +75,7 @@ def download_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Download pack from another instance",
-      operationId: "PleromaAPI.EmojiAPIController.download",
+      operationId: "PleromaAPI.EmojiPackController.download",
       security: [%{"oAuth" => ["write"]}],
       requestBody: request_body("Parameters", download_request(), required: true),
       responses: %{
@@ -105,7 +105,7 @@ def create_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Create an empty pack",
-      operationId: "PleromaAPI.EmojiAPIController.create",
+      operationId: "PleromaAPI.EmojiPackController.create",
       security: [%{"oAuth" => ["write"]}],
       parameters: [name_param()],
       responses: %{
@@ -121,7 +121,7 @@ def delete_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Delete a custom emoji pack",
-      operationId: "PleromaAPI.EmojiAPIController.delete",
+      operationId: "PleromaAPI.EmojiPackController.delete",
       security: [%{"oAuth" => ["write"]}],
       parameters: [name_param()],
       responses: %{
@@ -136,7 +136,7 @@ def update_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Updates (replaces) pack metadata",
-      operationId: "PleromaAPI.EmojiAPIController.update",
+      operationId: "PleromaAPI.EmojiPackController.update",
       security: [%{"oAuth" => ["write"]}],
       requestBody: request_body("Parameters", update_request(), required: true),
       parameters: [name_param()],
@@ -151,7 +151,7 @@ def add_file_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Add new file to the pack",
-      operationId: "PleromaAPI.EmojiAPIController.add_file",
+      operationId: "PleromaAPI.EmojiPackController.add_file",
       security: [%{"oAuth" => ["write"]}],
       requestBody: request_body("Parameters", add_file_request(), required: true),
       parameters: [name_param()],
@@ -194,7 +194,7 @@ def update_file_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Add new file to the pack",
-      operationId: "PleromaAPI.EmojiAPIController.update_file",
+      operationId: "PleromaAPI.EmojiPackController.update_file",
       security: [%{"oAuth" => ["write"]}],
       requestBody: request_body("Parameters", update_file_request(), required: true),
       parameters: [name_param()],
@@ -236,7 +236,7 @@ def delete_file_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Delete emoji file from pack",
-      operationId: "PleromaAPI.EmojiAPIController.delete_file",
+      operationId: "PleromaAPI.EmojiPackController.delete_file",
       security: [%{"oAuth" => ["write"]}],
       parameters: [
         name_param(),
@@ -256,7 +256,7 @@ def import_from_filesystem_operation do
     %Operation{
       tags: ["Emoji Packs"],
       summary: "Imports packs from filesystem",
-      operationId: "PleromaAPI.EmojiAPIController.import",
+      operationId: "PleromaAPI.EmojiPackController.import",
       security: [%{"oAuth" => ["write"]}],
       responses: %{
         200 =>
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
similarity index 99%
rename from lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
index 834fc717e..2c53dcde1 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex
@@ -1,4 +1,4 @@
-defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
+defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
   use Pleroma.Web, :controller
 
   alias Pleroma.Emoji.Pack
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
   @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.PleromaEmojiOperation
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
 
   def remote(conn, %{url: url}) do
     with {:ok, packs} <- Pack.list_remote(url) do
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 0d4ebf4ce..9eec66e65 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -216,25 +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
       pipe_through(:api)
-      get("/", EmojiAPIController, :index)
-      get("/:name", EmojiAPIController, :show)
-      get("/:name/archive", EmojiAPIController, :archive)
+      get("/", EmojiPackController, :index)
+      get("/:name", EmojiPackController, :show)
+      get("/:name/archive", EmojiPackController, :archive)
     end
   end
 
diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs
similarity index 99%
rename from test/web/pleroma_api/controllers/emoji_api_controller_test.exs
rename to test/web/pleroma_api/controllers/emoji_pack_controller_test.exs
index 6871111d7..ee3d281a0 100644
--- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/emoji_pack_controller_test.exs
@@ -2,7 +2,7 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
+defmodule Pleroma.Web.PleromaAPI.EmojiPackControllerTest do
   use Pleroma.Web.ConnCase
 
   import Tesla.Mock

From e4c720f14c0760ff5863c58a2ed1aafb9bf1bdc5 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 19 May 2020 14:59:50 +0400
Subject: [PATCH 4/4] Fix typo

---
 docs/API/pleroma_api.md                                         | 2 +-
 .../web/api_spec/operations/pleroma_emoji_pack_operation.ex     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md
index 5895613a3..8cdd5808c 100644
--- 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/web/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
index 439127935..567688ff5 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex
@@ -179,7 +179,7 @@ defp add_file_request do
         shortcode: %Schema{
           type: :string,
           description:
-            "Shortcode for new emoji, must be uniq for all emoji. If not sended, shortcode will be taken from original filename."
+            "Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
         },
         filename: %Schema{
           type: :string,