summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Felder <feld@feld.me>2024-03-15 15:10:45 -0400
committerMark Felder <feld@feld.me>2024-08-12 17:06:29 -0400
commit488c4b8b983f99d036e3f3bf67dec782f9959319 (patch)
tree9e16bee7544d50f56b35601b32ffa1d56b9b411b
parentc29441f30d0229192868ec4149a46fea562b0997 (diff)
downloadpleroma-488c4b8b983f99d036e3f3bf67dec782f9959319.tar.gz
pleroma-488c4b8b983f99d036e3f3bf67dec782f9959319.zip
MRF.FODirectReply
Force replies to followers-only posts to always be direct
-rw-r--r--changelog.d/mrf-fodirectreply.add1
-rw-r--r--lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex65
-rw-r--r--test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs87
3 files changed, 153 insertions, 0 deletions
diff --git a/changelog.d/mrf-fodirectreply.add b/changelog.d/mrf-fodirectreply.add
new file mode 100644
index 000000000..c144e1bc0
--- /dev/null
+++ b/changelog.d/mrf-fodirectreply.add
@@ -0,0 +1 @@
+Added MRF.FODirectReply which changes replies to followers-only posts to be direct
diff --git a/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex b/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
new file mode 100644
index 000000000..4eb97afa8
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/fo_direct_reply.ex
@@ -0,0 +1,65 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.FODirectReply do
+ @moduledoc """
+ FODirectReply alters the scope of replies to activities which are Followers Only to be Direct. The purpose of this policy is to prevent broken threads for followers of the reply author because their response was to a user that they are not also following.
+ """
+
+ alias Pleroma.User
+
+ @behaviour Pleroma.Web.ActivityPub.MRF.Policy
+
+ @impl true
+ def filter(
+ %{
+ "type" => "Create",
+ "to" => to,
+ "object" => %{
+ "actor" => actor,
+ "type" => "Note",
+ "inReplyTo" => in_reply_to
+ }
+ } = activity
+ ) do
+ with true <- is_binary(in_reply_to),
+ %User{follower_address: followers_collection, local: true} <- User.get_by_ap_id(actor),
+ true <- followers_only?(in_reply_to) do
+ direct_to = to -- [followers_collection]
+
+ updated_activity =
+ activity
+ |> Map.put("cc", [])
+ |> Map.put("to", direct_to)
+ |> Map.put("directMessage", true)
+ |> put_in(["object", "cc"], [])
+ |> put_in(["object", "to"], direct_to)
+
+ {:ok, updated_activity}
+ else
+ _ -> {:ok, activity}
+ end
+ end
+
+ @impl true
+ def filter(activity), do: {:ok, activity}
+
+ @impl true
+ def describe, do: {:ok, %{}}
+
+ defp followers_only?(parent_ap_id) do
+ with %Pleroma.Object{} = object <- Pleroma.Object.get_by_ap_id(parent_ap_id),
+ object_data <- Map.get(object, :data),
+ %Pleroma.User{} = user <- User.get_cached_by_ap_id(object_data["actor"]) do
+ if user.follower_address in object_data["to"] do
+ true
+ else
+ false
+ end
+ else
+ _ ->
+ false
+ end
+ end
+end
diff --git a/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs b/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs
new file mode 100644
index 000000000..7afc83ffc
--- /dev/null
+++ b/test/pleroma/web/activity_pub/mrf/fo_direct_reply_test.exs
@@ -0,0 +1,87 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.MRF.FODirectReplyTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+
+ require Pleroma.Constants
+
+ alias Pleroma.Object
+ alias Pleroma.Web.ActivityPub.MRF.FODirectReply
+ alias Pleroma.Web.CommonAPI
+
+ test "replying to followers-only/private is changed to direct" do
+ batman = insert(:user, nickname: "batman")
+ robin = insert(:user, nickname: "robin")
+
+ {:ok, post} =
+ CommonAPI.post(batman, %{
+ status: "Has anyone seen Selina Kyle's latest selfies?",
+ visibility: "private"
+ })
+
+ reply = %{
+ "type" => "Create",
+ "actor" => robin.ap_id,
+ "to" => [batman.ap_id, robin.follower_address],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "actor" => robin.ap_id,
+ "content" => "@batman 🤤 ❤️ 🐈‍⬛",
+ "to" => [batman.ap_id, robin.follower_address],
+ "cc" => [],
+ "inReplyTo" => Object.normalize(post).data["id"]
+ }
+ }
+
+ expected_to = [batman.ap_id]
+ expected_cc = []
+
+ assert {:ok, filtered} = FODirectReply.filter(reply)
+
+ assert expected_to == filtered["to"]
+ assert expected_cc == filtered["cc"]
+ assert expected_to == filtered["object"]["to"]
+ assert expected_cc == filtered["object"]["cc"]
+ end
+
+ test "replies to public posts are unmodified" do
+ batman = insert(:user, nickname: "batman")
+ robin = insert(:user, nickname: "robin")
+
+ {:ok, post} =
+ CommonAPI.post(batman, %{status: "Has anyone seen Selina Kyle's latest selfies?"})
+
+ reply = %{
+ "type" => "Create",
+ "actor" => robin.ap_id,
+ "to" => [batman.ap_id, robin.follower_address],
+ "cc" => [],
+ "object" => %{
+ "type" => "Note",
+ "actor" => robin.ap_id,
+ "content" => "@batman 🤤 ❤️ 🐈<200d>⬛",
+ "to" => [batman.ap_id, robin.follower_address],
+ "cc" => [],
+ "inReplyTo" => Object.normalize(post).data["id"]
+ }
+ }
+
+ assert {:ok, filtered} = FODirectReply.filter(reply)
+
+ assert match?(^filtered, reply)
+ end
+
+ test "non-reply posts are unmodified" do
+ batman = insert(:user, nickname: "batman")
+
+ {:ok, post} = CommonAPI.post(batman, %{status: "To the Batmobile!"})
+
+ assert {:ok, filtered} = FODirectReply.filter(post)
+
+ assert match?(^filtered, post)
+ end
+end