From 779d2b06941931ccfbc749c3d3c4d46451f18f3c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 20 Feb 2023 10:32:03 -0800 Subject: [PATCH 01/47] Attempt to complete inbox requests synchronously When an inbox activity comes in from another fediverse instance, the behavior prior to this commit was always to immediately give a 200 response to the external server and then create a celery activity (usually in the MEDIUM_PRIORITY queue) to complete it. Instead, this would receive a request and try to complete it without making any http requests (which would make the request take too long to process). If an external request is required to complete the activity, a task is created and added to the queue. Ideally, this will cause some tasks to happen very promptly, and reduce the load on celery, which would help queued tasks happen more quickly as well. One downside is that this will make completing http requests from external servers slowing (since it's doing a bunch of thinking before responding). --- bookwyrm/activitypub/base_activity.py | 62 ++++++++++++++++++++++--- bookwyrm/activitypub/verbs.py | 65 +++++++++++++++++--------- bookwyrm/models/fields.py | 66 +++++++++++++++++++-------- bookwyrm/views/inbox.py | 15 +++++- 4 files changed, 160 insertions(+), 48 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 02e5395fc..6751f9c8e 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -100,9 +100,27 @@ class ActivityObject: # pylint: disable=too-many-locals,too-many-branches,too-many-arguments def to_model( - self, model=None, instance=None, allow_create=True, save=True, overwrite=True + self, + model=None, + instance=None, + allow_create=True, + save=True, + overwrite=True, + allow_external_connections=True, ): - """convert from an activity to a model instance""" + """convert from an activity to a model instance. Args: + model: the django model that this object is being converted to + (will guess if not known) + instance: an existing database entry that is going to be updated by + this activity + allow_create: whether a new object should be created if there is no + existing object is provided or found matching the remote_id + save: store in the database if true, return an unsaved model obj if false + overwrite: replace fields in the database with this activity if true, + only update blank fields if false + allow_external_connections: look up missing data if true, + throw an exception if false and an external connection is needed + """ model = model or get_model_from_type(self.type) # only reject statuses if we're potentially creating them @@ -127,7 +145,10 @@ class ActivityObject: for field in instance.simple_fields: try: changed = field.set_field_from_activity( - instance, self, overwrite=overwrite + instance, + self, + overwrite=overwrite, + allow_external_connections=allow_external_connections, ) if changed: update_fields.append(field.name) @@ -138,7 +159,11 @@ class ActivityObject: # too early and jank up users for field in instance.image_fields: changed = field.set_field_from_activity( - instance, self, save=save, overwrite=overwrite + instance, + self, + save=save, + overwrite=overwrite, + allow_external_connections=allow_external_connections, ) if changed: update_fields.append(field.name) @@ -162,7 +187,11 @@ class ActivityObject: # add many to many fields, which have to be set post-save for field in instance.many_to_many_fields: # mention books/users, for example - field.set_field_from_activity(instance, self) + field.set_field_from_activity( + instance, + self, + allow_external_connections=allow_external_connections, + ) # reversed relationships in the models for ( @@ -266,10 +295,22 @@ def get_model_from_type(activity_type): return model[0] +# pylint: disable=too-many-arguments def resolve_remote_id( - remote_id, model=None, refresh=False, save=True, get_activity=False + remote_id, + model=None, + refresh=False, + save=True, + get_activity=False, + allow_external_connections=True, ): - """take a remote_id and return an instance, creating if necessary""" + """take a remote_id and return an instance, creating if necessary. Args: + remote_id: the unique url for looking up the object in the db or by http + model: a string or object representing the model that corresponds to the object + save: whether to return an unsaved database entry or a saved one + get_activity: whether to return the activitypub object or the model object + allow_external_connections: whether to make http connections + """ 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) @@ -277,6 +318,13 @@ def resolve_remote_id( if result and not refresh: return result if not get_activity else result.to_activity_dataclass() + # The above block will return the object if it already exists in the database. + # If it doesn't, an external connection would be needed, so check if that's cool + if not allow_external_connections: + raise ActivitySerializerError( + "Unable to serialize object without making external HTTP requests" + ) + # load the data and create the object try: data = get_data(remote_id) diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index b8c0ae779..4b7514b5a 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -14,12 +14,12 @@ class Verb(ActivityObject): actor: str object: ActivityObject - def action(self): + def action(self, allow_external_connections=True): """usually we just want to update and save""" # self.object may return None if the object is invalid in an expected way # ie, Question type if self.object: - self.object.to_model() + self.object.to_model(allow_external_connections=allow_external_connections) # pylint: disable=invalid-name @@ -42,7 +42,7 @@ class Delete(Verb): cc: List[str] = field(default_factory=lambda: []) type: str = "Delete" - def action(self): + def action(self, allow_external_connections=True): """find and delete the activity object""" if not self.object: return @@ -52,7 +52,11 @@ class Delete(Verb): model = apps.get_model("bookwyrm.User") obj = model.find_existing_by_remote_id(self.object) else: - obj = self.object.to_model(save=False, allow_create=False) + obj = self.object.to_model( + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) if obj: obj.delete() @@ -67,11 +71,13 @@ class Update(Verb): to: List[str] type: str = "Update" - def action(self): + def action(self, allow_external_connections=True): """update a model instance from the dataclass""" if not self.object: return - self.object.to_model(allow_create=False) + self.object.to_model( + allow_create=False, allow_external_connections=allow_external_connections + ) @dataclass(init=False) @@ -80,7 +86,7 @@ class Undo(Verb): type: str = "Undo" - def action(self): + def action(self, allow_external_connections=True): """find and remove the activity object""" if isinstance(self.object, str): # it may be that something should be done with these, but idk what @@ -92,13 +98,28 @@ class Undo(Verb): model = None if self.object.type == "Follow": model = apps.get_model("bookwyrm.UserFollows") - obj = self.object.to_model(model=model, save=False, allow_create=False) + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) if not obj: # this could be a follow request not a follow proper model = apps.get_model("bookwyrm.UserFollowRequest") - obj = self.object.to_model(model=model, save=False, allow_create=False) + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) else: - obj = self.object.to_model(model=model, save=False, allow_create=False) + obj = self.object.to_model( + model=model, + save=False, + allow_create=False, + allow_external_connections=allow_external_connections, + ) if not obj: # if we don't have the object, we can't undo it. happens a lot with boosts return @@ -112,9 +133,9 @@ class Follow(Verb): object: str type: str = "Follow" - def action(self): + def action(self, allow_external_connections=True): """relationship save""" - self.to_model() + self.to_model(allow_external_connections=allow_external_connections) @dataclass(init=False) @@ -124,9 +145,9 @@ class Block(Verb): object: str type: str = "Block" - def action(self): + def action(self, allow_external_connections=True): """relationship save""" - self.to_model() + self.to_model(allow_external_connections=allow_external_connections) @dataclass(init=False) @@ -136,7 +157,7 @@ class Accept(Verb): object: Follow type: str = "Accept" - def action(self): + def action(self, allow_external_connections=True): """accept a request""" obj = self.object.to_model(save=False, allow_create=True) obj.accept() @@ -149,7 +170,7 @@ class Reject(Verb): object: Follow type: str = "Reject" - def action(self): + def action(self, allow_external_connections=True): """reject a follow request""" obj = self.object.to_model(save=False, allow_create=False) obj.reject() @@ -163,7 +184,7 @@ class Add(Verb): object: CollectionItem type: str = "Add" - def action(self): + def action(self, allow_external_connections=True): """figure out the target to assign the item to a collection""" target = resolve_remote_id(self.target) item = self.object.to_model(save=False) @@ -177,7 +198,7 @@ class Remove(Add): type: str = "Remove" - def action(self): + def action(self, allow_external_connections=True): """find and remove the activity object""" obj = self.object.to_model(save=False, allow_create=False) if obj: @@ -191,9 +212,9 @@ class Like(Verb): object: str type: str = "Like" - def action(self): + def action(self, allow_external_connections=True): """like""" - self.to_model() + self.to_model(allow_external_connections=allow_external_connections) # pylint: disable=invalid-name @@ -207,6 +228,6 @@ class Announce(Verb): object: str type: str = "Announce" - def action(self): + def action(self, allow_external_connections=True): """boost""" - self.to_model() + self.to_model(allow_external_connections=allow_external_connections) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index d11f5fb1d..8a12736c9 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -67,7 +67,9 @@ class ActivitypubFieldMixin: self.activitypub_field = activitypub_field super().__init__(*args, **kwargs) - def set_field_from_activity(self, instance, data, overwrite=True): + def set_field_from_activity( + self, instance, data, overwrite=True, allow_external_connections=True + ): """helper function for assinging a value to the field. Returns if changed""" try: value = getattr(data, self.get_activitypub_field()) @@ -76,7 +78,9 @@ class ActivitypubFieldMixin: if self.get_activitypub_field() != "attributedTo": raise value = getattr(data, "actor") - formatted = self.field_from_activity(value) + formatted = self.field_from_activity( + value, allow_external_connections=allow_external_connections + ) if formatted is None or formatted is MISSING or formatted == {}: return False @@ -116,7 +120,7 @@ class ActivitypubFieldMixin: return {self.activitypub_wrapper: value} return value - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): """formatter to convert activitypub into a model value""" if value and hasattr(self, "activitypub_wrapper"): value = value.get(self.activitypub_wrapper) @@ -138,7 +142,7 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): self.load_remote = load_remote super().__init__(*args, **kwargs) - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): if not value: return None @@ -159,7 +163,11 @@ class ActivitypubRelatedFieldMixin(ActivitypubFieldMixin): if not self.load_remote: # only look in the local database return related_model.find_existing_by_remote_id(value) - return activitypub.resolve_remote_id(value, model=related_model) + return activitypub.resolve_remote_id( + value, + model=related_model, + allow_external_connections=allow_external_connections, + ) class RemoteIdField(ActivitypubFieldMixin, models.CharField): @@ -219,7 +227,9 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): super().__init__(*args, max_length=255, choices=PrivacyLevels, default="public") # pylint: disable=invalid-name - def set_field_from_activity(self, instance, data, overwrite=True): + def set_field_from_activity( + self, instance, data, overwrite=True, allow_external_connections=True + ): if not overwrite: return False @@ -234,7 +244,11 @@ class PrivacyField(ActivitypubFieldMixin, models.CharField): 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") + user = activitypub.resolve_remote_id( + getattr(data, user_field), + model="User", + allow_external_connections=allow_external_connections, + ) if to == [self.public]: setattr(instance, self.name, "public") @@ -295,13 +309,17 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): self.link_only = link_only super().__init__(*args, **kwargs) - def set_field_from_activity(self, instance, data, overwrite=True): + def set_field_from_activity( + self, instance, data, overwrite=True, allow_external_connections=True + ): """helper function for assigning a value to the field""" if not overwrite and getattr(instance, self.name).exists(): return False value = getattr(data, self.get_activitypub_field()) - formatted = self.field_from_activity(value) + formatted = self.field_from_activity( + value, allow_external_connections=allow_external_connections + ) if formatted is None or formatted is MISSING: return False getattr(instance, self.name).set(formatted) @@ -313,7 +331,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): return f"{value.instance.remote_id}/{self.name}" return [i.remote_id for i in value.all()] - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): if value is None or value is MISSING: return None if not isinstance(value, list): @@ -326,7 +344,11 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField): except ValidationError: continue items.append( - activitypub.resolve_remote_id(remote_id, model=self.related_model) + activitypub.resolve_remote_id( + remote_id, + model=self.related_model, + allow_external_connections=allow_external_connections, + ) ) return items @@ -353,7 +375,7 @@ class TagField(ManyToManyField): ) return tags - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): if not isinstance(value, list): return None items = [] @@ -366,7 +388,11 @@ class TagField(ManyToManyField): # tags can contain multiple types continue items.append( - activitypub.resolve_remote_id(link.href, model=self.related_model) + activitypub.resolve_remote_id( + link.href, + model=self.related_model, + allow_external_connections=allow_external_connections, + ) ) return items @@ -391,10 +417,14 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): super().__init__(*args, **kwargs) # pylint: disable=arguments-differ,arguments-renamed - def set_field_from_activity(self, instance, data, save=True, overwrite=True): + def set_field_from_activity( + self, instance, data, save=True, overwrite=True, allow_external_connections=True + ): """helper function for assinging a value to the field""" value = getattr(data, self.get_activitypub_field()) - formatted = self.field_from_activity(value) + formatted = self.field_from_activity( + value, allow_external_connections=allow_external_connections + ) if formatted is None or formatted is MISSING: return False @@ -426,7 +456,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): return activitypub.Document(url=url, name=alt) - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): image_slug = value # when it's an inline image (User avatar/icon, Book cover), it's a json # blob, but when it's an attached image, it's just a url @@ -481,7 +511,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): return None return value.isoformat() - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): try: date_value = dateutil.parser.parse(value) try: @@ -495,7 +525,7 @@ class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class HtmlField(ActivitypubFieldMixin, models.TextField): """a text field for storing html""" - def field_from_activity(self, value): + def field_from_activity(self, value, allow_external_connections=True): if not value or value == MISSING: return None return clean(value) diff --git a/bookwyrm/views/inbox.py b/bookwyrm/views/inbox.py index 747b5eccd..9b2b238f8 100644 --- a/bookwyrm/views/inbox.py +++ b/bookwyrm/views/inbox.py @@ -64,7 +64,7 @@ class Inbox(View): high = ["Follow", "Accept", "Reject", "Block", "Unblock", "Undo"] priority = HIGH if activity_json["type"] in high else MEDIUM - activity_task.apply_async(args=(activity_json,), queue=priority) + sometimes_async_activity_task(activity_json, queue=priority) return HttpResponse() @@ -102,6 +102,19 @@ def raise_is_blocked_activity(activity_json): raise PermissionDenied() +def sometimes_async_activity_task(activity_json, queue=MEDIUM): + """Sometimes we can effectively respond to a request without queuing a new task, + and whever that is possible, we should do it.""" + activity = activitypub.parse(activity_json) + + # try resolving this activity without making any http requests + try: + activity.action(allow_external_connections=False) + except activitypub.ActivitySerializerError: + # if that doesn't work, run it asynchronously + activity_task.apply_async(args=(activity_json,), queue=queue) + + @app.task(queue=MEDIUM) def activity_task(activity_json): """do something with this json we think is legit""" From 0211dee0ff5e6a9e49a366835fdb835a15e1c55b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 20 Feb 2023 11:09:42 -0800 Subject: [PATCH 02/47] Avoid unnecessary errors when a remote re-sends an Accept --- bookwyrm/models/relationship.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index c8a508117..422967855 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -171,7 +171,11 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship): return with transaction.atomic(): - UserFollows.from_request(self) + try: + UserFollows.from_request(self) + except IntegrityError: + # this just means we already saved this relationship + pass if self.id: self.delete() From 12ed0f46f37ea2714c7512dffa7c2f03b26d2e5e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 20 Feb 2023 12:23:18 -0800 Subject: [PATCH 03/47] Fixes mocks for tests --- bookwyrm/tests/test_signing.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index 8a7f65249..cde193f08 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -95,7 +95,8 @@ class Signature(TestCase): def test_correct_signature(self): """this one should just work""" - response = self.send_test_request(sender=self.mouse) + with patch("bookwyrm.models.relationship.UserFollowRequest.accept"): + response = self.send_test_request(sender=self.mouse) self.assertEqual(response.status_code, 200) def test_wrong_signature(self): @@ -124,8 +125,12 @@ class Signature(TestCase): ) with patch("bookwyrm.models.user.get_remote_reviews.delay"): - response = self.send_test_request(sender=self.fake_remote) + with patch( + "bookwyrm.models.relationship.UserFollowRequest.accept" + ) as accept_mock: + response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) + self.assertTrue(accept_mock.called) @responses.activate def test_key_needs_refresh(self): @@ -148,16 +153,28 @@ class Signature(TestCase): with patch("bookwyrm.models.user.get_remote_reviews.delay"): # Key correct: - response = self.send_test_request(sender=self.fake_remote) + with patch( + "bookwyrm.models.relationship.UserFollowRequest.accept" + ) as accept_mock: + response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) + self.assertTrue(accept_mock.called) # Old key is cached, so still works: - response = self.send_test_request(sender=self.fake_remote) + with patch( + "bookwyrm.models.relationship.UserFollowRequest.accept" + ) as accept_mock: + response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) + self.assertTrue(accept_mock.called) # Try with new key: - response = self.send_test_request(sender=new_sender) + with patch( + "bookwyrm.models.relationship.UserFollowRequest.accept" + ) as accept_mock: + response = self.send_test_request(sender=new_sender) self.assertEqual(response.status_code, 200) + self.assertTrue(accept_mock.called) # Now the old key will fail: response = self.send_test_request(sender=self.fake_remote) From 216be2aeead0282a1159d56cbfb61336e46642ac Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 20 Feb 2023 12:24:53 -0800 Subject: [PATCH 04/47] Fixes pylint complaints "fixes" as in silences, sorry --- bookwyrm/models/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/models/fields.py b/bookwyrm/models/fields.py index 8a12736c9..a970e4124 100644 --- a/bookwyrm/models/fields.py +++ b/bookwyrm/models/fields.py @@ -120,6 +120,7 @@ class ActivitypubFieldMixin: return {self.activitypub_wrapper: value} return value + # pylint: disable=unused-argument def field_from_activity(self, value, allow_external_connections=True): """formatter to convert activitypub into a model value""" if value and hasattr(self, "activitypub_wrapper"): @@ -416,7 +417,7 @@ class ImageField(ActivitypubFieldMixin, models.ImageField): self.alt_field = alt_field super().__init__(*args, **kwargs) - # pylint: disable=arguments-differ,arguments-renamed + # pylint: disable=arguments-differ,arguments-renamed,too-many-arguments def set_field_from_activity( self, instance, data, save=True, overwrite=True, allow_external_connections=True ): From 2470a0fd1c8dcf6acf19308c3f43833225a294ba Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 22 Feb 2023 09:36:39 -0800 Subject: [PATCH 05/47] Create notifications for link domains that need approval --- bookwyrm/models/notification.py | 28 +++++++++++++++++++--- bookwyrm/templates/notifications/item.html | 2 ++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/bookwyrm/models/notification.py b/bookwyrm/models/notification.py index fa2ce54e2..29f7b0c2d 100644 --- a/bookwyrm/models/notification.py +++ b/bookwyrm/models/notification.py @@ -2,8 +2,8 @@ from django.db import models, transaction from django.dispatch import receiver from .base_model import BookWyrmModel -from . import Boost, Favorite, GroupMemberInvitation, ImportJob, ListItem, Report -from . import Status, User, UserFollowRequest +from . import Boost, Favorite, GroupMemberInvitation, ImportJob, LinkDomain +from . import ListItem, Report, Status, User, UserFollowRequest class Notification(BookWyrmModel): @@ -28,6 +28,7 @@ class Notification(BookWyrmModel): # Admin REPORT = "REPORT" + LINK_DOMAIN = "LINK_DOMAIN" # Groups INVITE = "INVITE" @@ -43,7 +44,7 @@ class Notification(BookWyrmModel): NotificationType = models.TextChoices( # there has got be a better way to do this "NotificationType", - f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}", + f"{FAVORITE} {REPLY} {MENTION} {TAG} {FOLLOW} {FOLLOW_REQUEST} {BOOST} {IMPORT} {ADD} {REPORT} {LINK_DOMAIN} {INVITE} {ACCEPT} {JOIN} {LEAVE} {REMOVE} {GROUP_PRIVACY} {GROUP_NAME} {GROUP_DESCRIPTION}", ) user = models.ForeignKey("User", on_delete=models.CASCADE) @@ -64,6 +65,7 @@ class Notification(BookWyrmModel): "ListItem", symmetrical=False, related_name="notifications" ) related_reports = models.ManyToManyField("Report", symmetrical=False) + related_link_domains = models.ManyToManyField("LinkDomain", symmetrical=False) @classmethod @transaction.atomic @@ -241,6 +243,26 @@ def notify_admins_on_report(sender, instance, created, *args, **kwargs): notification.related_reports.add(instance) +@receiver(models.signals.post_save, sender=LinkDomain) +@transaction.atomic +# pylint: disable=unused-argument +def notify_admins_on_link_domain(sender, instance, created, *args, **kwargs): + """a new link domain needs to be verified""" + if not created: + # otherwise you'll get a notification when you approve a domain + return + + # moderators and superusers should be notified + admins = User.admins() + for admin in admins: + notification, _ = Notification.objects.get_or_create( + user=admin, + notification_type=Notification.LINK_DOMAIN, + read=False, + ) + notification.related_link_domains.add(instance) + + @receiver(models.signals.post_save, sender=GroupMemberInvitation) # pylint: disable=unused-argument def notify_user_on_group_invite(sender, instance, *args, **kwargs): diff --git a/bookwyrm/templates/notifications/item.html b/bookwyrm/templates/notifications/item.html index e8e2dcb26..b53abe3d1 100644 --- a/bookwyrm/templates/notifications/item.html +++ b/bookwyrm/templates/notifications/item.html @@ -17,6 +17,8 @@ {% include 'notifications/items/add.html' %} {% elif notification.notification_type == 'REPORT' %} {% include 'notifications/items/report.html' %} +{% elif notification.notification_type == 'LINK_DOMAIN' %} + {% include 'notifications/items/link_domain.html' %} {% elif notification.notification_type == 'INVITE' %} {% include 'notifications/items/invite.html' %} {% elif notification.notification_type == 'ACCEPT' %} From 268946a77c2019003f3449315a75e793a732652d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 22 Feb 2023 10:43:49 -0800 Subject: [PATCH 06/47] Adds missing template and migration files --- .../migrations/0174_auto_20230222_1742.py | 46 +++++++++++++++++++ .../notifications/items/link_domain.html | 20 ++++++++ 2 files changed, 66 insertions(+) create mode 100644 bookwyrm/migrations/0174_auto_20230222_1742.py create mode 100644 bookwyrm/templates/notifications/items/link_domain.html diff --git a/bookwyrm/migrations/0174_auto_20230222_1742.py b/bookwyrm/migrations/0174_auto_20230222_1742.py new file mode 100644 index 000000000..0f2f89ec5 --- /dev/null +++ b/bookwyrm/migrations/0174_auto_20230222_1742.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.18 on 2023-02-22 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0173_default_user_auth_group_setting"), + ] + + operations = [ + migrations.AddField( + model_name="notification", + name="related_link_domains", + field=models.ManyToManyField(to="bookwyrm.LinkDomain"), + ), + migrations.AlterField( + model_name="notification", + name="notification_type", + field=models.CharField( + choices=[ + ("FAVORITE", "Favorite"), + ("REPLY", "Reply"), + ("MENTION", "Mention"), + ("TAG", "Tag"), + ("FOLLOW", "Follow"), + ("FOLLOW_REQUEST", "Follow Request"), + ("BOOST", "Boost"), + ("IMPORT", "Import"), + ("ADD", "Add"), + ("REPORT", "Report"), + ("LINK_DOMAIN", "Link Domain"), + ("INVITE", "Invite"), + ("ACCEPT", "Accept"), + ("JOIN", "Join"), + ("LEAVE", "Leave"), + ("REMOVE", "Remove"), + ("GROUP_PRIVACY", "Group Privacy"), + ("GROUP_NAME", "Group Name"), + ("GROUP_DESCRIPTION", "Group Description"), + ], + max_length=255, + ), + ), + ] diff --git a/bookwyrm/templates/notifications/items/link_domain.html b/bookwyrm/templates/notifications/items/link_domain.html new file mode 100644 index 000000000..aaed830ed --- /dev/null +++ b/bookwyrm/templates/notifications/items/link_domain.html @@ -0,0 +1,20 @@ +{% extends 'notifications/items/layout.html' %} +{% load humanize %} +{% load i18n %} + +{% block primary_link %}{% spaceless %} +{% url 'settings-link-domain' %} +{% endspaceless %}{% endblock %} + +{% block icon %} + +{% endblock %} + +{% block description %} + {% url 'settings-link-domain' as path %} + {% blocktrans trimmed count counter=notification.related_link_domains.count with display_count=notification.related_link_domains.count|intcomma %} + A new link domain needs review + {% plural %} + {{ display_count }} new link domains need moderation + {% endblocktrans %} +{% endblock %} From 99fc2b7a3660707c8830fcdc54d4c255a2dcee8e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 25 Feb 2023 15:56:03 -0800 Subject: [PATCH 07/47] Only use chronological pagination sometimes The timeline uses chronological buttons, but other paginated pages do not (by default). This also reversed the chronology. --- bookwyrm/templates/feed/layout.html | 2 +- bookwyrm/templates/snippets/pagination.html | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/feed/layout.html b/bookwyrm/templates/feed/layout.html index 16a868c2a..b70ed99ea 100644 --- a/bookwyrm/templates/feed/layout.html +++ b/bookwyrm/templates/feed/layout.html @@ -23,7 +23,7 @@ {% block panel %}{% endblock %} {% if activities %} - {% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %} + {% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" mode="chronological" %} {% endif %} diff --git a/bookwyrm/templates/snippets/pagination.html b/bookwyrm/templates/snippets/pagination.html index 85f966f50..5ab724c06 100644 --- a/bookwyrm/templates/snippets/pagination.html +++ b/bookwyrm/templates/snippets/pagination.html @@ -9,7 +9,11 @@ {% endif %}> - {% trans "Older" %} + {% if mode == "chronological" %} + {% trans "Newer" %} + {% else %} + {% trans "Previous" %} + {% endif %} From d1110630db91c0349a4d90d6e9155f102aa1998c Mon Sep 17 00:00:00 2001 From: Christof Dorner Date: Sun, 26 Feb 2023 11:24:00 +0100 Subject: [PATCH 08/47] Use chronological pagination on user profile activity lists --- bookwyrm/templates/user/reviews_comments.html | 2 +- bookwyrm/templates/user/user.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/user/reviews_comments.html b/bookwyrm/templates/user/reviews_comments.html index f5c5c9265..d85f8f512 100644 --- a/bookwyrm/templates/user/reviews_comments.html +++ b/bookwyrm/templates/user/reviews_comments.html @@ -25,6 +25,6 @@ {% endif %} - {% include 'snippets/pagination.html' with page=activities path=path %} + {% include 'snippets/pagination.html' with page=activities path=path mode="chronological" %} {% endblock %} diff --git a/bookwyrm/templates/user/user.html b/bookwyrm/templates/user/user.html index 42b0ffbb5..0d015760c 100755 --- a/bookwyrm/templates/user/user.html +++ b/bookwyrm/templates/user/user.html @@ -87,7 +87,7 @@ {% trans "Back" %} - + {% endblock %} From 9c92ba169861ebea0948af627f8e2f0686098515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 1 Mar 2023 14:14:42 +0100 Subject: [PATCH 09/47] Add attributes to images to hint async load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was suggested on Matrix a while ago but I only found the time now to move forward with it. Signed-off-by: André Jaenisch --- bookwyrm/templates/book/book.html | 2 ++ bookwyrm/templates/book/cover_show_modal.html | 2 +- bookwyrm/templates/email/html_layout.html | 2 +- bookwyrm/templates/embed-layout.html | 2 +- bookwyrm/templates/get_started/layout.html | 2 ++ bookwyrm/templates/layout.html | 2 +- bookwyrm/templates/ostatus/template.html | 2 +- bookwyrm/templates/setup/layout.html | 2 ++ bookwyrm/templates/snippets/about.html | 2 +- bookwyrm/templates/snippets/avatar.html | 2 ++ bookwyrm/templates/snippets/book_cover.html | 2 ++ bookwyrm/templates/snippets/status/content_status.html | 2 ++ bookwyrm/templates/two_factor_auth/two_factor_login.html | 2 +- bookwyrm/templates/two_factor_auth/two_factor_prompt.html | 2 +- 14 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index d27c7ec54..e9eff99ab 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -82,6 +82,8 @@ src="{% static "images/no_cover.jpg" %}" alt="" aria-hidden="true" + loading="lazy" + decoding="async" > {{ book.alt_text }} diff --git a/bookwyrm/templates/book/cover_show_modal.html b/bookwyrm/templates/book/cover_show_modal.html index f244aa535..7ab4dbf29 100644 --- a/bookwyrm/templates/book/cover_show_modal.html +++ b/bookwyrm/templates/book/cover_show_modal.html @@ -5,7 +5,7 @@ diff --git a/bookwyrm/templates/email/html_layout.html b/bookwyrm/templates/email/html_layout.html index 01e2f35c6..b9f88732f 100644 --- a/bookwyrm/templates/email/html_layout.html +++ b/bookwyrm/templates/email/html_layout.html @@ -2,7 +2,7 @@
- logo + logo
{{ site_name }}
diff --git a/bookwyrm/templates/embed-layout.html b/bookwyrm/templates/embed-layout.html index 6a8d77016..c619bf2dc 100644 --- a/bookwyrm/templates/embed-layout.html +++ b/bookwyrm/templates/embed-layout.html @@ -17,7 +17,7 @@
- + {{ site.name }}
diff --git a/bookwyrm/templates/get_started/layout.html b/bookwyrm/templates/get_started/layout.html index b8e7c861b..4eea59fe7 100644 --- a/bookwyrm/templates/get_started/layout.html +++ b/bookwyrm/templates/get_started/layout.html @@ -15,6 +15,8 @@ src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" aria-hidden="true" alt="{{ site.name }}" + loading="lazy" + decoding="async" >

{% blocktrans %}Welcome to {{ site_name }}!{% endblocktrans %} diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index c3408f44e..239137b8a 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -28,7 +28,7 @@ {% with notification_count=request.user.unread_notification_count has_unread_mentions=request.user.has_unread_mentions %}