aboutsummaryrefslogtreecommitdiff
path: root/static/modules/high_roller_policy.ex
diff options
context:
space:
mode:
authordcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
committerdcc <dcc@logografos.com>2023-09-02 00:52:52 -0700
commit3a4773c3c2bd0bbef244eb519b07208da9108e49 (patch)
tree973567a6f3abb37bfb0f785b1cad14ed55840ef5 /static/modules/high_roller_policy.ex
downloadanni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.gz
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.tar.bz2
anni-3a4773c3c2bd0bbef244eb519b07208da9108e49.zip
First
Diffstat (limited to 'static/modules/high_roller_policy.ex')
-rw-r--r--static/modules/high_roller_policy.ex339
1 files changed, 339 insertions, 0 deletions
diff --git a/static/modules/high_roller_policy.ex b/static/modules/high_roller_policy.ex
new file mode 100644
index 0000000..50b21ea
--- /dev/null
+++ b/static/modules/high_roller_policy.ex
@@ -0,0 +1,339 @@
+# Three-in-one policy for block, report and unfollow 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
+# NEETzsche for the block MRF with configurable message
+# https://gitlab.com/soapbox-pub/rebased/-/blob/develop/lib/pleroma/web/activity_pub/mrf/block_notification_policy.ex
+# Nekobit for the unfollow MRF (nigga nuked his account at the time, linking cached version)
+# https://eientei.org/notice/AL6nnjih8H6Lco8QoS
+# Pete for the example of Cachex-based rate limiting
+# https://freespeechextremist.com/objects/9f24a3e4-2e34-4fcb-a0d1-42229e27da3e
+
+defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Config
+
+ 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 is_block_or_unblock(%{
+ "type" => "Undo",
+ "object" => %{"type" => "Block", "object" => object}
+ }),
+ do: {true, "unblocked", object}
+
+ defp is_block_or_unblock(_), do: {false, nil, nil}
+
+ defp is_report(%{"type" => "Flag", "object" => objects}) do
+ case objects do
+ [head | tail] when is_binary(head) -> {true, tail, head}
+ s when is_binary(s) -> {true, [], s}
+ _ -> {true, [], nil}
+ end
+ end
+
+ 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
+ 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)
+
+ 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
+
+ {_, 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)
+
+ 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
+
+ replacements = %{
+ "actor" => blocker,
+ "target" => "@" <> recipient.nickname,
+ "action" => action
+ }
+
+ msg =
+ Regex.replace(
+ ~r/{([a-z]+)?}/,
+ Config.get([:mrf_high_roller, :block_message]),
+ fn _, match ->
+ replacements[match]
+ 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
+ end
+
+ with {true, objects, to} <- is_report(message),
+ %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
+ %User{} = recipient <- User.get_cached_by_ap_id(to),
+ 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
+
+ replacements = %{
+ "actor" => reporter,
+ "target" => "@" <> recipient.nickname
+ }
+
+ msg =
+ Regex.replace(
+ ~r/{([a-z]+)?}/,
+ Pleroma.Config.get([:mrf_high_roller, :report_message]),
+ 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]
+ 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])
+ })
+ 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
+ end
+
+ {:ok, message}
+ end
+
+ @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",
+ children: [
+ %{
+ key: :user,
+ type: :string,
+ label: "Account",
+ description: "Account from which notifications would be sent",
+ suggestions: ["blockbot"]
+ },
+ %{
+ key: :timeout,
+ type: :integer,
+ label: "Timeout",
+ description: "Timeout (in seconds) between which no new notifications of the same type can be sent",
+ suggestions: [60]
+ },
+ %{
+ key: :global_threshold,
+ type: :integer,
+ label: "Global threshold",
+ description: "Global threshold of the actions for the actor",
+ suggestions: [5]
+ },
+ %{
+ key: :actor_blacklist,
+ type: {:list, :string},
+ label: "Actor blacklist",
+ description: "List of actors to skip sending a notification about",
+ suggestions: ["https://freespeechextremist.com/users/p"]
+ },
+ %{
+ key: :domain_greylist,
+ type: {:list, :string},
+ label: "Domain greylist",
+ description: "List of domains to exclude their users from being tagged",
+ suggestions: ["freespeechextremist.com"]
+ },
+ %{
+ key: :domain_blacklist,
+ type: {:list, :string},
+ label: "Domain blacklist",
+ description: "List of domains to skip sending a notification about",
+ suggestions: ["freespeechextremist.com"]
+ },
+ %{
+ key: :block_message,
+ 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}"]
+ },
+ %{
+ key: :tag_blocking_actor,
+ type: :boolean,
+ label: "Tag blocking actor",
+ description: "Whether to tag the blocking actor or not"
+ },
+ %{
+ key: :block_visibility,
+ type: :string,
+ label: "Block visibility",
+ description: "Visibility of the block notification",
+ suggestions: ["public", "unlisted", "private", "direct"]
+ },
+ %{
+ key: :report_message,
+ type: :string,
+ label: "Report message",
+ description:
+ "The message to send when someone is reported; use {actor} and {target} variables",
+ suggestions: ["{target} you have been reported by {actor}"]
+ },
+ %{
+ key: :tag_reporting_actor,
+ type: :boolean,
+ label: "Tag reporting actor",
+ description: "Whether to tag the reporting actor or not"
+ },
+ %{
+ key: :report_visibility,
+ type: :string,
+ label: "Report visibility",
+ description: "Visibility of the report notification",
+ suggestions: ["public", "unlisted", "private", "direct"]
+ },
+ %{
+ key: :unfollow_message,
+ type: :string,
+ label: "Unfollow message",
+ description:
+ "The message to send when someone is unfollowed; use {actor} and {target} variables",
+ suggestions: ["{target} you have been unfollowed by {actor}"]
+ },
+ %{
+ key: :tag_unfollowing_actor,
+ type: :boolean,
+ label: "Tag unfollowing actor",
+ description: "Whether to tag the unfollowing actor or not"
+ },
+ %{
+ key: :unfollow_visibility,
+ type: :string,
+ label: "Unfollow visibility",
+ description: "Visibility of the unfollow notification",
+ suggestions: ["public", "unlisted", "private", "direct"]
+ }
+ ]
+ }
+ end
+
+ @impl true
+ def describe, do: {:ok, %{}}
+end