From 2c4844237f294d27f58737f9694f77b1cfcb10e7 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 30 Apr 2020 18:19:51 +0300 Subject: [PATCH 01/20] Refactoring of :if_func / :unless_func plug options (general availability). Added tests for Pleroma.Web.Plug. --- .../plugs/ensure_authenticated_plug.ex | 17 +--- lib/pleroma/plugs/federating_plug.ex | 3 + .../activity_pub/activity_pub_controller.ex | 2 +- lib/pleroma/web/feed/user_controller.ex | 2 +- lib/pleroma/web/ostatus/ostatus_controller.ex | 2 +- .../web/static_fe/static_fe_controller.ex | 2 +- lib/pleroma/web/web.ex | 10 +- test/plugs/ensure_authenticated_plug_test.exs | 4 +- test/web/plugs/plug_test.exs | 91 +++++++++++++++++++ 9 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 test/web/plugs/plug_test.exs diff --git a/lib/pleroma/plugs/ensure_authenticated_plug.ex b/lib/pleroma/plugs/ensure_authenticated_plug.ex index 9c8f5597f..9d5176e2b 100644 --- a/lib/pleroma/plugs/ensure_authenticated_plug.ex +++ b/lib/pleroma/plugs/ensure_authenticated_plug.ex @@ -19,22 +19,7 @@ def perform(%{assigns: %{user: %User{}}} = conn, _) do conn end - def perform(conn, options) do - perform = - cond do - options[:if_func] -> options[:if_func].() - options[:unless_func] -> !options[:unless_func].() - true -> true - end - - if perform do - fail(conn) - else - conn - end - end - - def fail(conn) do + def perform(conn, _) do conn |> render_error(:forbidden, "Invalid credentials.") |> halt() diff --git a/lib/pleroma/plugs/federating_plug.ex b/lib/pleroma/plugs/federating_plug.ex index 7d947339f..09038f3c6 100644 --- a/lib/pleroma/plugs/federating_plug.ex +++ b/lib/pleroma/plugs/federating_plug.ex @@ -19,6 +19,9 @@ def call(conn, _opts) do def federating?, do: Pleroma.Config.get([:instance, :federating]) + # Definition for the use in :if_func / :unless_func plug options + def federating?(_conn), do: federating?() + defp fail(conn) do conn |> put_status(404) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index d625530ec..a909516be 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do plug( EnsureAuthenticatedPlug, - [unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions + [unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions ) plug( diff --git a/lib/pleroma/web/feed/user_controller.ex b/lib/pleroma/web/feed/user_controller.ex index e27f85929..1b72e23dc 100644 --- a/lib/pleroma/web/feed/user_controller.ex +++ b/lib/pleroma/web/feed/user_controller.ex @@ -27,7 +27,7 @@ def feed_redirect(%{assigns: %{format: format}} = conn, _params) when format in ["json", "activity+json"] do with %{halted: false} = conn <- Pleroma.Plugs.EnsureAuthenticatedPlug.call(conn, - unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 ) do ActivityPubController.call(conn, :user) end diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 6fd3cfce5..6971cd9f8 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.Router plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 ) plug( diff --git a/lib/pleroma/web/static_fe/static_fe_controller.ex b/lib/pleroma/web/static_fe/static_fe_controller.ex index 7a35238d7..c3efb6651 100644 --- a/lib/pleroma/web/static_fe/static_fe_controller.ex +++ b/lib/pleroma/web/static_fe/static_fe_controller.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do plug(:assign_id) plug(Pleroma.Plugs.EnsureAuthenticatedPlug, - unless_func: &Pleroma.Web.FederatingPlug.federating?/0 + unless_func: &Pleroma.Web.FederatingPlug.federating?/1 ) @page_keys ["max_id", "min_id", "limit", "since_id", "order"] diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 08e42a7e5..4f9281851 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -200,11 +200,17 @@ def skip_plug(conn) do @impl Plug @doc """ - If marked as skipped, returns `conn`, otherwise calls `perform/2`. + Before-plug hook that + * ensures the plug is not skipped + * processes `:if_func` / `:unless_func` functional pre-run conditions + * adds plug to the list of called plugs and calls `perform/2` if checks are passed + Note: multiple invocations of the same plug (with different or same options) are allowed. """ def call(%Plug.Conn{} = conn, options) do - if PlugHelper.plug_skipped?(conn, __MODULE__) do + if PlugHelper.plug_skipped?(conn, __MODULE__) || + (options[:if_func] && !options[:if_func].(conn)) || + (options[:unless_func] && options[:unless_func].(conn)) do conn else conn = diff --git a/test/plugs/ensure_authenticated_plug_test.exs b/test/plugs/ensure_authenticated_plug_test.exs index 689fe757f..4e6142aab 100644 --- a/test/plugs/ensure_authenticated_plug_test.exs +++ b/test/plugs/ensure_authenticated_plug_test.exs @@ -27,8 +27,8 @@ test "it continues if a user is assigned", %{conn: conn} do describe "with :if_func / :unless_func options" do setup do %{ - true_fn: fn -> true end, - false_fn: fn -> false end + true_fn: fn _conn -> true end, + false_fn: fn _conn -> false end } end diff --git a/test/web/plugs/plug_test.exs b/test/web/plugs/plug_test.exs new file mode 100644 index 000000000..943e484e7 --- /dev/null +++ b/test/web/plugs/plug_test.exs @@ -0,0 +1,91 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PlugTest do + @moduledoc "Tests for the functionality added via `use Pleroma.Web, :plug`" + + alias Pleroma.Plugs.ExpectAuthenticatedCheckPlug + alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug + alias Pleroma.Plugs.PlugHelper + + import Mock + + use Pleroma.Web.ConnCase + + describe "when plug is skipped, " do + setup_with_mocks( + [ + {ExpectPublicOrAuthenticatedCheckPlug, [:passthrough], []} + ], + %{conn: conn} + ) do + conn = ExpectPublicOrAuthenticatedCheckPlug.skip_plug(conn) + %{conn: conn} + end + + test "it neither adds plug to called plugs list nor calls `perform/2`, " <> + "regardless of :if_func / :unless_func options", + %{conn: conn} do + for opts <- [%{}, %{if_func: fn _ -> true end}, %{unless_func: fn _ -> false end}] do + ret_conn = ExpectPublicOrAuthenticatedCheckPlug.call(conn, opts) + + refute called(ExpectPublicOrAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectPublicOrAuthenticatedCheckPlug) + end + end + end + + describe "when plug is NOT skipped, " do + setup_with_mocks([{ExpectAuthenticatedCheckPlug, [:passthrough], []}]) do + :ok + end + + test "with no pre-run checks, adds plug to called plugs list and calls `perform/2`", %{ + conn: conn + } do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "when :if_func option is given, calls the plug only if provided function evals tru-ish", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> false end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{if_func: fn _ -> true end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "if :unless_func option is given, calls the plug only if provided function evals falsy", + %{conn: conn} do + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> true end}) + + refute called(ExpectAuthenticatedCheckPlug.perform(:_, :_)) + refute PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + + ret_conn = ExpectAuthenticatedCheckPlug.call(conn, %{unless_func: fn _ -> false end}) + + assert called(ExpectAuthenticatedCheckPlug.perform(ret_conn, :_)) + assert PlugHelper.plug_called?(ret_conn, ExpectAuthenticatedCheckPlug) + end + + test "allows a plug to be called multiple times (even if it's in called plugs list)", %{ + conn: conn + } do + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value1}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value1})) + + assert PlugHelper.plug_called?(conn, ExpectAuthenticatedCheckPlug) + + conn = ExpectAuthenticatedCheckPlug.call(conn, %{an_option: :value2}) + assert called(ExpectAuthenticatedCheckPlug.perform(conn, %{an_option: :value2})) + end + end +end From ecf37b46d2c06c701da390eba65239984afe683f Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 1 May 2020 14:31:24 +0300 Subject: [PATCH 02/20] pagination fix for service users filters --- lib/pleroma/user/query.ex | 11 +++--- .../web/admin_api/admin_api_controller.ex | 29 +++----------- lib/pleroma/web/admin_api/search.ex | 1 + .../admin_api/admin_api_controller_test.exs | 38 ++++++++++++++++++- 4 files changed, 49 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex index ac77aab71..3a3b04793 100644 --- a/lib/pleroma/user/query.ex +++ b/lib/pleroma/user/query.ex @@ -45,6 +45,7 @@ defmodule Pleroma.User.Query do is_admin: boolean(), is_moderator: boolean(), super_users: boolean(), + exclude_service_users: boolean(), followers: User.t(), friends: User.t(), recipients_from_activity: [String.t()], @@ -88,6 +89,10 @@ defp compose_query({key, value}, query) where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) end + defp compose_query({:exclude_service_users, _}, query) do + where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch")) + end + defp compose_query({key, value}, query) when key in @equal_criteria and not_empty_string(value) do where(query, [u], ^[{key, value}]) @@ -98,7 +103,7 @@ defp compose_query({key, values}, query) when key in @contains_criteria and is_l end defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do - Enum.reduce(tags, query, &prepare_tag_criteria/2) + where(query, [u], fragment("? && ?", u.tags, ^tags)) end defp compose_query({:is_admin, _}, query) do @@ -192,10 +197,6 @@ defp compose_query({:limit, limit}, query) do defp compose_query(_unsupported_param, query), do: query - defp prepare_tag_criteria(tag, query) do - or_where(query, [u], fragment("? = any(?)", ^tag, u.tags)) - end - defp location_query(query, local) do where(query, [u], u.local == ^local) |> where([u], not is_nil(u.nickname)) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 816c11e01..bfcc81cb8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -392,29 +392,12 @@ def list_users(conn, params) do email: params["email"] } - with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), - {:ok, users, count} <- filter_service_users(users, count), - do: - conn - |> json( - AccountView.render("index.json", - users: users, - count: count, - page_size: page_size - ) - ) - end - - defp filter_service_users(users, count) do - filtered_users = Enum.reject(users, &service_user?/1) - count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count - - {:ok, filtered_users, count} - end - - defp service_user?(user) do - String.match?(user.ap_id, ~r/.*\/relay$/) or - String.match?(user.ap_id, ~r/.*\/internal\/fetch$/) + with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do + json( + conn, + AccountView.render("index.json", users: users, count: count, page_size: page_size) + ) + end end @filters ~w(local external active deactivated is_admin is_moderator) diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index 29cea1f44..c28efadd5 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -21,6 +21,7 @@ def user(params \\ %{}) do query = params |> Map.drop([:page, :page_size]) + |> Map.put(:exclude_service_users, true) |> User.Query.build() |> order_by([u], u.nickname) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index f80dbf8dd..e3af01089 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -18,6 +18,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User + alias Pleroma.Web alias Pleroma.UserInviteToken alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI @@ -737,6 +738,39 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do } end + test "pagination works correctly with service users", %{conn: conn} do + service1 = insert(:user, ap_id: Web.base_url() <> "/relay") + service2 = insert(:user, ap_id: Web.base_url() <> "/internal/fetch") + insert_list(25, :user) + + assert %{"count" => 26, "page_size" => 10, "users" => users1} = + conn + |> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users1) == 10 + assert service1 not in [users1] + assert service2 not in [users1] + + assert %{"count" => 26, "page_size" => 10, "users" => users2} = + conn + |> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users2) == 10 + assert service1 not in [users2] + assert service2 not in [users2] + + assert %{"count" => 26, "page_size" => 10, "users" => users3} = + conn + |> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"}) + |> json_response(200) + + assert Enum.count(users3) == 6 + assert service1 not in [users3] + assert service2 not in [users3] + end + test "renders empty array for the second page", %{conn: conn} do insert(:user) @@ -3526,7 +3560,7 @@ test "errors", %{conn: conn} do end test "success", %{conn: conn} do - base_url = Pleroma.Web.base_url() + base_url = Web.base_url() app_name = "Trusted app" response = @@ -3547,7 +3581,7 @@ test "success", %{conn: conn} do end test "with trusted", %{conn: conn} do - base_url = Pleroma.Web.base_url() + base_url = Web.base_url() app_name = "Trusted app" response = From aea781cbd8fb43f906c6022a8d2e0bf896008203 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Fri, 1 May 2020 16:31:05 +0300 Subject: [PATCH 03/20] credo fix --- test/web/admin_api/admin_api_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index e3af01089..d798412e3 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -18,8 +18,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.ReportNote alias Pleroma.Tests.ObanHelpers alias Pleroma.User - alias Pleroma.Web alias Pleroma.UserInviteToken + alias Pleroma.Web alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.CommonAPI alias Pleroma.Web.MediaProxy From a7966f2080a0e9b3c2b35efa7ea647c1bdef2a2d Mon Sep 17 00:00:00 2001 From: lain Date: Sun, 3 May 2020 13:48:01 +0200 Subject: [PATCH 04/20] Webfinger: Request account info with the acct scheme --- lib/pleroma/web/web_finger/web_finger.ex | 6 ++++-- test/support/http_request_mock.ex | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 7ffd0e51b..442b25165 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -194,13 +194,15 @@ def finger(account) do URI.parse(account).host end + encoded_account = URI.encode("acct:#{account}") + address = case find_lrdd_template(domain) do {:ok, template} -> - String.replace(template, "{uri}", URI.encode(account)) + String.replace(template, "{uri}", encoded_account) _ -> - "https://#{domain}/.well-known/webfinger?resource=acct:#{account}" + "https://#{domain}/.well-known/webfinger?resource=#{encoded_account}" end with response <- diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 9624cb0f7..3a95e92da 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -211,7 +211,7 @@ def get( end def get( - "https://squeet.me/xrd/?uri=lain@squeet.me", + "https://squeet.me/xrd/?uri=acct:lain@squeet.me", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -870,7 +870,7 @@ def get( end def get( - "https://social.heldscal.la/.well-known/webfinger?resource=shp@social.heldscal.la", + "https://social.heldscal.la/.well-known/webfinger?resource=acct:shp@social.heldscal.la", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -883,7 +883,7 @@ def get( end def get( - "https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la", + "https://social.heldscal.la/.well-known/webfinger?resource=acct:invalid_content@social.heldscal.la", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -900,7 +900,7 @@ def get("http://framatube.org/.well-known/host-meta", _, _, _) do end def get( - "http://framatube.org/main/xrd?uri=framasoft@framatube.org", + "http://framatube.org/main/xrd?uri=acct:framasoft@framatube.org", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -959,7 +959,7 @@ def get("http://gerzilla.de/.well-known/host-meta", _, _, _) do end def get( - "https://gerzilla.de/xrd/?uri=kaniini@gerzilla.de", + "https://gerzilla.de/xrd/?uri=acct:kaniini@gerzilla.de", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -1155,7 +1155,7 @@ def get("http://404.site" <> _, _, _, _) do end def get( - "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=lain@zetsubou.xn--q9jyb4c", + "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:lain@zetsubou.xn--q9jyb4c", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] @@ -1168,7 +1168,7 @@ def get( end def get( - "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=https://zetsubou.xn--q9jyb4c/users/lain", + "https://zetsubou.xn--q9jyb4c/.well-known/webfinger?resource=acct:https://zetsubou.xn--q9jyb4c/users/lain", _, _, [{"accept", "application/xrd+xml,application/jrd+json"}] From 7dd47bee82c9f4a5e3b4ce6d74c5a22cac596b52 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 4 May 2020 12:22:31 +0200 Subject: [PATCH 05/20] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522285efe..92dd6f0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again - Fix follower/blocks import when nicknames starts with @ - Filtering of push notifications on activities from blocked domains +- Resolving Peertube accounts with Webfinger ## [unreleased-patch] ### Security From bf0e41f0daa5809db53ed4a9130ade63952e8da0 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 4 May 2020 23:32:53 +0200 Subject: [PATCH 06/20] Transmogrifier.set_sensitive/1: Keep sensitive set to true --- CHANGELOG.md | 1 + .../web/activity_pub/transmogrifier.ex | 4 ++++ .../activity_pub_controller_test.exs | 22 +++++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 522285efe..cdb8a2080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Logger configuration through AdminFE - HTTP Basic Authentication permissions issue - ObjectAgePolicy didn't filter out old messages +- Transmogrifier: Keep object sensitive settings for outgoing representation (AP C2S) ### Added - NodeInfo: ObjectAgePolicy settings to the `federation` list. diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 581e7040b..3a4d364e7 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -1195,6 +1195,10 @@ def set_conversation(object) do Map.put(object, "conversation", object["context"]) end + def set_sensitive(%{"sensitive" => true} = object) do + object + end + def set_sensitive(object) do tags = object["tag"] || [] Map.put(object, "sensitive", "nsfw" in tags) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index a8f1f0e26..5c8d20ac4 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -820,21 +820,29 @@ test "it inserts an incoming sensitive activity into the database", %{ activity: activity } do user = insert(:user) + conn = assign(conn, :user, user) object = Map.put(activity["object"], "sensitive", true) activity = Map.put(activity, "object", object) - result = + response = conn - |> assign(:user, user) |> put_req_header("content-type", "application/activity+json") |> post("/users/#{user.nickname}/outbox", activity) |> json_response(201) - assert Activity.get_by_ap_id(result["id"]) - assert result["object"] - assert %Object{data: object} = Object.normalize(result["object"]) - assert object["sensitive"] == activity["object"]["sensitive"] - assert object["content"] == activity["object"]["content"] + assert Activity.get_by_ap_id(response["id"]) + assert response["object"] + assert %Object{data: response_object} = Object.normalize(response["object"]) + assert response_object["sensitive"] == true + assert response_object["content"] == activity["object"]["content"] + + representation = + conn + |> put_req_header("accept", "application/activity+json") + |> get(response["id"]) + |> json_response(200) + + assert representation["object"]["sensitive"] == true end test "it rejects an incoming activity with bogus type", %{conn: conn, activity: activity} do From d861b0790a62767b31b8a85862fc249a4f8ca542 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 16:43:00 +0400 Subject: [PATCH 07/20] Add OpenAPI spec for SubscriptionController --- lib/pleroma/web/api_spec.ex | 7 +- .../operations/subscription_operation.ex | 188 ++++++++++++++++++ .../web/api_spec/schemas/push_subscription.ex | 66 ++++++ .../controllers/subscription_controller.ex | 12 +- lib/pleroma/web/push/subscription.ex | 10 +- lib/pleroma/web/router.ex | 2 +- .../subscription_controller_test.exs | 28 +-- 7 files changed, 288 insertions(+), 25 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/subscription_operation.ex create mode 100644 lib/pleroma/web/api_spec/schemas/push_subscription.ex diff --git a/lib/pleroma/web/api_spec.ex b/lib/pleroma/web/api_spec.ex index b3c1e3ea2..79fd5f871 100644 --- a/lib/pleroma/web/api_spec.ex +++ b/lib/pleroma/web/api_spec.ex @@ -39,7 +39,12 @@ def spec do password: %OpenApiSpex.OAuthFlow{ authorizationUrl: "/oauth/authorize", tokenUrl: "/oauth/token", - scopes: %{"read" => "read", "write" => "write", "follow" => "follow"} + scopes: %{ + "read" => "read", + "write" => "write", + "follow" => "follow", + "push" => "push" + } } } } diff --git a/lib/pleroma/web/api_spec/operations/subscription_operation.ex b/lib/pleroma/web/api_spec/operations/subscription_operation.ex new file mode 100644 index 000000000..663b8fa11 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/subscription_operation.ex @@ -0,0 +1,188 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.PushSubscription + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def create_operation do + %Operation{ + tags: ["Push Subscriptions"], + summary: "Subscribe to push notifications", + description: + "Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.", + operationId: "SubscriptionController.create", + security: [%{"oAuth" => ["push"]}], + requestBody: Helpers.request_body("Parameters", create_request(), required: true), + responses: %{ + 200 => Operation.response("Push Subscription", "application/json", PushSubscription), + 400 => Operation.response("Error", "application/json", ApiError), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Push Subscriptions"], + summary: "Get current subscription", + description: "View the PushSubscription currently associated with this access token.", + operationId: "SubscriptionController.show", + security: [%{"oAuth" => ["push"]}], + responses: %{ + 200 => Operation.response("Push Subscription", "application/json", PushSubscription), + 403 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Push Subscriptions"], + summary: "Change types of notifications", + description: + "Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.", + operationId: "SubscriptionController.update", + security: [%{"oAuth" => ["push"]}], + requestBody: Helpers.request_body("Parameters", update_request(), required: true), + responses: %{ + 200 => Operation.response("Push Subscription", "application/json", PushSubscription), + 403 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Push Subscriptions"], + summary: "Remove current subscription", + description: "Removes the current Web Push API subscription.", + operationId: "SubscriptionController.delete", + security: [%{"oAuth" => ["push"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}), + 403 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + defp create_request do + %Schema{ + title: "SubscriptionCreateRequest", + description: "POST body for creating a push subscription", + type: :object, + properties: %{ + subscription: %Schema{ + type: :object, + properties: %{ + endpoint: %Schema{ + type: :string, + description: "Endpoint URL that is called when a notification event occurs." + }, + keys: %Schema{ + type: :object, + properties: %{ + p256dh: %Schema{ + type: :string, + description: + "User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve." + }, + auth: %Schema{ + type: :string, + description: "Auth secret. Base64 encoded string of 16 bytes of random data." + } + }, + required: [:p256dh, :auth] + } + }, + required: [:endpoint, :keys] + }, + data: %Schema{ + type: :object, + properties: %{ + alerts: %Schema{ + type: :object, + properties: %{ + follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + favourite: %Schema{ + type: :boolean, + description: "Receive favourite notifications?" + }, + reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, + mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, + poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + } + } + } + } + }, + required: [:subscription], + example: %{ + "subscription" => %{ + "endpoint" => "https://example.com/example/1234", + "keys" => %{ + "auth" => "8eDyX_uCN0XRhSbY5hs7Hg==", + "p256dh" => + "BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA=" + } + }, + "data" => %{ + "alerts" => %{ + "follow" => true, + "mention" => true, + "poll" => false + } + } + } + } + end + + defp update_request do + %Schema{ + title: "SubscriptionUpdateRequest", + type: :object, + properties: %{ + data: %Schema{ + type: :object, + properties: %{ + alerts: %Schema{ + type: :object, + properties: %{ + follow: %Schema{type: :boolean, description: "Receive follow notifications?"}, + favourite: %Schema{ + type: :boolean, + description: "Receive favourite notifications?" + }, + reblog: %Schema{type: :boolean, description: "Receive reblog notifications?"}, + mention: %Schema{type: :boolean, description: "Receive mention notifications?"}, + poll: %Schema{type: :boolean, description: "Receive poll notifications?"} + } + } + } + } + }, + example: %{ + "data" => %{ + "alerts" => %{ + "follow" => true, + "favourite" => true, + "reblog" => true, + "mention" => true, + "poll" => true + } + } + } + } + end +end diff --git a/lib/pleroma/web/api_spec/schemas/push_subscription.ex b/lib/pleroma/web/api_spec/schemas/push_subscription.ex new file mode 100644 index 000000000..cc91b95b8 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/push_subscription.ex @@ -0,0 +1,66 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.PushSubscription do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "PushSubscription", + description: "Response schema for a push subscription", + type: :object, + properties: %{ + id: %Schema{ + anyOf: [%Schema{type: :string}, %Schema{type: :integer}], + description: "The id of the push subscription in the database." + }, + endpoint: %Schema{type: :string, description: "Where push alerts will be sent to."}, + server_key: %Schema{type: :string, description: "The streaming server's VAPID key."}, + alerts: %Schema{ + type: :object, + description: "Which alerts should be delivered to the endpoint.", + properties: %{ + follow: %Schema{ + type: :boolean, + description: "Receive a push notification when someone has followed you?" + }, + favourite: %Schema{ + type: :boolean, + description: + "Receive a push notification when a status you created has been favourited by someone else?" + }, + reblog: %Schema{ + type: :boolean, + description: + "Receive a push notification when a status you created has been boosted by someone else?" + }, + mention: %Schema{ + type: :boolean, + description: + "Receive a push notification when someone else has mentioned you in a status?" + }, + poll: %Schema{ + type: :boolean, + description: + "Receive a push notification when a poll you voted in or created has ended? " + } + } + } + }, + example: %{ + "id" => "328_183", + "endpoint" => "https://yourdomain.example/listener", + "alerts" => %{ + "follow" => true, + "favourite" => true, + "reblog" => true, + "mention" => true, + "poll" => true + }, + "server_key" => + "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=" + } + }) +end diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index d184ea1d0..34eac97c5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -11,14 +11,16 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do action_fallback(:errors) + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:restrict_push_enabled) plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]}) - plug(:restrict_push_enabled) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.SubscriptionOperation # Creates PushSubscription # POST /api/v1/push/subscription # - def create(%{assigns: %{user: user, token: token}} = conn, params) do + def create(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do with {:ok, _} <- Subscription.delete_if_exists(user, token), {:ok, subscription} <- Subscription.create(user, token, params) do render(conn, "show.json", subscription: subscription) @@ -28,7 +30,7 @@ def create(%{assigns: %{user: user, token: token}} = conn, params) do # Gets PushSubscription # GET /api/v1/push/subscription # - def get(%{assigns: %{user: user, token: token}} = conn, _params) do + def show(%{assigns: %{user: user, token: token}} = conn, _params) do with {:ok, subscription} <- Subscription.get(user, token) do render(conn, "show.json", subscription: subscription) end @@ -37,7 +39,7 @@ def get(%{assigns: %{user: user, token: token}} = conn, _params) do # Updates PushSubscription # PUT /api/v1/push/subscription # - def update(%{assigns: %{user: user, token: token}} = conn, params) do + def update(%{assigns: %{user: user, token: token}, body_params: params} = conn, _) do with {:ok, subscription} <- Subscription.update(user, token, params) do render(conn, "show.json", subscription: subscription) end @@ -66,7 +68,7 @@ defp restrict_push_enabled(conn, _) do def errors(conn, {:error, :not_found}) do conn |> put_status(:not_found) - |> json(dgettext("errors", "Not found")) + |> json(%{error: dgettext("errors", "Record not found")}) end def errors(conn, _) do diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index b99b0c5fb..3e401a490 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -25,9 +25,9 @@ defmodule Pleroma.Web.Push.Subscription do timestamps() end - @supported_alert_types ~w[follow favourite mention reblog] + @supported_alert_types ~w[follow favourite mention reblog]a - defp alerts(%{"data" => %{"alerts" => alerts}}) do + defp alerts(%{data: %{alerts: alerts}}) do alerts = Map.take(alerts, @supported_alert_types) %{"alerts" => alerts} end @@ -44,9 +44,9 @@ def create( %User{} = user, %Token{} = token, %{ - "subscription" => %{ - "endpoint" => endpoint, - "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh} + subscription: %{ + endpoint: endpoint, + keys: %{auth: key_auth, p256dh: key_p256dh} } } = params ) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 5b00243e9..eda8320ea 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -426,7 +426,7 @@ defmodule Pleroma.Web.Router do post("/statuses/:id/unmute", StatusController, :unmute_conversation) post("/push/subscription", SubscriptionController, :create) - get("/push/subscription", SubscriptionController, :get) + get("/push/subscription", SubscriptionController, :show) put("/push/subscription", SubscriptionController, :update) delete("/push/subscription", SubscriptionController, :delete) diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs index 5682498c0..4aa260663 100644 --- a/test/web/mastodon_api/controllers/subscription_controller_test.exs +++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory + alias Pleroma.Web.Push alias Pleroma.Web.Push.Subscription @@ -27,6 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionControllerTest do build_conn() |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") %{conn: conn, user: user, token: token} end @@ -47,8 +49,8 @@ defmacro assert_error_when_disable_push(do: yield) do test "returns error when push disabled ", %{conn: conn} do assert_error_when_disable_push do conn - |> post("/api/v1/push/subscription", %{}) - |> json_response(403) + |> post("/api/v1/push/subscription", %{subscription: @sub}) + |> json_response_and_validate_schema(403) end end @@ -59,7 +61,7 @@ test "successful creation", %{conn: conn} do "data" => %{"alerts" => %{"mention" => true, "test" => true}}, "subscription" => @sub }) - |> json_response(200) + |> json_response_and_validate_schema(200) [subscription] = Pleroma.Repo.all(Subscription) @@ -77,7 +79,7 @@ test "returns error when push disabled ", %{conn: conn} do assert_error_when_disable_push do conn |> get("/api/v1/push/subscription", %{}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -85,9 +87,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do res = conn |> get("/api/v1/push/subscription", %{}) - |> json_response(404) + |> json_response_and_validate_schema(404) - assert "Not found" == res + assert %{"error" => "Record not found"} == res end test "returns a user subsciption", %{conn: conn, user: user, token: token} do @@ -101,7 +103,7 @@ test "returns a user subsciption", %{conn: conn, user: user, token: token} do res = conn |> get("/api/v1/push/subscription", %{}) - |> json_response(200) + |> json_response_and_validate_schema(200) expect = %{ "alerts" => %{"mention" => true}, @@ -130,7 +132,7 @@ test "returns error when push disabled ", %{conn: conn} do assert_error_when_disable_push do conn |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -140,7 +142,7 @@ test "returns updated subsciption", %{conn: conn, subscription: subscription} do |> put("/api/v1/push/subscription", %{ data: %{"alerts" => %{"mention" => false, "follow" => true}} }) - |> json_response(200) + |> json_response_and_validate_schema(200) expect = %{ "alerts" => %{"follow" => true, "mention" => false}, @@ -158,7 +160,7 @@ test "returns error when push disabled ", %{conn: conn} do assert_error_when_disable_push do conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(403) + |> json_response_and_validate_schema(403) end end @@ -166,9 +168,9 @@ test "returns error when user hasn't subscription", %{conn: conn} do res = conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(404) + |> json_response_and_validate_schema(404) - assert "Not found" == res + assert %{"error" => "Record not found"} == res end test "returns empty result and delete user subsciption", %{ @@ -186,7 +188,7 @@ test "returns empty result and delete user subsciption", %{ res = conn |> delete("/api/v1/push/subscription", %{}) - |> json_response(200) + |> json_response_and_validate_schema(200) assert %{} == res refute Pleroma.Repo.get(Subscription, subscription.id) From 8096565653f262844214d715228c31d4ef761f57 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 14 Apr 2020 22:50:29 +0400 Subject: [PATCH 08/20] Add OpenAPI spec for MarkerController --- .../api_spec/operations/marker_operation.ex | 52 +++++++++++++++++++ lib/pleroma/web/api_spec/schemas/marker.ex | 31 +++++++++++ .../web/api_spec/schemas/markers_response.ex | 35 +++++++++++++ .../schemas/markers_upsert_request.ex | 35 +++++++++++++ .../controllers/marker_controller.ex | 12 ++++- .../controllers/marker_controller_test.exs | 12 ++++- 6 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/marker_operation.ex create mode 100644 lib/pleroma/web/api_spec/schemas/marker.ex create mode 100644 lib/pleroma/web/api_spec/schemas/markers_response.ex create mode 100644 lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex new file mode 100644 index 000000000..60adc7c7d --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.MarkerOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.MarkersResponse + alias Pleroma.Web.ApiSpec.Schemas.MarkersUpsertRequest + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["markers"], + summary: "Get saved timeline position", + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "MarkerController.index", + parameters: [ + Operation.parameter( + :timeline, + :query, + %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["home", "notifications"]} + }, + "Array of markers to fetch. If not provided, an empty object will be returned." + ) + ], + responses: %{ + 200 => Operation.response("Marker", "application/json", MarkersResponse) + } + } + end + + def upsert_operation do + %Operation{ + tags: ["markers"], + summary: "Save position in timeline", + operationId: "MarkerController.upsert", + requestBody: Helpers.request_body("Parameters", MarkersUpsertRequest, required: true), + security: [%{"oAuth" => ["follow", "write:blocks"]}], + responses: %{ + 200 => Operation.response("Marker", "application/json", MarkersResponse) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/schemas/marker.ex b/lib/pleroma/web/api_spec/schemas/marker.ex new file mode 100644 index 000000000..64fca5973 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/marker.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Marker do + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "Marker", + description: "Schema for a marker", + type: :object, + properties: %{ + last_read_id: %Schema{type: :string}, + version: %Schema{type: :integer}, + updated_at: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + unread_count: %Schema{type: :integer} + } + } + }, + example: %{ + "last_read_id" => "35098814", + "version" => 361, + "updated_at" => "2019-11-26T22:37:25.239Z", + "pleroma" => %{"unread_count" => 5} + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/markers_response.ex b/lib/pleroma/web/api_spec/schemas/markers_response.ex new file mode 100644 index 000000000..cb1121931 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/markers_response.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.MarkersResponse do + require OpenApiSpex + alias OpenApiSpex.Schema + + alias Pleroma.Web.ApiSpec.Schemas.Marker + + OpenApiSpex.schema(%{ + title: "MarkersResponse", + description: "Response schema for markers", + type: :object, + properties: %{ + notifications: %Schema{allOf: [Marker], nullable: true}, + home: %Schema{allOf: [Marker], nullable: true} + }, + items: %Schema{type: :string}, + example: %{ + "notifications" => %{ + "last_read_id" => "35098814", + "version" => 361, + "updated_at" => "2019-11-26T22:37:25.239Z", + "pleroma" => %{"unread_count" => 0} + }, + "home" => %{ + "last_read_id" => "103206604258487607", + "version" => 468, + "updated_at" => "2019-11-26T22:37:25.235Z", + "pleroma" => %{"unread_count" => 10} + } + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex b/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex new file mode 100644 index 000000000..97dcc24b4 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex @@ -0,0 +1,35 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.MarkersUpsertRequest do + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "MarkersUpsertRequest", + description: "Request schema for marker upsert", + type: :object, + properties: %{ + notifications: %Schema{ + type: :object, + properties: %{ + last_read_id: %Schema{type: :string} + } + }, + home: %Schema{ + type: :object, + properties: %{ + last_read_id: %Schema{type: :string} + } + } + }, + example: %{ + "home" => %{ + "last_read_id" => "103194548672408537", + "version" => 462, + "updated_at" => "2019-11-24T19:39:39.337Z" + } + } + }) +end diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex index 9f9d4574e..b94171b36 100644 --- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -15,15 +15,23 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation # GET /api/v1/markers def index(%{assigns: %{user: user}} = conn, params) do - markers = Pleroma.Marker.get_markers(user, params["timeline"]) + markers = Pleroma.Marker.get_markers(user, params[:timeline]) render(conn, "markers.json", %{markers: markers}) end # POST /api/v1/markers - def upsert(%{assigns: %{user: user}} = conn, params) do + def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do + params = + params + |> Map.from_struct() + |> Map.new(fn {key, value} -> {to_string(key), value} end) + with {:ok, result} <- Pleroma.Marker.upsert(user, params), markers <- Map.values(result) do render(conn, "markers.json", %{markers: markers}) diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs index 919f295bd..1c85ed032 100644 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -4,8 +4,10 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Web.ApiSpec import Pleroma.Factory + import OpenApiSpex.TestAssertions describe "GET /api/v1/markers" do test "gets markers with correct scopes", %{conn: conn} do @@ -22,7 +24,7 @@ test "gets markers with correct scopes", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, token) - |> get("/api/v1/markers", %{timeline: ["notifications"]}) + |> get("/api/v1/markers?timeline[]=notifications") |> json_response(200) assert response == %{ @@ -32,6 +34,8 @@ test "gets markers with correct scopes", %{conn: conn} do "version" => 0 } } + + assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "gets markers with missed scopes", %{conn: conn} do @@ -60,6 +64,7 @@ test "creates a marker with correct scopes", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69420"} @@ -73,6 +78,8 @@ test "creates a marker with correct scopes", %{conn: conn} do "version" => 0 } } = response + + assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "updates exist marker", %{conn: conn} do @@ -89,6 +96,7 @@ test "updates exist marker", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69888"} @@ -102,6 +110,8 @@ test "updates exist marker", %{conn: conn} do "version" => 0 } } + + assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "creates a marker with missed scopes", %{conn: conn} do From babcae7130d3bc75f85adeef1845997cd091eb84 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 16:45:34 +0400 Subject: [PATCH 09/20] Move single used schemas to Marker operation schema --- .../api_spec/operations/marker_operation.ex | 102 ++++++++++++++++-- lib/pleroma/web/api_spec/schemas/marker.ex | 31 ------ .../web/api_spec/schemas/markers_response.ex | 35 ------ .../schemas/markers_upsert_request.ex | 35 ------ .../controllers/marker_controller.ex | 8 +- .../web/mastodon_api/views/marker_view.ex | 13 +-- .../controllers/marker_controller_test.exs | 19 ++-- 7 files changed, 111 insertions(+), 132 deletions(-) delete mode 100644 lib/pleroma/web/api_spec/schemas/marker.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/markers_response.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex diff --git a/lib/pleroma/web/api_spec/operations/marker_operation.ex b/lib/pleroma/web/api_spec/operations/marker_operation.ex index 60adc7c7d..06620492a 100644 --- a/lib/pleroma/web/api_spec/operations/marker_operation.ex +++ b/lib/pleroma/web/api_spec/operations/marker_operation.ex @@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.MarkerOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers - alias Pleroma.Web.ApiSpec.Schemas.MarkersResponse - alias Pleroma.Web.ApiSpec.Schemas.MarkersUpsertRequest def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -16,7 +14,7 @@ def open_api_operation(action) do def index_operation do %Operation{ - tags: ["markers"], + tags: ["Markers"], summary: "Get saved timeline position", security: [%{"oAuth" => ["read:statuses"]}], operationId: "MarkerController.index", @@ -32,21 +30,111 @@ def index_operation do ) ], responses: %{ - 200 => Operation.response("Marker", "application/json", MarkersResponse) + 200 => Operation.response("Marker", "application/json", response()), + 403 => Operation.response("Error", "application/json", api_error()) } } end def upsert_operation do %Operation{ - tags: ["markers"], + tags: ["Markers"], summary: "Save position in timeline", operationId: "MarkerController.upsert", - requestBody: Helpers.request_body("Parameters", MarkersUpsertRequest, required: true), + requestBody: Helpers.request_body("Parameters", upsert_request(), required: true), security: [%{"oAuth" => ["follow", "write:blocks"]}], responses: %{ - 200 => Operation.response("Marker", "application/json", MarkersResponse) + 200 => Operation.response("Marker", "application/json", response()), + 403 => Operation.response("Error", "application/json", api_error()) } } end + + defp marker do + %Schema{ + title: "Marker", + description: "Schema for a marker", + type: :object, + properties: %{ + last_read_id: %Schema{type: :string}, + version: %Schema{type: :integer}, + updated_at: %Schema{type: :string}, + pleroma: %Schema{ + type: :object, + properties: %{ + unread_count: %Schema{type: :integer} + } + } + }, + example: %{ + "last_read_id" => "35098814", + "version" => 361, + "updated_at" => "2019-11-26T22:37:25.239Z", + "pleroma" => %{"unread_count" => 5} + } + } + end + + defp response do + %Schema{ + title: "MarkersResponse", + description: "Response schema for markers", + type: :object, + properties: %{ + notifications: %Schema{allOf: [marker()], nullable: true}, + home: %Schema{allOf: [marker()], nullable: true} + }, + items: %Schema{type: :string}, + example: %{ + "notifications" => %{ + "last_read_id" => "35098814", + "version" => 361, + "updated_at" => "2019-11-26T22:37:25.239Z", + "pleroma" => %{"unread_count" => 0} + }, + "home" => %{ + "last_read_id" => "103206604258487607", + "version" => 468, + "updated_at" => "2019-11-26T22:37:25.235Z", + "pleroma" => %{"unread_count" => 10} + } + } + } + end + + defp upsert_request do + %Schema{ + title: "MarkersUpsertRequest", + description: "Request schema for marker upsert", + type: :object, + properties: %{ + notifications: %Schema{ + type: :object, + properties: %{ + last_read_id: %Schema{type: :string} + } + }, + home: %Schema{ + type: :object, + properties: %{ + last_read_id: %Schema{type: :string} + } + } + }, + example: %{ + "home" => %{ + "last_read_id" => "103194548672408537", + "version" => 462, + "updated_at" => "2019-11-24T19:39:39.337Z" + } + } + } + end + + defp api_error do + %Schema{ + type: :object, + properties: %{error: %Schema{type: :string}} + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/marker.ex b/lib/pleroma/web/api_spec/schemas/marker.ex deleted file mode 100644 index 64fca5973..000000000 --- a/lib/pleroma/web/api_spec/schemas/marker.ex +++ /dev/null @@ -1,31 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.Marker do - require OpenApiSpex - alias OpenApiSpex.Schema - - OpenApiSpex.schema(%{ - title: "Marker", - description: "Schema for a marker", - type: :object, - properties: %{ - last_read_id: %Schema{type: :string}, - version: %Schema{type: :integer}, - updated_at: %Schema{type: :string}, - pleroma: %Schema{ - type: :object, - properties: %{ - unread_count: %Schema{type: :integer} - } - } - }, - example: %{ - "last_read_id" => "35098814", - "version" => 361, - "updated_at" => "2019-11-26T22:37:25.239Z", - "pleroma" => %{"unread_count" => 5} - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/markers_response.ex b/lib/pleroma/web/api_spec/schemas/markers_response.ex deleted file mode 100644 index cb1121931..000000000 --- a/lib/pleroma/web/api_spec/schemas/markers_response.ex +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.MarkersResponse do - require OpenApiSpex - alias OpenApiSpex.Schema - - alias Pleroma.Web.ApiSpec.Schemas.Marker - - OpenApiSpex.schema(%{ - title: "MarkersResponse", - description: "Response schema for markers", - type: :object, - properties: %{ - notifications: %Schema{allOf: [Marker], nullable: true}, - home: %Schema{allOf: [Marker], nullable: true} - }, - items: %Schema{type: :string}, - example: %{ - "notifications" => %{ - "last_read_id" => "35098814", - "version" => 361, - "updated_at" => "2019-11-26T22:37:25.239Z", - "pleroma" => %{"unread_count" => 0} - }, - "home" => %{ - "last_read_id" => "103206604258487607", - "version" => 468, - "updated_at" => "2019-11-26T22:37:25.235Z", - "pleroma" => %{"unread_count" => 10} - } - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex b/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex deleted file mode 100644 index 97dcc24b4..000000000 --- a/lib/pleroma/web/api_spec/schemas/markers_upsert_request.ex +++ /dev/null @@ -1,35 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.MarkersUpsertRequest do - require OpenApiSpex - alias OpenApiSpex.Schema - - OpenApiSpex.schema(%{ - title: "MarkersUpsertRequest", - description: "Request schema for marker upsert", - type: :object, - properties: %{ - notifications: %Schema{ - type: :object, - properties: %{ - last_read_id: %Schema{type: :string} - } - }, - home: %Schema{ - type: :object, - properties: %{ - last_read_id: %Schema{type: :string} - } - } - }, - example: %{ - "home" => %{ - "last_read_id" => "103194548672408537", - "version" => 462, - "updated_at" => "2019-11-24T19:39:39.337Z" - } - } - }) -end diff --git a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex index b94171b36..85310edfa 100644 --- a/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/marker_controller.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do use Pleroma.Web, :controller alias Pleroma.Plugs.OAuthScopesPlug + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug( OAuthScopesPlug, %{scopes: ["read:statuses"]} @@ -15,7 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.MarkerController do plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert) action_fallback(Pleroma.Web.MastodonAPI.FallbackController) - plug(OpenApiSpex.Plug.CastAndValidate) defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.MarkerOperation @@ -27,10 +28,7 @@ def index(%{assigns: %{user: user}} = conn, params) do # POST /api/v1/markers def upsert(%{assigns: %{user: user}, body_params: params} = conn, _) do - params = - params - |> Map.from_struct() - |> Map.new(fn {key, value} -> {to_string(key), value} end) + params = Map.new(params, fn {key, value} -> {to_string(key), value} end) with {:ok, result} <- Pleroma.Marker.upsert(user, params), markers <- Map.values(result) do diff --git a/lib/pleroma/web/mastodon_api/views/marker_view.ex b/lib/pleroma/web/mastodon_api/views/marker_view.ex index 985368fe5..9705b7a91 100644 --- a/lib/pleroma/web/mastodon_api/views/marker_view.ex +++ b/lib/pleroma/web/mastodon_api/views/marker_view.ex @@ -6,12 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.MarkerView do use Pleroma.Web, :view def render("markers.json", %{markers: markers}) do - Enum.reduce(markers, %{}, fn m, acc -> - Map.put_new(acc, m.timeline, %{ - last_read_id: m.last_read_id, - version: m.lock_version, - updated_at: NaiveDateTime.to_iso8601(m.updated_at) - }) + Map.new(markers, fn m -> + {m.timeline, + %{ + last_read_id: m.last_read_id, + version: m.lock_version, + updated_at: NaiveDateTime.to_iso8601(m.updated_at) + }} end) end end diff --git a/test/web/mastodon_api/controllers/marker_controller_test.exs b/test/web/mastodon_api/controllers/marker_controller_test.exs index 1c85ed032..bce719bea 100644 --- a/test/web/mastodon_api/controllers/marker_controller_test.exs +++ b/test/web/mastodon_api/controllers/marker_controller_test.exs @@ -4,10 +4,8 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.ApiSpec import Pleroma.Factory - import OpenApiSpex.TestAssertions describe "GET /api/v1/markers" do test "gets markers with correct scopes", %{conn: conn} do @@ -25,7 +23,7 @@ test "gets markers with correct scopes", %{conn: conn} do |> assign(:user, user) |> assign(:token, token) |> get("/api/v1/markers?timeline[]=notifications") - |> json_response(200) + |> json_response_and_validate_schema(200) assert response == %{ "notifications" => %{ @@ -34,8 +32,6 @@ test "gets markers with correct scopes", %{conn: conn} do "version" => 0 } } - - assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "gets markers with missed scopes", %{conn: conn} do @@ -49,7 +45,7 @@ test "gets markers with missed scopes", %{conn: conn} do |> assign(:user, user) |> assign(:token, token) |> get("/api/v1/markers", %{timeline: ["notifications"]}) - |> json_response(403) + |> json_response_and_validate_schema(403) assert response == %{"error" => "Insufficient permissions: read:statuses."} end @@ -69,7 +65,7 @@ test "creates a marker with correct scopes", %{conn: conn} do home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69420"} }) - |> json_response(200) + |> json_response_and_validate_schema(200) assert %{ "notifications" => %{ @@ -78,8 +74,6 @@ test "creates a marker with correct scopes", %{conn: conn} do "version" => 0 } } = response - - assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "updates exist marker", %{conn: conn} do @@ -101,7 +95,7 @@ test "updates exist marker", %{conn: conn} do home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69888"} }) - |> json_response(200) + |> json_response_and_validate_schema(200) assert response == %{ "notifications" => %{ @@ -110,8 +104,6 @@ test "updates exist marker", %{conn: conn} do "version" => 0 } } - - assert_schema(response, "MarkersResponse", ApiSpec.spec()) end test "creates a marker with missed scopes", %{conn: conn} do @@ -122,11 +114,12 @@ test "creates a marker with missed scopes", %{conn: conn} do conn |> assign(:user, user) |> assign(:token, token) + |> put_req_header("content-type", "application/json") |> post("/api/v1/markers", %{ home: %{last_read_id: "777"}, notifications: %{"last_read_id" => "69420"} }) - |> json_response(403) + |> json_response_and_validate_schema(403) assert response == %{"error" => "Insufficient permissions: write:statuses."} end From 5ec6aad5670cf0888942a13e83b9ffd16e97dd18 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 17:05:34 +0400 Subject: [PATCH 10/20] Add OpenAPI spec for ListController --- .../api_spec/operations/account_operation.ex | 19 +- .../web/api_spec/operations/list_operation.ex | 189 ++++++++++++++++++ lib/pleroma/web/api_spec/schemas/list.ex | 23 +++ .../controllers/list_controller.ex | 26 +-- test/support/conn_case.ex | 2 +- .../controllers/list_controller_test.exs | 60 ++++-- 6 files changed, 266 insertions(+), 53 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/list_operation.ex create mode 100644 lib/pleroma/web/api_spec/schemas/list.ex diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index fe9548b1b..470fc0215 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do alias Pleroma.Web.ApiSpec.Schemas.ActorType alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.BooleanLike + alias Pleroma.Web.ApiSpec.Schemas.List alias Pleroma.Web.ApiSpec.Schemas.Status alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope @@ -646,28 +647,12 @@ defp mute_request do } end - defp list do - %Schema{ - title: "List", - description: "Response schema for a list", - type: :object, - properties: %{ - id: %Schema{type: :string}, - title: %Schema{type: :string} - }, - example: %{ - "id" => "123", - "title" => "my list" - } - } - end - defp array_of_lists do %Schema{ title: "ArrayOfLists", description: "Response schema for lists", type: :array, - items: list(), + items: List, example: [ %{"id" => "123", "title" => "my list"}, %{"id" => "1337", "title" => "anotehr list"} diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex new file mode 100644 index 000000000..bb903a379 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -0,0 +1,189 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.ListOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.ApiError + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + alias Pleroma.Web.ApiSpec.Schemas.List + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Lists"], + summary: "Show user's lists", + description: "Fetch all lists that the user owns", + security: [%{"oAuth" => ["read:lists"]}], + operationId: "ListController.index", + responses: %{ + 200 => Operation.response("Array of List", "application/json", array_of_lists()) + } + } + end + + def create_operation do + %Operation{ + tags: ["Lists"], + summary: "Show a single list", + description: "Fetch the list with the given ID. Used for verifying the title of a list.", + operationId: "ListController.create", + requestBody: create_update_request(), + security: [%{"oAuth" => ["write:lists"]}], + responses: %{ + 200 => Operation.response("List", "application/json", List), + 400 => Operation.response("Error", "application/json", ApiError), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def show_operation do + %Operation{ + tags: ["Lists"], + summary: "Show a single list", + description: "Fetch the list with the given ID. Used for verifying the title of a list.", + operationId: "ListController.show", + parameters: [id_param()], + security: [%{"oAuth" => ["read:lists"]}], + responses: %{ + 200 => Operation.response("List", "application/json", List), + 404 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def update_operation do + %Operation{ + tags: ["Lists"], + summary: "Update a list", + description: "Change the title of a list", + operationId: "ListController.update", + parameters: [id_param()], + requestBody: create_update_request(), + security: [%{"oAuth" => ["write:lists"]}], + responses: %{ + 200 => Operation.response("List", "application/json", List), + 422 => Operation.response("Error", "application/json", ApiError) + } + } + end + + def delete_operation do + %Operation{ + tags: ["Lists"], + summary: "Delete a list", + operationId: "ListController.delete", + parameters: [id_param()], + security: [%{"oAuth" => ["write:lists"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) + } + } + end + + def list_accounts_operation do + %Operation{ + tags: ["Lists"], + summary: "View accounts in list", + operationId: "ListController.list_accounts", + parameters: [id_param()], + security: [%{"oAuth" => ["read:lists"]}], + responses: %{ + 200 => + Operation.response("Array of Account", "application/json", %Schema{ + type: :array, + items: Account + }) + } + } + end + + def add_to_list_operation do + %Operation{ + tags: ["Lists"], + summary: "Add accounts to list", + description: + "Add accounts to the given list. Note that the user must be following these accounts.", + operationId: "ListController.add_to_list", + parameters: [id_param()], + requestBody: add_remove_accounts_request(), + security: [%{"oAuth" => ["write:lists"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) + } + } + end + + def remove_from_list_operation do + %Operation{ + tags: ["Lists"], + summary: "Remove accounts from list", + operationId: "ListController.remove_from_list", + parameters: [id_param()], + requestBody: add_remove_accounts_request(), + security: [%{"oAuth" => ["write:lists"]}], + responses: %{ + 200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) + } + } + end + + defp array_of_lists do + %Schema{ + title: "ArrayOfLists", + description: "Response schema for lists", + type: :array, + items: List, + example: [ + %{"id" => "123", "title" => "my list"}, + %{"id" => "1337", "title" => "another list"} + ] + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "List ID", + example: "123", + required: true + ) + end + + defp create_update_request do + request_body( + "Parameters", + %Schema{ + description: "POST body for creating or updating a List", + type: :object, + properties: %{ + title: %Schema{type: :string, description: "List title"} + }, + required: [:title] + }, + required: true + ) + end + + defp add_remove_accounts_request do + request_body( + "Parameters", + %Schema{ + description: "POST body for adding/removing accounts to/from a List", + type: :object, + properties: %{ + account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID} + }, + required: [:account_ids] + }, + required: true + ) + end +end diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex new file mode 100644 index 000000000..78aa0736f --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -0,0 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.List do + alias OpenApiSpex.Schema + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "List", + description: "Represents a list of some users that the authenticated user follows", + type: :object, + properties: %{ + id: %Schema{type: :string, description: "The internal database ID of the list"}, + title: %Schema{type: :string, description: "The user-defined title of the list"} + }, + example: %{ + "id" => "12249", + "title" => "Friends" + } + }) +end diff --git a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex index bfe856025..acdc76fd2 100644 --- a/lib/pleroma/web/mastodon_api/controllers/list_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/list_controller.ex @@ -9,20 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.ListController do alias Pleroma.User alias Pleroma.Web.MastodonAPI.AccountView - plug(:list_by_id_and_user when action not in [:index, :create]) - @oauth_read_actions [:index, :show, :list_accounts] + plug(Pleroma.Web.ApiSpec.CastAndValidate) + plug(:list_by_id_and_user when action not in [:index, :create]) plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in @oauth_read_actions) - - plug( - OAuthScopesPlug, - %{scopes: ["write:lists"]} - when action not in @oauth_read_actions - ) + plug(OAuthScopesPlug, %{scopes: ["write:lists"]} when action not in @oauth_read_actions) action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ListOperation + # GET /api/v1/lists def index(%{assigns: %{user: user}} = conn, opts) do lists = Pleroma.List.for_user(user, opts) @@ -30,7 +27,7 @@ def index(%{assigns: %{user: user}} = conn, opts) do end # POST /api/v1/lists - def create(%{assigns: %{user: user}} = conn, %{"title" => title}) do + def create(%{assigns: %{user: user}, body_params: %{title: title}} = conn, _) do with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do render(conn, "show.json", list: list) end @@ -42,7 +39,7 @@ def show(%{assigns: %{list: list}} = conn, _) do end # PUT /api/v1/lists/:id - def update(%{assigns: %{list: list}} = conn, %{"title" => title}) do + def update(%{assigns: %{list: list}, body_params: %{title: title}} = conn, _) do with {:ok, list} <- Pleroma.List.rename(list, title) do render(conn, "show.json", list: list) end @@ -65,7 +62,7 @@ def list_accounts(%{assigns: %{user: user, list: list}} = conn, _) do end # POST /api/v1/lists/:id/accounts - def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do + def add_to_list(%{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, _) do Enum.each(account_ids, fn account_id -> with %User{} = followed <- User.get_cached_by_id(account_id) do Pleroma.List.follow(list, followed) @@ -76,7 +73,10 @@ def add_to_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids end # DELETE /api/v1/lists/:id/accounts - def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => account_ids}) do + def remove_from_list( + %{assigns: %{list: list}, body_params: %{account_ids: account_ids}} = conn, + _ + ) do Enum.each(account_ids, fn account_id -> with %User{} = followed <- User.get_cached_by_id(account_id) do Pleroma.List.unfollow(list, followed) @@ -86,7 +86,7 @@ def remove_from_list(%{assigns: %{list: list}} = conn, %{"account_ids" => accoun json(conn, %{}) end - defp list_by_id_and_user(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do + defp list_by_id_and_user(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do case Pleroma.List.get(id, user) do %Pleroma.List{} = list -> assign(conn, :list, list) nil -> conn |> render_error(:not_found, "List not found") |> halt() diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index fa30a0c41..91c03b1a8 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -74,7 +74,7 @@ defp json_response_and_validate_schema( status = Plug.Conn.Status.code(status) unless lookup[op_id].responses[status] do - err = "Response schema not found for #{conn.status} #{conn.method} #{conn.request_path}" + err = "Response schema not found for #{status} #{conn.method} #{conn.request_path}" flunk(err) end diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs index c9c4cbb49..57a9ef4a4 100644 --- a/test/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -12,37 +12,44 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do test "creating a list" do %{conn: conn} = oauth_access(["write:lists"]) - conn = post(conn, "/api/v1/lists", %{"title" => "cuties"}) - - assert %{"title" => title} = json_response(conn, 200) - assert title == "cuties" + assert %{"title" => "cuties"} = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response_and_validate_schema(:ok) end test "renders error for invalid params" do %{conn: conn} = oauth_access(["write:lists"]) - conn = post(conn, "/api/v1/lists", %{"title" => nil}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists", %{"title" => nil}) - assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + assert %{"error" => "title - null value where string expected."} = + json_response_and_validate_schema(conn, 400) end test "listing a user's lists" do %{conn: conn} = oauth_access(["read:lists", "write:lists"]) conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/lists", %{"title" => "cuties"}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) conn + |> put_req_header("content-type", "application/json") |> post("/api/v1/lists", %{"title" => "cofe"}) - |> json_response(:ok) + |> json_response_and_validate_schema(:ok) conn = get(conn, "/api/v1/lists") assert [ %{"id" => _, "title" => "cofe"}, %{"id" => _, "title" => "cuties"} - ] = json_response(conn, :ok) + ] = json_response_and_validate_schema(conn, :ok) end test "adding users to a list" do @@ -50,9 +57,12 @@ test "adding users to a list" do other_user = insert(:user) {:ok, list} = Pleroma.List.create("name", user) - conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) - assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [other_user.follower_address] end @@ -65,9 +75,12 @@ test "removing users from a list" do {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) - conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + assert %{} == + conn + |> put_req_header("content-type", "application/json") + |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + |> json_response_and_validate_schema(:ok) - assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [third_user.follower_address] end @@ -83,7 +96,7 @@ test "listing users in a list" do |> assign(:user, user) |> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) - assert [%{"id" => id}] = json_response(conn, 200) + assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200) assert id == to_string(other_user.id) end @@ -96,7 +109,7 @@ test "retrieving a list" do |> assign(:user, user) |> get("/api/v1/lists/#{list.id}") - assert %{"id" => id} = json_response(conn, 200) + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) assert id == to_string(list.id) end @@ -105,17 +118,18 @@ test "renders 404 if list is not found" do conn = get(conn, "/api/v1/lists/666") - assert %{"error" => "List not found"} = json_response(conn, :not_found) + assert %{"error" => "List not found"} = json_response_and_validate_schema(conn, :not_found) end test "renaming a list" do %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create("name", user) - conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"}) - - assert %{"title" => name} = json_response(conn, 200) - assert name == "newname" + assert %{"title" => "newname"} = + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + |> json_response_and_validate_schema(:ok) end test "validates title when renaming a list" do @@ -125,9 +139,11 @@ test "validates title when renaming a list" do conn = conn |> assign(:user, user) + |> put_req_header("content-type", "application/json") |> put("/api/v1/lists/#{list.id}", %{"title" => " "}) - assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) + assert %{"error" => "can't be blank"} == + json_response_and_validate_schema(conn, :unprocessable_entity) end test "deleting a list" do @@ -136,7 +152,7 @@ test "deleting a list" do conn = delete(conn, "/api/v1/lists/#{list.id}") - assert %{} = json_response(conn, 200) + assert %{} = json_response_and_validate_schema(conn, 200) assert is_nil(Repo.get(Pleroma.List, list.id)) end end From f2bf4390f4231d25486b803d426199975996f175 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 1 May 2020 19:53:00 +0400 Subject: [PATCH 11/20] Fix descriptions for List API spec --- lib/pleroma/web/api_spec/operations/list_operation.ex | 5 ++--- lib/pleroma/web/api_spec/schemas/list.ex | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/api_spec/operations/list_operation.ex b/lib/pleroma/web/api_spec/operations/list_operation.ex index bb903a379..c88ed5dd0 100644 --- a/lib/pleroma/web/api_spec/operations/list_operation.ex +++ b/lib/pleroma/web/api_spec/operations/list_operation.ex @@ -33,7 +33,7 @@ def index_operation do def create_operation do %Operation{ tags: ["Lists"], - summary: "Show a single list", + summary: "Create a list", description: "Fetch the list with the given ID. Used for verifying the title of a list.", operationId: "ListController.create", requestBody: create_update_request(), @@ -111,8 +111,7 @@ def add_to_list_operation do %Operation{ tags: ["Lists"], summary: "Add accounts to list", - description: - "Add accounts to the given list. Note that the user must be following these accounts.", + description: "Add accounts to the given list.", operationId: "ListController.add_to_list", parameters: [id_param()], requestBody: add_remove_accounts_request(), diff --git a/lib/pleroma/web/api_spec/schemas/list.ex b/lib/pleroma/web/api_spec/schemas/list.ex index 78aa0736f..b7d1685c9 100644 --- a/lib/pleroma/web/api_spec/schemas/list.ex +++ b/lib/pleroma/web/api_spec/schemas/list.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do OpenApiSpex.schema(%{ title: "List", - description: "Represents a list of some users that the authenticated user follows", + description: "Represents a list of users", type: :object, properties: %{ id: %Schema{type: :string, description: "The internal database ID of the list"}, From 88a14da8172cde6316926b5fbaa2f55b6da6f080 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 17:24:16 +0400 Subject: [PATCH 12/20] Add OpenAPI spec for InstanceController --- lib/pleroma/stats.ex | 2 +- .../api_spec/operations/instance_operation.ex | 169 ++++++++++++++++++ .../controllers/instance_controller.ex | 4 + .../controllers/instance_controller_test.exs | 6 +- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/instance_operation.ex diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 8d2809bbb..6b3a8a41f 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -91,7 +91,7 @@ def calculate_stat_data do peers: peers, stats: %{ domain_count: domain_count, - status_count: status_count, + status_count: status_count || 0, user_count: user_count } } diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex new file mode 100644 index 000000000..36a1a9043 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -0,0 +1,169 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.InstanceOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def show_operation do + %Operation{ + tags: ["Instance"], + summary: "Fetch instance", + description: "Information about the server", + operationId: "InstanceController.show", + responses: %{ + 200 => Operation.response("Instance", "application/json", instance()) + } + } + end + + def peers_operation do + %Operation{ + tags: ["Instance"], + summary: "List of connected domains", + operationId: "InstanceController.peers", + responses: %{ + 200 => Operation.response("Array of domains", "application/json", array_of_domains()) + } + } + end + + defp instance do + %Schema{ + type: :object, + properties: %{ + uri: %Schema{type: :string, description: "The domain name of the instance"}, + title: %Schema{type: :string, description: "The title of the website"}, + description: %Schema{ + type: :string, + description: "Admin-defined description of the Mastodon site" + }, + version: %Schema{ + type: :string, + description: "The version of Mastodon installed on the instance" + }, + email: %Schema{ + type: :string, + description: "An email that may be contacted for any inquiries", + format: :email + }, + urls: %Schema{ + type: :object, + description: "URLs of interest for clients apps", + properties: %{ + streaming_api: %Schema{ + type: :string, + description: "Websockets address for push streaming" + } + } + }, + stats: %Schema{ + type: :object, + description: "Statistics about how much information the instance contains", + properties: %{ + user_count: %Schema{ + type: :integer, + description: "Users registered on this instance" + }, + status_count: %Schema{ + type: :integer, + description: "Statuses authored by users on instance" + }, + domain_count: %Schema{ + type: :integer, + description: "Domains federated with this instance" + } + } + }, + thumbnail: %Schema{ + type: :string, + description: "Banner image for the website", + nullable: true + }, + languages: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Primary langauges of the website and its staff" + }, + registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"}, + # Extra (not present in Mastodon): + max_toot_chars: %Schema{ + type: :integer, + description: ": Posts character limit (CW/Subject included in the counter)" + }, + poll_limits: %Schema{ + type: :object, + description: "A map with poll limits for local polls", + properties: %{ + max_options: %Schema{ + type: :integer, + description: "Maximum number of options." + }, + max_option_chars: %Schema{ + type: :integer, + description: "Maximum number of characters per option." + }, + min_expiration: %Schema{ + type: :integer, + description: "Minimum expiration time (in seconds)." + }, + max_expiration: %Schema{ + type: :integer, + description: "Maximum expiration time (in seconds)." + } + } + }, + upload_limit: %Schema{ + type: :integer, + description: "File size limit of uploads (except for avatar, background, banner)" + }, + 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"} + }, + example: %{ + "avatar_upload_limit" => 2_000_000, + "background_upload_limit" => 4_000_000, + "banner_upload_limit" => 4_000_000, + "description" => "A Pleroma instance, an alternative fediverse server", + "email" => "lain@lain.com", + "languages" => ["en"], + "max_toot_chars" => 5000, + "poll_limits" => %{ + "max_expiration" => 31_536_000, + "max_option_chars" => 200, + "max_options" => 20, + "min_expiration" => 0 + }, + "registrations" => false, + "stats" => %{ + "domain_count" => 2996, + "status_count" => 15_802, + "user_count" => 5 + }, + "thumbnail" => "https://lain.com/instance/thumbnail.jpeg", + "title" => "lain.com", + "upload_limit" => 16_000_000, + "uri" => "https://lain.com", + "urls" => %{ + "streaming_api" => "wss://lain.com" + }, + "version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)" + } + } + end + + defp array_of_domains do + %Schema{ + type: :array, + items: %Schema{type: :string}, + example: ["pleroma.site", "lain.com", "bikeshed.party"] + } + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex index 237f85677..d8859731d 100644 --- a/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/instance_controller.ex @@ -5,12 +5,16 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do use Pleroma.Web, :controller + plug(OpenApiSpex.Plug.CastAndValidate) + plug( :skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug] when action in [:show, :peers] ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation + @doc "GET /api/v1/instance" def show(conn, _params) do render(conn, "show.json") diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index 2c7fd9fd0..90840d5ab 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -10,7 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceControllerTest do test "get instance information", %{conn: conn} do conn = get(conn, "/api/v1/instance") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) email = Pleroma.Config.get([:instance, :email]) # Note: not checking for "max_toot_chars" since it's optional @@ -56,7 +56,7 @@ test "get instance stats", %{conn: conn} do conn = get(conn, "/api/v1/instance") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) stats = result["stats"] @@ -74,7 +74,7 @@ test "get peers", %{conn: conn} do conn = get(conn, "/api/v1/instance/peers") - assert result = json_response(conn, 200) + assert result = json_response_and_validate_schema(conn, 200) assert ["peer1.com", "peer2.com"] == Enum.sort(result) end From b5189d2c50929aa67293e2e39ca020bad43f5f8b Mon Sep 17 00:00:00 2001 From: minibikini Date: Thu, 30 Apr 2020 17:45:48 +0000 Subject: [PATCH 13/20] Apply suggestion to lib/pleroma/web/api_spec/operations/instance_operation.ex --- lib/pleroma/web/api_spec/operations/instance_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 36a1a9043..9407fa74d 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -26,7 +26,7 @@ def show_operation do def peers_operation do %Operation{ tags: ["Instance"], - summary: "List of connected domains", + summary: "List of known hosts", operationId: "InstanceController.peers", responses: %{ 200 => Operation.response("Array of domains", "application/json", array_of_domains()) From 3817f179d777058259324d2e300780da06cce460 Mon Sep 17 00:00:00 2001 From: minibikini Date: Fri, 1 May 2020 12:46:53 +0000 Subject: [PATCH 14/20] Apply suggestion to lib/pleroma/web/api_spec/operations/instance_operation.ex --- lib/pleroma/web/api_spec/operations/instance_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 9407fa74d..5644cb54d 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -46,7 +46,7 @@ defp instance do }, version: %Schema{ type: :string, - description: "The version of Mastodon installed on the instance" + description: "The version of Pleroma installed on the instance" }, email: %Schema{ type: :string, From 42a4a863f159b863ec4617fc47697e11f92ff956 Mon Sep 17 00:00:00 2001 From: minibikini Date: Fri, 1 May 2020 12:46:56 +0000 Subject: [PATCH 15/20] Apply suggestion to lib/pleroma/web/api_spec/operations/instance_operation.ex --- lib/pleroma/web/api_spec/operations/instance_operation.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/api_spec/operations/instance_operation.ex b/lib/pleroma/web/api_spec/operations/instance_operation.ex index 5644cb54d..880bd3f1b 100644 --- a/lib/pleroma/web/api_spec/operations/instance_operation.ex +++ b/lib/pleroma/web/api_spec/operations/instance_operation.ex @@ -42,7 +42,7 @@ defp instance do title: %Schema{type: :string, description: "The title of the website"}, description: %Schema{ type: :string, - description: "Admin-defined description of the Mastodon site" + description: "Admin-defined description of the Pleroma site" }, version: %Schema{ type: :string, From ec1e4b4f1acb81fc36b396e7f58f67928dc6a0df Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 17:40:00 +0400 Subject: [PATCH 16/20] Add OpenAPI spec for FollowRequestController --- .../operations/follow_request_operation.ex | 65 +++++++++++++++++++ .../controllers/follow_request_controller.ex | 5 +- .../follow_request_controller_test.exs | 6 +- 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/follow_request_operation.ex diff --git a/lib/pleroma/web/api_spec/operations/follow_request_operation.ex b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex new file mode 100644 index 000000000..ac4aee6da --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/follow_request_operation.ex @@ -0,0 +1,65 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Follow Requests"], + summary: "Pending Follows", + security: [%{"oAuth" => ["read:follows", "follow"]}], + operationId: "FollowRequestController.index", + responses: %{ + 200 => + Operation.response("Array of Account", "application/json", %Schema{ + type: :array, + items: Account, + example: [Account.schema().example] + }) + } + } + end + + def authorize_operation do + %Operation{ + tags: ["Follow Requests"], + summary: "Accept Follow", + operationId: "FollowRequestController.authorize", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + def reject_operation do + %Operation{ + tags: ["Follow Requests"], + summary: "Reject Follow", + operationId: "FollowRequestController.reject", + parameters: [id_param()], + security: [%{"oAuth" => ["follow", "write:follows"]}], + responses: %{ + 200 => Operation.response("Relationship", "application/json", AccountRelationship) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex index 25f2269b9..748b6b475 100644 --- a/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do alias Pleroma.Web.CommonAPI plug(:put_view, Pleroma.Web.MastodonAPI.AccountView) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(:assign_follower when action != :index) action_fallback(:errors) @@ -21,6 +22,8 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestController do %{scopes: ["follow", "write:follows"]} when action != :index ) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FollowRequestOperation + @doc "GET /api/v1/follow_requests" def index(%{assigns: %{user: followed}} = conn, _params) do follow_requests = User.get_follow_requests(followed) @@ -42,7 +45,7 @@ def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do end end - defp assign_follower(%{params: %{"id" => id}} = conn, _) do + defp assign_follower(%{params: %{id: id}} = conn, _) do case User.get_cached_by_id(id) do %User{} = follower -> assign(conn, :follower, follower) nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt() diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index d8dbe4800..44e12d15a 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -27,7 +27,7 @@ test "/api/v1/follow_requests works", %{user: user, conn: conn} do conn = get(conn, "/api/v1/follow_requests") - assert [relationship] = json_response(conn, 200) + assert [relationship] = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] end @@ -44,7 +44,7 @@ test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") - assert relationship = json_response(conn, 200) + assert relationship = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] user = User.get_cached_by_id(user.id) @@ -62,7 +62,7 @@ test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") - assert relationship = json_response(conn, 200) + assert relationship = json_response_and_validate_schema(conn, 200) assert to_string(other_user.id) == relationship["id"] user = User.get_cached_by_id(user.id) From 7e7a3e15449792581412be002f287c504e3449a6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 14 Apr 2020 18:36:32 +0400 Subject: [PATCH 17/20] Add OpenAPI spec for FilterController --- lib/pleroma/filter.ex | 9 +- .../api_spec/operations/filter_operation.ex | 89 +++++++++++++++++++ lib/pleroma/web/api_spec/schemas/filter.ex | 51 +++++++++++ .../api_spec/schemas/filter_create_request.ex | 30 +++++++ .../api_spec/schemas/filter_update_request.ex | 41 +++++++++ .../web/api_spec/schemas/filters_response.ex | 40 +++++++++ .../controllers/filter_controller.ex | 54 +++++------ .../web/mastodon_api/views/filter_view.ex | 6 +- test/filter_test.exs | 10 +-- .../controllers/filter_controller_test.exs | 55 ++++++++++-- 10 files changed, 340 insertions(+), 45 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/filter_operation.ex create mode 100644 lib/pleroma/web/api_spec/schemas/filter.ex create mode 100644 lib/pleroma/web/api_spec/schemas/filter_create_request.ex create mode 100644 lib/pleroma/web/api_spec/schemas/filter_update_request.ex create mode 100644 lib/pleroma/web/api_spec/schemas/filters_response.ex diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 7cb49360f..4d61b3650 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -89,11 +89,10 @@ def delete(%Pleroma.Filter{id: filter_key} = filter) when is_nil(filter_key) do |> Repo.delete() end - def update(%Pleroma.Filter{} = filter) do - destination = Map.from_struct(filter) - - Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id}) - |> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word]) + def update(%Pleroma.Filter{} = filter, params) do + filter + |> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word]) + |> validate_required([:phrase, :context]) |> Repo.update() end end diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex new file mode 100644 index 000000000..0d673f566 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.FilterOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Helpers + alias Pleroma.Web.ApiSpec.Schemas.Filter + alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest + alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse + alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["apps"], + summary: "View all filters", + operationId: "FilterController.index", + security: [%{"oAuth" => ["read:filters"]}], + responses: %{ + 200 => Operation.response("Filters", "application/json", FiltersResponse) + } + } + end + + def create_operation do + %Operation{ + tags: ["apps"], + summary: "Create a filter", + operationId: "FilterController.create", + requestBody: Helpers.request_body("Parameters", FilterCreateRequest, required: true), + security: [%{"oAuth" => ["write:filters"]}], + responses: %{200 => Operation.response("Filter", "application/json", Filter)} + } + end + + def show_operation do + %Operation{ + tags: ["apps"], + summary: "View all filters", + parameters: [id_param()], + operationId: "FilterController.show", + security: [%{"oAuth" => ["read:filters"]}], + responses: %{ + 200 => Operation.response("Filter", "application/json", Filter) + } + } + end + + def update_operation do + %Operation{ + tags: ["apps"], + summary: "Update a filter", + parameters: [id_param()], + operationId: "FilterController.update", + requestBody: Helpers.request_body("Parameters", FilterUpdateRequest, required: true), + security: [%{"oAuth" => ["write:filters"]}], + responses: %{ + 200 => Operation.response("Filter", "application/json", Filter) + } + } + end + + def delete_operation do + %Operation{ + tags: ["apps"], + summary: "Remove a filter", + parameters: [id_param()], + operationId: "FilterController.delete", + security: [%{"oAuth" => ["write:filters"]}], + responses: %{ + 200 => + Operation.response("Filter", "application/json", %Schema{ + type: :object, + description: "Empty object" + }) + } + } + end + + defp id_param do + Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true) + end +end diff --git a/lib/pleroma/web/api_spec/schemas/filter.ex b/lib/pleroma/web/api_spec/schemas/filter.ex new file mode 100644 index 000000000..fc5480b71 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/filter.ex @@ -0,0 +1,51 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Filter do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Filter", + type: :object, + properties: %{ + id: %Schema{type: :string}, + phrase: %Schema{type: :string, description: "The text to be filtered"}, + context: %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, + description: "The contexts in which the filter should be applied." + }, + expires_at: %Schema{ + type: :string, + format: :"date-time", + description: + "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.", + nullable: true + }, + irreversible: %Schema{ + type: :boolean, + description: + "Should matching entities in home and notifications be dropped by the server?" + }, + whole_word: %Schema{ + type: :boolean, + description: "Should the filter consider word boundaries?" + } + }, + example: %{ + "id" => "5580", + "phrase" => "@twitter.com", + "context" => [ + "home", + "notifications", + "public", + "thread" + ], + "whole_word" => false, + "expires_at" => nil, + "irreversible" => true + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex new file mode 100644 index 000000000..f2a475b12 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex @@ -0,0 +1,30 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "FilterCreateRequest", + allOf: [ + %OpenApiSpex.Reference{"$ref": "#/components/schemas/FilterUpdateRequest"}, + %Schema{ + type: :object, + properties: %{ + irreversible: %Schema{ + type: :bolean, + description: + "Should the server irreversibly drop matching entities from home and notifications?", + default: false + } + } + } + ], + example: %{ + "phrase" => "knights", + "context" => ["home"] + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex new file mode 100644 index 000000000..e703db0ce --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest do + alias OpenApiSpex.Schema + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "FilterUpdateRequest", + type: :object, + properties: %{ + phrase: %Schema{type: :string, description: "The text to be filtered"}, + context: %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, + description: + "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." + }, + irreversible: %Schema{ + type: :bolean, + description: + "Should the server irreversibly drop matching entities from home and notifications?" + }, + whole_word: %Schema{type: :bolean, description: "Consider word boundaries?", default: true} + # TODO: probably should implement filter expiration + # expires_in: %Schema{ + # type: :string, + # format: :"date-time", + # description: + # "ISO 8601 Datetime for when the filter expires. Otherwise, + # null for a filter that doesn't expire." + # } + }, + required: [:phrase, :context], + example: %{ + "phrase" => "knights", + "context" => ["home"] + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/filters_response.ex b/lib/pleroma/web/api_spec/schemas/filters_response.ex new file mode 100644 index 000000000..8c56c5982 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/filters_response.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.FiltersResponse do + require OpenApiSpex + alias Pleroma.Web.ApiSpec.Schemas.Filter + + OpenApiSpex.schema(%{ + title: "FiltersResponse", + description: "Array of Filters", + type: :array, + items: Filter, + example: [ + %{ + "id" => "5580", + "phrase" => "@twitter.com", + "context" => [ + "home", + "notifications", + "public", + "thread" + ], + "whole_word" => false, + "expires_at" => nil, + "irreversible" => true + }, + %{ + "id" => "6191", + "phrase" => ":eurovision2019:", + "context" => [ + "home" + ], + "whole_word" => true, + "expires_at" => "2019-05-21T13:47:31.333Z", + "irreversible" => false + } + ] + }) +end diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index 7fd0562c9..dd13a8a09 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -10,67 +10,69 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do @oauth_read_actions [:show, :index] + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions) plug( OAuthScopesPlug, %{scopes: ["write:filters"]} when action not in @oauth_read_actions ) + + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation @doc "GET /api/v1/filters" def index(%{assigns: %{user: user}} = conn, _) do filters = Filter.get_filters(user) - render(conn, "filters.json", filters: filters) + render(conn, "index.json", filters: filters) end @doc "POST /api/v1/filters" - def create( - %{assigns: %{user: user}} = conn, - %{"phrase" => phrase, "context" => context} = params - ) do + def create(%{assigns: %{user: user}, body_params: params} = conn, _) do query = %Filter{ user_id: user.id, - phrase: phrase, - context: context, - hide: Map.get(params, "irreversible", false), - whole_word: Map.get(params, "boolean", true) + phrase: params.phrase, + context: params.context, + hide: params.irreversible, + whole_word: params.whole_word # expires_at } {:ok, response} = Filter.create(query) - render(conn, "filter.json", filter: response) + render(conn, "show.json", filter: response) end @doc "GET /api/v1/filters/:id" - def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + def show(%{assigns: %{user: user}} = conn, %{id: filter_id}) do filter = Filter.get(filter_id, user) - render(conn, "filter.json", filter: filter) + render(conn, "show.json", filter: filter) end @doc "PUT /api/v1/filters/:id" def update( - %{assigns: %{user: user}} = conn, - %{"phrase" => phrase, "context" => context, "id" => filter_id} = params + %{assigns: %{user: user}, body_params: params} = conn, + %{id: filter_id} ) do - query = %Filter{ - user_id: user.id, - filter_id: filter_id, - phrase: phrase, - context: context, - hide: Map.get(params, "irreversible", nil), - whole_word: Map.get(params, "boolean", true) - # expires_at - } + params = + params + |> Map.from_struct() + |> Map.delete(:irreversible) + |> Map.put(:hide, params.irreversible) + |> Enum.reject(fn {_key, value} -> is_nil(value) end) + |> Map.new() - {:ok, response} = Filter.update(query) - render(conn, "filter.json", filter: response) + # TODO: add expires_in -> expires_at + + with %Filter{} = filter <- Filter.get(filter_id, user), + {:ok, %Filter{} = filter} <- Filter.update(filter, params) do + render(conn, "show.json", filter: filter) + end end @doc "DELETE /api/v1/filters/:id" - def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do + def delete(%{assigns: %{user: user}} = conn, %{id: filter_id}) do query = %Filter{ user_id: user.id, filter_id: filter_id diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex index 97fd1e83f..8d5c381ec 100644 --- a/lib/pleroma/web/mastodon_api/views/filter_view.ex +++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex @@ -7,11 +7,11 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.FilterView - def render("filters.json", %{filters: filters} = opts) do - render_many(filters, FilterView, "filter.json", opts) + def render("index.json", %{filters: filters} = opts) do + render_many(filters, FilterView, "show.json", opts) end - def render("filter.json", %{filter: filter}) do + def render("show.json", %{filter: filter}) do expires_at = if filter.expires_at do Utils.to_masto_date(filter.expires_at) diff --git a/test/filter_test.exs b/test/filter_test.exs index b2a8330ee..63a30c736 100644 --- a/test/filter_test.exs +++ b/test/filter_test.exs @@ -141,17 +141,15 @@ test "updating a filter" do context: ["home"] } - query_two = %Pleroma.Filter{ - user_id: user.id, - filter_id: 1, + changes = %{ phrase: "who", context: ["home", "timeline"] } {:ok, filter_one} = Pleroma.Filter.create(query_one) - {:ok, filter_two} = Pleroma.Filter.update(query_two) + {:ok, filter_two} = Pleroma.Filter.update(filter_one, changes) assert filter_one != filter_two - assert filter_two.phrase == query_two.phrase - assert filter_two.context == query_two.context + assert filter_two.phrase == changes.phrase + assert filter_two.context == changes.context end end diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs index 97ab005e0..41a290eb2 100644 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -5,8 +5,15 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do use Pleroma.Web.ConnCase + alias Pleroma.Web.ApiSpec + alias Pleroma.Web.ApiSpec.Schemas.Filter + alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest + alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse + alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest alias Pleroma.Web.MastodonAPI.FilterView + import OpenApiSpex.TestAssertions + test "creating a filter" do %{conn: conn} = oauth_access(["write:filters"]) @@ -15,7 +22,10 @@ test "creating a filter" do context: ["home"] } - conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) assert response = json_response(conn, 200) assert response["phrase"] == filter.phrase @@ -23,6 +33,7 @@ test "creating a filter" do assert response["irreversible"] == false assert response["id"] != nil assert response["id"] != "" + assert_schema(response, "Filter", ApiSpec.spec()) end test "fetching a list of filters" do @@ -53,9 +64,11 @@ test "fetching a list of filters" do assert response == render_json( FilterView, - "filters.json", + "index.json", filters: [filter_two, filter_one] ) + + assert_schema(response, "FiltersResponse", ApiSpec.spec()) end test "get a filter" do @@ -72,7 +85,8 @@ test "get a filter" do conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - assert _response = json_response(conn, 200) + assert response = json_response(conn, 200) + assert_schema(response, "Filter", ApiSpec.spec()) end test "update a filter" do @@ -82,7 +96,8 @@ test "update a filter" do user_id: user.id, filter_id: 2, phrase: "knight", - context: ["home"] + context: ["home"], + hide: true } {:ok, _filter} = Pleroma.Filter.create(query) @@ -93,7 +108,9 @@ test "update a filter" do } conn = - put(conn, "/api/v1/filters/#{query.filter_id}", %{ + conn + |> put_req_header("content-type", "application/json") + |> put("/api/v1/filters/#{query.filter_id}", %{ phrase: new.phrase, context: new.context }) @@ -101,6 +118,8 @@ test "update a filter" do assert response = json_response(conn, 200) assert response["phrase"] == new.phrase assert response["context"] == new.context + assert response["irreversible"] == true + assert_schema(response, "Filter", ApiSpec.spec()) end test "delete a filter" do @@ -120,4 +139,30 @@ test "delete a filter" do assert response = json_response(conn, 200) assert response == %{} end + + describe "OpenAPI" do + test "Filter example matches schema" do + api_spec = ApiSpec.spec() + schema = Filter.schema() + assert_schema(schema.example, "Filter", api_spec) + end + + test "FiltersResponse example matches schema" do + api_spec = ApiSpec.spec() + schema = FiltersResponse.schema() + assert_schema(schema.example, "FiltersResponse", api_spec) + end + + test "FilterCreateRequest example matches schema" do + api_spec = ApiSpec.spec() + schema = FilterCreateRequest.schema() + assert_schema(schema.example, "FilterCreateRequest", api_spec) + end + + test "FilterUpdateRequest example matches schema" do + api_spec = ApiSpec.spec() + schema = FilterUpdateRequest.schema() + assert_schema(schema.example, "FilterUpdateRequest", api_spec) + end + end end From 46aae346f8530d4b9933b8e718e9578a96447f0a Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 27 Apr 2020 23:54:11 +0400 Subject: [PATCH 18/20] Move single used schemas to Filter operation schema --- .../api_spec/operations/filter_operation.ex | 158 ++++++++++++++++-- lib/pleroma/web/api_spec/schemas/filter.ex | 51 ------ .../api_spec/schemas/filter_create_request.ex | 30 ---- .../api_spec/schemas/filter_update_request.ex | 41 ----- .../web/api_spec/schemas/filters_response.ex | 40 ----- .../controllers/filter_controller.ex | 7 +- .../web/mastodon_api/views/filter_view.ex | 4 +- .../controllers/filter_controller_test.exs | 49 +----- 8 files changed, 158 insertions(+), 222 deletions(-) delete mode 100644 lib/pleroma/web/api_spec/schemas/filter.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/filter_create_request.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/filter_update_request.ex delete mode 100644 lib/pleroma/web/api_spec/schemas/filters_response.ex diff --git a/lib/pleroma/web/api_spec/operations/filter_operation.ex b/lib/pleroma/web/api_spec/operations/filter_operation.ex index 0d673f566..53e57b46b 100644 --- a/lib/pleroma/web/api_spec/operations/filter_operation.ex +++ b/lib/pleroma/web/api_spec/operations/filter_operation.ex @@ -6,10 +6,6 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do alias OpenApiSpex.Operation alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Helpers - alias Pleroma.Web.ApiSpec.Schemas.Filter - alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest - alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse - alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest def open_api_operation(action) do operation = String.to_existing_atom("#{action}_operation") @@ -23,7 +19,7 @@ def index_operation do operationId: "FilterController.index", security: [%{"oAuth" => ["read:filters"]}], responses: %{ - 200 => Operation.response("Filters", "application/json", FiltersResponse) + 200 => Operation.response("Filters", "application/json", array_of_filters()) } } end @@ -33,9 +29,9 @@ def create_operation do tags: ["apps"], summary: "Create a filter", operationId: "FilterController.create", - requestBody: Helpers.request_body("Parameters", FilterCreateRequest, required: true), + requestBody: Helpers.request_body("Parameters", create_request(), required: true), security: [%{"oAuth" => ["write:filters"]}], - responses: %{200 => Operation.response("Filter", "application/json", Filter)} + responses: %{200 => Operation.response("Filter", "application/json", filter())} } end @@ -47,7 +43,7 @@ def show_operation do operationId: "FilterController.show", security: [%{"oAuth" => ["read:filters"]}], responses: %{ - 200 => Operation.response("Filter", "application/json", Filter) + 200 => Operation.response("Filter", "application/json", filter()) } } end @@ -58,10 +54,10 @@ def update_operation do summary: "Update a filter", parameters: [id_param()], operationId: "FilterController.update", - requestBody: Helpers.request_body("Parameters", FilterUpdateRequest, required: true), + requestBody: Helpers.request_body("Parameters", update_request(), required: true), security: [%{"oAuth" => ["write:filters"]}], responses: %{ - 200 => Operation.response("Filter", "application/json", Filter) + 200 => Operation.response("Filter", "application/json", filter()) } } end @@ -86,4 +82,146 @@ def delete_operation do defp id_param do Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true) end + + defp filter do + %Schema{ + title: "Filter", + type: :object, + properties: %{ + id: %Schema{type: :string}, + phrase: %Schema{type: :string, description: "The text to be filtered"}, + context: %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, + description: "The contexts in which the filter should be applied." + }, + expires_at: %Schema{ + type: :string, + format: :"date-time", + description: + "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.", + nullable: true + }, + irreversible: %Schema{ + type: :boolean, + description: + "Should matching entities in home and notifications be dropped by the server?" + }, + whole_word: %Schema{ + type: :boolean, + description: "Should the filter consider word boundaries?" + } + }, + example: %{ + "id" => "5580", + "phrase" => "@twitter.com", + "context" => [ + "home", + "notifications", + "public", + "thread" + ], + "whole_word" => false, + "expires_at" => nil, + "irreversible" => true + } + } + end + + defp array_of_filters do + %Schema{ + title: "ArrayOfFilters", + description: "Array of Filters", + type: :array, + items: filter(), + example: [ + %{ + "id" => "5580", + "phrase" => "@twitter.com", + "context" => [ + "home", + "notifications", + "public", + "thread" + ], + "whole_word" => false, + "expires_at" => nil, + "irreversible" => true + }, + %{ + "id" => "6191", + "phrase" => ":eurovision2019:", + "context" => [ + "home" + ], + "whole_word" => true, + "expires_at" => "2019-05-21T13:47:31.333Z", + "irreversible" => false + } + ] + } + end + + defp create_request do + %Schema{ + title: "FilterCreateRequest", + allOf: [ + update_request(), + %Schema{ + type: :object, + properties: %{ + irreversible: %Schema{ + type: :bolean, + description: + "Should the server irreversibly drop matching entities from home and notifications?", + default: false + } + } + } + ], + example: %{ + "phrase" => "knights", + "context" => ["home"] + } + } + end + + defp update_request do + %Schema{ + title: "FilterUpdateRequest", + type: :object, + properties: %{ + phrase: %Schema{type: :string, description: "The text to be filtered"}, + context: %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, + description: + "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." + }, + irreversible: %Schema{ + type: :bolean, + description: + "Should the server irreversibly drop matching entities from home and notifications?" + }, + whole_word: %Schema{ + type: :bolean, + description: "Consider word boundaries?", + default: true + } + # TODO: probably should implement filter expiration + # expires_in: %Schema{ + # type: :string, + # format: :"date-time", + # description: + # "ISO 8601 Datetime for when the filter expires. Otherwise, + # null for a filter that doesn't expire." + # } + }, + required: [:phrase, :context], + example: %{ + "phrase" => "knights", + "context" => ["home"] + } + } + end end diff --git a/lib/pleroma/web/api_spec/schemas/filter.ex b/lib/pleroma/web/api_spec/schemas/filter.ex deleted file mode 100644 index fc5480b71..000000000 --- a/lib/pleroma/web/api_spec/schemas/filter.ex +++ /dev/null @@ -1,51 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.Filter do - alias OpenApiSpex.Schema - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "Filter", - type: :object, - properties: %{ - id: %Schema{type: :string}, - phrase: %Schema{type: :string, description: "The text to be filtered"}, - context: %Schema{ - type: :array, - items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, - description: "The contexts in which the filter should be applied." - }, - expires_at: %Schema{ - type: :string, - format: :"date-time", - description: - "When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.", - nullable: true - }, - irreversible: %Schema{ - type: :boolean, - description: - "Should matching entities in home and notifications be dropped by the server?" - }, - whole_word: %Schema{ - type: :boolean, - description: "Should the filter consider word boundaries?" - } - }, - example: %{ - "id" => "5580", - "phrase" => "@twitter.com", - "context" => [ - "home", - "notifications", - "public", - "thread" - ], - "whole_word" => false, - "expires_at" => nil, - "irreversible" => true - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex b/lib/pleroma/web/api_spec/schemas/filter_create_request.ex deleted file mode 100644 index f2a475b12..000000000 --- a/lib/pleroma/web/api_spec/schemas/filter_create_request.ex +++ /dev/null @@ -1,30 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest do - alias OpenApiSpex.Schema - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "FilterCreateRequest", - allOf: [ - %OpenApiSpex.Reference{"$ref": "#/components/schemas/FilterUpdateRequest"}, - %Schema{ - type: :object, - properties: %{ - irreversible: %Schema{ - type: :bolean, - description: - "Should the server irreversibly drop matching entities from home and notifications?", - default: false - } - } - } - ], - example: %{ - "phrase" => "knights", - "context" => ["home"] - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex b/lib/pleroma/web/api_spec/schemas/filter_update_request.ex deleted file mode 100644 index e703db0ce..000000000 --- a/lib/pleroma/web/api_spec/schemas/filter_update_request.ex +++ /dev/null @@ -1,41 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest do - alias OpenApiSpex.Schema - require OpenApiSpex - - OpenApiSpex.schema(%{ - title: "FilterUpdateRequest", - type: :object, - properties: %{ - phrase: %Schema{type: :string, description: "The text to be filtered"}, - context: %Schema{ - type: :array, - items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]}, - description: - "Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified." - }, - irreversible: %Schema{ - type: :bolean, - description: - "Should the server irreversibly drop matching entities from home and notifications?" - }, - whole_word: %Schema{type: :bolean, description: "Consider word boundaries?", default: true} - # TODO: probably should implement filter expiration - # expires_in: %Schema{ - # type: :string, - # format: :"date-time", - # description: - # "ISO 8601 Datetime for when the filter expires. Otherwise, - # null for a filter that doesn't expire." - # } - }, - required: [:phrase, :context], - example: %{ - "phrase" => "knights", - "context" => ["home"] - } - }) -end diff --git a/lib/pleroma/web/api_spec/schemas/filters_response.ex b/lib/pleroma/web/api_spec/schemas/filters_response.ex deleted file mode 100644 index 8c56c5982..000000000 --- a/lib/pleroma/web/api_spec/schemas/filters_response.ex +++ /dev/null @@ -1,40 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.ApiSpec.Schemas.FiltersResponse do - require OpenApiSpex - alias Pleroma.Web.ApiSpec.Schemas.Filter - - OpenApiSpex.schema(%{ - title: "FiltersResponse", - description: "Array of Filters", - type: :array, - items: Filter, - example: [ - %{ - "id" => "5580", - "phrase" => "@twitter.com", - "context" => [ - "home", - "notifications", - "public", - "thread" - ], - "whole_word" => false, - "expires_at" => nil, - "irreversible" => true - }, - %{ - "id" => "6191", - "phrase" => ":eurovision2019:", - "context" => [ - "home" - ], - "whole_word" => true, - "expires_at" => "2019-05-21T13:47:31.333Z", - "irreversible" => false - } - ] - }) -end diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index dd13a8a09..21dc374cd 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -35,7 +35,7 @@ def create(%{assigns: %{user: user}, body_params: params} = conn, _) do context: params.context, hide: params.irreversible, whole_word: params.whole_word - # expires_at + # TODO: support `expires_in` parameter (as in Mastodon API) } {:ok, response} = Filter.create(query) @@ -57,13 +57,12 @@ def update( ) do params = params - |> Map.from_struct() |> Map.delete(:irreversible) - |> Map.put(:hide, params.irreversible) + |> Map.put(:hide, params[:irreversible]) |> Enum.reject(fn {_key, value} -> is_nil(value) end) |> Map.new() - # TODO: add expires_in -> expires_at + # TODO: support `expires_in` parameter (as in Mastodon API) with %Filter{} = filter <- Filter.get(filter_id, user), {:ok, %Filter{} = filter} <- Filter.update(filter, params) do diff --git a/lib/pleroma/web/mastodon_api/views/filter_view.ex b/lib/pleroma/web/mastodon_api/views/filter_view.ex index 8d5c381ec..aeff646f5 100644 --- a/lib/pleroma/web/mastodon_api/views/filter_view.ex +++ b/lib/pleroma/web/mastodon_api/views/filter_view.ex @@ -7,8 +7,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterView do alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.MastodonAPI.FilterView - def render("index.json", %{filters: filters} = opts) do - render_many(filters, FilterView, "show.json", opts) + def render("index.json", %{filters: filters}) do + render_many(filters, FilterView, "show.json") end def render("show.json", %{filter: filter}) do diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs index 41a290eb2..f29547d13 100644 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -5,15 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Web.ApiSpec - alias Pleroma.Web.ApiSpec.Schemas.Filter - alias Pleroma.Web.ApiSpec.Schemas.FilterCreateRequest - alias Pleroma.Web.ApiSpec.Schemas.FiltersResponse - alias Pleroma.Web.ApiSpec.Schemas.FilterUpdateRequest alias Pleroma.Web.MastodonAPI.FilterView - import OpenApiSpex.TestAssertions - test "creating a filter" do %{conn: conn} = oauth_access(["write:filters"]) @@ -27,13 +20,12 @@ test "creating a filter" do |> put_req_header("content-type", "application/json") |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) - assert response = json_response(conn, 200) + assert response = json_response_and_validate_schema(conn, 200) assert response["phrase"] == filter.phrase assert response["context"] == filter.context assert response["irreversible"] == false assert response["id"] != nil assert response["id"] != "" - assert_schema(response, "Filter", ApiSpec.spec()) end test "fetching a list of filters" do @@ -59,7 +51,7 @@ test "fetching a list of filters" do response = conn |> get("/api/v1/filters") - |> json_response(200) + |> json_response_and_validate_schema(200) assert response == render_json( @@ -67,8 +59,6 @@ test "fetching a list of filters" do "index.json", filters: [filter_two, filter_one] ) - - assert_schema(response, "FiltersResponse", ApiSpec.spec()) end test "get a filter" do @@ -85,8 +75,7 @@ test "get a filter" do conn = get(conn, "/api/v1/filters/#{filter.filter_id}") - assert response = json_response(conn, 200) - assert_schema(response, "Filter", ApiSpec.spec()) + assert response = json_response_and_validate_schema(conn, 200) end test "update a filter" do @@ -115,11 +104,10 @@ test "update a filter" do context: new.context }) - assert response = json_response(conn, 200) + assert response = json_response_and_validate_schema(conn, 200) assert response["phrase"] == new.phrase assert response["context"] == new.context assert response["irreversible"] == true - assert_schema(response, "Filter", ApiSpec.spec()) end test "delete a filter" do @@ -136,33 +124,6 @@ test "delete a filter" do conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") - assert response = json_response(conn, 200) - assert response == %{} - end - - describe "OpenAPI" do - test "Filter example matches schema" do - api_spec = ApiSpec.spec() - schema = Filter.schema() - assert_schema(schema.example, "Filter", api_spec) - end - - test "FiltersResponse example matches schema" do - api_spec = ApiSpec.spec() - schema = FiltersResponse.schema() - assert_schema(schema.example, "FiltersResponse", api_spec) - end - - test "FilterCreateRequest example matches schema" do - api_spec = ApiSpec.spec() - schema = FilterCreateRequest.schema() - assert_schema(schema.example, "FilterCreateRequest", api_spec) - end - - test "FilterUpdateRequest example matches schema" do - api_spec = ApiSpec.spec() - schema = FilterUpdateRequest.schema() - assert_schema(schema.example, "FilterUpdateRequest", api_spec) - end + assert json_response_and_validate_schema(conn, 200) == %{} end end From 32ca9f2c59369c15905f665bee3c759ae963ff91 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 28 Apr 2020 16:25:13 +0400 Subject: [PATCH 19/20] Render mastodon-like errors in FilterController --- lib/pleroma/web/mastodon_api/controllers/filter_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex index 21dc374cd..abbf0ce02 100644 --- a/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/filter_controller.ex @@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.FilterController do OAuthScopesPlug, %{scopes: ["write:filters"]} when action not in @oauth_read_actions ) - + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FilterOperation @doc "GET /api/v1/filters" From 3a45952a3a324e5fb823e9bdf3ffe19fb3923cb3 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 5 May 2020 17:44:46 +0400 Subject: [PATCH 20/20] Add OpenAPI spec for ConversationController --- lib/pleroma/conversation/participation.ex | 4 +- .../operations/conversation_operation.ex | 61 +++++++++++++++++++ .../web/api_spec/schemas/conversation.ex | 41 +++++++++++++ lib/pleroma/web/api_spec/schemas/status.ex | 7 ++- .../controllers/conversation_controller.ex | 5 +- .../conversation_controller_test.exs | 22 +++---- 6 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 lib/pleroma/web/api_spec/operations/conversation_operation.ex create mode 100644 lib/pleroma/web/api_spec/schemas/conversation.ex diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 215265fc9..51bb1bda9 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -128,7 +128,7 @@ def for_user(user, params \\ %{}) do |> Pleroma.Pagination.fetch_paginated(params) end - def restrict_recipients(query, user, %{"recipients" => user_ids}) do + def restrict_recipients(query, user, %{recipients: user_ids}) do user_binary_ids = [user.id | user_ids] |> Enum.uniq() @@ -172,7 +172,7 @@ def for_user_with_last_activity_id(user, params \\ %{}) do | last_activity_id: activity_id } end) - |> Enum.filter(& &1.last_activity_id) + |> Enum.reject(&is_nil(&1.last_activity_id)) end def get(_, _ \\ []) diff --git a/lib/pleroma/web/api_spec/operations/conversation_operation.ex b/lib/pleroma/web/api_spec/operations/conversation_operation.ex new file mode 100644 index 000000000..475468893 --- /dev/null +++ b/lib/pleroma/web/api_spec/operations/conversation_operation.ex @@ -0,0 +1,61 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.ConversationOperation do + alias OpenApiSpex.Operation + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Conversation + alias Pleroma.Web.ApiSpec.Schemas.FlakeID + + import Pleroma.Web.ApiSpec.Helpers + + def open_api_operation(action) do + operation = String.to_existing_atom("#{action}_operation") + apply(__MODULE__, operation, []) + end + + def index_operation do + %Operation{ + tags: ["Conversations"], + summary: "Show conversation", + security: [%{"oAuth" => ["read:statuses"]}], + operationId: "ConversationController.index", + parameters: [ + Operation.parameter( + :recipients, + :query, + %Schema{type: :array, items: FlakeID}, + "Only return conversations with the given recipients (a list of user ids)" + ) + | pagination_params() + ], + responses: %{ + 200 => + Operation.response("Array of Conversation", "application/json", %Schema{ + type: :array, + items: Conversation, + example: [Conversation.schema().example] + }) + } + } + end + + def mark_as_read_operation do + %Operation{ + tags: ["Conversations"], + summary: "Mark as read", + operationId: "ConversationController.mark_as_read", + parameters: [ + Operation.parameter(:id, :path, :string, "Conversation ID", + example: "123", + required: true + ) + ], + security: [%{"oAuth" => ["write:conversations"]}], + responses: %{ + 200 => Operation.response("Conversation", "application/json", Conversation) + } + } + end +end diff --git a/lib/pleroma/web/api_spec/schemas/conversation.ex b/lib/pleroma/web/api_spec/schemas/conversation.ex new file mode 100644 index 000000000..d8ff5ba26 --- /dev/null +++ b/lib/pleroma/web/api_spec/schemas/conversation.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ApiSpec.Schemas.Conversation do + alias OpenApiSpex.Schema + alias Pleroma.Web.ApiSpec.Schemas.Account + alias Pleroma.Web.ApiSpec.Schemas.Status + + require OpenApiSpex + + OpenApiSpex.schema(%{ + title: "Conversation", + description: "Represents a conversation with \"direct message\" visibility.", + type: :object, + required: [:id, :accounts, :unread], + properties: %{ + id: %Schema{type: :string}, + accounts: %Schema{ + type: :array, + items: Account, + description: "Participants in the conversation" + }, + unread: %Schema{ + type: :boolean, + description: "Is the conversation currently marked as unread?" + }, + # last_status: Status + last_status: %Schema{ + allOf: [Status], + description: "The last status in the conversation, to be used for optional display" + } + }, + example: %{ + "id" => "418450", + "unread" => true, + "accounts" => [Account.schema().example], + "last_status" => Status.schema().example + } + }) +end diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index aef0588d4..42e9dae19 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -86,7 +86,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do properties: %{ content: %Schema{type: :object, additionalProperties: %Schema{type: :string}}, conversation_id: %Schema{type: :integer}, - direct_conversation_id: %Schema{type: :string, nullable: true}, + direct_conversation_id: %Schema{ + type: :integer, + nullable: true, + description: + "The ID of the Mastodon direct message conversation the status is associated with (if any)" + }, emoji_reactions: %Schema{ type: :array, items: %Schema{ diff --git a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex index c44641526..f35ec3596 100644 --- a/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex @@ -13,9 +13,12 @@ defmodule Pleroma.Web.MastodonAPI.ConversationController do action_fallback(Pleroma.Web.MastodonAPI.FallbackController) + plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index) plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action != :index) + defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ConversationOperation + @doc "GET /api/v1/conversations" def index(%{assigns: %{user: user}} = conn, params) do participations = Participation.for_user_with_last_activity_id(user, params) @@ -26,7 +29,7 @@ def index(%{assigns: %{user: user}} = conn, params) do end @doc "POST /api/v1/conversations/:id/read" - def mark_as_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + def mark_as_read(%{assigns: %{user: user}} = conn, %{id: participation_id}) do with %Participation{} = participation <- Repo.get_by(Participation, id: participation_id, user_id: user.id), {:ok, participation} <- Participation.mark_as_read(participation) do diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs index 801b0259b..04695572e 100644 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -36,7 +36,7 @@ test "returns a list of conversations", %{user: user_one, conn: conn} do res_conn = get(conn, "/api/v1/conversations") - assert response = json_response(res_conn, 200) + assert response = json_response_and_validate_schema(res_conn, 200) assert [ %{ @@ -91,18 +91,18 @@ test "filters conversations by recipients", %{user: user_one, conn: conn} do "visibility" => "direct" }) - [conversation1, conversation2] = - conn - |> get("/api/v1/conversations", %{"recipients" => [user_two.id]}) - |> json_response(200) + assert [conversation1, conversation2] = + conn + |> get("/api/v1/conversations?recipients[]=#{user_two.id}") + |> json_response_and_validate_schema(200) assert conversation1["last_status"]["id"] == direct5.id assert conversation2["last_status"]["id"] == direct1.id [conversation1] = conn - |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]}) - |> json_response(200) + |> get("/api/v1/conversations?recipients[]=#{user_two.id}&recipients[]=#{user_three.id}") + |> json_response_and_validate_schema(200) assert conversation1["last_status"]["id"] == direct3.id end @@ -126,7 +126,7 @@ test "updates the last_status on reply", %{user: user_one, conn: conn} do [%{"last_status" => res_last_status}] = conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) assert res_last_status["id"] == direct_reply.id end @@ -154,12 +154,12 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do [%{"id" => direct_conversation_id, "unread" => true}] = user_two_conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) %{"unread" => false} = user_two_conn |> post("/api/v1/conversations/#{direct_conversation_id}/read") - |> json_response(200) + |> json_response_and_validate_schema(200) assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 @@ -175,7 +175,7 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do [%{"unread" => true}] = conn |> get("/api/v1/conversations") - |> json_response(200) + |> json_response_and_validate_schema(200) assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0