2022-11-12 05:02:43 +00:00
|
|
|
from django.db import models
|
2022-12-18 16:44:56 +00:00
|
|
|
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"
|
2022-11-25 04:30:21 +00:00
|
|
|
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"
|
|
|
|
boosted = "boosted" # Someone boosting one of our posts
|
2022-12-18 16:22:15 +00:00
|
|
|
announcement = "announcement" # Server announcement
|
2023-01-15 21:48:17 +00:00
|
|
|
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",
|
|
|
|
)
|
|
|
|
|
2022-12-18 16:44:56 +00:00
|
|
|
published = models.DateTimeField(default=timezone.now)
|
2022-12-18 16:22:15 +00:00
|
|
|
seen = models.BooleanField(default=False)
|
|
|
|
|
2022-11-12 05:02:43 +00:00
|
|
|
created = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
index_together = [
|
|
|
|
# This relies on a DB that can use left subsets of indexes
|
|
|
|
("identity", "type", "subject_post", "subject_identity"),
|
|
|
|
("identity", "type", "subject_identity"),
|
2023-01-14 19:01:44 +00:00
|
|
|
("identity", "created"),
|
2022-11-12 05:02:43 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
### Alternate constructors ###
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def add_follow(cls, identity, source_identity):
|
|
|
|
"""
|
|
|
|
Adds a follow to the timeline if it's not there already
|
|
|
|
"""
|
|
|
|
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]
|
|
|
|
|
|
|
|
@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,
|
2022-12-18 16:44:56 +00:00
|
|
|
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,
|
2022-12-18 16:44:56 +00:00
|
|
|
defaults={"published": post.published or post.created},
|
2022-11-16 13:53:39 +00:00
|
|
|
)[0]
|
|
|
|
|
2023-01-15 21:48:17 +00:00
|
|
|
@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]
|
2022-11-16 01:30:30 +00:00
|
|
|
|
|
|
|
@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-01-16 18:53:40 +00:00
|
|
|
### 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-01-15 21:48:17 +00:00
|
|
|
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
|
2022-12-29 17:53:31 +00:00
|
|
|
|
|
|
|
def to_mastodon_status_json(self, interactions=None):
|
|
|
|
if self.type == self.Types.post:
|
|
|
|
return self.subject_post.to_mastodon_json(interactions=interactions)
|
|
|
|
elif self.type == self.Types.boost:
|
|
|
|
return self.subject_post_interaction.to_mastodon_status_json(
|
|
|
|
interactions=interactions
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise ValueError(f"Cannot make status JSON for type {self.type}")
|