From 63477d07adb614413a382a87f06af2bc2495b432 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Tue, 12 May 2020 06:44:33 +0300
Subject: [PATCH 1/8] unsubscribes of friends when user deactivated

---
 lib/mix/tasks/pleroma/user.ex | 14 ++------------
 lib/pleroma/user.ex           | 20 +++++++++++++++-----
 test/tasks/user_test.exs      |  8 ++++----
 3 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index da140ac86..93ecb4631 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -150,22 +150,12 @@ defmodule Mix.Tasks.Pleroma.User do
     with %User{} = user <- User.get_cached_by_nickname(nickname) do
       shell_info("Deactivating #{user.nickname}")
       User.deactivate(user)
-
-      user
-      |> User.get_friends()
-      |> Enum.each(fn friend ->
-        user = User.get_cached_by_id(user.id)
-
-        shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
-        User.unfollow(user, friend)
-      end)
-
       :timer.sleep(500)
 
       user = User.get_cached_by_id(user.id)
 
-      if Enum.empty?(User.get_friends(user)) do
-        shell_info("Successfully unsubscribed all followers from #{user.nickname}")
+      if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
+        shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
       end
     else
       _ ->
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index a86cc3202..1c456b27c 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -750,7 +750,19 @@ defmodule Pleroma.User do
     {:error, "Not subscribed!"}
   end
 
+  @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
   def unfollow(%User{} = follower, %User{} = followed) do
+    case do_unfollow(follower, followed) do
+      {:ok, follower, followed} ->
+        {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
+
+      error ->
+        error
+    end
+  end
+
+  @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
+  defp do_unfollow(%User{} = follower, %User{} = followed) do
     case get_follow_state(follower, followed) do
       state when state in [:follow_pending, :follow_accept] ->
         FollowingRelationship.unfollow(follower, followed)
@@ -761,7 +773,7 @@ defmodule Pleroma.User do
           |> update_following_count()
           |> set_cache()
 
-        {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
+        {:ok, follower, followed}
 
       nil ->
         {:error, "Not subscribed!"}
@@ -1401,15 +1413,13 @@ defmodule Pleroma.User do
       user
       |> get_followers()
       |> Enum.filter(& &1.local)
-      |> Enum.each(fn follower ->
-        follower |> update_following_count() |> set_cache()
-      end)
+      |> Enum.each(&set_cache(update_following_count(&1)))
 
       # Only update local user counts, remote will be update during the next pull.
       user
       |> get_friends()
       |> Enum.filter(& &1.local)
-      |> Enum.each(&update_follower_count/1)
+      |> Enum.each(&do_unfollow(user, &1))
 
       {:ok, user}
     end
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index b4f68d494..4b3ab5a87 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -172,23 +172,23 @@ defmodule Mix.Tasks.Pleroma.UserTest do
   describe "running unsubscribe" do
     test "user is unsubscribed" do
       followed = insert(:user)
+      remote_followed = insert(:user, local: false)
       user = insert(:user)
+
       User.follow(user, followed, :follow_accept)
+      User.follow(user, remote_followed, :follow_accept)
 
       Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
 
       assert_received {:mix_shell, :info, [message]}
       assert message =~ "Deactivating"
 
-      assert_received {:mix_shell, :info, [message]}
-      assert message =~ "Unsubscribing"
-
       # Note that the task has delay :timer.sleep(500)
       assert_received {:mix_shell, :info, [message]}
       assert message =~ "Successfully unsubscribed"
 
       user = User.get_cached_by_nickname(user.nickname)
-      assert Enum.empty?(User.get_friends(user))
+      assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local))
       assert user.deactivated
     end
 

From e688d4ee69dfbda0f8fd3a5544720a566b3946c5 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Tue, 14 Apr 2020 18:59:04 +0200
Subject: [PATCH 2/8] MRF.StealEmojiPolicy: New Policy

Inspired by https://git.pleroma.social/moonman/emoji-stealer-mrf/-/blob/master/steal_emoji_policy.ex
---
 CHANGELOG.md                                  |  1 +
 docs/configuration/cheatsheet.md              |  5 +
 .../activity_pub/mrf/steal_emoji_policy.ex    | 97 +++++++++++++++++++
 test/support/http_request_mock.ex             |  4 +
 .../mrf/steal_emoji_policy_test.exs           | 64 ++++++++++++
 5 files changed, 171 insertions(+)
 create mode 100644 lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
 create mode 100644 test/web/activity_pub/mrf/steal_emoji_policy_test.exs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b7fb603d..acd0fe171 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mix task to create trusted OAuth App.
 - Notifications: Added `follow_request` notification type.
 - Added `:reject_deletes` group to SimplePolicy
+- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
 <details>
   <summary>API Changes</summary>
 - Mastodon API: Extended `/api/v1/instance`.
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 707d7fdbd..1b2d72087 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -149,6 +149,11 @@ config :pleroma, :mrf_user_allowlist,
   * `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
   * `:reject` rejects the message entirely
 
+#### mrf_steal_emoji
+* `hosts`: List of hosts to steal emojis from
+* `rejected_shortcodes`: Regex-list of shortcodes to reject
+* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
+
 ### :activitypub
 * `unfollow_blocked`: Whether blocks result in people getting unfollowed
 * `outgoing_blocks`: Whether to federate blocks to other instances
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
new file mode 100644
index 000000000..2858af9eb
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -0,0 +1,97 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
+  require Logger
+
+  alias Pleroma.Config
+
+  @moduledoc "Detect new emojis by their shortcode and steals them"
+  @behaviour Pleroma.Web.ActivityPub.MRF
+
+  defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
+
+  defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
+
+  defp steal_emoji({shortcode, url}) do
+    url = Pleroma.Web.MediaProxy.url(url)
+    {:ok, response} = Pleroma.HTTP.get(url)
+    size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
+
+    if byte_size(response.body) <= size_limit do
+      emoji_dir_path =
+        Config.get(
+          [:mrf_steal_emoji, :path],
+          Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
+        )
+
+      extension =
+        url
+        |> URI.parse()
+        |> Map.get(:path)
+        |> Path.basename()
+        |> Path.extname()
+
+      file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
+
+      try do
+        :ok = File.write(file_path, response.body)
+
+        shortcode
+      rescue
+        e ->
+          Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
+          nil
+      end
+    else
+      Logger.debug(
+        "MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
+          size_limit
+        } B)"
+      )
+
+      nil
+    end
+  rescue
+    e ->
+      Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
+      nil
+  end
+
+  @impl true
+  def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
+    host = URI.parse(actor).host
+
+    if remote_host?(host) and accept_host?(host) do
+      installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+
+      new_emojis =
+        foreign_emojis
+        |> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
+        |> Enum.filter(fn {shortcode, _url} ->
+          reject_emoji? =
+            Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
+            |> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
+
+          !reject_emoji?
+        end)
+        |> Enum.map(&steal_emoji(&1))
+        |> Enum.filter(& &1)
+
+      if !Enum.empty?(new_emojis) do
+        Logger.info("Stole new emojis: #{inspect(new_emojis)}")
+        Pleroma.Emoji.reload()
+      end
+    end
+
+    {:ok, message}
+  end
+
+  def filter(message), do: {:ok, message}
+
+  @impl true
+  def describe do
+    {:ok, %{}}
+  end
+end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 3a95e92da..3d5128835 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1291,6 +1291,10 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("https://example.org/emoji/firedfox.png", _, _, _) do
+    {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}}
+  end
+
   def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
   end
diff --git a/test/web/activity_pub/mrf/steal_emoji_policy_test.exs b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs
new file mode 100644
index 000000000..8882c8c13
--- /dev/null
+++ b/test/web/activity_pub/mrf/steal_emoji_policy_test.exs
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicyTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Config
+  alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy
+
+  setup_all do
+    Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
+    :ok
+  end
+
+  setup do
+    clear_config(:mrf_steal_emoji)
+
+    emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
+    File.rm_rf!(emoji_path)
+    File.mkdir!(emoji_path)
+
+    Pleroma.Emoji.reload()
+  end
+
+  test "does nothing by default" do
+    installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+    refute "firedfox" in installed_emoji
+
+    message = %{
+      "type" => "Create",
+      "object" => %{
+        "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}],
+        "actor" => "https://example.org/users/admin"
+      }
+    }
+
+    assert {:ok, message} == StealEmojiPolicy.filter(message)
+
+    installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+    refute "firedfox" in installed_emoji
+  end
+
+  test "Steals emoji on unknown shortcode from allowed remote host" do
+    installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+    refute "firedfox" in installed_emoji
+
+    message = %{
+      "type" => "Create",
+      "object" => %{
+        "emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}],
+        "actor" => "https://example.org/users/admin"
+      }
+    }
+
+    Config.put([:mrf_steal_emoji, :hosts], ["example.org"])
+    Config.put([:mrf_steal_emoji, :size_limit], 284_468)
+
+    assert {:ok, message} == StealEmojiPolicy.filter(message)
+
+    installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
+    assert "firedfox" in installed_emoji
+  end
+end

From 490a3a34b63fa10e9151e9a385920c10615a1a3c Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 19 May 2020 21:52:26 +0400
Subject: [PATCH 3/8] Add OpenAPI spec for PleromaAPI.PleromaAPIController

---
 docs/API/pleroma_api.md                       |   4 +-
 .../operations/notification_operation.ex      |   2 +-
 .../api_spec/operations/pleroma_operation.ex  | 223 ++++++++++++++++++
 .../controllers/pleroma_api_controller.ex     |  28 ++-
 test/support/api_spec_helpers.ex              |   2 +-
 .../pleroma_api_controller_test.exs           |  44 ++--
 6 files changed, 264 insertions(+), 39 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_operation.ex

diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md
index 867f59919..d6dbafc06 100644
--- a/docs/API/pleroma_api.md
+++ b/docs/API/pleroma_api.md
@@ -358,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
     * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
 * Response: JSON, statuses (200 - healthy, 503 unhealthy)
 
-## `GET /api/v1/pleroma/conversations/read`
+## `POST /api/v1/pleroma/conversations/read`
 ### Marks all user's conversations as read.
 * Method `POST`
 * Authentication: required
@@ -536,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
 ```
 
 ## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
-### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji`
+### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
 * Method: `GET`
 * Authentication: optional
 * Params: None
diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex
index 64adc5319..46e72f8bf 100644
--- a/lib/pleroma/web/api_spec/operations/notification_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex
@@ -145,7 +145,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
     }
   end
 
-  defp notification do
+  def notification do
     %Schema{
       title: "Notification",
       description: "Response schema for a notification",
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
new file mode 100644
index 000000000..c6df5c854
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
@@ -0,0 +1,223 @@
+# 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.PleromaOperation 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.Status
+  alias Pleroma.Web.ApiSpec.Schemas.Conversation
+  alias Pleroma.Web.ApiSpec.StatusOperation
+  alias Pleroma.Web.ApiSpec.NotificationOperation
+
+  import Pleroma.Web.ApiSpec.Helpers
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def emoji_reactions_by_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary:
+        "Get an object of emoji to account mappings with accounts that reacted to the post",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
+          required: false
+        )
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "PleromaController.emoji_reactions_by",
+      responses: %{
+        200 => array_of_reactions_response()
+      }
+    }
+  end
+
+  def react_with_emoji_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary: "React to a post with a unicode emoji",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      operationId: "PleromaController.react_with_emoji",
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status)
+      }
+    }
+  end
+
+  def unreact_with_emoji_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary: "Remove a reaction to a post with a unicode emoji",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      operationId: "PleromaController.unreact_with_emoji",
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status)
+      }
+    }
+  end
+
+  defp array_of_reactions_response do
+    Operation.response("Array of Emoji Reactions", "application/json", %Schema{
+      type: :array,
+      items: emoji_reaction(),
+      example: [emoji_reaction().example]
+    })
+  end
+
+  defp emoji_reaction do
+    %Schema{
+      title: "EmojiReaction",
+      type: :object,
+      properties: %{
+        name: %Schema{type: :string, description: "Emoji"},
+        count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
+        me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
+        accounts: %Schema{
+          type: :array,
+          items: Account,
+          description: "Array of accounts reacted with this emoji"
+        }
+      },
+      example: %{
+        "name" => "😱",
+        "count" => 1,
+        "me" => false,
+        "accounts" => [Account.schema().example]
+      }
+    }
+  end
+
+  def conversation_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "The conversation with the given ID",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "PleromaController.conversation",
+      responses: %{
+        200 => Operation.response("Conversation", "application/json", Conversation)
+      }
+    }
+  end
+
+  def conversation_statuses_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Timeline for a given conversation",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        )
+        | pagination_params()
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "PleromaController.conversation_statuses",
+      responses: %{
+        200 =>
+          Operation.response(
+            "Array of Statuses",
+            "application/json",
+            StatusOperation.array_of_statuses()
+          )
+      }
+    }
+  end
+
+  def update_conversation_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Update a conversation. Used to change the set of recipients.",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        ),
+        Operation.parameter(
+          :recipients,
+          :query,
+          %Schema{type: :array, items: FlakeID},
+          "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:conversations"]}],
+      operationId: "PleromaController.update_conversation",
+      responses: %{
+        200 => Operation.response("Conversation", "application/json", Conversation)
+      }
+    }
+  end
+
+  def mark_conversations_as_read_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Marks all user's conversations as read",
+      security: [%{"oAuth" => ["write:conversations"]}],
+      operationId: "PleromaController.mark_conversations_as_read",
+      responses: %{
+        200 =>
+          Operation.response(
+            "Array of Conversations that were marked as read",
+            "application/json",
+            %Schema{
+              type: :array,
+              items: Conversation,
+              example: [Conversation.schema().example]
+            }
+          )
+      }
+    }
+  end
+
+  def mark_notifications_as_read_operation do
+    %Operation{
+      tags: ["Notifications"],
+      summary: "Mark notifications as read. Query parameters are mutually exclusive.",
+      parameters: [
+        Operation.parameter(:id, :query, :string, "A single notification ID to read"),
+        Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id")
+      ],
+      security: [%{"oAuth" => ["write:notifications"]}],
+      operationId: "PleromaController.mark_notifications_as_read",
+      responses: %{
+        200 =>
+          Operation.response(
+            "A Notification or array of Motifications",
+            "application/json",
+            %Schema{
+              anyOf: [
+                %Schema{type: :array, items: NotificationOperation.notification()},
+                NotificationOperation.notification()
+              ]
+            }
+          ),
+        400 => Operation.response("Bad Request", "application/json", ApiError)
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index e834133b2..8220d13bc 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -20,6 +20,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
 
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+
   plug(
     OAuthScopesPlug,
     %{scopes: ["read:statuses"]}
@@ -49,14 +51,16 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
   )
 
-  def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation
+
+  def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
     with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
          %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
            Object.normalize(activity) do
       reactions =
         emoji_reactions
         |> Enum.map(fn [emoji, user_ap_ids] ->
-          if params["emoji"] && params["emoji"] != emoji do
+          if params[:emoji] && params[:emoji] != emoji do
             nil
           else
             users =
@@ -79,7 +83,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
             }
           end
         end)
-        |> Enum.filter(& &1)
+        |> Enum.reject(&is_nil/1)
 
       conn
       |> json(reactions)
@@ -90,7 +94,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
+  def react_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
     with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
          activity <- Activity.get_by_id(activity_id) do
       conn
@@ -99,10 +103,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
-        "id" => activity_id,
-        "emoji" => emoji
-      }) do
+  def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
     with {:ok, _activity} <-
            CommonAPI.unreact_with_emoji(activity_id, user, emoji),
          activity <- Activity.get_by_id(activity_id) do
@@ -112,7 +113,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
+  def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
     with %Participation{} = participation <- Participation.get(participation_id),
          true <- user.id == participation.user_id do
       conn
@@ -128,12 +129,13 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   def conversation_statuses(
         %{assigns: %{user: %{id: user_id} = user}} = conn,
-        %{"id" => participation_id} = params
+        %{id: participation_id} = params
       ) do
     with %Participation{user_id: ^user_id} = participation <-
            Participation.get(participation_id, preload: [:conversation]) do
       params =
         params
+        |> Map.new(fn {key, value} -> {to_string(key), value} end)
         |> Map.put("blocking_user", user)
         |> Map.put("muting_user", user)
         |> Map.put("user", user)
@@ -162,7 +164,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   def update_conversation(
         %{assigns: %{user: user}} = conn,
-        %{"id" => participation_id, "recipients" => recipients}
+        %{id: participation_id, recipients: recipients}
       ) do
     with %Participation{} = participation <- Participation.get(participation_id),
          true <- user.id == participation.user_id,
@@ -192,7 +194,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
+  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do
     with {:ok, notification} <- Notification.read_one(user, notification_id) do
       conn
       |> put_view(NotificationView)
@@ -205,7 +207,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     end
   end
 
-  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
+  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do
     with notifications <- Notification.set_read_up_to(user, max_id) do
       notifications = Enum.take(notifications, 80)
 
diff --git a/test/support/api_spec_helpers.ex b/test/support/api_spec_helpers.ex
index 80c69c788..46388f92c 100644
--- a/test/support/api_spec_helpers.ex
+++ b/test/support/api_spec_helpers.ex
@@ -51,7 +51,7 @@ defmodule Pleroma.Tests.ApiSpecHelpers do
       |> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace])
       |> Map.values()
       |> Enum.reject(&is_nil/1)
-      |> Enum.uniq()
     end)
+    |> Enum.uniq()
   end
 end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index cfd1dbd24..f0cdc2f08 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -27,7 +27,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
       |> assign(:user, other_user)
       |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
       |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     # We return the status, but this our implementation detail.
     assert %{"id" => id} = result
@@ -53,7 +53,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
       |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
       |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
 
-    assert %{"id" => id} = json_response(result, 200)
+    assert %{"id" => id} = json_response_and_validate_schema(result, 200)
     assert to_string(activity.id) == id
 
     ObanHelpers.perform_all()
@@ -73,7 +73,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert result == []
 
@@ -85,7 +85,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
 
@@ -96,7 +96,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
       |> assign(:user, other_user)
       |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"]))
       |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
              result
@@ -111,7 +111,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert result == []
 
@@ -121,7 +121,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
 
@@ -140,7 +140,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/conversations/#{participation.id}")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert result["id"] == participation.id |> to_string()
   end
@@ -168,7 +168,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     result =
       conn
       |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     assert length(result) == 2
 
@@ -186,12 +186,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     assert [%{"id" => ^id_two}, %{"id" => ^id_three}] =
              conn
              |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2")
-             |> json_response(:ok)
+             |> json_response_and_validate_schema(:ok)
 
     assert [%{"id" => ^id_three}] =
              conn
              |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}")
-             |> json_response(:ok)
+             |> json_response_and_validate_schema(:ok)
   end
 
   test "PATCH /api/v1/pleroma/conversations/:id" do
@@ -208,12 +208,12 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     assert [user] == participation.recipients
     assert other_user not in participation.recipients
 
+    query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}"
+
     result =
       conn
-      |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
-        "recipients" => [user.id, other_user.id]
-      })
-      |> json_response(200)
+      |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}")
+      |> json_response_and_validate_schema(200)
 
     assert result["id"] == participation.id |> to_string
 
@@ -242,7 +242,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     [%{"unread" => false}, %{"unread" => false}] =
       conn
       |> post("/api/v1/pleroma/conversations/read", %{})
-      |> json_response(200)
+      |> json_response_and_validate_schema(200)
 
     [participation2, participation1] = Participation.for_user(other_user)
     assert Participation.get(participation2.id).read == true
@@ -262,8 +262,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
 
       response =
         conn
-        |> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
-        |> json_response(:ok)
+        |> post("/api/v1/pleroma/notifications/read?id=#{notification1.id}")
+        |> json_response_and_validate_schema(:ok)
 
       assert %{"pleroma" => %{"is_seen" => true}} = response
       assert Repo.get(Notification, notification1.id).seen
@@ -280,8 +280,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
 
       [response1, response2] =
         conn
-        |> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"})
-        |> json_response(:ok)
+        |> post("/api/v1/pleroma/notifications/read?max_id=#{notification2.id}")
+        |> json_response_and_validate_schema(:ok)
 
       assert %{"pleroma" => %{"is_seen" => true}} = response1
       assert %{"pleroma" => %{"is_seen" => true}} = response2
@@ -293,8 +293,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
     test "it returns error when notification not found", %{conn: conn} do
       response =
         conn
-        |> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"})
-        |> json_response(:bad_request)
+        |> post("/api/v1/pleroma/notifications/read?id=22222222222222")
+        |> json_response_and_validate_schema(:bad_request)
 
       assert response == %{"error" => "Cannot get notification"}
     end

From 9a5de0f4548cfe6b62265596bbe3cef2d639b978 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Tue, 19 May 2020 23:50:49 +0400
Subject: [PATCH 4/8] Move reaction actions to EmojiReactionController

---
 .../operations/emoji_reaction_operation.ex    | 102 ++++++++++++++
 .../api_spec/operations/pleroma_operation.ex  |  96 +-------------
 .../controllers/emoji_reaction_controller.ex  |  61 +++++++++
 .../controllers/pleroma_api_controller.ex     |  77 -----------
 .../pleroma_api/views/emoji_reaction_view.ex  |  33 +++++
 lib/pleroma/web/router.ex                     |   8 +-
 .../emoji_reaction_controller_test.exs        | 125 ++++++++++++++++++
 .../pleroma_api_controller_test.exs           | 115 ----------------
 8 files changed, 329 insertions(+), 288 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
 create mode 100644 lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
 create mode 100644 lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
 create mode 100644 test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs

diff --git a/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
new file mode 100644
index 000000000..7c08fbaa7
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
@@ -0,0 +1,102 @@
+# 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.EmojiReactionOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.Account
+  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+  alias Pleroma.Web.ApiSpec.Schemas.Status
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def index_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary:
+        "Get an object of emoji to account mappings with accounts that reacted to the post",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
+          required: false
+        )
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "EmojiReactionController.index",
+      responses: %{
+        200 => array_of_reactions_response()
+      }
+    }
+  end
+
+  def create_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary: "React to a post with a unicode emoji",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      operationId: "EmojiReactionController.create",
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status)
+      }
+    }
+  end
+
+  def delete_operation do
+    %Operation{
+      tags: ["Emoji Reactions"],
+      summary: "Remove a reaction to a post with a unicode emoji",
+      parameters: [
+        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
+        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:statuses"]}],
+      operationId: "EmojiReactionController.delete",
+      responses: %{
+        200 => Operation.response("Status", "application/json", Status)
+      }
+    }
+  end
+
+  defp array_of_reactions_response do
+    Operation.response("Array of Emoji Reactions", "application/json", %Schema{
+      type: :array,
+      items: emoji_reaction(),
+      example: [emoji_reaction().example]
+    })
+  end
+
+  defp emoji_reaction do
+    %Schema{
+      title: "EmojiReaction",
+      type: :object,
+      properties: %{
+        name: %Schema{type: :string, description: "Emoji"},
+        count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
+        me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
+        accounts: %Schema{
+          type: :array,
+          items: Account,
+          description: "Array of accounts reacted with this emoji"
+        }
+      },
+      example: %{
+        "name" => "😱",
+        "count" => 1,
+        "me" => false,
+        "accounts" => [Account.schema().example]
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
index c6df5c854..7e46ba553 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
@@ -5,13 +5,11 @@
 defmodule Pleroma.Web.ApiSpec.PleromaOperation 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.Status
-  alias Pleroma.Web.ApiSpec.Schemas.Conversation
-  alias Pleroma.Web.ApiSpec.StatusOperation
   alias Pleroma.Web.ApiSpec.NotificationOperation
+  alias Pleroma.Web.ApiSpec.Schemas.ApiError
+  alias Pleroma.Web.ApiSpec.Schemas.Conversation
+  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+  alias Pleroma.Web.ApiSpec.StatusOperation
 
   import Pleroma.Web.ApiSpec.Helpers
 
@@ -20,92 +18,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation do
     apply(__MODULE__, operation, [])
   end
 
-  def emoji_reactions_by_operation do
-    %Operation{
-      tags: ["Emoji Reactions"],
-      summary:
-        "Get an object of emoji to account mappings with accounts that reacted to the post",
-      parameters: [
-        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
-        Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
-          required: false
-        )
-      ],
-      security: [%{"oAuth" => ["read:statuses"]}],
-      operationId: "PleromaController.emoji_reactions_by",
-      responses: %{
-        200 => array_of_reactions_response()
-      }
-    }
-  end
-
-  def react_with_emoji_operation do
-    %Operation{
-      tags: ["Emoji Reactions"],
-      summary: "React to a post with a unicode emoji",
-      parameters: [
-        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
-        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
-          required: true
-        )
-      ],
-      security: [%{"oAuth" => ["write:statuses"]}],
-      operationId: "PleromaController.react_with_emoji",
-      responses: %{
-        200 => Operation.response("Status", "application/json", Status)
-      }
-    }
-  end
-
-  def unreact_with_emoji_operation do
-    %Operation{
-      tags: ["Emoji Reactions"],
-      summary: "Remove a reaction to a post with a unicode emoji",
-      parameters: [
-        Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
-        Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
-          required: true
-        )
-      ],
-      security: [%{"oAuth" => ["write:statuses"]}],
-      operationId: "PleromaController.unreact_with_emoji",
-      responses: %{
-        200 => Operation.response("Status", "application/json", Status)
-      }
-    }
-  end
-
-  defp array_of_reactions_response do
-    Operation.response("Array of Emoji Reactions", "application/json", %Schema{
-      type: :array,
-      items: emoji_reaction(),
-      example: [emoji_reaction().example]
-    })
-  end
-
-  defp emoji_reaction do
-    %Schema{
-      title: "EmojiReaction",
-      type: :object,
-      properties: %{
-        name: %Schema{type: :string, description: "Emoji"},
-        count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
-        me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
-        accounts: %Schema{
-          type: :array,
-          items: Account,
-          description: "Array of accounts reacted with this emoji"
-        }
-      },
-      example: %{
-        "name" => "😱",
-        "count" => 1,
-        "me" => false,
-        "accounts" => [Account.schema().example]
-      }
-    }
-  end
-
   def conversation_operation do
     %Operation{
       tags: ["Conversations"],
diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
new file mode 100644
index 000000000..a002912f3
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.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.PleromaAPI.EmojiReactionController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Activity
+  alias Pleroma.Object
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.MastodonAPI.StatusView
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+  plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
+    when action == :index
+  )
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation
+
+  def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
+    with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
+         %Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
+           Object.normalize(activity) do
+      reactions = filter(reactions, params)
+      render(conn, "index.json", emoji_reactions: reactions, user: user)
+    else
+      _e -> json(conn, [])
+    end
+  end
+
+  defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
+    Enum.filter(reactions, fn [e, _] -> e == emoji end)
+  end
+
+  defp filter(reactions, _), do: reactions
+
+  def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+    with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
+      activity = Activity.get_by_id(activity_id)
+
+      conn
+      |> put_view(StatusView)
+      |> render("show.json", activity: activity, for: user, as: :activity)
+    end
+  end
+
+  def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
+    with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
+      activity = Activity.get_by_id(activity_id)
+
+      conn
+      |> put_view(StatusView)
+      |> render("show.json", activity: activity, for: user, as: :activity)
+    end
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index 8220d13bc..61273f7ee 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -7,15 +7,10 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
 
-  alias Pleroma.Activity
   alias Pleroma.Conversation.Participation
   alias Pleroma.Notification
-  alias Pleroma.Object
   alias Pleroma.Plugs.OAuthScopesPlug
-  alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
-  alias Pleroma.Web.CommonAPI
-  alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
@@ -28,18 +23,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
     when action in [:conversation, :conversation_statuses]
   )
 
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
-    when action == :emoji_reactions_by
-  )
-
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["write:statuses"]}
-    when action in [:react_with_emoji, :unreact_with_emoji]
-  )
-
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:conversations"]}
@@ -53,66 +36,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation
 
-  def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
-    with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
-         %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
-           Object.normalize(activity) do
-      reactions =
-        emoji_reactions
-        |> Enum.map(fn [emoji, user_ap_ids] ->
-          if params[:emoji] && params[:emoji] != emoji do
-            nil
-          else
-            users =
-              Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
-              |> Enum.filter(fn
-                %{deactivated: false} -> true
-                _ -> false
-              end)
-
-            %{
-              name: emoji,
-              count: length(users),
-              accounts:
-                AccountView.render("index.json", %{
-                  users: users,
-                  for: user,
-                  as: :user
-                }),
-              me: !!(user && user.ap_id in user_ap_ids)
-            }
-          end
-        end)
-        |> Enum.reject(&is_nil/1)
-
-      conn
-      |> json(reactions)
-    else
-      _e ->
-        conn
-        |> json([])
-    end
-  end
-
-  def react_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
-    with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
-         activity <- Activity.get_by_id(activity_id) do
-      conn
-      |> put_view(StatusView)
-      |> render("show.json", %{activity: activity, for: user, as: :activity})
-    end
-  end
-
-  def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
-    with {:ok, _activity} <-
-           CommonAPI.unreact_with_emoji(activity_id, user, emoji),
-         activity <- Activity.get_by_id(activity_id) do
-      conn
-      |> put_view(StatusView)
-      |> render("show.json", %{activity: activity, for: user, as: :activity})
-    end
-  end
-
   def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
     with %Participation{} = participation <- Participation.get(participation_id),
          true <- user.id == participation.user_id do
diff --git a/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
new file mode 100644
index 000000000..84d2d303d
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
@@ -0,0 +1,33 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
+  use Pleroma.Web, :view
+
+  alias Pleroma.Web.MastodonAPI.AccountView
+
+  def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
+    render_many(emoji_reactions, __MODULE__, "show.json", opts)
+  end
+
+  def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
+    users = fetch_users(user_ap_ids)
+
+    %{
+      name: emoji,
+      count: length(users),
+      accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
+      me: !!(user && user.ap_id in user_ap_ids)
+    }
+  end
+
+  defp fetch_users(user_ap_ids) do
+    user_ap_ids
+    |> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
+    |> Enum.filter(fn
+      %{deactivated: false} -> true
+      _ -> false
+    end)
+  end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 369c54cf4..12381511e 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -297,8 +297,8 @@ defmodule Pleroma.Web.Router do
   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
     pipe_through(:api)
 
-    get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
-    get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
+    get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
+    get("/statuses/:id/reactions", EmojiReactionController, :index)
   end
 
   scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
@@ -314,8 +314,8 @@ defmodule Pleroma.Web.Router do
       pipe_through(:authenticated_api)
 
       patch("/conversations/:id", PleromaAPIController, :update_conversation)
-      put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
-      delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
+      put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
+      delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
       post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
 
       patch("/accounts/update_avatar", AccountController, :update_avatar)
diff --git a/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
new file mode 100644
index 000000000..ee66ebf87
--- /dev/null
+++ b/test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
@@ -0,0 +1,125 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
+  use Oban.Testing, repo: Pleroma.Repo
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Object
+  alias Pleroma.Tests.ObanHelpers
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+
+    result =
+      conn
+      |> assign(:user, other_user)
+      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+      |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
+      |> json_response_and_validate_schema(200)
+
+    # We return the status, but this our implementation detail.
+    assert %{"id" => id} = result
+    assert to_string(activity.id) == id
+
+    assert result["pleroma"]["emoji_reactions"] == [
+             %{"name" => "☕", "count" => 1, "me" => true}
+           ]
+  end
+
+  test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+    {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+    ObanHelpers.perform_all()
+
+    result =
+      conn
+      |> assign(:user, other_user)
+      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
+      |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
+
+    assert %{"id" => id} = json_response_and_validate_schema(result, 200)
+    assert to_string(activity.id) == id
+
+    ObanHelpers.perform_all()
+
+    object = Object.get_by_ap_id(activity.data["object"])
+
+    assert object.data["reaction_count"] == 0
+  end
+
+  test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+    doomed_user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+
+    result =
+      conn
+      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
+      |> json_response_and_validate_schema(200)
+
+    assert result == []
+
+    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+    {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
+
+    User.perform(:delete, doomed_user)
+
+    result =
+      conn
+      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
+      |> json_response_and_validate_schema(200)
+
+    [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
+
+    assert represented_user["id"] == other_user.id
+
+    result =
+      conn
+      |> assign(:user, other_user)
+      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"]))
+      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
+      |> json_response_and_validate_schema(200)
+
+    assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
+             result
+  end
+
+  test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
+
+    result =
+      conn
+      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
+      |> json_response_and_validate_schema(200)
+
+    assert result == []
+
+    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
+    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+
+    assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] =
+             conn
+             |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
+             |> json_response_and_validate_schema(200)
+
+    assert represented_user["id"] == other_user.id
+  end
+end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index f0cdc2f08..6f4f01e6f 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -3,131 +3,16 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
-  use Oban.Testing, repo: Pleroma.Repo
   use Pleroma.Web.ConnCase
 
   alias Pleroma.Conversation.Participation
   alias Pleroma.Notification
-  alias Pleroma.Object
   alias Pleroma.Repo
-  alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
-  test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
-
-    result =
-      conn
-      |> assign(:user, other_user)
-      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
-      |> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
-      |> json_response_and_validate_schema(200)
-
-    # We return the status, but this our implementation detail.
-    assert %{"id" => id} = result
-    assert to_string(activity.id) == id
-
-    assert result["pleroma"]["emoji_reactions"] == [
-             %{"name" => "☕", "count" => 1, "me" => true}
-           ]
-  end
-
-  test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
-    {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
-
-    ObanHelpers.perform_all()
-
-    result =
-      conn
-      |> assign(:user, other_user)
-      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
-      |> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
-
-    assert %{"id" => id} = json_response_and_validate_schema(result, 200)
-    assert to_string(activity.id) == id
-
-    ObanHelpers.perform_all()
-
-    object = Object.get_by_ap_id(activity.data["object"])
-
-    assert object.data["reaction_count"] == 0
-  end
-
-  test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-    doomed_user = insert(:user)
-
-    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response_and_validate_schema(200)
-
-    assert result == []
-
-    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
-    {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
-
-    User.perform(:delete, doomed_user)
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response_and_validate_schema(200)
-
-    [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
-
-    assert represented_user["id"] == other_user.id
-
-    result =
-      conn
-      |> assign(:user, other_user)
-      |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"]))
-      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
-      |> json_response_and_validate_schema(200)
-
-    assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
-             result
-  end
-
-  test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
-      |> json_response_and_validate_schema(200)
-
-    assert result == []
-
-    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
-    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
-      |> json_response_and_validate_schema(200)
-
-    [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result
-
-    assert represented_user["id"] == other_user.id
-  end
-
   test "/api/v1/pleroma/conversations/:id" do
     user = insert(:user)
     %{user: other_user, conn: conn} = oauth_access(["read:statuses"])

From f3fc8b22b1dca8d432d066417e2bb9b62a3f1520 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Wed, 20 May 2020 15:00:11 +0400
Subject: [PATCH 5/8] Move conversation actions to
 PleromaAPI.ConversationController

---
 .../pleroma_conversation_operation.ex         | 106 ++++++++++++++
 .../api_spec/operations/pleroma_operation.ex  |  93 ------------
 .../controllers/conversation_controller.ex    |  95 ++++++++++++
 .../controllers/pleroma_api_controller.ex     |  99 -------------
 lib/pleroma/web/router.ex                     |  12 +-
 .../conversation_controller_test.exs          | 136 ++++++++++++++++++
 .../pleroma_api_controller_test.exs           | 124 ----------------
 7 files changed, 341 insertions(+), 324 deletions(-)
 create mode 100644 lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
 create mode 100644 lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
 create mode 100644 test/web/pleroma_api/controllers/conversation_controller_test.exs

diff --git a/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
new file mode 100644
index 000000000..e885eab20
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
@@ -0,0 +1,106 @@
+# 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.PleromaConversationOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  alias Pleroma.Web.ApiSpec.Schemas.Conversation
+  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
+  alias Pleroma.Web.ApiSpec.StatusOperation
+
+  import Pleroma.Web.ApiSpec.Helpers
+
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  def show_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "The conversation with the given ID",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "PleromaAPI.ConversationController.show",
+      responses: %{
+        200 => Operation.response("Conversation", "application/json", Conversation)
+      }
+    }
+  end
+
+  def statuses_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Timeline for a given conversation",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        )
+        | pagination_params()
+      ],
+      security: [%{"oAuth" => ["read:statuses"]}],
+      operationId: "PleromaAPI.ConversationController.statuses",
+      responses: %{
+        200 =>
+          Operation.response(
+            "Array of Statuses",
+            "application/json",
+            StatusOperation.array_of_statuses()
+          )
+      }
+    }
+  end
+
+  def update_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Update a conversation. Used to change the set of recipients.",
+      parameters: [
+        Operation.parameter(:id, :path, :string, "Conversation ID",
+          example: "123",
+          required: true
+        ),
+        Operation.parameter(
+          :recipients,
+          :query,
+          %Schema{type: :array, items: FlakeID},
+          "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
+          required: true
+        )
+      ],
+      security: [%{"oAuth" => ["write:conversations"]}],
+      operationId: "PleromaAPI.ConversationController.update",
+      responses: %{
+        200 => Operation.response("Conversation", "application/json", Conversation)
+      }
+    }
+  end
+
+  def mark_as_read_operation do
+    %Operation{
+      tags: ["Conversations"],
+      summary: "Marks all user's conversations as read",
+      security: [%{"oAuth" => ["write:conversations"]}],
+      operationId: "PleromaAPI.ConversationController.mark_as_read",
+      responses: %{
+        200 =>
+          Operation.response(
+            "Array of Conversations that were marked as read",
+            "application/json",
+            %Schema{
+              type: :array,
+              items: Conversation,
+              example: [Conversation.schema().example]
+            }
+          )
+      }
+    }
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
index 7e46ba553..d28451933 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
@@ -7,105 +7,12 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation do
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.NotificationOperation
   alias Pleroma.Web.ApiSpec.Schemas.ApiError
-  alias Pleroma.Web.ApiSpec.Schemas.Conversation
-  alias Pleroma.Web.ApiSpec.Schemas.FlakeID
-  alias Pleroma.Web.ApiSpec.StatusOperation
-
-  import Pleroma.Web.ApiSpec.Helpers
 
   def open_api_operation(action) do
     operation = String.to_existing_atom("#{action}_operation")
     apply(__MODULE__, operation, [])
   end
 
-  def conversation_operation do
-    %Operation{
-      tags: ["Conversations"],
-      summary: "The conversation with the given ID",
-      parameters: [
-        Operation.parameter(:id, :path, :string, "Conversation ID",
-          example: "123",
-          required: true
-        )
-      ],
-      security: [%{"oAuth" => ["read:statuses"]}],
-      operationId: "PleromaController.conversation",
-      responses: %{
-        200 => Operation.response("Conversation", "application/json", Conversation)
-      }
-    }
-  end
-
-  def conversation_statuses_operation do
-    %Operation{
-      tags: ["Conversations"],
-      summary: "Timeline for a given conversation",
-      parameters: [
-        Operation.parameter(:id, :path, :string, "Conversation ID",
-          example: "123",
-          required: true
-        )
-        | pagination_params()
-      ],
-      security: [%{"oAuth" => ["read:statuses"]}],
-      operationId: "PleromaController.conversation_statuses",
-      responses: %{
-        200 =>
-          Operation.response(
-            "Array of Statuses",
-            "application/json",
-            StatusOperation.array_of_statuses()
-          )
-      }
-    }
-  end
-
-  def update_conversation_operation do
-    %Operation{
-      tags: ["Conversations"],
-      summary: "Update a conversation. Used to change the set of recipients.",
-      parameters: [
-        Operation.parameter(:id, :path, :string, "Conversation ID",
-          example: "123",
-          required: true
-        ),
-        Operation.parameter(
-          :recipients,
-          :query,
-          %Schema{type: :array, items: FlakeID},
-          "A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
-          required: true
-        )
-      ],
-      security: [%{"oAuth" => ["write:conversations"]}],
-      operationId: "PleromaController.update_conversation",
-      responses: %{
-        200 => Operation.response("Conversation", "application/json", Conversation)
-      }
-    }
-  end
-
-  def mark_conversations_as_read_operation do
-    %Operation{
-      tags: ["Conversations"],
-      summary: "Marks all user's conversations as read",
-      security: [%{"oAuth" => ["write:conversations"]}],
-      operationId: "PleromaController.mark_conversations_as_read",
-      responses: %{
-        200 =>
-          Operation.response(
-            "Array of Conversations that were marked as read",
-            "application/json",
-            %Schema{
-              type: :array,
-              items: Conversation,
-              example: [Conversation.schema().example]
-            }
-          )
-      }
-    }
-  end
-
   def mark_notifications_as_read_operation do
     %Operation{
       tags: ["Notifications"],
diff --git a/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
new file mode 100644
index 000000000..21d5eb8d5
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
@@ -0,0 +1,95 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ConversationController do
+  use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
+
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.Plugs.OAuthScopesPlug
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.MastodonAPI.StatusView
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+  plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
+  plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses])
+
+  plug(
+    OAuthScopesPlug,
+    %{scopes: ["write:conversations"]} when action in [:update, :mark_as_read]
+  )
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation
+
+  def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do
+    with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do
+      render(conn, "participation.json", participation: participation, for: user)
+    else
+      _error ->
+        conn
+        |> put_status(:not_found)
+        |> json(%{"error" => "Unknown conversation id"})
+    end
+  end
+
+  def statuses(
+        %{assigns: %{user: %{id: user_id} = user}} = conn,
+        %{id: participation_id} = params
+      ) do
+    with %Participation{user_id: ^user_id} = participation <-
+           Participation.get(participation_id, preload: [:conversation]) do
+      params =
+        params
+        |> Map.new(fn {key, value} -> {to_string(key), value} end)
+        |> Map.put("blocking_user", user)
+        |> Map.put("muting_user", user)
+        |> Map.put("user", user)
+
+      activities =
+        participation.conversation.ap_id
+        |> ActivityPub.fetch_activities_for_context_query(params)
+        |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
+        |> Enum.reverse()
+
+      conn
+      |> add_link_headers(activities)
+      |> put_view(StatusView)
+      |> render("index.json", activities: activities, for: user, as: :activity)
+    else
+      _error ->
+        conn
+        |> put_status(:not_found)
+        |> json(%{"error" => "Unknown conversation id"})
+    end
+  end
+
+  def update(
+        %{assigns: %{user: %{id: user_id} = user}} = conn,
+        %{id: participation_id, recipients: recipients}
+      ) do
+    with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id),
+         {:ok, participation} <- Participation.set_recipients(participation, recipients) do
+      render(conn, "participation.json", participation: participation, for: user)
+    else
+      {:error, message} ->
+        conn
+        |> put_status(:bad_request)
+        |> json(%{"error" => message})
+
+      _error ->
+        conn
+        |> put_status(:not_found)
+        |> json(%{"error" => "Unknown conversation id"})
+    end
+  end
+
+  def mark_as_read(%{assigns: %{user: user}} = conn, _params) do
+    with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
+      conn
+      |> add_link_headers(participations)
+      |> render("participations.json", participations: participations, for: user)
+    end
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
index 61273f7ee..a58665abe 100644
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
@@ -5,30 +5,12 @@
 defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   use Pleroma.Web, :controller
 
-  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
-
-  alias Pleroma.Conversation.Participation
   alias Pleroma.Notification
   alias Pleroma.Plugs.OAuthScopesPlug
-  alias Pleroma.Web.ActivityPub.ActivityPub
-  alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Web.MastodonAPI.NotificationView
-  alias Pleroma.Web.MastodonAPI.StatusView
 
   plug(Pleroma.Web.ApiSpec.CastAndValidate)
 
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["read:statuses"]}
-    when action in [:conversation, :conversation_statuses]
-  )
-
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["write:conversations"]}
-    when action in [:update_conversation, :mark_conversations_as_read]
-  )
-
   plug(
     OAuthScopesPlug,
     %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
@@ -36,87 +18,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
 
   defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation
 
-  def conversation(%{assigns: %{user: user}} = conn, %{id: participation_id}) do
-    with %Participation{} = participation <- Participation.get(participation_id),
-         true <- user.id == participation.user_id do
-      conn
-      |> put_view(ConversationView)
-      |> render("participation.json", %{participation: participation, for: user})
-    else
-      _error ->
-        conn
-        |> put_status(404)
-        |> json(%{"error" => "Unknown conversation id"})
-    end
-  end
-
-  def conversation_statuses(
-        %{assigns: %{user: %{id: user_id} = user}} = conn,
-        %{id: participation_id} = params
-      ) do
-    with %Participation{user_id: ^user_id} = participation <-
-           Participation.get(participation_id, preload: [:conversation]) do
-      params =
-        params
-        |> Map.new(fn {key, value} -> {to_string(key), value} end)
-        |> Map.put("blocking_user", user)
-        |> Map.put("muting_user", user)
-        |> Map.put("user", user)
-
-      activities =
-        participation.conversation.ap_id
-        |> ActivityPub.fetch_activities_for_context_query(params)
-        |> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
-        |> Enum.reverse()
-
-      conn
-      |> add_link_headers(activities)
-      |> put_view(StatusView)
-      |> render("index.json",
-        activities: activities,
-        for: user,
-        as: :activity
-      )
-    else
-      _error ->
-        conn
-        |> put_status(404)
-        |> json(%{"error" => "Unknown conversation id"})
-    end
-  end
-
-  def update_conversation(
-        %{assigns: %{user: user}} = conn,
-        %{id: participation_id, recipients: recipients}
-      ) do
-    with %Participation{} = participation <- Participation.get(participation_id),
-         true <- user.id == participation.user_id,
-         {:ok, participation} <- Participation.set_recipients(participation, recipients) do
-      conn
-      |> put_view(ConversationView)
-      |> render("participation.json", %{participation: participation, for: user})
-    else
-      {:error, message} ->
-        conn
-        |> put_status(:bad_request)
-        |> json(%{"error" => message})
-
-      _error ->
-        conn
-        |> put_status(404)
-        |> json(%{"error" => "Unknown conversation id"})
-    end
-  end
-
-  def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
-    with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
-      conn
-      |> add_link_headers(participations)
-      |> put_view(ConversationView)
-      |> render("participations.json", participations: participations, for: user)
-    end
-  end
-
   def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do
     with {:ok, notification} <- Notification.read_one(user, notification_id) do
       conn
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 12381511e..78da4a871 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -305,15 +305,11 @@ defmodule Pleroma.Web.Router do
     scope [] do
       pipe_through(:authenticated_api)
 
-      get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
-      get("/conversations/:id", PleromaAPIController, :conversation)
-      post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
-    end
+      get("/conversations/:id/statuses", ConversationController, :statuses)
+      get("/conversations/:id", ConversationController, :show)
+      post("/conversations/read", ConversationController, :mark_as_read)
+      patch("/conversations/:id", ConversationController, :update)
 
-    scope [] do
-      pipe_through(:authenticated_api)
-
-      patch("/conversations/:id", PleromaAPIController, :update_conversation)
       put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
       delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
       post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
diff --git a/test/web/pleroma_api/controllers/conversation_controller_test.exs b/test/web/pleroma_api/controllers/conversation_controller_test.exs
new file mode 100644
index 000000000..e6d0b3e37
--- /dev/null
+++ b/test/web/pleroma_api/controllers/conversation_controller_test.exs
@@ -0,0 +1,136 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.Repo
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  test "/api/v1/pleroma/conversations/:id" do
+    user = insert(:user)
+    %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})
+
+    [participation] = Participation.for_user(other_user)
+
+    result =
+      conn
+      |> get("/api/v1/pleroma/conversations/#{participation.id}")
+      |> json_response_and_validate_schema(200)
+
+    assert result["id"] == participation.id |> to_string()
+  end
+
+  test "/api/v1/pleroma/conversations/:id/statuses" do
+    user = insert(:user)
+    %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
+    third_user = insert(:user)
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"})
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})
+
+    [participation] = Participation.for_user(other_user)
+
+    {:ok, activity_two} =
+      CommonAPI.post(other_user, %{
+        status: "Hi!",
+        in_reply_to_status_id: activity.id,
+        in_reply_to_conversation_id: participation.id
+      })
+
+    result =
+      conn
+      |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
+      |> json_response_and_validate_schema(200)
+
+    assert length(result) == 2
+
+    id_one = activity.id
+    id_two = activity_two.id
+    assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
+
+    {:ok, %{id: id_three}} =
+      CommonAPI.post(other_user, %{
+        status: "Bye!",
+        in_reply_to_status_id: activity.id,
+        in_reply_to_conversation_id: participation.id
+      })
+
+    assert [%{"id" => ^id_two}, %{"id" => ^id_three}] =
+             conn
+             |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2")
+             |> json_response_and_validate_schema(:ok)
+
+    assert [%{"id" => ^id_three}] =
+             conn
+             |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}")
+             |> json_response_and_validate_schema(:ok)
+  end
+
+  test "PATCH /api/v1/pleroma/conversations/:id" do
+    %{user: user, conn: conn} = oauth_access(["write:conversations"])
+    other_user = insert(:user)
+
+    {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"})
+
+    [participation] = Participation.for_user(user)
+
+    participation = Repo.preload(participation, :recipients)
+
+    user = User.get_cached_by_id(user.id)
+    assert [user] == participation.recipients
+    assert other_user not in participation.recipients
+
+    query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}"
+
+    result =
+      conn
+      |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}")
+      |> json_response_and_validate_schema(200)
+
+    assert result["id"] == participation.id |> to_string
+
+    [participation] = Participation.for_user(user)
+    participation = Repo.preload(participation, :recipients)
+
+    assert user in participation.recipients
+    assert other_user in participation.recipients
+  end
+
+  test "POST /api/v1/pleroma/conversations/read" do
+    user = insert(:user)
+    %{user: other_user, conn: conn} = oauth_access(["write:conversations"])
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})
+
+    [participation2, participation1] = Participation.for_user(other_user)
+    assert Participation.get(participation2.id).read == false
+    assert Participation.get(participation1.id).read == false
+    assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2
+
+    [%{"unread" => false}, %{"unread" => false}] =
+      conn
+      |> post("/api/v1/pleroma/conversations/read", %{})
+      |> json_response_and_validate_schema(200)
+
+    [participation2, participation1] = Participation.for_user(other_user)
+    assert Participation.get(participation2.id).read == true
+    assert Participation.get(participation1.id).read == true
+    assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
+  end
+end
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
index 6f4f01e6f..c4c661266 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
@@ -5,136 +5,12 @@
 defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
   use Pleroma.Web.ConnCase
 
-  alias Pleroma.Conversation.Participation
   alias Pleroma.Notification
   alias Pleroma.Repo
-  alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
-  test "/api/v1/pleroma/conversations/:id" do
-    user = insert(:user)
-    %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
-
-    {:ok, _activity} =
-      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})
-
-    [participation] = Participation.for_user(other_user)
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/conversations/#{participation.id}")
-      |> json_response_and_validate_schema(200)
-
-    assert result["id"] == participation.id |> to_string()
-  end
-
-  test "/api/v1/pleroma/conversations/:id/statuses" do
-    user = insert(:user)
-    %{user: other_user, conn: conn} = oauth_access(["read:statuses"])
-    third_user = insert(:user)
-
-    {:ok, _activity} =
-      CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"})
-
-    {:ok, activity} =
-      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})
-
-    [participation] = Participation.for_user(other_user)
-
-    {:ok, activity_two} =
-      CommonAPI.post(other_user, %{
-        status: "Hi!",
-        in_reply_to_status_id: activity.id,
-        in_reply_to_conversation_id: participation.id
-      })
-
-    result =
-      conn
-      |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
-      |> json_response_and_validate_schema(200)
-
-    assert length(result) == 2
-
-    id_one = activity.id
-    id_two = activity_two.id
-    assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
-
-    {:ok, %{id: id_three}} =
-      CommonAPI.post(other_user, %{
-        status: "Bye!",
-        in_reply_to_status_id: activity.id,
-        in_reply_to_conversation_id: participation.id
-      })
-
-    assert [%{"id" => ^id_two}, %{"id" => ^id_three}] =
-             conn
-             |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2")
-             |> json_response_and_validate_schema(:ok)
-
-    assert [%{"id" => ^id_three}] =
-             conn
-             |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}")
-             |> json_response_and_validate_schema(:ok)
-  end
-
-  test "PATCH /api/v1/pleroma/conversations/:id" do
-    %{user: user, conn: conn} = oauth_access(["write:conversations"])
-    other_user = insert(:user)
-
-    {:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"})
-
-    [participation] = Participation.for_user(user)
-
-    participation = Repo.preload(participation, :recipients)
-
-    user = User.get_cached_by_id(user.id)
-    assert [user] == participation.recipients
-    assert other_user not in participation.recipients
-
-    query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}"
-
-    result =
-      conn
-      |> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}")
-      |> json_response_and_validate_schema(200)
-
-    assert result["id"] == participation.id |> to_string
-
-    [participation] = Participation.for_user(user)
-    participation = Repo.preload(participation, :recipients)
-
-    assert user in participation.recipients
-    assert other_user in participation.recipients
-  end
-
-  test "POST /api/v1/pleroma/conversations/read" do
-    user = insert(:user)
-    %{user: other_user, conn: conn} = oauth_access(["write:conversations"])
-
-    {:ok, _activity} =
-      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})
-
-    {:ok, _activity} =
-      CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})
-
-    [participation2, participation1] = Participation.for_user(other_user)
-    assert Participation.get(participation2.id).read == false
-    assert Participation.get(participation1.id).read == false
-    assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2
-
-    [%{"unread" => false}, %{"unread" => false}] =
-      conn
-      |> post("/api/v1/pleroma/conversations/read", %{})
-      |> json_response_and_validate_schema(200)
-
-    [participation2, participation1] = Participation.for_user(other_user)
-    assert Participation.get(participation2.id).read == true
-    assert Participation.get(participation1.id).read == true
-    assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
-  end
-
   describe "POST /api/v1/pleroma/notifications/read" do
     setup do: oauth_access(["write:notifications"])
 

From 5ba6e1c322c0937849eca53fc816f348659fb34c Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Wed, 20 May 2020 15:14:11 +0400
Subject: [PATCH 6/8] Move notification actions to
 PleromaAPI.NotificationController

---
 ...n.ex => pleroma_notification_operation.ex} |  6 +--
 .../controllers/notification_controller.ex    | 36 +++++++++++++++
 .../controllers/pleroma_api_controller.ex     | 46 -------------------
 lib/pleroma/web/router.ex                     |  2 +-
 ...t.exs => notification_controller_test.exs} |  2 +-
 5 files changed, 41 insertions(+), 51 deletions(-)
 rename lib/pleroma/web/api_spec/operations/{pleroma_operation.ex => pleroma_notification_operation.ex} (89%)
 create mode 100644 lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
 delete mode 100644 lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
 rename test/web/pleroma_api/controllers/{pleroma_api_controller_test.exs => notification_controller_test.exs} (97%)

diff --git a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
similarity index 89%
rename from lib/pleroma/web/api_spec/operations/pleroma_operation.ex
rename to lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
index d28451933..636c39a15 100644
--- a/lib/pleroma/web/api_spec/operations/pleroma_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
@@ -2,7 +2,7 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.ApiSpec.PleromaOperation do
+defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
   alias OpenApiSpex.Operation
   alias OpenApiSpex.Schema
   alias Pleroma.Web.ApiSpec.NotificationOperation
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation do
     apply(__MODULE__, operation, [])
   end
 
-  def mark_notifications_as_read_operation do
+  def mark_as_read_operation do
     %Operation{
       tags: ["Notifications"],
       summary: "Mark notifications as read. Query parameters are mutually exclusive.",
@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaOperation do
         Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id")
       ],
       security: [%{"oAuth" => ["write:notifications"]}],
-      operationId: "PleromaController.mark_notifications_as_read",
+      operationId: "PleromaAPI.NotificationController.mark_as_read",
       responses: %{
         200 =>
           Operation.response(
diff --git a/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
new file mode 100644
index 000000000..0b2f678c5
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
@@ -0,0 +1,36 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.NotificationController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Notification
+  alias Pleroma.Plugs.OAuthScopesPlug
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+  plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read)
+  plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView)
+
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation
+
+  def mark_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do
+    with {:ok, notification} <- Notification.read_one(user, notification_id) do
+      render(conn, "show.json", notification: notification, for: user)
+    else
+      {:error, message} ->
+        conn
+        |> put_status(:bad_request)
+        |> json(%{"error" => message})
+    end
+  end
+
+  def mark_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do
+    notifications =
+      user
+      |> Notification.set_read_up_to(max_id)
+      |> Enum.take(80)
+
+    render(conn, "index.json", notifications: notifications, for: user)
+  end
+end
diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
deleted file mode 100644
index a58665abe..000000000
--- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
+++ /dev/null
@@ -1,46 +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.PleromaAPI.PleromaAPIController do
-  use Pleroma.Web, :controller
-
-  alias Pleroma.Notification
-  alias Pleroma.Plugs.OAuthScopesPlug
-  alias Pleroma.Web.MastodonAPI.NotificationView
-
-  plug(Pleroma.Web.ApiSpec.CastAndValidate)
-
-  plug(
-    OAuthScopesPlug,
-    %{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
-  )
-
-  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaOperation
-
-  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do
-    with {:ok, notification} <- Notification.read_one(user, notification_id) do
-      conn
-      |> put_view(NotificationView)
-      |> render("show.json", %{notification: notification, for: user})
-    else
-      {:error, message} ->
-        conn
-        |> put_status(:bad_request)
-        |> json(%{"error" => message})
-    end
-  end
-
-  def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do
-    with notifications <- Notification.set_read_up_to(user, max_id) do
-      notifications = Enum.take(notifications, 80)
-
-      conn
-      |> put_view(NotificationView)
-      |> render("index.json",
-        notifications: notifications,
-        for: user
-      )
-    end
-  end
-end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 78da4a871..0e29e5645 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -312,7 +312,7 @@ defmodule Pleroma.Web.Router do
 
       put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
       delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
-      post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
+      post("/notifications/read", NotificationController, :mark_as_read)
 
       patch("/accounts/update_avatar", AccountController, :update_avatar)
       patch("/accounts/update_banner", AccountController, :update_banner)
diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/notification_controller_test.exs
similarity index 97%
rename from test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
rename to test/web/pleroma_api/controllers/notification_controller_test.exs
index c4c661266..7c5ace804 100644
--- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/controllers/notification_controller_test.exs
@@ -2,7 +2,7 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
+defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do
   use Pleroma.Web.ConnCase
 
   alias Pleroma.Notification

From 9bc5e18adeef2c68c5fae2435ed01555f1b29c93 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Thu, 21 May 2020 08:06:57 +0300
Subject: [PATCH 7/8] rename mix task: `pleroma.user unsubscribe` ->
 `pleroma.user deactivate`

---
 docs/administration/CLI_tasks/user.md | 19 +++++++++----------
 lib/mix/tasks/pleroma/user.ex         |  6 +++---
 test/tasks/user_test.exs              |  8 ++++----
 3 files changed, 16 insertions(+), 17 deletions(-)

diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md
index f535dad82..797641898 100644
--- a/docs/administration/CLI_tasks/user.md
+++ b/docs/administration/CLI_tasks/user.md
@@ -95,33 +95,33 @@ mix pleroma.user sign_out <nickname>
 ```
 
 
-## Deactivate or activate a user 
+## Deactivate or activate a user
 ```sh tab="OTP"
- ./bin/pleroma_ctl user toggle_activated <nickname> 
+ ./bin/pleroma_ctl user toggle_activated <nickname>
 ```
 
 ```sh tab="From Source"
-mix pleroma.user toggle_activated <nickname> 
+mix pleroma.user toggle_activated <nickname>
 ```
 
 
-## Unsubscribe local users from a user and deactivate the user
+## Deactivate a user and unsubscribes local users from the user
 ```sh tab="OTP"
- ./bin/pleroma_ctl user unsubscribe NICKNAME
+ ./bin/pleroma_ctl user deactivate NICKNAME
 ```
 
 ```sh tab="From Source"
-mix pleroma.user unsubscribe NICKNAME
+mix pleroma.user deactivate NICKNAME
 ```
 
 
-## Unsubscribe local users from an instance and deactivate all accounts on it
+## Deactivate all accounts from an instance and unsubscribe local users on it
 ```sh tab="OTP"
- ./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
+ ./bin/pleroma_ctl user deacitivate_all_from_instance <instance>
 ```
 
 ```sh tab="From Source"
-mix pleroma.user unsubscribe_all_from_instance <instance>
+mix pleroma.user deactivate_all_from_instance <instance>
 ```
 
 
@@ -177,4 +177,3 @@ mix pleroma.user untag <nickname> <tags>
 ```sh tab="From Source"
 mix pleroma.user toggle_confirmed <nickname>
 ```
-
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index 93ecb4631..3635c02bc 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -144,7 +144,7 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
-  def run(["unsubscribe", nickname]) do
+  def run(["deactivate", nickname]) do
     start_pleroma()
 
     with %User{} = user <- User.get_cached_by_nickname(nickname) do
@@ -163,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do
     end
   end
 
-  def run(["unsubscribe_all_from_instance", instance]) do
+  def run(["deactivate_all_from_instance", instance]) do
     start_pleroma()
 
     Pleroma.User.Query.build(%{nickname: "@#{instance}"})
@@ -171,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.User do
     |> Stream.each(fn users ->
       users
       |> Enum.each(fn user ->
-        run(["unsubscribe", user.nickname])
+        run(["deactivate", user.nickname])
       end)
     end)
     |> Stream.run()
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index 4b3ab5a87..ab7637511 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -169,7 +169,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do
     end
   end
 
-  describe "running unsubscribe" do
+  describe "running deactivate" do
     test "user is unsubscribed" do
       followed = insert(:user)
       remote_followed = insert(:user, local: false)
@@ -178,7 +178,7 @@ defmodule Mix.Tasks.Pleroma.UserTest do
       User.follow(user, followed, :follow_accept)
       User.follow(user, remote_followed, :follow_accept)
 
-      Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
+      Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname])
 
       assert_received {:mix_shell, :info, [message]}
       assert message =~ "Deactivating"
@@ -192,8 +192,8 @@ defmodule Mix.Tasks.Pleroma.UserTest do
       assert user.deactivated
     end
 
-    test "no user to unsubscribe" do
-      Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"])
+    test "no user to deactivate" do
+      Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"])
 
       assert_received {:mix_shell, :error, [message]}
       assert message =~ "No user"

From 4ae2f75c3e5d293c24fac978b1ae10fdfa7a3c00 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Thu, 21 May 2020 10:27:06 +0000
Subject: [PATCH 8/8] Apply suggestion to docs/administration/CLI_tasks/user.md

---
 docs/administration/CLI_tasks/user.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/administration/CLI_tasks/user.md b/docs/administration/CLI_tasks/user.md
index 797641898..afeb8d52f 100644
--- a/docs/administration/CLI_tasks/user.md
+++ b/docs/administration/CLI_tasks/user.md
@@ -117,7 +117,7 @@ mix pleroma.user deactivate NICKNAME
 
 ## Deactivate all accounts from an instance and unsubscribe local users on it
 ```sh tab="OTP"
- ./bin/pleroma_ctl user deacitivate_all_from_instance <instance>
+ ./bin/pleroma_ctl user deactivate_all_from_instance <instance>
 ```
 
 ```sh tab="From Source"