diff --git a/activities/models/fan_out.py b/activities/models/fan_out.py index 40d73dd..de5fbf8 100644 --- a/activities/models/fan_out.py +++ b/activities/models/fan_out.py @@ -72,7 +72,10 @@ class FanOutStates(StateGraph): try: await post.author.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(post.to_create_ap()), ) except httpx.RequestError: @@ -85,7 +88,10 @@ class FanOutStates(StateGraph): try: await post.author.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(post.to_update_ap()), ) except httpx.RequestError: @@ -108,7 +114,10 @@ class FanOutStates(StateGraph): try: await post.author.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(post.to_delete_ap()), ) except httpx.RequestError: @@ -130,7 +139,10 @@ class FanOutStates(StateGraph): try: await interaction.identity.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(interaction.to_ap()), ) except httpx.RequestError: @@ -153,7 +165,10 @@ class FanOutStates(StateGraph): try: await interaction.identity.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(interaction.to_undo_ap()), ) except httpx.RequestError: @@ -165,7 +180,10 @@ class FanOutStates(StateGraph): try: await identity.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(fan_out.subject_identity.to_update_ap()), ) except httpx.RequestError: @@ -177,7 +195,10 @@ class FanOutStates(StateGraph): try: await identity.signed_request( method="post", - uri=fan_out.identity.inbox_uri, + uri=( + fan_out.identity.shared_inbox_uri + or fan_out.identity.inbox_uri + ), body=canonicalise(fan_out.subject_identity.to_delete_ap()), ) except httpx.RequestError: @@ -214,6 +235,9 @@ class FanOut(StatorModel): state = StateField(FanOutStates) # The user this event is targeted at + # We always need this, but if there is a shared inbox URL on the user + # we'll deliver to that and won't have fanouts for anyone else with the + # same one. identity = models.ForeignKey( "users.Identity", on_delete=models.CASCADE, diff --git a/activities/models/post.py b/activities/models/post.py index ed28c83..74ea0b4 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -707,7 +707,20 @@ class Post(StatorModel): # If it's a local post, include the author if self.local: targets.add(self.author) - return targets + # Now dedupe the targets based on shared inboxes (we only keep one per + # shared inbox) + deduped_targets = set() + shared_inboxes = set() + for target in targets: + if target.local or not target.shared_inbox_uri: + deduped_targets.add(target) + elif target.shared_inbox_uri not in shared_inboxes: + shared_inboxes.add(target.shared_inbox_uri) + deduped_targets.add(target) + else: + # Their shared inbox is already being sent to + pass + return deduped_targets ### ActivityPub (inbound) ### diff --git a/tests/activities/models/test_post_targets.py b/tests/activities/models/test_post_targets.py index 0fbfdd6..5d235d5 100644 --- a/tests/activities/models/test_post_targets.py +++ b/tests/activities/models/test_post_targets.py @@ -2,7 +2,7 @@ import pytest from asgiref.sync import async_to_sync from activities.models import Post -from users.models import Follow +from users.models import Domain, Follow, Identity @pytest.mark.django_db @@ -50,6 +50,59 @@ def test_post_targets_simple(identity, other_identity, remote_identity): assert targets == {other_identity} +@pytest.mark.django_db +def test_post_targets_shared(identity, other_identity): + """ + Tests that remote identities with the same shared inbox only get one target. + """ + # Create a pair of remote identities that share an inbox URI + domain = Domain.objects.create(domain="remote.test", local=False, state="updated") + remote1 = Identity.objects.create( + actor_uri="https://remote.test/test1/", + inbox_uri="https://remote.test/@test1/inbox/", + shared_inbox_uri="https://remote.test/inbox/", + profile_uri="https://remote.test/@test1/", + username="test1", + domain=domain, + name="Test1", + local=False, + state="updated", + ) + remote2 = Identity.objects.create( + actor_uri="https://remote.test/test2/", + inbox_uri="https://remote.test/@test2/inbox/", + shared_inbox_uri="https://remote.test/inbox/", + profile_uri="https://remote.test/@test2/", + username="test2", + domain=domain, + name="Test2", + local=False, + state="updated", + ) + + # Make a post mentioning one local and two remote identities + post = Post.objects.create( + content="

Test

", + author=identity, + local=True, + ) + post.mentions.add(other_identity) + post.mentions.add(remote1) + post.mentions.add(remote2) + targets = async_to_sync(post.aget_targets)() + + # We should only have one of remote1 or remote2 in there as they share a + # shared inbox URI + assert (targets == {identity, other_identity, remote1}) or ( + targets + == { + identity, + other_identity, + remote2, + } + ) + + @pytest.mark.django_db def test_post_local_only(identity, other_identity, remote_identity): """