From 2c4844237f294d27f58737f9694f77b1cfcb10e7 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Thu, 30 Apr 2020 18:19:51 +0300
Subject: [PATCH 01/37] 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 <https://pleroma.social/>
+# 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 <alex.strizhakov@gmail.com>
Date: Fri, 1 May 2020 14:31:24 +0300
Subject: [PATCH 02/37] 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 <alex.strizhakov@gmail.com>
Date: Fri, 1 May 2020 16:31:05 +0300
Subject: [PATCH 03/37] 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 370e313e2df19e4579c154388336dd5e09bff7bf Mon Sep 17 00:00:00 2001
From: Mark Felder <feld@FreeBSD.org>
Date: Sat, 2 May 2020 13:28:10 -0500
Subject: [PATCH 04/37] Only update follower/following stats for actor types of
 users and bots.

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 1f4a09370..31304c340 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1532,9 +1532,14 @@ defp normalize_counter(_), do: 0
 
   defp maybe_update_follow_information(data) do
     with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
-         {:ok, info} <- fetch_follow_information_for_user(data) do
+         {:ok, info} <- fetch_follow_information_for_user(data),
+         {:ok, actor_type} <- Map.fetch(data, :actor_type) do
       info = Map.merge(data[:info] || %{}, info)
-      Map.put(data, :info, info)
+
+      cond do
+        actor_type in ["Person", "Service"] -> Map.put(data, :info, info)
+        true -> data
+      end
     else
       {:enabled, false} ->
         data

From a7966f2080a0e9b3c2b35efa7ea647c1bdef2a2d Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Sun, 3 May 2020 13:48:01 +0200
Subject: [PATCH 05/37] 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 <lain@soykaf.club>
Date: Mon, 4 May 2020 12:22:31 +0200
Subject: [PATCH 06/37] 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 ec24c70db80665aaaf4d7794ea62e9f9e6676bec Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 4 May 2020 14:22:54 +0200
Subject: [PATCH 07/37] ActivityPub: Don't fetch `Application` follower counts.

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 30 +++++++-----
 test/web/activity_pub/activity_pub_test.exs  | 50 +++++++++++++++++++-
 2 files changed, 68 insertions(+), 12 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 31304c340..1c21d78af 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1530,26 +1530,34 @@ def fetch_follow_information_for_user(user) do
   defp normalize_counter(counter) when is_integer(counter), do: counter
   defp normalize_counter(_), do: 0
 
-  defp maybe_update_follow_information(data) do
+  def maybe_update_follow_information(user_data) do
     with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
-         {:ok, info} <- fetch_follow_information_for_user(data),
-         {:ok, actor_type} <- Map.fetch(data, :actor_type) do
-      info = Map.merge(data[:info] || %{}, info)
+         {_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
+         {_, true} <-
+           {:collections_available,
+            !!(user_data[:following_address] && user_data[:follower_address])},
+         {:ok, info} <-
+           fetch_follow_information_for_user(user_data) do
+      info = Map.merge(user_data[:info] || %{}, info)
 
-      cond do
-        actor_type in ["Person", "Service"] -> Map.put(data, :info, info)
-        true -> data
-      end
+      user_data
+      |> Map.put(:info, info)
     else
+      {:user_type_check, false} ->
+        user_data
+
+      {:collections_available, false} ->
+        user_data
+
       {:enabled, false} ->
-        data
+        user_data
 
       e ->
         Logger.error(
-          "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
+          "Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
         )
 
-        data
+        user_data
     end
   end
 
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index edd7dfb22..84ead93bb 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -18,9 +18,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.Federator
 
+  import ExUnit.CaptureLog
+  import Mock
   import Pleroma.Factory
   import Tesla.Mock
-  import Mock
 
   setup do
     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -2403,4 +2404,51 @@ defp private_messages(_) do
      u3: %{r1: r3_1.id, r2: r3_2.id},
      u4: %{r1: r4_1.id}}
   end
+
+  describe "maybe_update_follow_information/1" do
+    setup do
+      clear_config([:instance, :external_user_synchronization], true)
+
+      user = %{
+        local: false,
+        ap_id: "https://gensokyo.2hu/users/raymoo",
+        following_address: "https://gensokyo.2hu/users/following",
+        follower_address: "https://gensokyo.2hu/users/followers",
+        type: "Person"
+      }
+
+      %{user: user}
+    end
+
+    test "logs an error when it can't fetch the info", %{user: user} do
+      assert capture_log(fn ->
+               ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+
+    test "just returns the input if the user type is Application", %{
+      user: user
+    } do
+      user =
+        user
+        |> Map.put(:type, "Application")
+
+      refute capture_log(fn ->
+               assert ^user = ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+
+    test "it just returns the input if the user has no following/follower addresses", %{
+      user: user
+    } do
+      user =
+        user
+        |> Map.put(:following_address, nil)
+        |> Map.put(:follower_address, nil)
+
+      refute capture_log(fn ->
+               assert ^user = ActivityPub.maybe_update_follow_information(user)
+             end) =~ "Follower/Following counter update for #{user.ap_id} failed"
+    end
+  end
 end

From d08c63500b5deca268ebc24833be4cb3279bdaaa Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 4 May 2020 20:16:18 +0400
Subject: [PATCH 08/37] Ignore unexpected query params

---
 lib/pleroma/web/api_spec/cast_and_validate.ex | 120 ++++++++++++++++++
 .../controllers/account_controller.ex         |   2 +-
 .../controllers/app_controller.ex             |   2 +-
 .../controllers/custom_emoji_controller.ex    |   2 +-
 .../controllers/domain_block_controller.ex    |   2 +-
 .../controllers/notification_controller.ex    |   2 +-
 .../controllers/report_controller.ex          |   2 +-
 7 files changed, 126 insertions(+), 6 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/cast_and_validate.ex

diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
new file mode 100644
index 000000000..f36cf7a55
--- /dev/null
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -0,0 +1,120 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.CastAndValidate do
+  @moduledoc """
+  This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
+  (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
+  The main difference is ignoring unexpected query params
+  instead of throwing an error. Also, the default rendering
+  error module is `Pleroma.Web.ApiSpec.RenderError`.
+  """
+
+  @behaviour Plug
+
+  alias Plug.Conn
+
+  @impl Plug
+  def init(opts) do
+    opts
+    |> Map.new()
+    |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
+  end
+
+  @impl Plug
+  def call(%{private: %{open_api_spex: private_data}} = conn, %{
+        operation_id: operation_id,
+        render_error: render_error
+      }) do
+    spec = private_data.spec
+    operation = private_data.operation_lookup[operation_id]
+
+    content_type =
+      case Conn.get_req_header(conn, "content-type") do
+        [header_value | _] ->
+          header_value
+          |> String.split(";")
+          |> List.first()
+
+        _ ->
+          nil
+      end
+
+    private_data = Map.put(private_data, :operation_id, operation_id)
+    conn = Conn.put_private(conn, :open_api_spex, private_data)
+
+    case cast_and_validate(spec, operation, conn, content_type) do
+      {:ok, conn} ->
+        conn
+
+      {:error, reason} ->
+        opts = render_error.init(reason)
+
+        conn
+        |> render_error.call(opts)
+        |> Plug.Conn.halt()
+    end
+  end
+
+  def call(
+        %{
+          private: %{
+            phoenix_controller: controller,
+            phoenix_action: action,
+            open_api_spex: private_data
+          }
+        } = conn,
+        opts
+      ) do
+    operation =
+      case private_data.operation_lookup[{controller, action}] do
+        nil ->
+          operation_id = controller.open_api_operation(action).operationId
+          operation = private_data.operation_lookup[operation_id]
+
+          operation_lookup =
+            private_data.operation_lookup
+            |> Map.put({controller, action}, operation)
+
+          OpenApiSpex.Plug.Cache.adapter().put(
+            private_data.spec_module,
+            {private_data.spec, operation_lookup}
+          )
+
+          operation
+
+        operation ->
+          operation
+      end
+
+    if operation.operationId do
+      call(conn, Map.put(opts, :operation_id, operation.operationId))
+    else
+      raise "operationId was not found in action API spec"
+    end
+  end
+
+  def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
+
+  defp cast_and_validate(spec, operation, conn, content_type) do
+    case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
+      {:ok, conn} ->
+        {:ok, conn}
+
+      # Remove unexpected query params and cast/validate again
+      {:error, errors} ->
+        query_params =
+          Enum.reduce(errors, conn.query_params, fn
+            %{reason: :unexpected_field, name: name, path: [name]}, params ->
+              Map.delete(params, name)
+
+            _, params ->
+              params
+          end)
+
+        conn = %Conn{conn | query_params: query_params}
+        OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+    end
+  end
+end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 61b0e2f63..8458cbdd5 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   alias Pleroma.Web.OAuth.Token
   alias Pleroma.Web.TwitterAPI.TwitterAPI
 
-  plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
 
   plug(:skip_plug, [OAuthScopesPlug, EnsurePublicOrAuthenticatedPlug] when action == :create)
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
index 408e11474..a516b6c20 100644
--- a/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/app_controller.ex
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
 
   plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
 
-  plug(OpenApiSpex.Plug.CastAndValidate)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
 
   @local_mastodon_name "Mastodon-Local"
 
diff --git a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
index 000ad743f..c5f47c5df 100644
--- a/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/custom_emoji_controller.ex
@@ -5,7 +5,7 @@
 defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
   use Pleroma.Web, :controller
 
-  plug(OpenApiSpex.Plug.CastAndValidate)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
 
   plug(
     :skip_plug,
diff --git a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
index c4fa383f2..825b231ab 100644
--- a/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
   alias Pleroma.Plugs.OAuthScopesPlug
   alias Pleroma.User
 
-  plug(OpenApiSpex.Plug.CastAndValidate)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
 
   plug(
diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
index a14c86893..596b85617 100644
--- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
 
   @oauth_read_actions [:show, :index]
 
-  plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
 
   plug(
     OAuthScopesPlug,
diff --git a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
index f65c5c62b..405167108 100644
--- a/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/report_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.ReportController do
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
-  plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
   plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)
 
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ReportOperation

From bfbff7d82673d6128a18e73dcc91f70ee669c2ac Mon Sep 17 00:00:00 2001
From: minibikini <egor@kislitsyn.com>
Date: Mon, 4 May 2020 16:38:23 +0000
Subject: [PATCH 09/37] Apply suggestion to
 lib/pleroma/web/api_spec/cast_and_validate.ex

---
 lib/pleroma/web/api_spec/cast_and_validate.ex | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index f36cf7a55..cd02403c1 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -1,5 +1,6 @@
 # Pleroma: A lightweight social networking server
-# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
+# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.ApiSpec.CastAndValidate do

From 4b9ab67aa8bdf7fdf7390080932fee2e5879a5e4 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 4 May 2020 21:46:25 +0400
Subject: [PATCH 10/37] Ignore unexpected ENUM values in query string

---
 lib/pleroma/web/api_spec/cast_and_validate.ex | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index cd02403c1..b94517c52 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -110,6 +110,10 @@ defp cast_and_validate(spec, operation, conn, content_type) do
             %{reason: :unexpected_field, name: name, path: [name]}, params ->
               Map.delete(params, name)
 
+            %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
+              path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
+              update_in(params, path, &List.delete(&1, value))
+
             _, params ->
               params
           end)
@@ -118,4 +122,11 @@ defp cast_and_validate(spec, operation, conn, content_type) do
         OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
     end
   end
+
+  defp list_items_to_string(list) do
+    Enum.map(list, fn
+      i when is_atom(i) -> to_string(i)
+      i -> i
+    end)
+  end
 end

From f070b5569ca0eafdca79f1f3e3b6b5025f3f8fc9 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 4 May 2020 22:33:05 +0400
Subject: [PATCH 11/37] Add a config option to enable strict validation

---
 config/config.exs                             |  2 ++
 lib/pleroma/web/api_spec/cast_and_validate.ex | 17 ++++++++++++-----
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index a6c6d6f99..ca9bbab64 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -653,6 +653,8 @@
   profiles: %{local: false, remote: false},
   activities: %{local: false, remote: false}
 
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env()}.exs"
diff --git a/lib/pleroma/web/api_spec/cast_and_validate.ex b/lib/pleroma/web/api_spec/cast_and_validate.ex
index b94517c52..bd9026237 100644
--- a/lib/pleroma/web/api_spec/cast_and_validate.ex
+++ b/lib/pleroma/web/api_spec/cast_and_validate.ex
@@ -7,9 +7,10 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
   @moduledoc """
   This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
   (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
-  The main difference is ignoring unexpected query params
-  instead of throwing an error. Also, the default rendering
-  error module is `Pleroma.Web.ApiSpec.RenderError`.
+  The main difference is ignoring unexpected query params instead of throwing
+  an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
+  to disable this behavior. Also, the default rendering error module
+  is `Pleroma.Web.ApiSpec.RenderError`.
   """
 
   @behaviour Plug
@@ -45,7 +46,7 @@ def call(%{private: %{open_api_spex: private_data}} = conn, %{
     private_data = Map.put(private_data, :operation_id, operation_id)
     conn = Conn.put_private(conn, :open_api_spex, private_data)
 
-    case cast_and_validate(spec, operation, conn, content_type) do
+    case cast_and_validate(spec, operation, conn, content_type, strict?()) do
       {:ok, conn} ->
         conn
 
@@ -98,7 +99,11 @@ def call(
 
   def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
 
-  defp cast_and_validate(spec, operation, conn, content_type) do
+  defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
+    OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
+  end
+
+  defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
     case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
       {:ok, conn} ->
         {:ok, conn}
@@ -129,4 +134,6 @@ defp list_items_to_string(list) do
       i -> i
     end)
   end
+
+  defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
 end

From e55fd530bc9a6ab42e475efe689e239963906928 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 4 May 2020 22:33:34 +0400
Subject: [PATCH 12/37] Render better errors for ENUM validation

---
 lib/pleroma/web/api_spec/render_error.ex | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/lib/pleroma/web/api_spec/render_error.ex b/lib/pleroma/web/api_spec/render_error.ex
index b5877ca9c..d476b8ef3 100644
--- a/lib/pleroma/web/api_spec/render_error.ex
+++ b/lib/pleroma/web/api_spec/render_error.ex
@@ -17,6 +17,9 @@ def init(opts), do: opts
   def call(conn, errors) do
     errors =
       Enum.map(errors, fn
+        %{name: nil, reason: :invalid_enum} = err ->
+          %OpenApiSpex.Cast.Error{err | name: err.value}
+
         %{name: nil} = err ->
           %OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
 

From 1cb89aac1eef7711aa7950fe03e02e24bc665317 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 4 May 2020 22:35:28 +0400
Subject: [PATCH 13/37] Enable strict validation mode in dev and test
 environments

---
 config/dev.exs  | 2 ++
 config/test.exs | 2 ++
 2 files changed, 4 insertions(+)

diff --git a/config/dev.exs b/config/dev.exs
index 7e1e3b4be..4faaeff5b 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -52,6 +52,8 @@
   hostname: "localhost",
   pool_size: 10
 
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
 if File.exists?("./config/dev.secret.exs") do
   import_config "dev.secret.exs"
 else
diff --git a/config/test.exs b/config/test.exs
index 040e67e4a..cbf775109 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -96,6 +96,8 @@
 
 config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
 
+config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
+
 if File.exists?("./config/test.secret.exs") do
   import_config "test.secret.exs"
 else

From 2ab52d52248224c61b857f2fc54adff80343e3c0 Mon Sep 17 00:00:00 2001
From: Angelina Filippova <linakirsanova@gmail.com>
Date: Mon, 4 May 2020 22:41:14 +0300
Subject: [PATCH 14/37] Fix inconsistency in language for activating settings

---
 config/description.exs | 1 +
 1 file changed, 1 insertion(+)

diff --git a/config/description.exs b/config/description.exs
index 7fac1e561..62f17c92f 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2247,6 +2247,7 @@
         children: [
           %{
             key: :active,
+            label: "Enabled",
             type: :boolean,
             description: "Globally enable or disable digest emails"
           },

From bf0e41f0daa5809db53ed4a9130ade63952e8da0 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Mon, 4 May 2020 23:32:53 +0200
Subject: [PATCH 15/37] 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 8bed6ea922dbc1cfb8166fea6ce344d3618b3d52 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 5 May 2020 09:25:09 +0200
Subject: [PATCH 16/37] User, Webfinger: Remove OStatus vestiges

Mainly the `magic_key` field
---
 lib/pleroma/user.ex                           |  2 -
 lib/pleroma/web/web_finger/web_finger.ex      | 39 +------------------
 .../20200505072231_remove_magic_key_field.exs |  9 +++++
 test/web/web_finger/web_finger_test.exs       |  4 +-
 4 files changed, 12 insertions(+), 42 deletions(-)
 create mode 100644 priv/repo/migrations/20200505072231_remove_magic_key_field.exs

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 99358ddaf..2c343eb22 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -113,7 +113,6 @@ defmodule Pleroma.User do
     field(:is_admin, :boolean, default: false)
     field(:show_role, :boolean, default: true)
     field(:settings, :map, default: nil)
-    field(:magic_key, :string, default: nil)
     field(:uri, Types.Uri, default: nil)
     field(:hide_followers_count, :boolean, default: false)
     field(:hide_follows_count, :boolean, default: false)
@@ -387,7 +386,6 @@ def remote_user_changeset(struct \\ %User{local: false}, params) do
         :banner,
         :locked,
         :last_refreshed_at,
-        :magic_key,
         :uri,
         :follower_address,
         :following_address,
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 7ffd0e51b..b26453828 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -86,53 +86,19 @@ def represent_user(user, "XML") do
     |> XmlBuilder.to_doc()
   end
 
-  defp get_magic_key("data:application/magic-public-key," <> magic_key) do
-    {:ok, magic_key}
-  end
-
-  defp get_magic_key(nil) do
-    Logger.debug("Undefined magic key.")
-    {:ok, nil}
-  end
-
-  defp get_magic_key(_) do
-    {:error, "Missing magic key data."}
-  end
-
   defp webfinger_from_xml(doc) do
-    with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
-         {:ok, magic_key} <- get_magic_key(magic_key),
-         topic <-
-           XML.string_from_xpath(
-             ~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
-             doc
-           ),
-         subject <- XML.string_from_xpath("//Subject", doc),
-         subscribe_address <-
-           XML.string_from_xpath(
-             ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
-             doc
-           ),
+    with subject <- XML.string_from_xpath("//Subject", doc),
          ap_id <-
            XML.string_from_xpath(
              ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
              doc
            ) do
       data = %{
-        "magic_key" => magic_key,
-        "topic" => topic,
         "subject" => subject,
-        "subscribe_address" => subscribe_address,
         "ap_id" => ap_id
       }
 
       {:ok, data}
-    else
-      {:error, e} ->
-        {:error, e}
-
-      e ->
-        {:error, e}
     end
   end
 
@@ -146,9 +112,6 @@ defp webfinger_from_json(doc) do
           {"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "self"} ->
             Map.put(data, "ap_id", link["href"])
 
-          {_, "http://ostatus.org/schema/1.0/subscribe"} ->
-            Map.put(data, "subscribe_address", link["template"])
-
           _ ->
             Logger.debug("Unhandled type: #{inspect(link["type"])}")
             data
diff --git a/priv/repo/migrations/20200505072231_remove_magic_key_field.exs b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
new file mode 100644
index 000000000..2635e671b
--- /dev/null
+++ b/priv/repo/migrations/20200505072231_remove_magic_key_field.exs
@@ -0,0 +1,9 @@
+defmodule Pleroma.Repo.Migrations.RemoveMagicKeyField do
+  use Ecto.Migration
+
+  def change do
+    alter table(:users) do
+      remove(:magic_key, :string)
+    end
+  end
+end
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 4b4282727..ce17f83d6 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -67,10 +67,10 @@ test "it work for AP-only user" do
       assert data["magic_key"] == nil
       assert data["salmon"] == nil
 
-      assert data["topic"] == "https://mstdn.jp/users/kPherox.atom"
+      assert data["topic"] == nil
       assert data["subject"] == "acct:kPherox@mstdn.jp"
       assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
-      assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
+      assert data["subscribe_address"] == nil
     end
 
     test "it works for friendica" do

From f897da21158796eb3962e50add312d62165160fc Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 5 May 2020 09:36:38 +0200
Subject: [PATCH 17/37] WebFinger: Add back in subscribe_address.

It's used for remote following.
---
 lib/pleroma/web/web_finger/web_finger.ex | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index b26453828..d0775fa28 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -88,6 +88,11 @@ def represent_user(user, "XML") do
 
   defp webfinger_from_xml(doc) do
     with subject <- XML.string_from_xpath("//Subject", doc),
+         subscribe_address <-
+           XML.string_from_xpath(
+             ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
+             doc
+           ),
          ap_id <-
            XML.string_from_xpath(
              ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
@@ -95,6 +100,7 @@ defp webfinger_from_xml(doc) do
            ) do
       data = %{
         "subject" => subject,
+        "subscribe_address" => subscribe_address,
         "ap_id" => ap_id
       }
 

From 6a2905ccf08f89bd988b1bcd0788566930fbf17e Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 5 May 2020 09:55:33 +0200
Subject: [PATCH 18/37] WebFinger Test: Add back test.

---
 test/web/web_finger/web_finger_test.exs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index ce17f83d6..f4884e0a2 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -70,7 +70,7 @@ test "it work for AP-only user" do
       assert data["topic"] == nil
       assert data["subject"] == "acct:kPherox@mstdn.jp"
       assert data["ap_id"] == "https://mstdn.jp/users/kPherox"
-      assert data["subscribe_address"] == nil
+      assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}"
     end
 
     test "it works for friendica" do

From cc922e7d8ccbf22a0f7e0898a6ff4639123f0c7f Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 5 May 2020 14:44:29 +0400
Subject: [PATCH 19/37] Document configuration for
 Pleroma.Web.ApiSpec.CastAndValidate

---
 config/description.exs           | 14 ++++++++++++++
 docs/configuration/cheatsheet.md |  6 +++++-
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/config/description.exs b/config/description.exs
index 9d8e3b93c..72bb4d436 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -3194,5 +3194,19 @@
         ]
       }
     ]
+  },
+  %{
+    group: :pleroma,
+    key: Pleroma.Web.ApiSpec.CastAndValidate,
+    type: :group,
+    children: [
+      %{
+        key: :strict,
+        type: :boolean,
+        description:
+          "Enables strict input validation (useful in development, not recommended in production)",
+        suggestions: [false]
+      }
+    ]
   }
 ]
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 681ab6b93..705c4c15e 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -924,4 +924,8 @@ Restrict access for unauthenticated users to timelines (public and federate), us
   * `remote`
 * `activities` - statuses
   * `local`
-  * `remote`
\ No newline at end of file
+  * `remote`
+
+## Pleroma.Web.ApiSpec.CastAndValidate
+
+* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.

From d20152700470c9b84a9404193ff08dd6d90b97a3 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 5 May 2020 11:17:44 +0000
Subject: [PATCH 20/37] Apply suggestion to
 lib/pleroma/web/web_finger/web_finger.ex

---
 lib/pleroma/web/web_finger/web_finger.ex | 34 +++++++++++-------------
 1 file changed, 16 insertions(+), 18 deletions(-)

diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index d0775fa28..84ece1be2 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -87,25 +87,23 @@ def represent_user(user, "XML") do
   end
 
   defp webfinger_from_xml(doc) do
-    with subject <- XML.string_from_xpath("//Subject", doc),
-         subscribe_address <-
-           XML.string_from_xpath(
-             ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
-             doc
-           ),
-         ap_id <-
-           XML.string_from_xpath(
-             ~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
-             doc
-           ) do
-      data = %{
-        "subject" => subject,
-        "subscribe_address" => subscribe_address,
-        "ap_id" => ap_id
-      }
+    subject = XML.string_from_xpath("//Subject", doc)
 
-      {:ok, data}
-    end
+    subscribe_address =
+      ~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}
+      |> XML.string_from_xpath(doc)
+
+    ap_id =
+      ~s{//Link[@rel="self" and @type="application/activity+json"]/@href}
+      |> XML.string_from_xpath(doc)
+
+    data = %{
+      "subject" => subject,
+      "subscribe_address" => subscribe_address,
+      "ap_id" => ap_id
+    }
+
+    {:ok, data}
   end
 
   defp webfinger_from_json(doc) do

From d861b0790a62767b31b8a85862fc249a4f8ca542 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 5 May 2020 16:43:00 +0400
Subject: [PATCH 21/37] 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Tue, 14 Apr 2020 22:50:29 +0400
Subject: [PATCH 22/37] 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Tue, 5 May 2020 16:45:34 +0400
Subject: [PATCH 23/37] 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 <https://pleroma.social/>
-# 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 <https://pleroma.social/>
-# 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 <https://pleroma.social/>
-# 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 <egor@kislitsyn.com>
Date: Tue, 5 May 2020 17:05:34 +0400
Subject: [PATCH 24/37] 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Fri, 1 May 2020 19:53:00 +0400
Subject: [PATCH 25/37] 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 e7d8ab8303cb69682a75c30a356572a75deb9837 Mon Sep 17 00:00:00 2001
From: Alexander Strizhakov <alex.strizhakov@gmail.com>
Date: Tue, 5 May 2020 16:08:44 +0300
Subject: [PATCH 26/37] admin_api fetch status by id

---
 CHANGELOG.md                                  |  1 +
 docs/API/admin_api.md                         | 11 +++++++++++
 .../web/admin_api/admin_api_controller.ex     | 12 +++++++++++-
 lib/pleroma/web/router.ex                     |  1 +
 .../admin_api/admin_api_controller_test.exs   | 19 +++++++++++++++++++
 5 files changed, 43 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 522285efe..114bfac4d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
 - Mastodon API: Add support for filtering replies in public and home timelines
 - Admin API: endpoints for create/update/delete OAuth Apps.
+- Admin API: endpoint for status view.
 </details>
 
 ### Fixed
diff --git a/docs/API/admin_api.md b/docs/API/admin_api.md
index 6202c5a1a..23af08961 100644
--- a/docs/API/admin_api.md
+++ b/docs/API/admin_api.md
@@ -755,6 +755,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
     - 400 Bad Request `"Invalid parameters"` when `status` is missing
   - On success: `204`, empty response
 
+## `GET /api/pleroma/admin/statuses/:id`
+
+### Show status by id
+
+- Params:
+  - `id`: required, status id
+- Response:
+  - On failure:
+    - 404 Not Found `"Not Found"`
+  - On success: JSON, Mastodon Status entity
+
 ## `PUT /api/pleroma/admin/statuses/:id`
 
 ### Change the scope of an individual reported status
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 816c11e01..ac661e515 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -93,7 +93,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:statuses"], admin: true}
-    when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
+    when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
   )
 
   plug(
@@ -837,6 +837,16 @@ def list_statuses(%{assigns: %{user: _admin}} = conn, params) do
     |> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
   end
 
+  def status_show(conn, %{"id" => id}) do
+    with %Activity{} = activity <- Activity.get_by_id(id) do
+      conn
+      |> put_view(StatusView)
+      |> render("show.json", %{activity: activity})
+    else
+      _ -> errors(conn, {:error, :not_found})
+    end
+  end
+
   def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
     with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
       {:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5b00243e9..ef2239d59 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -188,6 +188,7 @@ defmodule Pleroma.Web.Router do
     post("/reports/:id/notes", AdminAPIController, :report_notes_create)
     delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
 
+    get("/statuses/:id", AdminAPIController, :status_show)
     put("/statuses/:id", AdminAPIController, :status_update)
     delete("/statuses/:id", AdminAPIController, :status_delete)
     get("/statuses", AdminAPIController, :list_statuses)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 1862a9589..c3f3ad051 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1620,6 +1620,25 @@ test "returns 403 when requested by anonymous" do
     end
   end
 
+  describe "GET /api/pleroma/admin/statuses/:id" do
+    test "not found", %{conn: conn} do
+      assert conn
+             |> get("/api/pleroma/admin/statuses/not_found")
+             |> json_response(:not_found)
+    end
+
+    test "shows activity", %{conn: conn} do
+      activity = insert(:note_activity)
+
+      response =
+        conn
+        |> get("/api/pleroma/admin/statuses/#{activity.id}")
+        |> json_response(200)
+
+      assert response["id"] == activity.id
+    end
+  end
+
   describe "PUT /api/pleroma/admin/statuses/:id" do
     setup do
       activity = insert(:note_activity)

From 88a14da8172cde6316926b5fbaa2f55b6da6f080 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 5 May 2020 17:24:16 +0400
Subject: [PATCH 27/37] 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Thu, 30 Apr 2020 17:45:48 +0000
Subject: [PATCH 28/37] 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 <egor@kislitsyn.com>
Date: Fri, 1 May 2020 12:46:53 +0000
Subject: [PATCH 29/37] 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 <egor@kislitsyn.com>
Date: Fri, 1 May 2020 12:46:56 +0000
Subject: [PATCH 30/37] 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 <egor@kislitsyn.com>
Date: Tue, 5 May 2020 17:40:00 +0400
Subject: [PATCH 31/37] 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Tue, 14 Apr 2020 18:36:32 +0400
Subject: [PATCH 32/37] 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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 <egor@kislitsyn.com>
Date: Mon, 27 Apr 2020 23:54:11 +0400
Subject: [PATCH 33/37] 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 <https://pleroma.social/>
-# 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 <https://pleroma.social/>
-# 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 <https://pleroma.social/>
-# 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 <https://pleroma.social/>
-# 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 <egor@kislitsyn.com>
Date: Tue, 28 Apr 2020 16:25:13 +0400
Subject: [PATCH 34/37] 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 <egor@kislitsyn.com>
Date: Tue, 5 May 2020 17:44:46 +0400
Subject: [PATCH 35/37] 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 <https://pleroma.social/>
+# 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 <https://pleroma.social/>
+# 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

From 6ba25d11973e56008e5d674313421197ff418d6d Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 5 May 2020 23:19:16 +0400
Subject: [PATCH 36/37] Add Attachment schema

---
 .../web/api_spec/schemas/attachment.ex        | 68 +++++++++++++++++++
 .../web/api_spec/schemas/scheduled_status.ex  | 53 +++++++++++++++
 lib/pleroma/web/api_spec/schemas/status.ex    | 18 +----
 3 files changed, 123 insertions(+), 16 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/schemas/attachment.ex
 create mode 100644 lib/pleroma/web/api_spec/schemas/scheduled_status.ex

diff --git a/lib/pleroma/web/api_spec/schemas/attachment.ex b/lib/pleroma/web/api_spec/schemas/attachment.ex
new file mode 100644
index 000000000..c146c416e
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/attachment.ex
@@ -0,0 +1,68 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.Attachment do
+  alias OpenApiSpex.Schema
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "Attachment",
+    description: "Represents a file or media attachment that can be added to a status.",
+    type: :object,
+    requried: [:id, :url, :preview_url],
+    properties: %{
+      id: %Schema{type: :string},
+      url: %Schema{
+        type: :string,
+        format: :uri,
+        description: "The location of the original full-size attachment"
+      },
+      remote_url: %Schema{
+        type: :string,
+        format: :uri,
+        description:
+          "The location of the full-size original attachment on the remote website. String (URL), or null if the attachment is local",
+        nullable: true
+      },
+      preview_url: %Schema{
+        type: :string,
+        format: :uri,
+        description: "The location of a scaled-down preview of the attachment"
+      },
+      text_url: %Schema{
+        type: :string,
+        format: :uri,
+        description: "A shorter URL for the attachment"
+      },
+      description: %Schema{
+        type: :string,
+        nullable: true,
+        description:
+          "Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load"
+      },
+      type: %Schema{
+        type: :string,
+        enum: ["image", "video", "audio", "unknown"],
+        description: "The type of the attachment"
+      },
+      pleroma: %Schema{
+        type: :object,
+        properties: %{
+          mime_type: %Schema{type: :string, description: "mime type of the attachment"}
+        }
+      }
+    },
+    example: %{
+      id: "1638338801",
+      type: "image",
+      url: "someurl",
+      remote_url: "someurl",
+      preview_url: "someurl",
+      text_url: "someurl",
+      description: nil,
+      pleroma: %{mime_type: "image/png"}
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
new file mode 100644
index 000000000..f0bc4ee3c
--- /dev/null
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -0,0 +1,53 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+  alias Pleroma.Web.ApiSpec.Schemas.Poll
+
+  require OpenApiSpex
+
+  OpenApiSpex.schema(%{
+    title: "ScheduledStatus",
+    description: "Represents a status that will be published at a future scheduled date.",
+    type: :object,
+    required: [:id, :scheduled_at, :params],
+    properties: %{
+      id: %Schema{type: :string},
+      scheduled_at: %Schema{type: :string, format: :"date-time"},
+      media_attachments: %Schema{type: :array, format: :"date-time"},
+      params: %Schema{
+        type: :object,
+        required: [:text, :visibility],
+        properties: %{
+          text: %Schema{type: :string, nullable: true},
+          media_ids: %Schema{type: :array, nullable: true, items: %Schema{type: :string}},
+          sensitive: %Schema{type: :boolean, nullable: true},
+          spoiler_text: %Schema{type: :string, nullable: true},
+          visibility: %Schema{type: VisibilityScope, nullable: true},
+          scheduled_at: %Schema{type: :string, format: :"date-time", nullable: true},
+          poll: %Schema{type: Poll, nullable: true},
+          in_reply_to_id: %Schema{type: :string, nullable: true}
+        }
+      }
+    },
+    example: %{
+      id: "3221",
+      scheduled_at: "2019-12-05T12:33:01.000Z",
+      params: %{
+        text: "test content",
+        media_ids: nil,
+        sensitive: nil,
+        spoiler_text: nil,
+        visibility: nil,
+        scheduled_at: nil,
+        poll: nil,
+        idempotency: nil,
+        in_reply_to_id: nil
+      },
+      media_attachments: []
+    }
+  })
+end
diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex
index aef0588d4..d44636a48 100644
--- a/lib/pleroma/web/api_spec/schemas/status.ex
+++ b/lib/pleroma/web/api_spec/schemas/status.ex
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.ApiSpec.Schemas.Status do
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.Schemas.Account
+  alias Pleroma.Web.ApiSpec.Schemas.Attachment
   alias Pleroma.Web.ApiSpec.Schemas.Emoji
   alias Pleroma.Web.ApiSpec.Schemas.FlakeID
   alias Pleroma.Web.ApiSpec.Schemas.Poll
@@ -50,22 +51,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
       language: %Schema{type: :string, nullable: true},
       media_attachments: %Schema{
         type: :array,
-        items: %Schema{
-          type: :object,
-          properties: %{
-            id: %Schema{type: :string},
-            url: %Schema{type: :string, format: :uri},
-            remote_url: %Schema{type: :string, format: :uri},
-            preview_url: %Schema{type: :string, format: :uri},
-            text_url: %Schema{type: :string, format: :uri},
-            description: %Schema{type: :string},
-            type: %Schema{type: :string, enum: ["image", "video", "audio", "unknown"]},
-            pleroma: %Schema{
-              type: :object,
-              properties: %{mime_type: %Schema{type: :string}}
-            }
-          }
-        }
+        items: Attachment
       },
       mentions: %Schema{
         type: :array,

From 332e016bcdbda5dca90d916bc62a9c67544b5323 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 5 May 2020 23:42:18 +0400
Subject: [PATCH 37/37] Add OpenAPI spec for ScheduledActivityController

---
 .../scheduled_activity_operation.ex           | 96 +++++++++++++++++++
 .../web/api_spec/schemas/scheduled_status.ex  |  7 +-
 .../scheduled_activity_controller.ex          | 12 ++-
 test/support/helpers.ex                       |  8 +-
 .../scheduled_activity_controller_test.exs    | 34 ++++---
 .../mastodon_api/views/status_view_test.exs   |  8 +-
 6 files changed, 144 insertions(+), 21 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex

diff --git a/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
new file mode 100644
index 000000000..fe675a923
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/scheduled_activity_operation.ex
@@ -0,0 +1,96 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.ApiError
+  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+  alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
+
+  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: ["Scheduled Statuses"],
+      summary: "View scheduled statuses",
+      security: [%{"oAuth" => ["read:statuses"]}],
+      parameters: pagination_params(),
+      operationId: "ScheduledActivity.index",
+      responses: %{
+        200 =>
+          Operation.response("Array of ScheduledStatus", "application/json", %Schema{
+            type: :array,
+            items: ScheduledStatus
+          })
+      }
+    }
+  end
+
+  def show_operation do
+    %Operation{
+      tags: ["Scheduled Statuses"],
+      summary: "View a single scheduled status",
+      security: [%{"oAuth" => ["read:statuses"]}],
+      parameters: [id_param()],
+      operationId: "ScheduledActivity.show",
+      responses: %{
+        200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+        404 => Operation.response("Error", "application/json", ApiError)
+      }
+    }
+  end
+
+  def update_operation do
+    %Operation{
+      tags: ["Scheduled Statuses"],
+      summary: "Schedule a status",
+      operationId: "ScheduledActivity.update",
+      security: [%{"oAuth" => ["write:statuses"]}],
+      parameters: [id_param()],
+      requestBody:
+        request_body("Parameters", %Schema{
+          type: :object,
+          properties: %{
+            scheduled_at: %Schema{
+              type: :string,
+              format: :"date-time",
+              description:
+                "ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
+            }
+          }
+        }),
+      responses: %{
+        200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
+        404 => Operation.response("Error", "application/json", ApiError)
+      }
+    }
+  end
+
+  def delete_operation do
+    %Operation{
+      tags: ["Scheduled Statuses"],
+      summary: "Cancel a scheduled status",
+      security: [%{"oAuth" => ["write:statuses"]}],
+      parameters: [id_param()],
+      operationId: "ScheduledActivity.delete",
+      responses: %{
+        200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
+        404 => Operation.response("Error", "application/json", ApiError)
+      }
+    }
+  end
+
+  defp id_param do
+    Operation.parameter(:id, :path, FlakeID, "Poll ID",
+      example: "123",
+      required: true
+    )
+  end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
index f0bc4ee3c..0520d0848 100644
--- a/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
+++ b/lib/pleroma/web/api_spec/schemas/scheduled_status.ex
@@ -4,8 +4,9 @@
 
 defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
   alias OpenApiSpex.Schema
-  alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
+  alias Pleroma.Web.ApiSpec.Schemas.Attachment
   alias Pleroma.Web.ApiSpec.Schemas.Poll
+  alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
 
   require OpenApiSpex
 
@@ -17,7 +18,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
     properties: %{
       id: %Schema{type: :string},
       scheduled_at: %Schema{type: :string, format: :"date-time"},
-      media_attachments: %Schema{type: :array, format: :"date-time"},
+      media_attachments: %Schema{type: :array, items: Attachment},
       params: %Schema{
         type: :object,
         required: [:text, :visibility],
@@ -47,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.ScheduledStatus do
         idempotency: nil,
         in_reply_to_id: nil
       },
-      media_attachments: []
+      media_attachments: [Attachment.schema().example]
     }
   })
 end
diff --git a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
index 899b78873..1719c67ea 100644
--- a/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
@@ -11,17 +11,21 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityController do
   alias Pleroma.ScheduledActivity
   alias Pleroma.Web.MastodonAPI.MastodonAPI
 
-  plug(:assign_scheduled_activity when action != :index)
-
   @oauth_read_actions [:show, :index]
 
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
   plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
   plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)
+  plug(:assign_scheduled_activity when action != :index)
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ScheduledActivityOperation
+
   @doc "GET /api/v1/scheduled_statuses"
   def index(%{assigns: %{user: user}} = conn, params) do
+    params = Map.new(params, fn {key, value} -> {to_string(key), value} end)
+
     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
       conn
       |> add_link_headers(scheduled_activities)
@@ -35,7 +39,7 @@ def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params)
   end
 
   @doc "PUT /api/v1/scheduled_statuses/:id"
-  def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
+  def update(%{assigns: %{scheduled_activity: scheduled_activity}, body_params: params} = conn, _) do
     with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
       render(conn, "show.json", scheduled_activity: scheduled_activity)
     end
@@ -48,7 +52,7 @@ def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params
     end
   end
 
-  defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
+  defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{id: id}} = conn, _) do
     case ScheduledActivity.get(user, id) do
       %ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
       nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index e68e9bfd2..26281b45e 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -40,12 +40,18 @@ defmacro __using__(_opts) do
           clear_config: 2
         ]
 
-      def to_datetime(naive_datetime) do
+      def to_datetime(%NaiveDateTime{} = naive_datetime) do
         naive_datetime
         |> DateTime.from_naive!("Etc/UTC")
         |> DateTime.truncate(:second)
       end
 
+      def to_datetime(datetime) when is_binary(datetime) do
+        datetime
+        |> NaiveDateTime.from_iso8601!()
+        |> to_datetime()
+      end
+
       def collect_ids(collection) do
         collection
         |> Enum.map(& &1.id)
diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
index f86274d57..1ff871c89 100644
--- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
+++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs
@@ -24,19 +24,19 @@ test "shows scheduled activities" do
     # min_id
     conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}")
 
-    result = json_response(conn_res, 200)
+    result = json_response_and_validate_schema(conn_res, 200)
     assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
 
     # since_id
     conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}")
 
-    result = json_response(conn_res, 200)
+    result = json_response_and_validate_schema(conn_res, 200)
     assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result
 
     # max_id
     conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}")
 
-    result = json_response(conn_res, 200)
+    result = json_response_and_validate_schema(conn_res, 200)
     assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result
   end
 
@@ -46,12 +46,12 @@ test "shows a scheduled activity" do
 
     res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}")
 
-    assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200)
+    assert %{"id" => scheduled_activity_id} = json_response_and_validate_schema(res_conn, 200)
     assert scheduled_activity_id == scheduled_activity.id |> to_string()
 
     res_conn = get(conn, "/api/v1/scheduled_statuses/404")
 
-    assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+    assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
   end
 
   test "updates a scheduled activity" do
@@ -74,22 +74,32 @@ test "updates a scheduled activity" do
     assert job.args == %{"activity_id" => scheduled_activity.id}
     assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(scheduled_at)
 
-    new_scheduled_at = Timex.shift(NaiveDateTime.utc_now(), minutes: 120)
+    new_scheduled_at =
+      NaiveDateTime.utc_now()
+      |> Timex.shift(minutes: 120)
+      |> Timex.format!("%Y-%m-%dT%H:%M:%S.%fZ", :strftime)
 
     res_conn =
-      put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{
         scheduled_at: new_scheduled_at
       })
 
-    assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200)
+    assert %{"scheduled_at" => expected_scheduled_at} =
+             json_response_and_validate_schema(res_conn, 200)
+
     assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at)
     job = refresh_record(job)
 
     assert DateTime.truncate(job.scheduled_at, :second) == to_datetime(new_scheduled_at)
 
-    res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
+    res_conn =
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at})
 
-    assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+    assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
   end
 
   test "deletes a scheduled activity" do
@@ -115,7 +125,7 @@ test "deletes a scheduled activity" do
       |> assign(:user, user)
       |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
 
-    assert %{} = json_response(res_conn, 200)
+    assert %{} = json_response_and_validate_schema(res_conn, 200)
     refute Repo.get(ScheduledActivity, scheduled_activity.id)
     refute Repo.get(Oban.Job, job.id)
 
@@ -124,6 +134,6 @@ test "deletes a scheduled activity" do
       |> assign(:user, user)
       |> delete("/api/v1/scheduled_statuses/#{scheduled_activity.id}")
 
-    assert %{"error" => "Record not found"} = json_response(res_conn, 404)
+    assert %{"error" => "Record not found"} = json_response_and_validate_schema(res_conn, 404)
   end
 end
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index 6791c2fb0..451723e60 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -402,11 +402,17 @@ test "attachments" do
       pleroma: %{mime_type: "image/png"}
     }
 
+    api_spec = Pleroma.Web.ApiSpec.spec()
+
     assert expected == StatusView.render("attachment.json", %{attachment: object})
+    OpenApiSpex.TestAssertions.assert_schema(expected, "Attachment", api_spec)
 
     # If theres a "id", use that instead of the generated one
     object = Map.put(object, "id", 2)
-    assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
+    result = StatusView.render("attachment.json", %{attachment: object})
+
+    assert %{id: "2"} = result
+    OpenApiSpex.TestAssertions.assert_schema(result, "Attachment", api_spec)
   end
 
   test "put the url advertised in the Activity in to the url attribute" do