From 287f781808c88f43f5689508b5aa21f6639b9d16 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 8 May 2020 16:54:53 +0300 Subject: user deletion --- lib/pleroma/user.ex | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2a6a23fec..278129ad2 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1431,6 +1431,25 @@ defmodule Pleroma.User do BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) end + defp delete_and_invalidate_cache(%User{} = user) do + invalidate_cache(user) + Repo.delete(user) + end + + defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user) + + defp delete_or_deactivate(%User{local: true} = user) do + status = account_status(user) + + if status == :confirmation_pending do + delete_and_invalidate_cache(user) + else + user + |> change(%{deactivated: true, email: nil}) + |> update_and_set_cache() + end + end + def perform(:force_password_reset, user), do: force_password_reset(user) @spec perform(atom(), User.t()) :: {:ok, User.t()} @@ -1452,14 +1471,7 @@ defmodule Pleroma.User do delete_user_activities(user) - if user.local do - user - |> change(%{deactivated: true, email: nil}) - |> update_and_set_cache() - else - invalidate_cache(user) - Repo.delete(user) - end + delete_or_deactivate(user) end def perform(:deactivate_async, user, status), do: deactivate(user, status) -- cgit v1.2.3 From cb40602a167f4637dc6df6633ec2dfe33f774177 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Fri, 15 May 2020 21:34:46 +0300 Subject: added media proxy invalidation --- lib/pleroma/object.ex | 34 ++++++++++----- lib/pleroma/web/media_proxy/invalidation.ex | 19 +++++++++ lib/pleroma/web/media_proxy/invalidations/nginx.ex | 12 ++++++ .../web/media_proxy/invalidations/script.ex | 10 +++++ lib/pleroma/workers/attachments_cleanup_worker.ex | 49 +++++++++++++--------- 5 files changed, 94 insertions(+), 30 deletions(-) create mode 100644 lib/pleroma/web/media_proxy/invalidation.ex create mode 100644 lib/pleroma/web/media_proxy/invalidations/nginx.ex create mode 100644 lib/pleroma/web/media_proxy/invalidations/script.ex (limited to 'lib') diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index e678fd415..66b233498 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -9,11 +9,13 @@ defmodule Pleroma.Object do import Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Object alias Pleroma.Object.Fetcher alias Pleroma.ObjectTombstone alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Workers.AttachmentsCleanupWorker require Logger @@ -183,27 +185,37 @@ defmodule Pleroma.Object do def delete(%Object{data: %{"id" => id}} = object) do with {:ok, _obj} = swap_object_with_tombstone(object), deleted_activity = Activity.delete_all_by_object_ap_id(id), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do - with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do - {:ok, _} = - Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{ - "object" => object - }) - end + {:ok, _} <- invalid_object_cache(object) do + cleanup_attachments( + Config.get([:instance, :cleanup_attachments]), + %{"object" => object} + ) {:ok, object, deleted_activity} end end - def prune(%Object{data: %{"id" => id}} = object) do + @spec cleanup_attachments(boolean(), %{required(:object) => map()}) :: + {:ok, Oban.Job.t() | nil} + def cleanup_attachments(true, %{"object" => _} = params) do + AttachmentsCleanupWorker.enqueue("cleanup_attachments", params) + end + + def cleanup_attachments(_, _), do: {:ok, nil} + + def prune(%Object{data: %{"id" => _id}} = object) do with {:ok, object} <- Repo.delete(object), - {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"), - {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do + {:ok, _} <- invalid_object_cache(object) do {:ok, object} end end + def invalid_object_cache(%Object{data: %{"id" => id}}) do + with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do + Cachex.del(:web_resp_cache, URI.parse(id).path) + end + end + def set_cache(%Object{data: %{"id" => ap_id}} = object) do Cachex.put(:object_cache, "object:#{ap_id}", object) {:ok, object} diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex new file mode 100644 index 000000000..dd9a53a27 --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -0,0 +1,19 @@ +defmodule Pleroma.Web.MediaProxy.Invalidation do + @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()} + + alias Pleroma.Config + + def purge(urls) do + [:media_proxy, :invalidation, :enabled] + |> Config.get() + |> do_purge(urls) + end + + defp do_purge(true, urls) do + config = Config.get([:media_proxy, :invalidation]) + config[:provider].purge(urls, config[:options]) + :ok + end + + defp do_purge(_, _), do: :ok +end diff --git a/lib/pleroma/web/media_proxy/invalidations/nginx.ex b/lib/pleroma/web/media_proxy/invalidations/nginx.ex new file mode 100644 index 000000000..5bfdd505c --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/nginx.ex @@ -0,0 +1,12 @@ +defmodule Pleroma.Web.MediaProxy.Invalidation.Nginx do + @behaviour Pleroma.Web.MediaProxy.Invalidation + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, _opts) do + Enum.each(urls, fn url -> + Pleroma.HTTP.request(:purge, url, "", [], []) + end) + + {:ok, "success"} + end +end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex new file mode 100644 index 000000000..f458845a0 --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -0,0 +1,10 @@ +defmodule Pleroma.Web.MediaProxy.Invalidation.Script do + @behaviour Pleroma.Web.MediaProxy.Invalidation + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, %{script_path: script_path} = options) do + script_args = List.wrap(Map.get(options, :script_args, [])) + System.cmd(Path.expand(script_path), [urls] ++ script_args) + {:ok, "success"} + end +end diff --git a/lib/pleroma/workers/attachments_cleanup_worker.ex b/lib/pleroma/workers/attachments_cleanup_worker.ex index 3c5820a86..49352db2a 100644 --- a/lib/pleroma/workers/attachments_cleanup_worker.ex +++ b/lib/pleroma/workers/attachments_cleanup_worker.ex @@ -27,8 +27,20 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do uploader = Pleroma.Config.get([Pleroma.Upload, :uploader]) + prefix = + case Pleroma.Config.get([Pleroma.Upload, :base_url]) do + nil -> "media" + _ -> "" + end + + base_url = + String.trim_trailing( + Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), + "/" + ) + # find all objects for copies of the attachments, name and actor doesn't matter here - delete_ids = + object_ids_and_hrefs = from(o in Object, where: fragment( @@ -67,29 +79,28 @@ defmodule Pleroma.Workers.AttachmentsCleanupWorker do |> Enum.map(fn {href, %{id: id, count: count}} -> # only delete files that have single instance with 1 <- count do - prefix = - case Pleroma.Config.get([Pleroma.Upload, :base_url]) do - nil -> "media" - _ -> "" - end - - base_url = - String.trim_trailing( - Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()), - "/" - ) - - file_path = String.trim_leading(href, "#{base_url}/#{prefix}") + href + |> String.trim_leading("#{base_url}/#{prefix}") + |> uploader.delete_file() - uploader.delete_file(file_path) + {id, href} + else + _ -> {id, nil} end - - id end) - from(o in Object, where: o.id in ^delete_ids) + object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end) + + from(o in Object, where: o.id in ^object_ids) |> Repo.delete_all() + + object_ids_and_hrefs + |> Enum.filter(fn {_, href} -> not is_nil(href) end) + |> Enum.map(&elem(&1, 1)) + |> Pleroma.Web.MediaProxy.Invalidation.purge() + + {:ok, :success} end - def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok + def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip} end -- cgit v1.2.3 From 3f8d68bdf3224cd6023b3d7f8e64221222872820 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Sat, 16 May 2020 15:16:33 +0300 Subject: added example cache purge script --- lib/pleroma/web/media_proxy/invalidations/http.ex | 16 ++++++++++++++++ lib/pleroma/web/media_proxy/invalidations/nginx.ex | 12 ------------ lib/pleroma/web/media_proxy/invalidations/script.ex | 11 ++++++++--- 3 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 lib/pleroma/web/media_proxy/invalidations/http.ex delete mode 100644 lib/pleroma/web/media_proxy/invalidations/nginx.ex (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex new file mode 100644 index 000000000..40c624efc --- /dev/null +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.Web.MediaProxy.Invalidation.Http do + @behaviour Pleroma.Web.MediaProxy.Invalidation + + @impl Pleroma.Web.MediaProxy.Invalidation + def purge(urls, opts) do + method = Map.get(opts, :http_method, :purge) + headers = Map.get(opts, :http_headers, []) + options = Map.get(opts, :http_options, []) + + Enum.each(urls, fn url -> + Pleroma.HTTP.request(method, url, "", headers, options) + end) + + {:ok, "success"} + end +end diff --git a/lib/pleroma/web/media_proxy/invalidations/nginx.ex b/lib/pleroma/web/media_proxy/invalidations/nginx.ex deleted file mode 100644 index 5bfdd505c..000000000 --- a/lib/pleroma/web/media_proxy/invalidations/nginx.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Pleroma.Web.MediaProxy.Invalidation.Nginx do - @behaviour Pleroma.Web.MediaProxy.Invalidation - - @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, _opts) do - Enum.each(urls, fn url -> - Pleroma.HTTP.request(:purge, url, "", [], []) - end) - - {:ok, "success"} - end -end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index f458845a0..94c79511a 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -2,9 +2,14 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do @behaviour Pleroma.Web.MediaProxy.Invalidation @impl Pleroma.Web.MediaProxy.Invalidation - def purge(urls, %{script_path: script_path} = options) do - script_args = List.wrap(Map.get(options, :script_args, [])) - System.cmd(Path.expand(script_path), [urls] ++ script_args) + def purge(urls, %{script_path: script_path} = _options) do + args = + urls + |> List.wrap() + |> Enum.uniq() + |> Enum.join(" ") + + System.cmd(Path.expand(script_path), [args]) {:ok, "success"} end end -- cgit v1.2.3 From c33a4315fb09e67d0ed5f644877054a3fb7b1fe1 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 18 May 2020 06:48:19 +0300 Subject: updated docs --- lib/pleroma/web/media_proxy/invalidation.ex | 5 +++-- lib/pleroma/web/media_proxy/invalidations/http.ex | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index dd9a53a27..371aa8ae0 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -10,8 +10,9 @@ defmodule Pleroma.Web.MediaProxy.Invalidation do end defp do_purge(true, urls) do - config = Config.get([:media_proxy, :invalidation]) - config[:provider].purge(urls, config[:options]) + provider = Config.get([:media_proxy, :invalidation, :provider]) + options = Config.get(provider) + provider.purge(urls, options) :ok end diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index 40c624efc..66fafa7ba 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -3,9 +3,9 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Http do @impl Pleroma.Web.MediaProxy.Invalidation def purge(urls, opts) do - method = Map.get(opts, :http_method, :purge) - headers = Map.get(opts, :http_headers, []) - options = Map.get(opts, :http_options, []) + method = Map.get(opts, :method, :purge) + headers = Map.get(opts, :headers, []) + options = Map.get(opts, :options, []) Enum.each(urls, fn url -> Pleroma.HTTP.request(method, url, "", headers, options) -- cgit v1.2.3 From 188b32145e0a97411878a11bd4f8ad8bc9cc1d9a Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 18 May 2020 13:28:50 +0200 Subject: InstanceView: Expose background image link. This will make it easier for more clients to support this feature. --- lib/pleroma/web/mastodon_api/views/instance_view.ex | 1 + 1 file changed, 1 insertion(+) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index a329ffc28..8088306c3 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -33,6 +33,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit), background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), + background_image: Keyword.get(instance, :background_image), pleroma: %{ metadata: %{ features: features(), -- cgit v1.2.3 From 6e4de715b3ae2523fc90c2f5660a47fdda03bd6b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 14 May 2020 19:21:51 +0400 Subject: Add OpenAPI spec for PleromaAPI.EmojiAPIController --- lib/pleroma/emoji/pack.ex | 4 +- .../api_spec/operations/pleroma_emoji_operation.ex | 390 +++++++++++++++++++++ .../controllers/emoji_api_controller.ex | 54 +-- lib/pleroma/web/router.ex | 3 +- 4 files changed, 421 insertions(+), 30 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex (limited to 'lib') 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 @@ defmodule Pleroma.Emoji.Pack 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 +# 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]) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiOperation - def remote(conn, %{"url" => url}) do + def remote(conn, %{url: url}) do with {:ok, packs} <- Pack.list_remote(url) do json(conn, packs) else @@ -36,12 +37,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 -- cgit v1.2.3 From 8bde8dfec21dbc83bc73ea6f7ea43a432eea116b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 18 May 2020 19:43:23 +0400 Subject: Cleanup Pleroma.Emoji.Pack --- lib/pleroma/emoji/pack.ex | 688 +++++++++++---------- .../controllers/emoji_api_controller.ex | 36 +- 2 files changed, 379 insertions(+), 345 deletions(-) (limited to 'lib') 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 @@ defmodule Pleroma.Emoji.Pack do File.dir?(path) and File.exists?(Path.join(path, "pack.json")) end) |> Enum.map(&write_pack_contents/1) - |> Enum.filter(& &1) + |> Enum.reject(&is_nil/1) {:ok, names} else @@ -193,6 +109,117 @@ defmodule Pleroma.Emoji.Pack do end end + @spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()} + def list_remote(url) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri) do + uri + |> URI.merge("/api/pleroma/emoji/packs") + |> http_get() + end + end + + @spec list_local() :: {:ok, map()} + def list_local do + with {:ok, results} <- list_packs_dir() do + packs = + results + |> Enum.map(fn name -> + case load_pack(name) do + {:ok, pack} -> pack + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + |> Map.new(fn pack -> {pack.name, validate_pack(pack)} end) + + {:ok, packs} + end + end + + @spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()} + def get_archive(name) do + with {:ok, pack} <- load_pack(name), + :ok <- validate_downloadable(pack) do + {:ok, fetch_archive(pack)} + end + end + + @spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()} + def download(name, url, as) do + uri = url |> String.trim() |> URI.parse() + + with :ok <- validate_shareable_packs_available(uri), + {:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(), + {:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name), + {:ok, archive} <- download_archive(url, sha), + pack <- copy_as(remote_pack, as || name), + {:ok, _} = unzip(archive, pack_info, remote_pack, pack) do + # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 + # in it to depend on itself + if pack_info[:fallback] do + save_pack(pack) + else + {:ok, pack} + end + end + end + + @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} + def save_metadata(metadata, %__MODULE__{} = pack) do + pack + |> Map.put(:pack, metadata) + |> save_pack() + end + + @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} + def update_metadata(name, data) do + with {:ok, pack} <- load_pack(name) do + if fallback_sha_changed?(pack, data) do + update_sha_and_save_metadata(pack, data) + else + save_metadata(data, pack) + end + end + end + + @spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found} + def load_pack(name) do + pack_file = Path.join([emoji_path(), name, "pack.json"]) + + if File.exists?(pack_file) do + pack = + pack_file + |> File.read!() + |> from_json() + |> Map.put(:pack_file, pack_file) + |> Map.put(:path, Path.dirname(pack_file)) + |> Map.put(:name, name) + + {:ok, pack} + else + {:error, :not_found} + end + end + + @spec emoji_path() :: Path.t() + defp emoji_path do + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") + end + + defp validate_emoji_not_exists(shortcode, force \\ false) + defp validate_emoji_not_exists(_shortcode, true), do: :ok + + defp validate_emoji_not_exists(shortcode, _) do + case Emoji.get(shortcode) do + nil -> :ok + _ -> {:error, :already_exists} + end + end + defp write_pack_contents(path) do pack = %__MODULE__{ files: files_from_path(path), @@ -201,7 +228,7 @@ defmodule Pleroma.Emoji.Pack do } case save_pack(pack) do - :ok -> Path.basename(path) + {:ok, _pack} -> Path.basename(path) _ -> nil end end @@ -216,7 +243,8 @@ defmodule Pleroma.Emoji.Pack do # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2 # Create a map of shortcodes to filenames from emoji.txt - File.read!(txt_path) + txt_path + |> File.read!() |> String.split("\n") |> Enum.map(&String.trim/1) |> Enum.map(fn line -> @@ -226,21 +254,18 @@ defmodule Pleroma.Emoji.Pack do [name, file | _] -> file_dir_name = Path.dirname(file) - file = - if String.ends_with?(path, file_dir_name) do - Path.basename(file) - else - file - end - - {name, file} + if String.ends_with?(path, file_dir_name) do + {name, Path.basename(file)} + else + {name, file} + end _ -> nil end end) - |> Enum.filter(& &1) - |> Enum.into(%{}) + |> Enum.reject(&is_nil/1) + |> Map.new() else # If there's no emoji.txt, assume all files # that are of certain extensions from the config are emojis and import them all @@ -249,60 +274,20 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_remote(String.t()) :: {:ok, map()} - def list_remote(url) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - packs = - uri - |> URI.merge("/api/pleroma/emoji/packs") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - {:ok, packs} - end - end - - @spec list_local() :: {:ok, map()} - def list_local do - emoji_path = emoji_path() - - # Create the directory first if it does not exist. This is probably the first request made - # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, - {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do - packs = - results - |> Enum.map(&load_pack/1) - |> Enum.filter(& &1) - |> Enum.map(&validate_pack/1) - |> Map.new() - - {:ok, packs} - end - end - defp validate_pack(pack) do - if downloadable?(pack) do - archive = fetch_archive(pack) - archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() + info = + if downloadable?(pack) do + archive = fetch_archive(pack) + archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() - info = pack.pack |> Map.put("can-download", true) |> Map.put("download-sha256", archive_sha) + else + Map.put(pack.pack, "can-download", false) + end - {pack.name, Map.put(pack, :pack, info)} - else - info = Map.put(pack.pack, "can-download", false) - {pack.name, Map.put(pack, :pack, info)} - end + Map.put(pack, :pack, info) end defp downloadable?(pack) do @@ -315,26 +300,6 @@ defmodule Pleroma.Emoji.Pack do end) end - @spec get_archive(String.t()) :: {:ok, binary()} - def get_archive(name) do - with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)}, - {_, true} <- {:can_download?, downloadable?(pack)} do - {:ok, fetch_archive(pack)} - end - end - - defp fetch_archive(pack) do - hash = :crypto.hash(:md5, File.read!(pack.pack_file)) - - case Cachex.get!(:emoji_packs_cache, pack.name) do - %{hash: ^hash, pack_data: archive} -> - archive - - _ -> - create_archive_and_cache(pack, hash) - end - end - defp create_archive_and_cache(pack, hash) do files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] @@ -356,152 +321,221 @@ defmodule Pleroma.Emoji.Pack do result end - @spec download(String.t(), String.t(), String.t()) :: :ok - def download(name, url, as) do - uri = - url - |> String.trim() - |> URI.parse() - - with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - remote_pack = - uri - |> URI.merge("/api/pleroma/emoji/packs/#{name}") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - - result = - case remote_pack["pack"] do - %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> - {:ok, - %{ - sha: sha, - url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() - }} - - %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> - {:ok, - %{ - sha: sha, - url: src, - fallback: true - }} + defp save_pack(pack) do + with {:ok, json} <- Jason.encode(pack, pretty: true), + :ok <- File.write(pack.pack_file, json) do + {:ok, pack} + end + end - _ -> - {:error, - "The pack was not set as shared and there is no fallback src to download from"} - end + defp from_json(json) do + map = Jason.decode!(json) - with {:ok, %{sha: sha, url: url} = pinfo} <- result, - %{body: archive} <- Tesla.get!(url), - {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do - local_name = as || name + struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + end - path = Path.join(emoji_path(), local_name) + defp validate_shareable_packs_available(uri) do + with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(), + # Get the actual nodeinfo address and fetch it + {:ok, %{"metadata" => %{"features" => features}}} <- + links |> List.last() |> Map.get("href") |> http_get() do + if Enum.member?(features, "shareable_emoji_packs") do + :ok + else + {:error, :not_shareable} + end + end + end - pack = %__MODULE__{ - name: local_name, - path: path, - files: remote_pack["files"], - pack_file: Path.join(path, "pack.json") - } + defp validate_not_empty(list) do + if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do + :ok + else + {:error, :empty_values} + end + end - File.mkdir_p!(pack.path) + defp save_file(file, pack, filename) do + file_path = Path.join(pack.path, filename) + create_subdirs(file_path) - files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) - # Fallback cannot contain a pack.json file - files = if pinfo[:fallback], do: files, else: ['pack.json' | files] + case file do + %Plug.Upload{path: upload_path} -> + # Copy the uploaded file from the temporary directory + with {:ok, _} <- File.copy(upload_path, file_path), do: :ok - {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files) + url when is_binary(url) -> + # Download and write the file + file_contents = Tesla.get!(url).body + File.write(file_path, file_contents) + end + end - # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 - # in it to depend on itself - if pinfo[:fallback] do - save_pack(pack) - end + defp put_emoji(pack, shortcode, filename) do + files = Map.put(pack.files, shortcode, filename) + %{pack | files: files} + end - :ok - end + defp delete_emoji(pack, shortcode) do + files = Map.delete(pack.files, shortcode) + %{pack | files: files} + end + + defp rename_file(pack, filename, new_filename) do + old_path = Path.join(pack.path, filename) + new_path = Path.join(pack.path, new_filename) + create_subdirs(new_path) + + with :ok <- File.rename(old_path, new_path) do + remove_dir_if_empty(old_path, filename) end end - defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true)) + defp create_subdirs(file_path) do + if String.contains?(file_path, "/") do + file_path + |> Path.dirname() + |> File.mkdir_p!() + end + end - @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} - def save_metadata(metadata, %__MODULE__{} = pack) do - pack = Map.put(pack, :pack, metadata) + defp remove_file(pack, shortcode) do + with {:ok, filename} <- get_filename(pack, shortcode), + emoji <- Path.join(pack.path, filename), + :ok <- File.rm(emoji) do + remove_dir_if_empty(emoji, filename) + end + end - with :ok <- save_pack(pack) do - {:ok, pack} + defp remove_dir_if_empty(emoji, filename) do + dir = Path.dirname(emoji) + + if String.contains?(filename, "/") and File.ls!(dir) == [] do + File.rmdir!(dir) + else + :ok end end - @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} - def update_metadata(name, data) do - pack = load_pack(name) + defp get_filename(pack, shortcode) do + with %{^shortcode => filename} when is_binary(filename) <- pack.files, + true <- pack.path |> Path.join(filename) |> File.exists?() do + {:ok, filename} + else + _ -> {:error, :doesnt_exist} + end + end - fb_sha_changed? = - not is_nil(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack[:"fallback-src"] + defp http_get(%URI{} = url), do: url |> to_string() |> http_get() - with {_, true} <- {:update?, fb_sha_changed?}, - {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]), - {:ok, f_list} <- :zip.unzip(zip, [:memory]), - {_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do - fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16() + defp http_get(url) do + with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do + Jason.decode(body) + end + end - data - |> Map.put("fallback-src-sha256", fallback_sha) - |> save_metadata(pack) + defp list_packs_dir do + emoji_path = emoji_path() + # Create the directory first if it does not exist. This is probably the first request made + # with the API so it should be sufficient + with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do + {:ok, results} else - {:update?, _} -> save_metadata(data, pack) - e -> e + {:create_dir, {:error, e}} -> {:error, :create_dir, e} + {:ls, {:error, e}} -> {:error, :ls, e} end end - # Check if all files from the pack.json are in the archive - defp has_all_files?(files, f_list) do - Enum.all?(files, fn {_, from_manifest} -> - List.keyfind(f_list, to_charlist(from_manifest), 0) - end) + defp validate_downloadable(pack) do + if downloadable?(pack), do: :ok, else: {:error, :cant_download} end - @spec load_pack(String.t()) :: t() | nil - def load_pack(name) do - pack_file = Path.join([emoji_path(), name, "pack.json"]) + defp copy_as(remote_pack, local_name) do + path = Path.join(emoji_path(), local_name) - if File.exists?(pack_file) do - pack_file - |> File.read!() - |> from_json() - |> Map.put(:pack_file, pack_file) - |> Map.put(:path, Path.dirname(pack_file)) - |> Map.put(:name, name) + %__MODULE__{ + name: local_name, + path: path, + files: remote_pack["files"], + pack_file: Path.join(path, "pack.json") + } + end + + defp unzip(archive, pack_info, remote_pack, local_pack) do + with :ok <- File.mkdir_p!(local_pack.path) do + files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + # Fallback cannot contain a pack.json file + files = if pack_info[:fallback], do: files, else: ['pack.json' | files] + + :zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files) end end - defp from_json(json) do - map = Jason.decode!(json) + defp fetch_pack_info(remote_pack, uri, name) do + case remote_pack["pack"] do + %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> + {:ok, + %{ + sha: sha, + url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() + }} + + %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> + {:ok, + %{ + sha: sha, + url: src, + fallback: true + }} - struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + _ -> + {:error, "The pack was not set as shared and there is no fallback src to download from"} + end + end + + defp download_archive(url, sha) do + with {:ok, %{body: archive}} <- Tesla.get(url) do + if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do + {:ok, archive} + else + {:error, :imvalid_checksum} + end + end + end + + defp fetch_archive(pack) do + hash = :crypto.hash(:md5, File.read!(pack.pack_file)) + + case Cachex.get!(:emoji_packs_cache, pack.name) do + %{hash: ^hash, pack_data: archive} -> archive + _ -> create_archive_and_cache(pack, hash) + end + end + + defp fallback_sha_changed?(pack, data) do + is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"] + end + + defp update_sha_and_save_metadata(pack, data) do + with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]), + :ok <- validate_has_all_files(pack, zip) do + fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16() + + data + |> Map.put("fallback-src-sha256", fallback_sha) + |> save_metadata(pack) + end end - defp shareable_packs_available?(uri) do - uri - |> URI.merge("/.well-known/nodeinfo") - |> to_string() - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> Map.get("links") - |> List.last() - |> Map.get("href") - # Get the actual nodeinfo address and fetch it - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> get_in(["metadata", "features"]) - |> Enum.member?("shareable_emoji_packs") + defp validate_has_all_files(pack, zip) do + with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do + # Check if all files from the pack.json are in the archive + pack.files + |> Enum.all?(fn {_, from_manifest} -> + List.keyfind(f_list, to_charlist(from_manifest), 0) + end) + |> if(do: :ok, else: {:error, :incomplete}) + end end end diff --git a/lib/pleroma/web/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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do "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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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"}) -- cgit v1.2.3 From aef31c69df0424491a3c0bf45fbf46e2da132580 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 18 May 2020 19:38:22 +0400 Subject: Rename EmojiAPIController to EmojiPackController --- .../api_spec/operations/pleroma_emoji_operation.ex | 390 --------------------- .../operations/pleroma_emoji_pack_operation.ex | 390 +++++++++++++++++++++ .../controllers/emoji_api_controller.ex | 304 ---------------- .../controllers/emoji_pack_controller.ex | 304 ++++++++++++++++ lib/pleroma/web/router.ex | 24 +- 5 files changed, 706 insertions(+), 706 deletions(-) delete mode 100644 lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex delete mode 100644 lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex create mode 100644 lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex deleted file mode 100644 index fc881e657..000000000 --- a/lib/pleroma/web/api_spec/operations/pleroma_emoji_operation.ex +++ /dev/null @@ -1,390 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# 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/api_spec/operations/pleroma_emoji_pack_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex new file mode 100644 index 000000000..439127935 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex @@ -0,0 +1,390 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def remote_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Make request to another instance for emoji packs list", + security: [%{"oAuth" => ["write"]}], + parameters: [url_param()], + operationId: "PleromaAPI.EmojiPackController.remote", + responses: %{ + 200 => emoji_packs_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def index_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Lists local custom emoji packs", + operationId: "PleromaAPI.EmojiPackController.index", + responses: %{ + 200 => emoji_packs_response() + } + } + end + + def show_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Show emoji pack", + operationId: "PleromaAPI.EmojiPackController.show", + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Emoji Pack", "application/json", emoji_pack()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def archive_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Requests a local pack archive from the instance", + operationId: "PleromaAPI.EmojiPackController.archive", + parameters: [name_param()], + responses: %{ + 200 => + Operation.response("Archive file", "application/octet-stream", %Schema{ + type: :string, + format: :binary + }), + 403 => Operation.response("Forbidden", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def download_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Download pack from another instance", + operationId: "PleromaAPI.EmojiPackController.download", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", download_request(), required: true), + responses: %{ + 200 => ok_response(), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp download_request do + %Schema{ + type: :object, + required: [:url, :name], + properties: %{ + url: %Schema{ + type: :string, + format: :uri, + description: "URL of the instance to download from" + }, + name: %Schema{type: :string, format: :uri, description: "Pack Name"}, + as: %Schema{type: :string, format: :uri, description: "Save as"} + } + } + end + + def create_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Create an empty pack", + operationId: "PleromaAPI.EmojiPackController.create", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Not Found", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError), + 500 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete a custom emoji pack", + operationId: "PleromaAPI.EmojiPackController.delete", + security: [%{"oAuth" => ["write"]}], + parameters: [name_param()], + responses: %{ + 200 => ok_response(), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 404 => Operation.response("Not Found", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Updates (replaces) pack metadata", + operationId: "PleromaAPI.EmojiPackController.update", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Metadata", "application/json", metadata()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def add_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Add new file to the pack", + operationId: "PleromaAPI.EmojiPackController.add_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", add_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp add_file_request do + %Schema{ + type: :object, + required: [:file], + properties: %{ + file: %Schema{ + description: + "File needs to be uploaded with the multipart request or link to remote file", + anyOf: [ + %Schema{type: :string, format: :binary}, + %Schema{type: :string, format: :uri} + ] + }, + shortcode: %Schema{ + type: :string, + description: + "Shortcode for new emoji, must be 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.EmojiPackController.update_file", + security: [%{"oAuth" => ["write"]}], + requestBody: request_body("Parameters", update_file_request(), required: true), + parameters: [name_param()], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError), + 409 => Operation.response("Conflict", "application/json", ApiError) + } + } + end + + defp update_file_request do + %Schema{ + type: :object, + required: [:shortcode, :new_shortcode, :new_filename], + properties: %{ + shortcode: %Schema{ + type: :string, + description: "Emoji file shortcode" + }, + new_shortcode: %Schema{ + type: :string, + description: "New emoji file shortcode" + }, + new_filename: %Schema{ + type: :string, + description: "New filename for emoji file" + }, + force: %Schema{ + type: :boolean, + description: "With true value to overwrite existing emoji with new shortcode", + default: false + } + } + } + end + + def delete_file_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Delete emoji file from pack", + operationId: "PleromaAPI.EmojiPackController.delete_file", + security: [%{"oAuth" => ["write"]}], + parameters: [ + name_param(), + Operation.parameter(:shortcode, :query, :string, "File shortcode", + example: "cofe", + required: true + ) + ], + responses: %{ + 200 => Operation.response("Files Object", "application/json", files_object()), + 400 => Operation.response("Bad Request", "application/json", ApiError) + } + } + end + + def import_from_filesystem_operation do + %Operation{ + tags: ["Emoji Packs"], + summary: "Imports packs from filesystem", + operationId: "PleromaAPI.EmojiPackController.import", + security: [%{"oAuth" => ["write"]}], + responses: %{ + 200 => + Operation.response("Array of imported pack names", "application/json", %Schema{ + type: :array, + items: %Schema{type: :string} + }) + } + } + end + + defp name_param do + Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true) + end + + defp url_param do + Operation.parameter( + :url, + :query, + %Schema{type: :string, format: :uri}, + "URL of the instance", + required: true + ) + end + + defp ok_response do + Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"}) + end + + defp emoji_packs_response do + Operation.response( + "Object with pack names as keys and pack contents as values", + "application/json", + %Schema{ + type: :object, + additionalProperties: emoji_pack(), + example: %{ + "emojos" => emoji_pack().example + } + } + ) + end + + defp emoji_pack do + %Schema{ + title: "EmojiPack", + type: :object, + properties: %{ + files: files_object(), + pack: %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "can-download": %Schema{type: :boolean}, + "share-files": %Schema{type: :boolean}, + "download-sha256": %Schema{type: :string} + } + } + }, + example: %{ + "files" => %{"emacs" => "emacs.png", "guix" => "guix.png"}, + "pack" => %{ + "license" => "Test license", + "homepage" => "https://pleroma.social", + "description" => "Test description", + "can-download" => true, + "share-files" => true, + "download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238" + } + } + } + end + + defp files_object do + %Schema{ + type: :object, + additionalProperties: %Schema{type: :string}, + description: "Object with emoji names as keys and filenames as values" + } + end + + defp update_request do + %Schema{ + type: :object, + properties: %{ + metadata: %Schema{ + type: :object, + description: "Metadata to replace the old one", + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + } + } + end + + defp metadata do + %Schema{ + type: :object, + properties: %{ + license: %Schema{type: :string}, + homepage: %Schema{type: :string, format: :uri}, + description: %Schema{type: :string}, + "fallback-src": %Schema{ + type: :string, + format: :uri, + description: "Fallback url to download pack from" + }, + "fallback-src-sha256": %Schema{ + type: :string, + description: "SHA256 encoded for fallback pack archive" + }, + "share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"} + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex deleted file mode 100644 index 834fc717e..000000000 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ /dev/null @@ -1,304 +0,0 @@ -defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do - use Pleroma.Web, :controller - - alias Pleroma.Emoji.Pack - - plug(Pleroma.Web.ApiSpec.CastAndValidate) - - plug( - Pleroma.Plugs.OAuthScopesPlug, - %{scopes: ["write"], admin: true} - when action in [ - :import_from_filesystem, - :remote, - :download, - :create, - :update, - :delete, - :add_file, - :update_file, - :delete_file - ] - ) - - @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] - plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list]) - - defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiOperation - - def remote(conn, %{url: url}) do - with {:ok, packs} <- Pack.list_remote(url) do - json(conn, packs) - else - {:error, :not_shareable} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) - end - end - - def index(conn, _params) do - emoji_path = - [:instance, :static_dir] - |> Pleroma.Config.get!() - |> Path.join("emoji") - - with {:ok, packs} <- Pack.list_local() do - json(conn, packs) - else - {:error, :create_dir, e} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) - - {:error, :ls, e} -> - conn - |> put_status(:internal_server_error) - |> json(%{ - error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}" - }) - end - end - - def show(conn, %{name: name}) do - name = String.trim(name) - - with {:ok, pack} <- Pack.show(name) do - json(conn, pack) - else - {:error, :not_found} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - end - end - - def archive(conn, %{name: name}) do - with {:ok, archive} <- Pack.get_archive(name) do - send_download(conn, {:binary, archive}, filename: "#{name}.zip") - else - {:error, :cant_download} -> - conn - |> put_status(:forbidden) - |> json(%{ - error: - "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" - }) - - {:error, :not_found} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - end - end - - def download(%{body_params: %{url: url, name: name} = params} = conn, _) do - with {:ok, _pack} <- Pack.download(name, url, params[:as]) do - json(conn, "ok") - else - {:error, :not_shareable} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) - - {:error, :imvalid_checksum} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) - - {:error, e} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: e}) - end - end - - def create(conn, %{name: name}) do - name = String.trim(name) - - with {:ok, _pack} <- Pack.create(name) do - json(conn, "ok") - else - {:error, :eexist} -> - conn - |> put_status(:conflict) - |> json(%{error: "A pack named \"#{name}\" already exists"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while creating pack." - ) - end - end - - def delete(conn, %{name: name}) do - name = String.trim(name) - - with {:ok, deleted} when deleted != [] <- Pack.delete(name) do - json(conn, "ok") - else - {:ok, []} -> - conn - |> put_status(:not_found) - |> json(%{error: "Pack #{name} does not exist"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name cannot be empty"}) - - {:error, _, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Couldn't delete the pack #{name}"}) - end - end - - def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do - with {:ok, pack} <- Pack.update_metadata(name, metadata) do - json(conn, pack.pack) - else - {:error, :incomplete} -> - conn - |> put_status(:bad_request) - |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating pack metadata." - ) - end - end - - def add_file(%{body_params: params} = conn, %{name: name}) do - filename = params[:filename] || get_filename(params[:file]) - shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) - - with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do - json(conn, pack.files) - else - {:error, :already_exists} -> - conn - |> put_status(:conflict) - |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name, shortcode or filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while adding file to pack." - ) - end - end - - def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do - new_shortcode = params[:new_shortcode] - new_filename = params[:new_filename] - force = params[:force] - - with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do - json(conn, pack.files) - else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:error, :already_exists} -> - conn - |> put_status(:conflict) - |> json(%{ - error: - "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" - }) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_filename cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while updating file in pack." - ) - end - end - - def delete_file(conn, %{name: name, shortcode: shortcode}) do - with {:ok, pack} <- Pack.delete_file(name, shortcode) do - json(conn, pack.files) - else - {:error, :doesnt_exist} -> - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - - {:error, :not_found} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack \"#{name}\" is not found"}) - - {:error, :empty_values} -> - conn - |> put_status(:bad_request) - |> json(%{error: "pack name or shortcode cannot be empty"}) - - {:error, _} -> - render_error( - conn, - :internal_server_error, - "Unexpected error occurred while removing file from pack." - ) - end - end - - def import_from_filesystem(conn, _params) do - with {:ok, names} <- Pack.import_from_filesystem() do - json(conn, names) - else - {:error, :no_read_write} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Error: emoji pack directory must be writable"}) - - {:error, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "Error accessing emoji pack directory"}) - end - end - - defp get_filename(%Plug.Upload{filename: filename}), do: filename - defp get_filename(url) when is_binary(url), do: Path.basename(url) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex new file mode 100644 index 000000000..2c53dcde1 --- /dev/null +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_pack_controller.ex @@ -0,0 +1,304 @@ +defmodule Pleroma.Web.PleromaAPI.EmojiPackController do + use Pleroma.Web, :controller + + alias Pleroma.Emoji.Pack + + plug(Pleroma.Web.ApiSpec.CastAndValidate) + + plug( + Pleroma.Plugs.OAuthScopesPlug, + %{scopes: ["write"], admin: true} + when action in [ + :import_from_filesystem, + :remote, + :download, + :create, + :update, + :delete, + :add_file, + :update_file, + :delete_file + ] + ) + + @skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] + plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list]) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation + + def remote(conn, %{url: url}) do + with {:ok, packs} <- Pack.list_remote(url) do + json(conn, packs) + else + {:error, :not_shareable} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) + end + end + + def index(conn, _params) do + emoji_path = + [:instance, :static_dir] + |> Pleroma.Config.get!() + |> Path.join("emoji") + + with {:ok, packs} <- Pack.list_local() do + json(conn, packs) + else + {:error, :create_dir, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) + + {:error, :ls, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{ + error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}" + }) + end + end + + def show(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, pack} <- Pack.show(name) do + json(conn, pack) + else + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + end + end + + def archive(conn, %{name: name}) do + with {:ok, archive} <- Pack.get_archive(name) do + send_download(conn, {:binary, archive}, filename: "#{name}.zip") + else + {:error, :cant_download} -> + conn + |> put_status(:forbidden) + |> json(%{ + error: + "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" + }) + + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + end + end + + def download(%{body_params: %{url: url, name: name} = params} = conn, _) do + with {:ok, _pack} <- Pack.download(name, url, params[:as]) do + json(conn, "ok") + else + {:error, :not_shareable} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) + + {:error, :imvalid_checksum} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) + + {:error, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: e}) + end + end + + def create(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, _pack} <- Pack.create(name) do + json(conn, "ok") + else + {:error, :eexist} -> + conn + |> put_status(:conflict) + |> json(%{error: "A pack named \"#{name}\" already exists"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while creating pack." + ) + end + end + + def delete(conn, %{name: name}) do + name = String.trim(name) + + with {:ok, deleted} when deleted != [] <- Pack.delete(name) do + json(conn, "ok") + else + {:ok, []} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) + + {:error, _, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Couldn't delete the pack #{name}"}) + end + end + + def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do + with {:ok, pack} <- Pack.update_metadata(name, metadata) do + json(conn, pack.pack) + else + {:error, :incomplete} -> + conn + |> put_status(:bad_request) + |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating pack metadata." + ) + end + end + + def add_file(%{body_params: params} = conn, %{name: name}) do + filename = params[:filename] || get_filename(params[:file]) + shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename)) + + with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do + json(conn, pack.files) + else + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name, shortcode or filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while adding file to pack." + ) + end + end + + def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do + new_shortcode = params[:new_shortcode] + new_filename = params[:new_filename] + force = params[:force] + + with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :already_exists} -> + conn + |> put_status(:conflict) + |> json(%{ + error: + "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" + }) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating file in pack." + ) + end + end + + def delete_file(conn, %{name: name, shortcode: shortcode}) do + with {:ok, pack} <- Pack.delete_file(name, shortcode) do + json(conn, pack.files) + else + {:error, :doesnt_exist} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + + {:error, :not_found} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{name}\" is not found"}) + + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name or shortcode cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while removing file from pack." + ) + end + end + + def import_from_filesystem(conn, _params) do + with {:ok, names} <- Pack.import_from_filesystem() do + json(conn, names) + else + {:error, :no_read_write} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Error: emoji pack directory must be writable"}) + + {:error, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "Error accessing emoji pack directory"}) + end + end + + defp get_filename(%Plug.Upload{filename: filename}), do: filename + defp get_filename(url) when is_binary(url), do: Path.basename(url) +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex 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 -- cgit v1.2.3 From 45c3a7240449133176bf27bd2f753bb71d7f455b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 18 May 2020 20:58:59 +0400 Subject: [OpenAPI] Use BooleanLike in all request bodies --- .../web/api_spec/operations/account_operation.ex | 28 +++++++++++----------- .../web/api_spec/operations/filter_operation.ex | 7 +++--- .../web/api_spec/operations/report_operation.ex | 3 ++- .../web/api_spec/operations/status_operation.ex | 9 +++---- .../api_spec/operations/subscription_operation.ex | 21 ++++++++-------- 5 files changed, 36 insertions(+), 32 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 934f6038e..20572f8ea 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -393,7 +393,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do format: :password }, agreement: %Schema{ - type: :boolean, + allOf: [BooleanLike], description: "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE." }, @@ -463,7 +463,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ bot: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Whether the account has a bot flag." }, @@ -486,7 +486,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do format: :binary }, locked: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Whether manual approval of follow requests is required." }, @@ -510,37 +510,37 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do # Pleroma-specific fields no_rich_text: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "html tags are stripped from all statuses requested from the API" }, hide_followers: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's followers will be hidden" }, hide_follows: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's follows will be hidden" }, hide_followers_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's follower count will be hidden" }, hide_follows_count: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's follow count will be hidden" }, hide_favorites: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's favorites timeline will be hidden" }, show_role: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "user's role (e.g admin, moderator) will be exposed to anyone in the API" @@ -552,12 +552,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do description: "Opaque user settings to be saved on the backend." }, skip_thread_containment: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Skip filtering out broken threads" }, allow_following_move: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Allows automatically follow moved following accounts" }, @@ -568,7 +568,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do format: :binary }, discoverable: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Discovery of this account in search results and other services is allowed." @@ -678,7 +678,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do type: :object, properties: %{ notifications: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Mute notifications in addition to statuses? Defaults to true.", default: true diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex index 7310c1c4d..31e576f99 100644 --- a/lib/pleroma/web/api_spec/operations/filter_operation.ex +++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -171,7 +172,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do type: :object, properties: %{ irreversible: %Schema{ - type: :bolean, + allOf: [BooleanLike], description: "Should the server irreversibly drop matching entities from home and notifications?", default: false @@ -199,13 +200,13 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." }, irreversible: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Should the server irreversibly drop matching entities from home and notifications?" }, whole_word: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Consider word boundaries?", default: true diff --git a/lib/pleroma/web/api_spec/operations/report_operation.ex b/lib/pleroma/web/api_spec/operations/report_operation.ex index 882177c96..b9b4c4f79 100644 --- a/lib/pleroma/web/api_spec/operations/report_operation.ex +++ b/lib/pleroma/web/api_spec/operations/report_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -47,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do description: "Reason for the report" }, forward: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, default: false, description: diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 4b284c537..0682ca6e5 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.AccountOperation alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus alias Pleroma.Web.ApiSpec.Schemas.Status @@ -394,12 +395,12 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do "Duration the poll should be open, in seconds. Must be provided with `poll[options]`" }, multiple: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Allow multiple choices?" }, hide_totals: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Hide vote counts until the poll ends?" } @@ -411,7 +412,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do description: "ID of the status being replied to, if status is a reply" }, sensitive: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Mark status and attached media as sensitive?" }, @@ -435,7 +436,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do }, # Pleroma-specific properties: preview: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example" diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex index cf6dcb068..c575a87e6 100644 --- a/lib/pleroma/web/api_spec/operations/subscription_operation.ex +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.BooleanLike alias Pleroma.Web.ApiSpec.Schemas.PushSubscription def open_api_operation(action) do @@ -117,27 +118,27 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do type: :object, properties: %{ follow: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive follow notifications?" }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive favourite notifications?" }, reblog: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive reblog notifications?" }, mention: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive mention notifications?" }, poll: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive poll notifications?" } @@ -181,27 +182,27 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do type: :object, properties: %{ follow: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive follow notifications?" }, favourite: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive favourite notifications?" }, reblog: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive reblog notifications?" }, mention: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive mention notifications?" }, poll: %Schema{ - type: :boolean, + allOf: [BooleanLike], nullable: true, description: "Receive poll notifications?" } -- cgit v1.2.3 From fe5d423b43d6d4c4cd63aaee27b8aa0966441e02 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 18 May 2020 22:00:32 +0400 Subject: Add OpenAPI spec for MascotController --- .../operations/pleroma_mascot_operation.ex | 79 ++++++++++++++++++++++ .../pleroma_api/controllers/mascot_controller.ex | 5 +- 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex new file mode 100644 index 000000000..8c5f37ea6 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_mascot_operation.ex @@ -0,0 +1,79 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation 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 show_operation do + %Operation{ + tags: ["Mascot"], + summary: "Gets user mascot image", + security: [%{"oAuth" => ["read:accounts"]}], + operationId: "PleromaAPI.MascotController.show", + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()) + } + } + end + + def update_operation do + %Operation{ + tags: ["Mascot"], + summary: "Set/clear user avatar image", + description: + "Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.", + operationId: "PleromaAPI.MascotController.update", + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + file: %Schema{type: :string, format: :binary} + } + }, + required: true + ), + security: [%{"oAuth" => ["write:accounts"]}], + responses: %{ + 200 => Operation.response("Mascot", "application/json", mascot()), + 415 => Operation.response("Unsupported Media Type", "application/json", ApiError) + } + } + end + + defp mascot do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + url: %Schema{type: :string, format: :uri}, + type: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + mime_type: %Schema{type: :string} + } + } + }, + example: %{ + "id" => "abcdefg", + "url" => "https://pleroma.example.org/media/abcdefg.png", + "type" => "image", + "pleroma" => %{ + "mime_type" => "image/png" + } + } + } + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex index d4e0d8b7c..df6c50ca5 100644 --- a/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex @@ -9,16 +9,19 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show) plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation + @doc "GET /api/v1/pleroma/mascot" def show(%{assigns: %{user: user}} = conn, _params) do json(conn, User.get_mascot(user)) end @doc "PUT /api/v1/pleroma/mascot" - def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do + def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), # Reject if not an image %{type: "image"} = attachment <- render_attachment(object) do -- cgit v1.2.3 From 5f0a3ac74d51333a778e6be26876fe26b0ff625b Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Mon, 18 May 2020 09:22:26 +0300 Subject: added tests --- lib/pleroma/web/media_proxy/invalidation.ex | 8 +++++- lib/pleroma/web/media_proxy/invalidations/http.ex | 26 ++++++++++++++++++- .../web/media_proxy/invalidations/script.ex | 30 ++++++++++++++++++++-- 3 files changed, 60 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/invalidation.ex b/lib/pleroma/web/media_proxy/invalidation.ex index 371aa8ae0..c037ff13e 100644 --- a/lib/pleroma/web/media_proxy/invalidation.ex +++ b/lib/pleroma/web/media_proxy/invalidation.ex @@ -1,8 +1,15 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.Invalidation do + @moduledoc false + @callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()} alias Pleroma.Config + @spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()} def purge(urls) do [:media_proxy, :invalidation, :enabled] |> Config.get() @@ -13,7 +20,6 @@ defmodule Pleroma.Web.MediaProxy.Invalidation do provider = Config.get([:media_proxy, :invalidation, :provider]) options = Config.get(provider) provider.purge(urls, options) - :ok end defp do_purge(_, _), do: :ok diff --git a/lib/pleroma/web/media_proxy/invalidations/http.ex b/lib/pleroma/web/media_proxy/invalidations/http.ex index 66fafa7ba..07248df6e 100644 --- a/lib/pleroma/web/media_proxy/invalidations/http.ex +++ b/lib/pleroma/web/media_proxy/invalidations/http.ex @@ -1,16 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.Invalidation.Http do + @moduledoc false @behaviour Pleroma.Web.MediaProxy.Invalidation + require Logger + @impl Pleroma.Web.MediaProxy.Invalidation def purge(urls, opts) do method = Map.get(opts, :method, :purge) headers = Map.get(opts, :headers, []) options = Map.get(opts, :options, []) + Logger.debug("Running cache purge: #{inspect(urls)}") + Enum.each(urls, fn url -> - Pleroma.HTTP.request(method, url, "", headers, options) + with {:error, error} <- do_purge(method, url, headers, options) do + Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}") + end end) {:ok, "success"} end + + defp do_purge(method, url, headers, options) do + case Pleroma.HTTP.request(method, url, "", headers, options) do + {:ok, %{status: status} = env} when 400 <= status and status < 500 -> + {:error, env} + + {:error, error} = error -> + error + + _ -> + {:ok, "success"} + end + end end diff --git a/lib/pleroma/web/media_proxy/invalidations/script.ex b/lib/pleroma/web/media_proxy/invalidations/script.ex index 94c79511a..6be782132 100644 --- a/lib/pleroma/web/media_proxy/invalidations/script.ex +++ b/lib/pleroma/web/media_proxy/invalidations/script.ex @@ -1,6 +1,14 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MediaProxy.Invalidation.Script do + @moduledoc false + @behaviour Pleroma.Web.MediaProxy.Invalidation + require Logger + @impl Pleroma.Web.MediaProxy.Invalidation def purge(urls, %{script_path: script_path} = _options) do args = @@ -9,7 +17,25 @@ defmodule Pleroma.Web.MediaProxy.Invalidation.Script do |> Enum.uniq() |> Enum.join(" ") - System.cmd(Path.expand(script_path), [args]) - {:ok, "success"} + path = Path.expand(script_path) + + Logger.debug("Running cache purge: #{inspect(urls)}, #{path}") + + case do_purge(path, [args]) do + {result, exit_status} when exit_status > 0 -> + Logger.error("Error while cache purge: #{inspect(result)}") + {:error, inspect(result)} + + _ -> + {:ok, "success"} + end + end + + def purge(_, _), do: {:error, "not found script path"} + + defp do_purge(path, args) do + System.cmd(path, args) + rescue + error -> {inspect(error), 1} end end -- cgit v1.2.3 From 41f8f172609910efc9543632fecbd544d131535b Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 19 May 2020 09:42:41 +0300 Subject: fix api/v1/accounts/update_credentials --- lib/pleroma/web/mastodon_api/controllers/account_controller.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index ef41f9e96..75512442d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -177,6 +177,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do ) |> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store) |> add_if_present(params, :default_scope, :default_scope) + |> add_if_present(params["source"], "privacy", :default_scope) |> add_if_present(params, :actor_type, :actor_type) changeset = User.update_changeset(user, user_params) @@ -189,7 +190,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do end defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do - with true <- Map.has_key?(params, params_field), + with true <- is_map(params), + true <- Map.has_key?(params, params_field), {:ok, new_value} <- value_function.(Map.get(params, params_field)) do Map.put(map, map_field, new_value) else -- cgit v1.2.3 From e4c720f14c0760ff5863c58a2ed1aafb9bf1bdc5 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 14:59:50 +0400 Subject: Fix typo --- lib/pleroma/web/api_spec/operations/pleroma_emoji_pack_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') 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 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation 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, -- cgit v1.2.3 From a985bd57b4f0b33639d20ce7db8dc8c574bfff67 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 19 May 2020 14:11:32 +0200 Subject: User.Query: Speed up recipients query. --- lib/pleroma/user.ex | 4 +++- lib/pleroma/user/query.ex | 25 ++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index cba391072..6ca1e9a79 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1204,7 +1204,9 @@ defmodule Pleroma.User do def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do to = [actor | to] - User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) + + query |> Repo.all() end diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 3a3b04793..9ef073dff 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -167,20 +167,19 @@ defmodule Pleroma.User.Query do end defp compose_query({:recipients_from_activity, to}, query) do - query - |> join(:left, [u], r in FollowingRelationship, - as: :relationships, - on: r.follower_id == u.id - ) - |> join(:left, [relationships: r], f in User, - as: :following, - on: f.id == r.following_id - ) - |> where( - [u, following: f, relationships: r], - u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept) + following_query = + from(u in User, + join: f in FollowingRelationship, + on: u.id == f.following_id, + where: f.state == ^:follow_accept, + where: u.follower_address in ^to, + select: f.follower_id + ) + + from(u in query, + where: u.ap_id in ^to or u.id in subquery(following_query), + distinct: true ) - |> distinct(true) end defp compose_query({:order_by, key}, query) do -- cgit v1.2.3 From 524d04d9218f8e72bf88ab5e7d4b407452ae40bc Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 15:53:18 +0400 Subject: Add OpenAPI spec for PleromaAPI.ScrobbleController --- .../operations/pleroma_scrobble_operation.ex | 102 +++++++++++++++++++++ lib/pleroma/web/common_api/common_api.ex | 11 ++- .../pleroma_api/controllers/scrobble_controller.ex | 27 +++--- lib/pleroma/web/router.ex | 4 +- 4 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex new file mode 100644 index 000000000..779b8f84c --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -0,0 +1,102 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Reference + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Gets user mascot image", + security: [%{"oAuth" => ["write"]}], + operationId: "PleromaAPI.ScrobbleController.create", + requestBody: request_body("Parameters", create_request(), requried: true), + responses: %{ + 200 => Operation.response("Scrobble", "application/json", scrobble()) + } + } + end + + def index_operation do + %Operation{ + tags: ["Scrobbles"], + summary: "Requests a list of current and recent Listen activities for an account", + operationId: "PleromaAPI.ScrobbleController.index", + parameters: [ + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params() + ], + security: [%{"oAuth" => ["read"]}], + responses: %{ + 200 => + Operation.response("Array of Scrobble", "application/json", %Schema{ + type: :array, + items: scrobble() + }) + } + } + end + + defp create_request do + %Schema{ + type: :object, + required: [:title], + properties: %{ + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{type: :integer, description: "The length of the media playing"}, + visibility: %Schema{ + allOf: [VisibilityScope], + default: "public", + description: "Scrobble visibility" + } + }, + example: %{ + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000 + } + } + end + + defp scrobble do + %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string}, + account: Account, + title: %Schema{type: :string, description: "The title of the media playing"}, + album: %Schema{type: :string, description: "The album of the media playing"}, + artist: %Schema{type: :string, description: "The artist of the media playing"}, + length: %Schema{ + type: :integer, + description: "The length of the media playing", + nullable: true + }, + created_at: %Schema{type: :string, format: :"date-time"} + }, + example: %{ + "id" => "1234", + "account" => Account.schema().example, + "title" => "Some Title", + "artist" => "Some Artist", + "album" => "Some Album", + "length" => 180_000, + "created_at" => "2019-09-28T12:40:45.000Z" + } + } + end +end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 7c94f16b6..447dbe4e6 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -347,11 +347,14 @@ defmodule Pleroma.Web.CommonAPI do |> check_expiry_date() end - def listen(user, %{"title" => _} = data) do - with visibility <- data["visibility"] || "public", - {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), + def listen(user, data) do + visibility = Map.get(data, :visibility, "public") + + with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil), listen_data <- - Map.take(data, ["album", "artist", "title", "length"]) + data + |> Map.take([:album, :artist, :title, :length]) + |> Map.new(fn {key, value} -> {to_string(key), value} end) |> Map.put("type", "Audio") |> Map.put("to", to) |> Map.put("cc", cc) diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 22da6c0ad..35a37f69e 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2] + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2] alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User @@ -13,22 +13,18 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.StatusView + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( OAuthScopesPlug, - %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles + %{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index ) - plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles) + plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create) - def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do - params = - if !params["length"] do - params - else - params - |> Map.put("length", fetch_integer_param(params, "length")) - end + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation + def create(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, activity} <- CommonAPI.listen(user, params) do conn |> put_view(StatusView) @@ -41,9 +37,12 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do end end - def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do - params = Map.put(params, "type", ["Listen"]) + def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do + params = + params + |> Map.new(fn {key, value} -> {to_string(key), value} end) + |> Map.put("type", ["Listen"]) activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d77a61361..369c54cf4 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -325,7 +325,7 @@ defmodule Pleroma.Web.Router do get("/mascot", MascotController, :show) put("/mascot", MascotController, :update) - post("/scrobble", ScrobbleController, :new_scrobble) + post("/scrobble", ScrobbleController, :create) end scope [] do @@ -345,7 +345,7 @@ defmodule Pleroma.Web.Router do scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do pipe_through(:api) - get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles) + get("/accounts/:id/scrobbles", ScrobbleController, :index) end scope "/api/v1", Pleroma.Web.MastodonAPI do -- cgit v1.2.3 From 6609714d6694058e28ed789dd65eb50ca816c425 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 16:11:59 +0400 Subject: Move Scrobble views to ScrobbleView --- lib/pleroma/web/mastodon_api/views/status_view.ex | 21 ------------ .../pleroma_api/controllers/scrobble_controller.ex | 8 ++--- lib/pleroma/web/pleroma_api/views/scrobble_view.ex | 37 ++++++++++++++++++++++ 3 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 lib/pleroma/web/pleroma_api/views/scrobble_view.ex (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 05a26017a..8e3715093 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -436,27 +436,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end - def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do - object = Object.normalize(activity) - - user = get_user(activity.data["actor"]) - created_at = Utils.to_masto_date(activity.data["published"]) - - %{ - id: activity.id, - account: AccountView.render("show.json", %{user: user, for: opts[:for]}), - created_at: created_at, - title: object.data["title"] |> HTML.strip_tags(), - artist: object.data["artist"] |> HTML.strip_tags(), - album: object.data["album"] |> HTML.strip_tags(), - length: object.data["length"] - } - end - - def render("listens.json", opts) do - safe_render_many(opts.activities, StatusView, "listen.json", opts) - end - def render("context.json", %{activity: activity, activities: activities, user: user}) do %{ancestors: ancestors, descendants: descendants} = activities diff --git a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex index 35a37f69e..8665ca56c 100644 --- a/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.MastodonAPI.StatusView plug(Pleroma.Web.ApiSpec.CastAndValidate) @@ -26,9 +25,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do def create(%{assigns: %{user: user}, body_params: params} = conn, _) do with {:ok, activity} <- CommonAPI.listen(user, params) do - conn - |> put_view(StatusView) - |> render("listen.json", %{activity: activity, for: user}) + render(conn, "show.json", activity: activity, for: user) else {:error, message} -> conn @@ -48,8 +45,7 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleController do conn |> add_link_headers(activities) - |> put_view(StatusView) - |> render("listens.json", %{ + |> render("index.json", %{ activities: activities, for: reading_user, as: :activity diff --git a/lib/pleroma/web/pleroma_api/views/scrobble_view.ex b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex new file mode 100644 index 000000000..bbff93abe --- /dev/null +++ b/lib/pleroma/web/pleroma_api/views/scrobble_view.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.ScrobbleView do + use Pleroma.Web, :view + + require Pleroma.Constants + + alias Pleroma.Activity + alias Pleroma.HTML + alias Pleroma.Object + alias Pleroma.Web.CommonAPI.Utils + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do + object = Object.normalize(activity) + + user = StatusView.get_user(activity.data["actor"]) + created_at = Utils.to_masto_date(activity.data["published"]) + + %{ + id: activity.id, + account: AccountView.render("show.json", %{user: user, for: opts[:for]}), + created_at: created_at, + title: object.data["title"] |> HTML.strip_tags(), + artist: object.data["artist"] |> HTML.strip_tags(), + album: object.data["album"] |> HTML.strip_tags(), + length: object.data["length"] + } + end + + def render("index.json", opts) do + safe_render_many(opts.activities, __MODULE__, "show.json", opts) + end +end -- cgit v1.2.3 From be322541c8a3de1b6311bda340e5af151fe28c6c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 19 May 2020 14:36:34 +0200 Subject: User.Query: Remove superfluous `distinct` --- lib/pleroma/user/query.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index 9ef073dff..293bbc082 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -177,8 +177,7 @@ defmodule Pleroma.User.Query do ) from(u in query, - where: u.ap_id in ^to or u.id in subquery(following_query), - distinct: true + where: u.ap_id in ^to or u.id in subquery(following_query) ) end -- cgit v1.2.3 From 47ed9ee4411613cac81235f9751a8ccd0974e927 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 19 May 2020 17:29:58 +0400 Subject: Fix summary --- lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex index 779b8f84c..85a22aa0b 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_scrobble_operation.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do def create_operation do %Operation{ tags: ["Scrobbles"], - summary: "Gets user mascot image", + summary: "Creates a new Listen activity for an account", security: [%{"oAuth" => ["write"]}], operationId: "PleromaAPI.ScrobbleController.create", requestBody: request_body("Parameters", create_request(), requried: true), -- cgit v1.2.3 From 8a6ddf26ac63f63f68ad065392dc7f8fde2215d4 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 20 May 2020 12:12:07 +0200 Subject: InstanceOperation: Add background image to example --- lib/pleroma/web/api_spec/operations/instance_operation.ex | 1 + 1 file changed, 1 insertion(+) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 880bd3f1b..9d189d029 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -130,6 +130,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do example: %{ "avatar_upload_limit" => 2_000_000, "background_upload_limit" => 4_000_000, + "background_image" => "/static/image.png", "banner_upload_limit" => 4_000_000, "description" => "A Pleroma instance, an alternative fediverse server", "email" => "lain@lain.com", -- cgit v1.2.3 From 26f01744bcabf36aed01670254b3cc724758f7ca Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 20 May 2020 15:18:58 +0400 Subject: Add `background_image` to `InstanceOperation` --- lib/pleroma/web/api_spec/operations/instance_operation.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 9d189d029..d5c335d0c 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -125,7 +125,12 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do }, avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"}, background_upload_limit: %Schema{type: :integer, description: "The title of the website"}, - banner_upload_limit: %Schema{type: :integer, description: "The title of the website"} + banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}, + background_image: %Schema{ + type: :string, + format: :uri, + description: "The background image for the website" + } }, example: %{ "avatar_upload_limit" => 2_000_000, -- cgit v1.2.3