diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index d20e7e94..52b1b1f2 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -275,6 +275,8 @@ def resolve_remote_id( ): """take a remote_id and return an instance, creating if necessary""" if model: # a bonus check we can do if we already know the model + if isinstance(model, str): + model = apps.get_model(f"bookwyrm.{model}", require_ready=True) result = model.find_existing_by_remote_id(remote_id) if result and not refresh: return result if not get_activity else result.to_activity_dataclass() diff --git a/bookwyrm/migrations/0086_auto_20210828_1724.py b/bookwyrm/migrations/0086_auto_20210828_1724.py new file mode 100644 index 00000000..21247711 --- /dev/null +++ b/bookwyrm/migrations/0086_auto_20210828_1724.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.4 on 2021-08-28 17:24 + +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +from django.db.models import F, Value, CharField +from django.db.models.functions import Concat + + +def forwards_func(apps, schema_editor): + """generate followers url""" + db_alias = schema_editor.connection.alias + apps.get_model("bookwyrm", "User").objects.using(db_alias).annotate( + generated_url=Concat( + F("remote_id"), Value("/followers"), output_field=CharField() + ) + ).update(followers_url=models.F("generated_url")) + + +def reverse_func(apps, schema_editor): + """noop""" + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0085_user_saved_lists"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="followers_url", + field=bookwyrm.models.fields.CharField( + default="/followers", max_length=255 + ), + preserve_default=False, + ), + migrations.RunPython(forwards_func, reverse_func), + migrations.AlterField( + model_name="user", + name="followers", + field=models.ManyToManyField( + related_name="following", + through="bookwyrm.UserFollows", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 6ed5aa5e..cc5a7bb5 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -224,8 +224,20 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): original = getattr(instance, self.name) to = data.to cc = data.cc + + # we need to figure out who this is to get their followers link + for field in ["attributedTo", "owner", "actor"]: + if hasattr(data, field): + user_field = field + break + if not user_field: + raise ValidationError("No user field found for privacy", data) + user = activitypub.resolve_remote_id(getattr(data, user_field), model="User") + if to == [self.public]: setattr(instance, self.name, "public") + elif to == [user.followers_url]: + setattr(instance, self.name, "followers") elif cc == []: setattr(instance, self.name, "direct") elif self.public in cc: @@ -241,9 +253,7 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): mentions = [u.remote_id for u in instance.mention_users.all()] # this is a link to the followers list # pylint: disable=protected-access - followers = instance.user.__class__._meta.get_field( - "followers" - ).field_to_activity(instance.user.followers) + followers = instance.user.followers_url if instance.privacy == "public": activity["to"] = [self.public] activity["cc"] = [followers] + mentions diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 0ef23d3f..0745dffa 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -82,9 +82,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): preview_image = models.ImageField( upload_to="previews/avatars/", blank=True, null=True ) - followers = fields.ManyToManyField( + followers_url = fields.CharField(max_length=255, activitypub_field="followers") + followers = models.ManyToManyField( "self", - link_only=True, symmetrical=False, through="UserFollows", through_fields=("user_object", "user_subject"), @@ -228,7 +228,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): def to_followers_activity(self, **kwargs): """activitypub followers list""" - remote_id = "%s/followers" % self.remote_id + remote_id = self.followers_url return self.to_ordered_collection( self.followers.order_by("-updated_date").all(), remote_id=remote_id, @@ -275,10 +275,12 @@ class User(OrderedCollectionPageMixin, AbstractUser): return # populate fields for local users - self.remote_id = "%s/user/%s" % (site_link(), self.localname) - self.inbox = "%s/inbox" % self.remote_id - self.shared_inbox = "%s/inbox" % site_link() - self.outbox = "%s/outbox" % self.remote_id + link = site_link() + self.remote_id = f"{link}/user/{self.localname}" + self.followers_url = f"{self.remote_id}/followers" + self.inbox = f"{self.remote_id}/inbox" + self.shared_inbox = f"{link}/inbox" + self.outbox = f"{self.remote_id}/outbox" # an id needs to be set before we can proceed with related models super().save(*args, **kwargs) diff --git a/bookwyrm/tests/activitypub/test_person.py b/bookwyrm/tests/activitypub/test_person.py index 67aaf891..2722aaef 100644 --- a/bookwyrm/tests/activitypub/test_person.py +++ b/bookwyrm/tests/activitypub/test_person.py @@ -25,3 +25,4 @@ class Person(TestCase): self.assertEqual(user.username, "mouse@example.com") self.assertEqual(user.remote_id, "https://example.com/user/mouse") self.assertFalse(user.local) + self.assertEqual(user.followers_url, "https://example.com/user/mouse/followers") diff --git a/bookwyrm/tests/models/test_fields.py b/bookwyrm/tests/models/test_fields.py index 2520a2fd..6730d37b 100644 --- a/bookwyrm/tests/models/test_fields.py +++ b/bookwyrm/tests/models/test_fields.py @@ -146,6 +146,15 @@ class ModelFields(TestCase): def test_privacy_field_set_field_from_activity(self, _): """translate between to/cc fields and privacy""" + with patch("bookwyrm.models.user.set_remote_server.delay"): + test_user = User.objects.create_user( + username="test_user@example.com", + local=False, + remote_id="https://example.com/test_user", + inbox="https://example.com/users/test_user/inbox", + followers_url="https://example.com/users/test_user/followers", + ) + @dataclass(init=False) class TestActivity(ActivityObject): """real simple mock""" @@ -154,6 +163,7 @@ class ModelFields(TestCase): cc: List[str] id: str = "http://hi.com" type: str = "Test" + attributedTo: str = test_user.remote_id class TestPrivacyModel(ActivitypubMixin, BookWyrmModel): """real simple mock model because BookWyrmModel is abstract""" @@ -185,6 +195,16 @@ class ModelFields(TestCase): instance.set_field_from_activity(model_instance, data) self.assertEqual(model_instance.privacy_field, "unlisted") + data.to = [test_user.followers_url] + data.cc = [] + instance.set_field_from_activity(model_instance, data) + self.assertEqual(model_instance.privacy_field, "followers") + + data.to = ["http://user_remote/followers"] + data.cc = ["http://mentioned_user/remote_id"] + instance.set_field_from_activity(model_instance, data) + self.assertEqual(model_instance.privacy_field, "followers") + @patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast") @patch("bookwyrm.activitystreams.ActivityStream.add_status") def test_privacy_field_set_activity_from_field(self, *_): diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index 177b2ad6..230c503b 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -5,11 +5,13 @@ from django.test import TestCase import responses from bookwyrm import models -from bookwyrm.settings import DOMAIN +from bookwyrm.settings import USE_HTTPS, DOMAIN # pylint: disable=missing-class-docstring # pylint: disable=missing-function-docstring class User(TestCase): + protocol = "https://" if USE_HTTPS else "http://" + def setUp(self): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"): self.user = models.User.objects.create_user( @@ -24,13 +26,14 @@ class User(TestCase): def test_computed_fields(self): """username instead of id here""" - expected_id = "https://%s/user/mouse" % DOMAIN + expected_id = f"{self.protocol}{DOMAIN}/user/mouse" self.assertEqual(self.user.remote_id, expected_id) - self.assertEqual(self.user.username, "mouse@%s" % DOMAIN) + self.assertEqual(self.user.username, f"mouse@{DOMAIN}") self.assertEqual(self.user.localname, "mouse") - self.assertEqual(self.user.shared_inbox, "https://%s/inbox" % DOMAIN) - self.assertEqual(self.user.inbox, "%s/inbox" % expected_id) - self.assertEqual(self.user.outbox, "%s/outbox" % expected_id) + self.assertEqual(self.user.shared_inbox, f"{self.protocol}{DOMAIN}/inbox") + self.assertEqual(self.user.inbox, f"{expected_id}/inbox") + self.assertEqual(self.user.outbox, f"{expected_id}/outbox") + self.assertEqual(self.user.followers_url, f"{expected_id}/followers") self.assertIsNotNone(self.user.key_pair.private_key) self.assertIsNotNone(self.user.key_pair.public_key)