diff options
| author | Andrew Godwin | 2022-11-11 23:04:43 -0700 | 
|---|---|---|
| committer | Andrew Godwin | 2022-11-11 23:04:43 -0700 | 
| commit | 8fd5a9292c7d3aac352d3c0e96288bff8a79cb47 (patch) | |
| tree | 3a9199c1d35a77d209cf424141fd4aa0e0694118 /activities | |
| parent | feb5d9b74fa1e8454eaaf29afae3643c6d7c81f1 (diff) | |
| download | takahe-8fd5a9292c7d3aac352d3c0e96288bff8a79cb47.tar.gz takahe-8fd5a9292c7d3aac352d3c0e96288bff8a79cb47.tar.bz2 takahe-8fd5a9292c7d3aac352d3c0e96288bff8a79cb47.zip  | |
Posting and fan-out both working
Diffstat (limited to 'activities')
| -rw-r--r-- | activities/admin.py | 10 | ||||
| -rw-r--r-- | activities/migrations/0001_initial.py | 2 | ||||
| -rw-r--r-- | activities/migrations/0002_fan_out.py | 103 | ||||
| -rw-r--r-- | activities/models/__init__.py | 1 | ||||
| -rw-r--r-- | activities/models/fan_out.py | 81 | ||||
| -rw-r--r-- | activities/models/post.py | 82 | 
6 files changed, 272 insertions, 7 deletions
diff --git a/activities/admin.py b/activities/admin.py index 8b27951..2dec3bf 100644 --- a/activities/admin.py +++ b/activities/admin.py @@ -1,11 +1,11 @@  from django.contrib import admin -from activities.models import Post, TimelineEvent +from activities.models import FanOut, Post, TimelineEvent  @admin.register(Post)  class PostAdmin(admin.ModelAdmin): -    list_display = ["id", "author", "created"] +    list_display = ["id", "state", "author", "created"]      raw_id_fields = ["to", "mentions"] @@ -13,3 +13,9 @@ class PostAdmin(admin.ModelAdmin):  class TimelineEventAdmin(admin.ModelAdmin):      list_display = ["id", "identity", "created", "type"]      raw_id_fields = ["identity", "subject_post", "subject_identity"] + + +@admin.register(FanOut) +class FanOutAdmin(admin.ModelAdmin): +    list_display = ["id", "state", "state_attempted", "type", "identity"] +    raw_id_fields = ["identity", "subject_post"] diff --git a/activities/migrations/0001_initial.py b/activities/migrations/0001_initial.py index a97146d..0b350ef 100644 --- a/activities/migrations/0001_initial.py +++ b/activities/migrations/0001_initial.py @@ -28,7 +28,7 @@ class Migration(migrations.Migration):                          verbose_name="ID",                      ),                  ), -                ("state_ready", models.BooleanField(default=False)), +                ("state_ready", models.BooleanField(default=True)),                  ("state_changed", models.DateTimeField(auto_now_add=True)),                  ("state_attempted", models.DateTimeField(blank=True, null=True)),                  ("state_locked_until", models.DateTimeField(blank=True, null=True)), diff --git a/activities/migrations/0002_fan_out.py b/activities/migrations/0002_fan_out.py new file mode 100644 index 0000000..f3b626e --- /dev/null +++ b/activities/migrations/0002_fan_out.py @@ -0,0 +1,103 @@ +# Generated by Django 4.1.3 on 2022-11-12 05:36 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + +import activities.models.fan_out +import stator.models + + +class Migration(migrations.Migration): + +    dependencies = [ +        ("users", "0001_initial"), +        ("activities", "0001_initial"), +    ] + +    operations = [ +        migrations.AddField( +            model_name="post", +            name="authored", +            field=models.DateTimeField(default=django.utils.timezone.now), +        ), +        migrations.AlterField( +            model_name="post", +            name="author", +            field=models.ForeignKey( +                on_delete=django.db.models.deletion.PROTECT, +                related_name="posts", +                to="users.identity", +            ), +        ), +        migrations.AlterField( +            model_name="post", +            name="mentions", +            field=models.ManyToManyField( +                blank=True, related_name="posts_mentioning", to="users.identity" +            ), +        ), +        migrations.AlterField( +            model_name="post", +            name="to", +            field=models.ManyToManyField( +                blank=True, related_name="posts_to", to="users.identity" +            ), +        ), +        migrations.CreateModel( +            name="FanOut", +            fields=[ +                ( +                    "id", +                    models.BigAutoField( +                        auto_created=True, +                        primary_key=True, +                        serialize=False, +                        verbose_name="ID", +                    ), +                ), +                ("state_ready", models.BooleanField(default=True)), +                ("state_changed", models.DateTimeField(auto_now_add=True)), +                ("state_attempted", models.DateTimeField(blank=True, null=True)), +                ("state_locked_until", models.DateTimeField(blank=True, null=True)), +                ( +                    "state", +                    stator.models.StateField( +                        choices=[("new", "new"), ("sent", "sent")], +                        default="new", +                        graph=activities.models.fan_out.FanOutStates, +                        max_length=100, +                    ), +                ), +                ( +                    "type", +                    models.CharField( +                        choices=[("post", "Post"), ("boost", "Boost")], max_length=100 +                    ), +                ), +                ("created", models.DateTimeField(auto_now_add=True)), +                ("updated", models.DateTimeField(auto_now=True)), +                ( +                    "identity", +                    models.ForeignKey( +                        on_delete=django.db.models.deletion.CASCADE, +                        related_name="fan_outs", +                        to="users.identity", +                    ), +                ), +                ( +                    "subject_post", +                    models.ForeignKey( +                        blank=True, +                        null=True, +                        on_delete=django.db.models.deletion.CASCADE, +                        related_name="fan_outs", +                        to="activities.post", +                    ), +                ), +            ], +            options={ +                "abstract": False, +            }, +        ), +    ] 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":  | 
