Updates status model and serializer

This commit is contained in:
Mouse Reeve 2020-11-30 14:24:31 -08:00
parent 8bc0a57bd4
commit 3966c84e08
7 changed files with 167 additions and 166 deletions

View file

@ -8,7 +8,6 @@ from .image import Image
@dataclass(init=False)
class Tombstone(ActivityObject):
''' the placeholder for a deleted status '''
url: str
published: str
deleted: str
type: str = 'Tombstone'
@ -17,14 +16,13 @@ class Tombstone(ActivityObject):
@dataclass(init=False)
class Note(ActivityObject):
''' Note activity '''
url: str
inReplyTo: str
published: str
attributedTo: str
to: List[str]
cc: List[str]
content: str
replies: Dict
to: List[str] = field(default_factory=lambda: [])
cc: List[str] = field(default_factory=lambda: [])
replies: Dict = field(default_factory=lambda: {})
inReplyTo: str = ''
tag: List[Link] = field(default_factory=lambda: [])
attachment: List[Image] = field(default_factory=lambda: [])
sensitive: bool = False

View file

@ -3,30 +3,27 @@ from django.db import models
from bookwyrm import activitypub
from .base_model import ActivitypubMixin
from .base_model import ActivityMapping, BookWyrmModel
from .base_model import BookWyrmModel
from . import fields
class Attachment(ActivitypubMixin, BookWyrmModel):
''' an image (or, in the future, video etc) associated with a status '''
status = models.ForeignKey(
status = fields.ForeignKey(
'Status',
on_delete=models.CASCADE,
related_name='attachments',
null=True
)
reverse_unfurl = True
class Meta:
''' one day we'll have other types of attachments besides images '''
abstract = True
activity_mappings = [
ActivityMapping('id', 'remote_id'),
ActivityMapping('url', 'image'),
ActivityMapping('name', 'caption'),
]
class Image(Attachment):
''' an image attachment '''
image = models.ImageField(upload_to='status/', null=True, blank=True)
caption = models.TextField(null=True, blank=True)
image = fields.ImageField(upload_to='status/', null=True, blank=True)
caption = fields.TextField(null=True, blank=True)
activity_serializer = activitypub.Image

View file

@ -61,9 +61,19 @@ def get_field_name(field):
return components[0] + ''.join(x.title() for x in components[1:])
def unfurl_related_field(related_field):
''' load reverse lookups (like public key owner or Status attachment '''
if hasattr(related_field, 'all'):
return [unfurl_related_field(i) for i in related_field.all()]
if related_field.reverse_unfurl:
return related_field.to_activity()
return related_field.remote_id
class ActivitypubMixin:
''' add this mixin for models that are AP serializable '''
activity_serializer = lambda: {}
reverse_unfurl = False
def to_activity(self):
''' convert from a model to an activity '''
@ -73,18 +83,24 @@ class ActivitypubMixin:
continue
key = get_field_name(field)
value = field.to_activity(getattr(self, field.name))
if value is not None:
if value is None:
continue
if key in activity and isinstance(activity[key], list):
activity[key] += value
else:
activity[key] = value
if hasattr(self, 'serialize_reverse_fields'):
for field_name in self.serialize_reverse_fields:
activity[field_name] = getattr(self, field_name).remote_id
related_field = getattr(self, field_name)
activity[field_name] = unfurl_related_field(related_field)
return self.activity_serializer(**activity).serialize()
def to_create_activity(self, user, pure=False):
def to_create_activity(self, user):
''' returns the object wrapped in a Create activity '''
activity_object = self.to_activity(pure=pure)
activity_object = self.to_activity()
signer = pkcs1_15.new(RSA.import_key(user.private_key))
content = activity_object['content']
@ -100,8 +116,8 @@ class ActivitypubMixin:
return activitypub.Create(
id=create_id,
actor=user.remote_id,
to=['%s/followers' % user.remote_id],
cc=['https://www.w3.org/ns/activitystreams#Public'],
to=activity_object['to'],
cc=activity_object['cc'],
object=activity_object,
signature=signature,
).serialize()
@ -245,30 +261,3 @@ class ActivityMapping:
model_key: str
activity_formatter: Callable = lambda x: x
model_formatter: Callable = lambda x: x
def tag_formatter(items, name_field, activity_type):
''' helper function to format lists of foreign keys into Tags '''
tags = []
for item in items.all():
tags.append(activitypub.Link(
href=item.remote_id,
name=getattr(item, name_field),
type=activity_type
))
return tags
def image_formatter(image):
''' convert images into activitypub json '''
if image and hasattr(image, 'url'):
url = image.url
else:
return None
url = 'https://%s%s' % (DOMAIN, url)
return activitypub.Image(url=url)
def image_attachments_formatter(images):
''' create a list of image attachments '''
return [image_formatter(i) for i in images]

View file

@ -155,6 +155,7 @@ class Edition(Book):
'Work', on_delete=models.PROTECT, null=True, related_name='editions')
activity_serializer = activitypub.Edition
name_field = 'title'
def save(self, *args, **kwargs):
''' calculate isbn 10/13 '''

View file

@ -71,6 +71,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
)
def deconstruct(self):
''' implementation of models.Field deconstruct '''
name, path, args, kwargs = super().deconstruct()
del kwargs['verbose_name']
del kwargs['max_length']
@ -86,6 +87,8 @@ class UsernameField(ActivitypubFieldMixin, models.CharField):
class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
''' activitypub-aware foreign key field '''
def to_activity(self, value):
if not value:
return None
return value.remote_id
def from_activity(self, activity_data):
pass# TODO
@ -94,6 +97,10 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey):
class OneToOneField(ActivitypubFieldMixin, models.OneToOneField):
''' activitypub-aware foreign key field '''
def to_activity(self, value):
print('HIIIII')
print(value)
if not value:
return None
return value.to_activity()
def from_activity(self, activity_data):
@ -113,20 +120,45 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
def from_activity(self, activity_data):
if self.link_only:
return
values = super().from_activity(self, activity_data)
return None
values = super().from_activity(activity_data)
return values# TODO
class TagField(ManyToManyField):
''' special case of many to many that uses Tags '''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.activitypub_field = 'tag'
def to_activity(self, value):
tags = []
for item in value.all():
activity_type = item.__class__.__name__
if activity_type == 'User':
activity_type = 'Mention'
tags.append(activitypub.Link(
href=item.remote_id,
name=getattr(item, item.name_field),
type=activity_type
))
return tags
def image_serializer(value):
''' helper for serializing images '''
print(value)
if value and hasattr(value, 'url'):
url = value.url
else:
return None
url = 'https://%s%s' % (DOMAIN, url)
return activitypub.Image(url=url)
class ImageField(ActivitypubFieldMixin, models.ImageField):
''' activitypub-aware image field '''
def to_activity(self, value):
if value and hasattr(value, 'url'):
url = value.url
else:
return None
url = 'https://%s%s' % (DOMAIN, url)
return activitypub.Image(url=url)
return image_serializer(value)
def from_activity(self, activity_data):
image_slug = super().from_activity(activity_data)
@ -150,6 +182,12 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
return [image_name, image_content]
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
''' activitypub-aware datetime field '''
def to_activity(self, value):
return value.isoformat()
class CharField(ActivitypubFieldMixin, models.CharField):
''' activitypub-aware char field '''
@ -158,3 +196,6 @@ class TextField(ActivitypubFieldMixin, models.TextField):
class BooleanField(ActivitypubFieldMixin, models.BooleanField):
''' activitypub-aware boolean field '''
class IntegerField(ActivitypubFieldMixin, models.IntegerField):
''' activitypub-aware boolean field '''

View file

@ -6,26 +6,27 @@ from model_utils.managers import InheritanceManager
from bookwyrm import activitypub
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
from .base_model import ActivityMapping, BookWyrmModel, PrivacyLevels
from .base_model import tag_formatter, image_attachments_formatter
from .base_model import BookWyrmModel, PrivacyLevels
from . import fields
from .fields import image_serializer
class Status(OrderedCollectionPageMixin, BookWyrmModel):
''' any post, like a reply to a review, etc '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
content = models.TextField(blank=True, null=True)
mention_users = models.ManyToManyField('User', related_name='mention_user')
mention_books = models.ManyToManyField(
'Edition', related_name='mention_book')
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')
local = models.BooleanField(default=True)
privacy = models.CharField(
max_length=255,
default='public',
choices=PrivacyLevels.choices
)
sensitive = models.BooleanField(default=False)
sensitive = fields.BooleanField(default=False)
# the created date can't be this, because of receiving federated posts
published_date = models.DateTimeField(default=timezone.now)
published_date = fields.DateTimeField(
default=timezone.now, activitypub_field='published')
deleted = models.BooleanField(default=False)
deleted_date = models.DateTimeField(blank=True, null=True)
favorites = models.ManyToManyField(
@ -35,79 +36,21 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
through_fields=('status', 'user'),
related_name='user_favorites'
)
reply_parent = models.ForeignKey(
reply_parent = fields.ForeignKey(
'self',
null=True,
on_delete=models.PROTECT
on_delete=models.PROTECT,
activitypub_field='inReplyTo',
)
objects = InheritanceManager()
# ---- activitypub serialization settings for this model ----- #
@property
def ap_to(self):
''' should be related to post privacy I think '''
return ['https://www.w3.org/ns/activitystreams#Public']
@property
def ap_cc(self):
''' should be related to post privacy I think '''
return [self.user.ap_followers]
@property
def ap_replies(self):
''' structured replies block '''
return self.to_replies()
@property
def ap_status_image(self):
''' attach a book cover, if relevent '''
if hasattr(self, 'book'):
return self.book.ap_cover
if self.mention_books.first():
return self.mention_books.first().ap_cover
return None
shared_mappings = [
ActivityMapping('url', 'remote_id', lambda x: None),
ActivityMapping('id', 'remote_id'),
ActivityMapping('inReplyTo', 'reply_parent'),
ActivityMapping('published', 'published_date'),
ActivityMapping('attributedTo', 'user'),
ActivityMapping('to', 'ap_to'),
ActivityMapping('cc', 'ap_cc'),
ActivityMapping('replies', 'ap_replies'),
ActivityMapping(
'tag', 'mention_books',
lambda x: tag_formatter(x, 'title', 'Book'),
),
ActivityMapping(
'tag', 'mention_users',
lambda x: tag_formatter(x, 'username', 'Mention'),
),
ActivityMapping(
'attachment', 'attachments',
lambda x: image_attachments_formatter(x.all()),
)
]
# serializing to bookwyrm expanded activitypub
activity_mappings = shared_mappings + [
ActivityMapping('name', 'name'),
ActivityMapping('inReplyToBook', 'book'),
ActivityMapping('rating', 'rating'),
ActivityMapping('quote', 'quote'),
ActivityMapping('content', 'content'),
]
# for serializing to standard activitypub without extended types
pure_activity_mappings = shared_mappings + [
ActivityMapping('name', 'ap_pure_name'),
ActivityMapping('content', 'ap_pure_content'),
ActivityMapping('attachment', 'ap_status_image'),
]
activity_serializer = activitypub.Note
serialize_reverse_fields = ['attachments']
#----- replies collection activitypub ----#
@classmethod
@ -138,7 +81,43 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
deleted=self.deleted_date.isoformat(),
published=self.deleted_date.isoformat()
).serialize()
return ActivitypubMixin.to_activity(self, pure=pure)
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')\
.to_activity(self.user.followers)
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
def save(self, *args, **kwargs):
''' update user active time '''
@ -151,40 +130,40 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
class GeneratedNote(Status):
''' these are app-generated messages about user activity '''
@property
def ap_pure_content(self):
def pure_content(self):
''' indicate the book in question for mastodon (or w/e) users '''
message = self.content
books = ', '.join(
'<a href="%s">"%s"</a>' % (self.book.remote_id, self.book.title) \
'<a href="%s">"%s"</a>' % (book.remote_id, book.title) \
for book in self.mention_books.all()
)
return '%s %s' % (message, books)
return '%s %s %s' % (self.user.display_name, message, books)
activity_serializer = activitypub.GeneratedNote
pure_activity_serializer = activitypub.Note
pure_type = 'Note'
class Comment(Status):
''' like a review but without a rating and transient '''
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
book = fields.ForeignKey('Edition', on_delete=models.PROTECT)
@property
def ap_pure_content(self):
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>)' % \
(self.book.remote_id, self.book.title)
activity_serializer = activitypub.Comment
pure_activity_serializer = activitypub.Note
pure_type = 'Note'
class Quotation(Status):
''' like a review but without a rating and transient '''
quote = models.TextField()
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
quote = fields.TextField()
book = fields.ForeignKey('Edition', on_delete=models.PROTECT)
@property
def ap_pure_content(self):
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,
@ -194,14 +173,14 @@ class Quotation(Status):
)
activity_serializer = activitypub.Quotation
pure_activity_serializer = activitypub.Note
pure_type = 'Note'
class Review(Status):
''' a book review '''
name = models.CharField(max_length=255, null=True)
book = models.ForeignKey('Edition', on_delete=models.PROTECT)
rating = models.IntegerField(
name = fields.CharField(max_length=255, null=True)
book = fields.ForeignKey('Edition', on_delete=models.PROTECT)
rating = fields.IntegerField(
default=None,
null=True,
blank=True,
@ -209,7 +188,7 @@ class Review(Status):
)
@property
def ap_pure_name(self):
def pure_name(self):
''' clarify review names for mastodon serialization '''
if self.rating:
return 'Review of "%s" (%d stars): %s' % (
@ -223,26 +202,21 @@ class Review(Status):
)
@property
def ap_pure_content(self):
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>)' % \
(self.book.remote_id, self.book.title)
activity_serializer = activitypub.Review
pure_activity_serializer = activitypub.Article
pure_type = 'Article'
class Favorite(ActivitypubMixin, BookWyrmModel):
''' fav'ing a post '''
user = models.ForeignKey('User', on_delete=models.PROTECT)
status = models.ForeignKey('Status', on_delete=models.PROTECT)
# ---- activitypub serialization settings for this model ----- #
activity_mappings = [
ActivityMapping('id', 'remote_id'),
ActivityMapping('actor', 'user'),
ActivityMapping('object', 'status'),
]
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
@ -252,7 +226,6 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
self.user.save()
super().save(*args, **kwargs)
class Meta:
''' can't fav things twice '''
unique_together = ('user', 'status')
@ -260,16 +233,12 @@ class Favorite(ActivitypubMixin, BookWyrmModel):
class Boost(Status):
''' boost'ing a post '''
boosted_status = models.ForeignKey(
boosted_status = fields.ForeignKey(
'Status',
on_delete=models.PROTECT,
related_name="boosters")
activity_mappings = [
ActivityMapping('id', 'remote_id'),
ActivityMapping('actor', 'user'),
ActivityMapping('object', 'boosted_status'),
]
related_name='boosters',
activitypub_field='object',
)
activity_serializer = activitypub.Boost

View file

@ -88,8 +88,14 @@ class User(OrderedCollectionPageMixin, AbstractUser):
last_active_date = models.DateTimeField(auto_now=True)
manually_approves_followers = fields.BooleanField(default=False)
@property
def display_name(self):
''' show the cleanest version of the user's name possible '''
if self.name != '':
return self.name
return self.localname or self.username
activity_serializer = activitypub.Person
serialize_related = []
def to_outbox(self, **kwargs):
''' an ordered collection of statuses '''
@ -112,7 +118,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
return self.to_ordered_collection(self.followers, \
remote_id=remote_id, id_only=True, **kwargs)
def to_activity(self, pure=False):
def to_activity(self):
''' override default AP serializer to add context object
idk if this is the best way to go about this '''
activity_object = super().to_activity()