Delete mechanics and refactor of post fanout
This commit is contained in:
parent
3a608c2012
commit
786d6190f8
|
@ -20,21 +20,43 @@ class FanOutStates(StateGraph):
|
||||||
fan_out = await instance.afetch_full()
|
fan_out = await instance.afetch_full()
|
||||||
# Handle Posts
|
# Handle Posts
|
||||||
if fan_out.type == FanOut.Types.post:
|
if fan_out.type == FanOut.Types.post:
|
||||||
|
post = await fan_out.subject_post.afetch_full()
|
||||||
if fan_out.identity.local:
|
if fan_out.identity.local:
|
||||||
# Make a timeline event directly
|
# Make a timeline event directly
|
||||||
|
# TODO: Exclude replies to people we don't follow
|
||||||
await sync_to_async(TimelineEvent.add_post)(
|
await sync_to_async(TimelineEvent.add_post)(
|
||||||
identity=fan_out.identity,
|
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:
|
else:
|
||||||
# Send it to the remote inbox
|
|
||||||
post = await fan_out.subject_post.afetch_full()
|
|
||||||
# Sign it and send it
|
# Sign it and send it
|
||||||
await post.author.signed_request(
|
await post.author.signed_request(
|
||||||
method="post",
|
method="post",
|
||||||
uri=fan_out.identity.inbox_uri,
|
uri=fan_out.identity.inbox_uri,
|
||||||
body=canonicalise(post.to_create_ap()),
|
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
|
# Handle boosts/likes
|
||||||
elif fan_out.type == FanOut.Types.interaction:
|
elif fan_out.type == FanOut.Types.interaction:
|
||||||
interaction = await fan_out.subject_post_interaction.afetch_full()
|
interaction = await fan_out.subject_post_interaction.afetch_full()
|
||||||
|
@ -79,6 +101,8 @@ class FanOut(StatorModel):
|
||||||
|
|
||||||
class Types(models.TextChoices):
|
class Types(models.TextChoices):
|
||||||
post = "post"
|
post = "post"
|
||||||
|
post_edited = "post_edited"
|
||||||
|
post_deleted = "post_deleted"
|
||||||
interaction = "interaction"
|
interaction = "interaction"
|
||||||
undo_interaction = "undo_interaction"
|
undo_interaction = "undo_interaction"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Iterable, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import urlman
|
import urlman
|
||||||
|
@ -10,19 +10,21 @@ from django.utils import timezone
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from activities.models.fan_out import FanOut
|
from activities.models.fan_out import FanOut
|
||||||
from activities.models.timeline_event import TimelineEvent
|
|
||||||
from core.html import sanitize_post, strip_html
|
from core.html import sanitize_post, strip_html
|
||||||
from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date
|
from core.ld import canonicalise, format_ld_date, get_list, parse_ld_date
|
||||||
from stator.models import State, StateField, StateGraph, StatorModel
|
from stator.models import State, StateField, StateGraph, StatorModel
|
||||||
from users.models.follow import Follow
|
|
||||||
from users.models.identity import Identity
|
from users.models.identity import Identity
|
||||||
|
|
||||||
|
|
||||||
class PostStates(StateGraph):
|
class PostStates(StateGraph):
|
||||||
new = State(try_interval=300)
|
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)
|
new.transitions_to(fanned_out)
|
||||||
|
fanned_out.transitions_to(deleted)
|
||||||
|
deleted.transitions_to(deleted_fanned_out)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def handle_new(cls, instance: "Post"):
|
async def handle_new(cls, instance: "Post"):
|
||||||
|
@ -30,39 +32,29 @@ class PostStates(StateGraph):
|
||||||
Creates all needed fan-out objects for a new Post.
|
Creates all needed fan-out objects for a new Post.
|
||||||
"""
|
"""
|
||||||
post = await instance.afetch_full()
|
post = await instance.afetch_full()
|
||||||
# Non-local posts should not be here
|
# Fan out to each target
|
||||||
# TODO: This seems to keep happening. Work out how?
|
for follow in await post.aget_targets():
|
||||||
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:
|
|
||||||
await FanOut.objects.acreate(
|
await FanOut.objects.acreate(
|
||||||
identity=follow,
|
identity=follow,
|
||||||
type=FanOut.Types.post,
|
type=FanOut.Types.post,
|
||||||
subject_post=post,
|
subject_post=post,
|
||||||
)
|
)
|
||||||
# And one for themselves if they're local
|
return cls.fanned_out
|
||||||
# (most views will do this at time of post, but it's idempotent)
|
|
||||||
if post.author.local:
|
@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(
|
await FanOut.objects.acreate(
|
||||||
identity_id=post.author_id,
|
identity=follow,
|
||||||
type=FanOut.Types.post,
|
type=FanOut.Types.post_deleted,
|
||||||
subject_post=post,
|
subject_post=post,
|
||||||
)
|
)
|
||||||
return cls.fanned_out
|
return cls.deleted_fanned_out
|
||||||
|
|
||||||
|
|
||||||
class Post(StatorModel):
|
class Post(StatorModel):
|
||||||
|
@ -339,6 +331,43 @@ class Post(StatorModel):
|
||||||
"object": object,
|
"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) ###
|
### ActivityPub (inbound) ###
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -451,21 +480,8 @@ class Post(StatorModel):
|
||||||
# Ensure the Create actor is the Post's attributedTo
|
# Ensure the Create actor is the Post's attributedTo
|
||||||
if data["actor"] != data["object"]["attributedTo"]:
|
if data["actor"] != data["object"]["attributedTo"]:
|
||||||
raise ValueError("Create actor does not match its Post object", data)
|
raise ValueError("Create actor does not match its Post object", data)
|
||||||
# Create it
|
# Create it, stator will fan it out locally
|
||||||
post = cls.by_ap(data["object"], create=True, update=True)
|
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)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def handle_update_ap(cls, data):
|
def handle_update_ap(cls, data):
|
||||||
|
|
|
@ -775,16 +775,23 @@ h1.identity small {
|
||||||
}
|
}
|
||||||
|
|
||||||
.post .actions {
|
.post .actions {
|
||||||
|
position: relative;
|
||||||
float: right;
|
float: right;
|
||||||
padding: 3px 5px 0 0;
|
padding: 3px 5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post .actions a {
|
.post .actions a {
|
||||||
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-text-dull);
|
color: var(--color-text-dull);
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post .actions a.menu {
|
||||||
|
width: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.post .actions a:hover {
|
.post .actions a:hover {
|
||||||
color: var(--color-text-main);
|
color: var(--color-text-main);
|
||||||
}
|
}
|
||||||
|
@ -793,6 +800,32 @@ h1.identity small {
|
||||||
color: var(--color-highlight);
|
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,
|
.boost-banner,
|
||||||
.mention-banner,
|
.mention-banner,
|
||||||
.follow-banner,
|
.follow-banner,
|
||||||
|
|
|
@ -15,7 +15,7 @@ from stator.models import StatorModel
|
||||||
class StatorRunner:
|
class StatorRunner:
|
||||||
"""
|
"""
|
||||||
Runs tasks on models that are looking for state changes.
|
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__(
|
def __init__(
|
||||||
|
|
|
@ -30,6 +30,14 @@
|
||||||
{% include "activities/_reply.html" %}
|
{% include "activities/_reply.html" %}
|
||||||
{% include "activities/_like.html" %}
|
{% include "activities/_like.html" %}
|
||||||
{% include "activities/_boost.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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue