Merge branch 'improve-mrf-returns' into 'develop'

Draft: Improve MRF contract

See merge request pleroma/pleroma!4238
This commit is contained in:
feld 2024-09-20 16:29:53 +00:00
commit 7779620d13
38 changed files with 358 additions and 235 deletions

View file

@ -53,6 +53,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
@required_description_keys [:key, :related_policy]
@success_types [:ok, :pass, :filter]
def filter_one(policy, message) do
Code.ensure_loaded(policy)
@ -69,7 +71,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
else
main_result = policy.filter(message)
with {_, {:ok, main_message}} <- {:main, main_result},
with {_, {result_type, main_message}} when result_type in @success_types <-
{:main, main_result},
{_,
%{
"formerRepresentations" => %{
@ -82,8 +85,9 @@ defmodule Pleroma.Web.ActivityPub.MRF do
object["formerRepresentations"],
object,
fn item ->
with {:ok, filtered} <- policy.filter(Map.put(message, "object", item)) do
{:ok, filtered["object"]}
with {result_type, filtered} when result_type in @success_types <-
policy.filter(Map.put(message, "object", item)) do
{:filter, filtered["object"]}
else
e -> e
end
@ -114,7 +118,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do
ap_id = message["object"]
if object && ap_id do
with {:ok, message} <- filter(Map.put(message, "object", object)) do
with {type, message} when type in @success_types <-
filter(Map.put(message, "object", object)) do
meta = Keyword.put(meta, :object_data, message["object"])
{:ok, Map.put(message, "object", ap_id), meta}
else

View file

@ -8,14 +8,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
@impl true
def filter(activity) do
activity =
if note?(activity) and local?(activity) do
maybe_add_expiration(activity)
else
activity
end
{:ok, activity}
if note?(activity) and local?(activity) do
maybe_add_expiration(activity)
else
{:pass, activity}
end
end
@impl true
@ -35,9 +32,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicy do
with %{"expires_at" => existing_expires_at} <- activity,
:lt <- DateTime.compare(existing_expires_at, expires_at) do
activity
{:pass, activity}
else
_ -> Map.put(activity, "expires_at", expires_at)
_ ->
{:filter, Map.put(activity, "expires_at", expires_at)}
end
end

View file

@ -69,14 +69,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do
score = determine_if_followbot(actor)
if score < 0.8 || bot_allowed?(activity, actor) do
{:ok, activity}
{:pass, activity}
else
{:reject, "[AntiFollowbotPolicy] Scored #{actor_id} as #{score}"}
reason = "Scored #{actor_id} as #{score}"
{:reject, %{activity: activity, reason: reason}}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -7,8 +7,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
require Logger
@impl true
def history_awareness, do: :auto
@ -33,27 +31,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
{:contains_links, true} <- {:contains_links, contains_links?(object)},
{:old_user, true} <- {:old_user, old_user?(u)} do
{:ok, activity}
{:pass, activity}
else
{:ok, %User{local: true}} ->
{:ok, activity}
{:pass, activity}
{:contains_links, false} ->
{:ok, activity}
{:pass, activity}
{:old_user, false} ->
{:reject, "[AntiLinkSpamPolicy] User has no posts nor followers"}
reason = "User has no posts nor followers"
{:reject, %{activity: activity, reason: reason}}
{:error, _} ->
{:reject, "[AntiLinkSpamPolicy] Failed to get or fetch user by ap_id"}
reason = "Failed to get or fetch user by ap_id"
{:reject, %{activity: activity, reason: reason}}
e ->
{:reject, "[AntiLinkSpamPolicy] Unhandled error #{inspect(e)}"}
reason = "Unhandled error"
{:reject, %{activity: activity, reason: reason, error: e}}
end
end
# in all other cases, pass through
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -60,27 +60,32 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiMentionSpamPolicy do
with {:ok, %User{local: false} = u} <- User.get_or_fetch_by_ap_id(actor),
{:has_mentions, true} <- {:has_mentions, object_has_recipients?(activity)},
{:good_reputation, true} <- {:good_reputation, good_reputation?(u)} do
{:ok, activity}
{:pass, activity}
else
{:ok, %User{local: true}} ->
{:ok, activity}
{:pass, activity}
{:has_mentions, false} ->
{:ok, activity}
{:pass, activity}
{:good_reputation, false} ->
{:reject, "[AntiMentionSpamPolicy] User rejected"}
reason = "Bad reputation"
{:reject, %{activity: activity, reason: reason}}
{:error, _} ->
{:reject, "[AntiMentionSpamPolicy] Failed to get or fetch user by ap_id"}
reason = "Failed to get or fetch user by ap_id"
{:reject, %{activity: activity, reason: reason}}
e ->
{:reject, "[AntiMentionSpamPolicy] Unhandled error #{inspect(e)}"}
reason = "Unhandled error"
{:reject, %{activity: activity, reason: reason, error: e}}
end
end
# in all other cases, pass through
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -32,8 +32,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do
alias Pleroma.Config
require Logger
@query_retries 1
@query_timeout 500
@ -42,14 +40,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do
actor_info = URI.parse(actor)
with {:ok, activity} <- check_rbl(actor_info, activity) do
{:ok, activity}
{:pass, activity}
else
_ -> {:reject, "[DNSRBLPolicy]"}
{:error, reason} ->
{:reject, %{activity: activity, reason: reason}}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe do
@ -102,19 +103,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.DNSRBLPolicy do
if Enum.empty?(rbl_response) do
{:ok, activity}
else
Task.start(fn ->
reason =
case rblquery(query, :txt) do
[[result]] -> result
_ -> "undefined"
end
reason =
case rblquery(query, :txt) do
[[result]] -> to_string(result)
_ -> "undefined"
end
Logger.warning(
"DNSRBL Rejected activity from #{actor_host} for reason: #{inspect(reason)}"
)
end)
:error
{:error, reason}
end
else
_ -> {:ok, activity}

View file

@ -3,14 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
require Logger
@moduledoc "Drop and log everything received"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(activity) do
Logger.debug("REJECTING #{inspect(activity)}")
{:reject, activity}
{:reject, %{activity: activity}}
end
@impl true

View file

@ -44,7 +44,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
end),
activity <- Map.put(activity, "object", object),
activity <- maybe_delist(activity) do
{:ok, activity}
{:filter, activity}
end
end
@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do
with object <- process_remove(object, :url, config_remove_url()),
object <- process_remove(object, :shortcode, config_remove_shortcode()) do
{:ok, object}
{:filter, object}
end
end
@ -60,16 +60,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
def filter(%{"type" => "EmojiReact"} = object) do
with {:ok, _} <-
matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
{:ok, object}
{:filter, object}
else
_ ->
{:reject, "[EmojiPolicy] Rejected for having disallowed emoji"}
reason = "Has disallowed emoji"
{:reject, %{activity: object, reason: reason}}
end
end
@impl true
def filter(activity) do
{:ok, activity}
{:pass, activity}
end
defp match_string?(string, pattern) when is_binary(pattern) do

View file

@ -38,10 +38,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
activity = Map.put(activity, "object", child)
{:ok, activity}
if match?(^child, object["inReplyTo"]) do
{:pass, activity}
else
{:filter, activity}
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
def describe, do: {:ok, %{}}
end

View file

@ -39,14 +39,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.FODirectReply do
|> put_in(["object", "cc"], [])
|> put_in(["object", "to"], direct_to)
{:ok, updated_activity}
{:filter, updated_activity}
else
_ -> {:ok, activity}
_ ->
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -19,15 +19,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
try_follow(follower, activity)
else
nil ->
Logger.warning(
Logger.error(
"#{__MODULE__} skipped because of missing `:mrf_follow_bot, :follower_nickname` configuration, the :follower_nickname
account does not exist, or the account is not correctly configured as a bot."
)
{:ok, activity}
{:pass, activity}
_ ->
{:ok, activity}
{:pass, activity}
end
end
@ -45,15 +45,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.FollowBotPolicy do
false <- User.following?(follower, user),
false <- User.locked?(user),
false <- (user.bio || "") |> String.downcase() |> String.contains?("nobot") do
Logger.debug(
"#{__MODULE__}: Follow request from #{follower.nickname} to #{user.nickname}"
)
CommonAPI.follow(user, follower)
end
end)
{:ok, activity}
{:filter, activity}
end
@impl true

View file

@ -42,14 +42,16 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceBotUnlistedPolicy do
|> Map.put("cc", cc)
|> Map.put("object", object)
{:ok, activity}
{:filter, activity}
else
{:ok, activity}
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -36,7 +36,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
@impl true
def filter(%{"type" => "Create", "object" => %{"tag" => tag} = object} = activity) do
tag =
updated_tag =
tag
|> prepend_author(
object["inReplyTo"],
@ -48,11 +48,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMention do
)
|> Enum.uniq()
{:ok, put_in(activity["object"]["tag"], tag)}
if match?(^tag, updated_tag) do
{:pass, activity}
else
updated_activity = put_in(activity["object"]["tag"], updated_tag)
{:filter, updated_activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -113,7 +113,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
do: "<span class=\"recipients-inline\">#{added_mentions}</span>",
else: ""
content =
updated_content =
cond do
# For Markdown posts, insert the mentions inside the first <p> tag
recipients_inline != "" && String.starts_with?(content, "<p>") ->
@ -126,11 +126,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
content
end
{:ok, put_in(activity["object"]["content"], content)}
if match?(^content, updated_content) do
{:pass, activity}
else
updated_activity = put_in(activity["object"]["content"], updated_content)
{:filter, updated_activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -21,7 +21,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
defp check_reject(activity, hashtags) do
if Enum.any?(Config.get([:mrf_hashtag, :reject]), fn match -> match in hashtags end) do
{:reject, "[HashtagPolicy] Matches with rejected keyword"}
reason = "Matches with rejected keyword"
{:reject, %{activity: activity, reason: reason}}
else
{:ok, activity}
end
@ -83,23 +84,29 @@ defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicy do
hashtags = Object.hashtags(%Object{data: object}) ++ historical_hashtags
if hashtags != [] do
with {:ok, activity} <- check_reject(activity, hashtags),
{:ok, activity} <-
with {:ok, processed_activity} <- check_reject(activity, hashtags),
{:ok, processed_activity} <-
(if type == "Create" do
check_ftl_removal(activity, hashtags)
check_ftl_removal(processed_activity, hashtags)
else
{:ok, activity}
{:ok, processed_activity}
end),
{:ok, activity} <- check_sensitive(activity) do
{:ok, activity}
{:ok, processed_activity} <- check_sensitive(processed_activity) do
if match?(^activity, processed_activity) do
{:pass, processed_activity}
else
{:filter, processed_activity}
end
end
else
{:ok, activity}
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe do

View file

@ -43,7 +43,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
defp reject_activity(activity, threshold) when threshold > 0 do
with {_, recipients} <- get_recipient_count(activity) do
if recipients > threshold do
{:reject, "[HellthreadPolicy] #{recipients} recipients is over the limit of #{threshold}"}
reason = "#{recipients} recipients is over the limit of #{threshold}"
{:reject, %{activity: activity, reason: reason}}
else
{:ok, activity}
end
@ -84,15 +85,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do
delist_threshold = Pleroma.Config.get([:mrf_hellthread, :delist_threshold])
with {:ok, activity} <- reject_activity(activity, reject_threshold),
{:ok, activity} <- delist_activity(activity, delist_threshold) do
{:ok, activity}
else
e -> e
{:ok, processed_activity} <- delist_activity(activity, delist_threshold) do
if match?(^activity, processed_activity) do
{:pass, processed_activity}
else
{:filter, processed_activity}
end
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe,

View file

@ -31,24 +31,33 @@ defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
else
template = Pleroma.Config.get([:mrf_inline_quote, :template])
content =
updated_content =
if String.ends_with?(content, "</p>"),
do:
String.trim_trailing(content, "</p>") <>
build_inline_quote(template, quote_url) <> "</p>",
else: content <> build_inline_quote(template, quote_url)
Map.put(object, "content", content)
Map.put(object, "content", updated_content)
end
end
@impl true
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
{:ok, Map.put(activity, "object", filter_object(object))}
updated_object = filter_object(object)
if match?(^updated_object, object) do
{:pass, activity}
else
updated_activity = Map.put(activity, "object", updated_object)
{:filter, updated_activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -33,7 +33,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
if Enum.any?(Pleroma.Config.get([:mrf_keyword, :reject]), fn pattern ->
string_matches?(payload, pattern)
end) do
{:reject, "[KeywordPolicy] Matches with rejected keyword"}
{:reject, "Matches rejected keyword"}
else
{:ok, activity}
end
@ -112,18 +112,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
def filter(%{"type" => type, "object" => %{"content" => _content}} = activity)
when type in ["Create", "Update"] do
with {:ok, activity} <- check_reject(activity),
{:ok, activity} <- check_ftl_removal(activity),
{:ok, activity} <- check_replace(activity) do
{:ok, activity}
{:ok, processed_activity} <- check_ftl_removal(activity),
{:ok, processed_activity} <- check_replace(processed_activity) do
if match?(^activity, processed_activity) do
{:pass, processed_activity}
else
{:filter, processed_activity}
end
else
{:reject, nil} -> {:reject, "[KeywordPolicy] "}
{:reject, _} = e -> e
_e -> {:reject, "[KeywordPolicy] "}
{:reject, reason} ->
{:reject, %{activity: activity, reason: reason}}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe do

View file

@ -53,11 +53,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
when type in ["Create", "Update"] and is_list(attachments) and length(attachments) > 0 do
preload(activity)
{:ok, activity}
{:filter, activity}
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -14,14 +14,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do
if rejected_mention =
Enum.find(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do
{:reject, "[MentionPolicy] Rejected for mention of #{rejected_mention}"}
reason = "Rejected for mention of #{rejected_mention}"
{:reject, %{activity: activity, reason: reason}}
else
{:ok, activity}
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -15,14 +15,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoEmptyPolicy do
true <- note?(activity),
false <- has_attachment?(activity),
true <- only_mentions?(activity) do
{:reject, "[NoEmptyPolicy]"}
reason = "no content"
{:reject, %{activity: activity, reason: reason}}
else
_ ->
{:ok, activity}
{:pass, activity}
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
defp local?(actor) do
if actor |> String.starts_with?("#{Endpoint.url()}") do

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do
@impl true
def filter(activity) do
{:ok, activity}
{:pass, activity}
end
@impl true

View file

@ -17,11 +17,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do
} = activity
)
when type in ["Create", "Update"] and content in [".", "<p>.</p>"] do
{:ok, put_in(activity, ["object", "content"], "")}
{:filter, put_in(activity, ["object", "content"], "")}
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -16,16 +16,23 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do
when type in ["Create", "Update"] do
scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy])
content =
object["content"]
content = object["content"]
updated_content =
content
|> HTML.filter_tags(scrub_policy)
activity = put_in(activity, ["object", "content"], content)
{:ok, activity}
if match?(^content, updated_content) do
{:pass, activity}
else
updated_activity = put_in(activity, ["object", "content"], updated_content)
{:filter, updated_activity}
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -64,7 +64,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
Jason.decode(body)
else
error ->
Logger.warning("""
Logger.error("""
[NsfwApiPolicy]: The API server failed. Skipping.
#{inspect(error)}
""")
@ -134,17 +134,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.NsfwApiPolicy do
@impl true
def filter(activity) do
with {:sfw, activity} <- check_object_nsfw(activity) do
{:ok, activity}
{:pass, activity}
else
{:nsfw, _data} -> handle_nsfw(activity)
{:nsfw, _data} ->
handle_nsfw(activity)
end
end
defp handle_nsfw(activity) do
if Config.get([@policy, :reject]) do
{:reject, activity}
{:reject, %{activity: activity, reason: :nsfw}}
else
{:ok,
{:filter,
activity
|> maybe_unlist()
|> maybe_mark_sensitive()}

View file

@ -11,24 +11,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
defp check_date(%{"object" => %{"published" => published}} = activity) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
{:ttl, false} <- {:ttl, DateTime.diff(now, then) > max_ttl} do
{:ok, activity}
else
{:ttl, true} ->
{:reject, nil}
defp invalid_age?(%{"object" => %{"published" => published}}) do
now = DateTime.utc_now()
{:ok, %DateTime{} = then, _} = DateTime.from_iso8601(published)
max_ttl = Config.get([:mrf_object_age, :threshold])
e ->
{:error, e}
end
DateTime.diff(now, then) > max_ttl
end
defp check_reject(activity, actions) do
if :reject in actions do
{:reject, "[ObjectAgePolicy]"}
{:reject, %{activity: activity, reason: :object_age}}
else
{:ok, activity}
end
@ -54,8 +47,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, activity}
else
_e ->
{:reject, "[ObjectAgePolicy] Unhandled error"}
e ->
{:reject, %{activity: activity, reason: "Unhandled error", error: e}}
end
else
{:ok, activity}
@ -77,8 +70,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
{:ok, activity}
else
_e ->
{:reject, "[ObjectAgePolicy] Unhandled error"}
e ->
{:reject, %{activity: activity, reason: "Unhandled error", error: e}}
end
else
{:ok, activity}
@ -88,14 +81,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@impl true
def filter(%{"type" => "Create", "object" => %{"published" => _}} = activity) do
with actions <- Config.get([:mrf_object_age, :actions]),
{:reject, _} <- check_date(activity),
true <- invalid_age?(activity),
{:ok, activity} <- check_reject(activity, actions),
{:ok, activity} <- check_delist(activity, actions),
{:ok, activity} <- check_strip_followers(activity, actions) do
{:ok, activity}
{:filter, activity}
else
# check_date() is allowed to short-circuit the pipeline
e -> e
false ->
{:pass, activity}
{:reject, _reason} = result ->
result
end
end

View file

@ -3,7 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.Policy do
@callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()}
@type legacy_return :: {:ok | :reject, map()} | {:filter, map()}
@type return :: {:pass, map()} | {:filter, map()} | {:reject, %{activity: map(), reason: any()}}
@callback filter(Pleroma.Activity.t()) :: legacy_return() | return()
@callback describe() :: {:ok | :error, map()}
@callback config_description() :: %{
optional(:children) => [map()],

View file

@ -46,14 +46,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuietReply do
|> put_in(["object", "to"], updated_to)
|> put_in(["object", "cc"], updated_cc)
{:ok, updated_activity}
{:filter, updated_activity}
else
_ -> {:ok, activity}
_ ->
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -12,11 +12,20 @@ defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
@impl true
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
{:ok, Map.put(activity, "object", filter_object(object))}
updated_object = filter_object(object)
if match?(^object, updated_object) do
{:pass, activity}
else
updated_activity = Map.put(activity, "object", updated_object)
{:filter, updated_activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -13,15 +13,15 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
require Pleroma.Constants
@impl true
def filter(%{"type" => "Create"} = object) do
user = User.get_cached_by_ap_id(object["actor"])
def filter(%{"type" => "Create"} = activity) do
user = User.get_cached_by_ap_id(activity["actor"])
# Determine visibility
visibility =
cond do
Pleroma.Constants.as_public() in object["to"] -> "public"
Pleroma.Constants.as_public() in object["cc"] -> "unlisted"
user.follower_address in object["to"] -> "followers"
Pleroma.Constants.as_public() in activity["to"] -> "public"
Pleroma.Constants.as_public() in activity["cc"] -> "unlisted"
user.follower_address in activity["to"] -> "followers"
true -> "direct"
end
@ -29,21 +29,24 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do
cond do
visibility in ["public", "unlisted"] ->
{:ok, object}
{:pass, activity}
visibility == "followers" and Keyword.get(policy, :allow_followersonly) ->
{:ok, object}
{:pass, activity}
visibility == "direct" and Keyword.get(policy, :allow_direct) ->
{:ok, object}
{:pass, activity}
true ->
{:reject, "[RejectNonPublic] visibility: #{visibility}"}
reason = "visibility: #{visibility}"
{:reject, %{activity: activity, reason: reason}}
end
end
@impl true
def filter(object), do: {:ok, object}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe,

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
accepts == [] -> {:ok, activity}
actor_host == Config.get([Pleroma.Web.Endpoint, :url, :host]) -> {:ok, activity}
MRF.subdomain_match?(accepts, actor_host) -> {:ok, activity}
true -> {:reject, "[SimplePolicy] host not in accept list"}
true -> {:reject, %{activity: activity, reason: "host not in accept list"}}
end
end
@ -32,7 +32,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> MRF.subdomains_regex()
if MRF.subdomain_match?(rejects, actor_host) do
{:reject, "[SimplePolicy] host in reject list"}
{:reject, %{activity: activity, reason: "host in reject list"}}
else
{:ok, activity}
end
@ -142,7 +142,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> MRF.subdomains_regex()
if MRF.subdomain_match?(report_removal, actor_host) do
{:reject, "[SimplePolicy] host in report_removal list"}
{:reject, %{activity: activity, reason: "host in report_removal list"}}
else
{:ok, activity}
end
@ -200,7 +200,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do
{:reject, "[SimplePolicy] host in reject_deletes list"}
{:reject, %{activity: activity, reason: "host in reject_deletes list"}}
else
{:ok, activity}
end
@ -218,9 +218,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, activity} <- check_followers_only(actor_info, activity),
{:ok, activity} <- check_report_removal(actor_info, activity),
{:ok, activity} <- check_object(activity) do
{:ok, activity}
{:pass, activity}
else
{:reject, _} = e -> e
{:reject, _} = result ->
result
end
end
@ -232,24 +233,33 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
{:ok, activity} <- check_reject(actor_info, activity),
{:ok, activity} <- check_avatar_removal(actor_info, activity),
{:ok, activity} <- check_banner_removal(actor_info, activity) do
{:ok, activity}
{:pass, activity}
else
{:reject, _} = e -> e
{:reject, _} = result ->
result
end
end
def filter(activity) when is_binary(activity) do
uri = URI.parse(activity)
def filter(ap_id) when is_binary(ap_id) do
uri = URI.parse(ap_id)
with {:ok, activity} <- check_accept(uri, activity),
{:ok, activity} <- check_reject(uri, activity) do
{:ok, activity}
with {_, {:ok, ap_id}} <- {:check_accept, check_accept(uri, ap_id)},
{_, {:ok, ap_id}} <- {:check_reject, check_reject(uri, ap_id)} do
{:pass, ap_id}
else
{:reject, _} = e -> e
{:check_accept, {:reject, _}} ->
fake_activity = %{data: %{id: ap_id}}
{:reject, %{activity: fake_activity, reason: "Host not in accept list"}}
{:check_reject, {:reject, _}} ->
fake_activity = %{data: %{id: ap_id}}
{:reject, %{activity: fake_activity, reason: "Host in reject list"}}
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe do

View file

@ -97,10 +97,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
end
end
{:ok, activity}
{:pass, activity}
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
@spec config_description :: %{

View file

@ -6,8 +6,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
require Logger
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
defp lookup_subchain(actor) do
@ -21,19 +19,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do
@impl true
def filter(%{"actor" => actor} = activity) do
with {:ok, match, subchain} <- lookup_subchain(actor) do
Logger.debug(
"[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{inspect(subchain)}"
)
with {:ok, _match, subchain} <- lookup_subchain(actor) do
MRF.filter(subchain, activity)
else
_e -> {:ok, activity}
_e ->
{:pass, activity}
end
end
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -125,26 +125,40 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
if user.local == true do
{:ok, activity}
else
{:reject,
"[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"}
reason = "Follow from #{actor} tagged with mrf_tag:disable-remote-subscription"
{:reject, %{activity: activity, reason: reason}}
end
end
defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow", "actor" => actor}),
do: {:reject, "[TagPolicy] Follow from #{actor} tagged with mrf_tag:disable-any-subscription"}
defp process_tag(
"mrf_tag:disable-any-subscription",
%{"type" => "Follow", "actor" => actor} = activity
) do
reason = "Follow from #{actor} tagged with mrf_tag:disable-any-subscription"
{:reject, %{activity: activity, reason: reason}}
end
defp process_tag(_, activity), do: {:ok, activity}
def filter_activity(actor, activity) do
User.get_cached_by_ap_id(actor)
|> get_tags()
|> Enum.reduce({:ok, activity}, fn
tag, {:ok, activity} ->
process_tag(tag, activity)
result =
User.get_cached_by_ap_id(actor)
|> get_tags()
|> Enum.reduce({:ok, activity}, fn
tag, {:ok, activity} ->
process_tag(tag, activity)
_, error ->
error
end)
_, error ->
error
end)
case result do
{:ok, activity} ->
{:pass, activity}
{:reject, _} = reject ->
reject
end
end
@impl true
@ -156,7 +170,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do
do: filter_activity(actor, activity)
@impl true
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe, do: {:ok, %{}}

View file

@ -14,7 +14,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
if actor in allow_list do
{:ok, activity}
else
{:reject, "[UserAllowListPolicy] #{actor} not in the list"}
reason = "#{actor} not in the list"
{:reject, %{activity: activity, reason: reason}}
end
end
@ -28,10 +29,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do
[]
)
filter_by_list(activity, allow_list)
case filter_by_list(activity, allow_list) do
{:ok, activity} ->
{:pass, activity}
{:reject, _} = reject ->
reject
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe do

View file

@ -8,33 +8,36 @@ defmodule Pleroma.Web.ActivityPub.MRF.VocabularyPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
@impl true
def filter(%{"type" => "Undo", "object" => object} = activity) do
with {:ok, _} <- filter(object) do
{:ok, activity}
else
{:reject, _} = e -> e
end
end
def filter(%{"type" => "Undo", "object" => object} = _activity), do: filter(object)
def filter(%{"type" => activity_type} = activity) do
with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]),
rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]),
{_, true} <-
{:accepted,
{:accepted_vocab,
Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, activity_type)},
{_, false} <-
{:rejected,
{:rejected_vocab,
length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, activity_type)},
{:ok, _} <- filter(activity["object"]) do
{:ok, activity}
{:pass, _} <- filter(activity["object"]) do
{:pass, activity}
else
{:reject, _} = e -> e
{:accepted, _} -> {:reject, "[VocabularyPolicy] #{activity_type} not in accept list"}
{:rejected, _} -> {:reject, "[VocabularyPolicy] #{activity_type} in reject list"}
{:reject, reason} ->
{:reject, %{activity: activity, reason: reason}}
{:accepted_vocab, _} ->
reason = "#{activity_type} not in accept list"
{:reject, %{activity: activity, reason: reason}}
{:rejected_vocab, _} ->
reason = "#{activity_type} in reject list"
{:reject, %{activity: activity, reason: reason}}
end
end
def filter(activity), do: {:ok, activity}
def filter(activity) do
{:pass, activity}
end
@impl true
def describe,

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
@local_actor Pleroma.Web.Endpoint.url() <> "/users/cofe"
test "adds `expires_at` property" do
assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
assert {:filter, %{"type" => "Create", "expires_at" => expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"actor" => @local_actor,
@ -24,7 +24,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
test "keeps existing `expires_at` if it less than the config setting" do
expires_at = DateTime.utc_now() |> Timex.shift(days: 1)
assert {:ok, %{"type" => "Create", "expires_at" => ^expires_at}} =
assert {:pass, %{"type" => "Create", "expires_at" => ^expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"actor" => @local_actor,
@ -37,7 +37,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
test "overwrites existing `expires_at` if it greater than the config setting" do
too_distant_future = DateTime.utc_now() |> Timex.shift(years: 2)
assert {:ok, %{"type" => "Create", "expires_at" => expires_at}} =
assert {:filter, %{"type" => "Create", "expires_at" => expires_at}} =
ActivityExpirationPolicy.filter(%{
"id" => @id,
"actor" => @local_actor,
@ -50,7 +50,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
end
test "ignores remote activities" do
assert {:ok, activity} =
assert {:pass, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"actor" => "https://example.com/users/cofe",
@ -62,7 +62,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
end
test "ignores non-Create/Note activities" do
assert {:ok, activity} =
assert {:pass, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"actor" => "https://example.com/users/cofe",
@ -71,7 +71,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ActivityExpirationPolicyTest do
refute Map.has_key?(activity, "expires_at")
assert {:ok, activity} =
assert {:pass, activity} =
ActivityExpirationPolicy.filter(%{
"id" => "https://example.com/123",
"actor" => "https://example.com/users/cofe",

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
assert {:reject, %{reason: "Scored" <> _}} = AntiFollowbotPolicy.filter(message)
end
test "matches followbots by display name" do
@ -37,7 +37,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
assert {:reject, %{reason: "Scored" <> _}} = AntiFollowbotPolicy.filter(message)
end
test "matches followbots by actor_type" do
@ -52,7 +52,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
assert {:reject, "[AntiFollowbotPolicy]" <> _} = AntiFollowbotPolicy.filter(message)
assert {:reject, %{reason: "Scored" <> _}} = AntiFollowbotPolicy.filter(message)
end
end
@ -69,7 +69,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
{:ok, _} = AntiFollowbotPolicy.filter(message)
{:pass, _} = AntiFollowbotPolicy.filter(message)
end
test "bots if the target follows the bots" do
@ -86,7 +86,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
{:ok, _} = AntiFollowbotPolicy.filter(message)
{:pass, _} = AntiFollowbotPolicy.filter(message)
end
end
@ -102,6 +102,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicyTest do
"id" => "https://example.com/activities/1234"
}
{:ok, _} = AntiFollowbotPolicy.filter(message)
{:pass, _} = AntiFollowbotPolicy.filter(message)
end
end