Perform shared inbox delivery

This commit is contained in:
Andrew Godwin 2023-01-08 13:46:40 -07:00
parent f4a8a96b81
commit a875dd7a54
3 changed files with 99 additions and 9 deletions

View File

@ -72,7 +72,10 @@ class FanOutStates(StateGraph):
try: try:
await post.author.signed_request( await post.author.signed_request(
method="post", 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()), body=canonicalise(post.to_create_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -85,7 +88,10 @@ class FanOutStates(StateGraph):
try: try:
await post.author.signed_request( await post.author.signed_request(
method="post", 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()), body=canonicalise(post.to_update_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -108,7 +114,10 @@ class FanOutStates(StateGraph):
try: try:
await post.author.signed_request( await post.author.signed_request(
method="post", 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()), body=canonicalise(post.to_delete_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -130,7 +139,10 @@ class FanOutStates(StateGraph):
try: try:
await interaction.identity.signed_request( await interaction.identity.signed_request(
method="post", 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()), body=canonicalise(interaction.to_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -153,7 +165,10 @@ class FanOutStates(StateGraph):
try: try:
await interaction.identity.signed_request( await interaction.identity.signed_request(
method="post", 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()), body=canonicalise(interaction.to_undo_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -165,7 +180,10 @@ class FanOutStates(StateGraph):
try: try:
await identity.signed_request( await identity.signed_request(
method="post", 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()), body=canonicalise(fan_out.subject_identity.to_update_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -177,7 +195,10 @@ class FanOutStates(StateGraph):
try: try:
await identity.signed_request( await identity.signed_request(
method="post", 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()), body=canonicalise(fan_out.subject_identity.to_delete_ap()),
) )
except httpx.RequestError: except httpx.RequestError:
@ -214,6 +235,9 @@ class FanOut(StatorModel):
state = StateField(FanOutStates) state = StateField(FanOutStates)
# The user this event is targeted at # 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( identity = models.ForeignKey(
"users.Identity", "users.Identity",
on_delete=models.CASCADE, on_delete=models.CASCADE,

View File

@ -707,7 +707,20 @@ class Post(StatorModel):
# If it's a local post, include the author # If it's a local post, include the author
if self.local: if self.local:
targets.add(self.author) 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) ### ### ActivityPub (inbound) ###

View File

@ -2,7 +2,7 @@ import pytest
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from activities.models import Post from activities.models import Post
from users.models import Follow from users.models import Domain, Follow, Identity
@pytest.mark.django_db @pytest.mark.django_db
@ -50,6 +50,59 @@ def test_post_targets_simple(identity, other_identity, remote_identity):
assert targets == {other_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="<p>Test</p>",
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 @pytest.mark.django_db
def test_post_local_only(identity, other_identity, remote_identity): def test_post_local_only(identity, other_identity, remote_identity):
""" """