diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 0a34d067..4de4dc6b 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -92,44 +92,24 @@ class ActivityObject: if value is None: continue - # value is None if there's a default that isn't supplied - # in the activity but is supplied in the formatter - value = getattr(self, activitypub_field) model_field = getattr(model, field.name) - formatted_value = field.field_from_activity(value) if isinstance(model_field, ForwardManyToOneDescriptor): - if not formatted_value: - continue - # foreign key remote id reolver (work on Edition, for example) - fk_model = model_field.field.related_model - if isinstance(formatted_value, dict) and \ - formatted_value.get('id'): - # if the AP field is a serialized object (as in Add) - # or KeyPair (even though it's OneToOne) - related_model = field.related_model - related_activity = related_model.activity_serializer - mapped_fields[field.name] = related_activity( - **formatted_value - ).to_model(related_model) - else: - # if the field is just a remote_id (as in every other case) - remote_id = formatted_value - reference = resolve_remote_id(fk_model, remote_id) - mapped_fields[field.name] = reference - elif isinstance(model_field, ManyToManyDescriptor): + mapped_fields[field.name] = value + if isinstance(model_field, ManyToManyDescriptor): # status mentions book/users - many_to_many_fields[field.name] = formatted_value + many_to_many_fields[field.name] = value elif isinstance(model_field, ReverseManyToOneDescriptor): # attachments on Status, for example - one_to_many_fields[field.name] = formatted_value + one_to_many_fields[field.name] = value elif isinstance(model_field, ImageFileDescriptor): # image fields need custom handling - image_fields[field.name] = formatted_value + print(model_field, field.name, value) + image_fields[field.name] = value else: - if formatted_value == MISSING: - formatted_value = None - mapped_fields[field.name] = formatted_value + if value == MISSING: + value = None + mapped_fields[field.name] = value if instance: @@ -146,33 +126,14 @@ class ActivityObject: # add images for (model_key, value) in image_fields.items(): - if not formatted_value: + if not value: continue - getattr(instance, model_key).save(*formatted_value, save=True) + getattr(instance, model_key).save(*value, save=True) # add many to many fields for (model_key, values) in many_to_many_fields.items(): # mention books, mention users, followers - if values == MISSING or not isinstance(values, list): - # user followers is a link to an orderedcollection, skip it - continue - model_field = getattr(instance, model_key) - model = model_field.model - items = [] - for link in values: - if isinstance(link, dict): - # check that the Type matches the model (Status - # tags contain both user mentions and book tags) - if not model.activity_serializer.type == \ - link.get('type'): - continue - remote_id = link.get('href') - else: - remote_id = link - items.append( - resolve_remote_id(model, remote_id) - ) - getattr(instance, model_key).set(items) + getattr(instance, model_key).set(values) # add one to many fields for (model_key, values) in one_to_many_fields.items(): diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index b5e321ee..e1636f84 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -57,6 +57,27 @@ class ActivitypubFieldMixin: return components[0] + ''.join(x.title() for x in components[1:]) +class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): + ''' default (de)serialization for foreign key and one to one ''' + def field_from_activity(self, value): + if not value: + return None + + related_model = self.related_model + if isinstance(value, dict) and value.get('id'): + # this is an activitypub object, which we can deserialize + activity_serializer = related_model.activity_serializer + return activity_serializer(**value).to_model(related_model) + try: + # make sure the value looks like a remote id + validate_remote_id(value) + except ValidationError: + # we don't know what this is, ignore it + return None + # gets or creates the model field from the remote id + return activitypub.resolve_remote_id(related_model, value) + + class RemoteIdField(ActivitypubFieldMixin, models.CharField): ''' a url that serves as a unique identifier ''' def __init__(self, *args, max_length=255, validators=None, **kwargs): @@ -95,7 +116,7 @@ class UsernameField(ActivitypubFieldMixin, models.CharField): return value.split('@')[0] -class ForeignKey(ActivitypubFieldMixin, models.ForeignKey): +class ForeignKey(ActivitypubRelatedFieldMixin, models.ForeignKey): ''' activitypub-aware foreign key field ''' def field_to_activity(self, value): if not value: @@ -103,7 +124,7 @@ class ForeignKey(ActivitypubFieldMixin, models.ForeignKey): return value.remote_id -class OneToOneField(ActivitypubFieldMixin, models.OneToOneField): +class OneToOneField(ActivitypubRelatedFieldMixin, models.OneToOneField): ''' activitypub-aware foreign key field ''' def field_to_activity(self, value): if not value: @@ -122,6 +143,15 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): return '%s/%s' % (value.instance.remote_id, self.name) return [i.remote_id for i in value.all()] + def field_from_activity(self, value): + items = [] + for remote_id in value: + validate_remote_id(remote_id) + items.append( + activitypub.resolve_remote_id(self.related_model, remote_id) + ) + return items + class TagField(ManyToManyField): ''' special case of many to many that uses Tags ''' @@ -142,6 +172,15 @@ class TagField(ManyToManyField): )) return tags + def field_from_activity(self, value): + items = [] + for link_json in value: + link = activitypub.Link(**link_json) + items.append( + activitypub.resolve_remote_id(self.related_model, link.href) + ) + return items + def image_serializer(value): ''' helper for serializing images ''' diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index b157aab3..06ff01e0 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -14,7 +14,7 @@ from django.test import TestCase from django.utils import timezone from bookwyrm.models import fields, User -from bookwyrm.settings import DOMAIN +from bookwyrm import activitypub class ActivitypubFields(TestCase): def test_validate_remote_id(self): @@ -86,8 +86,23 @@ class ActivitypubFields(TestCase): instance = fields.ForeignKey('User', on_delete=models.CASCADE) Serializable = namedtuple('Serializable', ('to_activity', 'remote_id')) item = Serializable(lambda: {'a': 'b'}, 'https://e.b/c') + # returns the remote_id field of the related object self.assertEqual(instance.field_to_activity(item), 'https://e.b/c') + def test_foreign_key_from_activity(self): + instance = fields.ForeignKey(User, on_delete=models.CASCADE) + + # test receiving an unknown remote id and loading data TODO + + # test recieving activity json TODO + + # test receiving a remote id of an object in the db + user = User.objects.create_user( + 'mouse', 'mouse@mouse.mouse', 'mouseword', local=True) + value = instance.field_from_activity(user.remote_id) + self.assertEqual(value, user) + + def test_one_to_one_field(self): instance = fields.OneToOneField('User', on_delete=models.CASCADE) Serializable = namedtuple('Serializable', ('to_activity', 'remote_id'))