From fbfad9fbf5e061cb7c658dada3c4014c9796021c Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Thu, 10 Nov 2022 23:42:43 -0700 Subject: Inbound and outbound follows basic working --- users/models/domain.py | 12 +++++ users/models/follow.py | 114 ++++++++++++++++++++++++++++++++++++++---- users/models/inbox_message.py | 18 +++++-- 3 files changed, 128 insertions(+), 16 deletions(-) (limited to 'users/models') diff --git a/users/models/domain.py b/users/models/domain.py index 4ac6ee9..a3815ee 100644 --- a/users/models/domain.py +++ b/users/models/domain.py @@ -81,3 +81,15 @@ class Domain(models.Model): def __str__(self): return self.domain + + def save(self, *args, **kwargs): + # Ensure that we are not conflicting with other domains + if Domain.objects.filter(service_domain=self.domain).exists(): + raise ValueError( + f"Domain {self.domain} is already a service domain elsewhere!" + ) + if self.service_domain: + if Domain.objects.filter(domain=self.service_domain).exists(): + raise ValueError( + f"Service domain {self.service_domain} is already a domain elsewhere!" + ) diff --git a/users/models/follow.py b/users/models/follow.py index 6f62481..94ad40f 100644 --- a/users/models/follow.py +++ b/users/models/follow.py @@ -2,24 +2,110 @@ from typing import Optional from django.db import models +from core.ld import canonicalise +from core.signatures import HttpSignature from stator.models import State, StateField, StateGraph, StatorModel class FollowStates(StateGraph): unrequested = State(try_interval=30) - requested = State(try_interval=24 * 60 * 60) - accepted = State() - - unrequested.transitions_to(requested) - requested.transitions_to(accepted) + local_requested = State(try_interval=24 * 60 * 60) + remote_requested = State(try_interval=24 * 60 * 60) + accepted = State(externally_progressed=True) + undone_locally = State(try_interval=60 * 60) + undone_remotely = State() + + unrequested.transitions_to(local_requested) + unrequested.transitions_to(remote_requested) + local_requested.transitions_to(accepted) + remote_requested.transitions_to(accepted) + accepted.transitions_to(undone_locally) + undone_locally.transitions_to(undone_remotely) @classmethod async def handle_unrequested(cls, instance: "Follow"): - print("Would have tried to follow on", instance) + # Re-retrieve the follow with more things linked + follow = await Follow.objects.select_related( + "source", "source__domain", "target" + ).aget(pk=instance.pk) + # Remote follows should not be here + if not follow.source.local: + return cls.remote_requested + # Construct the request + request = canonicalise( + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": follow.uri, + "type": "Follow", + "actor": follow.source.actor_uri, + "object": follow.target.actor_uri, + } + ) + # Sign it and send it + await HttpSignature.signed_request( + follow.target.inbox_uri, request, follow.source + ) + return cls.local_requested + + @classmethod + async def handle_local_requested(cls, instance: "Follow"): + # TODO: Resend follow requests occasionally + pass + + @classmethod + async def handle_remote_requested(cls, instance: "Follow"): + # Re-retrieve the follow with more things linked + follow = await Follow.objects.select_related( + "source", "source__domain", "target" + ).aget(pk=instance.pk) + # Send an accept + request = canonicalise( + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": follow.target.actor_uri + f"follow/{follow.pk}/#accept", + "type": "Follow", + "actor": follow.source.actor_uri, + "object": { + "id": follow.uri, + "type": "Follow", + "actor": follow.source.actor_uri, + "object": follow.target.actor_uri, + }, + } + ) + # Sign it and send it + await HttpSignature.signed_request( + follow.source.inbox_uri, + request, + identity=follow.target, + ) + return cls.accepted @classmethod - async def handle_requested(cls, instance: "Follow"): - print("Would have tried to requested on", instance) + async def handle_undone_locally(cls, instance: "Follow"): + follow = Follow.objects.select_related( + "source", "source__domain", "target" + ).get(pk=instance.pk) + # Construct the request + request = canonicalise( + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": follow.uri + "#undo", + "type": "Undo", + "actor": follow.source.actor_uri, + "object": { + "id": follow.uri, + "type": "Follow", + "actor": follow.source.actor_uri, + "object": follow.target.actor_uri, + }, + } + ) + # Sign it and send it + await HttpSignature.signed_request( + follow.target.inbox_uri, request, follow.source + ) + return cls.undone_remotely class Follow(StatorModel): @@ -83,11 +169,17 @@ class Follow(StatorModel): follow = cls.maybe_get(source=source, target=target) if follow is None: follow = Follow.objects.create(source=source, target=target, uri=uri) - if follow.state == FollowStates.fresh: - follow.transition_perform(FollowStates.requested) + if follow.state == FollowStates.unrequested: + follow.transition_perform(FollowStates.remote_requested) @classmethod def remote_accepted(cls, source, target): + print(f"accepted follow source {source} target {target}") follow = cls.maybe_get(source=source, target=target) - if follow and follow.state == FollowStates.requested: + print(f"accepting follow {follow}") + if follow and follow.state in [ + FollowStates.unrequested, + FollowStates.local_requested, + ]: follow.transition_perform(FollowStates.accepted) + print("accepted") diff --git a/users/models/inbox_message.py b/users/models/inbox_message.py index 0dbdc3a..54b05e9 100644 --- a/users/models/inbox_message.py +++ b/users/models/inbox_message.py @@ -13,7 +13,7 @@ class InboxMessageStates(StateGraph): @classmethod async def handle_received(cls, instance: "InboxMessage"): - type = instance.message["type"].lower() + type = instance.message_type if type == "follow": await instance.follow_request() elif type == "accept": @@ -30,6 +30,7 @@ class InboxMessageStates(StateGraph): raise ValueError(f"Cannot handle activity of type undo.{inner_type}") else: raise ValueError(f"Cannot handle activity of type {type}") + return cls.processed class InboxMessage(StatorModel): @@ -60,10 +61,17 @@ class InboxMessage(StatorModel): """ Handles an incoming acceptance of one of our follow requests """ - Follow.remote_accepted( - source=Identity.by_actor_uri_with_create(self.message["actor"]), - target=Identity.by_actor_uri(self.message["object"]), - ) + target = Identity.by_actor_uri_with_create(self.message["actor"]) + source = Identity.by_actor_uri(self.message["object"]["actor"]) + if source is None: + raise ValueError( + f"Follow-Accept has invalid source {self.message['object']['actor']}" + ) + Follow.remote_accepted(source=source, target=target) + + @property + def message_type(self): + return self.message["type"].lower() async def follow_undo(self): """ -- cgit v1.2.3