From d310f99d6aa777aa03215c29f469140221716f9f Mon Sep 17 00:00:00 2001 From: FloatingGhost Date: Mon, 22 May 2023 23:53:44 +0100 Subject: [PATCH] Add MRFs for direct message manipulation --- .../akkoma/translators/libre_translate.ex | 4 +- lib/pleroma/user.ex | 15 ++++++ lib/pleroma/web/activity_pub/mrf.ex | 3 +- .../mrf/direct_message_disabled_policy.ex | 46 +++++++++++++++++ ...eject_newly_created_account_note_policy.ex | 49 +++++++++++++++++++ .../api_spec/operations/account_operation.ex | 11 +++++ ...2213837_add_unfollowed_dm_restrictions.exs | 10 ++++ .../translators/libre_translate_test.exs | 3 +- test/pleroma/user_test.exs | 32 ++++++++++++ .../direct_message_disabled_policy_test.exs | 42 ++++++++++++++++ 10 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 lib/pleroma/web/activity_pub/mrf/direct_message_disabled_policy.ex create mode 100644 lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex create mode 100644 priv/repo/migrations/20230522213837_add_unfollowed_dm_restrictions.exs create mode 100644 test/pleroma/web/activity_pub/mrf/direct_message_disabled_policy_test.exs diff --git a/lib/pleroma/akkoma/translators/libre_translate.ex b/lib/pleroma/akkoma/translators/libre_translate.ex index 5b08a6384..80956ab66 100644 --- a/lib/pleroma/akkoma/translators/libre_translate.ex +++ b/lib/pleroma/akkoma/translators/libre_translate.ex @@ -39,9 +39,9 @@ def translate(string, from_language, to_language) do detected = if Map.has_key?(body, "detectedLanguage") do get_in(body, ["detectedLanguage", "language"]) - else + else from_language || "" - end + end {:ok, detected, translated} else diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index ead37ccca..7f4dccf27 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -158,6 +158,8 @@ defmodule Pleroma.User do field(:last_status_at, :naive_datetime) field(:language, :string) field(:status_ttl_days, :integer, default: nil) + field(:accepts_direct_messages_from_followed, :boolean) + field(:accepts_direct_messages_from_not_followed, :boolean) embeds_one( :notification_settings, @@ -2722,4 +2724,17 @@ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do not is_nil(HashtagFollow.get(user, hashtag)) end + + def accepts_direct_messages?(%User{} = receiver, %User{} = sender) do + cond do + User.following?(receiver, sender) && receiver.accepts_direct_messages_from_followed == true -> + true + + receiver.accepts_direct_messages_from_not_followed == true -> + true + + true -> + false + end + end end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 6ecd62c99..88a58421e 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -147,7 +147,8 @@ def get_policies do |> Enum.concat([ Pleroma.Web.ActivityPub.MRF.HashtagPolicy, Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy, - Pleroma.Web.ActivityPub.MRF.NormalizeMarkup + Pleroma.Web.ActivityPub.MRF.NormalizeMarkup, + Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy ]) |> Enum.uniq() end diff --git a/lib/pleroma/web/activity_pub/mrf/direct_message_disabled_policy.ex b/lib/pleroma/web/activity_pub/mrf/direct_message_disabled_policy.ex new file mode 100644 index 000000000..27a59c4f1 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/direct_message_disabled_policy.ex @@ -0,0 +1,46 @@ +defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility + + @moduledoc """ + Removes entries from the "To" field from direct messages if the user has requested to not + allow direct messages + """ + + @impl true + def filter( + %{ + "type" => "Note", + "actor" => actor + } = activity + ) do + with true <- Visibility.is_direct?(%{data: activity}), + recipients <- Map.get(activity, "to"), + sender <- User.get_cached_by_ap_id(actor) do + new_to = + Enum.filter(recipients, fn recv -> + should_filter?(sender, recv) + end) + + {:ok, Map.put(activity, :to, new_to)} + else + _ -> {:ok, activity} + end + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} + + defp should_filter?(sender, receiver_ap_id) do + with %User{local: true} = receiver <- User.get_cached_by_ap_id(receiver_ap_id) do + User.accepts_direct_messages?(receiver, sender) + else + _ -> false + end + end +end diff --git a/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex b/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex new file mode 100644 index 000000000..4a2ab759a --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/reject_newly_created_account_note_policy.ex @@ -0,0 +1,49 @@ +defmodule Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy do + @behaviour Pleroma.Web.ActivityPub.MRF.Policy + + alias Pleroma.User + + @moduledoc """ + Rejects notes from accounts that were created below a certain threshold of time ago + """ + @impl true + def filter( + %{ + "type" => type, + "actor" => actor + } = activity + ) when type in ["Note", "Create"] do + min_age = Pleroma.Config.get([:mrf_reject_newly_created_account_notes, :age]) + + with %User{} = user <- Pleroma.User.get_cached_by_ap_id(actor), + true <- Timex.diff(Timex.now(), user.inserted_at, :seconds) < min_age do + {:reject, "Account created too recently"} + else + _ -> {:ok, activity} + end + end + + @impl true + def filter(object), do: {:ok, object} + + @impl true + def describe, do: {:ok, %{}} + + @impl true + def config_description do + %{ + key: :mrf_reject_newly_created_account_notes, + related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy", + label: "MRF Reject New Accounts", + description: "Reject notes from accounts created too recently", + children: [ + %{ + key: :age, + type: :integer, + description: "Time below which to reject (in seconds)", + suggestions: [86_400] + } + ] + } + end +end diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 894ad5db0..7971b5363 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -708,6 +708,17 @@ defp update_credentials_request do nullable: true, description: "Number of days after which statuses will be deleted. Set to -1 to disable." + }, + accepts_direct_messages_from_followed: %Schema{ + type: :boolean, + nullable: true, + description: + "Whether to accept DMs from people you follow (will be overridden by accepts_direct_messages_from_not_followed if true)" + }, + accepts_direct_messages_from_not_followed: %Schema{ + type: :boolean, + nullable: true, + description: "Whether to accept DMs from everyone" } }, example: %{ diff --git a/priv/repo/migrations/20230522213837_add_unfollowed_dm_restrictions.exs b/priv/repo/migrations/20230522213837_add_unfollowed_dm_restrictions.exs new file mode 100644 index 000000000..a373b11ee --- /dev/null +++ b/priv/repo/migrations/20230522213837_add_unfollowed_dm_restrictions.exs @@ -0,0 +1,10 @@ +defmodule Pleroma.Repo.Migrations.AddUnfollowedDmRestrictions do + use Ecto.Migration + + def change do + alter table(:users) do + add(:accepts_direct_messages_from_followed, :boolean, default: true) + add(:accepts_direct_messages_from_not_followed, :boolean, default: true) + end + end +end diff --git a/test/pleroma/translators/libre_translate_test.exs b/test/pleroma/translators/libre_translate_test.exs index a93f408f5..2ba75ec0e 100644 --- a/test/pleroma/translators/libre_translate_test.exs +++ b/test/pleroma/translators/libre_translate_test.exs @@ -146,8 +146,7 @@ test "should work when no detected language is received" do } end) - assert {:ok, "", "I will crush you"} = - LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") + assert {:ok, "", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en") end end end diff --git a/test/pleroma/user_test.exs b/test/pleroma/user_test.exs index 12ccc6bf4..094799968 100644 --- a/test/pleroma/user_test.exs +++ b/test/pleroma/user_test.exs @@ -2756,4 +2756,36 @@ test "should not error when trying to unfollow a hashtag twice" do assert user.followed_hashtags |> Enum.count() == 0 end end + + describe "accepts_direct_messages?/2" do + test "should return true if the recipient follows the sender and has turned on 'accept from follows'" do + recipient = + insert(:user, %{ + accepts_direct_messages_from_followed: true, + accepts_direct_messages_from_not_followed: false + }) + + sender = insert(:user) + + refute User.accepts_direct_messages?(recipient, sender) + + CommonAPI.follow(recipient, sender) + + assert User.accepts_direct_messages?(recipient, sender) + end + + test "should return true if the recipient has 'accept from everyone' on" do + recipient = insert(:user, %{accepts_direct_messages_from_not_followed: true}) + sender = insert(:user) + + assert User.accepts_direct_messages?(recipient, sender) + end + + test "should return false if the receipient has 'accept from everyone' off" do + recipient = insert(:user, %{accepts_direct_messages_from_not_followed: false}) + sender = insert(:user) + + refute User.accepts_direct_messages?(recipient, sender) + end + end end diff --git a/test/pleroma/web/activity_pub/mrf/direct_message_disabled_policy_test.exs b/test/pleroma/web/activity_pub/mrf/direct_message_disabled_policy_test.exs new file mode 100644 index 000000000..50ed36a0b --- /dev/null +++ b/test/pleroma/web/activity_pub/mrf/direct_message_disabled_policy_test.exs @@ -0,0 +1,42 @@ +defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicyTest do + use Pleroma.DataCase + import Pleroma.Factory + + alias Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy + alias Pleroma.User + + describe "strips recipients" do + test "when the user denies the direct message" do + sender = insert(:user) + recipient = insert(:user, %{accepts_direct_messages_from_not_followed: false}) + + refute User.accepts_direct_messages?(recipient, sender) + + message = %{ + "actor" => sender.ap_id, + "to" => [recipient.ap_id], + "cc" => [], + "type" => "Note" + } + + assert {:ok, %{to: []}} = DirectMessageDisabledPolicy.filter(message) + end + + test "when the user does not deny the direct message" do + sender = insert(:user) + recipient = insert(:user, %{accepts_direct_messages_from_not_followed: true}) + + assert User.accepts_direct_messages?(recipient, sender) + + message = %{ + "actor" => sender.ap_id, + "to" => [recipient.ap_id], + "cc" => [], + "type" => "Note" + } + + assert {:ok, message} = DirectMessageDisabledPolicy.filter(message) + assert message.to == [recipient.ap_id] + end + end +end