1 # Three-in-one policy for block, report and unfollow notifications.
2 # Credits for the individual parts:
3 # Yukkuri for the combined block/report MRF
4 # https://gitlab.eientei.org/eientei/pleroma/-/blob/eientei/lib/pleroma/web/activity_pub/mrf/block_bot_policy.ex
5 # NEETzsche for the block MRF with configurable message
6 # https://gitlab.com/soapbox-pub/rebased/-/blob/develop/lib/pleroma/web/activity_pub/mrf/block_notification_policy.ex
7 # Nekobit for the unfollow MRF (nigga nuked his account at the time, linking cached version)
8 # https://eientei.org/notice/AL6nnjih8H6Lco8QoS
9 # Pete for the example of Cachex-based rate limiting
10 # https://freespeechextremist.com/objects/9f24a3e4-2e34-4fcb-a0d1-42229e27da3e
12 defmodule Pleroma.Web.ActivityPub.MRF.HighRollerPolicy do
14 alias Pleroma.Web.CommonAPI
19 @moduledoc "Notify local users upon the block, report or unfollow."
20 @behaviour Pleroma.Web.ActivityPub.MRF.Policy
22 defp is_block_or_unblock(%{"type" => "Block", "object" => object}),
23 do: {true, "blocked", object}
25 defp is_block_or_unblock(%{
27 "object" => %{"type" => "Block", "object" => object}
29 do: {true, "unblocked", object}
31 defp is_block_or_unblock(_), do: {false, nil, nil}
33 defp is_report(%{"type" => "Flag", "object" => objects}) do
35 [head | tail] when is_binary(head) -> {true, tail, head}
36 s when is_binary(s) -> {true, [], s}
41 defp is_report(_), do: {false, [], nil}
43 defp extract_reported_post(post) do
46 s when is_binary(s) -> s
53 "object" => %{"type" => "Follow", "object" => object}
57 defp is_unfollow(_), do: {false, nil, nil}
60 def filter(message) do
61 with {:error, _} <- Cachex.stats(:highroller), do: Cachex.start(:highroller, [ stats: true ])
62 systime = :os.system_time(:seconds)
64 with {true, action, object} <- is_block_or_unblock(message),
65 %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
66 %User{} = recipient <- User.get_cached_by_ap_id(object),
67 false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
68 false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
69 true <- recipient.local do
71 {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
72 {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
74 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
82 "target" => "@" <> recipient.nickname,
89 Config.get([:mrf_high_roller, :block_message]),
95 if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
96 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
97 Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, systime+Config.get([:mrf_high_roller, :timeout]))
98 CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
100 visibility: Config.get([:mrf_high_roller, :block_visibility])
103 Logger.warn("Rate-limited incoming block notif! #{inspect(message)}")
104 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
105 Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>","<>action, 30*(1+(systime-actiontime)))
109 with {true, objects, to} <- is_report(message),
110 %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
111 %User{} = recipient <- User.get_cached_by_ap_id(to),
112 false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
113 false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
114 true <- recipient.local do
116 {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",report", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
117 {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
119 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
120 "@" <> actor.nickname
127 "target" => "@" <> recipient.nickname
133 Pleroma.Config.get([:mrf_high_roller, :report_message]),
141 |> Enum.map(&extract_reported_post/1)
142 |> Enum.reject(&is_nil/1)
143 |> Enum.map(fn s -> "- " <> s end)
148 s -> "\n\nReported objects:\n" <> s
153 case message["content"] do
155 s when is_binary(s) -> "\n\nReport message:\n" <> s
159 if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
160 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
161 Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",report", systime+Config.get([:mrf_high_roller, :timeout]))
162 CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
163 status: msg <> comment <> posts,
164 visibility: Config.get([:mrf_high_roller, :report_visibility])
167 Logger.warn("Rate-limited incoming report notif! #{inspect(message)}")
168 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
169 Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",report", 30*(1+(systime-actiontime)))
173 with {true, object} <- is_unfollow(message),
174 %User{} = actor <- User.get_cached_by_ap_id(message["actor"]),
175 %User{} = recipient <- User.get_cached_by_ap_id(object),
176 false <- Enum.member?(Config.get([:mrf_high_roller, :actor_blacklist]), message["actor"]),
177 false <- Enum.member?(Config.get([:mrf_high_roller, :domain_blacklist]), URI.parse(message["actor"]).host),
178 true <- recipient.local do
180 {_, actiontime} = Cachex.fetch(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", fn(_i) -> {:commit, :os.system_time(:seconds)-1} end)
181 {_, globalcount} = Cachex.fetch(:highroller, "global:"<>actor.nickname, fn(_i) -> {:commit, 0} end)
183 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
184 "@" <> actor.nickname
190 "actor" => unfollower,
191 "target" => "@" <> recipient.nickname
197 Pleroma.Config.get([:mrf_high_roller, :unfollow_message]),
203 if (systime > actiontime && globalcount < Config.get([:mrf_high_roller, :global_threshold])) do
204 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
205 Cachex.put(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", systime+Config.get([:mrf_high_roller, :timeout]))
206 CommonAPI.post(User.get_cached_by_nickname(Config.get([:mrf_high_roller, :user])), %{
208 visibility: Config.get([:mrf_high_roller, :unfollow_visibility])
211 Logger.warn("Rate-limited incoming unfollow notif! #{inspect(message)}")
212 Cachex.incr(:highroller, "global:"<>actor.nickname, globalcount+1)
213 Cachex.incr(:highroller, actor.nickname<>","<>recipient.nickname<>",unfollow", 30*(1+(systime-actiontime)))
221 def config_description do
223 key: :mrf_high_roller,
224 related_policy: "Pleroma.Web.ActivityPub.MRF.HighRollerPolicy",
225 label: "High Roller Policy",
226 description: "Three-in-one policy for notifying users upon being blocked, unfollowed or reported",
232 description: "Account from which notifications would be sent",
233 suggestions: ["blockbot"]
239 description: "Timeout (in seconds) between which no new notifications of the same type can be sent",
243 key: :global_threshold,
245 label: "Global threshold",
246 description: "Global threshold of the actions for the actor",
250 key: :actor_blacklist,
251 type: {:list, :string},
252 label: "Actor blacklist",
253 description: "List of actors to skip sending a notification about",
254 suggestions: ["https://freespeechextremist.com/users/p"]
257 key: :domain_greylist,
258 type: {:list, :string},
259 label: "Domain greylist",
260 description: "List of domains to exclude their users from being tagged",
261 suggestions: ["freespeechextremist.com"]
264 key: :domain_blacklist,
265 type: {:list, :string},
266 label: "Domain blacklist",
267 description: "List of domains to skip sending a notification about",
268 suggestions: ["freespeechextremist.com"]
273 label: "Block message",
275 "The message to send when someone is blocked or unblocked; use {actor}, {target}, and {action} variables",
276 suggestions: ["{target} you have been {action} by {actor}"]
279 key: :tag_blocking_actor,
281 label: "Tag blocking actor",
282 description: "Whether to tag the blocking actor or not"
285 key: :block_visibility,
287 label: "Block visibility",
288 description: "Visibility of the block notification",
289 suggestions: ["public", "unlisted", "private", "direct"]
292 key: :report_message,
294 label: "Report message",
296 "The message to send when someone is reported; use {actor} and {target} variables",
297 suggestions: ["{target} you have been reported by {actor}"]
300 key: :tag_reporting_actor,
302 label: "Tag reporting actor",
303 description: "Whether to tag the reporting actor or not"
306 key: :report_visibility,
308 label: "Report visibility",
309 description: "Visibility of the report notification",
310 suggestions: ["public", "unlisted", "private", "direct"]
313 key: :unfollow_message,
315 label: "Unfollow message",
317 "The message to send when someone is unfollowed; use {actor} and {target} variables",
318 suggestions: ["{target} you have been unfollowed by {actor}"]
321 key: :tag_unfollowing_actor,
323 label: "Tag unfollowing actor",
324 description: "Whether to tag the unfollowing actor or not"
327 key: :unfollow_visibility,
329 label: "Unfollow visibility",
330 description: "Visibility of the unfollow notification",
331 suggestions: ["public", "unlisted", "private", "direct"]
338 def describe, do: {:ok, %{}}