From 8fd5a9292c7d3aac352d3c0e96288bff8a79cb47 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 11 Nov 2022 23:04:43 -0700 Subject: Posting and fan-out both working --- activities/models/__init__.py | 1 + activities/models/fan_out.py | 81 ++++++++++++++++++++++++++++++++++++++++++ activities/models/post.py | 82 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 activities/models/fan_out.py (limited to 'activities/models') diff --git a/activities/models/__init__.py b/activities/models/__init__.py index b74075f..b0ed474 100644 --- a/activities/models/__init__.py +++ b/activities/models/__init__.py @@ -1,2 +1,3 @@ +from .fan_out import FanOut # noqa from .post import Post # noqa from .timeline_event import TimelineEvent # noqa diff --git a/activities/models/fan_out.py b/activities/models/fan_out.py new file mode 100644 index 0000000..79b7409 --- /dev/null +++ b/activities/models/fan_out.py @@ -0,0 +1,81 @@ +from asgiref.sync import sync_to_async +from django.db import models + +from activities.models.timeline_event import TimelineEvent +from core.ld import canonicalise +from core.signatures import HttpSignature +from stator.models import State, StateField, StateGraph, StatorModel + + +class FanOutStates(StateGraph): + new = State(try_interval=300) + sent = State() + + new.transitions_to(sent) + + @classmethod + async def handle_new(cls, instance: "FanOut"): + """ + Sends the fan-out to the right inbox. + """ + fan_out = await instance.afetch_full() + if fan_out.identity.local: + # Make a timeline event directly + await sync_to_async(TimelineEvent.add_post)( + identity=fan_out.identity, + post=fan_out.subject_post, + ) + else: + # Send it to the remote inbox + post = await fan_out.subject_post.afetch_full() + # Sign it and send it + await HttpSignature.signed_request( + uri=fan_out.identity.inbox_uri, + body=canonicalise(post.to_create_ap()), + identity=post.author, + ) + return cls.sent + + +class FanOut(StatorModel): + """ + An activity that needs to get to an inbox somewhere. + """ + + class Types(models.TextChoices): + post = "post" + boost = "boost" + + state = StateField(FanOutStates) + + # The user this event is targeted at + identity = models.ForeignKey( + "users.Identity", + on_delete=models.CASCADE, + related_name="fan_outs", + ) + + # What type of activity it is + type = models.CharField(max_length=100, choices=Types.choices) + + # Links to the appropriate objects + subject_post = models.ForeignKey( + "activities.Post", + on_delete=models.CASCADE, + blank=True, + null=True, + related_name="fan_outs", + ) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + ### Async helpers ### + + async def afetch_full(self): + """ + Returns a version of the object with all relations pre-loaded + """ + return await FanOut.objects.select_related("identity", "subject_post").aget( + pk=self.pk + ) diff --git a/activities/models/post.py b/activities/models/post.py index f4d159f..4c40033 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -1,8 +1,13 @@ +from typing import Dict + import urlman from django.db import models +from django.utils import timezone +from activities.models.fan_out import FanOut from activities.models.timeline_event import TimelineEvent from core.html import sanitize_post +from core.ld import format_date from stator.models import State, StateField, StateGraph, StatorModel from users.models.follow import Follow from users.models.identity import Identity @@ -19,7 +24,8 @@ class PostStates(StateGraph): """ Creates all needed fan-out objects for a new Post. """ - pass + await instance.afan_out() + return cls.fanned_out class Post(StatorModel): @@ -84,11 +90,21 @@ class Post(StatorModel): blank=True, ) + # When the post was originally created (as opposed to when we received it) + authored = models.DateTimeField(default=timezone.now) + created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) class urls(urlman.Urls): - view = "{self.identity.urls.view}posts/{self.id}/" + view = "{self.author.urls.view}posts/{self.id}/" + object_uri = "{self.author.urls.actor}posts/{self.id}/" + + def get_scheme(self, url): + return "https" + + def get_hostname(self, url): + return self.instance.author.domain.uri_domain def __str__(self): return f"{self.author} #{self.id}" @@ -97,6 +113,16 @@ class Post(StatorModel): def safe_content(self): return sanitize_post(self.content) + ### Async helpers ### + + async def afetch_full(self): + """ + Returns a version of the object with all relations pre-loaded + """ + return await Post.objects.select_related("author", "author__domain").aget( + pk=self.pk + ) + ### Local creation ### @classmethod @@ -111,9 +137,57 @@ class Post(StatorModel): post.save() return post - ### ActivityPub (outgoing) ### + ### ActivityPub (outbound) ### - ### ActivityPub (incoming) ### + async def afan_out(self): + """ + Creates FanOuts for a new post + """ + # Send a copy to all people who follow this user + post = await self.afetch_full() + async for follow in post.author.inbound_follows.all(): + await FanOut.objects.acreate( + identity_id=follow.source_id, + type=FanOut.Types.post, + subject_post=post, + ) + # And one for themselves + await FanOut.objects.acreate( + identity_id=post.author_id, + type=FanOut.Types.post, + subject_post=post, + ) + + def to_ap(self) -> Dict: + """ + Returns the AP JSON for this object + """ + value = { + "type": "Note", + "id": self.object_uri, + "published": format_date(self.created), + "attributedTo": self.author.actor_uri, + "content": self.safe_content, + "to": "as:Public", + "as:sensitive": self.sensitive, + "url": self.urls.view.full(), # type: ignore + } + if self.summary: + value["summary"] = self.summary + return value + + def to_create_ap(self): + """ + Returns the AP JSON to create this object + """ + return { + "type": "Create", + "id": self.object_uri + "#create", + "actor": self.author.actor_uri, + "object": self.to_ap(), + } + + ### ActivityPub (inbound) ### @classmethod def by_ap(cls, data, create=False) -> "Post": -- cgit v1.2.3