summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrew Godwin2022-11-24 17:11:04 -0700
committerAndrew Godwin2022-11-24 17:11:04 -0700
commit786d6190f856fddb32157764717f871c6f8cb3fa (patch)
tree3e7569aba3c09fb58837158755679b0ee04d02b8
parent3a608c2012b610a8f9b7e5179dcb93ab2e767251 (diff)
downloadtakahe-786d6190f856fddb32157764717f871c6f8cb3fa.tar.gz
takahe-786d6190f856fddb32157764717f871c6f8cb3fa.tar.bz2
takahe-786d6190f856fddb32157764717f871c6f8cb3fa.zip
Delete mechanics and refactor of post fanout
-rw-r--r--activities/models/fan_out.py30
-rw-r--r--activities/models/post.py104
-rw-r--r--static/css/style.css33
-rw-r--r--stator/runner.py2
-rw-r--r--templates/activities/_post.html8
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 %}