From 5a5f4794d3efdc13efed1954a7cd88293f1d955c Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Tue, 27 Oct 2026 20:45:32 +0400 Subject: [PATCH] perf(federation): always prefer the shared inbox Followers-only posts and DMs went to each recipient's personal inbox, so a followers-only post was sent once per follower on the same server. Mastodon, GoToSocial and Iceshrimp.NET always use the shared inbox. Only activities with bcc or bto still go to personal inboxes. Based on Akkoma a80444041. Co-authored-by: Oneric --- changelog.d/shared-inbox.change | 1 + lib/pleroma/web/activity_pub/publisher.ex | 38 +++++------------ .../web/activity_pub/publisher_test.exs | 42 ++++++++++++++++++- 3 files changed, 52 insertions(+), 29 deletions(-) create mode 100644 changelog.d/shared-inbox.change diff --git a/changelog.d/shared-inbox.change b/changelog.d/shared-inbox.change new file mode 100644 index 000000000..0cb74711e --- /dev/null +++ b/changelog.d/shared-inbox.change @@ -0,0 +1 @@ +Deliver to shared inboxes whenever possible, including followers-only posts and DMs, so each server gets one copy diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 17fae2a86..89c8168d4 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -281,36 +281,18 @@ defmodule Pleroma.Web.ActivityPub.Publisher do defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox @doc """ - Determine a user inbox to use based on heuristics. These heuristics - are based on an approximation of the ``sharedInbox`` rules in the - [ActivityPub specification][ap-sharedinbox]. + Determines the inbox to deliver an activity to. - Please do not edit this function (or its children) without reading - the spec, as editing the code is likely to introduce some breakage - without some familiarity. - - [ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery + Like Mastodon, GoToSocial and Akkoma, this always prefers the shared inbox, + which saves duplicate deliveries to the same server. Only activities with + `bcc` or `bto` go to personal inboxes, because the receiver rebuilds that + addressing from the inbox the activity arrived at. """ - def determine_inbox( - %Activity{data: activity_data}, - %User{inbox: inbox} = user - ) do - to = activity_data["to"] || [] - cc = activity_data["cc"] || [] - type = activity_data["type"] - - cond do - type == "Delete" -> - maybe_use_sharedinbox(user) - - Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc -> - maybe_use_sharedinbox(user) - - length(to) + length(cc) > 1 -> - maybe_use_sharedinbox(user) - - true -> - inbox + def determine_inbox(%Activity{data: activity_data}, %User{inbox: inbox} = user) do + if Enum.empty?(activity_data["bcc"] || []) and Enum.empty?(activity_data["bto"] || []) do + maybe_use_sharedinbox(user) + else + inbox end end diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index e8b6ecf38..0c9035b4e 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -137,7 +137,7 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" end - test "it returns inbox for messages involving single recipients in total" do + test "it returns sharedInbox for messages to a single recipient" do user = insert(:user, %{ shared_inbox: "http://example.com/inbox", @@ -148,6 +148,46 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do data: %{"to" => [user.ap_id], "cc" => []} } + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns sharedInbox for followers-only messages" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + author = insert(:user) + + activity = %Activity{ + data: %{"to" => [author.follower_address], "cc" => []} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox" + end + + test "it returns inbox for messages with bcc or bto, which needs personal inboxes" do + user = + insert(:user, %{ + shared_inbox: "http://example.com/inbox", + inbox: "http://example.com/personal-inbox" + }) + + for field <- ["bcc", "bto"] do + activity = %Activity{ + data: %{"to" => [@as_public], "cc" => [], field => [user.ap_id]} + } + + assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" + end + end + + test "it returns inbox when the user has no sharedInbox" do + user = insert(:user, %{shared_inbox: nil, inbox: "http://example.com/personal-inbox"}) + + activity = %Activity{data: %{"to" => [@as_public], "cc" => []}} + assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox" end end