From cb363f018380cceb9531e0ddd12a979b8accc0b2 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Wed, 29 Apr 2020 17:38:14 +0200 Subject: [PATCH 1/4] MastodonAPI: /api/v2/media endpoints --- .../controllers/media_controller.ex | 31 ++++++++++ lib/pleroma/web/router.ex | 3 + .../controllers/media_controller_test.exs | 62 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index e36751220..1997ac1af 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -29,6 +29,26 @@ def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do end end + def create(_conn, _data), do: {:error, :bad_request} + + @doc "POST /api/v2/media" + def create2(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + with {:ok, object} <- + ActivityPub.upload( + file, + actor: User.ap_id(user), + description: Map.get(data, "description") + ) do + attachment_data = Map.put(object.data, "id", object.id) + + conn + |> put_status(202) + |> render("attachment.json", %{attachment: attachment_data}) + end + end + + def create2(_conn, _data), do: {:error, :bad_request} + @doc "PUT /api/v1/media/:id" def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description}) when is_binary(description) do @@ -42,4 +62,15 @@ def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => desc end def update(_conn, _data), do: {:error, :bad_request} + + @doc "GET /api/v1/media/:id" + def show(conn, %{"id" => id}) do + with %Object{data: data, id: object_id} <- Object.get_by_id(id) do + attachment_data = Map.put(data, "id", object_id) + + render(conn, "attachment.json", %{attachment: attachment_data}) + end + end + + def get_media(_conn, _data), do: {:error, :bad_request} end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 7a171f9fb..d77a61361 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -403,6 +403,7 @@ defmodule Pleroma.Web.Router do post("/markers", MarkerController, :upsert) post("/media", MediaController, :create) + get("/media/:id", MediaController, :show) put("/media/:id", MediaController, :update) get("/notifications", NotificationController, :index) @@ -497,6 +498,8 @@ defmodule Pleroma.Web.Router do scope "/api/v2", Pleroma.Web.MastodonAPI do pipe_through(:api) get("/search", SearchController, :search2) + + post("/media", MediaController, :create2) end scope "/api", Pleroma.Web do diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs index 6ac4cf63b..d872ff484 100644 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do setup do: oauth_access(["write:media"]) - describe "media upload" do + describe "Upload media" do setup do image = %Plug.Upload{ content_type: "image/jpg", @@ -25,7 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do setup do: clear_config([:media_proxy]) setup do: clear_config([Pleroma.Upload]) - test "returns uploaded image", %{conn: conn, image: image} do + test "/api/v1/media", %{conn: conn, image: image} do desc = "Description of the image" media = @@ -40,9 +40,31 @@ test "returns uploaded image", %{conn: conn, image: image} do object = Object.get_by_id(media["id"]) assert object.data["actor"] == User.ap_id(conn.assigns[:user]) end + + test "/api/v2/media", %{conn: conn, image: image} do + desc = "Description of the image" + + response = + conn + |> post("/api/v2/media", %{"file" => image, "description" => desc}) + |> json_response(202) + + assert media_id = response["id"] + + media = + conn + |> get("/api/v1/media/#{media_id}") + |> json_response(200) + + assert media["type"] == "image" + assert media["description"] == desc + assert media["id"] + object = Object.get_by_id(media["id"]) + assert object.data["actor"] == User.ap_id(conn.assigns[:user]) + end end - describe "PUT /api/v1/media/:id" do + describe "Update media description" do setup %{user: actor} do file = %Plug.Upload{ content_type: "image/jpg", @@ -60,7 +82,7 @@ test "returns uploaded image", %{conn: conn, image: image} do [object: object] end - test "updates name of media", %{conn: conn, object: object} do + test "/api/v1/media/:id good request", %{conn: conn, object: object} do media = conn |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) @@ -70,7 +92,7 @@ test "updates name of media", %{conn: conn, object: object} do assert refresh_record(object).data["name"] == "test-media" end - test "returns error when request is bad", %{conn: conn, object: object} do + test "/api/v1/media/:id bad request", %{conn: conn, object: object} do media = conn |> put("/api/v1/media/#{object.id}", %{}) @@ -79,4 +101,34 @@ test "returns error when request is bad", %{conn: conn, object: object} do assert media == %{"error" => "bad_request"} end end + + describe "Get media by id" do + setup %{user: actor} do + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image.jpg"), + filename: "an_image.jpg" + } + + {:ok, %Object{} = object} = + ActivityPub.upload( + file, + actor: User.ap_id(actor), + description: "test-media" + ) + + [object: object] + end + + test "/api/v1/media/:id", %{conn: conn, object: object} do + media = + conn + |> get("/api/v1/media/#{object.id}") + |> json_response(:ok) + + assert media["description"] == "test-media" + assert media["type"] == "image" + assert media["id"] + end + end end From 099e314a1bb823a83d9c1af0cca2363487a07899 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Thu, 14 May 2020 10:50:12 +0200 Subject: [PATCH 2/4] Add OpenAPISpex for MediaController --- .../api_spec/operations/media_operation.ex | 131 ++++++++++++++++++ .../web/api_spec/schemas/attachment.ex | 2 +- .../controllers/media_controller.ex | 3 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/web/api_spec/operations/media_operation.ex diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex new file mode 100644 index 000000000..0fe686efa --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -0,0 +1,131 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.MediaOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 200 => + Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp create_request() do + %Schema{ + title: "MediaCreateRequest", + description: "POST body for creating an attachment", + type: :object, + properties: %{ + file: %Schema{ + type: :binary, + description: "The file to be attached, using multipart form data.", + required: true + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def update_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.update", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", update_request()), + responses: %{ + 200 => + Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + defp update_request() do + %Schema{ + title: "MediaCreateRequest", + description: "POST body for creating an attachment", + type: :object, + properties: %{ + id: %Schema{ + type: :string, + description: "The id of the Attachment entity to be updated", + required: true + }, + file: %Schema{ + type: :binary, + description: "The file to be attached, using multipart form data." + }, + description: %Schema{ + type: :string, + description: "A plain-text description of the media, for accessibility purposes." + }, + focus: %Schema{ + type: :string, + description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0." + } + } + } + end + + def show_operation do + %Operation{ + tags: ["media"], + summary: "Show Uploaded media attachment", + operationId: "MediaController.show", + security: [%{"oAuth" => ["read:media"]}], + responses: %{ + 200 => + Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 401 => Operation.response("Media", "application/json", ApiError), + 422 => Operation.response("Media", "application/json", ApiError) + } + } + end + + def create2_operation do + %Operation{ + tags: ["media"], + summary: "Upload media as attachment", + description: "Creates an attachment to be used with a new status.", + operationId: "MediaController.create2", + security: [%{"oAuth" => ["write:media"]}], + requestBody: Helpers.request_body("Parameters", create_request()), + responses: %{ + 202 => + Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 422 => Operation.response("Media", "application/json", ApiError), + 500 => Operation.response("Media", "application/json", ApiError) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex index c146c416e..c6edf6d36 100644 --- a/lib/pleroma/web/api_spec/schemas/attachment.ex +++ b/lib/pleroma/web/api_spec/schemas/attachment.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do type: :object, requried: [:id, :url, :preview_url], properties: %{ - id: %Schema{type: :string}, + id: %Schema{type: :string, description: "The ID of the attachment in the database."}, url: %Schema{ type: :string, format: :uri, diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 1997ac1af..52e0b22d8 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -11,10 +11,13 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do alias Pleroma.Web.ActivityPub.ActivityPub action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:put_view, Pleroma.Web.MastodonAPI.StatusView) plug(OAuthScopesPlug, %{scopes: ["write:media"]}) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation + @doc "POST /api/v1/media" def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do with {:ok, object} <- From 0f885b4b86ad7ba738ef0dd0de7f7d0496b7e43d Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 14 May 2020 16:18:30 +0400 Subject: [PATCH 3/4] Fix OpenAPI spec --- .../api_spec/operations/media_operation.ex | 43 ++++++++++--------- .../controllers/media_controller.ex | 14 +++--- .../controllers/media_controller_test.exs | 16 ++++--- 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/media_operation.ex b/lib/pleroma/web/api_spec/operations/media_operation.ex index 0fe686efa..d9c3c42db 100644 --- a/lib/pleroma/web/api_spec/operations/media_operation.ex +++ b/lib/pleroma/web/api_spec/operations/media_operation.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.MediaOperation do alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.Attachment def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -22,24 +23,24 @@ def create_operation do security: [%{"oAuth" => ["write:media"]}], requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ - 200 => - Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 200 => Operation.response("Media", "application/json", Attachment), 401 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError) } } end - defp create_request() do + defp create_request do %Schema{ title: "MediaCreateRequest", description: "POST body for creating an attachment", type: :object, + required: [:file], properties: %{ file: %Schema{ - type: :binary, - description: "The file to be attached, using multipart form data.", - required: true + type: :string, + format: :binary, + description: "The file to be attached, using multipart form data." }, description: %Schema{ type: :string, @@ -60,29 +61,26 @@ def update_operation do description: "Creates an attachment to be used with a new status.", operationId: "MediaController.update", security: [%{"oAuth" => ["write:media"]}], + parameters: [id_param()], requestBody: Helpers.request_body("Parameters", update_request()), responses: %{ - 200 => - Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 200 => Operation.response("Media", "application/json", Attachment), + 400 => Operation.response("Media", "application/json", ApiError), 401 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError) } } end - defp update_request() do + defp update_request do %Schema{ - title: "MediaCreateRequest", - description: "POST body for creating an attachment", + title: "MediaUpdateRequest", + description: "POST body for updating an attachment", type: :object, properties: %{ - id: %Schema{ - type: :string, - description: "The id of the Attachment entity to be updated", - required: true - }, file: %Schema{ - type: :binary, + type: :string, + format: :binary, description: "The file to be attached, using multipart form data." }, description: %Schema{ @@ -102,10 +100,10 @@ def show_operation do tags: ["media"], summary: "Show Uploaded media attachment", operationId: "MediaController.show", + parameters: [id_param()], security: [%{"oAuth" => ["read:media"]}], responses: %{ - 200 => - Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 200 => Operation.response("Media", "application/json", Attachment), 401 => Operation.response("Media", "application/json", ApiError), 422 => Operation.response("Media", "application/json", ApiError) } @@ -121,11 +119,14 @@ def create2_operation do security: [%{"oAuth" => ["write:media"]}], requestBody: Helpers.request_body("Parameters", create_request()), responses: %{ - 202 => - Operation.response("Media", "application/json", Pleroma.Web.ApiSpec.Schemas.Attachment), + 202 => Operation.response("Media", "application/json", Attachment), 422 => Operation.response("Media", "application/json", ApiError), 500 => Operation.response("Media", "application/json", ApiError) } } end + + defp id_param do + Operation.parameter(:id, :path, :string, "The ID of the Attachment entity") + end end diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 52e0b22d8..3b2ea751c 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -19,12 +19,12 @@ defmodule Pleroma.Web.MastodonAPI.MediaController do defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MediaOperation @doc "POST /api/v1/media" - def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + def create(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do with {:ok, object} <- ActivityPub.upload( file, actor: User.ap_id(user), - description: Map.get(data, "description") + description: Map.get(data, :description) ) do attachment_data = Map.put(object.data, "id", object.id) @@ -35,12 +35,12 @@ def create(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def create(_conn, _data), do: {:error, :bad_request} @doc "POST /api/v2/media" - def create2(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do + def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, _) do with {:ok, object} <- ActivityPub.upload( file, actor: User.ap_id(user), - description: Map.get(data, "description") + description: Map.get(data, :description) ) do attachment_data = Map.put(object.data, "id", object.id) @@ -53,7 +53,9 @@ def create2(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def create2(_conn, _data), do: {:error, :bad_request} @doc "PUT /api/v1/media/:id" - def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => description}) + def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{ + id: id + }) when is_binary(description) do with %Object{} = object <- Object.get_by_id(id), true <- Object.authorize_mutation(object, user), @@ -67,7 +69,7 @@ def update(%{assigns: %{user: user}} = conn, %{"id" => id, "description" => desc def update(_conn, _data), do: {:error, :bad_request} @doc "GET /api/v1/media/:id" - def show(conn, %{"id" => id}) do + def show(conn, %{id: id}) do with %Object{data: data, id: object_id} <- Object.get_by_id(id) do attachment_data = Map.put(data, "id", object_id) diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs index d872ff484..715747818 100644 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -30,8 +30,9 @@ test "/api/v1/media", %{conn: conn, image: image} do media = conn + |> put_req_header("content-type", "multipart/form-data") |> post("/api/v1/media", %{"file" => image, "description" => desc}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert media["type"] == "image" assert media["description"] == desc @@ -46,15 +47,16 @@ test "/api/v2/media", %{conn: conn, image: image} do response = conn + |> put_req_header("content-type", "multipart/form-data") |> post("/api/v2/media", %{"file" => image, "description" => desc}) - |> json_response(202) + |> json_response_and_validate_schema(202) assert media_id = response["id"] media = conn |> get("/api/v1/media/#{media_id}") - |> json_response(200) + |> json_response_and_validate_schema(200) assert media["type"] == "image" assert media["description"] == desc @@ -85,8 +87,9 @@ test "/api/v2/media", %{conn: conn, image: image} do test "/api/v1/media/:id good request", %{conn: conn, object: object} do media = conn + |> put_req_header("content-type", "multipart/form-data") |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert media["description"] == "test-media" assert refresh_record(object).data["name"] == "test-media" @@ -95,8 +98,9 @@ test "/api/v1/media/:id good request", %{conn: conn, object: object} do test "/api/v1/media/:id bad request", %{conn: conn, object: object} do media = conn + |> put_req_header("content-type", "multipart/form-data") |> put("/api/v1/media/#{object.id}", %{}) - |> json_response(400) + |> json_response_and_validate_schema(400) assert media == %{"error" => "bad_request"} end @@ -124,7 +128,7 @@ test "/api/v1/media/:id", %{conn: conn, object: object} do media = conn |> get("/api/v1/media/#{object.id}") - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) assert media["description"] == "test-media" assert media["type"] == "image" From bb03dfdb03714027640087ad1bd6475a8bb1c2c3 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 14 May 2020 16:29:32 +0400 Subject: [PATCH 4/4] Do not require `description` in `update` action --- .../web/mastodon_api/controllers/media_controller.ex | 7 ++----- .../mastodon_api/controllers/media_controller_test.exs | 10 ---------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex index 3b2ea751c..a21233393 100644 --- a/lib/pleroma/web/mastodon_api/controllers/media_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/media_controller.ex @@ -53,10 +53,7 @@ def create2(%{assigns: %{user: user}, body_params: %{file: file} = data} = conn, def create2(_conn, _data), do: {:error, :bad_request} @doc "PUT /api/v1/media/:id" - def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{ - id: id - }) - when is_binary(description) do + def update(%{assigns: %{user: user}, body_params: %{description: description}} = conn, %{id: id}) do with %Object{} = object <- Object.get_by_id(id), true <- Object.authorize_mutation(object, user), {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do @@ -66,7 +63,7 @@ def update(%{assigns: %{user: user}, body_params: %{description: description}} = end end - def update(_conn, _data), do: {:error, :bad_request} + def update(conn, data), do: show(conn, data) @doc "GET /api/v1/media/:id" def show(conn, %{id: id}) do diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs index 715747818..7ba1727f2 100644 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -94,16 +94,6 @@ test "/api/v1/media/:id good request", %{conn: conn, object: object} do assert media["description"] == "test-media" assert refresh_record(object).data["name"] == "test-media" end - - test "/api/v1/media/:id bad request", %{conn: conn, object: object} do - media = - conn - |> put_req_header("content-type", "multipart/form-data") - |> put("/api/v1/media/#{object.id}", %{}) - |> json_response_and_validate_schema(400) - - assert media == %{"error" => "bad_request"} - end end describe "Get media by id" do