Merge pull request #625 from mouse-reeve/inbox-refactor

Inbox refactor
This commit is contained in:
Mouse Reeve
2021-02-24 13:34:59 -08:00
committed by GitHub
43 changed files with 958 additions and 887 deletions

View File

@ -157,10 +157,14 @@ class ActivitypubMixin:
return recipients
def to_activity(self):
def to_activity_dataclass(self):
''' convert from a model to an activity '''
activity = generate_activity(self)
return self.activity_serializer(**activity).serialize()
return self.activity_serializer(**activity)
def to_activity(self, **kwargs): # pylint: disable=unused-argument
''' convert from a model to a json activity '''
return self.to_activity_dataclass().serialize()
class ObjectMixin(ActivitypubMixin):
@ -188,7 +192,7 @@ class ObjectMixin(ActivitypubMixin):
try:
software = None
# do we have a "pure" activitypub version of this for mastodon?
# do we have a "pure" activitypub version of this for mastodon?
if hasattr(self, 'pure_content'):
pure_activity = self.to_create_activity(user, pure=True)
self.broadcast(pure_activity, user, software='other')
@ -196,7 +200,7 @@ class ObjectMixin(ActivitypubMixin):
# sends to BW only if we just did a pure version for masto
activity = self.to_create_activity(user)
self.broadcast(activity, user, software=software)
except KeyError:
except AttributeError:
# janky as heck, this catches the mutliple inheritence chain
# for boosts and ignores this auxilliary broadcast
return
@ -225,26 +229,26 @@ class ObjectMixin(ActivitypubMixin):
def to_create_activity(self, user, **kwargs):
''' returns the object wrapped in a Create activity '''
activity_object = self.to_activity(**kwargs)
activity_object = self.to_activity_dataclass(**kwargs)
signature = None
create_id = self.remote_id + '/activity'
if 'content' in activity_object and activity_object['content']:
if hasattr(activity_object, 'content') and activity_object.content:
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
content = activity_object['content']
content = activity_object.content
signed_message = signer.sign(SHA256.new(content.encode('utf8')))
signature = activitypub.Signature(
creator='%s#main-key' % user.remote_id,
created=activity_object['published'],
created=activity_object.published,
signatureValue=b64encode(signed_message).decode('utf8')
)
return activitypub.Create(
id=create_id,
actor=user.remote_id,
to=activity_object['to'],
cc=activity_object['cc'],
to=activity_object.to,
cc=activity_object.cc,
object=activity_object,
signature=signature,
).serialize()
@ -257,7 +261,7 @@ class ObjectMixin(ActivitypubMixin):
actor=user.remote_id,
to=['%s/followers' % user.remote_id],
cc=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity(),
object=self,
).serialize()
@ -268,7 +272,7 @@ class ObjectMixin(ActivitypubMixin):
id=activity_id,
actor=user.remote_id,
to=['https://www.w3.org/ns/activitystreams#Public'],
object=self.to_activity()
object=self
).serialize()
@ -283,7 +287,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
def to_ordered_collection(self, queryset, \
remote_id=None, page=False, collection_only=False, **kwargs):
'pure=pure, '' an ordered collection of whatevers '''
''' an ordered collection of whatevers '''
if not queryset.ordered:
raise RuntimeError('queryset must be ordered')
@ -309,7 +313,7 @@ class OrderedCollectionPageMixin(ObjectMixin):
activity['first'] = '%s?page=1' % remote_id
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
return serializer(**activity).serialize()
return serializer(**activity)
class OrderedCollectionMixin(OrderedCollectionPageMixin):
@ -321,9 +325,13 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin):
activity_serializer = activitypub.OrderedCollection
def to_activity_dataclass(self, **kwargs):
return self.to_ordered_collection(self.collection_queryset, **kwargs)
def to_activity(self, **kwargs):
''' an ordered collection of the specified model queryset '''
return self.to_ordered_collection(self.collection_queryset, **kwargs)
return self.to_ordered_collection(
self.collection_queryset, **kwargs).serialize()
class CollectionItemMixin(ActivitypubMixin):
@ -360,7 +368,7 @@ class CollectionItemMixin(ActivitypubMixin):
return activitypub.Add(
id='%s#add' % self.remote_id,
actor=self.user.remote_id,
object=object_field.to_activity(),
object=object_field,
target=collection_field.remote_id
).serialize()
@ -371,7 +379,7 @@ class CollectionItemMixin(ActivitypubMixin):
return activitypub.Remove(
id='%s#remove' % self.remote_id,
actor=self.user.remote_id,
object=object_field.to_activity(),
object=object_field,
target=collection_field.remote_id
).serialize()
@ -400,7 +408,7 @@ class ActivityMixin(ActivitypubMixin):
return activitypub.Undo(
id='%s#undo' % self.remote_id,
actor=user.remote_id,
object=self.to_activity()
object=self,
).serialize()
@ -495,4 +503,4 @@ def to_ordered_collection_page(
orderedItems=items,
next=next_page,
prev=prev_page
).serialize()
)

View File

@ -122,13 +122,12 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
return None
related_model = self.related_model
if isinstance(value, dict) and value.get('id'):
if hasattr(value, 'id') and value.id:
if not self.load_remote:
# only look in the local database
return related_model.find_existing(value)
return related_model.find_existing(value.serialize())
# this is an activitypub object, which we can deserialize
activity_serializer = related_model.activity_serializer
return activity_serializer(**value).to_model(related_model)
return value.to_model(model=related_model)
try:
# make sure the value looks like a remote id
validate_remote_id(value)
@ -139,7 +138,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin):
if not self.load_remote:
# only look in the local database
return related_model.find_existing_by_remote_id(value)
return activitypub.resolve_remote_id(related_model, value)
return activitypub.resolve_remote_id(value, model=related_model)
class RemoteIdField(ActivitypubFieldMixin, models.CharField):
@ -280,7 +279,8 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
except ValidationError:
continue
items.append(
activitypub.resolve_remote_id(self.related_model, remote_id)
activitypub.resolve_remote_id(
remote_id, model=self.related_model)
)
return items
@ -317,7 +317,8 @@ class TagField(ManyToManyField):
# tags can contain multiple types
continue
items.append(
activitypub.resolve_remote_id(self.related_model, link.href)
activitypub.resolve_remote_id(
link.href, model=self.related_model)
)
return items
@ -366,8 +367,8 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
image_slug = value
# when it's an inline image (User avatar/icon, Book cover), it's a json
# blob, but when it's an attached image, it's just a url
if isinstance(image_slug, dict):
url = image_slug.get('url')
if hasattr(image_slug, 'url'):
url = image_slug.url
elif isinstance(image_slug, str):
url = image_slug
else:

View File

@ -68,7 +68,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
order = fields.IntegerField(blank=True, null=True)
endorsement = models.ManyToManyField('User', related_name='endorsers')
activity_serializer = activitypub.AddListItem
activity_serializer = activitypub.Add
object_field = 'book'
collection_field = 'book_list'

View File

@ -5,6 +5,7 @@ from django.db.models import Q
from bookwyrm import activitypub
from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import generate_activity
from .base_model import BookWyrmModel
from . import fields
@ -55,10 +56,13 @@ class UserRelationship(BookWyrmModel):
return '%s#%s/%d' % (base_path, status, self.id)
class UserFollows(ActivitypubMixin, UserRelationship):
class UserFollows(ActivityMixin, UserRelationship):
''' Following a user '''
status = 'follows'
activity_serializer = activitypub.Follow
def to_activity(self):
''' overrides default to manually set serializer '''
return activitypub.Follow(**generate_activity(self))
def save(self, *args, **kwargs):
''' really really don't let a user follow someone who blocked them '''
@ -73,7 +77,9 @@ class UserFollows(ActivitypubMixin, UserRelationship):
)
).exists():
raise IntegrityError()
super().save(*args, **kwargs)
# don't broadcast this type of relationship -- accepts and requests
# are handled by the UserFollowRequest model
super().save(*args, broadcast=False, **kwargs)
@classmethod
def from_request(cls, follow_request):
@ -109,16 +115,19 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
)
).exists():
raise IntegrityError()
super().save(*args, **kwargs)
if broadcast and self.user_subject.local and not self.user_object.local:
self.broadcast(self.to_activity(), self.user_subject)
if self.user_object.local:
manually_approves = self.user_object.manually_approves_followers
if not manually_approves:
self.accept()
model = apps.get_model('bookwyrm.Notification', require_ready=True)
notification_type = 'FOLLOW_REQUEST' \
if self.user_object.manually_approves_followers else 'FOLLOW'
notification_type = 'FOLLOW_REQUEST' if \
manually_approves else 'FOLLOW'
model.objects.create(
user=self.user_object,
related_user=self.user_subject,
@ -129,28 +138,30 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
def accept(self):
''' turn this request into the real deal'''
user = self.user_object
activity = activitypub.Accept(
id=self.get_remote_id(status='accepts'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
if not self.user_subject.local:
activity = activitypub.Accept(
id=self.get_remote_id(status='accepts'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
self.broadcast(activity, user)
with transaction.atomic():
UserFollows.from_request(self)
self.delete()
self.broadcast(activity, user)
def reject(self):
''' generate a Reject for this follow request '''
user = self.user_object
activity = activitypub.Reject(
id=self.get_remote_id(status='rejects'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
if self.user_object.local:
activity = activitypub.Reject(
id=self.get_remote_id(status='rejects'),
actor=self.user_object.remote_id,
object=self.to_activity()
).serialize()
self.broadcast(activity, self.user_object)
self.delete()
self.broadcast(activity, user)
class UserBlocks(ActivityMixin, UserRelationship):

View File

@ -57,7 +57,7 @@ class ShelfBook(CollectionItemMixin, BookWyrmModel):
user = fields.ForeignKey(
'User', on_delete=models.PROTECT, activitypub_field='actor')
activity_serializer = activitypub.AddBook
activity_serializer = activitypub.Add
object_field = 'book'
collection_field = 'shelf'

View File

@ -84,6 +84,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
related_status=self,
)
def delete(self, *args, **kwargs):#pylint: disable=unused-argument
''' "delete" a status '''
if hasattr(self, 'boosted_status'):
# okay but if it's a boost really delete it
super().delete(*args, **kwargs)
return
self.deleted = True
self.deleted_date = timezone.now()
self.save()
@property
def recipients(self):
''' tagged users who definitely need to get this status in broadcast '''
@ -96,6 +106,12 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
@classmethod
def ignore_activity(cls, activity):
''' keep notes if they are replies to existing statuses '''
if activity.type == 'Announce':
# keep it if the booster or the boosted are local
boosted = activitypub.resolve_remote_id(activity.object, save=False)
return cls.ignore_activity(boosted.to_activity_dataclass())
# keep if it if it's a custom type
if activity.type != 'Note':
return False
if cls.objects.filter(
@ -106,8 +122,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
if activity.tag == MISSING or activity.tag is None:
return True
tags = [l['href'] for l in activity.tag if l['type'] == 'Mention']
user_model = apps.get_model('bookwyrm.User', require_ready=True)
for tag in tags:
user_model = apps.get_model('bookwyrm.User', require_ready=True)
if user_model.objects.filter(
remote_id=tag, local=True).exists():
# we found a mention of a known use boost
@ -139,9 +155,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
remote_id='%s/replies' % self.remote_id,
collection_only=True,
**kwargs
)
).serialize()
def to_activity(self, pure=False):# pylint: disable=arguments-differ
def to_activity_dataclass(self, pure=False):# pylint: disable=arguments-differ
''' return tombstone if the status is deleted '''
if self.deleted:
return activitypub.Tombstone(
@ -149,25 +165,29 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
url=self.remote_id,
deleted=self.deleted_date.isoformat(),
published=self.deleted_date.isoformat()
).serialize()
activity = ActivitypubMixin.to_activity(self)
activity['replies'] = self.to_replies()
)
activity = ActivitypubMixin.to_activity_dataclass(self)
activity.replies = self.to_replies()
# "pure" serialization for non-bookwyrm instances
if pure and hasattr(self, 'pure_content'):
activity['content'] = self.pure_content
if 'name' in activity:
activity['name'] = self.pure_name
activity['type'] = self.pure_type
activity['attachment'] = [
activity.content = self.pure_content
if hasattr(activity, 'name'):
activity.name = self.pure_name
activity.type = self.pure_type
activity.attachment = [
image_serializer(b.cover, b.alt_text) \
for b in self.mention_books.all()[:4] if b.cover]
if hasattr(self, 'book') and self.book.cover:
activity['attachment'].append(
activity.attachment.append(
image_serializer(self.book.cover, self.book.alt_text)
)
return activity
def to_activity(self, pure=False):# pylint: disable=arguments-differ
''' json serialized activitypub class '''
return self.to_activity_dataclass(pure=pure).serialize()
class GeneratedNote(Status):
''' these are app-generated messages about user activity '''
@ -266,7 +286,7 @@ class Boost(ActivityMixin, Status):
related_name='boosters',
activitypub_field='object',
)
activity_serializer = activitypub.Boost
activity_serializer = activitypub.Announce
def save(self, *args, **kwargs):
''' save and notify '''

View File

@ -1,6 +1,7 @@
''' models for storing different kinds of Activities '''
import urllib.parse
from django.apps import apps
from django.db import models
from bookwyrm import activitypub
@ -15,17 +16,15 @@ class Tag(OrderedCollectionMixin, BookWyrmModel):
name = fields.CharField(max_length=100, unique=True)
identifier = models.CharField(max_length=100)
@classmethod
def book_queryset(cls, identifier):
''' county of books associated with this tag '''
return cls.objects.filter(
identifier=identifier
).order_by('-updated_date')
@property
def collection_queryset(self):
''' books associated with this tag '''
return self.book_queryset(self.identifier)
def books(self):
''' count of books associated with this tag '''
edition_model = apps.get_model('bookwyrm.Edition', require_ready=True)
return edition_model.objects.filter(
usertag__tag__identifier=self.identifier
).order_by('-created_date').distinct()
collection_queryset = books
def get_remote_id(self):
''' tag should use identifier not id in remote_id '''
@ -50,7 +49,7 @@ class UserTag(CollectionItemMixin, BookWyrmModel):
tag = fields.ForeignKey(
'Tag', on_delete=models.PROTECT, activitypub_field='target')
activity_serializer = activitypub.AddBook
activity_serializer = activitypub.Add
object_field = 'book'
collection_field = 'tag'

View File

@ -140,7 +140,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
privacy__in=['public', 'unlisted'],
).select_subclasses().order_by('-published_date')
return self.to_ordered_collection(queryset, \
collection_only=True, remote_id=self.outbox, **kwargs)
collection_only=True, remote_id=self.outbox, **kwargs).serialize()
def to_following_activity(self, **kwargs):
''' activitypub following list '''
@ -375,4 +375,4 @@ def get_remote_reviews(outbox):
for activity in data['orderedItems']:
if not activity['type'] == 'Review':
continue
activitypub.Review(**activity).to_model(Review)
activitypub.Review(**activity).to_model()