takahe/activities/models/timeline_event.py

265 lines
9.2 KiB
Python
Raw Normal View History

2022-11-12 05:02:43 +00:00
from django.db import models
from django.utils import timezone
2022-11-12 05:02:43 +00:00
2022-12-11 18:22:06 +00:00
from core.ld import format_ld_date
2022-11-12 05:02:43 +00:00
class TimelineEvent(models.Model):
"""
Something that has happened to an identity that we want them to see on one
or more timelines, like posts, likes and follows.
"""
class Types(models.TextChoices):
post = "post"
boost = "boost" # A boost from someone (post substitute)
2022-11-14 01:42:47 +00:00
mentioned = "mentioned"
liked = "liked" # Someone liking one of our posts
followed = "followed"
2023-08-18 06:19:45 +00:00
follow_requested = "follow_requested"
2022-11-14 01:42:47 +00:00
boosted = "boosted" # Someone boosting one of our posts
announcement = "announcement" # Server announcement
identity_created = "identity_created" # New identity created
2022-11-12 05:02:43 +00:00
# The user this event is for
identity = models.ForeignKey(
"users.Identity",
on_delete=models.CASCADE,
related_name="timeline_events",
)
# What type of event it is
type = models.CharField(max_length=100, choices=Types.choices)
# The subject of the event (which is used depends on the type)
subject_post = models.ForeignKey(
"activities.Post",
on_delete=models.CASCADE,
blank=True,
null=True,
2022-11-14 01:42:47 +00:00
related_name="timeline_events",
)
subject_post_interaction = models.ForeignKey(
"activities.PostInteraction",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="timeline_events",
2022-11-12 05:02:43 +00:00
)
subject_identity = models.ForeignKey(
"users.Identity",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="timeline_events_about_us",
)
published = models.DateTimeField(default=timezone.now)
seen = models.BooleanField(default=False)
dismissed = models.BooleanField(default=False)
2022-11-12 05:02:43 +00:00
created = models.DateTimeField(auto_now_add=True)
class Meta:
2023-05-13 17:30:42 +00:00
indexes = [
2022-11-12 05:02:43 +00:00
# This relies on a DB that can use left subsets of indexes
2023-05-13 17:30:42 +00:00
models.Index(
fields=["identity", "type", "subject_post", "subject_identity"]
),
models.Index(fields=["identity", "type", "subject_identity"]),
models.Index(fields=["identity", "created"]),
2022-11-12 05:02:43 +00:00
]
### Alternate constructors ###
@classmethod
def add_follow(cls, identity, source_identity):
"""
2023-08-18 06:19:45 +00:00
Adds a follow to the timeline if it's not there already, remove follow request if any
2022-11-12 05:02:43 +00:00
"""
2023-08-18 06:19:45 +00:00
cls.objects.filter(
type=cls.Types.follow_requested,
identity=identity,
subject_identity=source_identity,
).delete()
2022-11-12 05:02:43 +00:00
return cls.objects.get_or_create(
identity=identity,
2022-11-18 01:52:00 +00:00
type=cls.Types.followed,
2022-11-12 05:02:43 +00:00
subject_identity=source_identity,
)[0]
2023-08-18 06:19:45 +00:00
@classmethod
def add_follow_request(cls, identity, source_identity):
"""
Adds a follow request to the timeline if it's not there already
"""
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.follow_requested,
subject_identity=source_identity,
)[0]
2022-11-12 05:02:43 +00:00
@classmethod
def add_post(cls, identity, post):
"""
Adds a post to the timeline if it's not there already
"""
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.post,
subject_post=post,
defaults={"published": post.published or post.created},
2022-11-12 05:02:43 +00:00
)[0]
2022-11-16 13:53:39 +00:00
@classmethod
def add_mentioned(cls, identity, post):
"""
Adds a mention of identity by post
"""
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.mentioned,
subject_post=post,
2022-11-18 01:52:00 +00:00
subject_identity=post.author,
defaults={"published": post.published or post.created},
2022-11-16 13:53:39 +00:00
)[0]
@classmethod
def add_identity_created(cls, identity, new_identity):
"""
Adds a new identity item
"""
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.identity_created,
subject_identity=new_identity,
)[0]
2022-11-12 05:02:43 +00:00
@classmethod
2022-11-14 01:42:47 +00:00
def add_post_interaction(cls, identity, interaction):
2022-11-12 05:02:43 +00:00
"""
2022-11-14 01:42:47 +00:00
Adds a boost/like to the timeline if it's not there already.
For boosts, may make two objects - one "boost" and one "boosted".
It'll return the "boost" in that case.
2022-11-12 05:02:43 +00:00
"""
2022-11-14 01:42:47 +00:00
if interaction.type == interaction.Types.like:
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.liked,
subject_post_id=interaction.post_id,
subject_identity_id=interaction.identity_id,
subject_post_interaction=interaction,
)[0]
elif interaction.type == interaction.Types.boost:
# If the boost is on one of our posts, then that's a boosted too
if interaction.post.author_id == identity.id:
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.boosted,
subject_post_id=interaction.post_id,
subject_identity_id=interaction.identity_id,
subject_post_interaction=interaction,
)[0]
return cls.objects.get_or_create(
identity=identity,
type=cls.Types.boost,
subject_post_id=interaction.post_id,
subject_identity_id=interaction.identity_id,
subject_post_interaction=interaction,
)[0]
@classmethod
def delete_post_interaction(cls, identity, interaction):
if interaction.type == interaction.Types.like:
cls.objects.filter(
identity=identity,
type=cls.Types.liked,
subject_post_id=interaction.post_id,
subject_identity_id=interaction.identity_id,
).delete()
elif interaction.type == interaction.Types.boost:
cls.objects.filter(
identity=identity,
type__in=[cls.Types.boosted, cls.Types.boost],
subject_post_id=interaction.post_id,
subject_identity_id=interaction.identity_id,
).delete()
2022-12-11 18:22:06 +00:00
2023-08-18 06:19:45 +00:00
@classmethod
def delete_follow(cls, target, source):
TimelineEvent.objects.filter(
type__in=[cls.Types.followed, cls.Types.follow_requested],
identity=target,
subject_identity=source,
).delete()
### Background tasks ###
@classmethod
def handle_clear_timeline(cls, message):
"""
Internal stator handler for clearing all events by a user off another
user's timeline.
"""
actor_id = message["actor"]
object_id = message["object"]
full_erase = message.get("fullErase", False)
if full_erase:
q = (
models.Q(subject_post__author_id=object_id)
| models.Q(subject_post_interaction__identity_id=object_id)
| models.Q(subject_identity_id=object_id)
)
else:
q = models.Q(
type=cls.Types.post, subject_post__author_id=object_id
) | models.Q(type=cls.Types.boost, subject_identity_id=object_id)
TimelineEvent.objects.filter(q, identity_id=actor_id).delete()
2022-12-11 18:22:06 +00:00
### Mastodon Client API ###
2022-12-11 19:37:28 +00:00
def to_mastodon_notification_json(self, interactions=None):
2022-12-11 18:22:06 +00:00
result = {
"id": self.pk,
"created_at": format_ld_date(self.created),
"account": self.subject_identity.to_mastodon_json(),
}
if self.type == self.Types.liked:
result["type"] = "favourite"
2022-12-11 19:37:28 +00:00
result["status"] = self.subject_post.to_mastodon_json(
interactions=interactions
)
2022-12-11 18:22:06 +00:00
elif self.type == self.Types.boosted:
result["type"] = "reblog"
2022-12-11 19:37:28 +00:00
result["status"] = self.subject_post.to_mastodon_json(
interactions=interactions
)
2022-12-11 18:22:06 +00:00
elif self.type == self.Types.mentioned:
result["type"] = "mention"
2022-12-11 19:37:28 +00:00
result["status"] = self.subject_post.to_mastodon_json(
interactions=interactions
)
2022-12-11 18:22:06 +00:00
elif self.type == self.Types.followed:
result["type"] = "follow"
2023-08-18 06:19:45 +00:00
elif self.type == self.Types.follow_requested:
result["type"] = "follow_request"
elif self.type == self.Types.identity_created:
result["type"] = "admin.sign_up"
2022-12-11 18:22:06 +00:00
else:
raise ValueError(f"Cannot convert {self.type} to notification JSON")
return result
2023-03-11 18:17:20 +00:00
def to_mastodon_status_json(self, interactions=None, bookmarks=None, identity=None):
if self.type == self.Types.post:
return self.subject_post.to_mastodon_json(
2023-03-11 18:17:20 +00:00
interactions=interactions, bookmarks=bookmarks, identity=identity
)
elif self.type == self.Types.boost:
return self.subject_post_interaction.to_mastodon_status_json(
interactions=interactions, identity=identity
)
else:
raise ValueError(f"Cannot make status JSON for type {self.type}")