186 lines
6.2 KiB
Python
186 lines
6.2 KiB
Python
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)
|
|
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"):
|
|
# 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_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):
|
|
"""
|
|
When one user (the source) follows other (the target)
|
|
"""
|
|
|
|
source = models.ForeignKey(
|
|
"users.Identity",
|
|
on_delete=models.CASCADE,
|
|
related_name="outbound_follows",
|
|
)
|
|
target = models.ForeignKey(
|
|
"users.Identity",
|
|
on_delete=models.CASCADE,
|
|
related_name="inbound_follows",
|
|
)
|
|
|
|
uri = models.CharField(blank=True, null=True, max_length=500)
|
|
note = models.TextField(blank=True, null=True)
|
|
|
|
state = StateField(FollowStates)
|
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
updated = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
unique_together = [("source", "target")]
|
|
|
|
@classmethod
|
|
def maybe_get(cls, source, target) -> Optional["Follow"]:
|
|
"""
|
|
Returns a follow if it exists between source and target
|
|
"""
|
|
try:
|
|
return Follow.objects.get(source=source, target=target)
|
|
except Follow.DoesNotExist:
|
|
return None
|
|
|
|
@classmethod
|
|
def create_local(cls, source, target):
|
|
"""
|
|
Creates a Follow from a local Identity to the target
|
|
(which can be local or remote).
|
|
"""
|
|
if not source.local:
|
|
raise ValueError("You cannot initiate follows from a remote Identity")
|
|
try:
|
|
follow = Follow.objects.get(source=source, target=target)
|
|
except Follow.DoesNotExist:
|
|
follow = Follow.objects.create(source=source, target=target, uri="")
|
|
follow.uri = source.actor_uri + f"follow/{follow.pk}/"
|
|
# TODO: Local follow approvals
|
|
if target.local:
|
|
follow.state = FollowStates.accepted
|
|
follow.save()
|
|
return follow
|
|
|
|
@classmethod
|
|
def remote_created(cls, source, target, uri):
|
|
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.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)
|
|
print(f"accepting follow {follow}")
|
|
if follow and follow.state in [
|
|
FollowStates.unrequested,
|
|
FollowStates.local_requested,
|
|
]:
|
|
follow.transition_perform(FollowStates.accepted)
|
|
print("accepted")
|