From 1a4875adfa8fa8f65f1db7b4ec3cf868b7e3dee7 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Tue, 7 Apr 2020 21:52:32 +0300
Subject: [PATCH 01/11] [#1559] Support for "follow_request" notifications
 (configurable). (Not currently supported by PleromaFE, thus disabled by
 default).

---
 CHANGELOG.md                                  |  1 +
 config/config.exs                             |  2 +
 config/description.exs                        | 14 ++++
 lib/pleroma/activity.ex                       | 36 +++++++--
 lib/pleroma/notification.ex                   | 11 ++-
 lib/pleroma/user.ex                           |  2 +
 .../mastodon_api/views/notification_view.ex   |  3 +
 lib/pleroma/web/push/impl.ex                  | 76 ++++++++++++-------
 lib/pleroma/web/push/subscription.ex          |  8 ++
 9 files changed, 117 insertions(+), 36 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6e5d807c..b3b63ac54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - NodeInfo: `pleroma_emoji_reactions` to the `features` list.
 - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
 - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
+- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
 <details>
   <summary>API Changes</summary>
 - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
diff --git a/config/config.exs b/config/config.exs
index 232a91bf1..d40c2240b 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -559,6 +559,8 @@
     inactivity_threshold: 7
   }
 
+config :pleroma, :notifications, enable_follow_request_notifications: false
+
 config :pleroma, :oauth2,
   token_expires_in: 600,
   issue_new_refresh_token: true,
diff --git a/config/description.exs b/config/description.exs
index 642f1a3ce..b1938912c 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2267,6 +2267,20 @@
       }
     ]
   },
+  %{
+    group: :pleroma,
+    key: :notifications,
+    type: :group,
+    description: "Notification settings",
+    children: [
+      %{
+        key: :enable_follow_request_notifications,
+        type: :boolean,
+        description:
+          "Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
+      }
+    ]
+  },
   %{
     group: :pleroma,
     key: Pleroma.Emails.UserEmail,
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 5a8329e69..3803d8e50 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
   # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
   @mastodon_notification_types %{
     "Create" => "mention",
-    "Follow" => "follow",
+    "Follow" => ["follow", "follow_request"],
     "Announce" => "reblog",
     "Like" => "favourite",
     "Move" => "move",
     "EmojiReact" => "pleroma:emoji_reaction"
   }
 
-  @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
-                                         into: %{},
-                                         do: {v, k}
-
   schema "activities" do
     field(:data, :map)
     field(:local, :boolean, default: true)
@@ -291,15 +287,41 @@ defp purge_web_resp_cache(%Activity{} = activity) do
 
   defp purge_web_resp_cache(nil), do: nil
 
-  for {ap_type, type} <- @mastodon_notification_types do
+  def follow_accepted?(
+        %Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
+      ) do
+    with %User{} = follower <- Activity.user_actor(activity),
+         %User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
+      Pleroma.FollowingRelationship.following?(follower, followed)
+    else
+      _ -> false
+    end
+  end
+
+  def follow_accepted?(_), do: false
+
+  for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
     def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
       do: unquote(type)
   end
 
+  def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
+    if follow_accepted?(activity) do
+      "follow"
+    else
+      "follow_request"
+    end
+  end
+
   def mastodon_notification_type(%Activity{}), do: nil
 
   def from_mastodon_notification_type(type) do
-    Map.get(@mastodon_to_ap_notification_types, type)
+    with {k, _v} <-
+           Enum.find(@mastodon_notification_types, fn {_k, v} ->
+             v == type or (is_list(v) and type in v)
+           end) do
+      k
+    end
   end
 
   def all_by_actor_and_id(actor, status_ids \\ [])
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 04ee510b9..73e19bf97 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -284,8 +284,17 @@ def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = act
     end
   end
 
+  def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
+    if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
+         Activity.follow_accepted?(activity) do
+      do_create_notifications(activity)
+    else
+      {:ok, []}
+    end
+  end
+
   def create_notifications(%Activity{data: %{"type" => type}} = activity)
-      when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
+      when type in ["Like", "Announce", "Move", "EmojiReact"] do
     do_create_notifications(activity)
   end
 
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 71c8c3a4e..ac2594417 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -699,6 +699,8 @@ def needs_update?(%User{local: false} = user) do
   def needs_update?(_), do: true
 
   @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
+
+  # "Locked" (self-locked) users demand explicit authorization of follow requests
   def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
     follow(follower, followed, "pending")
   end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index ae87d4701..feed47129 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -116,6 +116,9 @@ def render(
         "follow" ->
           response
 
+        "follow_request" ->
+          response
+
         "pleroma:emoji_reaction" ->
           response
           |> put_status(parent_activity_fn.(), reading_user, render_opts)
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index afa510f08..89d45b2e1 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
   require Logger
   import Ecto.Query
 
+  defdelegate mastodon_notification_type(activity), to: Activity
+
   @types ["Create", "Follow", "Announce", "Like", "Move"]
 
   @doc "Performs sending notifications for user subscriptions"
@@ -24,32 +26,32 @@ def perform(
         %{
           activity: %{data: %{"type" => activity_type}} = activity,
           user: %User{id: user_id}
-        } = notif
+        } = notification
       )
       when activity_type in @types do
-    actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
+    actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
 
-    type = Activity.mastodon_notification_type(notif.activity)
+    mastodon_type = mastodon_notification_type(notification.activity)
     gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
     avatar_url = User.avatar_url(actor)
     object = Object.normalize(activity)
     user = User.get_cached_by_id(user_id)
     direct_conversation_id = Activity.direct_conversation_id(activity, user)
 
-    for subscription <- fetch_subsriptions(user_id),
-        get_in(subscription.data, ["alerts", type]) do
+    for subscription <- fetch_subscriptions(user_id),
+        Subscription.enabled?(subscription, mastodon_type) do
       %{
         access_token: subscription.token.token,
-        notification_id: notif.id,
-        notification_type: type,
+        notification_id: notification.id,
+        notification_type: mastodon_type,
         icon: avatar_url,
         preferred_locale: "en",
         pleroma: %{
-          activity_id: notif.activity.id,
+          activity_id: notification.activity.id,
           direct_conversation_id: direct_conversation_id
         }
       }
-      |> Map.merge(build_content(notif, actor, object))
+      |> Map.merge(build_content(notification, actor, object, mastodon_type))
       |> Jason.encode!()
       |> push_message(build_sub(subscription), gcm_api_key, subscription)
     end
@@ -82,7 +84,7 @@ def push_message(body, sub, api_key, subscription) do
   end
 
   @doc "Gets user subscriptions"
-  def fetch_subsriptions(user_id) do
+  def fetch_subscriptions(user_id) do
     Subscription
     |> where(user_id: ^user_id)
     |> preload(:token)
@@ -99,28 +101,36 @@ def build_sub(subscription) do
     }
   end
 
+  def build_content(notification, actor, object, mastodon_type \\ nil)
+
   def build_content(
         %{
           activity: %{data: %{"directMessage" => true}},
           user: %{notification_settings: %{privacy_option: true}}
         },
         actor,
-        _
+        _object,
+        _mastodon_type
       ) do
     %{title: "New Direct Message", body: "@#{actor.nickname}"}
   end
 
-  def build_content(notif, actor, object) do
+  def build_content(notification, actor, object, mastodon_type) do
+    mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
     %{
-      title: format_title(notif),
-      body: format_body(notif, actor, object)
+      title: format_title(notification, mastodon_type),
+      body: format_body(notification, actor, object, mastodon_type)
     }
   end
 
+  def format_body(activity, actor, object, mastodon_type \\ nil)
+
   def format_body(
         %{activity: %{data: %{"type" => "Create"}}},
         actor,
-        %{data: %{"content" => content}}
+        %{data: %{"content" => content}},
+        _mastodon_type
       ) do
     "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
   end
@@ -128,33 +138,43 @@ def format_body(
   def format_body(
         %{activity: %{data: %{"type" => "Announce"}}},
         actor,
-        %{data: %{"content" => content}}
+        %{data: %{"content" => content}},
+        _mastodon_type
       ) do
     "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
   end
 
   def format_body(
-        %{activity: %{data: %{"type" => type}}},
+        %{activity: %{data: %{"type" => type}}} = notification,
         actor,
-        _object
+        _object,
+        mastodon_type
       )
       when type in ["Follow", "Like"] do
-    case type do
-      "Follow" -> "@#{actor.nickname} has followed you"
-      "Like" -> "@#{actor.nickname} has favorited your post"
+    mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
+    case {type, mastodon_type} do
+      {"Follow", "follow"} -> "@#{actor.nickname} has followed you"
+      {"Follow", "follow_request"} -> "@#{actor.nickname} has requested to follow you"
+      {"Like", _} -> "@#{actor.nickname} has favorited your post"
     end
   end
 
-  def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
+  def format_title(activity, mastodon_type \\ nil)
+
+  def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
     "New Direct Message"
   end
 
-  def format_title(%{activity: %{data: %{"type" => type}}}) do
-    case type do
-      "Create" -> "New Mention"
-      "Follow" -> "New Follower"
-      "Announce" -> "New Repeat"
-      "Like" -> "New Favorite"
+  def format_title(%{activity: %{data: %{"type" => type}}} = notification, mastodon_type) do
+    mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+
+    case {type, mastodon_type} do
+      {"Create", _} -> "New Mention"
+      {"Follow", "follow"} -> "New Follower"
+      {"Follow", "follow_request"} -> "New Follow Request"
+      {"Announce", _} -> "New Repeat"
+      {"Like", _} -> "New Favorite"
     end
   end
 end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
index 5c448d6c9..b99b0c5fb 100644
--- a/lib/pleroma/web/push/subscription.ex
+++ b/lib/pleroma/web/push/subscription.ex
@@ -32,6 +32,14 @@ defp alerts(%{"data" => %{"alerts" => alerts}}) do
     %{"alerts" => alerts}
   end
 
+  def enabled?(subscription, "follow_request") do
+    enabled?(subscription, "follow")
+  end
+
+  def enabled?(subscription, alert_type) do
+    get_in(subscription.data, ["alerts", alert_type])
+  end
+
   def create(
         %User{} = user,
         %Token{} = token,

From f35c28bf070014dfba4b988bfc47fbf93baef81f Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Wed, 8 Apr 2020 21:26:22 +0300
Subject: [PATCH 02/11] [#1559] Added / fixed tests for follow / follow_request
 notifications.

---
 test/notification_test.exs | 80 +++++++++++++++++++++++++++++++++-----
 1 file changed, 70 insertions(+), 10 deletions(-)

diff --git a/test/notification_test.exs b/test/notification_test.exs
index 837a9dacd..0877aaaaf 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -11,8 +11,10 @@ defmodule Pleroma.NotificationTest do
   alias Pleroma.Notification
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.Push
   alias Pleroma.Web.Streamer
 
@@ -272,16 +274,6 @@ test "it doesn't create a notification for user if he is the activity author" do
       refute Notification.create_notification(activity, author)
     end
 
-    test "it doesn't create a notification for follow-unfollow-follow chains" do
-      user = insert(:user)
-      followed_user = insert(:user)
-      {:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
-      Notification.create_notification(activity, followed_user)
-      CommonAPI.unfollow(user, followed_user)
-      {:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
-      refute Notification.create_notification(activity_dupe, followed_user)
-    end
-
     test "it doesn't create duplicate notifications for follow+subscribed users" do
       user = insert(:user)
       subscriber = insert(:user)
@@ -304,6 +296,74 @@ test "it doesn't create subscription notifications if the recipient cannot see t
     end
   end
 
+  describe "follow / follow_request notifications" do
+    test "it creates `follow` notification for approved Follow activity" do
+      user = insert(:user)
+      followed_user = insert(:user, locked: false)
+
+      {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
+      assert FollowingRelationship.following?(user, followed_user)
+      assert [notification] = Notification.for_user(followed_user)
+
+      assert %{type: "follow"} =
+               NotificationView.render("show.json", %{
+                 notification: notification,
+                 for: followed_user
+               })
+    end
+
+    test "if `follow_request` notifications are enabled, " <>
+           "it creates `follow_request` notification for pending Follow activity" do
+      clear_config([:notifications, :enable_follow_request_notifications], true)
+      user = insert(:user)
+      followed_user = insert(:user, locked: true)
+
+      {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
+      refute FollowingRelationship.following?(user, followed_user)
+      assert [notification] = Notification.for_user(followed_user)
+
+      render_opts = %{notification: notification, for: followed_user}
+      assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts)
+
+      # After request is accepted, the same notification is rendered with type "follow":
+      assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
+
+      notification_id = notification.id
+      assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
+      assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
+    end
+
+    test "if `follow_request` notifications are disabled, " <>
+           "it does NOT create `follow*` notification for pending Follow activity" do
+      clear_config([:notifications, :enable_follow_request_notifications], false)
+      user = insert(:user)
+      followed_user = insert(:user, locked: true)
+
+      {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
+      refute FollowingRelationship.following?(user, followed_user)
+      assert [] = Notification.for_user(followed_user)
+
+      # After request is accepted, no new notifications are generated:
+      assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
+      assert [] = Notification.for_user(followed_user)
+    end
+
+    test "it doesn't create a notification for follow-unfollow-follow chains" do
+      user = insert(:user)
+      followed_user = insert(:user, locked: false)
+
+      {:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
+      assert FollowingRelationship.following?(user, followed_user)
+      assert [notification] = Notification.for_user(followed_user)
+
+      CommonAPI.unfollow(user, followed_user)
+      {:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
+
+      notification_id = notification.id
+      assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
+    end
+  end
+
   describe "get notification" do
     test "it gets a notification that belongs to the user" do
       user = insert(:user)

From 3965772b261e78669441a5bf3a597f1a69f78a7f Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Wed, 8 Apr 2020 21:33:37 +0300
Subject: [PATCH 03/11] [#1559] Minor change (analysis).

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

diff --git a/test/notification_test.exs b/test/notification_test.exs
index 0877aaaaf..a7f53e319 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -8,10 +8,10 @@ defmodule Pleroma.NotificationTest do
   import Pleroma.Factory
   import Mock
 
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Notification
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
-  alias Pleroma.FollowingRelationship
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.NotificationView

From ac672a9d6bfdd3cba7692f80a883bd38b0b09a57 Mon Sep 17 00:00:00 2001
From: Ivan Tashkinov <ivantashkinov@gmail.com>
Date: Thu, 9 Apr 2020 15:13:37 +0300
Subject: [PATCH 04/11] [#1559] Addressed code review requests.

---
 lib/pleroma/activity.ex                       |  8 +++---
 .../mastodon_api/views/notification_view.ex   |  9 +++----
 lib/pleroma/web/push/impl.ex                  | 25 ++++++++++---------
 3 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index 3803d8e50..6213d0eb7 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -300,6 +300,8 @@ def follow_accepted?(
 
   def follow_accepted?(_), do: false
 
+  @spec mastodon_notification_type(Activity.t()) :: String.t() | nil
+
   for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
     def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
       do: unquote(type)
@@ -315,11 +317,11 @@ def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity
 
   def mastodon_notification_type(%Activity{}), do: nil
 
+  @spec from_mastodon_notification_type(String.t()) :: String.t() | nil
+  @doc "Converts Mastodon notification type to AR activity type"
   def from_mastodon_notification_type(type) do
     with {k, _v} <-
-           Enum.find(@mastodon_notification_types, fn {_k, v} ->
-             v == type or (is_list(v) and type in v)
-           end) do
+           Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
       k
     end
   end
diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex
index feed47129..7001fd7b9 100644
--- a/lib/pleroma/web/mastodon_api/views/notification_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex
@@ -113,17 +113,14 @@ def render(
         "move" ->
           put_target(response, activity, reading_user, render_opts)
 
-        "follow" ->
-          response
-
-        "follow_request" ->
-          response
-
         "pleroma:emoji_reaction" ->
           response
           |> put_status(parent_activity_fn.(), reading_user, render_opts)
           |> put_emoji(activity)
 
+        type when type in ["follow", "follow_request"] ->
+          response
+
         _ ->
           nil
       end
diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex
index 89d45b2e1..f1740a6e0 100644
--- a/lib/pleroma/web/push/impl.ex
+++ b/lib/pleroma/web/push/impl.ex
@@ -153,10 +153,10 @@ def format_body(
       when type in ["Follow", "Like"] do
     mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
 
-    case {type, mastodon_type} do
-      {"Follow", "follow"} -> "@#{actor.nickname} has followed you"
-      {"Follow", "follow_request"} -> "@#{actor.nickname} has requested to follow you"
-      {"Like", _} -> "@#{actor.nickname} has favorited your post"
+    case mastodon_type do
+      "follow" -> "@#{actor.nickname} has followed you"
+      "follow_request" -> "@#{actor.nickname} has requested to follow you"
+      "favourite" -> "@#{actor.nickname} has favorited your post"
     end
   end
 
@@ -166,15 +166,16 @@ def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_typ
     "New Direct Message"
   end
 
-  def format_title(%{activity: %{data: %{"type" => type}}} = notification, mastodon_type) do
-    mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
+  def format_title(%{activity: activity}, mastodon_type) do
+    mastodon_type = mastodon_type || mastodon_notification_type(activity)
 
-    case {type, mastodon_type} do
-      {"Create", _} -> "New Mention"
-      {"Follow", "follow"} -> "New Follower"
-      {"Follow", "follow_request"} -> "New Follow Request"
-      {"Announce", _} -> "New Repeat"
-      {"Like", _} -> "New Favorite"
+    case mastodon_type do
+      "mention" -> "New Mention"
+      "follow" -> "New Follower"
+      "follow_request" -> "New Follow Request"
+      "reblog" -> "New Repeat"
+      "favourite" -> "New Favorite"
+      type -> "New #{String.capitalize(type || "event")}"
     end
   end
 end

From ed894802d5dfe60072b9445cb28e7b474a9f393b Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Apr 2020 18:46:47 -0500
Subject: [PATCH 05/11] Expand MRF SimplePolicy docs

---
 docs/configuration/mrf.md | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/docs/configuration/mrf.md b/docs/configuration/mrf.md
index c3957c255..9f13c3d18 100644
--- a/docs/configuration/mrf.md
+++ b/docs/configuration/mrf.md
@@ -41,11 +41,14 @@ config :pleroma, :instance,
 
 Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
 
-* `media_removal`: Servers in this group will have media stripped from incoming messages.
-* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
 * `reject`: Servers in this group will have their messages rejected.
-* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
+* `accept`: If not empty, only messages from these instances will be accepted (whitelist federation).
+* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
+* `media_removal`: Servers in this group will have media stripped from incoming messages.
+* `avatar_removal`: Avatars from these servers will be stripped from incoming messages.
+* `banner_removal`: Banner images from these servers will be stripped from incoming messages.
 * `report_removal`: Servers in this group will have their reports (flags) rejected.
+* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
 
 Servers should be configured as lists.
 

From 9a3c74b244bce6097a8c6da99692bfc9973e1ec8 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Sun, 12 Apr 2020 20:26:35 -0500
Subject: [PATCH 06/11] Always accept deletions through SimplePolicy

---
 .../web/activity_pub/mrf/simple_policy.ex     |  3 +++
 .../activity_pub/mrf/simple_policy_test.exs   | 23 +++++++++++++++++++
 2 files changed, 26 insertions(+)

diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 4edc007fd..b23f263f5 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -148,6 +148,9 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image
 
   defp check_banner_removal(_actor_info, object), do: {:ok, object}
 
+  @impl true
+  def filter(%{"type" => "Delete"} = object), do: {:ok, object}
+
   @impl true
   def filter(%{"actor" => actor} = object) do
     actor_info = URI.parse(actor)
diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs
index 91c24c2d9..eaa595706 100644
--- a/test/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/web/activity_pub/mrf/simple_policy_test.exs
@@ -258,6 +258,14 @@ test "actor has a matching host" do
 
       assert SimplePolicy.filter(remote_user) == {:reject, nil}
     end
+
+    test "always accept deletions" do
+      Config.put([:mrf_simple, :reject], ["remote.instance"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
   end
 
   describe "when :accept" do
@@ -308,6 +316,14 @@ test "actor has a matching host" do
 
       assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
     end
+
+    test "always accept deletions" do
+      Config.put([:mrf_simple, :accept], ["non.matching.remote"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
   end
 
   describe "when :avatar_removal" do
@@ -408,4 +424,11 @@ defp build_remote_user do
       "type" => "Person"
     }
   end
+
+  defp build_remote_deletion_message do
+    %{
+      "type" => "Delete",
+      "actor" => "https://remote.instance/users/bob"
+    }
+  end
 end

From cf4ebba77471f188ce7da45df0b9ea76dbe31916 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Wed, 15 Apr 2020 22:59:25 +0400
Subject: [PATCH 07/11] Cleanup SubscriptionController

---
 .../controllers/subscription_controller.ex    | 34 ++++++++++---------
 ...scription_view.ex => subscription_view.ex} |  4 +--
 .../subscription_controller_test.exs          | 13 ++++---
 ...ew_test.exs => subscription_view_test.exs} |  6 ++--
 4 files changed, 31 insertions(+), 26 deletions(-)
 rename lib/pleroma/web/mastodon_api/views/{push_subscription_view.ex => subscription_view.ex} (77%)
 rename test/web/mastodon_api/views/{push_subscription_view_test.exs => subscription_view_test.exs} (72%)

diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
index 11df6fc4a..4647c1f96 100644
--- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex
@@ -6,25 +6,22 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
   @moduledoc "The module represents functions to manage user subscriptions."
   use Pleroma.Web, :controller
 
-  alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
   alias Pleroma.Web.Push
   alias Pleroma.Web.Push.Subscription
 
   action_fallback(:errors)
 
   plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
-
   plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
+  plug(:restrict_push_enabled)
 
   # Creates PushSubscription
   # POST /api/v1/push/subscription
   #
   def create(%{assigns: %{user: user, token: token}} = conn, params) do
-    with true <- Push.enabled(),
-         {:ok, _} <- Subscription.delete_if_exists(user, token),
+    with {:ok, _} <- Subscription.delete_if_exists(user, token),
          {:ok, subscription} <- Subscription.create(user, token, params) do
-      view = View.render("push_subscription.json", subscription: subscription)
-      json(conn, view)
+      render(conn, "show.json", subscription: subscription)
     end
   end
 
@@ -32,10 +29,8 @@ def create(%{assigns: %{user: user, token: token}} = conn, params) do
   # GET /api/v1/push/subscription
   #
   def get(%{assigns: %{user: user, token: token}} = conn, _params) do
-    with true <- Push.enabled(),
-         {:ok, subscription} <- Subscription.get(user, token) do
-      view = View.render("push_subscription.json", subscription: subscription)
-      json(conn, view)
+    with {:ok, subscription} <- Subscription.get(user, token) do
+      render(conn, "show.json", subscription: subscription)
     end
   end
 
@@ -43,10 +38,8 @@ def get(%{assigns: %{user: user, token: token}} = conn, _params) do
   # PUT /api/v1/push/subscription
   #
   def update(%{assigns: %{user: user, token: token}} = conn, params) do
-    with true <- Push.enabled(),
-         {:ok, subscription} <- Subscription.update(user, token, params) do
-      view = View.render("push_subscription.json", subscription: subscription)
-      json(conn, view)
+    with {:ok, subscription} <- Subscription.update(user, token, params) do
+      render(conn, "show.json", subscription: subscription)
     end
   end
 
@@ -54,11 +47,20 @@ def update(%{assigns: %{user: user, token: token}} = conn, params) do
   # DELETE /api/v1/push/subscription
   #
   def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
-    with true <- Push.enabled(),
-         {:ok, _response} <- Subscription.delete(user, token),
+    with {:ok, _response} <- Subscription.delete(user, token),
          do: json(conn, %{})
   end
 
+  defp restrict_push_enabled(conn, _) do
+    if Push.enabled() do
+      conn
+    else
+      conn
+      |> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
+      |> halt()
+    end
+  end
+
   # fallback action
   #
   def errors(conn, {:error, :not_found}) do
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
similarity index 77%
rename from lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
rename to lib/pleroma/web/mastodon_api/views/subscription_view.ex
index d32cef6e2..7c67cc924 100644
--- a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/subscription_view.ex
@@ -2,11 +2,11 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
   use Pleroma.Web, :view
   alias Pleroma.Web.Push
 
-  def render("push_subscription.json", %{subscription: subscription}) do
+  def render("show.json", %{subscription: subscription}) do
     %{
       id: to_string(subscription.id),
       endpoint: subscription.endpoint,
diff --git a/test/web/mastodon_api/controllers/subscription_controller_test.exs b/test/web/mastodon_api/controllers/subscription_controller_test.exs
index 987158a74..5682498c0 100644
--- a/test/web/mastodon_api/controllers/subscription_controller_test.exs
+++ b/test/web/mastodon_api/controllers/subscription_controller_test.exs
@@ -35,7 +35,10 @@ defmacro assert_error_when_disable_push(do: yield) do
     quote do
       vapid_details = Application.get_env(:web_push_encryption, :vapid_details, [])
       Application.put_env(:web_push_encryption, :vapid_details, [])
-      assert "Something went wrong" == unquote(yield)
+
+      assert %{"error" => "Web push subscription is disabled on this Pleroma instance"} ==
+               unquote(yield)
+
       Application.put_env(:web_push_encryption, :vapid_details, vapid_details)
     end
   end
@@ -45,7 +48,7 @@ test "returns error when push disabled ", %{conn: conn} do
       assert_error_when_disable_push do
         conn
         |> post("/api/v1/push/subscription", %{})
-        |> json_response(500)
+        |> json_response(403)
       end
     end
 
@@ -74,7 +77,7 @@ test "returns error when push disabled ", %{conn: conn} do
       assert_error_when_disable_push do
         conn
         |> get("/api/v1/push/subscription", %{})
-        |> json_response(500)
+        |> json_response(403)
       end
     end
 
@@ -127,7 +130,7 @@ test "returns error when push disabled ", %{conn: conn} do
       assert_error_when_disable_push do
         conn
         |> put("/api/v1/push/subscription", %{data: %{"alerts" => %{"mention" => false}}})
-        |> json_response(500)
+        |> json_response(403)
       end
     end
 
@@ -155,7 +158,7 @@ test "returns error when push disabled ", %{conn: conn} do
       assert_error_when_disable_push do
         conn
         |> delete("/api/v1/push/subscription", %{})
-        |> json_response(500)
+        |> json_response(403)
       end
     end
 
diff --git a/test/web/mastodon_api/views/push_subscription_view_test.exs b/test/web/mastodon_api/views/subscription_view_test.exs
similarity index 72%
rename from test/web/mastodon_api/views/push_subscription_view_test.exs
rename to test/web/mastodon_api/views/subscription_view_test.exs
index 10c6082a5..981524c0e 100644
--- a/test/web/mastodon_api/views/push_subscription_view_test.exs
+++ b/test/web/mastodon_api/views/subscription_view_test.exs
@@ -2,10 +2,10 @@
 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 # SPDX-License-Identifier: AGPL-3.0-only
 
-defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do
+defmodule Pleroma.Web.MastodonAPI.SubscriptionViewTest do
   use Pleroma.DataCase
   import Pleroma.Factory
-  alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
+  alias Pleroma.Web.MastodonAPI.SubscriptionView, as: View
   alias Pleroma.Web.Push
 
   test "Represent a subscription" do
@@ -18,6 +18,6 @@ test "Represent a subscription" do
       server_key: Keyword.get(Push.vapid_config(), :public_key)
     }
 
-    assert expected == View.render("push_subscription.json", %{subscription: subscription})
+    assert expected == View.render("show.json", %{subscription: subscription})
   end
 end

From c906ffc51a3c36213f2c10d6d20046beb32e5d10 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sun, 19 Apr 2020 20:23:48 +0200
Subject: [PATCH 08/11] =?UTF-8?q?mix.exs:=20Do=20not=20bail=20out=20when?=
 =?UTF-8?q?=20.git=20doesn=E2=80=99t=20exists?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 mix.exs | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/mix.exs b/mix.exs
index c5e5fd432..b76aef180 100644
--- a/mix.exs
+++ b/mix.exs
@@ -221,19 +221,26 @@ defp version(version) do
     identifier_filter = ~r/[^0-9a-z\-]+/i
 
     # Pre-release version, denoted from patch version with a hyphen
+    {tag, tag_err} =
+      System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
+
+    {describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
+    {commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
+
     git_pre_release =
-      with {tag, 0} <-
-             System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
-           {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
-        describe
-        |> String.trim()
-        |> String.replace(String.trim(tag), "")
-        |> String.trim_leading("-")
-        |> String.trim()
-      else
-        _ ->
-          {commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
+      cond do
+        tag_err == 0 and describe_err == 0 ->
+          describe
+          |> String.trim()
+          |> String.replace(String.trim(tag), "")
+          |> String.trim_leading("-")
+          |> String.trim()
+
+        commit_hash_err == 0 ->
           "0-g" <> String.trim(commit_hash)
+
+        true ->
+          ""
       end
 
     # Branch name as pre-release version component, denoted with a dot
@@ -251,6 +258,8 @@ defp version(version) do
           |> String.replace(identifier_filter, "-")
 
         branch_name
+      else
+        _ -> "stable"
       end
 
     build_name =

From 258d8975797298b9eadddb48a8a2fcf3a9dbf211 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn <egor@kislitsyn.com>
Date: Mon, 20 Apr 2020 16:38:00 +0400
Subject: [PATCH 09/11] Cleanup and DRY the Router

---
 lib/pleroma/web/router.ex | 113 ++++++++++++++------------------------
 1 file changed, 40 insertions(+), 73 deletions(-)

diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 7e5960949..153802a43 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -16,79 +16,60 @@ defmodule Pleroma.Web.Router do
     plug(Pleroma.Plugs.UserEnabledPlug)
   end
 
-  pipeline :api do
-    plug(:accepts, ["json"])
-    plug(:fetch_session)
+  pipeline :authenticate do
     plug(Pleroma.Plugs.OAuthPlug)
     plug(Pleroma.Plugs.BasicAuthDecoderPlug)
     plug(Pleroma.Plugs.UserFetcherPlug)
     plug(Pleroma.Plugs.SessionAuthenticationPlug)
     plug(Pleroma.Plugs.LegacyAuthenticationPlug)
     plug(Pleroma.Plugs.AuthenticationPlug)
+  end
+
+  pipeline :after_auth do
     plug(Pleroma.Plugs.UserEnabledPlug)
     plug(Pleroma.Plugs.SetUserSessionIdPlug)
     plug(Pleroma.Plugs.EnsureUserKeyPlug)
-    plug(Pleroma.Plugs.IdempotencyPlug)
+  end
+
+  pipeline :base_api do
+    plug(:accepts, ["json"])
+    plug(:fetch_session)
+    plug(:authenticate)
     plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
+  pipeline :api do
+    plug(:base_api)
+    plug(:after_auth)
+    plug(Pleroma.Plugs.IdempotencyPlug)
+  end
+
   pipeline :authenticated_api do
-    plug(:accepts, ["json"])
-    plug(:fetch_session)
+    plug(:base_api)
     plug(Pleroma.Plugs.AuthExpectedPlug)
-    plug(Pleroma.Plugs.OAuthPlug)
-    plug(Pleroma.Plugs.BasicAuthDecoderPlug)
-    plug(Pleroma.Plugs.UserFetcherPlug)
-    plug(Pleroma.Plugs.SessionAuthenticationPlug)
-    plug(Pleroma.Plugs.LegacyAuthenticationPlug)
-    plug(Pleroma.Plugs.AuthenticationPlug)
-    plug(Pleroma.Plugs.UserEnabledPlug)
-    plug(Pleroma.Plugs.SetUserSessionIdPlug)
+    plug(:after_auth)
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
-    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :admin_api do
-    plug(:accepts, ["json"])
-    plug(:fetch_session)
-    plug(Pleroma.Plugs.OAuthPlug)
-    plug(Pleroma.Plugs.BasicAuthDecoderPlug)
-    plug(Pleroma.Plugs.UserFetcherPlug)
-    plug(Pleroma.Plugs.SessionAuthenticationPlug)
-    plug(Pleroma.Plugs.LegacyAuthenticationPlug)
-    plug(Pleroma.Plugs.AuthenticationPlug)
+    plug(:base_api)
     plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
-    plug(Pleroma.Plugs.UserEnabledPlug)
-    plug(Pleroma.Plugs.SetUserSessionIdPlug)
+    plug(:after_auth)
     plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
     plug(Pleroma.Plugs.UserIsAdminPlug)
     plug(Pleroma.Plugs.IdempotencyPlug)
-    plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
   end
 
   pipeline :mastodon_html do
-    plug(:accepts, ["html"])
-    plug(:fetch_session)
-    plug(Pleroma.Plugs.OAuthPlug)
-    plug(Pleroma.Plugs.BasicAuthDecoderPlug)
-    plug(Pleroma.Plugs.UserFetcherPlug)
-    plug(Pleroma.Plugs.SessionAuthenticationPlug)
-    plug(Pleroma.Plugs.LegacyAuthenticationPlug)
-    plug(Pleroma.Plugs.AuthenticationPlug)
-    plug(Pleroma.Plugs.UserEnabledPlug)
-    plug(Pleroma.Plugs.SetUserSessionIdPlug)
-    plug(Pleroma.Plugs.EnsureUserKeyPlug)
+    plug(:browser)
+    plug(:authenticate)
+    plug(:after_auth)
   end
 
   pipeline :pleroma_html do
-    plug(:accepts, ["html"])
-    plug(:fetch_session)
-    plug(Pleroma.Plugs.OAuthPlug)
-    plug(Pleroma.Plugs.BasicAuthDecoderPlug)
-    plug(Pleroma.Plugs.UserFetcherPlug)
-    plug(Pleroma.Plugs.SessionAuthenticationPlug)
-    plug(Pleroma.Plugs.AuthenticationPlug)
+    plug(:browser)
+    plug(:authenticate)
     plug(Pleroma.Plugs.EnsureUserKeyPlug)
   end
 
@@ -515,7 +496,7 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/api" do
-    pipe_through(:api)
+    pipe_through(:base_api)
 
     get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
   end
@@ -529,10 +510,6 @@ defmodule Pleroma.Web.Router do
     post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
   end
 
-  pipeline :ap_service_actor do
-    plug(:accepts, ["activity+json", "json"])
-  end
-
   pipeline :ostatus do
     plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
     plug(Pleroma.Plugs.StaticFEPlug)
@@ -543,8 +520,7 @@ defmodule Pleroma.Web.Router do
   end
 
   scope "/", Pleroma.Web do
-    pipe_through(:ostatus)
-    pipe_through(:http_signature)
+    pipe_through([:ostatus, :http_signature])
 
     get("/objects/:uuid", OStatus.OStatusController, :object)
     get("/activities/:uuid", OStatus.OStatusController, :activity)
@@ -562,13 +538,6 @@ defmodule Pleroma.Web.Router do
     get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
   end
 
-  # Server to Server (S2S) AP interactions
-  pipeline :activitypub do
-    plug(:accepts, ["activity+json", "json"])
-    plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
-    plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
-  end
-
   scope "/", Pleroma.Web.ActivityPub do
     # XXX: not really ostatus
     pipe_through(:ostatus)
@@ -576,19 +545,22 @@ defmodule Pleroma.Web.Router do
     get("/users/:nickname/outbox", ActivityPubController, :outbox)
   end
 
+  pipeline :ap_service_actor do
+    plug(:accepts, ["activity+json", "json"])
+  end
+
+  # Server to Server (S2S) AP interactions
+  pipeline :activitypub do
+    plug(:ap_service_actor)
+    plug(:http_signature)
+  end
+
   # Client to Server (C2S) AP interactions
   pipeline :activitypub_client do
-    plug(:accepts, ["activity+json", "json"])
+    plug(:ap_service_actor)
     plug(:fetch_session)
-    plug(Pleroma.Plugs.OAuthPlug)
-    plug(Pleroma.Plugs.BasicAuthDecoderPlug)
-    plug(Pleroma.Plugs.UserFetcherPlug)
-    plug(Pleroma.Plugs.SessionAuthenticationPlug)
-    plug(Pleroma.Plugs.LegacyAuthenticationPlug)
-    plug(Pleroma.Plugs.AuthenticationPlug)
-    plug(Pleroma.Plugs.UserEnabledPlug)
-    plug(Pleroma.Plugs.SetUserSessionIdPlug)
-    plug(Pleroma.Plugs.EnsureUserKeyPlug)
+    plug(:authenticate)
+    plug(:after_auth)
   end
 
   scope "/", Pleroma.Web.ActivityPub do
@@ -660,12 +632,7 @@ defmodule Pleroma.Web.Router do
     get("/web/*path", MastoFEController, :index)
   end
 
-  pipeline :remote_media do
-  end
-
   scope "/proxy/", Pleroma.Web.MediaProxy do
-    pipe_through(:remote_media)
-
     get("/:sig/:url", MediaProxyController, :remote)
     get("/:sig/:url/:filename", MediaProxyController, :remote)
   end

From 8b4de61d6449f70e0a5e84be3082724c7f50ffee Mon Sep 17 00:00:00 2001
From: Ilja <pleroma@spectraltheorem.be>
Date: Mon, 20 Apr 2020 12:59:16 +0000
Subject: [PATCH 10/11] Fix ObjectAgePolicyTest

The policy didn't block old posts as it should.
* I fixed it and tested on a test server
* I added the settings to description so that this information is shown in nodeinfo
* TODO: I didn't work TTD and still need to fix the tests
---
 CHANGELOG.md                                  |  2 +
 .../web/activity_pub/mrf/object_age_policy.ex | 10 +++-
 .../mrf/object_age_policy_test.exs            | 52 ++++++++++---------
 3 files changed, 37 insertions(+), 27 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9270e801f..e454bd9d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,8 +28,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 ### Fixed
 - Logger configuration through AdminFE
 - HTTP Basic Authentication permissions issue
+- ObjectAgePolicy didn't filter out old messages
 
 ### Added
+- NodeInfo: ObjectAgePolicy settings to the `federation` list.
 <details>
   <summary>API Changes</summary>
 - Admin API: `GET /api/pleroma/admin/need_reboot`.
diff --git a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
index 4a8bc91ae..b0ccb63c8 100644
--- a/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/object_age_policy.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
   @moduledoc "Filter activities depending on their age"
   @behaviour Pleroma.Web.ActivityPub.MRF
 
-  defp check_date(%{"published" => published} = message) do
+  defp check_date(%{"object" => %{"published" => published}} = message) do
     with %DateTime{} = now <- DateTime.utc_now(),
          {:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
          max_ttl <- Config.get([:mrf_object_age, :threshold]),
@@ -96,5 +96,11 @@ def filter(%{"type" => "Create", "published" => _} = message) do
   def filter(message), do: {:ok, message}
 
   @impl true
-  def describe, do: {:ok, %{}}
+  def describe do
+    mrf_object_age =
+      Pleroma.Config.get(:mrf_object_age)
+      |> Enum.into(%{})
+
+    {:ok, %{mrf_object_age: mrf_object_age}}
+  end
 end
diff --git a/test/web/activity_pub/mrf/object_age_policy_test.exs b/test/web/activity_pub/mrf/object_age_policy_test.exs
index 7ee195eeb..b0fb753bd 100644
--- a/test/web/activity_pub/mrf/object_age_policy_test.exs
+++ b/test/web/activity_pub/mrf/object_age_policy_test.exs
@@ -20,26 +20,38 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicyTest do
     :ok
   end
 
+  defp get_old_message do
+    File.read!("test/fixtures/mastodon-post-activity.json")
+    |> Poison.decode!()
+  end
+
+  defp get_new_message do
+    old_message = get_old_message()
+
+    new_object =
+      old_message
+      |> Map.get("object")
+      |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+
+    old_message
+    |> Map.put("object", new_object)
+  end
+
   describe "with reject action" do
     test "it rejects an old post" do
       Config.put([:mrf_object_age, :actions], [:reject])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
+      data = get_old_message()
 
-      {:reject, _} = ObjectAgePolicy.filter(data)
+      assert match?({:reject, _}, ObjectAgePolicy.filter(data))
     end
 
     test "it allows a new post" do
       Config.put([:mrf_object_age, :actions], [:reject])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
-        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+      data = get_new_message()
 
-      {:ok, _} = ObjectAgePolicy.filter(data)
+      assert match?({:ok, _}, ObjectAgePolicy.filter(data))
     end
   end
 
@@ -47,9 +59,7 @@ test "it allows a new post" do
     test "it delists an old post" do
       Config.put([:mrf_object_age, :actions], [:delist])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
+      data = get_old_message()
 
       {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
 
@@ -61,14 +71,11 @@ test "it delists an old post" do
     test "it allows a new post" do
       Config.put([:mrf_object_age, :actions], [:delist])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
-        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+      data = get_new_message()
 
       {:ok, _user} = User.get_or_fetch_by_ap_id(data["actor"])
 
-      {:ok, ^data} = ObjectAgePolicy.filter(data)
+      assert match?({:ok, ^data}, ObjectAgePolicy.filter(data))
     end
   end
 
@@ -76,9 +83,7 @@ test "it allows a new post" do
     test "it strips followers collections from an old post" do
       Config.put([:mrf_object_age, :actions], [:strip_followers])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
+      data = get_old_message()
 
       {:ok, user} = User.get_or_fetch_by_ap_id(data["actor"])
 
@@ -91,14 +96,11 @@ test "it strips followers collections from an old post" do
     test "it allows a new post" do
       Config.put([:mrf_object_age, :actions], [:strip_followers])
 
-      data =
-        File.read!("test/fixtures/mastodon-post-activity.json")
-        |> Poison.decode!()
-        |> Map.put("published", DateTime.utc_now() |> DateTime.to_iso8601())
+      data = get_new_message()
 
       {:ok, _u} = User.get_or_fetch_by_ap_id(data["actor"])
 
-      {:ok, ^data} = ObjectAgePolicy.filter(data)
+      assert match?({:ok, ^data}, ObjectAgePolicy.filter(data))
     end
   end
 end

From b54c8813d632cb44c7deb207e91bd32f01f33794 Mon Sep 17 00:00:00 2001
From: Alex Gleason <alex@alexgleason.me>
Date: Mon, 13 Apr 2020 13:48:32 -0500
Subject: [PATCH 11/11] Add :reject_deletes option to SimplePolicy

---
 CHANGELOG.md                                  |  2 +
 config/config.exs                             |  3 +-
 config/description.exs                        | 10 ++-
 docs/configuration/mrf.md                     |  3 +-
 .../web/activity_pub/mrf/simple_policy.ex     | 14 +++-
 .../activity_pub/mrf/simple_policy_test.exs   | 79 +++++++++++++++----
 6 files changed, 89 insertions(+), 22 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36897503a..cd2536f9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - NodeInfo: `pleroma_emoji_reactions` to the `features` list.
 - Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
 - New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma won’t start. For hackney OTP update is not required.
+- Added `:reject_deletes` group to SimplePolicy
 <details>
   <summary>API Changes</summary>
 - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
@@ -20,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 - Support pagination in conversations API
+- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
 
 ## [unreleased-patch]
 
diff --git a/config/config.exs b/config/config.exs
index 232a91bf1..9a6b93a37 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -334,7 +334,8 @@
   reject: [],
   accept: [],
   avatar_removal: [],
-  banner_removal: []
+  banner_removal: [],
+  reject_deletes: []
 
 config :pleroma, :mrf_keyword,
   reject: [],
diff --git a/config/description.exs b/config/description.exs
index 642f1a3ce..9d8e3b93c 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1317,13 +1317,13 @@
       %{
         key: :reject,
         type: {:list, :string},
-        description: "List of instances to reject any activities from",
+        description: "List of instances to reject activities from (except deletes)",
         suggestions: ["example.com", "*.example.com"]
       },
       %{
         key: :accept,
         type: {:list, :string},
-        description: "List of instances to accept any activities from",
+        description: "List of instances to only accept activities from (except deletes)",
         suggestions: ["example.com", "*.example.com"]
       },
       %{
@@ -1343,6 +1343,12 @@
         type: {:list, :string},
         description: "List of instances to strip banners from",
         suggestions: ["example.com", "*.example.com"]
+      },
+      %{
+        key: :reject_deletes,
+        type: {:list, :string},
+        description: "List of instances to reject deletions from",
+        suggestions: ["example.com", "*.example.com"]
       }
     ]
   },
diff --git a/docs/configuration/mrf.md b/docs/configuration/mrf.md
index c3957c255..2eb9631bd 100644
--- a/docs/configuration/mrf.md
+++ b/docs/configuration/mrf.md
@@ -43,9 +43,10 @@ Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_si
 
 * `media_removal`: Servers in this group will have media stripped from incoming messages.
 * `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
-* `reject`: Servers in this group will have their messages rejected.
+* `reject`: Servers in this group will have their messages (except deletions) rejected.
 * `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
 * `report_removal`: Servers in this group will have their reports (flags) rejected.
+* `reject_deletes`: Deletion requests will be rejected from these servers.
 
 Servers should be configured as lists.
 
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index b23f263f5..b7dcb1b86 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -149,7 +149,19 @@ defp check_banner_removal(%{host: actor_host} = _actor_info, %{"image" => _image
   defp check_banner_removal(_actor_info, object), do: {:ok, object}
 
   @impl true
-  def filter(%{"type" => "Delete"} = object), do: {:ok, object}
+  def filter(%{"type" => "Delete", "actor" => actor} = object) do
+    %{host: actor_host} = URI.parse(actor)
+
+    reject_deletes =
+      Pleroma.Config.get([:mrf_simple, :reject_deletes])
+      |> MRF.subdomains_regex()
+
+    if MRF.subdomain_match?(reject_deletes, actor_host) do
+      {:reject, nil}
+    else
+      {:ok, object}
+    end
+  end
 
   @impl true
   def filter(%{"actor" => actor} = object) do
diff --git a/test/web/activity_pub/mrf/simple_policy_test.exs b/test/web/activity_pub/mrf/simple_policy_test.exs
index eaa595706..b7b9bc6a2 100644
--- a/test/web/activity_pub/mrf/simple_policy_test.exs
+++ b/test/web/activity_pub/mrf/simple_policy_test.exs
@@ -17,7 +17,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
             reject: [],
             accept: [],
             avatar_removal: [],
-            banner_removal: []
+            banner_removal: [],
+            reject_deletes: []
           )
 
   describe "when :media_removal" do
@@ -258,14 +259,6 @@ test "actor has a matching host" do
 
       assert SimplePolicy.filter(remote_user) == {:reject, nil}
     end
-
-    test "always accept deletions" do
-      Config.put([:mrf_simple, :reject], ["remote.instance"])
-
-      deletion_message = build_remote_deletion_message()
-
-      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
-    end
   end
 
   describe "when :accept" do
@@ -316,14 +309,6 @@ test "actor has a matching host" do
 
       assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
     end
-
-    test "always accept deletions" do
-      Config.put([:mrf_simple, :accept], ["non.matching.remote"])
-
-      deletion_message = build_remote_deletion_message()
-
-      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
-    end
   end
 
   describe "when :avatar_removal" do
@@ -398,6 +383,66 @@ test "match with wildcard domain" do
     end
   end
 
+  describe "when :reject_deletes is empty" do
+    setup do: Config.put([:mrf_simple, :reject_deletes], [])
+
+    test "it accepts deletions even from rejected servers" do
+      Config.put([:mrf_simple, :reject], ["remote.instance"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
+
+    test "it accepts deletions even from non-whitelisted servers" do
+      Config.put([:mrf_simple, :accept], ["non.matching.remote"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
+  end
+
+  describe "when :reject_deletes is not empty but it doesn't have a matching host" do
+    setup do: Config.put([:mrf_simple, :reject_deletes], ["non.matching.remote"])
+
+    test "it accepts deletions even from rejected servers" do
+      Config.put([:mrf_simple, :reject], ["remote.instance"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
+
+    test "it accepts deletions even from non-whitelisted servers" do
+      Config.put([:mrf_simple, :accept], ["non.matching.remote"])
+
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:ok, deletion_message}
+    end
+  end
+
+  describe "when :reject_deletes has a matching host" do
+    setup do: Config.put([:mrf_simple, :reject_deletes], ["remote.instance"])
+
+    test "it rejects the deletion" do
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:reject, nil}
+    end
+  end
+
+  describe "when :reject_deletes match with wildcard domain" do
+    setup do: Config.put([:mrf_simple, :reject_deletes], ["*.remote.instance"])
+
+    test "it rejects the deletion" do
+      deletion_message = build_remote_deletion_message()
+
+      assert SimplePolicy.filter(deletion_message) == {:reject, nil}
+    end
+  end
+
   defp build_local_message do
     %{
       "actor" => "#{Pleroma.Web.base_url()}/users/alice",