Allow for follow accepts with object URIs only

Fixes #214
This commit is contained in:
Andrew Godwin 2022-12-28 22:25:07 -07:00
parent d525a2f465
commit 716b74404f
5 changed files with 90 additions and 2 deletions

View File

@ -106,13 +106,15 @@ def domain2() -> Domain:
@pytest.fixture @pytest.fixture
@pytest.mark.django_db @pytest.mark.django_db
def identity(user, domain) -> Identity: def identity(user, domain, keypair) -> Identity:
""" """
Creates a basic test identity with a user and domain. Creates a basic test identity with a user and domain.
""" """
identity = Identity.objects.create( identity = Identity.objects.create(
actor_uri="https://example.com/@test@example.com/", actor_uri="https://example.com/@test@example.com/",
inbox_uri="https://example.com/@test@example.com/inbox/", inbox_uri="https://example.com/@test@example.com/inbox/",
private_key=keypair["private_key"],
public_key=keypair["public_key"],
username="test", username="test",
domain=domain, domain=domain,
name="Test User", name="Test User",
@ -171,6 +173,7 @@ def remote_identity() -> Identity:
domain=domain, domain=domain,
name="Test Remote User", name="Test Remote User",
local=False, local=False,
state="updated",
) )

View File

@ -0,0 +1,56 @@
import json
import pytest
from pytest_httpx import HTTPXMock
from users.models import Follow, FollowStates, Identity, InboxMessage
from users.services import IdentityService
@pytest.mark.django_db
@pytest.mark.parametrize("ref_only", [True, False])
def test_follow(
identity: Identity,
remote_identity: Identity,
stator,
httpx_mock: HTTPXMock,
ref_only: bool,
):
"""
Ensures that follow sending and acceptance works
"""
# Make the follow
follow = IdentityService(remote_identity).follow_from(identity)
assert Follow.objects.get(pk=follow.pk).state == FollowStates.unrequested
# Run stator to make it try and send out the remote request
httpx_mock.add_response(
url="https://remote.test/@test/inbox/",
status_code=202,
)
stator.run_single_cycle_sync()
outbound_data = json.loads(httpx_mock.get_request().content)
assert outbound_data["type"] == "Follow"
assert outbound_data["actor"] == identity.actor_uri
assert outbound_data["object"] == remote_identity.actor_uri
assert outbound_data["id"] == f"{identity.actor_uri}follow/{follow.pk}/"
assert Follow.objects.get(pk=follow.pk).state == FollowStates.local_requested
# Come in with an inbox message of either a reference type or an embedded type
if ref_only:
message = {
"type": "Accept",
"id": "test",
"actor": remote_identity.actor_uri,
"object": outbound_data["id"],
}
else:
del outbound_data["@context"]
message = {
"type": "Accept",
"id": "test",
"actor": remote_identity.actor_uri,
"object": outbound_data,
}
InboxMessage.objects.create(message=message)
# Run stator and ensure that accepted our follow
stator.run_single_cycle_sync()
assert Follow.objects.get(pk=follow.pk).state == FollowStates.accepted

View File

@ -272,6 +272,28 @@ class Follow(StatorModel):
]: ]:
follow.transition_perform(FollowStates.accepted) follow.transition_perform(FollowStates.accepted)
@classmethod
def handle_accept_ref_ap(cls, data):
"""
Handles an incoming Follow Accept for one of our follows where there is
only an object URI reference.
"""
# Ensure the object ref is in a format we expect
bits = data["object"].strip("/").split("/")
if bits[-2] != "follow":
raise ValueError(f"Unknown Follow object URI in Accept: {data['object']}")
# Retrieve the object by PK
follow = cls.objects.get(pk=bits[-1])
# Ensure it's from the right actor
if data["actor"] != follow.target.actor_uri:
raise ValueError("Accept actor does not match its Follow object", data)
# If the follow was waiting to be accepted, transition it
if follow.state in [
FollowStates.unrequested,
FollowStates.local_requested,
]:
follow.transition_perform(FollowStates.accepted)
@classmethod @classmethod
def handle_undo_ap(cls, data): def handle_undo_ap(cls, data):
""" """

View File

@ -65,6 +65,10 @@ class InboxMessageStates(StateGraph):
match instance.message_object_type: match instance.message_object_type:
case "follow": case "follow":
await sync_to_async(Follow.handle_accept_ap)(instance.message) await sync_to_async(Follow.handle_accept_ap)(instance.message)
case None:
await sync_to_async(Follow.handle_accept_ref_ap)(
instance.message
)
case unknown: case unknown:
raise ValueError( raise ValueError(
f"Cannot handle activity of type accept.{unknown}" f"Cannot handle activity of type accept.{unknown}"
@ -113,6 +117,9 @@ class InboxMessageStates(StateGraph):
case "remove": case "remove":
# We are ignoring these right now (probably pinned items) # We are ignoring these right now (probably pinned items)
pass pass
case "move":
# We're ignoring moves for now
pass
case "http://litepub.social/ns#emojireact": case "http://litepub.social/ns#emojireact":
# We're ignoring emoji reactions for now # We're ignoring emoji reactions for now
pass pass

View File

@ -38,7 +38,7 @@ class IdentityService:
""" """
existing_follow = Follow.maybe_get(from_identity, self.identity) existing_follow = Follow.maybe_get(from_identity, self.identity)
if not existing_follow: if not existing_follow:
Follow.create_local(from_identity, self.identity) return Follow.create_local(from_identity, self.identity)
elif existing_follow.state not in FollowStates.group_active(): elif existing_follow.state not in FollowStates.group_active():
existing_follow.transition_perform(FollowStates.unrequested) existing_follow.transition_perform(FollowStates.unrequested)
return cast(Follow, existing_follow) return cast(Follow, existing_follow)