mirror of
https://github.com/jointakahe/takahe.git
synced 2024-11-21 23:01:00 +00:00
Delete mechanics and refactor of post fanout
This commit is contained in:
parent
3a608c2012
commit
786d6190f8
5 changed files with 129 additions and 48 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__(
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
Loading…
Reference in a new issue