2023-10-23 09:33:55 -07:00
|
|
|
import logging
|
|
|
|
|
2022-12-27 10:53:12 -08:00
|
|
|
from activities.models import (
|
|
|
|
Post,
|
|
|
|
PostInteraction,
|
|
|
|
PostInteractionStates,
|
|
|
|
PostStates,
|
|
|
|
TimelineEvent,
|
|
|
|
)
|
2022-12-20 01:59:06 -08:00
|
|
|
from users.models import Identity
|
|
|
|
|
2023-11-16 09:27:20 -08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2022-12-20 01:59:06 -08:00
|
|
|
|
|
|
|
class PostService:
|
|
|
|
"""
|
|
|
|
High-level operations on Posts
|
|
|
|
"""
|
|
|
|
|
2022-12-27 10:53:12 -08:00
|
|
|
@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",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2022-12-20 01:59:06 -08:00
|
|
|
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]
|
2022-12-24 09:50:01 -08:00
|
|
|
if interaction.state not in PostInteractionStates.group_active():
|
2022-12-20 01:59:06 -08:00
|
|
|
interaction.transition_perform(PostInteractionStates.new)
|
2022-12-31 12:48:35 -08:00
|
|
|
self.post.calculate_stats()
|
2022-12-20 01:59:06 -08:00
|
|
|
|
|
|
|
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)
|
2022-12-31 12:48:35 -08:00
|
|
|
self.post.calculate_stats()
|
2022-12-20 01:59:06 -08:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2023-05-03 21:42:37 -07:00
|
|
|
def context(
|
|
|
|
self,
|
|
|
|
identity: Identity | None,
|
|
|
|
num_ancestors: int = 10,
|
|
|
|
num_descendants: int = 50,
|
|
|
|
) -> tuple[list[Post], list[Post]]:
|
2022-12-20 01:59:06 -08:00
|
|
|
"""
|
|
|
|
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:
|
2022-12-30 10:06:38 -08:00
|
|
|
object_uri = ancestor.in_reply_to
|
2023-01-14 10:31:17 -08:00
|
|
|
reason = ancestor.object_uri
|
2022-12-30 10:06:38 -08:00
|
|
|
ancestor = self.queryset().filter(object_uri=object_uri).first()
|
2022-12-20 07:02:20 -08:00
|
|
|
if ancestor is None:
|
2023-01-15 09:28:44 -08:00
|
|
|
try:
|
|
|
|
Post.ensure_object_uri(object_uri, reason=reason)
|
|
|
|
except ValueError:
|
2023-11-16 09:27:20 -08:00
|
|
|
logger.error(
|
2023-01-15 09:28:44 -08:00
|
|
|
f"Cannot fetch ancestor Post={self.post.pk}, ancestor_uri={object_uri}"
|
|
|
|
)
|
2022-12-20 07:02:20 -08:00
|
|
|
break
|
2022-12-20 01:59:06 -08:00
|
|
|
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]
|
2023-03-02 09:28:27 -08:00
|
|
|
seen: set[str] = set()
|
2022-12-20 01:59:06 -08:00
|
|
|
while queue and len(descendants) < num_descendants:
|
|
|
|
node = queue.pop()
|
|
|
|
child_queryset = (
|
2022-12-24 09:50:01 -08:00
|
|
|
self.queryset()
|
2022-12-20 01:59:06 -08:00
|
|
|
.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:
|
2023-03-02 09:28:27 -08:00
|
|
|
if child.pk not in seen:
|
|
|
|
descendants.append(child)
|
|
|
|
queue.append(child)
|
|
|
|
seen.add(child.pk)
|
2022-12-20 01:59:06 -08:00
|
|
|
return ancestors, descendants
|
2022-12-27 10:53:12 -08:00
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
2023-05-13 09:01:27 -07:00
|
|
|
|
|
|
|
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)
|