takahe/activities/services/post.py
2023-05-13 10:01:27 -06:00

163 lines
5.4 KiB
Python

from activities.models import (
Post,
PostInteraction,
PostInteractionStates,
PostStates,
TimelineEvent,
)
from core.exceptions import capture_message
from users.models import Identity
class PostService:
"""
High-level operations on Posts
"""
@classmethod
def queryset(cls):
"""
Returns the base queryset to use for fetching posts efficiently.
"""
return (
Post.objects.not_hidden()
.prefetch_related(
"attachments",
"mentions",
"emojis",
)
.select_related(
"author",
"author__domain",
)
)
def __init__(self, post: Post):
self.post = post
def interact_as(self, identity: Identity, type: str):
"""
Performs an interaction on this Post
"""
interaction = PostInteraction.objects.get_or_create(
type=type,
identity=identity,
post=self.post,
)[0]
if interaction.state not in PostInteractionStates.group_active():
interaction.transition_perform(PostInteractionStates.new)
self.post.calculate_stats()
def uninteract_as(self, identity, type):
"""
Undoes an interaction on this Post
"""
for interaction in PostInteraction.objects.filter(
type=type,
identity=identity,
post=self.post,
):
interaction.transition_perform(PostInteractionStates.undone)
self.post.calculate_stats()
def like_as(self, identity: Identity):
self.interact_as(identity, PostInteraction.Types.like)
def unlike_as(self, identity: Identity):
self.uninteract_as(identity, PostInteraction.Types.like)
def boost_as(self, identity: Identity):
self.interact_as(identity, PostInteraction.Types.boost)
def unboost_as(self, identity: Identity):
self.uninteract_as(identity, PostInteraction.Types.boost)
def context(
self,
identity: Identity | None,
num_ancestors: int = 10,
num_descendants: int = 50,
) -> tuple[list[Post], list[Post]]:
"""
Returns ancestor/descendant information.
Ancestors are guaranteed to be in order from closest to furthest.
Descendants are in depth-first order, starting with closest.
If identity is provided, includes mentions/followers-only posts they
can see. Otherwise, shows unlisted and above only.
"""
# Retrieve ancestors via parent walk
ancestors: list[Post] = []
ancestor = self.post
while ancestor.in_reply_to and len(ancestors) < num_ancestors:
object_uri = ancestor.in_reply_to
reason = ancestor.object_uri
ancestor = self.queryset().filter(object_uri=object_uri).first()
if ancestor is None:
try:
Post.ensure_object_uri(object_uri, reason=reason)
except ValueError:
capture_message(
f"Cannot fetch ancestor Post={self.post.pk}, ancestor_uri={object_uri}"
)
break
if ancestor.state in [PostStates.deleted, PostStates.deleted_fanned_out]:
break
ancestors.append(ancestor)
# Retrieve descendants via breadth-first-search
descendants: list[Post] = []
queue = [self.post]
seen: set[str] = set()
while queue and len(descendants) < num_descendants:
node = queue.pop()
child_queryset = (
self.queryset()
.filter(in_reply_to=node.object_uri)
.order_by("published")
)
if identity:
child_queryset = child_queryset.visible_to(
identity=identity, include_replies=True
)
else:
child_queryset = child_queryset.unlisted(include_replies=True)
for child in child_queryset:
if child.pk not in seen:
descendants.append(child)
queue.append(child)
seen.add(child.pk)
return ancestors, descendants
def delete(self):
"""
Marks a post as deleted and immediately cleans up its timeline events etc.
"""
self.post.transition_perform(PostStates.deleted)
TimelineEvent.objects.filter(subject_post=self.post).delete()
PostInteraction.transition_perform_queryset(
PostInteraction.objects.filter(
post=self.post,
state__in=PostInteractionStates.group_active(),
),
PostInteractionStates.undone,
)
def pin_as(self, identity: Identity):
if identity != self.post.author:
raise ValueError("Not the author of this post")
if self.post.visibility == Post.Visibilities.mentioned:
raise ValueError("Cannot pin a mentioned-only post")
if (
PostInteraction.objects.filter(
type=PostInteraction.Types.pin,
identity=identity,
).count()
>= 5
):
raise ValueError("Maximum number of pins already reached")
self.interact_as(identity, PostInteraction.Types.pin)
def unpin_as(self, identity: Identity):
self.uninteract_as(identity, PostInteraction.Types.pin)