From ded40182b0aa6848b55febe73ec7e41eace1e0f6 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Mon, 5 May 2025 15:28:02 +0400 Subject: [PATCH 1/7] Public getting stripped from unlisted activity CC: Add possible tests --- test/fixtures/poast_unlisted.json | 65 +++++++++++++++++++ .../transmogrifier/note_handling_test.exs | 31 +++++++++ .../web/activity_pub/transmogrifier_test.exs | 37 +++++++++++ 3 files changed, 133 insertions(+) create mode 100644 test/fixtures/poast_unlisted.json diff --git a/test/fixtures/poast_unlisted.json b/test/fixtures/poast_unlisted.json new file mode 100644 index 000000000..fa23153ba --- /dev/null +++ b/test/fixtures/poast_unlisted.json @@ -0,0 +1,65 @@ +{ + "@context" : [ + "https://www.w3.org/ns/activitystreams", + "https://poa.st/schemas/litepub-0.1.jsonld", + { + "@language" : "und" + } + ], + "actor" : "https://poa.st/users/TrevorGoodchild", + "attachment" : [], + "attributedTo" : "https://poa.st/users/TrevorGoodchild", + "cc" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "directMessage" : false, + "id" : "https://poa.st/activities/bbd3347a-4a89-4cdb-bf86-4f9eed9506e3", + "object" : { + "actor" : "https://poa.st/users/TrevorGoodchild", + "attachment" : [], + "attributedTo" : "https://poa.st/users/TrevorGoodchild", + "cc" : [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "content" : "@HoroTheWhiteWolf >please let this be his zero fucks given final statement before he joins the 52%+ tranny club", + "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", + "id" : "https://poa.st/objects/7eb785d5-a556-4070-9091-f4afb226466c", + "inReplyTo" : "https://poa.st/objects/71995b41-cfb2-48ce-abce-76d570d54edc", + "published" : "2025-05-03T23:54:07.489885Z", + "repliesCount" : 2, + "sensitive" : false, + "source" : { + "content" : ">please let this be his zero fucks given final statement before he joins the 52%+ tranny club", + "mediaType" : "text/plain" + }, + "summary" : "", + "tag" : [ + { + "href" : "https://poa.st/users/HoroTheWhiteWolf", + "name" : "@HoroTheWhiteWolf", + "type" : "Mention" + } + ], + "to" : [ + "https://poa.st/users/HoroTheWhiteWolf", + "https://poa.st/users/TrevorGoodchild/followers" + ], + "type" : "Note" + }, + "published" : "2025-05-03T23:54:07.489837Z", + "tag" : [ + { + "href" : "https://poa.st/users/HoroTheWhiteWolf", + "name" : "@HoroTheWhiteWolf", + "type" : "Mention" + } + ], + "to" : [ + "https://poa.st/users/HoroTheWhiteWolf", + "https://poa.st/users/TrevorGoodchild/followers" + ], + "type" : "Create" +} diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index fd7a3c772..13982940a 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -786,4 +786,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do assert object.data["context"] == object.data["inReplyTo"] assert modified.data["context"] == object.data["inReplyTo"] end + + test "it keeps the public address in cc in the activity when it is present" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Jason.decode!() + + object = + data["object"] + |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("to", []) + + data = + data + |> Map.put("object", object) + |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("to", []) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) + assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] + end + + test "it tries it with the real poast_unlisted.json, ensuring that public is in the cc" do + data = + File.read!("test/fixtures/poast_unlisted.json") + |> Jason.decode!() + + _user = insert(:user, ap_id: data["actor"]) + + {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) + assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] + end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index e0395d7bb..ef6e004f1 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -757,6 +757,43 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute recipient.follower_address in fixed_object["cc"] refute recipient.follower_address in fixed_object["to"] end + + test "preserves public URL in cc even when not explicitly mentioned", %{user: user} do + public_url = "https://www.w3.org/ns/activitystreams#Public" + + # Case 1: Public URL in cc but no mentions + object = %{ + "actor" => user.ap_id, + "to" => ["https://social.beepboop.ga/users/dirb"], + "cc" => [public_url], + "tag" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["cc"] + + # Case 2: Public URL in cc, with mentions but public not in to + object = %{ + "actor" => user.ap_id, + "to" => ["https://pleroma.gold/users/user1"], + "cc" => [public_url], + "tag" => [%{"type" => "Mention", "href" => "https://pleroma.gold/users/user1"}] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["cc"] + + # Case 3: Public URL in to, it should be moved to to + object = %{ + "actor" => user.ap_id, + "to" => [public_url], + "cc" => [], + "tag" => [] + } + + fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) + assert public_url in fixed_object["to"] + end end describe "fix_summary/1" do From 27ec46814cfb5515b16727d36bb028b5634b6b5d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:27:19 -0700 Subject: [PATCH 2/7] Revert "Public getting stripped from unlisted activity CC: Add possible tests" This reverts commit ded40182b0aa6848b55febe73ec7e41eace1e0f6. --- test/fixtures/poast_unlisted.json | 65 ------------------- .../transmogrifier/note_handling_test.exs | 31 --------- .../web/activity_pub/transmogrifier_test.exs | 37 ----------- 3 files changed, 133 deletions(-) delete mode 100644 test/fixtures/poast_unlisted.json diff --git a/test/fixtures/poast_unlisted.json b/test/fixtures/poast_unlisted.json deleted file mode 100644 index fa23153ba..000000000 --- a/test/fixtures/poast_unlisted.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "@context" : [ - "https://www.w3.org/ns/activitystreams", - "https://poa.st/schemas/litepub-0.1.jsonld", - { - "@language" : "und" - } - ], - "actor" : "https://poa.st/users/TrevorGoodchild", - "attachment" : [], - "attributedTo" : "https://poa.st/users/TrevorGoodchild", - "cc" : [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "directMessage" : false, - "id" : "https://poa.st/activities/bbd3347a-4a89-4cdb-bf86-4f9eed9506e3", - "object" : { - "actor" : "https://poa.st/users/TrevorGoodchild", - "attachment" : [], - "attributedTo" : "https://poa.st/users/TrevorGoodchild", - "cc" : [ - "https://www.w3.org/ns/activitystreams#Public" - ], - "content" : "@HoroTheWhiteWolf >please let this be his zero fucks given final statement before he joins the 52%+ tranny club", - "context" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "conversation" : "https://poa.st/contexts/c6d125f1-4e7f-43bd-aa31-33de4d90d049", - "id" : "https://poa.st/objects/7eb785d5-a556-4070-9091-f4afb226466c", - "inReplyTo" : "https://poa.st/objects/71995b41-cfb2-48ce-abce-76d570d54edc", - "published" : "2025-05-03T23:54:07.489885Z", - "repliesCount" : 2, - "sensitive" : false, - "source" : { - "content" : ">please let this be his zero fucks given final statement before he joins the 52%+ tranny club", - "mediaType" : "text/plain" - }, - "summary" : "", - "tag" : [ - { - "href" : "https://poa.st/users/HoroTheWhiteWolf", - "name" : "@HoroTheWhiteWolf", - "type" : "Mention" - } - ], - "to" : [ - "https://poa.st/users/HoroTheWhiteWolf", - "https://poa.st/users/TrevorGoodchild/followers" - ], - "type" : "Note" - }, - "published" : "2025-05-03T23:54:07.489837Z", - "tag" : [ - { - "href" : "https://poa.st/users/HoroTheWhiteWolf", - "name" : "@HoroTheWhiteWolf", - "type" : "Mention" - } - ], - "to" : [ - "https://poa.st/users/HoroTheWhiteWolf", - "https://poa.st/users/TrevorGoodchild/followers" - ], - "type" : "Create" -} diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs index 13982940a..fd7a3c772 100644 --- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs @@ -786,35 +786,4 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do assert object.data["context"] == object.data["inReplyTo"] assert modified.data["context"] == object.data["inReplyTo"] end - - test "it keeps the public address in cc in the activity when it is present" do - data = - File.read!("test/fixtures/mastodon-post-activity.json") - |> Jason.decode!() - - object = - data["object"] - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("to", []) - - data = - data - |> Map.put("object", object) - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) - |> Map.put("to", []) - - {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) - assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] - end - - test "it tries it with the real poast_unlisted.json, ensuring that public is in the cc" do - data = - File.read!("test/fixtures/poast_unlisted.json") - |> Jason.decode!() - - _user = insert(:user, ap_id: data["actor"]) - - {:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(data) - assert modified.data["cc"] == ["https://www.w3.org/ns/activitystreams#Public"] - end end diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs index ef6e004f1..e0395d7bb 100644 --- a/test/pleroma/web/activity_pub/transmogrifier_test.exs +++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs @@ -757,43 +757,6 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do refute recipient.follower_address in fixed_object["cc"] refute recipient.follower_address in fixed_object["to"] end - - test "preserves public URL in cc even when not explicitly mentioned", %{user: user} do - public_url = "https://www.w3.org/ns/activitystreams#Public" - - # Case 1: Public URL in cc but no mentions - object = %{ - "actor" => user.ap_id, - "to" => ["https://social.beepboop.ga/users/dirb"], - "cc" => [public_url], - "tag" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["cc"] - - # Case 2: Public URL in cc, with mentions but public not in to - object = %{ - "actor" => user.ap_id, - "to" => ["https://pleroma.gold/users/user1"], - "cc" => [public_url], - "tag" => [%{"type" => "Mention", "href" => "https://pleroma.gold/users/user1"}] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["cc"] - - # Case 3: Public URL in to, it should be moved to to - object = %{ - "actor" => user.ap_id, - "to" => [public_url], - "cc" => [], - "tag" => [] - } - - fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address) - assert public_url in fixed_object["to"] - end end describe "fix_summary/1" do From 9f79df75082cfc563ce7816a1839800aa22ec350 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:28:58 -0700 Subject: [PATCH 3/7] Add test demonstrating public getting stripped from unlisted activity CC --- .../web/activity_pub/publisher_test.exs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 99ed42877..ec3201b96 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -520,4 +520,73 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end + + test "retains public address in cc for unlisted activities" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + # Call prepare_one without an explicit cc parameter (default in production) + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id + }) + + # Parse the JSON to verify the cc field in the federated message + {:ok, decoded} = Jason.decode(prepared.json) + + # The public address should be preserved in the cc field + # Currently this will fail because it's being removed + assert @as_public in decoded["cc"] + + # For verification, also test with an explicit cc parameter + # to show the cc field is completely replaced + prepared_with_cc = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded_with_cc} = Jason.decode(prepared_with_cc.json) + + # Verify cc is completely replaced with the provided value + assert decoded_with_cc["cc"] == ["https://example.com/specific/user"] + end + + test "public address in cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public, "https://example.org/users/other"], + "to" => [user.follower_address] + } + ) + + assert @as_public in activity.data["cc"] + + prepared_with_public_cc = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: [@as_public] + }) + + {:ok, decoded_with_public_cc} = Jason.decode(prepared_with_public_cc.json) + + assert @as_public in decoded_with_public_cc["cc"] + end end From 23be24b92fa4f868b814b2c6927f2a6a69fa882d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:37:50 -0700 Subject: [PATCH 4/7] Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instances --- changelog.d/preserve-public-cc.fix | 1 + lib/pleroma/web/activity_pub/publisher.ex | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 changelog.d/preserve-public-cc.fix diff --git a/changelog.d/preserve-public-cc.fix b/changelog.d/preserve-public-cc.fix new file mode 100644 index 000000000..1b20ce9ad --- /dev/null +++ b/changelog.d/preserve-public-cc.fix @@ -0,0 +1 @@ +Fix federation issue where Public visibility information in cc field was lost when sent to remote servers, causing posts to appear with inconsistent visibility across instances diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 0de3a0d43..762c991fd 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -93,7 +93,20 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - cc = Map.get(params, :cc, []) + param_cc = Map.get(params, :cc, []) + + original_cc = Map.get(data, "cc", []) + + public_address = Pleroma.Constants.as_public() + + # Avoid overriding explicitly set cc values for specific recipients. + # e.g., this ensures unlisted posts are visible to users on other servers. + cc = + if public_address in original_cc and param_cc == [] do + [public_address] + else + param_cc + end json = data From d3adc3e05e09fdcb663ec1a3e20c1bc2d04a6ab5 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 21:59:26 -0700 Subject: [PATCH 5/7] Split this cc test into two individual cases --- .../web/activity_pub/publisher_test.exs | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index ec3201b96..7bc571595 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -521,9 +521,11 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert decoded["cc"] == [] end - test "retains public address in cc for unlisted activities" do + test "unlisted activities retain public address in cc" do user = insert(:user) + # simulate unlistd activity by only having + # public address in cc activity = insert(:note_activity, user: user, @@ -535,58 +537,66 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert @as_public in activity.data["cc"] - # Call prepare_one without an explicit cc parameter (default in production) prepared = Publisher.prepare_one(%{ inbox: "https://remote.instance/users/someone/inbox", activity_id: activity.id }) - # Parse the JSON to verify the cc field in the federated message {:ok, decoded} = Jason.decode(prepared.json) - # The public address should be preserved in the cc field - # Currently this will fail because it's being removed assert @as_public in decoded["cc"] - - # For verification, also test with an explicit cc parameter - # to show the cc field is completely replaced - prepared_with_cc = - Publisher.prepare_one(%{ - inbox: "https://remote.instance/users/someone/inbox", - activity_id: activity.id, - cc: ["https://example.com/specific/user"] - }) - - {:ok, decoded_with_cc} = Jason.decode(prepared_with_cc.json) - - # Verify cc is completely replaced with the provided value - assert decoded_with_cc["cc"] == ["https://example.com/specific/user"] end test "public address in cc parameter is preserved" do user = insert(:user) + cc_with_public = [@as_public, "https://example.org/users/other"] + activity = insert(:note_activity, user: user, data_attrs: %{ - "cc" => [@as_public, "https://example.org/users/other"], + "cc" => cc_with_public, "to" => [user.follower_address] } ) assert @as_public in activity.data["cc"] - prepared_with_public_cc = + prepared = Publisher.prepare_one(%{ inbox: "https://remote.instance/users/someone/inbox", activity_id: activity.id, - cc: [@as_public] + cc: cc_with_public }) - {:ok, decoded_with_public_cc} = Jason.decode(prepared_with_public_cc.json) + {:ok, decoded} = Jason.decode(prepared.json) - assert @as_public in decoded_with_public_cc["cc"] + assert cc_with_public == decoded["cc"] + end + + test "cc parameter is preserved" do + user = insert(:user) + + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => ["https://example.com/specific/user"], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://example.com/specific/user"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == ["https://example.com/specific/user"] end end From fe6d2ecc970008f99f9d948b86e5da07e80c2a29 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 22:33:57 -0700 Subject: [PATCH 6/7] Test for unlisted but Publisher param_cc is not empty --- .../web/activity_pub/publisher_test.exs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 7bc571595..b7ff0ed5f 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -546,6 +546,28 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do {:ok, decoded} = Jason.decode(prepared.json) assert @as_public in decoded["cc"] + + # maybe we also have another inbox in cc + # during Publishing + activity = + insert(:note_activity, + user: user, + data_attrs: %{ + "cc" => [@as_public], + "to" => [user.follower_address] + } + ) + + prepared = + Publisher.prepare_one(%{ + inbox: "https://remote.instance/users/someone/inbox", + activity_id: activity.id, + cc: ["https://remote.instance/users/someone_else/inbox"] + }) + + {:ok, decoded} = Jason.decode(prepared.json) + + assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"] end test "public address in cc parameter is preserved" do From 7c64bfaace454185c4428fec0e7247ba93fff048 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 12 Jun 2025 22:42:40 -0700 Subject: [PATCH 7/7] Include public address in cc if original activity specified it and Publisher param_cc also has values --- lib/pleroma/web/activity_pub/publisher.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 762c991fd..f160f1e17 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -99,11 +99,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do public_address = Pleroma.Constants.as_public() - # Avoid overriding explicitly set cc values for specific recipients. - # e.g., this ensures unlisted posts are visible to users on other servers. + # Ensure unlisted posts don't lose the public address in the cc + # if the param_cc was set cc = - if public_address in original_cc and param_cc == [] do - [public_address] + if public_address in original_cc and public_address not in param_cc do + [public_address | param_cc] else param_cc end