Delete mechanics and refactor of post fanout

This commit is contained in:
Andrew Godwin 2022-11-24 17:11:04 -07:00
parent 3a608c2012
commit 786d6190f8
5 changed files with 129 additions and 48 deletions

View file

@ -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"

View file

@ -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):

View file

@ -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,

View file

@ -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__(

View file

@ -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 %}