From 75169a45f6738e87018d3556425700a7b9071ba2 Mon Sep 17 00:00:00 2001
From: Ilja <pleroma@spectraltheorem.be>
Date: Sun, 8 May 2022 18:10:40 +0000
Subject: [PATCH] Also use actor_type to determine if an account is a bot in
 antiFollowbotPolicy

---
 CHANGELOG.md                                  |  1 +
 docs/configuration/cheatsheet.md              |  1 +
 .../activity_pub/mrf/anti_followbot_policy.ex | 21 +++++--
 .../mrf/anti_followbot_policy_test.exs        | 57 +++++++++++++++----
 4 files changed, 65 insertions(+), 15 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc5abb9f1..19bccd115 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -97,6 +97,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Improved Twittercard and OpenGraph meta tag generation including thumbnails and image dimension metadata when available.
 - AdminAPI: sort users so the newest are at the top.
 - ActivityPub Client-to-Server(C2S): Limitation on the type of Activity/Object are lifted as they are now passed through ObjectValidators
+- MRF (`AntiFollowbotPolicy`): Bot accounts are now also considered followbots. Users can still allow bots to follow them by first following the bot.
 
 ### Added
 
diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md
index 4dacdc68c..1e74d40e6 100644
--- a/docs/configuration/cheatsheet.md
+++ b/docs/configuration/cheatsheet.md
@@ -125,6 +125,7 @@ To add configuration to your config file, you can copy it from the base config.
     * `Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy`: Sets a default expiration on all posts made by users of the local instance. Requires `Pleroma.Workers.PurgeExpiredActivity` to be enabled for processing the scheduled delections.
     * `Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy`: Makes all bot posts to disappear from public timelines.
     * `Pleroma.Web.ActivityPub.MRF.FollowBotPolicy`: Automatically follows newly discovered users from the specified bot account. Local accounts, locked accounts, and users with "#nobot" in their bio are respected and excluded from being followed.
+    * `Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy`: Drops follow requests from followbots. Users can still allow bots to follow them by first following the bot.
     * `Pleroma.Web.ActivityPub.MRF.KeywordPolicy`: Rejects or removes from the federated timeline or replaces keywords. (See [`:mrf_keyword`](#mrf_keyword)).
     * `Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent`: Forces every mentioned user to be reflected in the post content.
 * `transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
index 605ca9971..97d75ecf2 100644
--- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
@@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
   defp score_displayname("fedibot"), do: 1.0
   defp score_displayname(_), do: 0.0
 
-  defp determine_if_followbot(%User{nickname: nickname, name: displayname}) do
+  defp determine_if_followbot(%User{nickname: nickname, name: displayname, actor_type: actor_type}) do
     # nickname will be a binary string except when following a relay
     nick_score =
       if is_binary(nickname) do
@@ -45,19 +45,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
         0.0
       end
 
-    nick_score + name_score
+    # actor_type "Service" is a Bot account
+    actor_type_score =
+      if actor_type == "Service" do
+        1.0
+      else
+        0.0
+      end
+
+    nick_score + name_score + actor_type_score
   end
 
   defp determine_if_followbot(_), do: 0.0
 
+  defp bot_allowed?(%{"object" => target}, bot_actor) do
+    %User{} = user = normalize_by_ap_id(target)
+
+    User.following?(user, bot_actor)
+  end
+
   @impl true
   def filter(%{"type" => "Follow", "actor" => actor_id} = message) do
     %User{} = actor = normalize_by_ap_id(actor_id)
 
     score = determine_if_followbot(actor)
 
-    # TODO: scan biography data for keywords and score it somehow.
-    if score < 0.8 do
+    if score < 0.8 || bot_allowed?(message, actor) do
       {:ok, message}
     else
       {:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
diff --git a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs
index 8192629c6..732a5a7f3 100644
--- a/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/anti_followbot_policy_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
   use Pleroma.DataCase, async: true
   import Pleroma.Factory
 
+  alias Pleroma.User
   alias Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy
 
   describe "blocking based on attributes" do
@@ -38,21 +39,55 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
 
       assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
     end
+
+    test "matches followbots by actor_type" do
+      actor = insert(:user, %{actor_type: "Service"})
+      target = insert(:user)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
+    end
   end
 
-  test "it allows non-followbots" do
-    actor = insert(:user)
-    target = insert(:user)
+  describe "it allows" do
+    test "non-followbots" do
+      actor = insert(:user)
+      target = insert(:user)
 
-    message = %{
-      "@context" => "https://www.w3.org/ns/activitystreams",
-      "type" => "Follow",
-      "actor" => actor.ap_id,
-      "object" => target.ap_id,
-      "id" => "https://example.com/activities/1234"
-    }
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
 
-    {:ok, _} = AntiFollowbotPolicy.filter(message)
+      {:ok, _} = AntiFollowbotPolicy.filter(message)
+    end
+
+    test "bots if the target follows the bots" do
+      actor = insert(:user, %{actor_type: "Service"})
+      target = insert(:user)
+
+      User.follow(target, actor)
+
+      message = %{
+        "@context" => "https://www.w3.org/ns/activitystreams",
+        "type" => "Follow",
+        "actor" => actor.ap_id,
+        "object" => target.ap_id,
+        "id" => "https://example.com/activities/1234"
+      }
+
+      {:ok, _} = AntiFollowbotPolicy.filter(message)
+    end
   end
 
   test "it gracefully handles nil display names" do
