From 73e41d568ed9a5157836908c22b2b9ddc014cc6a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 20 Nov 2020 08:14:16 -0800 Subject: [PATCH 1/3] Serialize model user and book tags to activitypub --- bookwyrm/incoming.py | 1 - bookwyrm/models/base_model.py | 9 ++++++++- bookwyrm/models/status.py | 38 +++++++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index c2c223bd7..0e7c18567 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -253,7 +253,6 @@ def handle_delete_status(activity): status_builder.delete_status(status) - @app.task def handle_favorite(activity): ''' approval of your good good post ''' diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 8150d650c..c44b8c35d 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -72,7 +72,14 @@ class ActivitypubMixin: value = value.remote_id if isinstance(value, datetime): value = value.isoformat() - fields[mapping.activity_key] = mapping.activity_formatter(value) + result = mapping.activity_formatter(value) + if mapping.activity_key in fields and \ + isinstance(fields[mapping.activity_key], list): + # there are two database fields that map to the same AP list + # this happens in status, which combines user and book tags + fields[mapping.activity_key] += result + else: + fields[mapping.activity_key] = result if pure: return self.pure_activity_serializer( diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 09ceda856..53bada1dc 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -9,6 +9,38 @@ from .base_model import ActivitypubMixin, OrderedCollectionPageMixin from .base_model import ActivityMapping, BookWyrmModel, PrivacyLevels +# --- Formatters ----- # +def ap_book_tags(mention_books): + ''' convert books into tags field ''' + tags = [] + for book in mention_books.all(): + tags.append(activitypub.Link( + href=book.remote_id, + name=book.title, + type='Book' + )) + return tags + +def ap_user_tags(mention_users): + ''' convert users into tag fields ''' + tags = [] + for user in mention_users.all(): + tags.append(activitypub.Mention( + href=user.remote_id, + name=user.username, + )) + return tags + + +def model_book_tags(activity_tags): + ''' grab the tagged books out of the activity ''' + pass + +def model_user_tags(): + ''' create user mentions ''' + pass + + class Status(OrderedCollectionPageMixin, BookWyrmModel): ''' any post, like a reply to a review, etc ''' user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -60,7 +92,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): @property def ap_tag(self): ''' references to books and/or users ''' - tags = [] for book in self.mention_books.all(): tags.append(activitypub.Link( @@ -75,6 +106,7 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): )) return tags + @property def ap_status_image(self): ''' attach a book cover, if relevent ''' @@ -94,7 +126,9 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ActivityMapping('to', 'ap_to'), ActivityMapping('cc', 'ap_cc'), ActivityMapping('replies', 'ap_replies'), - ActivityMapping('tag', 'ap_tag'), + ActivityMapping('tag', 'mention_books', ap_book_tags, model_book_tags), + # since one activitypub field populates two model fields, we do this + ActivityMapping('tag', 'mention_users', ap_user_tags, model_user_tags), ] # serializing to bookwyrm expanded activitypub From 72b4c150f6719ddc400fe87e0a32abe5d5e31ccf Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 20 Nov 2020 09:28:54 -0800 Subject: [PATCH 2/3] (De)serializers for tag fields --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/base_activity.py | 20 +++++++++ bookwyrm/models/base_model.py | 12 +++++ bookwyrm/models/status.py | 64 +++++---------------------- 4 files changed, 44 insertions(+), 54 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 931af0a0c..9a32bfc96 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -4,7 +4,7 @@ import sys from .base_activity import ActivityEncoder, Image, PublicKey, Signature from .base_activity import Link, Mention -from .base_activity import ActivitySerializerError +from .base_activity import ActivitySerializerError, tag_formatter from .note import Note, GeneratedNote, Article, Comment, Review, Quotation from .note import Tombstone from .interaction import Boost, Like diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 6ae7883ef..01f7665ec 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -145,3 +145,23 @@ def resolve_foreign_key(model, remote_id): 'Could not resolve remote_id in %s model: %s' % \ (model.__name__, remote_id)) return result + + +def tag_formatter(tags): + ''' helper function to extract foreign keys from tag activity json ''' + items = [] + types = { + 'Book': models.Book, + 'Mention': models.User, + } + for tag in tags: + tag_type = tag.get('type') + if not tag_type in types: + continue + remote_id = tag.get('href') + try: + item = resolve_foreign_key(types[tag_type], remote_id) + except ActivitySerializerError: + continue + items.append(item) + return items diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index c44b8c35d..df6b4f755 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -249,3 +249,15 @@ 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 diff --git a/bookwyrm/models/status.py b/bookwyrm/models/status.py index 53bada1dc..1ed98515a 100644 --- a/bookwyrm/models/status.py +++ b/bookwyrm/models/status.py @@ -7,38 +7,7 @@ from model_utils.managers import InheritanceManager from bookwyrm import activitypub from .base_model import ActivitypubMixin, OrderedCollectionPageMixin from .base_model import ActivityMapping, BookWyrmModel, PrivacyLevels - - -# --- Formatters ----- # -def ap_book_tags(mention_books): - ''' convert books into tags field ''' - tags = [] - for book in mention_books.all(): - tags.append(activitypub.Link( - href=book.remote_id, - name=book.title, - type='Book' - )) - return tags - -def ap_user_tags(mention_users): - ''' convert users into tag fields ''' - tags = [] - for user in mention_users.all(): - tags.append(activitypub.Mention( - href=user.remote_id, - name=user.username, - )) - return tags - - -def model_book_tags(activity_tags): - ''' grab the tagged books out of the activity ''' - pass - -def model_user_tags(): - ''' create user mentions ''' - pass +from .base_model import tag_formatter class Status(OrderedCollectionPageMixin, BookWyrmModel): @@ -89,24 +58,6 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ''' structured replies block ''' return self.to_replies() - @property - def ap_tag(self): - ''' references to books and/or users ''' - tags = [] - for book in self.mention_books.all(): - tags.append(activitypub.Link( - href=book.remote_id, - name=book.title, - type='Book' - )) - for user in self.mention_users.all(): - tags.append(activitypub.Mention( - href=user.remote_id, - name=user.username, - )) - return tags - - @property def ap_status_image(self): ''' attach a book cover, if relevent ''' @@ -126,9 +77,16 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel): ActivityMapping('to', 'ap_to'), ActivityMapping('cc', 'ap_cc'), ActivityMapping('replies', 'ap_replies'), - ActivityMapping('tag', 'mention_books', ap_book_tags, model_book_tags), - # since one activitypub field populates two model fields, we do this - ActivityMapping('tag', 'mention_users', ap_user_tags, model_user_tags), + ActivityMapping( + 'tag', 'mention_books', + lambda x: tag_formatter(x, 'title', 'Book'), + activitypub.tag_formatter + ), + ActivityMapping( + 'tag', 'mention_users', + lambda x: tag_formatter(x, 'username', 'Mention'), + activitypub.tag_formatter + ), ] # serializing to bookwyrm expanded activitypub From 9a55dba3150fe6f68b1b8c6ace375cefff5ad03f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 20 Nov 2020 09:59:55 -0800 Subject: [PATCH 3/3] Special handling for setting many to many fields in serializer --- bookwyrm/activitypub/base_activity.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 01f7665ec..18e5ccbe4 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -5,7 +5,7 @@ from json import JSONEncoder from bookwyrm import books_manager, models from django.db.models.fields.related_descriptors \ - import ForwardManyToOneDescriptor + import ForwardManyToOneDescriptor, ManyToManyDescriptor class ActivitySerializerError(ValueError): @@ -90,6 +90,7 @@ class ActivityObject: model_fields = [m.name for m in model._meta.get_fields()] mapped_fields = {} + many_to_many_fields = {} for mapping in model.activity_mappings: if mapping.model_key not in model_fields: @@ -106,7 +107,11 @@ class ActivityObject: fk_model = model_field.field.related_model value = resolve_foreign_key(fk_model, value) - mapped_fields[mapping.model_key] = mapping.model_formatter(value) + formatted_value = mapping.model_formatter(value) + if isinstance(model_field, ManyToManyDescriptor): + many_to_many_fields[mapping.model_key] = formatted_value + else: + mapped_fields[mapping.model_key] = formatted_value # updating an existing model isntance @@ -114,10 +119,14 @@ class ActivityObject: for k, v in mapped_fields.items(): setattr(instance, k, v) instance.save() - return instance + else: + # creating a new model instance + instance = model.objects.create(**mapped_fields) - # creating a new model instance - return model.objects.create(**mapped_fields) + for (model_key, values) in many_to_many_fields.items(): + getattr(instance, model_key).set(values) + instance.save() + return instance def serialize(self): @@ -129,7 +138,7 @@ class ActivityObject: def resolve_foreign_key(model, remote_id): ''' look up the remote_id on an activity json field ''' - if model in [models.Edition, models.Work]: + if model in [models.Edition, models.Work, models.Book]: return books_manager.get_or_create_book(remote_id) result = model.objects