From 7483679a7b6ff63c9c61c3df3e9e37f2c24012ff Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 31 Jul 2019 15:12:29 +0200
Subject: [PATCH 01/40] StatusView: Return direct conversation id.

---
 lib/pleroma/conversation/participation.ex      |  8 ++++++++
 .../web/mastodon_api/views/status_view.ex      | 18 +++++++++++++++++-
 test/web/mastodon_api/status_view_test.exs     | 18 +++++++++++++++++-
 3 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 5883e4183..77b3f61e9 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -65,6 +65,14 @@ def for_user(user, params \\ %{}) do
     |> Pleroma.Pagination.fetch_paginated(params)
   end
 
+  def for_user_and_conversation(user, conversation) do
+    from(p in __MODULE__,
+      where: p.user_id == ^user.id,
+      where: p.conversation_id == ^conversation.id
+    )
+    |> Repo.one()
+  end
+
   def for_user_with_last_activity_id(user, params \\ %{}) do
     for_user(user, params)
     |> Enum.map(fn participation ->
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 80df9b2ac..a862554b1 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   use Pleroma.Web, :view
 
   alias Pleroma.Activity
+  alias Pleroma.Conversation
+  alias Pleroma.Conversation.Participation
   alias Pleroma.HTML
   alias Pleroma.Object
   alias Pleroma.Repo
@@ -225,6 +227,19 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
         object.data["url"] || object.data["external_url"] || object.data["id"]
       end
 
+    direct_conversation_id =
+      with {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
+           {_, %User{} = for_user} <- {:for_user, opts[:for]},
+           %{data: %{"context" => context}} when is_binary(context) <- activity,
+           %Conversation{} = conversation <- Conversation.get_for_ap_id(context),
+           %Participation{id: participation_id} <-
+             Participation.for_user_and_conversation(for_user, conversation) do
+        participation_id
+      else
+        _e ->
+          nil
+      end
+
     %{
       id: to_string(activity.id),
       uri: object.data["id"],
@@ -262,7 +277,8 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
         conversation_id: get_context_id(activity),
         in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
         content: %{"text/plain" => content_plaintext},
-        spoiler_text: %{"text/plain" => summary_plaintext}
+        spoiler_text: %{"text/plain" => summary_plaintext},
+        direct_conversation_id: direct_conversation_id
       }
     }
   end
diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs
index 0b167f839..c983b494f 100644
--- a/test/web/mastodon_api/status_view_test.exs
+++ b/test/web/mastodon_api/status_view_test.exs
@@ -23,6 +23,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
     :ok
   end
 
+  test "returns the direct conversation id when given the `with_conversation_id` option" do
+    user = insert(:user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
+
+    status =
+      StatusView.render("status.json",
+        activity: activity,
+        with_direct_conversation_id: true,
+        for: user
+      )
+
+    assert status[:pleroma][:direct_conversation_id]
+  end
+
   test "returns a temporary ap_id based user for activities missing db users" do
     user = insert(:user)
 
@@ -133,7 +148,8 @@ test "a note activity" do
         conversation_id: convo_id,
         in_reply_to_account_acct: nil,
         content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
-        spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}
+        spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
+        direct_conversation_id: nil
       }
     }
 

From fd4b7239cd6f44a25c9aa4195750e94e0612a3b1 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Thu, 1 Aug 2019 17:25:46 +0200
Subject: [PATCH 02/40] nothing


From f88560accd801ac88c60344cef93ef00cf136069 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 2 Aug 2019 11:55:41 +0200
Subject: [PATCH 03/40] Conversations: Add recipient list to conversation
 participation.

This enables to address the same group of people every time.
---
 lib/pleroma/conversation.ex                   | 11 ++++++
 lib/pleroma/conversation/participation.ex     |  4 +++
 .../participation_recipient_ship.ex           | 34 +++++++++++++++++++
 lib/pleroma/user.ex                           |  7 ++++
 .../20190205114625_create_thread_mutes.exs    |  2 +-
 ...ersation_participation_recipient_ships.exs | 13 +++++++
 test/conversation/participation_test.exs      | 30 ++++++++++++++++
 7 files changed, 100 insertions(+), 1 deletion(-)
 create mode 100644 lib/pleroma/conversation/participation_recipient_ship.ex
 create mode 100644 priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs

diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index bc97b39ca..fb0dfedca 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Conversation do
   alias Pleroma.Conversation.Participation
+  alias Pleroma.Conversation.Participation.RecipientShip
   alias Pleroma.Repo
   alias Pleroma.User
   use Ecto.Schema
@@ -39,6 +40,15 @@ def get_for_ap_id(ap_id) do
     Repo.get_by(__MODULE__, ap_id: ap_id)
   end
 
+  def maybe_set_recipients(participation, activity) do
+    participation = Repo.preload(participation, :recipients)
+
+    if participation.recipients |> Enum.empty?() do
+      recipients = User.get_all_by_ap_id(activity.recipients)
+      RecipientShip.create(recipients, participation)
+    end
+  end
+
   @doc """
   This will
   1. Create a conversation if there isn't one already
@@ -60,6 +70,7 @@ def create_or_bump_for(activity, opts \\ []) do
           {:ok, participation} =
             Participation.create_for_user_and_conversation(user, conversation, opts)
 
+          maybe_set_recipients(participation, activity)
           participation
         end)
 
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 77b3f61e9..121efb671 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -5,6 +5,7 @@
 defmodule Pleroma.Conversation.Participation do
   use Ecto.Schema
   alias Pleroma.Conversation
+  alias Pleroma.Conversation.Participation.RecipientShip
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -17,6 +18,9 @@ defmodule Pleroma.Conversation.Participation do
     field(:read, :boolean, default: false)
     field(:last_activity_id, Pleroma.FlakeId, virtual: true)
 
+    has_many(:recipient_ships, RecipientShip)
+    has_many(:recipients, through: [:recipient_ships, :user])
+
     timestamps()
   end
 
diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex
new file mode 100644
index 000000000..27c0c89cd
--- /dev/null
+++ b/lib/pleroma/conversation/participation_recipient_ship.ex
@@ -0,0 +1,34 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Conversation.Participation.RecipientShip do
+  use Ecto.Schema
+
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.User
+  alias Pleroma.Repo
+
+  import Ecto.Changeset
+
+  schema "conversation_participation_recipient_ships" do
+    belongs_to(:user, User, type: Pleroma.FlakeId)
+    belongs_to(:participation, Participation)
+  end
+
+  def creation_cng(struct, params) do
+    struct
+    |> cast(params, [:user_id, :participation_id])
+    |> validate_required([:user_id, :participation_id])
+  end
+
+  def create(%User{} = user, participation), do: create([user], participation)
+
+  def create(users, participation) do
+    Enum.each(users, fn user ->
+      %__MODULE__{}
+      |> creation_cng(%{user_id: user.id, participation_id: participation.id})
+      |> Repo.insert!()
+    end)
+  end
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 6e2fd3af8..a021e77f0 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -450,6 +450,13 @@ def get_by_ap_id(ap_id) do
     Repo.get_by(User, ap_id: ap_id)
   end
 
+  def get_all_by_ap_id(ap_ids) do
+    from(u in __MODULE__,
+      where: u.ap_id in ^ap_ids
+    )
+    |> Repo.all()
+  end
+
   # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
   # of the ap_id and the domain and tries to get that user
   def get_by_guessed_nickname(ap_id) do
diff --git a/priv/repo/migrations/20190205114625_create_thread_mutes.exs b/priv/repo/migrations/20190205114625_create_thread_mutes.exs
index 7e44db121..baaf07253 100644
--- a/priv/repo/migrations/20190205114625_create_thread_mutes.exs
+++ b/priv/repo/migrations/20190205114625_create_thread_mutes.exs
@@ -6,7 +6,7 @@ def change do
       add :user_id, references(:users, type: :uuid, on_delete: :delete_all)
       add :context, :string
     end
-    
+
     create_if_not_exists unique_index(:thread_mutes, [:user_id, :context], name: :unique_index)
   end
 end
diff --git a/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs
new file mode 100644
index 000000000..c6e3469d5
--- /dev/null
+++ b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs
@@ -0,0 +1,13 @@
+defmodule Pleroma.Repo.Migrations.CreateConversationParticipationRecipientShips do
+  use Ecto.Migration
+
+  def change do
+    create_if_not_exists table(:conversation_participation_recipient_ships) do
+      add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+      add(:participation_id, references(:conversation_participations, on_delete: :delete_all))
+    end
+
+    create_if_not_exists index(:conversation_participation_recipient_ships, [:user_id])
+    create_if_not_exists index(:conversation_participation_recipient_ships, [:participation_id])
+  end
+end
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index 2a03e5d67..4a3c397bd 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -8,6 +8,36 @@ defmodule Pleroma.Conversation.ParticipationTest do
   alias Pleroma.Conversation.Participation
   alias Pleroma.Web.CommonAPI
 
+  test "for a new conversation, it sets the recipents of the participation" do
+    user = insert(:user)
+    other_user = insert(:user)
+    third_user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+    [participation] = Participation.for_user(user)
+    participation = Pleroma.Repo.preload(participation, :recipients)
+
+    assert length(participation.recipients) == 2
+    assert user in participation.recipients
+    assert other_user in participation.recipients
+
+    # Mentioning another user in the same conversation will not add a new recipients.
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{
+        "in_reply_to_status_id" => activity.id,
+        "status" => "Hey @#{third_user.nickname}.",
+        "visibility" => "direct"
+      })
+
+    [participation] = Participation.for_user(user)
+    participation = Pleroma.Repo.preload(participation, :recipients)
+
+    assert length(participation.recipients) == 2
+  end
+
   test "it creates a participation for a conversation and a user" do
     user = insert(:user)
     conversation = insert(:conversation)

From 56b1c3af13c9519e13da688bdbbfdd8d69cda4ac Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 2 Aug 2019 15:05:27 +0200
Subject: [PATCH 04/40] CommonAPI: Extend api with conversation replies.

---
 lib/pleroma/conversation/participation.ex |  6 +++++
 lib/pleroma/web/common_api/common_api.ex  | 20 ++++++++++-----
 lib/pleroma/web/common_api/utils.ex       | 27 ++++++++++++++------
 test/web/common_api/common_api_test.exs   | 30 +++++++++++++++++++++++
 4 files changed, 70 insertions(+), 13 deletions(-)

diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index 121efb671..f1e1a6958 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -93,4 +93,10 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
     end)
     |> Enum.filter(& &1.last_activity_id)
   end
+
+  def get(nil), do: nil
+
+  def get(id) do
+    Repo.get(__MODULE__, id)
+  end
 end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 2db58324b..86e95cd0f 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -4,6 +4,7 @@
 
 defmodule Pleroma.Web.CommonAPI do
   alias Pleroma.Activity
+  alias Pleroma.Conversation.Participation
   alias Pleroma.Formatter
   alias Pleroma.Object
   alias Pleroma.ThreadMute
@@ -171,21 +172,25 @@ defp normalize_and_validate_choice_indices(choices, count) do
     end)
   end
 
-  def get_visibility(%{"visibility" => visibility}, in_reply_to)
+  def get_visibility(_, _, %Participation{}) do
+    {"direct", "direct"}
+  end
+
+  def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
       when visibility in ~w{public unlisted private direct},
       do: {visibility, get_replied_to_visibility(in_reply_to)}
 
-  def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do
+  def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do
     visibility = {:list, String.to_integer(list_id)}
     {visibility, get_replied_to_visibility(in_reply_to)}
   end
 
-  def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do
+  def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
     visibility = get_replied_to_visibility(in_reply_to)
     {visibility, visibility}
   end
 
-  def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)}
+  def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)}
 
   def get_replied_to_visibility(nil), do: nil
 
@@ -201,7 +206,9 @@ def post(user, %{"status" => status} = data) do
     with status <- String.trim(status),
          attachments <- attachments_from_ids(data),
          in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
-         {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to),
+         in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
+         {visibility, in_reply_to_visibility} <-
+           get_visibility(data, in_reply_to, in_reply_to_conversation),
          {_, false} <-
            {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
          {content_html, mentions, tags} <-
@@ -214,7 +221,8 @@ def post(user, %{"status" => status} = data) do
          mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
          addressed_users <- get_addressed_users(mentioned_users, data["to"]),
          {poll, poll_emoji} <- make_poll_data(data),
-         {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility),
+         {to, cc} <-
+           get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
          context <- make_context(in_reply_to),
          cw <- data["spoiler_text"] || "",
          sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index d80fffa26..e70ba7d43 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
   alias Calendar.Strftime
   alias Pleroma.Activity
   alias Pleroma.Config
+  alias Pleroma.Conversation.Participation
   alias Pleroma.Formatter
   alias Pleroma.Object
   alias Pleroma.Plugs.AuthenticationPlug
@@ -64,9 +65,21 @@ def attachments_from_ids_descs(ids, descs_str) do
     end)
   end
 
-  @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) ::
+  @spec get_to_and_cc(
+          User.t(),
+          list(String.t()),
+          Activity.t() | nil,
+          String.t(),
+          Participation.t() | nil
+        ) ::
           {list(String.t()), list(String.t())}
-  def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
+
+  def get_to_and_cc(_, _, _, _, %Participation{} = participation) do
+    participation = Repo.preload(participation, :recipients)
+    {Enum.map(participation.recipients, & &1.ap_id), []}
+  end
+
+  def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do
     to = [Pleroma.Constants.as_public() | mentioned_users]
     cc = [user.follower_address]
 
@@ -77,7 +90,7 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do
     end
   end
 
-  def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
+  def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do
     to = [user.follower_address | mentioned_users]
     cc = [Pleroma.Constants.as_public()]
 
@@ -88,12 +101,12 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do
     end
   end
 
-  def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do
-    {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct")
+  def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do
+    {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil)
     {[user.follower_address | to], cc}
   end
 
-  def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
+  def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do
     if inReplyTo do
       {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
     else
@@ -101,7 +114,7 @@ def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do
     end
   end
 
-  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []}
+  def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []}
 
   def get_addressed_users(_, to) when is_list(to) do
     User.get_ap_ids_by_nicknames(to)
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 16b3f121d..e2a5bf117 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -5,6 +5,7 @@
 defmodule Pleroma.Web.CommonAPITest do
   use Pleroma.DataCase
   alias Pleroma.Activity
+  alias Pleroma.Conversation.Participation
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
@@ -12,6 +13,35 @@ defmodule Pleroma.Web.CommonAPITest do
 
   import Pleroma.Factory
 
+  test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
+    har = insert(:user)
+    jafnhar = insert(:user)
+    tridi = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(har, %{
+        "status" => "@#{jafnhar.nickname} hey",
+        "visibility" => "direct"
+      })
+
+    assert har.ap_id in activity.recipients
+    assert jafnhar.ap_id in activity.recipients
+
+    [participation] = Participation.for_user(har)
+
+    {:ok, activity} =
+      CommonAPI.post(har, %{
+        "status" => "I don't really like @#{tridi.nickname}",
+        "visibility" => "direct",
+        "in_reply_to_status_id" => activity.id,
+        "in_reply_to_conversation_id" => participation.id
+      })
+
+    assert har.ap_id in activity.recipients
+    assert jafnhar.ap_id in activity.recipients
+    refute tridi.ap_id in activity.recipients
+  end
+
   test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do
     har = insert(:user)
     jafnhar = insert(:user)

From eee98aaa738c1aa5f2e4203a61b67648d62965c8 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Fri, 2 Aug 2019 19:53:08 +0200
Subject: [PATCH 05/40] Pleroma API: Add endpoint to get conversation statuses.

---
 lib/pleroma/web/controller_helper.ex          | 76 +++++++++++++++++++
 .../mastodon_api/mastodon_api_controller.ex   | 68 +----------------
 .../web/pleroma_api/pleroma_api_controller.ex | 49 ++++++++++++
 lib/pleroma/web/router.ex                     |  9 +++
 test/web/common_api/common_api_utils_test.exs | 16 ++--
 .../pleroma_api_controller_test.exs           | 45 +++++++++++
 6 files changed, 189 insertions(+), 74 deletions(-)
 create mode 100644 lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
 create mode 100644 test/web/pleroma_api/pleroma_api_controller_test.exs

diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex
index 8a753bb4f..eeac9f503 100644
--- a/lib/pleroma/web/controller_helper.ex
+++ b/lib/pleroma/web/controller_helper.ex
@@ -33,4 +33,80 @@ defp param_to_integer(val, default) when is_binary(val) do
   end
 
   defp param_to_integer(_, default), do: default
+
+  def add_link_headers(
+        conn,
+        method,
+        activities,
+        param \\ nil,
+        params \\ %{},
+        func3 \\ nil,
+        func4 \\ nil
+      ) do
+    params =
+      conn.params
+      |> Map.drop(["since_id", "max_id", "min_id"])
+      |> Map.merge(params)
+
+    last = List.last(activities)
+
+    func3 = func3 || (&mastodon_api_url/3)
+    func4 = func4 || (&mastodon_api_url/4)
+
+    if last do
+      max_id = last.id
+
+      limit =
+        params
+        |> Map.get("limit", "20")
+        |> String.to_integer()
+
+      min_id =
+        if length(activities) <= limit do
+          activities
+          |> List.first()
+          |> Map.get(:id)
+        else
+          activities
+          |> Enum.at(limit * -1)
+          |> Map.get(:id)
+        end
+
+      {next_url, prev_url} =
+        if param do
+          {
+            func4.(
+              Pleroma.Web.Endpoint,
+              method,
+              param,
+              Map.merge(params, %{max_id: max_id})
+            ),
+            func4.(
+              Pleroma.Web.Endpoint,
+              method,
+              param,
+              Map.merge(params, %{min_id: min_id})
+            )
+          }
+        else
+          {
+            func3.(
+              Pleroma.Web.Endpoint,
+              method,
+              Map.merge(params, %{max_id: max_id})
+            ),
+            func3.(
+              Pleroma.Web.Endpoint,
+              method,
+              Map.merge(params, %{min_id: min_id})
+            )
+          }
+        end
+
+      conn
+      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
+    else
+      conn
+    end
+  end
 end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 174e93468..0deeab2be 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -5,7 +5,8 @@
 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
 
-  import Pleroma.Web.ControllerHelper, only: [json_response: 3]
+  import Pleroma.Web.ControllerHelper,
+    only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
 
   alias Ecto.Changeset
   alias Pleroma.Activity
@@ -342,71 +343,6 @@ def custom_emojis(conn, _params) do
     json(conn, mastodon_emoji)
   end
 
-  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
-    params =
-      conn.params
-      |> Map.drop(["since_id", "max_id", "min_id"])
-      |> Map.merge(params)
-
-    last = List.last(activities)
-
-    if last do
-      max_id = last.id
-
-      limit =
-        params
-        |> Map.get("limit", "20")
-        |> String.to_integer()
-
-      min_id =
-        if length(activities) <= limit do
-          activities
-          |> List.first()
-          |> Map.get(:id)
-        else
-          activities
-          |> Enum.at(limit * -1)
-          |> Map.get(:id)
-        end
-
-      {next_url, prev_url} =
-        if param do
-          {
-            mastodon_api_url(
-              Pleroma.Web.Endpoint,
-              method,
-              param,
-              Map.merge(params, %{max_id: max_id})
-            ),
-            mastodon_api_url(
-              Pleroma.Web.Endpoint,
-              method,
-              param,
-              Map.merge(params, %{min_id: min_id})
-            )
-          }
-        else
-          {
-            mastodon_api_url(
-              Pleroma.Web.Endpoint,
-              method,
-              Map.merge(params, %{max_id: max_id})
-            ),
-            mastodon_api_url(
-              Pleroma.Web.Endpoint,
-              method,
-              Map.merge(params, %{min_id: min_id})
-            )
-          }
-        end
-
-      conn
-      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
-    else
-      conn
-    end
-  end
-
   def home_timeline(%{assigns: %{user: user}} = conn, params) do
     params =
       params
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
new file mode 100644
index 000000000..b677892ed
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -0,0 +1,49 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
+  use Pleroma.Web, :controller
+
+  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
+
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Repo
+
+  def conversation_statuses(
+        %{assigns: %{user: user}} = conn,
+        %{"id" => participation_id} = params
+      ) do
+    params =
+      params
+      |> Map.put("blocking_user", user)
+      |> Map.put("muting_user", user)
+      |> Map.put("user", user)
+
+    participation =
+      participation_id
+      |> Participation.get()
+      |> Repo.preload(:conversation)
+
+    if user.id == participation.user_id do
+      activities =
+        participation.conversation.ap_id
+        |> ActivityPub.fetch_activities_for_context(params)
+        |> Enum.reverse()
+
+      conn
+      |> add_link_headers(
+        :conversation_statuses,
+        activities,
+        participation_id,
+        params,
+        nil,
+        &pleroma_api_url/4
+      )
+      |> put_view(StatusView)
+      |> render("index.json", %{activities: activities, for: user, as: :activity})
+    end
+  end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 0689d69fb..40298538a 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -257,6 +257,15 @@ defmodule Pleroma.Web.Router do
     end
   end
 
+  scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+    pipe_through(:authenticated_api)
+
+    scope [] do
+      pipe_through(:oauth_write)
+      get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
+    end
+  end
+
   scope "/api/v1", Pleroma.Web.MastodonAPI do
     pipe_through(:authenticated_api)
 
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index af320f31f..7510c8def 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -239,7 +239,7 @@ test "for public posts, not a reply" do
       mentioned_user = insert(:user)
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil)
 
       assert length(to) == 2
       assert length(cc) == 1
@@ -256,7 +256,7 @@ test "for public posts, a reply" do
       {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil)
 
       assert length(to) == 3
       assert length(cc) == 1
@@ -272,7 +272,7 @@ test "for unlisted posts, not a reply" do
       mentioned_user = insert(:user)
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil)
 
       assert length(to) == 2
       assert length(cc) == 1
@@ -289,7 +289,7 @@ test "for unlisted posts, a reply" do
       {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil)
 
       assert length(to) == 3
       assert length(cc) == 1
@@ -305,7 +305,7 @@ test "for private posts, not a reply" do
       mentioned_user = insert(:user)
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil)
 
       assert length(to) == 2
       assert length(cc) == 0
@@ -321,7 +321,7 @@ test "for private posts, a reply" do
       {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil)
 
       assert length(to) == 3
       assert length(cc) == 0
@@ -336,7 +336,7 @@ test "for direct posts, not a reply" do
       mentioned_user = insert(:user)
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil)
 
       assert length(to) == 1
       assert length(cc) == 0
@@ -351,7 +351,7 @@ test "for direct posts, a reply" do
       {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"})
       mentions = [mentioned_user.ap_id]
 
-      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct")
+      {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil)
 
       assert length(to) == 2
       assert length(cc) == 0
diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs
new file mode 100644
index 000000000..43104e36e
--- /dev/null
+++ b/test/web/pleroma_api/pleroma_api_controller_test.exs
@@ -0,0 +1,45 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
+  use Pleroma.Web.ConnCase
+
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.Web.CommonAPI
+
+  import Pleroma.Factory
+
+  test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+    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
+      |> assign(:user, other_user)
+      |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
+      |> json_response(200)
+
+    assert length(result) == 2
+
+    id_one = activity.id
+    id_two = activity_two.id
+    assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
+  end
+end

From 3af6d14da769aa5adfdd6360b43c691fd8c8eed5 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 5 Aug 2019 15:09:19 +0200
Subject: [PATCH 06/40] Pleroma Conversations API: Add a way to set recipients.

---
 lib/pleroma/conversation/participation.ex     | 20 ++++++++++
 .../mastodon_api/views/conversation_view.ex   | 13 +++++-
 .../web/pleroma_api/pleroma_api_controller.ex | 17 ++++++++
 lib/pleroma/web/router.ex                     |  1 +
 .../mastodon_api/conversation_view_test.exs   | 40 +++++++++++++++++++
 .../pleroma_api_controller_test.exs           | 31 ++++++++++++++
 6 files changed, 120 insertions(+), 2 deletions(-)
 create mode 100644 test/web/mastodon_api/conversation_view_test.exs

diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index f1e1a6958..acdee5517 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -99,4 +99,24 @@ def get(nil), do: nil
   def get(id) do
     Repo.get(__MODULE__, id)
   end
+
+  def set_recipients(participation, user_ids) do
+    Repo.transaction(fn ->
+      query =
+        from(r in RecipientShip,
+          where: r.participation_id == ^participation.id
+        )
+
+      Repo.delete_all(query)
+
+      users =
+        from(u in User,
+          where: u.id in ^user_ids
+        )
+        |> Repo.all()
+
+      RecipientShip.create(users, participation)
+      :ok
+    end)
+  end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 38bdec737..5adaecdb0 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
   alias Pleroma.Web.MastodonAPI.StatusView
 
   def render("participation.json", %{participation: participation, user: user}) do
-    participation = Repo.preload(participation, conversation: :users)
+    participation = Repo.preload(participation, conversation: :users, recipients: [])
 
     last_activity_id =
       with nil <- participation.last_activity_id do
@@ -37,11 +37,20 @@ def render("participation.json", %{participation: participation, user: user}) do
         as: :user
       })
 
+    recipients =
+      AccountView.render("accounts.json", %{
+        users: participation.recipients,
+        as: :user
+      })
+
     %{
       id: participation.id |> to_string(),
       accounts: accounts,
       unread: !participation.read,
-      last_status: last_status
+      last_status: last_status,
+      pleroma: %{
+        recipients: recipients
+      }
     }
   end
 end
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index b677892ed..759d8aef0 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -10,6 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   alias Pleroma.Conversation.Participation
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.MastodonAPI.StatusView
+  alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Repo
 
   def conversation_statuses(
@@ -46,4 +47,20 @@ def conversation_statuses(
       |> render("index.json", %{activities: activities, for: user, as: :activity})
     end
   end
+
+  def update_conversation(
+        %{assigns: %{user: user}} = conn,
+        %{"id" => participation_id, "recipients" => recipients}
+      ) do
+    participation =
+      participation_id
+      |> Participation.get()
+
+    with true <- user.id == participation.user_id,
+         {:ok, _} <- Participation.set_recipients(participation, recipients) do
+      conn
+      |> put_view(ConversationView)
+      |> render("participation.json", %{participation: participation, user: user})
+    end
+  end
 end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 40298538a..6cdef7e2f 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -263,6 +263,7 @@ defmodule Pleroma.Web.Router do
     scope [] do
       pipe_through(:oauth_write)
       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
+      patch("/conversations/:id", PleromaAPIController, :update_conversation)
     end
   end
 
diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs
new file mode 100644
index 000000000..2a4b41fa4
--- /dev/null
+++ b/test/web/mastodon_api/conversation_view_test.exs
@@ -0,0 +1,40 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
+  use Pleroma.DataCase
+
+  alias Pleroma.Web.CommonAPI
+  alias Pleroma.Conversation.Participation
+  alias Pleroma.Web.MastodonAPI.ConversationView
+
+  import Pleroma.Factory
+
+  test "represents a Mastodon Conversation entity" do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, activity} =
+      CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"})
+
+    [participation] = Participation.for_user_with_last_activity_id(user)
+
+    assert participation
+
+    conversation =
+      ConversationView.render("participation.json", %{participation: participation, user: user})
+
+    assert conversation.id == participation.id |> to_string()
+    assert conversation.last_status.id == activity.id
+
+    assert [account] = conversation.accounts
+    assert account.id == other_user.id
+
+    assert recipients = conversation.pleroma.recipients
+    recipient_ids = recipients |> Enum.map(& &1.id)
+
+    assert user.id in recipient_ids
+    assert other_user.id in recipient_ids
+  end
+end
diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs
index 43104e36e..7989defe0 100644
--- a/test/web/pleroma_api/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/pleroma_api_controller_test.exs
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
 
   alias Pleroma.Conversation.Participation
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Repo
 
   import Pleroma.Factory
 
@@ -42,4 +43,34 @@ test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
     id_two = activity_two.id
     assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result
   end
+
+  test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"})
+
+    [participation] = Participation.for_user(user)
+
+    participation = Repo.preload(participation, :recipients)
+
+    assert [user] == participation.recipients
+    assert other_user not in participation.recipients
+
+    result =
+      conn
+      |> assign(:user, user)
+      |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
+        "recipients" => [user.id, other_user.id]
+      })
+      |> json_response(200)
+
+    assert result["id"] == participation.id |> to_string
+
+    assert recipients = result["pleroma"]["recipients"]
+    recipient_ids = Enum.map(recipients, & &1["id"])
+
+    assert user.id in recipient_ids
+    assert other_user.id in recipient_ids
+  end
 end

From b64b6fee2a78fbfbc557b89550128494ca7d2894 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 5 Aug 2019 15:33:22 +0200
Subject: [PATCH 07/40] CommonAPI: Replies to conversations also get the
 correct context id.

---
 lib/pleroma/web/common_api/common_api.ex |  2 +-
 lib/pleroma/web/common_api/utils.ex      |  8 ++++++--
 test/web/common_api/common_api_test.exs  | 15 +++++++++++++++
 3 files changed, 22 insertions(+), 3 deletions(-)

diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 86e95cd0f..72da46263 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -223,7 +223,7 @@ def post(user, %{"status" => status} = data) do
          {poll, poll_emoji} <- make_poll_data(data),
          {to, cc} <-
            get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
-         context <- make_context(in_reply_to),
+         context <- make_context(in_reply_to, in_reply_to_conversation),
          cw <- data["spoiler_text"] || "",
          sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
          full_payload <- String.trim(status <> cw),
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index e70ba7d43..425b6d656 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -244,8 +244,12 @@ defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
 
   defp maybe_add_nsfw_tag(data, _), do: data
 
-  def make_context(%Activity{data: %{"context" => context}}), do: context
-  def make_context(_), do: Utils.generate_context_id()
+  def make_context(_, %Participation{} = participation) do
+    Repo.preload(participation, :conversation).conversation.ap_id
+  end
+
+  def make_context(%Activity{data: %{"context" => context}}, _), do: context
+  def make_context(_, _), do: Utils.generate_context_id()
 
   def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
 
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index e2a5bf117..454523349 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -9,10 +9,25 @@ defmodule Pleroma.Web.CommonAPITest do
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.Visibility
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 
+  test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
+    user = insert(:user)
+    {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+
+    [participation] = Participation.for_user(user)
+
+    {:ok, convo_reply} =
+      CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id})
+
+    assert Visibility.is_direct?(convo_reply)
+
+    assert activity.data["context"] == convo_reply.data["context"]
+  end
+
   test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do
     har = insert(:user)
     jafnhar = insert(:user)

From ddabe7784b47939120a838b9c65381fd2262c161 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 5 Aug 2019 15:58:14 +0200
Subject: [PATCH 08/40] Pleroma Conversations: Document differences.

---
 docs/api/differences_in_mastoapi_responses.md |  8 +++++
 docs/api/pleroma_api.md                       | 29 +++++++++++++++++++
 2 files changed, 37 insertions(+)

diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md
index 1907d70c8..79ca531b8 100644
--- a/docs/api/differences_in_mastoapi_responses.md
+++ b/docs/api/differences_in_mastoapi_responses.md
@@ -59,12 +59,19 @@ Has these additional fields under the `pleroma` object:
 - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
 - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
 
+## Conversations
+
+Has an additional field under the `pleroma` object:
+
+- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation.
+
 ## Account Search
 
 Behavior has changed:
 
 - `/api/v1/accounts/search`: Does not require authentication
 
+
 ## Notifications
 
 Has these additional fields under the `pleroma` object:
@@ -79,6 +86,7 @@ Additional parameters can be added to the JSON body/Form data:
 - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
 - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
 - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
+- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
 
 ## PATCH `/api/v1/update_credentials`
 
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 5698e88ac..4323e59f0 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -319,3 +319,32 @@ See [Admin-API](Admin-API.md)
   "healthy": true # Instance state
 }
 ```
+
+# Pleroma Conversations
+
+Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints:
+
+1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user. 
+2. Pleroma Conversations statuses can be requested by Conversation id.
+3. Pleroma Conversations can be replied to.
+
+Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation.
+
+The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation.
+
+
+## `GET /api/v1/pleroma/conversations/:id/statuses`
+### Timeline for a given conversation
+* Method `GET`
+* Authentication: required
+* Params: Like other timelines
+* Response: JSON, statuses (200 - healthy, 503 unhealthy).
+
+
+## `PATCH /api/v1/pleroma/conversations/:id`
+### Update a conversation. Used to change the set of recipients.
+* Method `PATCH`
+* Authentication: required
+* Params:
+    * `recipients`: A list of ids of users that should receive posts to this conversation.
+* Response: JSON, statuses (200 - healthy, 503 unhealthy)

From d6fe220e32d581220cc33f4f44d6517a401eabbf Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 5 Aug 2019 16:11:23 +0200
Subject: [PATCH 09/40] Linting.

---
 lib/pleroma/conversation/participation_recipient_ship.ex | 2 +-
 lib/pleroma/web/pleroma_api/pleroma_api_controller.ex    | 6 +++---
 test/web/mastodon_api/conversation_view_test.exs         | 2 +-
 test/web/pleroma_api/pleroma_api_controller_test.exs     | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex
index 27c0c89cd..932cbd04c 100644
--- a/lib/pleroma/conversation/participation_recipient_ship.ex
+++ b/lib/pleroma/conversation/participation_recipient_ship.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Conversation.Participation.RecipientShip do
   use Ecto.Schema
 
   alias Pleroma.Conversation.Participation
-  alias Pleroma.User
   alias Pleroma.Repo
+  alias Pleroma.User
 
   import Ecto.Changeset
 
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index 759d8aef0..018564452 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -8,10 +8,10 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
 
   alias Pleroma.Conversation.Participation
-  alias Pleroma.Web.ActivityPub.ActivityPub
-  alias Pleroma.Web.MastodonAPI.StatusView
-  alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Repo
+  alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.MastodonAPI.ConversationView
+  alias Pleroma.Web.MastodonAPI.StatusView
 
   def conversation_statuses(
         %{assigns: %{user: user}} = conn,
diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs
index 2a4b41fa4..e32cde5a8 100644
--- a/test/web/mastodon_api/conversation_view_test.exs
+++ b/test/web/mastodon_api/conversation_view_test.exs
@@ -5,8 +5,8 @@
 defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do
   use Pleroma.DataCase
 
-  alias Pleroma.Web.CommonAPI
   alias Pleroma.Conversation.Participation
+  alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.ConversationView
 
   import Pleroma.Factory
diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs
index 7989defe0..7c75fb229 100644
--- a/test/web/pleroma_api/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/pleroma_api_controller_test.exs
@@ -6,8 +6,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
   use Pleroma.Web.ConnCase
 
   alias Pleroma.Conversation.Participation
-  alias Pleroma.Web.CommonAPI
   alias Pleroma.Repo
+  alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
 

From a49c92f6ae2dc68a80345cff4793820a75835eb1 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 6 Aug 2019 14:51:17 +0200
Subject: [PATCH 10/40] Participation: Setting recipients will always add the
 owner.

---
 docs/api/pleroma_api.md                   |  2 +-
 lib/pleroma/conversation/participation.ex |  6 ++++++
 test/conversation/participation_test.exs  | 19 +++++++++++++++++++
 3 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 4323e59f0..590f2a3fb 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -346,5 +346,5 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
 * Method `PATCH`
 * Authentication: required
 * Params:
-    * `recipients`: A list of ids of users that should receive posts to this conversation.
+    * `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)
diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index acdee5517..d17b6f7c5 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -101,6 +101,10 @@ def get(id) do
   end
 
   def set_recipients(participation, user_ids) do
+    user_ids =
+      [participation.user_id | user_ids]
+      |> Enum.uniq()
+
     Repo.transaction(fn ->
       query =
         from(r in RecipientShip,
@@ -118,5 +122,7 @@ def set_recipients(participation, user_ids) do
       RecipientShip.create(users, participation)
       :ok
     end)
+
+    {:ok, Repo.preload(participation, :recipients, force: true)}
   end
 end
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index 4a3c397bd..7958e8e89 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -132,4 +132,23 @@ test "Doesn't die when the conversation gets empty" do
 
     [] = Participation.for_user_with_last_activity_id(user)
   end
+
+  test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
+    [participation] = Participation.for_user_with_last_activity_id(user)
+
+    participation = Repo.preload(participation, :recipients)
+
+    assert participation.recipients |> length() == 1
+    assert user in participation.recipients
+
+    {:ok, participation} = Participation.set_recipients(participation, [other_user.id])
+
+    assert participation.recipients |> length() == 2
+    assert user in participation.recipients
+    assert other_user in participation.recipients
+  end
 end

From e4a01d253ef7ab09d028198e5e39b9aba357486c Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Tue, 6 Aug 2019 15:06:19 +0200
Subject: [PATCH 11/40] Conversation: Rename function to better express what it
 does.

---
 lib/pleroma/conversation.ex | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex
index fb0dfedca..be5821ad7 100644
--- a/lib/pleroma/conversation.ex
+++ b/lib/pleroma/conversation.ex
@@ -40,7 +40,7 @@ def get_for_ap_id(ap_id) do
     Repo.get_by(__MODULE__, ap_id: ap_id)
   end
 
-  def maybe_set_recipients(participation, activity) do
+  def maybe_create_recipientships(participation, activity) do
     participation = Repo.preload(participation, :recipients)
 
     if participation.recipients |> Enum.empty?() do
@@ -70,7 +70,7 @@ def create_or_bump_for(activity, opts \\ []) do
           {:ok, participation} =
             Participation.create_for_user_and_conversation(user, conversation, opts)
 
-          maybe_set_recipients(participation, activity)
+          maybe_create_recipientships(participation, activity)
           participation
         end)
 

From 23c46f7e72701b773d87b825526450e5f4ec6322 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 12 Aug 2019 12:51:08 +0200
Subject: [PATCH 12/40] Conversations: Use 'recipients' for accounts in
 conversation view.

According to gargron, this is the intended usage.
---
 .../web/mastodon_api/views/conversation_view.ex   | 15 +++------------
 test/web/mastodon_api/conversation_view_test.exs  |  6 ------
 .../pleroma_api/pleroma_api_controller_test.exs   |  8 ++++----
 3 files changed, 7 insertions(+), 22 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 5adaecdb0..4a81f0248 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
   alias Pleroma.Web.MastodonAPI.StatusView
 
   def render("participation.json", %{participation: participation, user: user}) do
-    participation = Repo.preload(participation, conversation: :users, recipients: [])
+    participation = Repo.preload(participation, conversation: [], recipients: [])
 
     last_activity_id =
       with nil <- participation.last_activity_id do
@@ -28,7 +28,7 @@ def render("participation.json", %{participation: participation, user: user}) do
 
     # Conversations return all users except the current user.
     users =
-      participation.conversation.users
+      participation.recipients
       |> Enum.reject(&(&1.id == user.id))
 
     accounts =
@@ -37,20 +37,11 @@ def render("participation.json", %{participation: participation, user: user}) do
         as: :user
       })
 
-    recipients =
-      AccountView.render("accounts.json", %{
-        users: participation.recipients,
-        as: :user
-      })
-
     %{
       id: participation.id |> to_string(),
       accounts: accounts,
       unread: !participation.read,
-      last_status: last_status,
-      pleroma: %{
-        recipients: recipients
-      }
+      last_status: last_status
     }
   end
 end
diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs
index e32cde5a8..27f668d9f 100644
--- a/test/web/mastodon_api/conversation_view_test.exs
+++ b/test/web/mastodon_api/conversation_view_test.exs
@@ -30,11 +30,5 @@ test "represents a Mastodon Conversation entity" do
 
     assert [account] = conversation.accounts
     assert account.id == other_user.id
-
-    assert recipients = conversation.pleroma.recipients
-    recipient_ids = recipients |> Enum.map(& &1.id)
-
-    assert user.id in recipient_ids
-    assert other_user.id in recipient_ids
   end
 end
diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs
index 7c75fb229..56bc1572c 100644
--- a/test/web/pleroma_api/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/pleroma_api_controller_test.exs
@@ -67,10 +67,10 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do
 
     assert result["id"] == participation.id |> to_string
 
-    assert recipients = result["pleroma"]["recipients"]
-    recipient_ids = Enum.map(recipients, & &1["id"])
+    [participation] = Participation.for_user(user)
+    participation = Repo.preload(participation, :recipients)
 
-    assert user.id in recipient_ids
-    assert other_user.id in recipient_ids
+    assert user in participation.recipients
+    assert other_user in participation.recipients
   end
 end

From 60231ec7bd0af993dc19f69a57b261b3b4167636 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 12 Aug 2019 13:58:04 +0200
Subject: [PATCH 13/40] Conversation: Add endpoint to get a conversation by id.

---
 docs/api/pleroma_api.md                        |  6 ++++++
 .../web/pleroma_api/pleroma_api_controller.ex  |  9 +++++++++
 lib/pleroma/web/router.ex                      |  1 +
 .../pleroma_api_controller_test.exs            | 18 ++++++++++++++++++
 4 files changed, 34 insertions(+)

diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 590f2a3fb..b134b31a8 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -340,6 +340,12 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
 * Params: Like other timelines
 * Response: JSON, statuses (200 - healthy, 503 unhealthy).
 
+## `GET /api/v1/pleroma/conversations/:id`
+### The conversation with the given ID.
+* Method `GET`
+* Authentication: required
+* Params: None
+* Response: JSON, statuses (200 - healthy, 503 unhealthy).
 
 ## `PATCH /api/v1/pleroma/conversations/:id`
 ### Update a conversation. Used to change the set of recipients.
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index 018564452..3175a99b1 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -13,6 +13,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Web.MastodonAPI.StatusView
 
+  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, user: user})
+    end
+  end
+
   def conversation_statuses(
         %{assigns: %{user: user}} = conn,
         %{"id" => participation_id} = params
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index c835f06b4..f0b6a02e9 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -265,6 +265,7 @@ defmodule Pleroma.Web.Router do
     scope [] do
       pipe_through(:oauth_write)
       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
+      get("/conversations/:id", PleromaAPIController, :conversation)
       patch("/conversations/:id", PleromaAPIController, :update_conversation)
     end
   end
diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs
index 56bc1572c..ed6b79727 100644
--- a/test/web/pleroma_api/pleroma_api_controller_test.exs
+++ b/test/web/pleroma_api/pleroma_api_controller_test.exs
@@ -11,6 +11,24 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
 
   import Pleroma.Factory
 
+  test "/api/v1/pleroma/conversations/:id", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"})
+
+    [participation] = Participation.for_user(other_user)
+
+    result =
+      conn
+      |> assign(:user, other_user)
+      |> get("/api/v1/pleroma/conversations/#{participation.id}")
+      |> json_response(200)
+
+    assert result["id"] == participation.id |> to_string()
+  end
+
   test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do
     user = insert(:user)
     other_user = insert(:user)

From 511ccea5aa36b4b0098e49b409b335b0ce8f042e Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 12 Aug 2019 14:23:06 +0200
Subject: [PATCH 14/40] ConversationView: Align parameter names with other
 views.

---
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 ++--
 lib/pleroma/web/mastodon_api/views/conversation_view.ex | 2 +-
 lib/pleroma/web/pleroma_api/pleroma_api_controller.ex   | 4 ++--
 lib/pleroma/web/streamer.ex                             | 2 +-
 test/web/mastodon_api/conversation_view_test.exs        | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 0deeab2be..eb2351eb7 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -1743,7 +1743,7 @@ def conversations(%{assigns: %{user: user}} = conn, params) do
 
     conversations =
       Enum.map(participations, fn participation ->
-        ConversationView.render("participation.json", %{participation: participation, user: user})
+        ConversationView.render("participation.json", %{participation: participation, for: user})
       end)
 
     conn
@@ -1756,7 +1756,7 @@ def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_
            Repo.get_by(Participation, id: participation_id, user_id: user.id),
          {:ok, participation} <- Participation.mark_as_read(participation) do
       participation_view =
-        ConversationView.render("participation.json", %{participation: participation, user: user})
+        ConversationView.render("participation.json", %{participation: participation, for: user})
 
       conn
       |> json(participation_view)
diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
index 4a81f0248..40acc07b3 100644
--- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.StatusView
 
-  def render("participation.json", %{participation: participation, user: user}) do
+  def render("participation.json", %{participation: participation, for: user}) do
     participation = Repo.preload(participation, conversation: [], recipients: [])
 
     last_activity_id =
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index 3175a99b1..b5c3d2728 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -18,7 +18,7 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id})
          true <- user.id == participation.user_id do
       conn
       |> put_view(ConversationView)
-      |> render("participation.json", %{participation: participation, user: user})
+      |> render("participation.json", %{participation: participation, for: user})
     end
   end
 
@@ -69,7 +69,7 @@ def update_conversation(
          {:ok, _} <- Participation.set_recipients(participation, recipients) do
       conn
       |> put_view(ConversationView)
-      |> render("participation.json", %{participation: participation, user: user})
+      |> render("participation.json", %{participation: participation, for: user})
     end
   end
 end
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 9ee331030..a0bb10895 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -209,7 +209,7 @@ def represent_conversation(%Participation{} = participation) do
       payload:
         Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{
           participation: participation,
-          user: participation.user
+          for: participation.user
         })
         |> Jason.encode!()
     }
diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs
index 27f668d9f..a2a880705 100644
--- a/test/web/mastodon_api/conversation_view_test.exs
+++ b/test/web/mastodon_api/conversation_view_test.exs
@@ -23,7 +23,7 @@ test "represents a Mastodon Conversation entity" do
     assert participation
 
     conversation =
-      ConversationView.render("participation.json", %{participation: participation, user: user})
+      ConversationView.render("participation.json", %{participation: participation, for: user})
 
     assert conversation.id == participation.id |> to_string()
     assert conversation.last_status.id == activity.id

From 2674db14a2ee29e98265c0c0b1db412835b6bbed Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Mon, 12 Aug 2019 14:26:18 +0200
Subject: [PATCH 15/40] Modify Changelog.

---
 CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 069974e44..f8c90a73b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Report email not being sent to admins when the reporter is a remote user
 
 ### Added
+- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
 - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
 - MRF: Support for excluding specific domains from Transparency.
 - MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)

From df81abb68c46e36dfb2af7aab77e0e57a1a1eb28 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 14 Aug 2019 15:55:43 +0200
Subject: [PATCH 16/40] Conversations: Use correct oauth paths for extended
 api.

---
 lib/pleroma/web/router.ex | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 9759268f9..1eb6f7b9d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -263,9 +263,13 @@ defmodule Pleroma.Web.Router do
     pipe_through(:authenticated_api)
 
     scope [] do
-      pipe_through(:oauth_write)
+      pipe_through(:oauth_read)
       get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
       get("/conversations/:id", PleromaAPIController, :conversation)
+    end
+
+    scope [] do
+      pipe_through(:oauth_write)
       patch("/conversations/:id", PleromaAPIController, :update_conversation)
     end
   end

From f73212b2a36deef631716f3c8a80d7da11cec759 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 14 Aug 2019 15:56:15 +0200
Subject: [PATCH 17/40] Conversation: Render new participation on update.

---
 lib/pleroma/web/pleroma_api/pleroma_api_controller.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index b5c3d2728..6d74d418e 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -66,7 +66,7 @@ def update_conversation(
       |> Participation.get()
 
     with true <- user.id == participation.user_id,
-         {:ok, _} <- Participation.set_recipients(participation, recipients) do
+         {:ok, participation} <- Participation.set_recipients(participation, recipients) do
       conn
       |> put_view(ConversationView)
       |> render("participation.json", %{participation: participation, for: user})

From d3af9e19edb32e04d101e50ae2868ba6f66cbed9 Mon Sep 17 00:00:00 2001
From: lain <lain@soykaf.club>
Date: Wed, 14 Aug 2019 17:01:11 +0200
Subject: [PATCH 18/40] Conversations: Load relations in one query.

---
 lib/pleroma/conversation/participation.ex        | 16 +++++++++++++---
 .../web/pleroma_api/pleroma_api_controller.ex    |  4 +---
 test/conversation/participation_test.exs         | 14 ++++++++++++++
 3 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex
index d17b6f7c5..ea5b9fe17 100644
--- a/lib/pleroma/conversation/participation.ex
+++ b/lib/pleroma/conversation/participation.ex
@@ -94,10 +94,20 @@ def for_user_with_last_activity_id(user, params \\ %{}) do
     |> Enum.filter(& &1.last_activity_id)
   end
 
-  def get(nil), do: nil
+  def get(_, _ \\ [])
+  def get(nil, _), do: nil
 
-  def get(id) do
-    Repo.get(__MODULE__, id)
+  def get(id, params) do
+    query =
+      if preload = params[:preload] do
+        from(p in __MODULE__,
+          preload: ^preload
+        )
+      else
+        __MODULE__
+      end
+
+    Repo.get(query, id)
   end
 
   def set_recipients(participation, user_ids) do
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
index 6d74d418e..b6d2bf86b 100644
--- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
   import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7]
 
   alias Pleroma.Conversation.Participation
-  alias Pleroma.Repo
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.MastodonAPI.ConversationView
   alias Pleroma.Web.MastodonAPI.StatusView
@@ -34,8 +33,7 @@ def conversation_statuses(
 
     participation =
       participation_id
-      |> Participation.get()
-      |> Repo.preload(:conversation)
+      |> Participation.get(preload: [:conversation])
 
     if user.id == participation.user_id do
       activities =
diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs
index 7958e8e89..a27167d42 100644
--- a/test/conversation/participation_test.exs
+++ b/test/conversation/participation_test.exs
@@ -8,6 +8,20 @@ defmodule Pleroma.Conversation.ParticipationTest do
   alias Pleroma.Conversation.Participation
   alias Pleroma.Web.CommonAPI
 
+  test "getting a participation will also preload things" do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, _activity} =
+      CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
+
+    [participation] = Participation.for_user(user)
+
+    participation = Participation.get(participation.id, preload: [:conversation])
+
+    assert %Pleroma.Conversation{} = participation.conversation
+  end
+
   test "for a new conversation, it sets the recipents of the participation" do
     user = insert(:user)
     other_user = insert(:user)

From 51bdf0cab6dc96bfd48a6d98d9f21584b42c0e44 Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Wed, 14 Aug 2019 11:55:17 -0400
Subject: [PATCH 19/40] use default child_specs

---
 lib/pleroma/application.ex                  | 185 ++++++--------------
 lib/pleroma/captcha/captcha.ex              |   2 +-
 lib/pleroma/config/transfer_task.ex         |   2 +-
 lib/pleroma/emoji.ex                        |   2 +-
 lib/pleroma/flake_id.ex                     |   2 +-
 lib/pleroma/gopher/server.ex                |   2 +-
 lib/pleroma/scheduled_activity_worker.ex    |   2 +-
 lib/pleroma/stats.ex                        |   4 +-
 lib/pleroma/web/chat_channel.ex             |   4 +-
 lib/pleroma/web/federator/retry_queue.ex    |   2 +-
 lib/pleroma/web/oauth/token/clean_worker.ex |   3 +-
 lib/pleroma/web/streamer.ex                 |   2 +-
 test/config/transfer_task_test.exs          |   4 +-
 13 files changed, 73 insertions(+), 143 deletions(-)

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 00b06f723..3bb0718e4 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -3,11 +3,14 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Pleroma.Application do
+  import Cachex.Spec
   use Application
 
   @name Mix.Project.config()[:name]
   @version Mix.Project.config()[:version]
   @repository Mix.Project.config()[:source_url]
+  @env Mix.env()
+
   def name, do: @name
   def version, do: @version
   def named_version, do: @name <> " " <> @version
@@ -21,116 +24,25 @@ def user_agent do
   # See http://elixir-lang.org/docs/stable/elixir/Application.html
   # for more information on OTP Applications
   def start(_type, _args) do
-    import Cachex.Spec
-
     Pleroma.Config.DeprecationWarnings.warn()
     setup_instrumenters()
 
     # Define workers and child supervisors to be supervised
     children =
       [
-        # Start the Ecto repository
-        %{id: Pleroma.Repo, start: {Pleroma.Repo, :start_link, []}, type: :supervisor},
-        %{id: Pleroma.Config.TransferTask, start: {Pleroma.Config.TransferTask, :start_link, []}},
-        %{id: Pleroma.Emoji, start: {Pleroma.Emoji, :start_link, []}},
-        %{id: Pleroma.Captcha, start: {Pleroma.Captcha, :start_link, []}},
-        %{
-          id: :cachex_used_captcha_cache,
-          start:
-            {Cachex, :start_link,
-             [
-               :used_captcha_cache,
-               [
-                 ttl_interval:
-                   :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
-               ]
-             ]}
-        },
-        %{
-          id: :cachex_user,
-          start:
-            {Cachex, :start_link,
-             [
-               :user_cache,
-               [
-                 default_ttl: 25_000,
-                 ttl_interval: 1000,
-                 limit: 2500
-               ]
-             ]}
-        },
-        %{
-          id: :cachex_object,
-          start:
-            {Cachex, :start_link,
-             [
-               :object_cache,
-               [
-                 default_ttl: 25_000,
-                 ttl_interval: 1000,
-                 limit: 2500
-               ]
-             ]}
-        },
-        %{
-          id: :cachex_rich_media,
-          start:
-            {Cachex, :start_link,
-             [
-               :rich_media_cache,
-               [
-                 default_ttl: :timer.minutes(120),
-                 limit: 5000
-               ]
-             ]}
-        },
-        %{
-          id: :cachex_scrubber,
-          start:
-            {Cachex, :start_link,
-             [
-               :scrubber_cache,
-               [
-                 limit: 2500
-               ]
-             ]}
-        },
-        %{
-          id: :cachex_idem,
-          start:
-            {Cachex, :start_link,
-             [
-               :idempotency_cache,
-               [
-                 expiration:
-                   expiration(
-                     default: :timer.seconds(6 * 60 * 60),
-                     interval: :timer.seconds(60)
-                   ),
-                 limit: 2500
-               ]
-             ]}
-        },
-        %{id: Pleroma.FlakeId, start: {Pleroma.FlakeId, :start_link, []}},
-        %{
-          id: Pleroma.ScheduledActivityWorker,
-          start: {Pleroma.ScheduledActivityWorker, :start_link, []}
-        }
+        Pleroma.Repo,
+        Pleroma.Config.TransferTask,
+        Pleroma.Emoji,
+        Pleroma.Captcha,
+        Pleroma.FlakeId,
+        Pleroma.ScheduledActivityWorker
       ] ++
+        cachex_children() ++
         hackney_pool_children() ++
         [
-          %{
-            id: Pleroma.Web.Federator.RetryQueue,
-            start: {Pleroma.Web.Federator.RetryQueue, :start_link, []}
-          },
-          %{
-            id: Pleroma.Web.OAuth.Token.CleanWorker,
-            start: {Pleroma.Web.OAuth.Token.CleanWorker, :start_link, []}
-          },
-          %{
-            id: Pleroma.Stats,
-            start: {Pleroma.Stats, :start_link, []}
-          },
+          Pleroma.Web.Federator.RetryQueue,
+          Pleroma.Web.OAuth.Token.CleanWorker,
+          Pleroma.Stats,
           %{
             id: :web_push_init,
             start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
@@ -147,16 +59,11 @@ def start(_type, _args) do
             restart: :temporary
           }
         ] ++
-        streamer_child() ++
-        chat_child() ++
+        streamer_child(@env) ++
+        chat_child(@env, chat_enabled?()) ++
         [
-          # Start the endpoint when the application starts
-          %{
-            id: Pleroma.Web.Endpoint,
-            start: {Pleroma.Web.Endpoint, :start_link, []},
-            type: :supervisor
-          },
-          %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}
+          Pleroma.Web.Endpoint,
+          Pleroma.Gopher.Server
         ]
 
     # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
@@ -201,28 +108,46 @@ def enabled_hackney_pools do
       end
   end
 
-  if Pleroma.Config.get(:env) == :test do
-    defp streamer_child, do: []
-    defp chat_child, do: []
-  else
-    defp streamer_child do
-      [%{id: Pleroma.Web.Streamer, start: {Pleroma.Web.Streamer, :start_link, []}}]
-    end
-
-    defp chat_child do
-      if Pleroma.Config.get([:chat, :enabled]) do
-        [
-          %{
-            id: Pleroma.Web.ChatChannel.ChatChannelState,
-            start: {Pleroma.Web.ChatChannel.ChatChannelState, :start_link, []}
-          }
-        ]
-      else
-        []
-      end
-    end
+  defp cachex_children do
+    [
+      build_cachex("used_captcha", ttl_interval: seconds_valid_interval()),
+      build_cachex("user", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
+      build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
+      build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
+      build_cachex("scrubber", limit: 2500),
+      build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500)
+    ]
   end
 
+  defp idempotency_expiration,
+       do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
+
+  defp seconds_valid_interval,
+       do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
+
+  defp build_cachex(type, opts),
+       do: %{
+         id: String.to_atom("cachex_" <> type),
+         start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
+         type: :worker
+       }
+
+  defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
+
+  defp streamer_child(:test), do: []
+
+  defp streamer_child(_) do
+    [Pleroma.Web.Streamer]
+  end
+
+  defp chat_child(:test, _), do: []
+
+  defp chat_child(_env, true) do
+    [Pleroma.Web.ChatChannel.ChatChannelState]
+  end
+
+  defp chat_child(_, _), do: []
+
   defp hackney_pool_children do
     for pool <- enabled_hackney_pools() do
       options = Pleroma.Config.get([:hackney_pools, pool])
diff --git a/lib/pleroma/captcha/captcha.ex b/lib/pleroma/captcha/captcha.ex
index a73b87251..c2765a5b8 100644
--- a/lib/pleroma/captcha/captcha.ex
+++ b/lib/pleroma/captcha/captcha.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Captcha do
   use GenServer
 
   @doc false
-  def start_link do
+  def start_link(_) do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
index 7799b2a78..3214c9951 100644
--- a/lib/pleroma/config/transfer_task.ex
+++ b/lib/pleroma/config/transfer_task.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Config.TransferTask do
   use Task
   alias Pleroma.Web.AdminAPI.Config
 
-  def start_link do
+  def start_link(_) do
     load_and_update_env()
     if Pleroma.Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Pleroma.Repo)
     :ignore
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 052501642..66e20f0e4 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Emoji do
   @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
 
   @doc false
-  def start_link do
+  def start_link(_) do
     GenServer.start_link(__MODULE__, [], name: __MODULE__)
   end
 
diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex
index ca0610abc..47d61ca5f 100644
--- a/lib/pleroma/flake_id.ex
+++ b/lib/pleroma/flake_id.ex
@@ -98,7 +98,7 @@ def dump(value) do
   def autogenerate, do: get()
 
   # -- GenServer API
-  def start_link do
+  def start_link(_) do
     :gen_server.start_link({:local, :flake}, __MODULE__, [], [])
   end
 
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index b3319e137..d4e4f3e55 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Gopher.Server do
   use GenServer
   require Logger
 
-  def start_link do
+  def start_link(_) do
     config = Pleroma.Config.get(:gopher, [])
     ip = Keyword.get(config, :ip, {0, 0, 0, 0})
     port = Keyword.get(config, :port, 1234)
diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex
index 65b38622f..8578cab5e 100644
--- a/lib/pleroma/scheduled_activity_worker.ex
+++ b/lib/pleroma/scheduled_activity_worker.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.ScheduledActivityWorker do
 
   @schedule_interval :timer.minutes(1)
 
-  def start_link do
+  def start_link(_) do
     GenServer.start_link(__MODULE__, nil)
   end
 
diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 5b242927b..101effbe4 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -7,7 +7,9 @@ defmodule Pleroma.Stats do
   alias Pleroma.Repo
   alias Pleroma.User
 
-  def start_link do
+  use Agent
+
+  def start_link(_) do
     agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
     spawn(fn -> schedule_update() end)
     agent
diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex
index f63f4bda1..b543909f1 100644
--- a/lib/pleroma/web/chat_channel.ex
+++ b/lib/pleroma/web/chat_channel.ex
@@ -33,9 +33,11 @@ def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}}
 end
 
 defmodule Pleroma.Web.ChatChannel.ChatChannelState do
+  use Agent
+
   @max_messages 20
 
-  def start_link do
+  def start_link(_) do
     Agent.start_link(fn -> %{max_id: 1, messages: []} end, name: __MODULE__)
   end
 
diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex
index 3db948c2e..9eab8c218 100644
--- a/lib/pleroma/web/federator/retry_queue.ex
+++ b/lib/pleroma/web/federator/retry_queue.ex
@@ -13,7 +13,7 @@ def init(args) do
     {:ok, %{args | queue_table: queue_table, running_jobs: :sets.new()}}
   end
 
-  def start_link do
+  def start_link(_) do
     enabled =
       if Pleroma.Config.get(:env) == :test,
         do: true,
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
index dca852449..e39a4986a 100644
--- a/lib/pleroma/web/oauth/token/clean_worker.ex
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do
   @moduledoc """
   The module represents functions to clean an expired oauth tokens.
   """
+  use GenServer
 
   # 10 seconds
   @start_interval 10_000
@@ -18,7 +19,7 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do
 
   alias Pleroma.Web.OAuth.Token
 
-  def start_link, do: GenServer.start_link(__MODULE__, nil)
+  def start_link(_), do: GenServer.start_link(__MODULE__, nil)
 
   def init(_) do
     if Pleroma.Config.get([:oauth2, :clean_expired_tokens], false) do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 9ee331030..e66378ceb 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -18,7 +18,7 @@ defmodule Pleroma.Web.Streamer do
 
   @keepalive_interval :timer.seconds(30)
 
-  def start_link do
+  def start_link(_) do
     GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
   end
 
diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index dbeadbe87..4455a4d47 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -31,7 +31,7 @@ test "transfer config values from db to env" do
       value: [live: 15, com: 35]
     })
 
-    Pleroma.Config.TransferTask.start_link()
+    Pleroma.Config.TransferTask.start_link([])
 
     assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
     assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
@@ -50,7 +50,7 @@ test "non existing atom" do
     })
 
     assert ExUnit.CaptureLog.capture_log(fn ->
-             Pleroma.Config.TransferTask.start_link()
+             Pleroma.Config.TransferTask.start_link([])
            end) =~
              "updating env causes error, key: \"undefined_atom_key\", error: %ArgumentError{message: \"argument error\"}"
   end

From 15ef521009f4c232f417ca9164c6be3f4ee4e018 Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Wed, 14 Aug 2019 11:57:50 -0400
Subject: [PATCH 20/40] Isolate OAuth.Token.CleanWorker

---
 lib/pleroma/application.ex                  |  9 ++++++++
 lib/pleroma/web/oauth/token/clean_worker.ex | 25 ++++++++-------------
 2 files changed, 18 insertions(+), 16 deletions(-)

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 3bb0718e4..c460a3bc5 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -59,6 +59,7 @@ def start(_type, _args) do
             restart: :temporary
           }
         ] ++
+        oauth_cleanup_child(oauth_cleanup_enabled?()) ++
         streamer_child(@env) ++
         chat_child(@env, chat_enabled?()) ++
         [
@@ -134,12 +135,20 @@ defp build_cachex(type, opts),
 
   defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
 
+  defp oauth_cleanup_enabled?,
+    do: Pleroma.Config.get([:oauth2, :clean_expired_tokens], false)
+
   defp streamer_child(:test), do: []
 
   defp streamer_child(_) do
     [Pleroma.Web.Streamer]
   end
 
+  defp oauth_cleanup_child(true),
+       do: [Pleroma.Web.OAuth.Token.CleanWorker]
+
+  defp oauth_cleanup_child(_), do: []
+
   defp chat_child(:test, _), do: []
 
   defp chat_child(_env, true) do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
index e39a4986a..f50098302 100644
--- a/lib/pleroma/web/oauth/token/clean_worker.ex
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -8,35 +8,28 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do
   """
   use GenServer
 
-  # 10 seconds
-  @start_interval 10_000
+  @ten_seconds 10_000
+  @one_day 86_400_000
+
   @interval Pleroma.Config.get(
-              # 24 hours
               [:oauth2, :clean_expired_tokens_interval],
-              86_400_000
+              @one_day
             )
-  @queue :background
 
   alias Pleroma.Web.OAuth.Token
 
-  def start_link(_), do: GenServer.start_link(__MODULE__, nil)
+  def start_link(_), do: GenServer.start_link(__MODULE__, %{})
 
   def init(_) do
-    if Pleroma.Config.get([:oauth2, :clean_expired_tokens], false) do
-      Process.send_after(self(), :perform, @start_interval)
-      {:ok, nil}
-    else
-      :ignore
-    end
+    Process.send_after(self(), :perform, @ten_seconds)
+    {:ok, nil}
   end
 
   @doc false
   def handle_info(:perform, state) do
+    Token.delete_expired_tokens()
+
     Process.send_after(self(), :perform, @interval)
-    PleromaJobQueue.enqueue(@queue, __MODULE__, [:clean])
     {:noreply, state}
   end
-
-  # Job Worker Callbacks
-  def perform(:clean), do: Token.delete_expired_tokens()
 end

From 574856ef01fae7ea411ec363929ab9a22d76a65d Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Wed, 14 Aug 2019 11:58:32 -0400
Subject: [PATCH 21/40] streamline Streamer pings

---
 lib/pleroma/web/streamer.ex | 17 +++++------------
 1 file changed, 5 insertions(+), 12 deletions(-)

diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index e66378ceb..bbaddd852 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -35,28 +35,21 @@ def stream(topic, item) do
   end
 
   def init(args) do
-    spawn(fn ->
-      # 30 seconds
-      Process.sleep(@keepalive_interval)
-      GenServer.cast(__MODULE__, %{action: :ping})
-    end)
+    Process.send_after(self(), %{action: :ping}, @keepalive_interval)
 
     {:ok, args}
   end
 
-  def handle_cast(%{action: :ping}, topics) do
-    Map.values(topics)
+  def handle_info(%{action: :ping}, topics) do
+    topics
+    |> Map.values()
     |> List.flatten()
     |> Enum.each(fn socket ->
       Logger.debug("Sending keepalive ping")
       send(socket.transport_pid, {:text, ""})
     end)
 
-    spawn(fn ->
-      # 30 seconds
-      Process.sleep(@keepalive_interval)
-      GenServer.cast(__MODULE__, %{action: :ping})
-    end)
+    Process.send_after(self(), %{action: :ping}, @keepalive_interval)
 
     {:noreply, topics}
   end

From d81f63845a71e5cc60d95007ecaa2aea52a90422 Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Wed, 14 Aug 2019 11:59:33 -0400
Subject: [PATCH 22/40] Implement Pleroma.Stats as GenServer

---
 lib/pleroma/stats.ex                          | 60 +++++++++++++------
 .../mastodon_api_controller_test.exs          |  4 +-
 2 files changed, 44 insertions(+), 20 deletions(-)

diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index 101effbe4..a3b8a4d66 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -7,33 +7,56 @@ defmodule Pleroma.Stats do
   alias Pleroma.Repo
   alias Pleroma.User
 
-  use Agent
+  use GenServer
+
+  @interval 1000 * 60 * 60
 
   def start_link(_) do
-    agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
-    spawn(fn -> schedule_update() end)
-    agent
+    GenServer.start_link(__MODULE__, initial_data(), name: __MODULE__)
+  end
+
+  def force_update do
+    GenServer.call(__MODULE__, :force_update)
   end
 
   def get_stats do
-    Agent.get(__MODULE__, fn {_, stats} -> stats end)
+    %{stats: stats} = GenServer.call(__MODULE__, :get_state)
+
+    stats
   end
 
   def get_peers do
-    Agent.get(__MODULE__, fn {peers, _} -> peers end)
+    %{peers: peers} = GenServer.call(__MODULE__, :get_state)
+
+    peers
   end
 
-  def schedule_update do
-    spawn(fn ->
-      # 1 hour
-      Process.sleep(1000 * 60 * 60)
-      schedule_update()
-    end)
-
-    update_stats()
+  def init(args) do
+    Process.send_after(self(), :run_update, @interval)
+    {:ok, args}
   end
 
-  def update_stats do
+  def handle_call(:force_update, _from, _state) do
+    new_stats = get_stat_data()
+    {:reply, new_stats, new_stats}
+  end
+
+  def handle_call(:get_state, _from, state) do
+    {:reply, state, state}
+  end
+
+  def handle_info(:run_update, _state) do
+    new_stats = get_stat_data()
+
+    Process.send_after(self(), :run_update, @interval)
+    {:noreply, new_stats}
+  end
+
+  defp initial_data do
+    %{peers: [], stats: %{}}
+  end
+
+  defp get_stat_data do
     peers =
       from(
         u in User,
@@ -54,8 +77,9 @@ def update_stats do
 
     user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id)
 
-    Agent.update(__MODULE__, fn _ ->
-      {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}
-    end)
+    %{
+      peers: peers,
+      stats: %{domain_count: domain_count, status_count: status_count, user_count: user_count}
+    }
   end
 end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 2febe8b3a..112e272f9 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -2624,7 +2624,7 @@ test "get instance stats", %{conn: conn} do
       |> Changeset.put_embed(:info, info_change)
       |> User.update_and_set_cache()
 
-    Pleroma.Stats.update_stats()
+    Pleroma.Stats.force_update()
 
     conn = get(conn, "/api/v1/instance")
 
@@ -2642,7 +2642,7 @@ test "get peers", %{conn: conn} do
     insert(:user, %{local: false, nickname: "u@peer1.com"})
     insert(:user, %{local: false, nickname: "u@peer2.com"})
 
-    Pleroma.Stats.update_stats()
+    Pleroma.Stats.force_update()
 
     conn = get(conn, "/api/v1/instance/peers")
 

From c43152f6c17a287a9fe4f2556ca20a140ea30248 Mon Sep 17 00:00:00 2001
From: stwf <steven.fuchs@dockyard.com>
Date: Wed, 14 Aug 2019 14:01:11 -0400
Subject: [PATCH 23/40] fix formatting

---
 lib/pleroma/application.ex | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index c460a3bc5..aa673188f 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -121,17 +121,17 @@ defp cachex_children do
   end
 
   defp idempotency_expiration,
-       do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
+    do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
 
   defp seconds_valid_interval,
-       do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
+    do: :timer.seconds(Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]))
 
   defp build_cachex(type, opts),
-       do: %{
-         id: String.to_atom("cachex_" <> type),
-         start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
-         type: :worker
-       }
+    do: %{
+      id: String.to_atom("cachex_" <> type),
+      start: {Cachex, :start_link, [String.to_atom(type <> "_cache"), opts]},
+      type: :worker
+    }
 
   defp chat_enabled?, do: Pleroma.Config.get([:chat, :enabled])
 
@@ -145,7 +145,7 @@ defp streamer_child(_) do
   end
 
   defp oauth_cleanup_child(true),
-       do: [Pleroma.Web.OAuth.Token.CleanWorker]
+    do: [Pleroma.Web.OAuth.Token.CleanWorker]
 
   defp oauth_cleanup_child(_), do: []
 

From 626e094589689681845d8057f8abe6ab3cd52f69 Mon Sep 17 00:00:00 2001
From: Ariadne Conill <ariadne@dereferenced.org>
Date: Wed, 14 Aug 2019 18:53:18 +0000
Subject: [PATCH 24/40] MRF: fix up unserializable option lists in describe
 implementations

---
 CHANGELOG.md                                          | 1 +
 lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex | 3 ++-
 lib/pleroma/web/activity_pub/mrf/reject_non_public.ex | 3 ++-
 lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex | 3 ++-
 4 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 358287096..835dbc14b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
 - Report email not being sent to admins when the reporter is a remote user
 - MRF: ensure that subdomain_match calls are case-insensitive
+- MRF: fix use of unserializable keyword lists in describe() implementations
 
 ### Added
 - **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
index 9863454fa..b3c742954 100644
--- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
@@ -92,5 +92,6 @@ def filter(%{"type" => "Create"} = message) do
   def filter(message), do: {:ok, message}
 
   @impl true
-  def describe, do: {:ok, %{mrf_hellthread: Pleroma.Config.get([:mrf_hellthread])}}
+  def describe,
+    do: {:ok, %{mrf_hellthread: Pleroma.Config.get(:mrf_hellthread) |> Enum.into(%{})}}
 end
diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
index 0ae9397ed..5a809a321 100644
--- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
+++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
@@ -46,5 +46,6 @@ def filter(%{"type" => "Create"} = object) do
   def filter(object), do: {:ok, object}
 
   @impl true
-  def describe, do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get([:mrf_rejectnonpublic])}}
+  def describe,
+    do: {:ok, %{mrf_rejectnonpublic: Pleroma.Config.get(:mrf_rejectnonpublic) |> Enum.into(%{})}}
 end
diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
index 74da8d57e..4eaea00d8 100644
--- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex
@@ -32,5 +32,6 @@ def filter(%{"type" => message_type} = message) do
 
   def filter(message), do: {:ok, message}
 
-  def describe, do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary)}}
+  def describe,
+    do: {:ok, %{mrf_vocabulary: Pleroma.Config.get(:mrf_vocabulary) |> Enum.into(%{})}}
 end

From 5bb418a90d9efb1fa889028080c5de3a929ff2cc Mon Sep 17 00:00:00 2001
From: Ariadne Conill <ariadne@dereferenced.org>
Date: Wed, 14 Aug 2019 19:00:48 +0000
Subject: [PATCH 25/40] activitypub: publisher: add (request-target) to http
 signature when POSTing

---
 CHANGELOG.md                              | 1 +
 lib/pleroma/web/activity_pub/publisher.ex | 5 ++++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 358287096..5a6150d98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Rich Media: The crawled URL is now spliced into the rich media data.
 - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
 - ActivityPub S2S: remote user deletions now work the same as local user deletions.
+- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
 - Not being able to access the Mastodon FE login page on private instances
 - Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
 - Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 46edab0bd..987a25377 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -46,7 +46,9 @@ def is_representable?(%Activity{} = activity) do
   """
   def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
     Logger.info("Federating #{id} to #{inbox}")
-    host = URI.parse(inbox).host
+    uri = URI.parse(inbox)
+    host = uri.host
+    path = uri.path
 
     digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
 
@@ -56,6 +58,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
 
     signature =
       Pleroma.Signature.sign(actor, %{
+        "(request-target)": "post #{path}",
         host: host,
         "content-length": byte_size(json),
         digest: digest,

From 1754f8ce6d9ee896a57961a354001b713c620cd5 Mon Sep 17 00:00:00 2001
From: kaniini <ariadne@dereferenced.org>
Date: Wed, 14 Aug 2019 19:05:44 +0000
Subject: [PATCH 26/40] Apply suggestion to
 lib/pleroma/web/activity_pub/publisher.ex

---
 lib/pleroma/web/activity_pub/publisher.ex | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index 987a25377..262529b84 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -46,9 +46,7 @@ def is_representable?(%Activity{} = activity) do
   """
   def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do
     Logger.info("Federating #{id} to #{inbox}")
-    uri = URI.parse(inbox)
-    host = uri.host
-    path = uri.path
+    %{host: host, path: path} = URI.parse(inbox)
 
     digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
 

From a6a814420ded3973b271d04b29b4d6ad24b6bdf7 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Wed, 14 Aug 2019 22:48:44 +0200
Subject: [PATCH 27/40] html.ex: Allow sub and sup elements by default

Closes: https://git.pleroma.social/pleroma/pleroma/issues/1191
---
 lib/pleroma/html.ex | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 2fae7281c..06e60cba3 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -203,6 +203,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
   Meta.allow_tag_with_these_attributes("p", [])
   Meta.allow_tag_with_these_attributes("pre", [])
   Meta.allow_tag_with_these_attributes("strong", [])
+  Meta.allow_tag_with_these_attributes("sub", [])
+  Meta.allow_tag_with_these_attributes("sup", [])
   Meta.allow_tag_with_these_attributes("u", [])
   Meta.allow_tag_with_these_attributes("ul", [])
 

From a9e75fa6a4ede24fbd4549d4deb06edf368e7c52 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 00:43:02 +0300
Subject: [PATCH 28/40] Add a task to benchmark timeline rendering

---
 lib/mix/tasks/pleroma/benchmark.ex | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index 5222cce80..a45940bf3 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -26,4 +26,28 @@ def run(["tag"]) do
       end
     })
   end
+
+  def run(["render_timeline", nickname]) do
+    start_pleroma()
+    user = Pleroma.User.get_by_nickname(nickname)
+
+    activities =
+      %{}
+      |> Map.put("type", ["Create", "Announce"])
+      |> Map.put("blocking_user", user)
+      |> Map.put("muting_user", user)
+      |> Map.put("user", user)
+      |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
+      |> Enum.reverse()
+
+    Benchee.run(%{
+      "render_timeline" => fn ->
+        Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
+          activities: activities,
+          for: user,
+          as: :activity
+        })
+      end
+    })
+  end
 end

From bd5ad0af787e65bc05b7df64ef41c414900085af Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 00:47:30 +0300
Subject: [PATCH 29/40] Cache follow state

---
 lib/pleroma/user.ex                           | 22 +++++++++++++++++++
 lib/pleroma/web/activity_pub/activity_pub.ex  |  3 ++-
 lib/pleroma/web/activity_pub/utils.ex         |  9 ++++++--
 .../web/mastodon_api/views/account_view.ex    |  6 ++---
 4 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index b67743846..a1040fe71 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -132,6 +132,28 @@ def user_info(%User{} = user, args \\ %{}) do
     |> Map.put(:follower_count, follower_count)
   end
 
+  def follow_state(%User{} = user, %User{} = target) do
+    follow_activity = Utils.fetch_latest_follow(user, target)
+
+    if follow_activity,
+      do: follow_activity.data["state"],
+      # Ideally this would be nil, but then Cachex does not commit the value
+      else: false
+  end
+
+  def get_cached_follow_state(user, target) do
+    key = "follow_state:#{user.ap_id}|#{target.ap_id}"
+    Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
+  end
+
+  def set_follow_state_cache(user_ap_id, target_ap_id, state) do
+    Cachex.put(
+      :user_cache,
+      "follow_state:#{user_ap_id}|#{target_ap_id}",
+      state
+    )
+  end
+
   def set_info_cache(user, args) do
     Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
   end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index cf55c9520..01052846f 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -388,7 +388,8 @@ def unannounce(
   def follow(follower, followed, activity_id \\ nil, local \\ true) do
     with data <- make_follow_data(follower, followed, activity_id),
          {:ok, activity} <- insert(data, local),
-         :ok <- maybe_federate(activity) do
+         :ok <- maybe_federate(activity),
+         _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
       {:ok, activity}
     end
   end
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index fc5305c58..1c3058658 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -374,6 +374,7 @@ def update_follow_state_for_all(
         [state, actor, object]
       )
 
+      User.set_follow_state_cache(actor, object, state)
       activity = Activity.get_by_id(activity.id)
       {:ok, activity}
     rescue
@@ -382,12 +383,16 @@ def update_follow_state_for_all(
     end
   end
 
-  def update_follow_state(%Activity{} = activity, state) do
+  def update_follow_state(
+        %Activity{data: %{"actor" => actor, "object" => object}} = activity,
+        state
+      ) do
     with new_data <-
            activity.data
            |> Map.put("state", state),
          changeset <- Changeset.change(activity, data: new_data),
-         {:ok, activity} <- Repo.update(changeset) do
+         {:ok, activity} <- Repo.update(changeset),
+         _ <- User.set_follow_state_cache(actor, object, state) do
       {:ok, activity}
     end
   end
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 72c092f25..0ef568f0f 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -37,11 +37,11 @@ def render("relationship.json", %{user: nil, target: _target}) do
   end
 
   def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
-    follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
+    follow_state = User.get_cached_follow_state(user, target)
 
     requested =
-      if follow_activity && !User.following?(target, user) do
-        follow_activity.data["state"] == "pending"
+      if follow_state && !User.following?(user, target) do
+        follow_state == "pending"
       else
         false
       end

From e8a8d50138f70240bf853ba67008dc19e75127e8 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 01:01:13 +0300
Subject: [PATCH 30/40] Collect stats immediately after init

---
 lib/pleroma/stats.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex
index a3b8a4d66..df80fbaa4 100644
--- a/lib/pleroma/stats.ex
+++ b/lib/pleroma/stats.ex
@@ -32,7 +32,7 @@ def get_peers do
   end
 
   def init(args) do
-    Process.send_after(self(), :run_update, @interval)
+    Process.send(self(), :run_update, [])
     {:ok, args}
   end
 

From a4a3e3becd5e008dbfa9a23157ae4b16a0652bce Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 17:37:30 +0300
Subject: [PATCH 31/40] Hide muted theads from home/public timelines unless
 `with_muted` is set

---
 lib/pleroma/activity.ex                      |  1 +
 lib/pleroma/web/activity_pub/activity_pub.ex | 20 +++++++++++------
 lib/pleroma/web/streamer.ex                  |  6 ++---
 test/web/activity_pub/activity_pub_test.exs  | 23 ++++++++++++++++++++
 test/web/streamer_test.exs                   | 20 +++++++++++++++++
 5 files changed, 60 insertions(+), 10 deletions(-)

diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index baf1e7722..35612c882 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -96,6 +96,7 @@ def with_set_thread_muted_field(query, %User{} = user) do
     from([a] in query,
       left_join: tm in ThreadMute,
       on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data),
+      as: :thread_mute,
       select: %Activity{a | thread_muted?: not is_nil(tm.id)}
     )
   end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index cf55c9520..defccade8 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -790,14 +790,20 @@ defp restrict_reblogs(query, _), do: query
 
   defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
 
-  defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
+  defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
     mutes = info.mutes
 
-    from(
-      activity in query,
-      where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
-      where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
-    )
+    query =
+      from([activity] in query,
+        where: fragment("not (? = ANY(?))", activity.actor, ^mutes),
+        where: fragment("not (?->'to' \\?| ?)", activity.data, ^mutes)
+      )
+
+    unless opts["skip_preload"] do
+      from([thread_mute: tm] in query, where: is_nil(tm))
+    else
+      query
+    end
   end
 
   defp restrict_muted(query, _), do: query
@@ -898,7 +904,7 @@ defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
 
   defp maybe_set_thread_muted_field(query, opts) do
     query
-    |> Activity.with_set_thread_muted_field(opts["user"])
+    |> Activity.with_set_thread_muted_field(opts["muting_user"] || opts["user"])
   end
 
   defp maybe_order(query, %{order: :desc}) do
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index bbaddd852..826be2f9a 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -113,8 +113,7 @@ def handle_cast(
     |> Map.get("#{topic}:#{item.user_id}", [])
     |> Enum.each(fn socket ->
       with %User{} = user <- User.get_cached_by_ap_id(socket.assigns[:user].ap_id),
-           true <- should_send?(user, item),
-           false <- CommonAPI.thread_muted?(user, item.activity) do
+           true <- should_send?(user, item) do
         send(
           socket.transport_pid,
           {:text, represent_notification(socket.assigns[:user], item)}
@@ -236,7 +235,8 @@ defp should_send?(%User{} = user, %Activity{} = item) do
          %{host: parent_host} <- URI.parse(parent.data["actor"]),
          false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host),
          false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host),
-         true <- thread_containment(item, user) do
+         true <- thread_containment(item, user),
+         false <- CommonAPI.thread_muted?(user, item) do
       true
     else
       _ -> false
diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index d723f331f..0377d29f6 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -538,6 +538,29 @@ test "doesn't return muted activities" do
     assert Enum.member?(activities, activity_one)
   end
 
+  test "doesn't return thread muted activities" do
+    user = insert(:user)
+    activity_one = insert(:note_activity)
+    note_two = insert(:note, data: %{"context" => "suya.."})
+    activity_two = insert(:note_activity, note: note_two)
+
+    {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
+
+    assert [activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
+  end
+
+  test "returns thread muted activities when with_muted is set" do
+    user = insert(:user)
+    activity_one = insert(:note_activity)
+    note_two = insert(:note, data: %{"context" => "suya.."})
+    activity_two = insert(:note_activity, note: note_two)
+
+    {:ok, activity_two} = CommonAPI.add_mute(user, activity_two)
+
+    assert [activity_two, activity_one] =
+             ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
+  end
+
   test "does include announces on request" do
     activity_three = insert(:note_activity)
     user = insert(:user)
diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs
index d47b37efb..5b7fe44d4 100644
--- a/test/web/streamer_test.exs
+++ b/test/web/streamer_test.exs
@@ -414,6 +414,26 @@ test "it doesn't send muted reblogs" do
     Task.await(task)
   end
 
+  test "it doesn't send posts from muted threads" do
+    user = insert(:user)
+    user2 = insert(:user)
+    {:ok, user2, user, _activity} = CommonAPI.follow(user2, user)
+
+    {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"})
+
+    {:ok, activity} = CommonAPI.add_mute(user2, activity)
+
+    task = Task.async(fn -> refute_receive {:text, _}, 4_000 end)
+
+    Streamer.add_socket(
+      "user",
+      %{transport_pid: task.pid, assigns: %{user: user2}}
+    )
+
+    Streamer.stream("user", activity)
+    Task.await(task)
+  end
+
   describe "direct streams" do
     setup do
       GenServer.start(Streamer, %{}, name: Streamer)

From 1ad71592adb47762287aec8c36d0fca565c38362 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 17:41:26 +0300
Subject: [PATCH 32/40] Parallelize template rendering

---
 lib/mix/tasks/pleroma/benchmark.ex            | 38 ++++++++++++++-----
 .../web/mastodon_api/views/status_view.ex     |  4 +-
 lib/pleroma/web/web.ex                        | 18 ++++++++-
 3 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/lib/mix/tasks/pleroma/benchmark.ex b/lib/mix/tasks/pleroma/benchmark.ex
index a45940bf3..4cc634727 100644
--- a/lib/mix/tasks/pleroma/benchmark.ex
+++ b/lib/mix/tasks/pleroma/benchmark.ex
@@ -37,17 +37,37 @@ def run(["render_timeline", nickname]) do
       |> Map.put("blocking_user", user)
       |> Map.put("muting_user", user)
       |> Map.put("user", user)
+      |> Map.put("limit", 80)
       |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
       |> Enum.reverse()
 
-    Benchee.run(%{
-      "render_timeline" => fn ->
-        Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
-          activities: activities,
-          for: user,
-          as: :activity
-        })
-      end
-    })
+    inputs = %{
+      "One activity" => Enum.take_random(activities, 1),
+      "Ten activities" => Enum.take_random(activities, 10),
+      "Twenty activities" => Enum.take_random(activities, 20),
+      "Forty activities" => Enum.take_random(activities, 40),
+      "Eighty activities" => Enum.take_random(activities, 80)
+    }
+
+    Benchee.run(
+      %{
+        "Parallel rendering" => fn activities ->
+          Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
+            activities: activities,
+            for: user,
+            as: :activity
+          })
+        end,
+        "Standart rendering" => fn activities ->
+          Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
+            activities: activities,
+            for: user,
+            as: :activity,
+            parallel: false
+          })
+        end
+      },
+      inputs: inputs
+    )
   end
 end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 492af1702..7e4e99280 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -70,12 +70,14 @@ defp reblogged?(activity, user) do
 
   def render("index.json", opts) do
     replied_to_activities = get_replied_to_activities(opts.activities)
+    parallel = unless is_nil(opts[:parallel]), do: opts[:parallel], else: true
 
     opts.activities
     |> safe_render_many(
       StatusView,
       "status.json",
-      Map.put(opts, :replied_to_activities, replied_to_activities)
+      Map.put(opts, :replied_to_activities, replied_to_activities),
+      parallel
     )
   end
 
diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex
index 687346554..bfb6c7287 100644
--- a/lib/pleroma/web/web.ex
+++ b/lib/pleroma/web/web.ex
@@ -66,9 +66,23 @@ def safe_render(view, template, assigns \\ %{}) do
       end
 
       @doc """
-      Same as `render_many/4` but wrapped in rescue block.
+      Same as `render_many/4` but wrapped in rescue block and parallelized (unless disabled by passing false as a fifth argument).
       """
-      def safe_render_many(collection, view, template, assigns \\ %{}) do
+      def safe_render_many(collection, view, template, assigns \\ %{}, parallel \\ true)
+
+      def safe_render_many(collection, view, template, assigns, true) do
+        Enum.map(collection, fn resource ->
+          Task.async(fn ->
+            as = Map.get(assigns, :as) || view.__resource__
+            assigns = Map.put(assigns, as, resource)
+            safe_render(view, template, assigns)
+          end)
+        end)
+        |> Enum.map(&Task.await(&1, :infinity))
+        |> Enum.filter(& &1)
+      end
+
+      def safe_render_many(collection, view, template, assigns, false) do
         Enum.map(collection, fn resource ->
           as = Map.get(assigns, :as) || view.__resource__
           assigns = Map.put(assigns, as, resource)

From fba3c16d205408bf6ba00e40bad7d8b5a7abe510 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 15 Aug 2019 20:36:20 +0300
Subject: [PATCH 33/40] Fix OAuth cleanup worker unconditionally starting

!1576 removed enabled/disabled check from the worker, in favor of just
not starting it in application.ex if disabled. However a line
unconditionally starting the worker was removed
---
 lib/pleroma/application.ex | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index aa673188f..25e56b9e2 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -41,7 +41,6 @@ def start(_type, _args) do
         hackney_pool_children() ++
         [
           Pleroma.Web.Federator.RetryQueue,
-          Pleroma.Web.OAuth.Token.CleanWorker,
           Pleroma.Stats,
           %{
             id: :web_push_init,

From 3315a2a1c3cae3375bbb1c7112b2c75563f3a4af Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Fri, 16 Aug 2019 15:58:42 +0300
Subject: [PATCH 34/40] fixed User.unfollow with synchronization external user

---
 lib/pleroma/user.ex |  4 ++-
 test/user_test.exs  | 64 ++++++++++++++++++++++++++++++++++++---------
 2 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index a1040fe71..23748ef26 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -742,6 +742,7 @@ def update_note_count(%User{} = user) do
     |> update_and_set_cache()
   end
 
+  @spec maybe_fetch_follow_information(User.t()) :: User.t()
   def maybe_fetch_follow_information(user) do
     with {:ok, user} <- fetch_follow_information(user) do
       user
@@ -799,9 +800,10 @@ def update_follower_count(%User{} = user) do
     end
   end
 
+  @spec maybe_update_following_count(User.t()) :: User.t()
   def maybe_update_following_count(%User{local: false} = user) do
     if Pleroma.Config.get([:instance, :external_user_synchronization]) do
-      {:ok, maybe_fetch_follow_information(user)}
+      maybe_fetch_follow_information(user)
     else
       user
     end
diff --git a/test/user_test.exs b/test/user_test.exs
index b363b322c..3861cadac 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -192,24 +192,64 @@ test "local users do not automatically follow local locked accounts" do
   #   assert websub
   # end
 
-  test "unfollow takes a user and another user" do
-    followed = insert(:user)
-    user = insert(:user, %{following: [User.ap_followers(followed)]})
+  describe "unfollow/2" do
+    setup do
+      setting = Pleroma.Config.get([:instance, :external_user_synchronization])
 
-    {:ok, user, _activity} = User.unfollow(user, followed)
+      on_exit(fn ->
+        Pleroma.Config.put([:instance, :external_user_synchronization], setting)
+      end)
 
-    user = User.get_cached_by_id(user.id)
+      :ok
+    end
 
-    assert user.following == []
-  end
+    test "unfollow with syncronizes external user" do
+      Pleroma.Config.put([:instance, :external_user_synchronization], true)
 
-  test "unfollow doesn't unfollow yourself" do
-    user = insert(:user)
+      followed =
+        insert(:user,
+          nickname: "fuser1",
+          follower_address: "http://localhost:4001/users/fuser1/followers",
+          following_address: "http://localhost:4001/users/fuser1/following",
+          ap_id: "http://localhost:4001/users/fuser1"
+        )
 
-    {:error, _} = User.unfollow(user, user)
+      user =
+        insert(:user, %{
+          local: false,
+          nickname: "fuser2",
+          ap_id: "http://localhost:4001/users/fuser2",
+          follower_address: "http://localhost:4001/users/fuser2/followers",
+          following_address: "http://localhost:4001/users/fuser2/following",
+          following: [User.ap_followers(followed)]
+        })
 
-    user = User.get_cached_by_id(user.id)
-    assert user.following == [user.ap_id]
+      {:ok, user, _activity} = User.unfollow(user, followed)
+
+      user = User.get_cached_by_id(user.id)
+
+      assert user.following == []
+    end
+
+    test "unfollow takes a user and another user" do
+      followed = insert(:user)
+      user = insert(:user, %{following: [User.ap_followers(followed)]})
+
+      {:ok, user, _activity} = User.unfollow(user, followed)
+
+      user = User.get_cached_by_id(user.id)
+
+      assert user.following == []
+    end
+
+    test "unfollow doesn't unfollow yourself" do
+      user = insert(:user)
+
+      {:error, _} = User.unfollow(user, user)
+
+      user = User.get_cached_by_id(user.id)
+      assert user.following == [user.ap_id]
+    end
   end
 
   test "test if a user is following another user" do

From 94e336d9d5731c236e17d58f66a6a1678ca148f7 Mon Sep 17 00:00:00 2001
From: Sadposter <hannah+pleroma@coffee-and-dreams.uk>
Date: Sun, 18 Aug 2019 20:29:31 +0100
Subject: [PATCH 35/40] clear follow requests when blocking a user

---
 lib/pleroma/user.ex |  8 ++++++++
 test/user_test.exs  | 11 +++++++++++
 2 files changed, 19 insertions(+)

diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 5c3c8a8a2..829de6e31 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -21,6 +21,7 @@ defmodule Pleroma.User do
   alias Pleroma.Web
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
+  alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
   alias Pleroma.Web.OAuth
   alias Pleroma.Web.OStatus
@@ -914,6 +915,13 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
         blocker
       end
 
+    # clear any requested follows as well
+    blocked =
+      case CommonAPI.reject_follow_request(blocked, blocker) do
+        {:ok, %User{} = updated_blocked} -> updated_blocked
+        nil -> blocked
+      end
+
     blocker =
       if subscribed_to?(blocked, blocker) do
         {:ok, blocker} = unsubscribe(blocked, blocker)
diff --git a/test/user_test.exs b/test/user_test.exs
index b363b322c..23011bdac 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -90,6 +90,17 @@ test "doesn't return already accepted or duplicate follow requests" do
     assert activity
   end
 
+  test "clears follow requests when requester is blocked" do
+    followed = insert(:user, %{info: %{locked: true}})
+    follower = insert(:user)
+
+    Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => followed.id})
+    assert {:ok, [_activity]} = User.get_follow_requests(followed)
+
+    {:ok, _follower} = User.block(followed, follower)
+    assert {:ok, []} = User.get_follow_requests(followed)
+  end
+
   test "follow_all follows mutliple users" do
     user = insert(:user)
     followed_zero = insert(:user)

From 58c1391c4dd7e916b54e49b0b17ea1f01cac02f3 Mon Sep 17 00:00:00 2001
From: Sadposter <hannah+pleroma@coffee-and-dreams.uk>
Date: Sun, 18 Aug 2019 22:54:40 +0100
Subject: [PATCH 36/40] use commonAPI in tests

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

diff --git a/test/user_test.exs b/test/user_test.exs
index 23011bdac..96f2a252d 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -94,7 +94,7 @@ test "clears follow requests when requester is blocked" do
     followed = insert(:user, %{info: %{locked: true}})
     follower = insert(:user)
 
-    Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => followed.id})
+    CommonAPI.follow(follower, followed)
     assert {:ok, [_activity]} = User.get_follow_requests(followed)
 
     {:ok, _follower} = User.block(followed, follower)

From a2fdc32368dec49449505ed85ebeff982afa449c Mon Sep 17 00:00:00 2001
From: Ariadne Conill <ariadne@dereferenced.org>
Date: Sun, 18 Aug 2019 22:21:31 +0000
Subject: [PATCH 37/40] tests: activitypub: fix typo

---
 test/web/activity_pub/activity_pub_test.exs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs
index 0377d29f6..f20cd2840 100644
--- a/test/web/activity_pub/activity_pub_test.exs
+++ b/test/web/activity_pub/activity_pub_test.exs
@@ -540,24 +540,24 @@ test "doesn't return muted activities" do
 
   test "doesn't return thread muted activities" do
     user = insert(:user)
-    activity_one = insert(:note_activity)
+    _activity_one = insert(:note_activity)
     note_two = insert(:note, data: %{"context" => "suya.."})
     activity_two = insert(:note_activity, note: note_two)
 
     {:ok, _activity_two} = CommonAPI.add_mute(user, activity_two)
 
-    assert [activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
+    assert [_activity_one] = ActivityPub.fetch_activities([], %{"muting_user" => user})
   end
 
   test "returns thread muted activities when with_muted is set" do
     user = insert(:user)
-    activity_one = insert(:note_activity)
+    _activity_one = insert(:note_activity)
     note_two = insert(:note, data: %{"context" => "suya.."})
     activity_two = insert(:note_activity, note: note_two)
 
     {:ok, activity_two} = CommonAPI.add_mute(user, activity_two)
 
-    assert [activity_two, activity_one] =
+    assert [_activity_two, _activity_one] =
              ActivityPub.fetch_activities([], %{"muting_user" => user, "with_muted" => true})
   end
 

From e652cef76b99588867315347e8dcc3d323d8a161 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov <parallel588@gmail.com>
Date: Mon, 19 Aug 2019 13:31:56 +0300
Subject: [PATCH 38/40] removes duplicates from relay subscription list

---
 lib/mix/tasks/pleroma/relay.ex | 12 +++++-------
 test/tasks/relay_test.exs      | 23 +++++++++++++++++++++++
 2 files changed, 28 insertions(+), 7 deletions(-)

diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex
index c7324fff6..a738fae75 100644
--- a/lib/mix/tasks/pleroma/relay.ex
+++ b/lib/mix/tasks/pleroma/relay.ex
@@ -53,13 +53,11 @@ def run(["unfollow", target]) do
   def run(["list"]) do
     start_pleroma()
 
-    with %User{} = user <- Relay.get_actor() do
-      user.following
-      |> Enum.each(fn entry ->
-        URI.parse(entry)
-        |> Map.get(:host)
-        |> shell_info()
-      end)
+    with %User{following: following} = _user <- Relay.get_actor() do
+      following
+      |> Enum.map(fn entry -> URI.parse(entry).host end)
+      |> Enum.uniq()
+      |> Enum.each(&shell_info(&1))
     else
       e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}")
     end
diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs
index 9d260da3e..0d341c8d6 100644
--- a/test/tasks/relay_test.exs
+++ b/test/tasks/relay_test.exs
@@ -69,4 +69,27 @@ test "relay is unfollowed" do
       assert undo_activity.data["object"] == cancelled_activity.data
     end
   end
+
+  describe "mix pleroma.relay list" do
+    test "Prints relay subscription list" do
+      :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
+
+      refute_receive {:mix_shell, :info, _}
+
+      Pleroma.Web.ActivityPub.Relay.get_actor()
+      |> Ecto.Changeset.change(
+        following: [
+          "http://test-app.com/user/test1",
+          "http://test-app.com/user/test1",
+          "http://test-app-42.com/user/test1"
+        ]
+      )
+      |> Pleroma.User.update_and_set_cache()
+
+      :ok = Mix.Tasks.Pleroma.Relay.run(["list"])
+
+      assert_receive {:mix_shell, :info, ["test-app.com"]}
+      assert_receive {:mix_shell, :info, ["test-app-42.com"]}
+    end
+  end
 end

From a320358703db249ab20df5afd81c92fb42b8cadb Mon Sep 17 00:00:00 2001
From: Maksim <parallel588@gmail.com>
Date: Mon, 19 Aug 2019 15:34:29 +0000
Subject: [PATCH 39/40] added test helpers to clear config after tests

---
 test/config/transfer_task_test.exs            |  8 +--
 test/conversation_test.exs                    | 10 +--
 test/emails/mailer_test.exs                   |  6 +-
 test/http/request_builder_test.exs            |  8 +--
 test/object/fetcher_test.exs                  |  8 +--
 ...sure_public_or_authenticated_plug_test.exs | 17 ++---
 test/plugs/http_security_plug_test.exs        | 17 +----
 test/plugs/instance_static_test.exs           | 10 ++-
 test/support/helpers.ex                       | 53 ++++++++++++++++
 test/tasks/config_test.exs                    |  9 ++-
 test/tasks/robots_txt_test.exs                |  8 +--
 .../upload/filter/anonymize_filename_test.exs |  8 +--
 test/upload/filter/mogrify_test.exs           |  8 +--
 test/upload/filter_test.exs                   |  8 +--
 test/upload_test.exs                          |  6 +-
 test/uploaders/s3_test.exs                    | 10 +--
 test/user_test.exs                            | 40 ++++--------
 .../activity_pub_controller_test.exs          | 15 ++---
 test/web/activity_pub/mrf/mrf_test.exs        |  6 +-
 .../mrf/reject_non_public_test.exs            |  7 +--
 .../activity_pub/mrf/simple_policy_test.exs   |  8 +--
 .../mrf/user_allowlist_policy_test.exs        |  7 +--
 .../mrf/vocabulary_policy_test.exs            | 25 ++------
 .../admin_api/admin_api_controller_test.exs   | 62 ++++++-------------
 test/web/common_api/common_api_test.exs       |  9 ++-
 test/web/federator_test.exs                   | 27 ++++----
 test/web/instances/instance_test.exs          | 10 +--
 test/web/instances/instances_test.exs         | 10 +--
 .../mastodon_api_controller_test.exs          | 49 +++++----------
 test/web/media_proxy/media_proxy_test.exs     |  7 +--
 test/web/oauth/ldap_authorization_test.exs    | 17 ++---
 test/web/oauth/oauth_controller_test.exs      | 36 ++++-------
 test/web/ostatus/ostatus_controller_test.exs  | 11 ++--
 test/web/plugs/federating_plug_test.exs       | 10 +--
 test/web/rich_media/helpers_test.exs          |  4 +-
 test/web/streamer_test.exs                    | 10 +--
 .../twitter_api_controller_test.exs           | 21 ++-----
 test/web/twitter_api/util_controller_test.exs | 28 ++-------
 .../web_finger/web_finger_controller_test.exs | 10 ++-
 test/web/websub/websub_controller_test.exs    | 10 +--
 40 files changed, 213 insertions(+), 420 deletions(-)

diff --git a/test/config/transfer_task_test.exs b/test/config/transfer_task_test.exs
index 4455a4d47..9074f3b97 100644
--- a/test/config/transfer_task_test.exs
+++ b/test/config/transfer_task_test.exs
@@ -5,14 +5,8 @@
 defmodule Pleroma.Config.TransferTaskTest do
   use Pleroma.DataCase
 
-  setup do
-    dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
+  clear_config([:instance, :dynamic_configuration]) do
     Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
-    on_exit(fn ->
-      Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
-    end)
   end
 
   test "transfer config values from db to env" do
diff --git a/test/conversation_test.exs b/test/conversation_test.exs
index aa193e0d4..4e36494f8 100644
--- a/test/conversation_test.exs
+++ b/test/conversation_test.exs
@@ -11,14 +11,8 @@ defmodule Pleroma.ConversationTest do
 
   import Pleroma.Factory
 
-  setup_all do
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
-    :ok
+  clear_config_all([:instance, :federating]) do
+    Pleroma.Config.put([:instance, :federating], true)
   end
 
   test "it goes through old direct conversations" do
diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs
index 450bb09c7..ae5effb7a 100644
--- a/test/emails/mailer_test.exs
+++ b/test/emails/mailer_test.exs
@@ -15,11 +15,7 @@ defmodule Pleroma.Emails.MailerTest do
     to: [{"Test User", "user1@example.com"}]
   }
 
-  setup do
-    value = Pleroma.Config.get([Pleroma.Emails.Mailer, :enabled])
-    on_exit(fn -> Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], value) end)
-    :ok
-  end
+  clear_config([Pleroma.Emails.Mailer, :enabled])
 
   test "not send email when mailer is disabled" do
     Pleroma.Config.put([Pleroma.Emails.Mailer, :enabled], false)
diff --git a/test/http/request_builder_test.exs b/test/http/request_builder_test.exs
index 7febe84c5..170ca916f 100644
--- a/test/http/request_builder_test.exs
+++ b/test/http/request_builder_test.exs
@@ -4,21 +4,19 @@
 
 defmodule Pleroma.HTTP.RequestBuilderTest do
   use ExUnit.Case, async: true
+  use Pleroma.Tests.Helpers
   alias Pleroma.HTTP.RequestBuilder
 
   describe "headers/2" do
+    clear_config([:http, :send_user_agent])
+
     test "don't send pleroma user agent" do
       assert RequestBuilder.headers(%{}, []) == %{headers: []}
     end
 
     test "send pleroma user agent" do
-      send = Pleroma.Config.get([:http, :send_user_agent])
       Pleroma.Config.put([:http, :send_user_agent], true)
 
-      on_exit(fn ->
-        Pleroma.Config.put([:http, :send_user_agent], send)
-      end)
-
       assert RequestBuilder.headers(%{}, []) == %{
                headers: [{"User-Agent", Pleroma.Application.user_agent()}]
              }
diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs
index 0ca87f035..895a73d2c 100644
--- a/test/object/fetcher_test.exs
+++ b/test/object/fetcher_test.exs
@@ -159,32 +159,28 @@ test "it can refetch pruned objects" do
   end
 
   describe "signed fetches" do
+    clear_config([:activitypub, :sign_object_fetches])
+
     test_with_mock "it signs fetches when configured to do so",
                    Pleroma.Signature,
                    [:passthrough],
                    [] do
-      option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
       Pleroma.Config.put([:activitypub, :sign_object_fetches], true)
 
       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       assert called(Pleroma.Signature.sign(:_, :_))
-
-      Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
     end
 
     test_with_mock "it doesn't sign fetches when not configured to do so",
                    Pleroma.Signature,
                    [:passthrough],
                    [] do
-      option = Pleroma.Config.get([:activitypub, :sign_object_fetches])
       Pleroma.Config.put([:activitypub, :sign_object_fetches], false)
 
       Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
 
       refute called(Pleroma.Signature.sign(:_, :_))
-
-      Pleroma.Config.put([:activitypub, :sign_object_fetches], option)
     end
   end
 end
diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs
index ce5d77ff7..d45662a2a 100644
--- a/test/plugs/ensure_public_or_authenticated_plug_test.exs
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -9,8 +9,10 @@ defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
   alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
   alias Pleroma.User
 
+  clear_config([:instance, :public])
+
   test "it halts if not public and no user is assigned", %{conn: conn} do
-    set_public_to(false)
+    Config.put([:instance, :public], false)
 
     conn =
       conn
@@ -21,7 +23,7 @@ test "it halts if not public and no user is assigned", %{conn: conn} do
   end
 
   test "it continues if public", %{conn: conn} do
-    set_public_to(true)
+    Config.put([:instance, :public], true)
 
     ret_conn =
       conn
@@ -31,7 +33,7 @@ test "it continues if public", %{conn: conn} do
   end
 
   test "it continues if a user is assigned, even if not public", %{conn: conn} do
-    set_public_to(false)
+    Config.put([:instance, :public], false)
 
     conn =
       conn
@@ -43,13 +45,4 @@ test "it continues if a user is assigned, even if not public", %{conn: conn} do
 
     assert ret_conn == conn
   end
-
-  defp set_public_to(value) do
-    orig = Config.get!([:instance, :public])
-    Config.put([:instance, :public], value)
-
-    on_exit(fn ->
-      Config.put([:instance, :public], orig)
-    end)
-  end
 end
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
index 7dfd50c1f..7a2835e3d 100644
--- a/test/plugs/http_security_plug_test.exs
+++ b/test/plugs/http_security_plug_test.exs
@@ -7,17 +7,12 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
   alias Pleroma.Config
   alias Plug.Conn
 
+  clear_config([:http_securiy, :enabled])
+  clear_config([:http_security, :sts])
+
   describe "http security enabled" do
     setup do
-      enabled = Config.get([:http_securiy, :enabled])
-
       Config.put([:http_security, :enabled], true)
-
-      on_exit(fn ->
-        Config.put([:http_security, :enabled], enabled)
-      end)
-
-      :ok
     end
 
     test "it sends CSP headers when enabled", %{conn: conn} do
@@ -81,14 +76,8 @@ test "it sends `report-to` & `report-uri` CSP response headers" do
   end
 
   test "it does not send CSP headers when disabled", %{conn: conn} do
-    enabled = Config.get([:http_securiy, :enabled])
-
     Config.put([:http_security, :enabled], false)
 
-    on_exit(fn ->
-      Config.put([:http_security, :enabled], enabled)
-    end)
-
     conn = get(conn, "/api/v1/instance")
 
     assert Conn.get_resp_header(conn, "x-xss-protection") == []
diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs
index e2dcfa3d8..6aabc45a4 100644
--- a/test/plugs/instance_static_test.exs
+++ b/test/plugs/instance_static_test.exs
@@ -8,14 +8,12 @@ defmodule Pleroma.Web.RuntimeStaticPlugTest do
   @dir "test/tmp/instance_static"
 
   setup do
-    static_dir = Pleroma.Config.get([:instance, :static_dir])
-    Pleroma.Config.put([:instance, :static_dir], @dir)
     File.mkdir_p!(@dir)
+    on_exit(fn -> File.rm_rf(@dir) end)
+  end
 
-    on_exit(fn ->
-      Pleroma.Config.put([:instance, :static_dir], static_dir)
-      File.rm_rf(@dir)
-    end)
+  clear_config([:instance, :static_dir]) do
+    Pleroma.Config.put([:instance, :static_dir], @dir)
   end
 
   test "overrides index" do
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index 1a92be065..a601b3ec8 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -7,8 +7,52 @@ defmodule Pleroma.Tests.Helpers do
   Helpers for use in tests.
   """
 
+  defmacro clear_config(config_path) do
+    quote do
+      clear_config(unquote(config_path)) do
+      end
+    end
+  end
+
+  defmacro clear_config(config_path, do: yield) do
+    quote do
+      setup do
+        initial_setting = Pleroma.Config.get(unquote(config_path))
+        unquote(yield)
+        on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+        :ok
+      end
+    end
+  end
+
+  defmacro clear_config_all(config_path) do
+    quote do
+      clear_config_all(unquote(config_path)) do
+      end
+    end
+  end
+
+  defmacro clear_config_all(config_path, do: yield) do
+    quote do
+      setup_all do
+        initial_setting = Pleroma.Config.get(unquote(config_path))
+        unquote(yield)
+        on_exit(fn -> Pleroma.Config.put(unquote(config_path), initial_setting) end)
+        :ok
+      end
+    end
+  end
+
   defmacro __using__(_opts) do
     quote do
+      import Pleroma.Tests.Helpers,
+        only: [
+          clear_config: 1,
+          clear_config: 2,
+          clear_config_all: 1,
+          clear_config_all: 2
+        ]
+
       def collect_ids(collection) do
         collection
         |> Enum.map(& &1.id)
@@ -30,6 +74,15 @@ def render_json(view, template, assigns) do
         |> Poison.encode!()
         |> Poison.decode!()
       end
+
+      defmacro guards_config(config_path) do
+        quote do
+          initial_setting = Pleroma.Config.get(config_path)
+
+          Pleroma.Config.put(config_path, true)
+          on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
+        end
+      end
     end
   end
 end
diff --git a/test/tasks/config_test.exs b/test/tasks/config_test.exs
index a9b79eb5b..9cd47380c 100644
--- a/test/tasks/config_test.exs
+++ b/test/tasks/config_test.exs
@@ -11,21 +11,20 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
     Mix.shell(Mix.Shell.Process)
     temp_file = "config/temp.exported_from_db.secret.exs"
 
-    dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
-    Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
     on_exit(fn ->
       Mix.shell(Mix.Shell.IO)
       Application.delete_env(:pleroma, :first_setting)
       Application.delete_env(:pleroma, :second_setting)
-      Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
       :ok = File.rm(temp_file)
     end)
 
     {:ok, temp_file: temp_file}
   end
 
+  clear_config_all([:instance, :dynamic_configuration]) do
+    Pleroma.Config.put([:instance, :dynamic_configuration], true)
+  end
+
   test "settings are migrated to db" do
     assert Repo.all(Config) == []
 
diff --git a/test/tasks/robots_txt_test.exs b/test/tasks/robots_txt_test.exs
index 78a3f17b4..917df2675 100644
--- a/test/tasks/robots_txt_test.exs
+++ b/test/tasks/robots_txt_test.exs
@@ -4,17 +4,17 @@
 
 defmodule Mix.Tasks.Pleroma.RobotsTxtTest do
   use ExUnit.Case
+  use Pleroma.Tests.Helpers
   alias Mix.Tasks.Pleroma.RobotsTxt
 
+  clear_config([:instance, :static_dir])
+
   test "creates new dir" do
     path = "test/fixtures/new_dir/"
     file_path = path <> "robots.txt"
-
-    static_dir = Pleroma.Config.get([:instance, :static_dir])
     Pleroma.Config.put([:instance, :static_dir], path)
 
     on_exit(fn ->
-      Pleroma.Config.put([:instance, :static_dir], static_dir)
       {:ok, ["test/fixtures/new_dir/", "test/fixtures/new_dir/robots.txt"]} = File.rm_rf(path)
     end)
 
@@ -29,11 +29,9 @@ test "creates new dir" do
   test "to existance folder" do
     path = "test/fixtures/"
     file_path = path <> "robots.txt"
-    static_dir = Pleroma.Config.get([:instance, :static_dir])
     Pleroma.Config.put([:instance, :static_dir], path)
 
     on_exit(fn ->
-      Pleroma.Config.put([:instance, :static_dir], static_dir)
       :ok = File.rm(file_path)
     end)
 
diff --git a/test/upload/filter/anonymize_filename_test.exs b/test/upload/filter/anonymize_filename_test.exs
index a31b38ab1..6b33e7395 100644
--- a/test/upload/filter/anonymize_filename_test.exs
+++ b/test/upload/filter/anonymize_filename_test.exs
@@ -9,12 +9,6 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
   alias Pleroma.Upload
 
   setup do
-    custom_filename = Config.get([Upload.Filter.AnonymizeFilename, :text])
-
-    on_exit(fn ->
-      Config.put([Upload.Filter.AnonymizeFilename, :text], custom_filename)
-    end)
-
     upload_file = %Upload{
       name: "an… image.jpg",
       content_type: "image/jpg",
@@ -24,6 +18,8 @@ defmodule Pleroma.Upload.Filter.AnonymizeFilenameTest do
     %{upload_file: upload_file}
   end
 
+  clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
+
   test "it replaces filename on pre-defined text", %{upload_file: upload_file} do
     Config.put([Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
     {:ok, %Upload{name: name}} = Upload.Filter.AnonymizeFilename.filter(upload_file)
diff --git a/test/upload/filter/mogrify_test.exs b/test/upload/filter/mogrify_test.exs
index c301440fd..210320d30 100644
--- a/test/upload/filter/mogrify_test.exs
+++ b/test/upload/filter/mogrify_test.exs
@@ -10,13 +10,7 @@ defmodule Pleroma.Upload.Filter.MogrifyTest do
   alias Pleroma.Upload
   alias Pleroma.Upload.Filter
 
-  setup do
-    filter = Config.get([Filter.Mogrify, :args])
-
-    on_exit(fn ->
-      Config.put([Filter.Mogrify, :args], filter)
-    end)
-  end
+  clear_config([Filter.Mogrify, :args])
 
   test "apply mogrify filter" do
     Config.put([Filter.Mogrify, :args], [{"tint", "40"}])
diff --git a/test/upload/filter_test.exs b/test/upload/filter_test.exs
index 640cd7107..03887c06a 100644
--- a/test/upload/filter_test.exs
+++ b/test/upload/filter_test.exs
@@ -8,13 +8,7 @@ defmodule Pleroma.Upload.FilterTest do
   alias Pleroma.Config
   alias Pleroma.Upload.Filter
 
-  setup do
-    custom_filename = Config.get([Pleroma.Upload.Filter.AnonymizeFilename, :text])
-
-    on_exit(fn ->
-      Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], custom_filename)
-    end)
-  end
+  clear_config([Pleroma.Upload.Filter.AnonymizeFilename, :text])
 
   test "applies filters" do
     Config.put([Pleroma.Upload.Filter.AnonymizeFilename, :text], "custom-file.png")
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 95b16078b..6721fe82e 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -250,12 +250,8 @@ test "escapes reserved uri characters" do
   end
 
   describe "Setting a custom base_url for uploaded media" do
-    setup do
+    clear_config([Pleroma.Upload, :base_url]) do
       Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social")
-
-      on_exit(fn ->
-        Pleroma.Config.put([Pleroma.Upload, :base_url], nil)
-      end)
     end
 
     test "returns a media url with configured base_url" do
diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs
index a0a1cfdf0..171316340 100644
--- a/test/uploaders/s3_test.exs
+++ b/test/uploaders/s3_test.exs
@@ -11,19 +11,11 @@ defmodule Pleroma.Uploaders.S3Test do
   import Mock
   import ExUnit.CaptureLog
 
-  setup do
-    config = Config.get([Pleroma.Uploaders.S3])
-
+  clear_config([Pleroma.Uploaders.S3]) do
     Config.put([Pleroma.Uploaders.S3],
       bucket: "test_bucket",
       public_endpoint: "https://s3.amazonaws.com"
     )
-
-    on_exit(fn ->
-      Config.put([Pleroma.Uploaders.S3], config)
-    end)
-
-    :ok
   end
 
   describe "get_file/1" do
diff --git a/test/user_test.exs b/test/user_test.exs
index 8cb6567a1..27156f036 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -21,6 +21,8 @@ defmodule Pleroma.UserTest do
     :ok
   end
 
+  clear_config([:instance, :account_activation_required])
+
   describe "when tags are nil" do
     test "tagging a user" do
       user = insert(:user, %{tags: nil})
@@ -287,6 +289,9 @@ test "fetches correct profile for nickname beginning with number" do
       password_confirmation: "test",
       email: "email@example.com"
     }
+    clear_config([:instance, :autofollowed_nicknames])
+    clear_config([:instance, :welcome_message])
+    clear_config([:instance, :welcome_user_nickname])
 
     test "it autofollows accounts that are set for it" do
       user = insert(:user)
@@ -303,8 +308,6 @@ test "it autofollows accounts that are set for it" do
 
       assert User.following?(registered_user, user)
       refute User.following?(registered_user, remote_user)
-
-      Pleroma.Config.put([:instance, :autofollowed_nicknames], [])
     end
 
     test "it sends a welcome message if it is set" do
@@ -320,9 +323,6 @@ test "it sends a welcome message if it is set" do
       assert registered_user.ap_id in activity.recipients
       assert Object.normalize(activity).data["content"] =~ "cool site"
       assert activity.actor == welcome_user.ap_id
-
-      Pleroma.Config.put([:instance, :welcome_user_nickname], nil)
-      Pleroma.Config.put([:instance, :welcome_message], nil)
     end
 
     test "it requires an email, name, nickname and password, bio is optional" do
@@ -388,15 +388,8 @@ test "it ensures info is not nil" do
       email: "email@example.com"
     }
 
-    setup do
-      setting = Pleroma.Config.get([:instance, :account_activation_required])
-
-      unless setting do
-        Pleroma.Config.put([:instance, :account_activation_required], true)
-        on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
-      end
-
-      :ok
+    clear_config([:instance, :account_activation_required]) do
+      Pleroma.Config.put([:instance, :account_activation_required], true)
     end
 
     test "it creates unconfirmed user" do
@@ -1043,6 +1036,8 @@ test "hide a user's statuses from timelines and notifications" do
       [user: user]
     end
 
+    clear_config([:instance, :federating])
+
     test ".delete_user_activities deletes all create activities", %{user: user} do
       {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"})
 
@@ -1093,9 +1088,7 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
                    Pleroma.Web.ActivityPub.Publisher,
                    [:passthrough],
                    [] do
-      config_path = [:instance, :federating]
-      initial_setting = Pleroma.Config.get(config_path)
-      Pleroma.Config.put(config_path, true)
+      Pleroma.Config.put([:instance, :federating], true)
 
       {:ok, follower} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin")
       {:ok, _} = User.follow(follower, user)
@@ -1107,8 +1100,6 @@ test "it deletes a user, all follow relationships and all activities", %{user: u
                  inbox: "http://mastodon.example.org/inbox"
                })
              )
-
-      Pleroma.Config.put(config_path, initial_setting)
     end
   end
 
@@ -1174,8 +1165,6 @@ test "auth_active?/1 works correctly" do
     refute User.auth_active?(local_user)
     assert User.auth_active?(confirmed_user)
     assert User.auth_active?(remote_user)
-
-    Pleroma.Config.put([:instance, :account_activation_required], false)
   end
 
   describe "superuser?/1" do
@@ -1220,8 +1209,6 @@ test "returns false when the account is unauthenticated and auth is required" do
       other_user = insert(:user, local: true)
 
       refute User.visible_for?(user, other_user)
-
-      Pleroma.Config.put([:instance, :account_activation_required], false)
     end
 
     test "returns true when the account is unauthenticated and auth is not required" do
@@ -1238,8 +1225,6 @@ test "returns true when the account is unauthenticated and being viewed by a pri
       other_user = insert(:user, local: true, info: %{is_admin: true})
 
       assert User.visible_for?(user, other_user)
-
-      Pleroma.Config.put([:instance, :account_activation_required], false)
     end
   end
 
@@ -1552,10 +1537,7 @@ test "performs update cache if user updated" do
   end
 
   describe "following/followers synchronization" do
-    setup do
-      sync = Pleroma.Config.get([:instance, :external_user_synchronization])
-      on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end)
-    end
+    clear_config([:instance, :external_user_synchronization])
 
     test "updates the counters normally on following/getting a follow when disabled" do
       Pleroma.Config.put([:instance, :external_user_synchronization], false)
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 251055ee1..77f5e39fa 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -16,17 +16,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
     :ok
   end
 
+  clear_config_all([:instance, :federating],
+    do: Pleroma.Config.put([:instance, :federating], true)
+  )
+
   describe "/relay" do
+    clear_config([:instance, :allow_relay])
+
     test "with the relay active, it returns the relay user", %{conn: conn} do
       res =
         conn
@@ -43,8 +42,6 @@ test "with the relay disabled, it returns 404", %{conn: conn} do
       |> get(activity_pub_path(conn, :relay))
       |> json_response(404)
       |> assert
-
-      Pleroma.Config.put([:instance, :allow_relay], true)
     end
   end
 
diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs
index 19e172939..04709df17 100644
--- a/test/web/activity_pub/mrf/mrf_test.exs
+++ b/test/web/activity_pub/mrf/mrf_test.exs
@@ -1,5 +1,6 @@
 defmodule Pleroma.Web.ActivityPub.MRFTest do
   use ExUnit.Case, async: true
+  use Pleroma.Tests.Helpers
   alias Pleroma.Web.ActivityPub.MRF
 
   test "subdomains_regex/1" do
@@ -59,6 +60,8 @@ test "matches are case-insensitive" do
   end
 
   describe "describe/0" do
+    clear_config([:instance, :rewrite_policy])
+
     test "it works as expected with noop policy" do
       expected = %{
         mrf_policies: ["NoOpPolicy"],
@@ -69,7 +72,6 @@ test "it works as expected with noop policy" do
     end
 
     test "it works as expected with mock policy" do
-      config = Pleroma.Config.get([:instance, :rewrite_policy])
       Pleroma.Config.put([:instance, :rewrite_policy], [MRFModuleMock])
 
       expected = %{
@@ -79,8 +81,6 @@ test "it works as expected with mock policy" do
       }
 
       {:ok, ^expected} = MRF.describe()
-
-      Pleroma.Config.put([:instance, :rewrite_policy], config)
     end
   end
 end
diff --git a/test/web/activity_pub/mrf/reject_non_public_test.exs b/test/web/activity_pub/mrf/reject_non_public_test.exs
index fdf6b245e..fc1d190bb 100644
--- a/test/web/activity_pub/mrf/reject_non_public_test.exs
+++ b/test/web/activity_pub/mrf/reject_non_public_test.exs
@@ -8,12 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublicTest do
 
   alias Pleroma.Web.ActivityPub.MRF.RejectNonPublic
 
-  setup do
-    policy = Pleroma.Config.get([:mrf_rejectnonpublic])
-    on_exit(fn -> Pleroma.Config.put([:mrf_rejectnonpublic], policy) end)
-
-    :ok
-  end
+  clear_config([:mrf_rejectnonpublic])
 
   describe "public message" do
     test "it's allowed when address is public" do
diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs
index 8e86d2219..7203b27da 100644
--- a/test/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/web/activity_pub/mrf/simple_policy_test.exs
@@ -8,9 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
   alias Pleroma.Config
   alias Pleroma.Web.ActivityPub.MRF.SimplePolicy
 
-  setup do
-    orig = Config.get!(:mrf_simple)
-
+  clear_config([:mrf_simple]) do
     Config.put(:mrf_simple,
       media_removal: [],
       media_nsfw: [],
@@ -21,10 +19,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
       avatar_removal: [],
       banner_removal: []
     )
-
-    on_exit(fn ->
-      Config.put(:mrf_simple, orig)
-    end)
   end
 
   describe "when :media_removal" do
diff --git a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
index 6519e2398..72084c0fd 100644
--- a/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
+++ b/test/web/activity_pub/mrf/user_allowlist_policy_test.exs
@@ -7,12 +7,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicyTest do
 
   alias Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy
 
-  setup do
-    policy = Pleroma.Config.get([:mrf_user_allowlist]) || []
-    on_exit(fn -> Pleroma.Config.put([:mrf_user_allowlist], policy) end)
-
-    :ok
-  end
+  clear_config([:mrf_user_allowlist, :localhost])
 
   test "pass filter if allow list is empty" do
     actor = insert(:user)
diff --git a/test/web/activity_pub/mrf/vocabulary_policy_test.exs b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
index c3b11d7a1..38309f9f1 100644
--- a/test/web/activity_pub/mrf/vocabulary_policy_test.exs
+++ b/test/web/activity_pub/mrf/vocabulary_policy_test.exs
@@ -8,8 +8,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicyTest do
   alias Pleroma.Web.ActivityPub.MRF.VocabularyPolicy
 
   describe "accept" do
+    clear_config([:mrf_vocabulary, :accept])
+
     test "it accepts based on parent activity type" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :accept])
       Pleroma.Config.put([:mrf_vocabulary, :accept], ["Like"])
 
       message = %{
@@ -18,12 +19,9 @@ test "it accepts based on parent activity type" do
       }
 
       {:ok, ^message} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :accept], config)
     end
 
     test "it accepts based on child object type" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :accept])
       Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
 
       message = %{
@@ -35,12 +33,9 @@ test "it accepts based on child object type" do
       }
 
       {:ok, ^message} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :accept], config)
     end
 
     test "it does not accept disallowed child objects" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :accept])
       Pleroma.Config.put([:mrf_vocabulary, :accept], ["Create", "Note"])
 
       message = %{
@@ -52,12 +47,9 @@ test "it does not accept disallowed child objects" do
       }
 
       {:reject, nil} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :accept], config)
     end
 
     test "it does not accept disallowed parent types" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :accept])
       Pleroma.Config.put([:mrf_vocabulary, :accept], ["Announce", "Note"])
 
       message = %{
@@ -69,14 +61,13 @@ test "it does not accept disallowed parent types" do
       }
 
       {:reject, nil} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :accept], config)
     end
   end
 
   describe "reject" do
+    clear_config([:mrf_vocabulary, :reject])
+
     test "it rejects based on parent activity type" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :reject])
       Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
 
       message = %{
@@ -85,12 +76,9 @@ test "it rejects based on parent activity type" do
       }
 
       {:reject, nil} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :reject], config)
     end
 
     test "it rejects based on child object type" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :reject])
       Pleroma.Config.put([:mrf_vocabulary, :reject], ["Note"])
 
       message = %{
@@ -102,12 +90,9 @@ test "it rejects based on child object type" do
       }
 
       {:reject, nil} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :reject], config)
     end
 
     test "it passes through objects that aren't disallowed" do
-      config = Pleroma.Config.get([:mrf_vocabulary, :reject])
       Pleroma.Config.put([:mrf_vocabulary, :reject], ["Like"])
 
       message = %{
@@ -116,8 +101,6 @@ test "it passes through objects that aren't disallowed" do
       }
 
       {:ok, ^message} = VocabularyPolicy.filter(message)
-
-      Pleroma.Config.put([:mrf_vocabulary, :reject], config)
     end
   end
 end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index bcbc18639..844cd0732 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -294,20 +294,17 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do
 
   describe "POST /api/pleroma/admin/email_invite, with valid config" do
     setup do
-      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
-      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
-      Pleroma.Config.put([:instance, :registrations_open], false)
-      Pleroma.Config.put([:instance, :invites_enabled], true)
-
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
-        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
-        :ok
-      end)
-
       [user: insert(:user, info: %{is_admin: true})]
     end
 
+    clear_config([:instance, :registrations_open]) do
+      Pleroma.Config.put([:instance, :registrations_open], false)
+    end
+
+    clear_config([:instance, :invites_enabled]) do
+      Pleroma.Config.put([:instance, :invites_enabled], true)
+    end
+
     test "sends invitation and returns 204", %{conn: conn, user: user} do
       recipient_email = "foo@bar.com"
       recipient_name = "J. D."
@@ -360,18 +357,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do
       [user: insert(:user, info: %{is_admin: true})]
     end
 
+    clear_config([:instance, :registrations_open])
+    clear_config([:instance, :invites_enabled])
+
     test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
-      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
-      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
       Pleroma.Config.put([:instance, :registrations_open], false)
       Pleroma.Config.put([:instance, :invites_enabled], false)
 
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
-        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
-        :ok
-      end)
-
       conn =
         conn
         |> assign(:user, user)
@@ -381,17 +373,9 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u
     end
 
     test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
-      registrations_open = Pleroma.Config.get([:instance, :registrations_open])
-      invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
       Pleroma.Config.put([:instance, :registrations_open], true)
       Pleroma.Config.put([:instance, :invites_enabled], true)
 
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :registrations_open], registrations_open)
-        Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
-        :ok
-      end)
-
       conn =
         conn
         |> assign(:user, user)
@@ -1402,17 +1386,13 @@ test "with settings in db", %{conn: conn} do
         :ok = File.rm(temp_file)
       end)
 
-      dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
-      Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
-      end)
-
       %{conn: assign(conn, :user, admin)}
     end
 
+    clear_config([:instance, :dynamic_configuration]) do
+      Pleroma.Config.put([:instance, :dynamic_configuration], true)
+    end
+
     test "create new config setting in db", %{conn: conn} do
       conn =
         post(conn, "/api/pleroma/admin/config", %{
@@ -1961,17 +1941,13 @@ test "delete part of settings by atom subkeys", %{conn: conn} do
         :ok = File.rm(temp_file)
       end)
 
-      dynamic = Pleroma.Config.get([:instance, :dynamic_configuration])
-
-      Pleroma.Config.put([:instance, :dynamic_configuration], true)
-
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :dynamic_configuration], dynamic)
-      end)
-
       %{conn: assign(conn, :user, admin), admin: admin}
     end
 
+    clear_config([:instance, :dynamic_configuration]) do
+      Pleroma.Config.put([:instance, :dynamic_configuration], true)
+    end
+
     test "transfer settings to DB and to file", %{conn: conn, admin: admin} do
       assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == []
       conn = get(conn, "/api/pleroma/admin/config/migrate_to_db")
diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs
index 454523349..bcbaad665 100644
--- a/test/web/common_api/common_api_test.exs
+++ b/test/web/common_api/common_api_test.exs
@@ -14,6 +14,10 @@ defmodule Pleroma.Web.CommonAPITest do
 
   import Pleroma.Factory
 
+  clear_config([:instance, :safe_dm_mentions])
+  clear_config([:instance, :limit])
+  clear_config([:instance, :max_pinned_statuses])
+
   test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
     user = insert(:user)
     {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
@@ -61,7 +65,6 @@ test "with the safe_dm_mention option set, it does not mention people beyond the
     har = insert(:user)
     jafnhar = insert(:user)
     tridi = insert(:user)
-    option = Pleroma.Config.get([:instance, :safe_dm_mentions])
     Pleroma.Config.put([:instance, :safe_dm_mentions], true)
 
     {:ok, activity} =
@@ -72,7 +75,6 @@ test "with the safe_dm_mention option set, it does not mention people beyond the
 
     refute tridi.ap_id in activity.recipients
     assert jafnhar.ap_id in activity.recipients
-    Pleroma.Config.put([:instance, :safe_dm_mentions], option)
   end
 
   test "it de-duplicates tags" do
@@ -195,15 +197,12 @@ test "it returns error when status is empty and no attachments" do
     end
 
     test "it returns error when character limit is exceeded" do
-      limit = Pleroma.Config.get([:instance, :limit])
       Pleroma.Config.put([:instance, :limit], 5)
 
       user = insert(:user)
 
       assert {:error, "The status is over the character limit"} =
                CommonAPI.post(user, %{"status" => "foobar"})
-
-      Pleroma.Config.put([:instance, :limit], limit)
     end
   end
 
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 73cfaa8f1..09e54533f 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -13,15 +13,17 @@ defmodule Pleroma.Web.FederatorTest do
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
 
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
     :ok
   end
 
+  clear_config_all([:instance, :federating]) do
+    Pleroma.Config.put([:instance, :federating], true)
+  end
+
+  clear_config([:instance, :allow_relay])
+  clear_config([:instance, :rewrite_policy])
+  clear_config([:mrf_keyword])
+
   describe "Publisher.perform" do
     test "call `perform` with unknown task" do
       assert {
@@ -67,8 +69,6 @@ test "with relays deactivated, it does not publish to the relay", %{
       end
 
       refute_received :relay_publish
-
-      Pleroma.Config.put([:instance, :allow_relay], true)
     end
   end
 
@@ -231,19 +231,18 @@ test "rejects incoming AP docs with incorrect origin" do
     end
 
     test "it does not crash if MRF rejects the post" do
-      policies = Pleroma.Config.get([:instance, :rewrite_policy])
-      mrf_keyword_policy = Pleroma.Config.get(:mrf_keyword)
       Pleroma.Config.put([:mrf_keyword, :reject], ["lain"])
-      Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.KeywordPolicy)
+
+      Pleroma.Config.put(
+        [:instance, :rewrite_policy],
+        Pleroma.Web.ActivityPub.MRF.KeywordPolicy
+      )
 
       params =
         File.read!("test/fixtures/mastodon-post-activity.json")
         |> Poison.decode!()
 
       assert Federator.incoming_ap_doc(params) == :error
-
-      Pleroma.Config.put([:instance, :rewrite_policy], policies)
-      Pleroma.Config.put(:mrf_keyword, mrf_keyword_policy)
     end
   end
 end
diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs
index d28730994..3fd011fd3 100644
--- a/test/web/instances/instance_test.exs
+++ b/test/web/instances/instance_test.exs
@@ -10,14 +10,8 @@ defmodule Pleroma.Instances.InstanceTest do
 
   import Pleroma.Factory
 
-  setup_all do
-    config_path = [:instance, :federation_reachability_timeout_days]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, 1)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
-    :ok
+  clear_config_all([:instance, :federation_reachability_timeout_days]) do
+    Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1)
   end
 
   describe "set_reachable/1" do
diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs
index f0d84edea..dea8e2aea 100644
--- a/test/web/instances/instances_test.exs
+++ b/test/web/instances/instances_test.exs
@@ -7,14 +7,8 @@ defmodule Pleroma.InstancesTest do
 
   use Pleroma.DataCase
 
-  setup_all do
-    config_path = [:instance, :federation_reachability_timeout_days]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, 1)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
-    :ok
+  clear_config_all([:instance, :federation_reachability_timeout_days]) do
+    Pleroma.Config.put([:instance, :federation_reachability_timeout_days], 1)
   end
 
   describe "reachable?/1" do
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 112e272f9..77430e9c9 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -33,6 +33,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     :ok
   end
 
+  clear_config([:instance, :public])
+  clear_config([:rich_media, :enabled])
+
   test "the home timeline", %{conn: conn} do
     user = insert(:user)
     following = insert(:user)
@@ -86,13 +89,8 @@ test "the public timeline", %{conn: conn} do
   end
 
   test "the public timeline when public is set to false", %{conn: conn} do
-    public = Config.get([:instance, :public])
     Config.put([:instance, :public], false)
 
-    on_exit(fn ->
-      Config.put([:instance, :public], public)
-    end)
-
     assert conn
            |> get("/api/v1/timelines/public", %{"local" => "False"})
            |> json_response(403) == %{"error" => "This resource requires authentication."}
@@ -261,7 +259,6 @@ test "posting a status with OGP link preview", %{conn: conn} do
 
       assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
       assert Activity.get_by_id(id)
-      Config.put([:rich_media, :enabled], false)
     end
 
     test "posting a direct status", %{conn: conn} do
@@ -1634,14 +1631,6 @@ test "returns the relationships for the current user", %{conn: conn} do
 
   describe "media upload" do
     setup do
-      upload_config = Config.get([Pleroma.Upload])
-      proxy_config = Config.get([:media_proxy])
-
-      on_exit(fn ->
-        Config.put([Pleroma.Upload], upload_config)
-        Config.put([:media_proxy], proxy_config)
-      end)
-
       user = insert(:user)
 
       conn =
@@ -1657,6 +1646,9 @@ test "returns the relationships for the current user", %{conn: conn} do
       [conn: conn, image: image]
     end
 
+    clear_config([:media_proxy])
+    clear_config([Pleroma.Upload])
+
     test "returns uploaded image", %{conn: conn, image: image} do
       desc = "Description of the image"
 
@@ -2667,14 +2659,16 @@ test "put settings", %{conn: conn} do
 
   describe "pinned statuses" do
     setup do
-      Config.put([:instance, :max_pinned_statuses], 1)
-
       user = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"})
 
       [user: user, activity: activity]
     end
 
+    clear_config([:instance, :max_pinned_statuses]) do
+      Config.put([:instance, :max_pinned_statuses], 1)
+    end
+
     test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do
       {:ok, _} = CommonAPI.pin(activity.id, user)
 
@@ -2769,10 +2763,6 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
     setup do
       Config.put([:rich_media, :enabled], true)
 
-      on_exit(fn ->
-        Config.put([:rich_media, :enabled], false)
-      end)
-
       user = insert(:user)
       %{user: user}
     end
@@ -3127,15 +3117,12 @@ test "redirects not logged-in users to the login page on private instances", %{
       conn: conn,
       path: path
     } do
-      is_public = Config.get([:instance, :public])
       Config.put([:instance, :public], false)
 
       conn = get(conn, path)
 
       assert conn.status == 302
       assert redirected_to(conn) == "/web/login"
-
-      Config.put([:instance, :public], is_public)
     end
 
     test "does not redirect logged in users to the login page", %{conn: conn, path: path} do
@@ -3910,13 +3897,6 @@ test "it returns 400 when user is not local", %{conn: conn, user: user} do
 
   describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
     setup do
-      setting = Config.get([:instance, :account_activation_required])
-
-      unless setting do
-        Config.put([:instance, :account_activation_required], true)
-        on_exit(fn -> Config.put([:instance, :account_activation_required], setting) end)
-      end
-
       user = insert(:user)
       info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
 
@@ -3931,6 +3911,10 @@ test "it returns 400 when user is not local", %{conn: conn, user: user} do
       [user: user]
     end
 
+    clear_config([:instance, :account_activation_required]) do
+      Config.put([:instance, :account_activation_required], true)
+    end
+
     test "resend account confirmation email", %{conn: conn, user: user} do
       conn
       |> assign(:user, user)
@@ -3953,9 +3937,6 @@ test "resend account confirmation email", %{conn: conn, user: user} do
     setup do
       user = insert(:user)
       other_user = insert(:user)
-      config = Config.get(:suggestions)
-      on_exit(fn -> Config.put(:suggestions, config) end)
-
       host = Config.get([Pleroma.Web.Endpoint, :url, :host])
       url500 = "http://test500?#{host}&#{user.nickname}"
       url200 = "http://test200?#{host}&#{user.nickname}"
@@ -3977,6 +3958,8 @@ test "resend account confirmation email", %{conn: conn, user: user} do
       [user: user, other_user: other_user]
     end
 
+    clear_config(:suggestions)
+
     test "returns empty result when suggestions disabled", %{conn: conn, user: user} do
       Config.put([:suggestions, :enabled], false)
 
diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs
index 0c94755df..79699cac5 100644
--- a/test/web/media_proxy/media_proxy_test.exs
+++ b/test/web/media_proxy/media_proxy_test.exs
@@ -4,14 +4,11 @@
 
 defmodule Pleroma.Web.MediaProxyTest do
   use ExUnit.Case
+  use Pleroma.Tests.Helpers
   import Pleroma.Web.MediaProxy
   alias Pleroma.Web.MediaProxy.MediaProxyController
 
-  setup do
-    enabled = Pleroma.Config.get([:media_proxy, :enabled])
-    on_exit(fn -> Pleroma.Config.put([:media_proxy, :enabled], enabled) end)
-    :ok
-  end
+  clear_config([:media_proxy, :enabled])
 
   describe "when enabled" do
     setup do
diff --git a/test/web/oauth/ldap_authorization_test.exs b/test/web/oauth/ldap_authorization_test.exs
index 0eb191c76..1cbe133b7 100644
--- a/test/web/oauth/ldap_authorization_test.exs
+++ b/test/web/oauth/ldap_authorization_test.exs
@@ -12,21 +12,12 @@ defmodule Pleroma.Web.OAuth.LDAPAuthorizationTest do
 
   @skip if !Code.ensure_loaded?(:eldap), do: :skip
 
-  setup_all do
-    ldap_authenticator =
-      Pleroma.Config.get(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.PleromaAuthenticator)
-
-    ldap_enabled = Pleroma.Config.get([:ldap, :enabled])
-
-    on_exit(fn ->
-      Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, ldap_authenticator)
-      Pleroma.Config.put([:ldap, :enabled], ldap_enabled)
-    end)
-
-    Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator)
+  clear_config_all([:ldap, :enabled]) do
     Pleroma.Config.put([:ldap, :enabled], true)
+  end
 
-    :ok
+  clear_config_all(Pleroma.Web.Auth.Authenticator) do
+    Pleroma.Config.put(Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator)
   end
 
   @tag @skip
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index 92e156347..b492c7794 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -11,23 +11,15 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
   alias Pleroma.Web.OAuth.OAuthController
   alias Pleroma.Web.OAuth.Token
 
-  @oauth_config_path [:oauth2, :issue_new_refresh_token]
   @session_opts [
     store: :cookie,
     key: "_test",
     signing_salt: "cooldude"
   ]
+  clear_config_all([:instance, :account_activation_required])
 
   describe "in OAuth consumer mode, " do
     setup do
-      oauth_consumer_strategies_path = [:auth, :oauth_consumer_strategies]
-      oauth_consumer_strategies = Pleroma.Config.get(oauth_consumer_strategies_path)
-      Pleroma.Config.put(oauth_consumer_strategies_path, ~w(twitter facebook))
-
-      on_exit(fn ->
-        Pleroma.Config.put(oauth_consumer_strategies_path, oauth_consumer_strategies)
-      end)
-
       [
         app: insert(:oauth_app),
         conn:
@@ -37,6 +29,13 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       ]
     end
 
+    clear_config([:auth, :oauth_consumer_strategies]) do
+      Pleroma.Config.put(
+        [:auth, :oauth_consumer_strategies],
+        ~w(twitter facebook)
+      )
+    end
+
     test "GET /oauth/authorize renders auth forms, including OAuth consumer form", %{
       app: app,
       conn: conn
@@ -775,12 +774,7 @@ test "rejects token exchange with invalid client credentials" do
     end
 
     test "rejects token exchange for valid credentials belonging to unconfirmed user and confirmation is required" do
-      setting = Pleroma.Config.get([:instance, :account_activation_required])
-
-      unless setting do
-        Pleroma.Config.put([:instance, :account_activation_required], true)
-        on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
-      end
+      Pleroma.Config.put([:instance, :account_activation_required], true)
 
       password = "testpassword"
       user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password))
@@ -857,16 +851,10 @@ test "rejects an invalid authorization code" do
   end
 
   describe "POST /oauth/token - refresh token" do
-    setup do
-      oauth_token_config = Pleroma.Config.get(@oauth_config_path)
-
-      on_exit(fn ->
-        Pleroma.Config.get(@oauth_config_path, oauth_token_config)
-      end)
-    end
+    clear_config([:oauth2, :issue_new_refresh_token])
 
     test "issues a new access token with keep fresh token" do
-      Pleroma.Config.put(@oauth_config_path, true)
+      Pleroma.Config.put([:oauth2, :issue_new_refresh_token], true)
       user = insert(:user)
       app = insert(:oauth_app, scopes: ["read", "write"])
 
@@ -906,7 +894,7 @@ test "issues a new access token with keep fresh token" do
     end
 
     test "issues a new access token with new fresh token" do
-      Pleroma.Config.put(@oauth_config_path, false)
+      Pleroma.Config.put([:oauth2, :issue_new_refresh_token], false)
       user = insert(:user)
       app = insert(:oauth_app, scopes: ["read", "write"])
 
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index 9f756effb..095ae7041 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -15,16 +15,13 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
 
   setup_all do
     Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
     :ok
   end
 
+  clear_config_all([:instance, :federating]) do
+    Pleroma.Config.put([:instance, :federating], true)
+  end
+
   describe "salmon_incoming" do
     test "decodes a salmon", %{conn: conn} do
       user = insert(:user)
diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs
index c01e01124..bb2e1687a 100644
--- a/test/web/plugs/federating_plug_test.exs
+++ b/test/web/plugs/federating_plug_test.exs
@@ -4,15 +4,7 @@
 
 defmodule Pleroma.Web.FederatingPlugTest do
   use Pleroma.Web.ConnCase
-
-  setup_all do
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
-    :ok
-  end
+  clear_config_all([:instance, :federating])
 
   test "returns and halt the conn when federating is disabled" do
     Pleroma.Config.put([:instance, :federating], false)
diff --git a/test/web/rich_media/helpers_test.exs b/test/web/rich_media/helpers_test.exs
index 92198f3d9..48884319d 100644
--- a/test/web/rich_media/helpers_test.exs
+++ b/test/web/rich_media/helpers_test.exs
@@ -15,12 +15,12 @@ defmodule Pleroma.Web.RichMedia.HelpersTest do
 
   setup do
     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
-    rich_media = Config.get([:rich_media, :enabled])
-    on_exit(fn -> Config.put([:rich_media, :enabled], rich_media) end)
 
     :ok
   end
 
+  clear_config([:rich_media, :enabled])
+
   test "refuses to crawl incomplete URLs" do
     user = insert(:user)
 
diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs
index 5b7fe44d4..96fa7645f 100644
--- a/test/web/streamer_test.exs
+++ b/test/web/streamer_test.exs
@@ -11,15 +11,7 @@ defmodule Pleroma.Web.StreamerTest do
   alias Pleroma.Web.Streamer
   import Pleroma.Factory
 
-  setup do
-    skip_thread_containment = Pleroma.Config.get([:instance, :skip_thread_containment])
-
-    on_exit(fn ->
-      Pleroma.Config.put([:instance, :skip_thread_containment], skip_thread_containment)
-    end)
-
-    :ok
-  end
+  clear_config_all([:instance, :skip_thread_containment])
 
   describe "user streams" do
     setup do
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index 8bb8aa36d..8ef14b4c5 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -151,6 +151,7 @@ test "with credentials", %{conn: conn, user: user} do
 
   describe "GET /statuses/public_timeline.json" do
     setup [:valid_user]
+    clear_config([:instance, :public])
 
     test "returns statuses", %{conn: conn} do
       user = insert(:user)
@@ -173,8 +174,6 @@ test "returns 403 to unauthenticated request when the instance is not public", %
       conn
       |> get("/api/statuses/public_timeline.json")
       |> json_response(403)
-
-      Pleroma.Config.put([:instance, :public], true)
     end
 
     test "returns 200 to authenticated request when the instance is not public",
@@ -185,8 +184,6 @@ test "returns 200 to authenticated request when the instance is not public",
       |> with_credentials(user.nickname, "test")
       |> get("/api/statuses/public_timeline.json")
       |> json_response(200)
-
-      Pleroma.Config.put([:instance, :public], true)
     end
 
     test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
@@ -220,6 +217,7 @@ test "returns 200 to authenticated request when the instance is public",
 
   describe "GET /statuses/public_and_external_timeline.json" do
     setup [:valid_user]
+    clear_config([:instance, :public])
 
     test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do
       Pleroma.Config.put([:instance, :public], false)
@@ -227,8 +225,6 @@ test "returns 403 to unauthenticated request when the instance is not public", %
       conn
       |> get("/api/statuses/public_and_external_timeline.json")
       |> json_response(403)
-
-      Pleroma.Config.put([:instance, :public], true)
     end
 
     test "returns 200 to authenticated request when the instance is not public",
@@ -239,8 +235,6 @@ test "returns 200 to authenticated request when the instance is not public",
       |> with_credentials(user.nickname, "test")
       |> get("/api/statuses/public_and_external_timeline.json")
       |> json_response(200)
-
-      Pleroma.Config.put([:instance, :public], true)
     end
 
     test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do
@@ -1176,13 +1170,6 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do
 
   describe "POST /api/account/resend_confirmation_email" do
     setup do
-      setting = Pleroma.Config.get([:instance, :account_activation_required])
-
-      unless setting do
-        Pleroma.Config.put([:instance, :account_activation_required], true)
-        on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end)
-      end
-
       user = insert(:user)
       info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true)
 
@@ -1197,6 +1184,10 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do
       [user: user]
     end
 
+    clear_config([:instance, :account_activation_required]) do
+      Pleroma.Config.put([:instance, :account_activation_required], true)
+    end
+
     test "it returns 204 No Content", %{conn: conn, user: user} do
       conn
       |> assign(:user, user)
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index 640579c09..fe4ffdb59 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -14,20 +14,13 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
 
   setup do
     Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
-    instance_config = Pleroma.Config.get([:instance])
-    pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
-    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
-
-    on_exit(fn ->
-      Pleroma.Config.put([:instance], instance_config)
-      Pleroma.Config.put([:frontend_configurations, :pleroma_fe], pleroma_fe)
-      Pleroma.Config.put([:user, :deny_follow_blocked], deny_follow_blocked)
-    end)
-
     :ok
   end
 
+  clear_config([:instance])
+  clear_config([:frontend_configurations, :pleroma_fe])
+  clear_config([:user, :deny_follow_blocked])
+
   describe "POST /api/pleroma/follow_import" do
     test "it returns HTTP 200", %{conn: conn} do
       user1 = insert(:user)
@@ -260,7 +253,6 @@ test "it returns config in json format", %{conn: conn} do
     end
 
     test "returns the state of safe_dm_mentions flag", %{conn: conn} do
-      option = Pleroma.Config.get([:instance, :safe_dm_mentions])
       Pleroma.Config.put([:instance, :safe_dm_mentions], true)
 
       response =
@@ -278,8 +270,6 @@ test "returns the state of safe_dm_mentions flag", %{conn: conn} do
         |> json_response(:ok)
 
       assert response["site"]["safeDMMentionsEnabled"] == "0"
-
-      Pleroma.Config.put([:instance, :safe_dm_mentions], option)
     end
 
     test "it returns the managed config", %{conn: conn} do
@@ -534,15 +524,7 @@ test "returns error when user is blocked", %{conn: conn} do
   end
 
   describe "GET /api/pleroma/healthcheck" do
-    setup do
-      config_healthcheck = Pleroma.Config.get([:instance, :healthcheck])
-
-      on_exit(fn ->
-        Pleroma.Config.put([:instance, :healthcheck], config_healthcheck)
-      end)
-
-      :ok
-    end
+    clear_config([:instance, :healthcheck])
 
     test "returns 503 when healthcheck disabled", %{conn: conn} do
       Pleroma.Config.put([:instance, :healthcheck], false)
diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs
index 7d861cbf5..e23086b2a 100644
--- a/test/web/web_finger/web_finger_controller_test.exs
+++ b/test/web/web_finger/web_finger_controller_test.exs
@@ -10,15 +10,13 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
 
   setup do
     mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
-
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
     :ok
   end
 
+  clear_config_all([:instance, :federating]) do
+    Pleroma.Config.put([:instance, :federating], true)
+  end
+
   test "GET host-meta" do
     response =
       build_conn()
diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs
index aa7262beb..59cacbe68 100644
--- a/test/web/websub/websub_controller_test.exs
+++ b/test/web/websub/websub_controller_test.exs
@@ -9,14 +9,8 @@ defmodule Pleroma.Web.Websub.WebsubControllerTest do
   alias Pleroma.Web.Websub
   alias Pleroma.Web.Websub.WebsubClientSubscription
 
-  setup_all do
-    config_path = [:instance, :federating]
-    initial_setting = Pleroma.Config.get(config_path)
-
-    Pleroma.Config.put(config_path, true)
-    on_exit(fn -> Pleroma.Config.put(config_path, initial_setting) end)
-
-    :ok
+  clear_config_all([:instance, :federating]) do
+    Pleroma.Config.put([:instance, :federating], true)
   end
 
   test "websub subscription request", %{conn: conn} do

From 75a5dd41ee4a1c196487f4cf2759a4d63bc393ef Mon Sep 17 00:00:00 2001
From: Sergey Suprunenko <suprunenko.s@gmail.com>
Date: Mon, 19 Aug 2019 16:10:00 +0000
Subject: [PATCH 40/40] Add more tests for Database tasks and DigestEmailWorker

---
 lib/pleroma/digest_email_worker.ex            |  4 ++
 test/tasks/database_test.exs                  | 47 +++++++++++++++++++
 .../digest_test.exs}                          |  0
 test/web/digest_email_worker_test.exs         | 31 ++++++++++++
 4 files changed, 82 insertions(+)
 rename test/{mix/tasks/pleroma.digest_test.exs => tasks/digest_test.exs} (100%)
 create mode 100644 test/web/digest_email_worker_test.exs

diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex
index 18e67d39b..5644d6a67 100644
--- a/lib/pleroma/digest_email_worker.ex
+++ b/lib/pleroma/digest_email_worker.ex
@@ -1,3 +1,7 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
 defmodule Pleroma.DigestEmailWorker do
   import Ecto.Query
 
diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs
index a8f25f500..a9925c361 100644
--- a/test/tasks/database_test.exs
+++ b/test/tasks/database_test.exs
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 
 defmodule Mix.Tasks.Pleroma.DatabaseTest do
+  alias Pleroma.Activity
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
@@ -22,6 +23,52 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
     :ok
   end
 
+  describe "running remove_embedded_objects" do
+    test "it replaces objects with references" do
+      user = insert(:user)
+      {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
+      new_data = Map.put(activity.data, "object", activity.object.data)
+
+      {:ok, activity} =
+        activity
+        |> Activity.change(%{data: new_data})
+        |> Repo.update()
+
+      assert is_map(activity.data["object"])
+
+      Mix.Tasks.Pleroma.Database.run(["remove_embedded_objects"])
+
+      activity = Activity.get_by_id_with_object(activity.id)
+      assert is_binary(activity.data["object"])
+    end
+  end
+
+  describe "prune_objects" do
+    test "it prunes old objects from the database" do
+      insert(:note)
+      deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
+
+      date =
+        Timex.now()
+        |> Timex.shift(days: -deadline)
+        |> Timex.to_naive_datetime()
+        |> NaiveDateTime.truncate(:second)
+
+      %{id: id} =
+        :note
+        |> insert()
+        |> Ecto.Changeset.change(%{inserted_at: date})
+        |> Repo.update!()
+
+      assert length(Repo.all(Object)) == 2
+
+      Mix.Tasks.Pleroma.Database.run(["prune_objects"])
+
+      assert length(Repo.all(Object)) == 1
+      refute Object.get_by_id(id)
+    end
+  end
+
   describe "running update_users_following_followers_counts" do
     test "following and followers count are updated" do
       [user, user2] = insert_pair(:user)
diff --git a/test/mix/tasks/pleroma.digest_test.exs b/test/tasks/digest_test.exs
similarity index 100%
rename from test/mix/tasks/pleroma.digest_test.exs
rename to test/tasks/digest_test.exs
diff --git a/test/web/digest_email_worker_test.exs b/test/web/digest_email_worker_test.exs
new file mode 100644
index 000000000..15002330f
--- /dev/null
+++ b/test/web/digest_email_worker_test.exs
@@ -0,0 +1,31 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.DigestEmailWorkerTest do
+  use Pleroma.DataCase
+  import Pleroma.Factory
+
+  alias Pleroma.DigestEmailWorker
+  alias Pleroma.User
+  alias Pleroma.Web.CommonAPI
+
+  test "it sends digest emails" do
+    user = insert(:user)
+
+    date =
+      Timex.now()
+      |> Timex.shift(days: -10)
+      |> Timex.to_naive_datetime()
+
+    user2 = insert(:user, last_digest_emailed_at: date)
+    User.switch_email_notifications(user2, "digest", true)
+    CommonAPI.post(user, %{"status" => "hey @#{user2.nickname}!"})
+
+    DigestEmailWorker.perform()
+
+    assert_received {:email, email}
+    assert email.to == [{user2.name, user2.email}]
+    assert email.subject == "Your digest from #{Pleroma.Config.get(:instance)[:name]}"
+  end
+end