bookwyrm/bookwyrm/models/status.py

298 lines
10 KiB
Python
Raw Normal View History

2020-02-11 23:17:21 +00:00
''' models for storing different kinds of Activities '''
2020-03-30 00:40:51 +00:00
from django.utils import timezone
2020-02-11 23:17:21 +00:00
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
2020-09-17 20:09:11 +00:00
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
2020-11-30 22:24:31 +00:00
from .base_model import BookWyrmModel, PrivacyLevels
from . import fields
from .fields import image_serializer
2020-09-21 15:16:34 +00:00
class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc '''
2020-11-30 22:24:31 +00:00
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='attributedTo')
content = fields.TextField(blank=True, null=True)
mention_users = fields.TagField('User', related_name='mention_user')
mention_books = fields.TagField('Edition', related_name='mention_book')
2020-02-15 22:38:46 +00:00
local = models.BooleanField(default=True)
privacy = models.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
)
2020-11-30 22:24:31 +00:00
sensitive = fields.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts
2020-11-30 22:24:31 +00:00
published_date = fields.DateTimeField(
default=timezone.now, activitypub_field='published')
2020-10-08 19:32:45 +00:00
deleted = models.BooleanField(default=False)
deleted_date = models.DateTimeField(blank=True, null=True)
2020-02-19 07:26:42 +00:00
favorites = models.ManyToManyField(
'User',
symmetrical=False,
through='Favorite',
through_fields=('status', 'user'),
related_name='user_favorites'
)
2020-11-30 22:24:31 +00:00
reply_parent = fields.ForeignKey(
'self',
null=True,
2020-11-30 22:24:31 +00:00
on_delete=models.PROTECT,
activitypub_field='inReplyTo',
)
objects = InheritanceManager()
activity_serializer = activitypub.Note
serialize_reverse_fields = [('attachments', 'attachment')]
deserialize_reverse_fields = [('attachments', 'attachment')]
#----- replies collection activitypub ----#
@classmethod
def replies(cls, status):
''' load all replies to a status. idk if there's a better way
to write this so it's just a property '''
return cls.objects.filter(reply_parent=status).select_subclasses()
2020-09-28 22:57:31 +00:00
@property
def status_type(self):
''' expose the type of status for the ui using activity type '''
return self.activity_serializer.__name__
def to_replies(self, **kwargs):
''' helper function for loading AP serialized replies to a status '''
return self.to_ordered_collection(
self.replies(self),
remote_id='%s/replies' % self.remote_id,
**kwargs
)
2020-11-01 16:54:10 +00:00
def to_activity(self, pure=False):
2020-10-08 19:32:45 +00:00
''' return tombstone if the status is deleted '''
if self.deleted:
return activitypub.Tombstone(
id=self.remote_id,
url=self.remote_id,
2020-10-30 22:22:20 +00:00
deleted=self.deleted_date.isoformat(),
published=self.deleted_date.isoformat()
2020-10-08 19:32:45 +00:00
).serialize()
2020-11-30 22:24:31 +00:00
activity = ActivitypubMixin.to_activity(self)
activity['replies'] = self.to_replies()
# privacy controls
public = 'https://www.w3.org/ns/activitystreams#Public'
mentions = [u.remote_id for u in self.mention_users.all()]
# this is a link to the followers list:
followers = self.user.__class__._meta.get_field('followers')\
.field_to_activity(self.user.followers)
2020-11-30 22:24:31 +00:00
if self.privacy == 'public':
activity['to'] = [public]
activity['cc'] = [followers] + mentions
elif self.privacy == 'unlisted':
activity['to'] = [followers]
activity['cc'] = [public] + mentions
elif self.privacy == 'followers':
activity['to'] = [followers]
activity['cc'] = mentions
if self.privacy == 'direct':
activity['to'] = mentions
activity['cc'] = []
# "pure" serialization for non-bookwyrm instances
if pure:
activity['content'] = self.pure_content
if 'name' in activity:
activity['name'] = self.pure_name
activity['type'] = self.pure_type
activity['attachment'] = [
image_serializer(b.cover) for b in self.mention_books.all() \
if b.cover]
if hasattr(self, 'book'):
activity['attachment'].append(
image_serializer(self.book.cover)
)
return activity
2020-10-08 19:32:45 +00:00
def save(self, *args, **kwargs):
2020-11-05 00:28:32 +00:00
''' update user active time '''
2020-11-24 19:25:07 +00:00
if self.user.local:
self.user.last_active_date = timezone.now()
self.user.save()
return super().save(*args, **kwargs)
2020-10-08 19:32:45 +00:00
2020-10-30 22:22:20 +00:00
class GeneratedNote(Status):
''' these are app-generated messages about user activity '''
@property
2020-11-30 22:24:31 +00:00
def pure_content(self):
''' indicate the book in question for mastodon (or w/e) users '''
message = self.content
books = ', '.join(
2020-11-30 22:24:31 +00:00
'<a href="%s">"%s"</a>' % (book.remote_id, book.title) \
2020-10-08 19:32:45 +00:00
for book in self.mention_books.all()
)
2020-11-30 22:24:31 +00:00
return '%s %s %s' % (self.user.display_name, message, books)
activity_serializer = activitypub.GeneratedNote
2020-11-30 22:24:31 +00:00
pure_type = 'Note'
2020-03-21 23:50:49 +00:00
class Comment(Status):
''' like a review but without a rating and transient '''
2020-12-03 21:14:04 +00:00
book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
2020-03-21 23:50:49 +00:00
@property
2020-11-30 22:24:31 +00:00
def pure_content(self):
''' indicate the book in question for mastodon (or w/e) users '''
return self.content + '<br><br>(comment on <a href="%s">"%s"</a>)' % \
2020-11-13 17:47:35 +00:00
(self.book.remote_id, self.book.title)
activity_serializer = activitypub.Comment
2020-11-30 22:24:31 +00:00
pure_type = 'Note'
2020-04-08 16:40:47 +00:00
class Quotation(Status):
''' like a review but without a rating and transient '''
2020-11-30 22:24:31 +00:00
quote = fields.TextField()
2020-12-03 21:14:04 +00:00
book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
2020-03-21 23:50:49 +00:00
@property
2020-11-30 22:24:31 +00:00
def pure_content(self):
''' indicate the book in question for mastodon (or w/e) users '''
return '"%s"<br>-- <a href="%s">"%s"</a><br><br>%s' % (
self.quote,
2020-11-13 17:47:35 +00:00
self.book.remote_id,
self.book.title,
self.content,
)
activity_serializer = activitypub.Quotation
2020-11-30 22:24:31 +00:00
pure_type = 'Note'
class Review(Status):
''' a book review '''
2020-11-30 22:24:31 +00:00
name = fields.CharField(max_length=255, null=True)
2020-12-03 21:14:04 +00:00
book = fields.ForeignKey(
'Edition', on_delete=models.PROTECT, activitypub_field='inReplyToBook')
2020-11-30 22:24:31 +00:00
rating = fields.IntegerField(
default=None,
null=True,
blank=True,
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
@property
2020-11-30 22:24:31 +00:00
def pure_name(self):
''' clarify review names for mastodon serialization '''
if self.rating:
return 'Review of "%s" (%d stars): %s' % (
self.book.title,
self.rating,
self.name
)
return 'Review of "%s": %s' % (
self.book.title,
self.name
)
@property
2020-11-30 22:24:31 +00:00
def pure_content(self):
''' indicate the book in question for mastodon (or w/e) users '''
return self.content + '<br><br>(<a href="%s">"%s"</a>)' % \
2020-11-13 17:47:35 +00:00
(self.book.remote_id, self.book.title)
activity_serializer = activitypub.Review
2020-11-30 22:24:31 +00:00
pure_type = 'Article'
2020-09-21 15:16:34 +00:00
class Favorite(ActivitypubMixin, BookWyrmModel):
2020-02-19 07:26:42 +00:00
''' fav'ing a post '''
2020-11-30 22:24:31 +00:00
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor')
status = fields.ForeignKey(
'Status', on_delete=models.PROTECT, activitypub_field='object')
activity_serializer = activitypub.Like
def save(self, *args, **kwargs):
2020-11-05 00:28:32 +00:00
''' update user active time '''
self.user.last_active_date = timezone.now()
self.user.save()
super().save(*args, **kwargs)
class Meta:
''' can't fav things twice '''
unique_together = ('user', 'status')
2020-02-21 06:19:19 +00:00
class Boost(Status):
''' boost'ing a post '''
2020-11-30 22:24:31 +00:00
boosted_status = fields.ForeignKey(
'Status',
on_delete=models.PROTECT,
2020-11-30 22:24:31 +00:00
related_name='boosters',
activitypub_field='object',
)
2020-09-29 01:25:05 +00:00
activity_serializer = activitypub.Boost
2020-04-01 21:55:32 +00:00
# This constraint can't work as it would cross tables.
# class Meta:
# unique_together = ('user', 'boosted_status')
2020-09-21 15:16:34 +00:00
class ReadThrough(BookWyrmModel):
2020-04-02 18:05:10 +00:00
''' Store progress through a book in the database. '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
book = models.ForeignKey('Book', on_delete=models.PROTECT)
pages_read = models.IntegerField(
null=True,
blank=True)
start_date = models.DateTimeField(
blank=True,
null=True)
finish_date = models.DateTimeField(
blank=True,
null=True)
def save(self, *args, **kwargs):
2020-11-05 00:28:32 +00:00
''' update user active time '''
self.user.last_active_date = timezone.now()
self.user.save()
super().save(*args, **kwargs)
2020-04-02 18:05:10 +00:00
NotificationType = models.TextChoices(
2020-04-20 16:10:19 +00:00
'NotificationType',
'FAVORITE REPLY MENTION TAG FOLLOW FOLLOW_REQUEST BOOST IMPORT')
2020-09-21 15:16:34 +00:00
class Notification(BookWyrmModel):
2020-03-07 22:50:29 +00:00
''' you've been tagged, liked, followed, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
related_book = models.ForeignKey(
'Edition', on_delete=models.PROTECT, null=True)
2020-03-07 22:50:29 +00:00
related_user = models.ForeignKey(
'User',
on_delete=models.PROTECT, null=True, related_name='related_user')
related_status = models.ForeignKey(
'Status', on_delete=models.PROTECT, null=True)
2020-04-22 11:43:10 +00:00
related_import = models.ForeignKey(
'ImportJob', on_delete=models.PROTECT, null=True)
2020-03-07 22:50:29 +00:00
read = models.BooleanField(default=False)
notification_type = models.CharField(
max_length=255, choices=NotificationType.choices)
class Meta:
''' checks if notifcation is in enum list for valid types '''
constraints = [
models.CheckConstraint(
check=models.Q(notification_type__in=NotificationType.values),
name="notification_type_valid",
)
2020-03-07 22:50:29 +00:00
]