total rebase
[anni] / static / modules / high_roller_policy.ex
old mode 100755 (executable)
new mode 100644 (file)
index 50b21ea..01ed179
@@ -1,4 +1,4 @@
-# Three-in-one policy for block, report and unfollow notifications.
+# Five-in-one policy for block, report, unfollow and follow reject/remove notifications.
 # Credits for the individual parts:
 # Yukkuri for the combined block/report MRF
 # https://gitlab.eientei.org/eientei/pleroma/-/blob/eientei/lib/pleroma/web/activity_pub/mrf/block_bot_policy.ex
@@ -8,27 +8,57 @@
 # https://eientei.org/notice/AL6nnjih8H6Lco8QoS
 # Pete for the example of Cachex-based rate limiting
 # https://freespeechextremist.com/objects/9f24a3e4-2e34-4fcb-a0d1-42229e27da3e
+# Phnt for force follower removal MRF
+# https://git.fluffytail.org/phnt/pleroma/commit/1c801703297bf41ca5bca84af9e47b514aba995b
 
 defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Config
+  alias Pleroma.Activity
 
   require Logger
 
   @moduledoc "Notify local users upon the block, report or unfollow."
   @behaviour Pleroma.Web.ActivityPub.MRF.Policy
 
-  defp is_block_or_unblock(%{"type" => "Block", "object" => object}),
-    do: {true, "blocked", object}
+  defp check_cache(message, actor, recepient, action) do
+    with {:error, _} <- Cachex.stats(:highroller), do: Cachex.start(:highroller, [ stats: true ])
+    systime = :os.system_time(:seconds)
 
-  defp is_block_or_unblock(%{
-         "type" => "Undo",
-         "object" => %{"type" => "Block", "object" => object}
-       }),
-       do: {true, "unblocked", object}
+    {_, actiontime} = Cachex.fetch(:highroller, actor<>","<>recepient<>","<>action, fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
+    {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor, fn(_i) -> {:commit, 0} end)
+
+    if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
+      Cachex.incr(:highroller, "global:"<>actor, globalcount+1)
+      Cachex.put(:highroller, actor<>","<>recepient<>","<>action, systime+Config.get([:mrf_high_roller, :timeout]))
+      true
+    else
+      Logger.warning("Rate-limited incoming "<>action<>" notif!  #{inspect(message)}")
+      Cachex.incr(:highroller, "global:"<>actor, globalcount+1)
+      Cachex.incr(:highroller, actor<>","<>recepient<>","<>action, 30*(1+(systime-actiontime)))
+      false
+    end
+  end
+
+  defp check_action(%{"type" => "Reject", "object" => object}) do
+    activity = Activity.normalize(object)
+    case activity.data do
+      %{"type" => "Follow", "state" => "accept"} -> {true, "follow_remove", Config.get([:mrf_high_roller, :follow_remove_message]), Config.get([:mrf_high_roller, :follow_remove_visibility])}
+      %{"type" => "Follow", "state" => "pending"} -> {true, "follow_reject", Config.get([:mrf_high_roller, :follow_reject_message]), Config.get([:mrf_high_roller, :follow_reject_visibility])}
+      _ -> {false, nil, nil, nil}
+    end
+  end
 
-  defp is_block_or_unblock(_), do: {false, nil, nil}
+  defp check_action(message) do
+    case message do
+      %{"type" => "Block"} -> {true, "block", Config.get([:mrf_high_roller, :block_message]), Config.get([:mrf_high_roller, :block_visibility])}
+      %{"type" => "Flag"} -> {true, "report", Config.get([:mrf_high_roller, :report_message]), Config.get([:mrf_high_roller, :report_visibility])}
+      %{"type" => "Undo", "object" => %{"type" => "Block"}} -> {true, "unblock", Config.get([:mrf_high_roller, :unblock_message]), Config.get([:mrf_high_roller, :unblock_visibility])}
+      %{"type" => "Undo", "object" => %{"type" => "Follow"}} -> {true, "unfollow", Config.get([:mrf_high_roller, :unfollow_message]), Config.get([:mrf_high_roller, :unfollow_visibility])}
+      _ -> {false, nil, nil, nil}
+    end
+  end
 
   defp is_report(%{"type" => "Flag", "object" => objects}) do
     case objects do
@@ -40,190 +70,124 @@ defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
 
   defp is_report(_), do: {false, [], nil}
 
-  defp extract_reported_post(post) do
-    case post do
-      %{"id" => id} -> id
-      s when is_binary(s) -> s
-      _ -> nil
+  defp check_recepient(action, message) when action == "report" do
+    with {true, _, to} <- is_report(message) do
+      User.get_cached_by_ap_id(to)
     end
   end
 
-  defp is_unfollow(%{
-         "type" => "Undo",
-         "object" => %{"type" => "Follow", "object" => object}
-       }),
-       do: {true, object}
-
-  defp is_unfollow(_), do: {false, nil, nil}
-
-  @impl true
-  def filter(message) do
-    with {:error, _} <- Cachex.stats(:highroller), do: Cachex.start(:highroller, [ stats: true ])
-    systime = :os.system_time(:seconds)
+  defp check_recepient(_, %{"type" => "Undo", "object" => %{"object" => object}}) do
+    User.get_cached_by_ap_id(object)
+  end
 
-    with {true, action, object} <- is_block_or_unblock(message),
-         %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
-         %User{} = recipient <- User.get_cached_by_ap_id(object),
-         false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
-         false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
-         true <- recipient.local do
+  defp check_recepient(_, %{"type" => "Reject", "to" => to}) do
+    User.get_cached_by_ap_id(to)
+  end
 
-      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
-      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
+  defp check_recepient(_, %{"object" => object}) do
+    User.get_cached_by_ap_id(object)
+  end
 
-      blocker = if(Config.get([:mrf_high_roller, :tag_blocking_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
-        "@" <> actor.nickname
-      else
-        actor.nickname
-      end
+  defp check_recepient(_, _) do
+    nil
+  end
 
-      replacements = %{
-        "actor" => blocker,
-        "target" => "@" <> recipient.nickname,
-        "action" => action
-      }
+  defp check_tag(host, actor, action) do
+    mention = case action do
+      "block" -> Config.get([:mrf_high_roller, :tag_blocking_actor])
+      "unblock" -> Config.get([:mrf_high_roller, :tag_unblocking_actor])
+      "report" -> Config.get([:mrf_high_roller, :tag_reporting_actor])
+      "unfollow" -> Config.get([:mrf_high_roller, :tag_unfollowing_actor])
+      "follow_remove" -> Config.get([:mrf_high_roller, :tag_follow_remove_actor])
+      "follow_reject" -> Config.get([:mrf_high_roller, :tag_follow_reject_actor])
+      _ -> false
+    end
 
-      msg =
-        Regex.replace(
-          ~r/{([a-z]+)?}/,
-          Config.get([:mrf_high_roller, :block_message]),
-          fn _, match ->
-            replacements[match]
-          end
-        )
+    if(mention && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), host)) do
+      "@" <> actor.nickname
+    else
+      actor.nickname
+    end
+  end
 
-      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, systime+Config.get([:mrf_high_roller, :timeout]))
-        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
-            status: msg,
-            visibility: Config.get([:mrf_high_roller, :block_visibility])
-        })
-      else
-        Logger.warn("Rate-limited incoming block notif!  #{inspect(message)}")
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, 30*(1+(systime-actiontime)))
-      end
+  defp extract_reported_post(post) do
+    case post do
+      %{"id" => id} -> id
+      s when is_binary(s) -> s
+      _ -> nil
     end
+  end
 
-    with {true, objects, to} <- is_report(message),
+  @impl true
+  def filter(%{"type" => type} = message) when type in ["Block", "Undo", "Flag", "Reject"] do
+    with {true, action, template, visibility} <- check_action(message),
          %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
-         %User{} = recipient <- User.get_cached_by_ap_id(to),
+         %User{} = recepient <- check_recepient(action, message),
+         tag <- check_tag(URI.parse(message["actor"]).host, actor, action),
          false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
          false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
-         true <- recipient.local do
-
-      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",report", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
-      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
-
-      reporter = if(Config.get([:mrf_high_roller, :tag_reporting_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
-        "@" <> actor.nickname
-      else
-        actor.nickname
-      end
+         true <- recepient.local,
+         true <- check_cache(message, actor.nickname, recepient.nickname, action) do
 
       replacements = %{
-        "actor" => reporter,
-        "target" => "@" <> recipient.nickname
+        "actor" => tag,
+        "target" => "@" <> recepient.nickname
       }
 
       msg =
         Regex.replace(
           ~r/{([a-z]+)?}/,
-          Pleroma.Config.get([:mrf_high_roller, :report_message]),
+          template,
           fn _, match ->
             replacements[match]
           end
         )
 
-      posts =
-        objects
-        |> Enum.map(&extract_reported_post/1)
-        |> Enum.reject(&is_nil/1)
-        |> Enum.map(fn s -> "- " <> s end)
-        |> Enum.join("\n")
-        |> (fn s ->
-              case s do
-                "" -> ""
-                s -> "\n\nReported objects:\n" <> s
-              end
-            end).()
-
-      comment =
-        case message["content"] do
-          "" -> ""
-          s when is_binary(s) -> "\n\nReport message:\n" <> s
-          _ -> ""
-        end
-
-      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",report", systime+Config.get([:mrf_high_roller, :timeout]))
-        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
-            status: msg <> comment <> posts,
-            visibility: Config.get([:mrf_high_roller, :report_visibility])
-        })
-      else
-        Logger.warn("Rate-limited incoming report notif!  #{inspect(message)}")
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",report", 30*(1+(systime-actiontime)))
-      end
-    end
-
-    with {true, object} <- is_unfollow(message),
-         %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
-         %User{} = recipient <- User.get_cached_by_ap_id(object),
-         false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
-         false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
-         true <- recipient.local do
-
-      {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
-      {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
-
-      unfollower = if(Config.get([:mrf_high_roller, :tag_unfollowing_actor]) && !Enum.member?(Config.get([:mrf_high_roller, :domain_greylist]), URI.parse(message["actor"]).host)) do
-        "@" <> actor.nickname
-      else
-        actor.nickname
-      end
-
-      replacements = %{
-        "actor" => unfollower,
-        "target" => "@" <> recipient.nickname
-      }
-
-      msg =
-        Regex.replace(
-          ~r/{([a-z]+)?}/,
-          Pleroma.Config.get([:mrf_high_roller, :unfollow_message]),
-          fn _, match ->
-            replacements[match]
+      info = with {true, objects, _} <- is_report(message) do
+        posts =
+          objects
+          |> Enum.map(&extract_reported_post/1)
+          |> Enum.reject(&is_nil/1)
+          |> Enum.map(fn s -> "- " <> s end)
+          |> Enum.join("\n")
+          |> (fn s ->
+                case s do
+                  "" -> ""
+                  s -> "\n\nReported objects:\n" <> s
+                end
+              end).()
+
+        comment =
+          case message["content"] do
+            "" -> ""
+            s when is_binary(s) -> "\n\nReport message:\n" <> s
+            _ -> ""
           end
-        )
 
-      if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", systime+Config.get([:mrf_high_roller, :timeout]))
-        CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
-            status: msg,
-            visibility: Config.get([:mrf_high_roller, :unfollow_visibility])
-        })
+        comment <> posts
       else
-        Logger.warn("Rate-limited incoming unfollow notif!  #{inspect(message)}")
-        Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
-        Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", 30*(1+(systime-actiontime)))
+        _ -> ""
       end
+
+      CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
+          status: msg <> info,
+          visibility: visibility
+      })
     end
 
     {:ok, message}
   end
 
+  @impl true
+  def filter(message), do: {:ok, message}
+
   @impl true
   def config_description do
     %{
       key: :mrf_high_roller,
       related_policy: "Pleroma.Web.ActivityPub.MRF.HighRollerPolicy",
       label: "High Roller Policy",
-      description: "Three-in-one policy for notifying users upon being blocked, unfollowed or reported",
+      description: "Five-in-one policy for notifying users upon being blocked, unfollowed, reported, having their follow rejected or force-removed",
       children: [
         %{
           key: :user,
@@ -272,8 +236,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
           type: :string,
           label: "Block message",
           description:
-            "The message to send when someone is blocked or unblocked; use {actor}, {target}, and {action} variables",
-          suggestions: ["{target} you have been {action} by {actor}"]
+            "The message to send when someone is blocked; use {actor} and {target} variables",
+          suggestions: ["{target} you have been blocked by {actor}"]
         },
         %{
           key: :tag_blocking_actor,
@@ -288,6 +252,27 @@ defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
           description: "Visibility of the block notification",
           suggestions: ["public", "unlisted", "private", "direct"]
         },
+        %{
+          key: :unblock_message,
+          type: :string,
+          label: "Unblock message",
+          description:
+            "The message to send when someone is blocked or unblocked; use {actor} and {target} variables",
+          suggestions: ["{target} you have been unblocked by {actor}"]
+        },
+        %{
+          key: :tag_unblocking_actor,
+          type: :boolean,
+          label: "Tag unblocking actor",
+          description: "Whether to tag the unblocking actor or not"
+        },
+        %{
+          key: :unblock_visibility,
+          type: :string,
+          label: "Unblock visibility",
+          description: "Visibility of the unblock notification",
+          suggestions: ["public", "unlisted", "private", "direct"]
+        },
         %{
           key: :report_message,
           type: :string,
@@ -329,6 +314,48 @@ defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
           label: "Unfollow visibility",
           description: "Visibility of the unfollow notification",
           suggestions: ["public", "unlisted", "private", "direct"]
+        },
+        %{
+          key: :follow_remove_message,
+          type: :string,
+          label: "Follower removal message",
+          description:
+            "The message to send when someone is being removed from followers; use {actor} and {target} variables",
+          suggestions: ["{target} you have been removed from followers by {actor}"]
+        },
+        %{
+          key: :tag_follow_remove_actor,
+          type: :boolean,
+          label: "Tag actor removing follower",
+          description: "Whether to tag the actor removing followers or not"
+        },
+        %{
+          key: :follow_remove_visibility,
+          type: :string,
+          label: "Follower removal visibility",
+          description: "Visibility of the follower removal notification",
+          suggestions: ["public", "unlisted", "private", "direct"]
+        },
+        %{
+          key: :follow_reject_message,
+          type: :string,
+          label: "Follower rejection message",
+          description:
+            "The message to send when someone is being rejectd from followers; use {actor} and {target} variables",
+          suggestions: ["{target} your follow request has been rejected by {actor}"]
+        },
+        %{
+          key: :tag_follow_reject_actor,
+          type: :boolean,
+          label: "Tag actor removing follower",
+          description: "Whether to tag the actor removing followers or not"
+        },
+        %{
+          key: :follow_reject_visibility,
+          type: :string,
+          label: "Follower rejection visibility",
+          description: "Visibility of the follower rejection notification",
+          suggestions: ["public", "unlisted", "private", "direct"]
         }
       ]
     }