takahe/users/models/follow.py
2022-11-10 23:42:43 -07:00

185 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")