mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-27 12:01:14 +00:00
Moves activitypub mixin to its own file
This commit is contained in:
parent
ae0034e678
commit
dfb5c396b0
13 changed files with 291 additions and 280 deletions
|
@ -1,6 +1,6 @@
|
||||||
# Generated by Django 3.0.7 on 2020-11-30 18:19
|
# Generated by Django 3.0.7 on 2020-11-30 18:19
|
||||||
|
|
||||||
import bookwyrm.models.base_model
|
import bookwyrm.models.activitypub_mixin
|
||||||
import bookwyrm.models.fields
|
import bookwyrm.models.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
|
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='user',
|
model_name='user',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Generated by Django 3.0.7 on 2021-01-31 16:14
|
# Generated by Django 3.0.7 on 2021-01-31 16:14
|
||||||
|
|
||||||
import bookwyrm.models.base_model
|
import bookwyrm.models.activitypub_mixin
|
||||||
import bookwyrm.models.fields
|
import bookwyrm.models.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -29,7 +29,7 @@ class Migration(migrations.Migration):
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.base_model.OrderedCollectionMixin, models.Model),
|
bases=(bookwyrm.models.activitypub_mixin.OrderedCollectionMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ListItem',
|
name='ListItem',
|
||||||
|
@ -50,7 +50,7 @@ class Migration(migrations.Migration):
|
||||||
'ordering': ('-created_date',),
|
'ordering': ('-created_date',),
|
||||||
'unique_together': {('book', 'book_list')},
|
'unique_together': {('book', 'book_list')},
|
||||||
},
|
},
|
||||||
bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model),
|
bases=(bookwyrm.models.activitypub_mixin.ActivitypubMixin, models.Model),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='list',
|
model_name='list',
|
||||||
|
|
268
bookwyrm/models/activitypub_mixin.py
Normal file
268
bookwyrm/models/activitypub_mixin.py
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
''' base model with default fields '''
|
||||||
|
from base64 import b64encode
|
||||||
|
from functools import reduce
|
||||||
|
import operator
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Signature import pkcs1_15
|
||||||
|
from Crypto.Hash import SHA256
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from bookwyrm import activitypub
|
||||||
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
from .fields import ImageField, ManyToManyField
|
||||||
|
|
||||||
|
|
||||||
|
class ActivitypubMixin:
|
||||||
|
''' add this mixin for models that are AP serializable '''
|
||||||
|
activity_serializer = lambda: {}
|
||||||
|
reverse_unfurl = False
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
''' collect some info on model fields '''
|
||||||
|
self.image_fields = []
|
||||||
|
self.many_to_many_fields = []
|
||||||
|
self.simple_fields = [] # "simple"
|
||||||
|
for field in self._meta.get_fields():
|
||||||
|
if not hasattr(field, 'field_to_activity'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(field, ImageField):
|
||||||
|
self.image_fields.append(field)
|
||||||
|
elif isinstance(field, ManyToManyField):
|
||||||
|
self.many_to_many_fields.append(field)
|
||||||
|
else:
|
||||||
|
self.simple_fields.append(field)
|
||||||
|
|
||||||
|
self.activity_fields = self.image_fields + \
|
||||||
|
self.many_to_many_fields + self.simple_fields
|
||||||
|
|
||||||
|
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
|
||||||
|
if hasattr(self, 'deserialize_reverse_fields') else []
|
||||||
|
self.serialize_reverse_fields = self.serialize_reverse_fields \
|
||||||
|
if hasattr(self, 'serialize_reverse_fields') else []
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_existing_by_remote_id(cls, remote_id):
|
||||||
|
''' look up a remote id in the db '''
|
||||||
|
return cls.find_existing({'id': remote_id})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_existing(cls, data):
|
||||||
|
''' compare data to fields that can be used for deduplation.
|
||||||
|
This always includes remote_id, but can also be unique identifiers
|
||||||
|
like an isbn for an edition '''
|
||||||
|
filters = []
|
||||||
|
for field in cls._meta.get_fields():
|
||||||
|
if not hasattr(field, 'deduplication_field') or \
|
||||||
|
not field.deduplication_field:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = data.get(field.get_activitypub_field())
|
||||||
|
if not value:
|
||||||
|
continue
|
||||||
|
filters.append({field.name: value})
|
||||||
|
|
||||||
|
if hasattr(cls, 'origin_id') and 'id' in data:
|
||||||
|
# kinda janky, but this handles special case for books
|
||||||
|
filters.append({'origin_id': data['id']})
|
||||||
|
|
||||||
|
if not filters:
|
||||||
|
# if there are no deduplication fields, it will match the first
|
||||||
|
# item no matter what. this shouldn't happen but just in case.
|
||||||
|
return None
|
||||||
|
|
||||||
|
objects = cls.objects
|
||||||
|
if hasattr(objects, 'select_subclasses'):
|
||||||
|
objects = objects.select_subclasses()
|
||||||
|
|
||||||
|
# an OR operation on all the match fields
|
||||||
|
match = objects.filter(
|
||||||
|
reduce(
|
||||||
|
operator.or_, (Q(**f) for f in filters)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# there OUGHT to be only one match
|
||||||
|
return match.first()
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast(self):
|
||||||
|
''' send out an activity '''
|
||||||
|
|
||||||
|
def to_activity(self):
|
||||||
|
''' convert from a model to an activity '''
|
||||||
|
activity = generate_activity(self)
|
||||||
|
return self.activity_serializer(**activity).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def to_create_activity(self, user, **kwargs):
|
||||||
|
''' returns the object wrapped in a Create activity '''
|
||||||
|
activity_object = self.to_activity(**kwargs)
|
||||||
|
|
||||||
|
signature = None
|
||||||
|
create_id = self.remote_id + '/activity'
|
||||||
|
if 'content' in activity_object:
|
||||||
|
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
||||||
|
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'],
|
||||||
|
signatureValue=b64encode(signed_message).decode('utf8')
|
||||||
|
)
|
||||||
|
|
||||||
|
return activitypub.Create(
|
||||||
|
id=create_id,
|
||||||
|
actor=user.remote_id,
|
||||||
|
to=activity_object['to'],
|
||||||
|
cc=activity_object['cc'],
|
||||||
|
object=activity_object,
|
||||||
|
signature=signature,
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def to_delete_activity(self, user):
|
||||||
|
''' notice of deletion '''
|
||||||
|
return activitypub.Delete(
|
||||||
|
id=self.remote_id + '/activity',
|
||||||
|
actor=user.remote_id,
|
||||||
|
to=['%s/followers' % user.remote_id],
|
||||||
|
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
object=self.to_activity(),
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def to_update_activity(self, user):
|
||||||
|
''' wrapper for Updates to an activity '''
|
||||||
|
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
||||||
|
return activitypub.Update(
|
||||||
|
id=activity_id,
|
||||||
|
actor=user.remote_id,
|
||||||
|
to=['https://www.w3.org/ns/activitystreams#Public'],
|
||||||
|
object=self.to_activity()
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
def to_undo_activity(self, user):
|
||||||
|
''' undo an action '''
|
||||||
|
return activitypub.Undo(
|
||||||
|
id='%s#undo' % self.remote_id,
|
||||||
|
actor=user.remote_id,
|
||||||
|
object=self.to_activity()
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedCollectionPageMixin(ActivitypubMixin):
|
||||||
|
''' just the paginator utilities, so you don't HAVE to
|
||||||
|
override ActivitypubMixin's to_activity (ie, for outbox '''
|
||||||
|
@property
|
||||||
|
def collection_remote_id(self):
|
||||||
|
''' this can be overriden if there's a special remote id, ie outbox '''
|
||||||
|
return self.remote_id
|
||||||
|
|
||||||
|
|
||||||
|
def to_ordered_collection(self, queryset, \
|
||||||
|
remote_id=None, page=False, collection_only=False, **kwargs):
|
||||||
|
''' an ordered collection of whatevers '''
|
||||||
|
if not queryset.ordered:
|
||||||
|
raise RuntimeError('queryset must be ordered')
|
||||||
|
|
||||||
|
remote_id = remote_id or self.remote_id
|
||||||
|
if page:
|
||||||
|
return to_ordered_collection_page(
|
||||||
|
queryset, remote_id, **kwargs)
|
||||||
|
|
||||||
|
if collection_only or not hasattr(self, 'activity_serializer'):
|
||||||
|
serializer = activitypub.OrderedCollection
|
||||||
|
activity = {}
|
||||||
|
else:
|
||||||
|
serializer = self.activity_serializer
|
||||||
|
# a dict from the model fields
|
||||||
|
activity = generate_activity(self)
|
||||||
|
|
||||||
|
if remote_id:
|
||||||
|
activity['id'] = remote_id
|
||||||
|
|
||||||
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
|
# add computed fields specific to orderd collections
|
||||||
|
activity['totalItems'] = paginated.count
|
||||||
|
activity['first'] = '%s?page=1' % remote_id
|
||||||
|
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
||||||
|
|
||||||
|
return serializer(**activity).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def to_ordered_collection_page(
|
||||||
|
queryset, remote_id, id_only=False, page=1, **kwargs):
|
||||||
|
''' serialize and pagiante a queryset '''
|
||||||
|
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||||
|
|
||||||
|
activity_page = paginated.page(page)
|
||||||
|
if id_only:
|
||||||
|
items = [s.remote_id for s in activity_page.object_list]
|
||||||
|
else:
|
||||||
|
items = [s.to_activity() for s in activity_page.object_list]
|
||||||
|
|
||||||
|
prev_page = next_page = None
|
||||||
|
if activity_page.has_next():
|
||||||
|
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
||||||
|
if activity_page.has_previous():
|
||||||
|
prev_page = '%s?page=%d' % \
|
||||||
|
(remote_id, activity_page.previous_page_number())
|
||||||
|
return activitypub.OrderedCollectionPage(
|
||||||
|
id='%s?page=%s' % (remote_id, page),
|
||||||
|
partOf=remote_id,
|
||||||
|
orderedItems=items,
|
||||||
|
next=next_page,
|
||||||
|
prev=prev_page
|
||||||
|
).serialize()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
||||||
|
''' extends activitypub models to work as ordered collections '''
|
||||||
|
@property
|
||||||
|
def collection_queryset(self):
|
||||||
|
''' usually an ordered collection model aggregates a different model '''
|
||||||
|
raise NotImplementedError('Model must define collection_queryset')
|
||||||
|
|
||||||
|
activity_serializer = activitypub.OrderedCollection
|
||||||
|
|
||||||
|
def to_activity(self, **kwargs):
|
||||||
|
''' an ordered collection of the specified model queryset '''
|
||||||
|
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_activity(obj):
|
||||||
|
''' go through the fields on an object '''
|
||||||
|
activity = {}
|
||||||
|
for field in obj.activity_fields:
|
||||||
|
field.set_activity_from_field(activity, obj)
|
||||||
|
|
||||||
|
if hasattr(obj, 'serialize_reverse_fields'):
|
||||||
|
# for example, editions of a work
|
||||||
|
for model_field_name, activity_field_name, sort_field in \
|
||||||
|
obj.serialize_reverse_fields:
|
||||||
|
related_field = getattr(obj, model_field_name)
|
||||||
|
activity[activity_field_name] = \
|
||||||
|
unfurl_related_field(related_field, sort_field)
|
||||||
|
|
||||||
|
if not activity.get('id'):
|
||||||
|
activity['id'] = obj.get_remote_id()
|
||||||
|
return activity
|
||||||
|
|
||||||
|
|
||||||
|
def unfurl_related_field(related_field, sort_field=None):
|
||||||
|
''' 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.order_by(
|
||||||
|
sort_field).all()]
|
||||||
|
if related_field.reverse_unfurl:
|
||||||
|
return related_field.field_to_activity()
|
||||||
|
return related_field.remote_id
|
|
@ -2,7 +2,7 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .base_model import ActivitypubMixin
|
from .activitypub_mixin import ActivitypubMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,9 @@
|
||||||
''' base model with default fields '''
|
''' base model with default fields '''
|
||||||
from base64 import b64encode
|
|
||||||
from functools import reduce
|
|
||||||
import operator
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from Crypto.Signature import pkcs1_15
|
|
||||||
from Crypto.Hash import SHA256
|
|
||||||
from django.core.paginator import Paginator
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm.settings import DOMAIN
|
||||||
from bookwyrm.settings import DOMAIN, PAGE_LENGTH
|
from .fields import RemoteIdField
|
||||||
from .fields import ImageField, ManyToManyField, RemoteIdField
|
|
||||||
|
|
||||||
|
|
||||||
class BookWyrmModel(models.Model):
|
class BookWyrmModel(models.Model):
|
||||||
|
@ -50,253 +39,3 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
|
||||||
if not instance.remote_id:
|
if not instance.remote_id:
|
||||||
instance.remote_id = instance.get_remote_id()
|
instance.remote_id = instance.get_remote_id()
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
|
||||||
def unfurl_related_field(related_field, sort_field=None):
|
|
||||||
''' 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.order_by(
|
|
||||||
sort_field).all()]
|
|
||||||
if related_field.reverse_unfurl:
|
|
||||||
return related_field.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 __init__(self, *args, **kwargs):
|
|
||||||
''' collect some info on model fields '''
|
|
||||||
self.image_fields = []
|
|
||||||
self.many_to_many_fields = []
|
|
||||||
self.simple_fields = [] # "simple"
|
|
||||||
for field in self._meta.get_fields():
|
|
||||||
if not hasattr(field, 'field_to_activity'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if isinstance(field, ImageField):
|
|
||||||
self.image_fields.append(field)
|
|
||||||
elif isinstance(field, ManyToManyField):
|
|
||||||
self.many_to_many_fields.append(field)
|
|
||||||
else:
|
|
||||||
self.simple_fields.append(field)
|
|
||||||
|
|
||||||
self.activity_fields = self.image_fields + \
|
|
||||||
self.many_to_many_fields + self.simple_fields
|
|
||||||
|
|
||||||
self.deserialize_reverse_fields = self.deserialize_reverse_fields \
|
|
||||||
if hasattr(self, 'deserialize_reverse_fields') else []
|
|
||||||
self.serialize_reverse_fields = self.serialize_reverse_fields \
|
|
||||||
if hasattr(self, 'serialize_reverse_fields') else []
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_existing_by_remote_id(cls, remote_id):
|
|
||||||
''' look up a remote id in the db '''
|
|
||||||
return cls.find_existing({'id': remote_id})
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_existing(cls, data):
|
|
||||||
''' compare data to fields that can be used for deduplation.
|
|
||||||
This always includes remote_id, but can also be unique identifiers
|
|
||||||
like an isbn for an edition '''
|
|
||||||
filters = []
|
|
||||||
for field in cls._meta.get_fields():
|
|
||||||
if not hasattr(field, 'deduplication_field') or \
|
|
||||||
not field.deduplication_field:
|
|
||||||
continue
|
|
||||||
|
|
||||||
value = data.get(field.get_activitypub_field())
|
|
||||||
if not value:
|
|
||||||
continue
|
|
||||||
filters.append({field.name: value})
|
|
||||||
|
|
||||||
if hasattr(cls, 'origin_id') and 'id' in data:
|
|
||||||
# kinda janky, but this handles special case for books
|
|
||||||
filters.append({'origin_id': data['id']})
|
|
||||||
|
|
||||||
if not filters:
|
|
||||||
# if there are no deduplication fields, it will match the first
|
|
||||||
# item no matter what. this shouldn't happen but just in case.
|
|
||||||
return None
|
|
||||||
|
|
||||||
objects = cls.objects
|
|
||||||
if hasattr(objects, 'select_subclasses'):
|
|
||||||
objects = objects.select_subclasses()
|
|
||||||
|
|
||||||
# an OR operation on all the match fields
|
|
||||||
match = objects.filter(
|
|
||||||
reduce(
|
|
||||||
operator.or_, (Q(**f) for f in filters)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# there OUGHT to be only one match
|
|
||||||
return match.first()
|
|
||||||
|
|
||||||
|
|
||||||
def to_activity(self):
|
|
||||||
''' convert from a model to an activity '''
|
|
||||||
activity = generate_activity(self)
|
|
||||||
return self.activity_serializer(**activity).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
def to_create_activity(self, user, **kwargs):
|
|
||||||
''' returns the object wrapped in a Create activity '''
|
|
||||||
activity_object = self.to_activity(**kwargs)
|
|
||||||
|
|
||||||
signature = None
|
|
||||||
create_id = self.remote_id + '/activity'
|
|
||||||
if 'content' in activity_object:
|
|
||||||
signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key))
|
|
||||||
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'],
|
|
||||||
signatureValue=b64encode(signed_message).decode('utf8')
|
|
||||||
)
|
|
||||||
|
|
||||||
return activitypub.Create(
|
|
||||||
id=create_id,
|
|
||||||
actor=user.remote_id,
|
|
||||||
to=activity_object['to'],
|
|
||||||
cc=activity_object['cc'],
|
|
||||||
object=activity_object,
|
|
||||||
signature=signature,
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
def to_delete_activity(self, user):
|
|
||||||
''' notice of deletion '''
|
|
||||||
return activitypub.Delete(
|
|
||||||
id=self.remote_id + '/activity',
|
|
||||||
actor=user.remote_id,
|
|
||||||
to=['%s/followers' % user.remote_id],
|
|
||||||
cc=['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
object=self.to_activity(),
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
def to_update_activity(self, user):
|
|
||||||
''' wrapper for Updates to an activity '''
|
|
||||||
activity_id = '%s#update/%s' % (self.remote_id, uuid4())
|
|
||||||
return activitypub.Update(
|
|
||||||
id=activity_id,
|
|
||||||
actor=user.remote_id,
|
|
||||||
to=['https://www.w3.org/ns/activitystreams#Public'],
|
|
||||||
object=self.to_activity()
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
def to_undo_activity(self, user):
|
|
||||||
''' undo an action '''
|
|
||||||
return activitypub.Undo(
|
|
||||||
id='%s#undo' % self.remote_id,
|
|
||||||
actor=user.remote_id,
|
|
||||||
object=self.to_activity()
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderedCollectionPageMixin(ActivitypubMixin):
|
|
||||||
''' just the paginator utilities, so you don't HAVE to
|
|
||||||
override ActivitypubMixin's to_activity (ie, for outbox '''
|
|
||||||
@property
|
|
||||||
def collection_remote_id(self):
|
|
||||||
''' this can be overriden if there's a special remote id, ie outbox '''
|
|
||||||
return self.remote_id
|
|
||||||
|
|
||||||
|
|
||||||
def to_ordered_collection(self, queryset, \
|
|
||||||
remote_id=None, page=False, collection_only=False, **kwargs):
|
|
||||||
''' an ordered collection of whatevers '''
|
|
||||||
if not queryset.ordered:
|
|
||||||
raise RuntimeError('queryset must be ordered')
|
|
||||||
|
|
||||||
remote_id = remote_id or self.remote_id
|
|
||||||
if page:
|
|
||||||
return to_ordered_collection_page(
|
|
||||||
queryset, remote_id, **kwargs)
|
|
||||||
|
|
||||||
if collection_only or not hasattr(self, 'activity_serializer'):
|
|
||||||
serializer = activitypub.OrderedCollection
|
|
||||||
activity = {}
|
|
||||||
else:
|
|
||||||
serializer = self.activity_serializer
|
|
||||||
# a dict from the model fields
|
|
||||||
activity = generate_activity(self)
|
|
||||||
|
|
||||||
if remote_id:
|
|
||||||
activity['id'] = remote_id
|
|
||||||
|
|
||||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
|
||||||
# add computed fields specific to orderd collections
|
|
||||||
activity['totalItems'] = paginated.count
|
|
||||||
activity['first'] = '%s?page=1' % remote_id
|
|
||||||
activity['last'] = '%s?page=%d' % (remote_id, paginated.num_pages)
|
|
||||||
|
|
||||||
return serializer(**activity).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def to_ordered_collection_page(
|
|
||||||
queryset, remote_id, id_only=False, page=1, **kwargs):
|
|
||||||
''' serialize and pagiante a queryset '''
|
|
||||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
|
||||||
|
|
||||||
activity_page = paginated.page(page)
|
|
||||||
if id_only:
|
|
||||||
items = [s.remote_id for s in activity_page.object_list]
|
|
||||||
else:
|
|
||||||
items = [s.to_activity() for s in activity_page.object_list]
|
|
||||||
|
|
||||||
prev_page = next_page = None
|
|
||||||
if activity_page.has_next():
|
|
||||||
next_page = '%s?page=%d' % (remote_id, activity_page.next_page_number())
|
|
||||||
if activity_page.has_previous():
|
|
||||||
prev_page = '%s?page=%d' % \
|
|
||||||
(remote_id, activity_page.previous_page_number())
|
|
||||||
return activitypub.OrderedCollectionPage(
|
|
||||||
id='%s?page=%s' % (remote_id, page),
|
|
||||||
partOf=remote_id,
|
|
||||||
orderedItems=items,
|
|
||||||
next=next_page,
|
|
||||||
prev=prev_page
|
|
||||||
).serialize()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderedCollectionMixin(OrderedCollectionPageMixin):
|
|
||||||
''' extends activitypub models to work as ordered collections '''
|
|
||||||
@property
|
|
||||||
def collection_queryset(self):
|
|
||||||
''' usually an ordered collection model aggregates a different model '''
|
|
||||||
raise NotImplementedError('Model must define collection_queryset')
|
|
||||||
|
|
||||||
activity_serializer = activitypub.OrderedCollection
|
|
||||||
|
|
||||||
def to_activity(self, **kwargs):
|
|
||||||
''' an ordered collection of the specified model queryset '''
|
|
||||||
return self.to_ordered_collection(self.collection_queryset, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_activity(obj):
|
|
||||||
''' go through the fields on an object '''
|
|
||||||
activity = {}
|
|
||||||
for field in obj.activity_fields:
|
|
||||||
field.set_activity_from_field(activity, obj)
|
|
||||||
|
|
||||||
if hasattr(obj, 'serialize_reverse_fields'):
|
|
||||||
# for example, editions of a work
|
|
||||||
for model_field_name, activity_field_name, sort_field in \
|
|
||||||
obj.serialize_reverse_fields:
|
|
||||||
related_field = getattr(obj, model_field_name)
|
|
||||||
activity[activity_field_name] = \
|
|
||||||
unfurl_related_field(related_field, sort_field)
|
|
||||||
|
|
||||||
if not activity.get('id'):
|
|
||||||
activity['id'] = obj.get_remote_id()
|
|
||||||
return activity
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ from model_utils.managers import InheritanceManager
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
|
|
||||||
|
from .activitypub_mixin import ActivitypubMixin, OrderedCollectionPageMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
class BookDataModel(ActivitypubMixin, BookWyrmModel):
|
class BookDataModel(ActivitypubMixin, BookWyrmModel):
|
||||||
|
@ -74,6 +74,7 @@ class Book(BookDataModel):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_readthrough(self):
|
def latest_readthrough(self):
|
||||||
|
''' most recent readthrough activity '''
|
||||||
return self.readthrough_set.order_by('-updated_date').first()
|
return self.readthrough_set.order_by('-updated_date').first()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -3,7 +3,8 @@ from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
from .activitypub_mixin import ActivitypubMixin
|
||||||
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
class Favorite(ActivitypubMixin, BookWyrmModel):
|
class Favorite(ActivitypubMixin, BookWyrmModel):
|
||||||
|
|
|
@ -3,8 +3,8 @@ from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
from .activitypub_mixin import ActivitypubMixin, OrderedCollectionMixin
|
||||||
from .base_model import OrderedCollectionMixin
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
from .activitypub_mixin import ActivitypubMixin
|
||||||
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ import re
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
from .activitypub_mixin import ActivitypubMixin, OrderedCollectionMixin
|
||||||
from .base_model import OrderedCollectionMixin
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ from django.utils import timezone
|
||||||
from model_utils.managers import InheritanceManager
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from .base_model import ActivitypubMixin, OrderedCollectionPageMixin
|
from .activitypub_mixin import ActivitypubMixin, OrderedCollectionPageMixin
|
||||||
from .base_model import BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
from .fields import image_serializer
|
from .fields import image_serializer
|
||||||
|
|
|
@ -5,7 +5,8 @@ from django.db import models
|
||||||
|
|
||||||
from bookwyrm import activitypub
|
from bookwyrm import activitypub
|
||||||
from bookwyrm.settings import DOMAIN
|
from bookwyrm.settings import DOMAIN
|
||||||
from .base_model import OrderedCollectionMixin, BookWyrmModel
|
from .activitypub_mixin import OrderedCollectionMixin
|
||||||
|
from .base_model import BookWyrmModel
|
||||||
from . import fields
|
from . import fields
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ from bookwyrm.settings import DOMAIN
|
||||||
from bookwyrm.signatures import create_key_pair
|
from bookwyrm.signatures import create_key_pair
|
||||||
from bookwyrm.tasks import app
|
from bookwyrm.tasks import app
|
||||||
from bookwyrm.utils import regex
|
from bookwyrm.utils import regex
|
||||||
from .base_model import OrderedCollectionPageMixin
|
from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin
|
||||||
from .base_model import ActivitypubMixin, BookWyrmModel
|
from .base_model import BookWyrmModel
|
||||||
from .federated_server import FederatedServer
|
from .federated_server import FederatedServer
|
||||||
from . import fields, Review
|
from . import fields, Review
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue