diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index 606705c8..3dcdc2f0 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -81,7 +81,7 @@ def handle_imported_book(user, item, include_reviews, privacy): return existing_shelf = models.ShelfBook.objects.filter( - book=item.book, added_by=user).exists() + book=item.book, user=user).exists() # shelve the book if it hasn't been shelved already if item.shelf and not existing_shelf: @@ -90,7 +90,7 @@ def handle_imported_book(user, item, include_reviews, privacy): user=user ) models.ShelfBook.objects.create( - book=item.book, shelf=desired_shelf, added_by=user) + book=item.book, shelf=desired_shelf, user=user) for read in item.reads: # check for an existing readthrough with the same dates diff --git a/bookwyrm/migrations/0043_auto_20210204_2223.py b/bookwyrm/migrations/0043_auto_20210204_2223.py new file mode 100644 index 00000000..b9c328ea --- /dev/null +++ b/bookwyrm/migrations/0043_auto_20210204_2223.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2021-02-04 22:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0042_auto_20210201_2108'), + ] + + operations = [ + migrations.RenameField( + model_name='listitem', + old_name='added_by', + new_name='user', + ), + migrations.RenameField( + model_name='shelfbook', + old_name='added_by', + new_name='user', + ), + ] diff --git a/bookwyrm/models/activitypub_mixin/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py similarity index 52% rename from bookwyrm/models/activitypub_mixin/activitypub_mixin.py rename to bookwyrm/models/activitypub_mixin.py index eef5ce2b..eee916bb 100644 --- a/bookwyrm/models/activitypub_mixin/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -2,20 +2,25 @@ from functools import reduce import json import operator +from base64 import b64encode +from uuid import uuid4 import requests +from Crypto.PublicKey import RSA +from Crypto.Signature import pkcs1_15 +from Crypto.Hash import SHA256 from django.apps import apps +from django.core.paginator import Paginator from django.db import models from django.db.models import Q from django.dispatch import receiver from django.utils.http import http_date - from bookwyrm import activitypub -from bookwyrm.settings import USER_AGENT +from bookwyrm.settings import USER_AGENT, PAGE_LENGTH from bookwyrm.signatures import make_signature, make_digest from bookwyrm.tasks import app -from .fields import ImageField, ManyToManyField +from bookwyrm.models.fields import ImageField, ManyToManyField class ActivitypubMixin: @@ -247,3 +252,218 @@ def execute_after_save(sender, instance, created, *args, **kwargs): if activity and user and user.local: instance.broadcast(activity, user) + + +class ObjectMixin(ActivitypubMixin): + ''' add this mixin for object models that are AP serializable ''' + + def save(self, *args, **kwargs): + ''' broadcast updated ''' + # first off, we want to save normally no matter what + super().save(*args, **kwargs) + + # we only want to handle updates, not newly created objects + if not self.id: + return + + # this will work for lists, shelves + user = self.user if hasattr(self, 'user') else None + if not user: + # users don't have associated users, they ARE users + user_model = apps.get_model('bookwyrm.User', require_ready=True) + if isinstance(self, user_model): + user = self + # book data tracks last editor + elif hasattr(self, 'last_edited_by'): + user = self.last_edited_by + # again, if we don't know the user or they're remote, don't bother + if not user or not user.local: + return + + # is this a deletion? + if self.deleted: + activity = self.to_delete_activity(user) + else: + activity = self.to_update_activity(user) + self.broadcast(activity, user) + + + 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() + + +class OrderedCollectionPageMixin(ObjectMixin): + ''' 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) + + +class CollectionItemMixin(ActivitypubMixin): + ''' for items that are part of an (Ordered)Collection ''' + activity_serializer = activitypub.Add + object_field = collection_field = None + + def to_add_activity(self): + ''' AP for shelving a book''' + object_field = getattr(self, self.object_field) + collection_field = getattr(self, self.collection_field) + return activitypub.Add( + id='%s#add' % self.remote_id, + actor=self.user.remote_id, + object=object_field.to_activity(), + target=collection_field.remote_id + ).serialize() + + def to_remove_activity(self): + ''' AP for un-shelving a book''' + object_field = getattr(self, self.object_field) + collection_field = getattr(self, self.collection_field) + return activitypub.Remove( + id='%s#remove' % self.remote_id, + actor=self.user.remote_id, + object=object_field.to_activity(), + target=collection_field.remote_id + ).serialize() + + +class ActivitybMixin(ActivitypubMixin): + ''' add this mixin for models that are AP serializable ''' + + def save(self, *args, **kwargs): + ''' broadcast activity ''' + super().save(*args, **kwargs) + self.broadcast(self.to_activity(), self.user) + + def delete(self, *args, **kwargs): + ''' nevermind, undo that activity ''' + self.broadcast(self.to_undo_activity(), self.user) + super().delete(*args, **kwargs) + + + def to_undo_activity(self): + ''' undo an action ''' + return activitypub.Undo( + id='%s#undo' % self.remote_id, + actor=self.user.remote_id, + object=self.to_activity() + ).serialize() diff --git a/bookwyrm/models/activitypub_mixin/__init__.py b/bookwyrm/models/activitypub_mixin/__init__.py deleted file mode 100644 index b6e690fd..00000000 --- a/bookwyrm/models/activitypub_mixin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import * diff --git a/bookwyrm/models/activitypub_mixin/activity_mixin.py b/bookwyrm/models/activitypub_mixin/activity_mixin.py deleted file mode 100644 index fc74fe17..00000000 --- a/bookwyrm/models/activitypub_mixin/activity_mixin.py +++ /dev/null @@ -1,25 +0,0 @@ -''' activitypub model functionality ''' -from bookwyrm import activitypub -from . import ActivitypubMixin - -class ActivitybMixin(ActivitypubMixin): - ''' add this mixin for models that are AP serializable ''' - - def save(self, *args, **kwargs): - ''' broadcast activity ''' - super().save(*args, **kwargs) - self.broadcast(self.to_activity(), self.user) - - def delete(self, *args, **kwargs): - ''' nevermind, undo that activity ''' - self.broadcast(self.to_undo_activity(), self.user) - super().delete(*args, **kwargs) - - - def to_undo_activity(self): - ''' undo an action ''' - return activitypub.Undo( - id='%s#undo' % self.remote_id, - actor=self.user.remote_id, - object=self.to_activity() - ).serialize() diff --git a/bookwyrm/models/activitypub_mixin/object_mixin.py b/bookwyrm/models/activitypub_mixin/object_mixin.py deleted file mode 100644 index 2eddf813..00000000 --- a/bookwyrm/models/activitypub_mixin/object_mixin.py +++ /dev/null @@ -1,94 +0,0 @@ -''' activitypub objects like Person and Book''' -from base64 import b64encode -from uuid import uuid4 - -from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 -from Crypto.Hash import SHA256 -from django.apps import apps - -from bookwyrm import activitypub -from . import ActivitypubMixin - - -class ObjectMixin(ActivitypubMixin): - ''' add this mixin for object models that are AP serializable ''' - - def save(self, *args, **kwargs): - ''' broadcast updated ''' - # first off, we want to save normally no matter what - super().save(*args, **kwargs) - - # we only want to handle updates, not newly created objects - if not self.id: - return - - # this will work for lists, shelves - user = self.user if hasattr(self, 'user') else None - if not user: - # users don't have associated users, they ARE users - user_model = apps.get_model('bookwyrm.User', require_ready=True) - if isinstance(self, user_model): - user = self - # book data tracks last editor - elif hasattr(self, 'last_edited_by'): - user = self.last_edited_by - # again, if we don't know the user or they're remote, don't bother - if not user or not user.local: - return - - # is this a deletion? - if self.deleted: - activity = self.to_delete_activity(user) - else: - activity = self.to_update_activity(user) - self.broadcast(activity, user) - - - 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() diff --git a/bookwyrm/models/activitypub_mixin/ordered_collection.py b/bookwyrm/models/activitypub_mixin/ordered_collection.py deleted file mode 100644 index 954e0364..00000000 --- a/bookwyrm/models/activitypub_mixin/ordered_collection.py +++ /dev/null @@ -1,115 +0,0 @@ -''' lists of objects ''' -from django.core.paginator import Paginator - -from bookwyrm import activitypub -from bookwyrm.settings import PAGE_LENGTH -from . import ActivitypubMixin, ObjectMixin, generate_activity - - -class OrderedCollectionPageMixin(ObjectMixin): - ''' 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) - - -class CollectionItemMixin(ActivitypubMixin): - ''' for items that are part of an (Ordered)Collection ''' - activity_serializer = activitypub.Add - object_field = collection_field = None - - def to_add_activity(self): - ''' AP for shelving a book''' - object_field = getattr(self, self.object_field) - collection_field = getattr(self, self.collection_field) - return activitypub.Add( - id='%s#add' % self.remote_id, - actor=self.user.remote_id, - object=object_field.to_activity(), - target=collection_field.remote_id - ).serialize() - - def to_remove_activity(self): - ''' AP for un-shelving a book''' - object_field = getattr(self, self.object_field) - collection_field = getattr(self, self.collection_field) - return activitypub.Remove( - id='%s#remove' % self.remote_id, - actor=self.user.remote_id, - object=object_field.to_activity(), - target=collection_field.remote_id - ).serialize() diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index ef1dd96d..f1f20830 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -7,7 +7,7 @@ from model_utils.managers import InheritanceManager from bookwyrm import activitypub from bookwyrm.settings import DOMAIN -from .activitypub_mixin import ObjectMixin, OrderedCollectionPageMixin +from .activitypub_mixin import OrderedCollectionPageMixin, ObjectMixin from .base_model import BookWyrmModel from . import fields diff --git a/bookwyrm/templates/lists/curate.html b/bookwyrm/templates/lists/curate.html index 20c8175d..a7e0fe79 100644 --- a/bookwyrm/templates/lists/curate.html +++ b/bookwyrm/templates/lists/curate.html @@ -23,7 +23,7 @@ {% include 'snippets/book_titleby.html' with book=item.book %} - {% include 'snippets/username.html' with user=item.added_by %} + {% include 'snippets/username.html' with user=item.user %}
diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 213f9dca..7899d593 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -31,9 +31,9 @@