diff options
| author | Andrew Godwin | 2022-11-24 17:11:04 -0700 | 
|---|---|---|
| committer | Andrew Godwin | 2022-11-24 17:11:04 -0700 | 
| commit | 786d6190f856fddb32157764717f871c6f8cb3fa (patch) | |
| tree | 3e7569aba3c09fb58837158755679b0ee04d02b8 | |
| parent | 3a608c2012b610a8f9b7e5179dcb93ab2e767251 (diff) | |
| download | takahe-786d6190f856fddb32157764717f871c6f8cb3fa.tar.gz takahe-786d6190f856fddb32157764717f871c6f8cb3fa.tar.bz2 takahe-786d6190f856fddb32157764717f871c6f8cb3fa.zip | |
Delete mechanics and refactor of post fanout
| -rw-r--r-- | activities/models/fan_out.py | 30 | ||||
| -rw-r--r-- | activities/models/post.py | 104 | ||||
| -rw-r--r-- | static/css/style.css | 33 | ||||
| -rw-r--r-- | stator/runner.py | 2 | ||||
| -rw-r--r-- | templates/activities/_post.html | 8 | 
5 files changed, 129 insertions, 48 deletions
| diff --git a/activities/models/fan_out.py b/activities/models/fan_out.py index 285ecc2..5eb20f3 100644 --- a/activities/models/fan_out.py +++ b/activities/models/fan_out.py @@ -20,21 +20,43 @@ class FanOutStates(StateGraph):          fan_out = await instance.afetch_full()          # Handle Posts          if fan_out.type == FanOut.Types.post: +            post = await fan_out.subject_post.afetch_full()              if fan_out.identity.local:                  # Make a timeline event directly +                # TODO: Exclude replies to people we don't follow                  await sync_to_async(TimelineEvent.add_post)(                      identity=fan_out.identity, -                    post=fan_out.subject_post, +                    post=post,                  ) +                # We might have been mentioned +                if fan_out.identity in list(post.mentions.all()): +                    TimelineEvent.add_mentioned( +                        identity=fan_out.identity, +                        post=post, +                    )              else: -                # Send it to the remote inbox -                post = await fan_out.subject_post.afetch_full()                  # Sign it and send it                  await post.author.signed_request(                      method="post",                      uri=fan_out.identity.inbox_uri,                      body=canonicalise(post.to_create_ap()),                  ) +        # Handle deleting posts +        elif fan_out.type == FanOut.Types.post_deleted: +            post = await fan_out.subject_post.afetch_full() +            if fan_out.identity.local: +                # Remove all timeline events mentioning it +                await TimelineEvent.objects.filter( +                    identity=fan_out.identity, +                    subject_post=post, +                ).adelete() +            else: +                # Send it to the remote inbox +                await post.author.signed_request( +                    method="post", +                    uri=fan_out.identity.inbox_uri, +                    body=canonicalise(post.to_delete_ap()), +                )          # Handle boosts/likes          elif fan_out.type == FanOut.Types.interaction:              interaction = await fan_out.subject_post_interaction.afetch_full() @@ -79,6 +101,8 @@ class FanOut(StatorModel):      class Types(models.TextChoices):          post = "post" +        post_edited = "post_edited" +        post_deleted = "post_deleted"          interaction = "interaction"          undo_interaction = "undo_interaction" diff --git a/activities/models/post.py b/activities/models/post.py index c86ec6a..c8165d6 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -1,5 +1,5 @@  import re -from typing import Dict, Optional +from typing import Dict, Iterable, Optional  import httpx  import urlman @@ -10,19 +10,21 @@ from django.utils import timezone  from django.utils.safestring import mark_safe  from activities.models.fan_out import FanOut -from activities.models.timeline_event import TimelineEvent  from core.html import sanitize_post, strip_html  from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date  from stator.models import State, StateField, StateGraph, StatorModel -from users.models.follow import Follow  from users.models.identity import Identity  class PostStates(StateGraph):      new = State(try_interval=300) -    fanned_out = State() +    fanned_out = State(externally_progressed=True) +    deleted = State(try_interval=300) +    deleted_fanned_out = State()      new.transitions_to(fanned_out) +    fanned_out.transitions_to(deleted) +    deleted.transitions_to(deleted_fanned_out)      @classmethod      async def handle_new(cls, instance: "Post"): @@ -30,39 +32,29 @@ class PostStates(StateGraph):          Creates all needed fan-out objects for a new Post.          """          post = await instance.afetch_full() -        # Non-local posts should not be here -        # TODO: This seems to keep happening. Work out how? -        if not post.local: -            print(f"Trying to run handle_new on a non-local post {post.pk}!") -            return cls.fanned_out -        # Build list of targets - mentions always included -        targets = set() -        async for mention in post.mentions.all(): -            targets.add(mention) -        # Then, if it's not mentions only, also deliver to followers -        if post.visibility != Post.Visibilities.mentioned: -            async for follower in post.author.inbound_follows.select_related("source"): -                targets.add(follower.source) -        # If it's a reply, always include the original author if we know them -        reply_post = await post.ain_reply_to_post() -        if reply_post: -            targets.add(reply_post.author) -        # Fan out to each one -        for follow in targets: +        # Fan out to each target +        for follow in await post.aget_targets():              await FanOut.objects.acreate(                  identity=follow,                  type=FanOut.Types.post,                  subject_post=post,              ) -        # And one for themselves if they're local -        # (most views will do this at time of post, but it's idempotent) -        if post.author.local: +        return cls.fanned_out + +    @classmethod +    async def handle_deleted(cls, instance: "Post"): +        """ +        Creates all needed fan-out objects needed to delete a Post. +        """ +        post = await instance.afetch_full() +        # Fan out to each target +        for follow in await post.aget_targets():              await FanOut.objects.acreate( -                identity_id=post.author_id, -                type=FanOut.Types.post, +                identity=follow, +                type=FanOut.Types.post_deleted,                  subject_post=post,              ) -        return cls.fanned_out +        return cls.deleted_fanned_out  class Post(StatorModel): @@ -339,6 +331,43 @@ class Post(StatorModel):              "object": object,          } +    def to_delete_ap(self): +        """ +        Returns the AP JSON to create this object +        """ +        object = self.to_ap() +        return { +            "to": object["to"], +            "cc": object.get("cc", []), +            "type": "Delete", +            "id": self.object_uri + "#delete", +            "actor": self.author.actor_uri, +            "object": object, +        } + +    async def aget_targets(self) -> Iterable[Identity]: +        """ +        Returns a list of Identities that need to see posts and their changes +        """ +        targets = set() +        async for mention in self.mentions.all(): +            targets.add(mention) +        # Then, if it's not mentions only, also deliver to followers +        if self.visibility != Post.Visibilities.mentioned: +            async for follower in self.author.inbound_follows.select_related("source"): +                targets.add(follower.source) +        # If it's a reply, always include the original author if we know them +        reply_post = await self.ain_reply_to_post() +        if reply_post: +            targets.add(reply_post.author) +        # If this is a remote post, filter to only include local identities +        if not self.local: +            targets = {target for target in targets if target.local} +        # If it's a local post, include the author +        else: +            targets.add(self.author) +        return targets +      ### ActivityPub (inbound) ###      @classmethod @@ -451,21 +480,8 @@ class Post(StatorModel):              # Ensure the Create actor is the Post's attributedTo              if data["actor"] != data["object"]["attributedTo"]:                  raise ValueError("Create actor does not match its Post object", data) -            # Create it -            post = cls.by_ap(data["object"], create=True, update=True) -            # Make timeline events for followers if it's not a reply -            # TODO: _do_ show replies to people we follow somehow -            if not post.in_reply_to: -                for follow in Follow.objects.filter( -                    target=post.author, source__local=True -                ): -                    TimelineEvent.add_post(follow.source, post) -            # Make timeline events for mentions if they're local -            for mention in post.mentions.all(): -                if mention.local: -                    TimelineEvent.add_mentioned(mention, post) -            # Force it into fanned_out as it's not ours -            post.transition_perform(PostStates.fanned_out) +            # Create it, stator will fan it out locally +            cls.by_ap(data["object"], create=True, update=True)      @classmethod      def handle_update_ap(cls, data): diff --git a/static/css/style.css b/static/css/style.css index 642057f..8660ae2 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -775,16 +775,23 @@ h1.identity small {  }  .post .actions { +    position: relative;      float: right;      padding: 3px 5px 0 0;  }  .post .actions a { +    text-align: center;      cursor: pointer;      color: var(--color-text-dull);      margin-right: 5px;  } +.post .actions a.menu { +    width: 16px; +    display: inline-block; +} +  .post .actions a:hover {      color: var(--color-text-main);  } @@ -793,6 +800,32 @@ h1.identity small {      color: var(--color-highlight);  } +.post .actions menu { +    position: absolute; +    display: none; +    top: 25px; +    right: 10px; +    background-color: var(--color-bg-menu); +    border-radius: 5px; +    padding: 5px 10px; +    box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); +} + +.post .actions menu.enabled { +    display: block; +} + +.post .actions menu a { +    text-align: left; +    display: block; +    width: 160px; +    font-size: 15px; +} + +.post .actions menu a i { +    margin-right: 4px; +} +  .boost-banner,  .mention-banner,  .follow-banner, diff --git a/stator/runner.py b/stator/runner.py index a954a2e..cb97f6e 100644 --- a/stator/runner.py +++ b/stator/runner.py @@ -15,7 +15,7 @@ from stator.models import StatorModel  class StatorRunner:      """      Runs tasks on models that are looking for state changes. -    Designed to run for a determinate amount of time, and then exit. +    Designed to run either indefinitely, or just for a few seconds.      """      def __init__( diff --git a/templates/activities/_post.html b/templates/activities/_post.html index dbdc99a..e109e9c 100644 --- a/templates/activities/_post.html +++ b/templates/activities/_post.html @@ -30,6 +30,14 @@          {% include "activities/_reply.html" %}          {% include "activities/_like.html" %}          {% include "activities/_boost.html" %} +        <a title="Menu" class="menu" _="on click toggle .enabled on the next <menu/>"> +            <i class="fa-solid fa-caret-down"></i> +        </a> +        <menu> +            <a> +                <i class="fa-solid fa-trash"></i> Delete +            </a> +        </menu>      </div>      {% endif %} | 
