From 097d86454a137495b1462e42ad11098507f4a08c Mon Sep 17 00:00:00 2001
From: "Renato \"Lond\" Cerqueira"
Date: Tue, 29 Dec 2020 20:51:05 +0100
Subject: [PATCH 01/63] Add signatures to requests to mastodon to support
authorized fetch mode
When mastodon is in authorized fetch mode any request has to be signed
or it fails with 401. This adds the needed signature to the requests
made to discover the actor when receiving something from mastodon (such
as a follow request)
---
bookwyrm/activitypub/base_activity.py | 40 +++++++++++++++++++++++++++
bookwyrm/models/activitypub_mixin.py | 2 +-
bookwyrm/signatures.py | 12 +++++---
bookwyrm/tests/test_signing.py | 2 +-
4 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 24d383ac79..c3287d45d6 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -8,6 +8,10 @@
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.tasks import app
+import requests
+from django.utils.http import http_date
+from bookwyrm import models
+from bookwyrm.signatures import make_signature
class ActivitySerializerError(ValueError):
"""routine problems serializing activitypub json"""
@@ -284,6 +288,12 @@ def resolve_remote_id(
# load the data and create the object
try:
data = get_data(remote_id)
+ except requests.HTTPError as e:
+ if e.response.status_code == 401:
+ ''' This most likely means it's a mastodon with secure fetch enabled. Need to be specific '''
+ data = get_activitypub_data(remote_id)
+ else:
+ raise e
except ConnectorException:
raise ActivitySerializerError(
f"Could not connect to host for remote_id: {remote_id}"
@@ -304,3 +314,33 @@ def resolve_remote_id(
# if we're refreshing, "result" will be set and we'll update it
return item.to_model(model=model, instance=result, save=save)
+
+def get_activitypub_data(url):
+ ''' wrapper for request.get '''
+ now = http_date()
+
+ # XXX TEMP!!
+ sender = models.User.objects.get(id=1)
+ if not sender.key_pair.private_key:
+ # this shouldn't happen. it would be bad if it happened.
+ raise ValueError('No private key found for sender')
+
+ try:
+ resp = requests.get(
+ url,
+ headers={
+ 'Accept': 'application/json; charset=utf-8',
+ 'Date': now,
+ 'Signature': make_signature('get', sender, url, now),
+ },
+ )
+ except RequestError:
+ raise ConnectorException()
+ if not resp.ok:
+ resp.raise_for_status()
+ try:
+ data = resp.json()
+ except ValueError:
+ raise ConnectorException()
+
+ return data
diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py
index 402cb040be..ee0b2c40dc 100644
--- a/bookwyrm/models/activitypub_mixin.py
+++ b/bookwyrm/models/activitypub_mixin.py
@@ -533,7 +533,7 @@ def sign_and_send(sender, data, destination):
headers={
"Date": now,
"Digest": digest,
- "Signature": make_signature(sender, destination, now, digest),
+ "Signature": make_signature("post", sender, destination, now, digest),
"Content-Type": "application/activity+json; charset=utf-8",
"User-Agent": USER_AGENT,
},
diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py
index 61cafe71f3..27c6357f67 100644
--- a/bookwyrm/signatures.py
+++ b/bookwyrm/signatures.py
@@ -22,27 +22,31 @@ def create_key_pair():
return private_key, public_key
-def make_signature(sender, destination, date, digest):
+def make_signature(method, sender, destination, date, digest):
"""uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination)
signature_headers = [
- f"(request-target): post {inbox_parts.path}",
+ f"(request-target): {method} {inbox_parts.path}",
f"host: {inbox_parts.netloc}",
f"date: {date}",
f"digest: {digest}",
]
+ headers = "(request-target) host date"
+ if digest is not None:
+ signature_headers.append("digest: %s" % digest)
+ headers = "(request-target) host date digest"
+
message_to_sign = "\n".join(signature_headers)
signer = pkcs1_15.new(RSA.import_key(sender.key_pair.private_key))
signed_message = signer.sign(SHA256.new(message_to_sign.encode("utf8")))
signature = {
"keyId": f"{sender.remote_id}#main-key",
"algorithm": "rsa-sha256",
- "headers": "(request-target) host date digest",
+ "headers": headers,
"signature": b64encode(signed_message).decode("utf8"),
}
return ",".join(f'{k}="{v}"' for (k, v) in signature.items())
-
def make_digest(data):
"""creates a message digest for signing"""
return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode(
diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py
index d33687a59e..afcfb69072 100644
--- a/bookwyrm/tests/test_signing.py
+++ b/bookwyrm/tests/test_signing.py
@@ -85,7 +85,7 @@ def send_test_request( # pylint: disable=too-many-arguments
now = date or http_date()
data = json.dumps(get_follow_activity(sender, self.rat))
digest = digest or make_digest(data)
- signature = make_signature(signer or sender, self.rat.inbox, now, digest)
+ signature = make_signature("post", signer or sender, self.rat.inbox, now, digest)
with patch("bookwyrm.views.inbox.activity_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"):
return self.send(signature, now, send_data or data, digest)
From e2ee3d27a7e40b877d8ddba90c056aa3f7418d25 Mon Sep 17 00:00:00 2001
From: "Renato \"Lond\" Cerqueira"
Date: Wed, 5 Jan 2022 15:42:54 +0100
Subject: [PATCH 02/63] WIP
---
bookwyrm/activitypub/base_activity.py | 12 ++++++++++--
bookwyrm/tests/activitypub/test_base_activity.py | 5 +++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index c3287d45d6..f58b0bde9e 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -12,6 +12,7 @@
from django.utils.http import http_date
from bookwyrm import models
from bookwyrm.signatures import make_signature
+from bookwyrm.settings import DOMAIN
class ActivitySerializerError(ValueError):
"""routine problems serializing activitypub json"""
@@ -315,12 +316,19 @@ def resolve_remote_id(
# if we're refreshing, "result" will be set and we'll update it
return item.to_model(model=model, instance=result, save=save)
+def get_representative():
+ try:
+ models.User.objects.get(id=-99)
+ except models.User.DoesNotExist:
+ username = "%s@%s" % (DOMAIN, DOMAIN)
+ email = "representative@%s" % (DOMAIN)
+ models.User.objects.create_user(id=-99, username=username, email=email, local=True, localname=DOMAIN)
+
def get_activitypub_data(url):
''' wrapper for request.get '''
now = http_date()
- # XXX TEMP!!
- sender = models.User.objects.get(id=1)
+ sender = get_representative()
if not sender.key_pair.private_key:
# this shouldn't happen. it would be bad if it happened.
raise ValueError('No private key found for sender')
diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py
index b951c7ab43..0eca7f7aac 100644
--- a/bookwyrm/tests/activitypub/test_base_activity.py
+++ b/bookwyrm/tests/activitypub/test_base_activity.py
@@ -14,6 +14,7 @@
ActivityObject,
resolve_remote_id,
set_related_field,
+ get_representative
)
from bookwyrm.activitypub import ActivitySerializerError
from bookwyrm import models
@@ -51,6 +52,10 @@ def setUp(self):
image.save(output, format=image.format)
self.image_data = output.getvalue()
+ def test_get_representative_not_existing(self, _):
+ representative = get_representative()
+ self.assertIsInstance(representative, models.User)
+
def test_init(self, *_):
"""simple successfuly init"""
instance = ActivityObject(id="a", type="b")
From 9a0f8f9c2a6882f88de26736cafb80bcdeb13060 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Thu, 19 Jan 2023 16:40:13 +1100
Subject: [PATCH 03/63] fix test_get_representative_not_existing params
---
bookwyrm/tests/activitypub/test_base_activity.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py
index e3ba19e6d9..6ae446ff7b 100644
--- a/bookwyrm/tests/activitypub/test_base_activity.py
+++ b/bookwyrm/tests/activitypub/test_base_activity.py
@@ -53,7 +53,7 @@ def setUp(self):
image.save(output, format=image.format)
self.image_data = output.getvalue()
- def test_get_representative_not_existing(self, _):
+ def test_get_representative_not_existing(self, *_):
representative = get_representative()
self.assertIsInstance(representative, models.User)
From 0c614e828fee016814d1e31f963aa23fba3d8dc6 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 08:24:46 +1100
Subject: [PATCH 04/63] deal with missing digests in signatures
If no digest value is passed to make_signature and Exception was thrown.
Since digest is added to the signature headers if it is not None anyway, there is no need to assign the digest value before that check.
When signing a request _as the server_ for Mastodon's AUTHORIZED_FETCH there is no need to include a digest.
---
bookwyrm/signatures.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py
index dc094d9884..ff634232d0 100644
--- a/bookwyrm/signatures.py
+++ b/bookwyrm/signatures.py
@@ -22,14 +22,13 @@ def create_key_pair():
return private_key, public_key
-def make_signature(method, sender, destination, date, digest):
+def make_signature(method, sender, destination, date, digest=None):
"""uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination)
signature_headers = [
f"(request-target): {method} {inbox_parts.path}",
f"host: {inbox_parts.netloc}",
- f"date: {date}",
- f"digest: {digest}",
+ f"date: {date}"
]
headers = "(request-target) host date"
if digest is not None:
From 0da5473b0c2beb6e4bc567d6761bb9c9d8113c42 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 16:31:27 +1100
Subject: [PATCH 05/63] black formatting
---
bookwyrm/signatures.py | 3 ++-
bookwyrm/tests/activitypub/test_base_activity.py | 2 +-
bookwyrm/tests/test_signing.py | 4 +++-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py
index ff634232d0..fb0ad49c10 100644
--- a/bookwyrm/signatures.py
+++ b/bookwyrm/signatures.py
@@ -28,7 +28,7 @@ def make_signature(method, sender, destination, date, digest=None):
signature_headers = [
f"(request-target): {method} {inbox_parts.path}",
f"host: {inbox_parts.netloc}",
- f"date: {date}"
+ f"date: {date}",
]
headers = "(request-target) host date"
if digest is not None:
@@ -46,6 +46,7 @@ def make_signature(method, sender, destination, date, digest=None):
}
return ",".join(f'{k}="{v}"' for (k, v) in signature.items())
+
def make_digest(data):
"""creates a message digest for signing"""
return "SHA-256=" + b64encode(hashlib.sha256(data.encode("utf-8")).digest()).decode(
diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py
index 6ae446ff7b..56bc142bb9 100644
--- a/bookwyrm/tests/activitypub/test_base_activity.py
+++ b/bookwyrm/tests/activitypub/test_base_activity.py
@@ -14,7 +14,7 @@
ActivityObject,
resolve_remote_id,
set_related_field,
- get_representative
+ get_representative,
)
from bookwyrm.activitypub import ActivitySerializerError
from bookwyrm import models
diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py
index 54675d9d50..ec50a3da0f 100644
--- a/bookwyrm/tests/test_signing.py
+++ b/bookwyrm/tests/test_signing.py
@@ -85,7 +85,9 @@ def send_test_request( # pylint: disable=too-many-arguments
now = date or http_date()
data = json.dumps(get_follow_activity(sender, self.rat))
digest = digest or make_digest(data)
- signature = make_signature("post", signer or sender, self.rat.inbox, now, digest)
+ signature = make_signature(
+ "post", signer or sender, self.rat.inbox, now, digest
+ )
with patch("bookwyrm.views.inbox.activity_task.delay"):
with patch("bookwyrm.models.user.set_remote_server.delay"):
return self.send(signature, now, send_data or data, digest)
From 41082387160341dd12951a31a4449520dc77b640 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 16:32:17 +1100
Subject: [PATCH 06/63] resolve SECURE_FETCH bugs
ERROR HANDLING FIXES
- use raise_for_status() to pass through response code
- handle exceptions where no response object is passed through
INSTANCE ACTOR
- models.User.objects.create_user function cannot take an ID
- allow instance admins to determine username and email for instance actor in settings.py
---
bookwyrm/activitypub/base_activity.py | 38 +++++++++++------------
bookwyrm/connectors/abstract_connector.py | 2 +-
bookwyrm/settings.py | 5 +++
3 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 3390c9cf50..e7061a4561 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -11,7 +11,7 @@
from bookwyrm import models
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.signatures import make_signature
-from bookwyrm.settings import DOMAIN
+from bookwyrm.settings import DOMAIN, INSTANCE_ACTOR_USERNAME, INSTANCE_ACTOR_EMAIL
from bookwyrm.tasks import app, MEDIUM
logger = logging.getLogger(__name__)
@@ -281,15 +281,12 @@ def resolve_remote_id(
try:
data = get_data(remote_id)
except requests.HTTPError as e:
- if e.response.status_code == 401:
- ''' This most likely means it's a mastodon with secure fetch enabled. Need to be specific '''
+ if (e.response is not None) and e.response.status_code == 401:
+ """This most likely means it's a mastodon with secure fetch enabled."""
data = get_activitypub_data(remote_id)
else:
- raise e
- except ConnectorException:
- logger.info("Could not connect to host for remote_id: %s", remote_id)
- return None
-
+ logger.info("Could not connect to host for remote_id: %s", remote_id)
+ return None
# determine the model implicitly, if not provided
# or if it's a model with subclasses like Status, check again
if not model or hasattr(model.objects, "select_subclasses"):
@@ -309,33 +306,34 @@ def resolve_remote_id(
def get_representative():
+ username = "%s@%s" % (INSTANCE_ACTOR_USERNAME, DOMAIN)
try:
- models.User.objects.get(id=-99)
+ user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
- username = "%s@%s" % (DOMAIN, DOMAIN)
- email = "representative@%s" % (DOMAIN)
- models.User.objects.create_user(id=-99, username=username, email=email, local=True, localname=DOMAIN)
+ email = INSTANCE_ACTOR_EMAIL
+ user = models.User.objects.create_user(
+ username=username, email=email, local=True, localname=DOMAIN
+ )
+ return user
def get_activitypub_data(url):
- ''' wrapper for request.get '''
+ """wrapper for request.get"""
now = http_date()
-
sender = get_representative()
if not sender.key_pair.private_key:
# this shouldn't happen. it would be bad if it happened.
- raise ValueError('No private key found for sender')
-
+ raise ValueError("No private key found for sender")
try:
resp = requests.get(
url,
headers={
- 'Accept': 'application/json; charset=utf-8',
- 'Date': now,
- 'Signature': make_signature('get', sender, url, now),
+ "Accept": "application/json; charset=utf-8",
+ "Date": now,
+ "Signature": make_signature("get", sender, url, now),
},
)
- except RequestError:
+ except requests.RequestException:
raise ConnectorException()
if not resp.ok:
resp.raise_for_status()
diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py
index 8ae93926a4..dbfb2668d1 100644
--- a/bookwyrm/connectors/abstract_connector.py
+++ b/bookwyrm/connectors/abstract_connector.py
@@ -244,7 +244,7 @@ def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT):
raise ConnectorException(err)
if not resp.ok:
- raise ConnectorException()
+ resp.raise_for_status()
try:
data = resp.json()
except ValueError as err:
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 74ef7d3136..528bf68e29 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -370,3 +370,8 @@
HTTP_X_FORWARDED_PROTO = env.bool("SECURE_PROXY_SSL_HEADER", False)
if HTTP_X_FORWARDED_PROTO:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
+
+# AUTHORIZED_FETCH Instance Actor
+# WARNING this must both be unique - not used by any other user
+INSTANCE_ACTOR_USERNAME = DOMAIN
+INSTANCE_ACTOR_EMAIL = f"representative@{DOMAIN}"
From f8c9df4aff68e92b47db14dbcebe2ba8980f99c1 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 18:20:18 +1100
Subject: [PATCH 07/63] pylint fixes
---
bookwyrm/activitypub/base_activity.py | 13 +++++++------
bookwyrm/signatures.py | 2 +-
bookwyrm/tests/activitypub/test_base_activity.py | 1 +
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index e7061a4561..6fc600b77e 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -1,8 +1,8 @@
""" basics for an activitypub serializer """
from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder
-import requests
import logging
+import requests
from django.apps import apps
from django.db import IntegrityError, transaction
@@ -251,10 +251,10 @@ def set_related_field(
def get_model_from_type(activity_type):
"""given the activity, what type of model"""
- models = apps.get_models()
+ activity_models = apps.get_models()
model = [
m
- for m in models
+ for m in activity_models
if hasattr(m, "activity_serializer")
and hasattr(m.activity_serializer, "type")
and m.activity_serializer.type == activity_type
@@ -280,9 +280,9 @@ def resolve_remote_id(
# load the data and create the object
try:
data = get_data(remote_id)
- except requests.HTTPError as e:
+ except ConnectorException as e:
if (e.response is not None) and e.response.status_code == 401:
- """This most likely means it's a mastodon with secure fetch enabled."""
+ # This most likely means it's a mastodon with secure fetch enabled.
data = get_activitypub_data(remote_id)
else:
logger.info("Could not connect to host for remote_id: %s", remote_id)
@@ -306,7 +306,8 @@ def resolve_remote_id(
def get_representative():
- username = "%s@%s" % (INSTANCE_ACTOR_USERNAME, DOMAIN)
+ """Get or create an actor representing the entire instance to sign requests to 'secure mastodon' servers"""
+ username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
diff --git a/bookwyrm/signatures.py b/bookwyrm/signatures.py
index fb0ad49c10..772d39cce7 100644
--- a/bookwyrm/signatures.py
+++ b/bookwyrm/signatures.py
@@ -32,7 +32,7 @@ def make_signature(method, sender, destination, date, digest=None):
]
headers = "(request-target) host date"
if digest is not None:
- signature_headers.append("digest: %s" % digest)
+ signature_headers.append(f"digest: {digest}")
headers = "(request-target) host date digest"
message_to_sign = "\n".join(signature_headers)
diff --git a/bookwyrm/tests/activitypub/test_base_activity.py b/bookwyrm/tests/activitypub/test_base_activity.py
index 56bc142bb9..120cd2c91a 100644
--- a/bookwyrm/tests/activitypub/test_base_activity.py
+++ b/bookwyrm/tests/activitypub/test_base_activity.py
@@ -54,6 +54,7 @@ def setUp(self):
self.image_data = output.getvalue()
def test_get_representative_not_existing(self, *_):
+ """test that an instance representative actor is created if it does not exist"""
representative = get_representative()
self.assertIsInstance(representative, models.User)
From e8452011f76c3c026e016669b7c717285c7281b0 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 19:55:38 +1100
Subject: [PATCH 08/63] handle get_data exceptions better
Makes exception handling more precise, only raising status for 401s.
Also fixes a string pylint was complaining about.
---
bookwyrm/activitypub/base_activity.py | 3 ++-
bookwyrm/connectors/abstract_connector.py | 6 +++++-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 6fc600b77e..a4affa172d 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -306,7 +306,8 @@ def resolve_remote_id(
def get_representative():
- """Get or create an actor representing the entire instance to sign requests to 'secure mastodon' servers"""
+ """Get or create an actor representing the instance
+ to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
try:
user = models.User.objects.get(username=username)
diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py
index dbfb2668d1..6dd8a3081c 100644
--- a/bookwyrm/connectors/abstract_connector.py
+++ b/bookwyrm/connectors/abstract_connector.py
@@ -244,7 +244,11 @@ def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT):
raise ConnectorException(err)
if not resp.ok:
- resp.raise_for_status()
+ if resp.status_code == 401:
+ # this is probably an AUTHORIZED_FETCH issue
+ resp.raise_for_status()
+ else:
+ raise ConnectorException()
try:
data = resp.json()
except ValueError as err:
From 317fa5cdfd63d49c34a6210a13674a596ee97bd7 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Fri, 20 Jan 2023 20:05:14 +1100
Subject: [PATCH 09/63] black
---
bookwyrm/activitypub/base_activity.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index a4affa172d..7befc5bc6b 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -307,7 +307,7 @@ def resolve_remote_id(
def get_representative():
"""Get or create an actor representing the instance
- to sign requests to 'secure mastodon' servers"""
+ to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
try:
user = models.User.objects.get(username=username)
From 803bba71a6da58b51caacd3210db279c13faba9f Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Sun, 22 Jan 2023 15:59:19 +1100
Subject: [PATCH 10/63] fix error handling
- when using raise_for_status we need to catch an HTTPError, not a ConnectionError
- simplify instance actor - use internal email address since it will never be used anyway, and make default username less likely to already be in use.
---
bookwyrm/activitypub/base_activity.py | 11 +++++++----
bookwyrm/settings.py | 4 +---
2 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 7befc5bc6b..3adddfeb9c 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -11,7 +11,7 @@
from bookwyrm import models
from bookwyrm.connectors import ConnectorException, get_data
from bookwyrm.signatures import make_signature
-from bookwyrm.settings import DOMAIN, INSTANCE_ACTOR_USERNAME, INSTANCE_ACTOR_EMAIL
+from bookwyrm.settings import DOMAIN, INSTANCE_ACTOR_USERNAME
from bookwyrm.tasks import app, MEDIUM
logger = logging.getLogger(__name__)
@@ -280,7 +280,10 @@ def resolve_remote_id(
# load the data and create the object
try:
data = get_data(remote_id)
- except ConnectorException as e:
+ except ConnectionError:
+ logger.info("Could not connect to host for remote_id: %s", remote_id)
+ return None
+ except requests.HTTPError as e:
if (e.response is not None) and e.response.status_code == 401:
# This most likely means it's a mastodon with secure fetch enabled.
data = get_activitypub_data(remote_id)
@@ -309,12 +312,12 @@ def get_representative():
"""Get or create an actor representing the instance
to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
+ email = "bookwyrm@localhost"
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
- email = INSTANCE_ACTOR_EMAIL
user = models.User.objects.create_user(
- username=username, email=email, local=True, localname=DOMAIN
+ username=username, email=email, local=True, localname=INSTANCE_ACTOR_USERNAME
)
return user
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 528bf68e29..ed1447b05d 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -372,6 +372,4 @@
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# AUTHORIZED_FETCH Instance Actor
-# WARNING this must both be unique - not used by any other user
-INSTANCE_ACTOR_USERNAME = DOMAIN
-INSTANCE_ACTOR_EMAIL = f"representative@{DOMAIN}"
+INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor"
From f0e1767bc96e9924cf66c80d8909b3ab4ed8facf Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Sun, 22 Jan 2023 16:10:30 +1100
Subject: [PATCH 11/63] black code
---
bookwyrm/activitypub/base_activity.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py
index 3adddfeb9c..02e5395fc4 100644
--- a/bookwyrm/activitypub/base_activity.py
+++ b/bookwyrm/activitypub/base_activity.py
@@ -317,7 +317,10 @@ def get_representative():
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
user = models.User.objects.create_user(
- username=username, email=email, local=True, localname=INSTANCE_ACTOR_USERNAME
+ username=username,
+ email=email,
+ local=True,
+ localname=INSTANCE_ACTOR_USERNAME,
)
return user
From 35d30a41f3c0975d2c91bec13a4aa73a9b87e415 Mon Sep 17 00:00:00 2001
From: Dustin Steiner
Date: Tue, 24 Jan 2023 13:00:18 +0000
Subject: [PATCH 12/63] feat: first version of a book series list by author
---
bookwyrm/templates/book/book.html | 2 +-
bookwyrm/templates/book/series.html | 35 +++++++++++++++++
bookwyrm/urls.py | 1 +
bookwyrm/views/__init__.py | 1 +
bookwyrm/views/books/series.py | 58 +++++++++++++++++++++++++++++
5 files changed, 96 insertions(+), 1 deletion(-)
create mode 100644 bookwyrm/templates/book/series.html
create mode 100644 bookwyrm/views/books/series.py
diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html
index 6ea051ab9e..d8f9110aa6 100644
--- a/bookwyrm/templates/book/book.html
+++ b/bookwyrm/templates/book/book.html
@@ -46,7 +46,7 @@
{% endif %}
diff --git a/bookwyrm/templates/book/series.html b/bookwyrm/templates/book/series.html
new file mode 100644
index 0000000000..55cebd3c14
--- /dev/null
+++ b/bookwyrm/templates/book/series.html
@@ -0,0 +1,35 @@
+{% extends 'layout.html' %}
+{% load i18n %}
+{% load book_display_tags %}
+
+{% block title %}{{ series_name }}{% endblock %}
+
+{% block content %}
+
+
{{ series_name }}
+
+
+
+ {% for book in books %}
+ {% with book=book %}
+
+
+ {% if book.series_number %}Book {{ book.series_number }}{% else %}Unsorted Book{% endif %}
+ {% include 'landing/small-book.html' with book=book %}
+
+
+ {% endwith %}
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index ac3a805803..6d9f34de00 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -610,6 +610,7 @@
# books
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
+ re_path(r"^series/by/(?P\d+)/?$", views.BookSeriesBy.as_view(), name="book-series-by"),
re_path(
rf"{BOOK_PATH}/(?Preview|comment|quote)/?$",
views.Book.as_view(),
diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py
index db88f1ae28..589ddb49c0 100644
--- a/bookwyrm/views/__init__.py
+++ b/bookwyrm/views/__init__.py
@@ -50,6 +50,7 @@
add_description,
resolve_book,
)
+from .books.series import BookSeriesBy
from .books.books import update_book_from_remote
from .books.edit_book import (
EditBook,
diff --git a/bookwyrm/views/books/series.py b/bookwyrm/views/books/series.py
new file mode 100644
index 0000000000..6085934d35
--- /dev/null
+++ b/bookwyrm/views/books/series.py
@@ -0,0 +1,58 @@
+""" books belonging to the same series """
+from django.views import View
+from django.shortcuts import get_object_or_404
+from django.template.response import TemplateResponse
+
+from bookwyrm.views.helpers import is_api_request
+from bookwyrm import models
+from bookwyrm.settings import PAGE_LENGTH
+
+
+# pylint: disable=no-self-use
+class BookSeriesBy(View):
+ def get(self, request, author_id, **kwargs):
+ """lists all books in a series"""
+ series_name = request.GET.get("series_name")
+
+ if is_api_request(request):
+ pass
+
+ author = get_object_or_404(models.Author, id=author_id)
+
+ results = (
+ models.Edition.objects.filter(authors=author, series=series_name)
+ )
+
+ # when there are multiple editions of the same work, pick the closest
+ editions_of_work = results.values_list("parent_work__id", flat=True).distinct()
+
+ # filter out multiple editions of the same work
+ numbered_books = []
+ dated_books = []
+ unsortable_books = []
+ for work_id in set(editions_of_work):
+ result = (
+ results.filter(parent_work=work_id)
+ .order_by("-edition_rank")
+ .first()
+ )
+ if result.series_number:
+ numbered_books.append(result)
+ elif result.first_published_date or result.published_date:
+ dated_books.append(result)
+ else:
+ unsortable_books.append(result)
+
+ list_results = (
+ sorted(numbered_books, key=lambda book: book.series_number) +
+ sorted(dated_books, key=lambda book: book.first_published_date if book.first_published_date else book.published_date) +
+ sorted(unsortable_books, key=lambda book: book.sort_title)
+ )
+
+ data = {
+ "series_name": series_name,
+ "author": author,
+ "books": list_results,
+ }
+
+ return TemplateResponse(request, "book/series.html", data)
From cd13e6f523fc38e703631b21570fb9cc79bb163a Mon Sep 17 00:00:00 2001
From: Dustin Steiner
Date: Tue, 24 Jan 2023 13:14:06 +0000
Subject: [PATCH 13/63] style: run linter
---
bookwyrm/urls.py | 6 ++-
bookwyrm/views/books/series.py | 93 +++++++++++++++++-----------------
2 files changed, 52 insertions(+), 47 deletions(-)
diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py
index 6d9f34de00..c4d6b9b45a 100644
--- a/bookwyrm/urls.py
+++ b/bookwyrm/urls.py
@@ -610,7 +610,11 @@
# books
re_path(rf"{BOOK_PATH}(.json)?/?$", views.Book.as_view(), name="book"),
re_path(rf"{BOOK_PATH}{regex.SLUG}/?$", views.Book.as_view(), name="book"),
- re_path(r"^series/by/(?P\d+)/?$", views.BookSeriesBy.as_view(), name="book-series-by"),
+ re_path(
+ r"^series/by/(?P\d+)/?$",
+ views.BookSeriesBy.as_view(),
+ name="book-series-by",
+ ),
re_path(
rf"{BOOK_PATH}/(?Preview|comment|quote)/?$",
views.Book.as_view(),
diff --git a/bookwyrm/views/books/series.py b/bookwyrm/views/books/series.py
index 6085934d35..f573aac572 100644
--- a/bookwyrm/views/books/series.py
+++ b/bookwyrm/views/books/series.py
@@ -10,49 +10,50 @@
# pylint: disable=no-self-use
class BookSeriesBy(View):
- def get(self, request, author_id, **kwargs):
- """lists all books in a series"""
- series_name = request.GET.get("series_name")
-
- if is_api_request(request):
- pass
-
- author = get_object_or_404(models.Author, id=author_id)
-
- results = (
- models.Edition.objects.filter(authors=author, series=series_name)
- )
-
- # when there are multiple editions of the same work, pick the closest
- editions_of_work = results.values_list("parent_work__id", flat=True).distinct()
-
- # filter out multiple editions of the same work
- numbered_books = []
- dated_books = []
- unsortable_books = []
- for work_id in set(editions_of_work):
- result = (
- results.filter(parent_work=work_id)
- .order_by("-edition_rank")
- .first()
- )
- if result.series_number:
- numbered_books.append(result)
- elif result.first_published_date or result.published_date:
- dated_books.append(result)
- else:
- unsortable_books.append(result)
-
- list_results = (
- sorted(numbered_books, key=lambda book: book.series_number) +
- sorted(dated_books, key=lambda book: book.first_published_date if book.first_published_date else book.published_date) +
- sorted(unsortable_books, key=lambda book: book.sort_title)
- )
-
- data = {
- "series_name": series_name,
- "author": author,
- "books": list_results,
- }
-
- return TemplateResponse(request, "book/series.html", data)
+ def get(self, request, author_id, **kwargs):
+ """lists all books in a series"""
+ series_name = request.GET.get("series_name")
+
+ if is_api_request(request):
+ pass
+
+ author = get_object_or_404(models.Author, id=author_id)
+
+ results = models.Edition.objects.filter(authors=author, series=series_name)
+
+ # when there are multiple editions of the same work, pick the closest
+ editions_of_work = results.values_list("parent_work__id", flat=True).distinct()
+
+ # filter out multiple editions of the same work
+ numbered_books = []
+ dated_books = []
+ unsortable_books = []
+ for work_id in set(editions_of_work):
+ result = (
+ results.filter(parent_work=work_id).order_by("-edition_rank").first()
+ )
+ if result.series_number:
+ numbered_books.append(result)
+ elif result.first_published_date or result.published_date:
+ dated_books.append(result)
+ else:
+ unsortable_books.append(result)
+
+ list_results = (
+ sorted(numbered_books, key=lambda book: book.series_number)
+ + sorted(
+ dated_books,
+ key=lambda book: book.first_published_date
+ if book.first_published_date
+ else book.published_date,
+ )
+ + sorted(unsortable_books, key=lambda book: book.sort_title)
+ )
+
+ data = {
+ "series_name": series_name,
+ "author": author,
+ "books": list_results,
+ }
+
+ return TemplateResponse(request, "book/series.html", data)
From 821169251ceada45de45d4cf31d0fbf9f99ea805 Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Thu, 26 Jan 2023 17:19:44 +1100
Subject: [PATCH 14/63] add more verbose comment to settings.py
---
bookwyrm/settings.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index ed1447b05d..cd63ebb0df 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -371,5 +371,8 @@
if HTTP_X_FORWARDED_PROTO:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
-# AUTHORIZED_FETCH Instance Actor
+# Instance Actor for signing GET requests to "secure mode"
+# Mastodon servers.
+# Do not change this setting unless you already have an existing
+# user with the same username - in which case you should change it!
INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor"
From 63dafd54d30108ce95a2bfebbc3045344aabbcfc Mon Sep 17 00:00:00 2001
From: Hugh Rundle
Date: Thu, 26 Jan 2023 17:24:51 +1100
Subject: [PATCH 15/63] black
I can't even tell what it thinks it did, but Black likes to complain.
---
bookwyrm/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index cd63ebb0df..5cbb4b1e4b 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -371,7 +371,7 @@
if HTTP_X_FORWARDED_PROTO:
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
-# Instance Actor for signing GET requests to "secure mode"
+# Instance Actor for signing GET requests to "secure mode"
# Mastodon servers.
# Do not change this setting unless you already have an existing
# user with the same username - in which case you should change it!
From eb4672ff18697ef1602cd21b2ccc40e33ce225d9 Mon Sep 17 00:00:00 2001
From: Dustin Steiner
Date: Thu, 26 Jan 2023 06:49:55 +0000
Subject: [PATCH 16/63] style: format
---
bookwyrm/views/books/series.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/views/books/series.py b/bookwyrm/views/books/series.py
index f573aac572..14a2a82c5d 100644
--- a/bookwyrm/views/books/series.py
+++ b/bookwyrm/views/books/series.py
@@ -5,12 +5,13 @@
from bookwyrm.views.helpers import is_api_request
from bookwyrm import models
-from bookwyrm.settings import PAGE_LENGTH
# pylint: disable=no-self-use
class BookSeriesBy(View):
- def get(self, request, author_id, **kwargs):
+ """book series by author"""
+
+ def get(self, request, author_id):
"""lists all books in a series"""
series_name = request.GET.get("series_name")
From 9be2f00064c0242c055432f36d1222548f291af0 Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 26 Jan 2023 07:19:53 -0800
Subject: [PATCH 17/63] Update test_signing.py
---
bookwyrm/tests/test_signing.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py
index 9c50980187..8a7f65249f 100644
--- a/bookwyrm/tests/test_signing.py
+++ b/bookwyrm/tests/test_signing.py
@@ -89,6 +89,7 @@ def send_test_request( # pylint: disable=too-many-arguments
signature = make_signature(
"post", signer or sender, self.rat.inbox, now, digest
)
+ with patch("bookwyrm.views.inbox.activity_task.apply_async"):
with patch("bookwyrm.models.user.set_remote_server.delay"):
return self.send(signature, now, send_data or data, digest)
From 7c75c246d24fe5514b4e0864f4b904641ba9fd6a Mon Sep 17 00:00:00 2001
From: Jascha Urbach
Date: Thu, 26 Jan 2023 16:51:32 +0100
Subject: [PATCH 18/63] Update requirements.txt
Important bugfixes and performance updates.
did not touch opentelemetry or the dev dependencies.
No breaking changes.
---
requirements.txt | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index be223f5cd9..e71fb52c84 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,24 +4,24 @@ celery==5.2.7
colorthief==0.2.1
Django==3.2.16
django-celery-beat==2.4.0
-django-compressor==4.1
+django-compressor==4.3.1
django-imagekit==4.1.0
-django-model-utils==4.2.0
+django-model-utils==4.3.1
django-sass-processor==1.2.2
environs==9.5.0
flower==1.2.0
libsass==0.22.0
-Markdown==3.3.3
-Pillow>=9.3.0
+Markdown==3.4.1
+Pillow==9.4.0
psycopg2==2.9.5
pycryptodome==3.16.0
python-dateutil==2.8.2
redis==3.4.1
-requests==2.28.1
+requests==2.28.2
responses==0.22.0
pytz>=2022.7
-boto3==1.26.32
-django-storages==1.11.1
+boto3==1.26.57
+django-storages==1.13.2
django-redis==5.2.0
opentelemetry-api==1.11.1
opentelemetry-exporter-otlp-proto-grpc==1.11.1
@@ -29,7 +29,7 @@ opentelemetry-instrumentation-celery==0.30b1
opentelemetry-instrumentation-django==0.30b1
opentelemetry-sdk==1.11.1
protobuf==3.20.*
-pyotp==2.6.0
+pyotp==2.8.0
qrcode==7.3.1
# Dev
From ef481498440d8dbcb064b7f7463640017769a76b Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 26 Jan 2023 07:52:37 -0800
Subject: [PATCH 19/63] Show import queue in Celery admin
---
bookwyrm/templates/settings/celery.html | 12 +++++++++---
bookwyrm/views/admin/celery_status.py | 1 +
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/bookwyrm/templates/settings/celery.html b/bookwyrm/templates/settings/celery.html
index b2bd95601a..7ce6553fb8 100644
--- a/bookwyrm/templates/settings/celery.html
+++ b/bookwyrm/templates/settings/celery.html
@@ -13,24 +13,30 @@
{% trans "Queues" %}
-
+
{{ queues.low_priority|intcomma }}
-
+
{{ queues.medium_priority|intcomma }}
-
+
{{ queues.high_priority|intcomma }}
+
+
+
+
{{ queues.imports|intcomma }}
+
+
{% else %}
diff --git a/bookwyrm/views/admin/celery_status.py b/bookwyrm/views/admin/celery_status.py
index 5221fd6190..0e88f55f1f 100644
--- a/bookwyrm/views/admin/celery_status.py
+++ b/bookwyrm/views/admin/celery_status.py
@@ -36,6 +36,7 @@ def get(self, request):
"low_priority": r.llen("low_priority"),
"medium_priority": r.llen("medium_priority"),
"high_priority": r.llen("high_priority"),
+ "imports": r.llen("imports"),
}
# pylint: disable=broad-except
except Exception as err:
From 80d3474cef27e33fda27d6c2b3ddaa595054de0f Mon Sep 17 00:00:00 2001
From: Christof Dorner
Date: Thu, 26 Jan 2023 17:22:53 +0100
Subject: [PATCH 20/63] Quote redis activity password
Same as the broker password in celerywyrm/settings.py
---
bookwyrm/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 99aae96948..88fed9c9e7 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -205,7 +205,7 @@
# redis/activity streams settings
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
-REDIS_ACTIVITY_PASSWORD = env("REDIS_ACTIVITY_PASSWORD", None)
+REDIS_ACTIVITY_PASSWORD = requests.utils.quote(env("REDIS_ACTIVITY_PASSWORD", None))
REDIS_ACTIVITY_DB_INDEX = env("REDIS_ACTIVITY_DB_INDEX", 0)
REDIS_ACTIVITY_URL = env(
"REDIS_ACTIVITY_URL",
From c26387baea7e7b81e1b8acdf582615f36b1e33d9 Mon Sep 17 00:00:00 2001
From: Jascha Urbach
Date: Thu, 26 Jan 2023 17:24:18 +0100
Subject: [PATCH 21/63] add "import" to celery worker
import was missing in ExecStart for celery
---
contrib/systemd/bookwyrm-worker.service | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/contrib/systemd/bookwyrm-worker.service b/contrib/systemd/bookwyrm-worker.service
index 7e25b0b17d..d79fdabf64 100644
--- a/contrib/systemd/bookwyrm-worker.service
+++ b/contrib/systemd/bookwyrm-worker.service
@@ -6,7 +6,7 @@ After=network.target postgresql.service redis.service
User=bookwyrm
Group=bookwyrm
WorkingDirectory=/opt/bookwyrm/
-ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority
+ExecStart=/opt/bookwyrm/venv/bin/celery -A celerywyrm worker -l info -Q high_priority,medium_priority,low_priority,import
StandardOutput=journal
StandardError=inherit
From afab255c851ca58fe54b04ab60118f990e910c23 Mon Sep 17 00:00:00 2001
From: Christof Dorner
Date: Thu, 26 Jan 2023 17:18:15 +0100
Subject: [PATCH 22/63] Allow empty broker and activity redis password
---
bookwyrm/settings.py | 2 +-
celerywyrm/settings.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 88fed9c9e7..6fbfd7e71f 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -205,7 +205,7 @@
# redis/activity streams settings
REDIS_ACTIVITY_HOST = env("REDIS_ACTIVITY_HOST", "localhost")
REDIS_ACTIVITY_PORT = env("REDIS_ACTIVITY_PORT", 6379)
-REDIS_ACTIVITY_PASSWORD = requests.utils.quote(env("REDIS_ACTIVITY_PASSWORD", None))
+REDIS_ACTIVITY_PASSWORD = requests.utils.quote(env("REDIS_ACTIVITY_PASSWORD", ""))
REDIS_ACTIVITY_DB_INDEX = env("REDIS_ACTIVITY_DB_INDEX", 0)
REDIS_ACTIVITY_URL = env(
"REDIS_ACTIVITY_URL",
diff --git a/celerywyrm/settings.py b/celerywyrm/settings.py
index 7519ea6e36..5f07a7839a 100644
--- a/celerywyrm/settings.py
+++ b/celerywyrm/settings.py
@@ -4,7 +4,7 @@
from bookwyrm.settings import *
# pylint: disable=line-too-long
-REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None))
+REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", ""))
REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")
REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379)
REDIS_BROKER_DB_INDEX = env("REDIS_BROKER_DB_INDEX", 0)
From a9142d98584607eb112deb2127b05e94a7b6d99f Mon Sep 17 00:00:00 2001
From: Mouse Reeve
Date: Thu, 26 Jan 2023 08:55:52 -0800
Subject: [PATCH 23/63] Updates locales
---
locale/ca_ES/LC_MESSAGES/django.mo | Bin 140331 -> 140453 bytes
locale/ca_ES/LC_MESSAGES/django.po | 181 +++++++++--------
locale/de_DE/LC_MESSAGES/django.mo | Bin 140843 -> 140960 bytes
locale/de_DE/LC_MESSAGES/django.po | 183 ++++++++++--------
locale/en_US/LC_MESSAGES/django.po | 279 ++++++++++++++++++---------
locale/es_ES/LC_MESSAGES/django.mo | Bin 138666 -> 139970 bytes
locale/es_ES/LC_MESSAGES/django.po | 217 +++++++++++----------
locale/eu_ES/LC_MESSAGES/django.mo | Bin 140834 -> 140994 bytes
locale/eu_ES/LC_MESSAGES/django.po | 269 ++++++++++++++------------
locale/fi_FI/LC_MESSAGES/django.mo | Bin 137876 -> 137990 bytes
locale/fi_FI/LC_MESSAGES/django.po | 183 ++++++++++--------
locale/fr_FR/LC_MESSAGES/django.mo | Bin 143532 -> 143662 bytes
locale/fr_FR/LC_MESSAGES/django.po | 181 +++++++++--------
locale/gl_ES/LC_MESSAGES/django.mo | Bin 136804 -> 136926 bytes
locale/gl_ES/LC_MESSAGES/django.po | 181 +++++++++--------
locale/it_IT/LC_MESSAGES/django.mo | Bin 138769 -> 139068 bytes
locale/it_IT/LC_MESSAGES/django.po | 183 ++++++++++--------
locale/lt_LT/LC_MESSAGES/django.mo | Bin 137410 -> 137410 bytes
locale/lt_LT/LC_MESSAGES/django.po | 181 +++++++++--------
locale/no_NO/LC_MESSAGES/django.mo | Bin 75787 -> 75787 bytes
locale/no_NO/LC_MESSAGES/django.po | 181 +++++++++--------
locale/pl_PL/LC_MESSAGES/django.mo | Bin 125218 -> 125218 bytes
locale/pl_PL/LC_MESSAGES/django.po | 181 +++++++++--------
locale/pt_BR/LC_MESSAGES/django.mo | Bin 85437 -> 85437 bytes
locale/pt_BR/LC_MESSAGES/django.po | 181 +++++++++--------
locale/pt_PT/LC_MESSAGES/django.mo | Bin 80391 -> 80391 bytes
locale/pt_PT/LC_MESSAGES/django.po | 181 +++++++++--------
locale/ro_RO/LC_MESSAGES/django.mo | Bin 124721 -> 124721 bytes
locale/ro_RO/LC_MESSAGES/django.po | 181 +++++++++--------
locale/sv_SE/LC_MESSAGES/django.mo | Bin 132075 -> 132075 bytes
locale/sv_SE/LC_MESSAGES/django.po | 181 +++++++++--------
locale/zh_Hans/LC_MESSAGES/django.mo | Bin 44096 -> 82134 bytes
locale/zh_Hans/LC_MESSAGES/django.po | 181 +++++++++--------
locale/zh_Hant/LC_MESSAGES/django.mo | Bin 38839 -> 32619 bytes
locale/zh_Hant/LC_MESSAGES/django.po | 181 +++++++++--------
35 files changed, 1901 insertions(+), 1585 deletions(-)
diff --git a/locale/ca_ES/LC_MESSAGES/django.mo b/locale/ca_ES/LC_MESSAGES/django.mo
index 2d41a99f353915815b870ab9fdcd9f2874a6636f..25ac305b5401032a8e7950ef6aa5d651616be7d3 100644
GIT binary patch
delta 28797
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
W*{NCQRz0Tg_3;T@w#3IL)&BtYu7`>M
diff --git a/locale/ca_ES/LC_MESSAGES/django.po b/locale/ca_ES/LC_MESSAGES/django.po
index cd2bb844bc..de12eac85c 100644
--- a/locale/ca_ES/LC_MESSAGES/django.po
+++ b/locale/ca_ES/LC_MESSAGES/django.po
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-12-20 00:41+0000\n"
-"PO-Revision-Date: 2023-01-11 17:07\n"
+"POT-Creation-Date: 2023-01-11 22:46+0000\n"
+"PO-Revision-Date: 2023-01-13 10:51\n"
"Last-Translator: Mouse Reeve \n"
"Language-Team: Catalan\n"
"Language: ca\n"
@@ -171,23 +171,23 @@ msgstr "Eliminació pel moderador"
msgid "Domain block"
msgstr "Bloqueig de domini"
-#: bookwyrm/models/book.py:277
+#: bookwyrm/models/book.py:272
msgid "Audiobook"
msgstr "Audiollibre"
-#: bookwyrm/models/book.py:278
+#: bookwyrm/models/book.py:273
msgid "eBook"
msgstr "Llibre electrònic"
-#: bookwyrm/models/book.py:279
+#: bookwyrm/models/book.py:274
msgid "Graphic novel"
msgstr "Novel·la gràfica"
-#: bookwyrm/models/book.py:280
+#: bookwyrm/models/book.py:275
msgid "Hardcover"
msgstr "Tapa dura"
-#: bookwyrm/models/book.py:281
+#: bookwyrm/models/book.py:276
msgid "Paperback"
msgstr "Edició de butxaca"
@@ -215,7 +215,7 @@ msgstr "%(value)s no és una remote_id vàlida"
msgid "%(value)s is not a valid username"
msgstr "%(value)s no és un nom d'usuari vàlid"
-#: bookwyrm/models/fields.py:182 bookwyrm/templates/layout.html:142
+#: bookwyrm/models/fields.py:182 bookwyrm/templates/layout.html:131
#: bookwyrm/templates/ostatus/error.html:29
msgid "username"
msgstr "nom d'usuari"
@@ -353,54 +353,58 @@ msgid "Español (Spanish)"
msgstr "Español (espanyol)"
#: bookwyrm/settings.py:291
+msgid "Euskara (Basque)"
+msgstr "Euskera (Basc)"
+
+#: bookwyrm/settings.py:292
msgid "Galego (Galician)"
msgstr "Galego (gallec)"
-#: bookwyrm/settings.py:292
+#: bookwyrm/settings.py:293
msgid "Italiano (Italian)"
msgstr "Italiano (italià)"
-#: bookwyrm/settings.py:293
+#: bookwyrm/settings.py:294
msgid "Suomi (Finnish)"
msgstr "Suomi (finès)"
-#: bookwyrm/settings.py:294
+#: bookwyrm/settings.py:295
msgid "Français (French)"
msgstr "Français (francès)"
-#: bookwyrm/settings.py:295
+#: bookwyrm/settings.py:296
msgid "Lietuvių (Lithuanian)"
msgstr "Lietuvių (Lituà)"
-#: bookwyrm/settings.py:296
+#: bookwyrm/settings.py:297
msgid "Norsk (Norwegian)"
msgstr "Norsk (noruec)"
-#: bookwyrm/settings.py:297
+#: bookwyrm/settings.py:298
msgid "Polski (Polish)"
msgstr "Polski (polonès)"
-#: bookwyrm/settings.py:298
+#: bookwyrm/settings.py:299
msgid "Português do Brasil (Brazilian Portuguese)"
msgstr "Português do Brasil (portuguès del Brasil)"
-#: bookwyrm/settings.py:299
+#: bookwyrm/settings.py:300
msgid "Português Europeu (European Portuguese)"
msgstr "Português Europeu (Portuguès europeu)"
-#: bookwyrm/settings.py:300
+#: bookwyrm/settings.py:301
msgid "Română (Romanian)"
msgstr "Română (romanès)"
-#: bookwyrm/settings.py:301
+#: bookwyrm/settings.py:302
msgid "Svenska (Swedish)"
msgstr "Svenska (suec)"
-#: bookwyrm/settings.py:302
+#: bookwyrm/settings.py:303
msgid "简体中文 (Simplified Chinese)"
msgstr "简体中文 (xinès simplificat)"
-#: bookwyrm/settings.py:303
+#: bookwyrm/settings.py:304
msgid "繁體中文 (Traditional Chinese)"
msgstr "繁體中文 (xinès tradicional)"
@@ -429,54 +433,54 @@ msgstr "Malauradament quelcom ha anat malament."
msgid "About"
msgstr "Sobre nosaltres "
-#: bookwyrm/templates/about/about.html:20
+#: bookwyrm/templates/about/about.html:21
#: bookwyrm/templates/get_started/layout.html:20
#, python-format
msgid "Welcome to %(site_name)s!"
msgstr "Benvingut a %(site_name)s!"
-#: bookwyrm/templates/about/about.html:24
+#: bookwyrm/templates/about/about.html:25
#, python-format
msgid "%(site_name)s is part of BookWyrm, a network of independent, self-directed communities for readers. While you can interact seamlessly with users anywhere in the BookWyrm network, this community is unique."
msgstr "%(site_name)s és part de BookWyrm, una xarxa de comunitats independents i autogestionades per a lectors. Tot i que pots interactuar sense problemes amb els usuaris de qualsevol part de la xarxa BookWyrm, aquesta comunitat és única."
-#: bookwyrm/templates/about/about.html:44
+#: bookwyrm/templates/about/about.html:45
#, python-format
msgid "%(title)s is %(site_name)s's most beloved book, with an average rating of %(rating)s out of 5."
msgstr "%(title)s és el llibre preferit de %(site_name)s, amb una valoració promig de %(rating)s sobre 5."
-#: bookwyrm/templates/about/about.html:63
+#: bookwyrm/templates/about/about.html:64
#, python-format
msgid "More %(site_name)s users want to read %(title)s than any other book."
msgstr "El llibre que volen llegir la majoria de membres de %(site_name)s és %(title)s."
-#: bookwyrm/templates/about/about.html:82
+#: bookwyrm/templates/about/about.html:83
#, python-format
msgid "%(title)s has the most divisive ratings of any book on %(site_name)s."
msgstr "%(title)s es el llibre amb valoracions més dispars a %(site_name)s."
-#: bookwyrm/templates/about/about.html:93
+#: bookwyrm/templates/about/about.html:94
msgid "Track your reading, talk about books, write reviews, and discover what to read next. Always ad-free, anti-corporate, and community-oriented, BookWyrm is human-scale software, designed to stay small and personal. If you have feature requests, bug reports, or grand dreams, reach out and make yourself heard."
msgstr "Feu seguiment de les vostres lectures, parleu sobre llibres, escriviu valoracions i descobriu properes lectures. Bookwyrm és un programari d'escala humana, dissenyat per a ser petit i personal i amb el compromís de no tenir mai publicitat, ser anticorporatiu i orientat a la comunitat. Si teniu suggeriments de característiques noves, informes d'errors o grans projectes, code of conduct, and respond when users report spam and bad behavior."
msgstr "Les persones moderadores i administradores de %(site_name)s mantenen en funcionament aquest lloc, fan complir el codi de conducta i responen els informes de brossa i mal comportament que puguin enviar els usuaris."
-#: bookwyrm/templates/about/about.html:121
+#: bookwyrm/templates/about/about.html:122
msgid "Moderator"
msgstr "Moderació"
-#: bookwyrm/templates/about/about.html:123 bookwyrm/templates/user_menu.html:63
+#: bookwyrm/templates/about/about.html:124 bookwyrm/templates/user_menu.html:67
msgid "Admin"
msgstr "Administració"
-#: bookwyrm/templates/about/about.html:139
+#: bookwyrm/templates/about/about.html:140
#: bookwyrm/templates/settings/users/user_moderation_actions.html:14
#: bookwyrm/templates/snippets/status/status_options.html:35
#: bookwyrm/templates/snippets/user_options.html:14
@@ -696,44 +700,48 @@ msgid "Wikipedia"
msgstr "Vikipèdia"
#: bookwyrm/templates/author/author.html:79
+msgid "Website"
+msgstr "Pàgina web"
+
+#: bookwyrm/templates/author/author.html:87
msgid "View ISNI record"
msgstr "Veure el registre ISNI"
-#: bookwyrm/templates/author/author.html:87
+#: bookwyrm/templates/author/author.html:95
#: bookwyrm/templates/book/book.html:164
msgid "View on ISFDB"
msgstr "Veure a ISFDB"
-#: bookwyrm/templates/author/author.html:92
+#: bookwyrm/templates/author/author.html:100
#: bookwyrm/templates/author/sync_modal.html:5
#: bookwyrm/templates/book/book.html:131
#: bookwyrm/templates/book/sync_modal.html:5
msgid "Load data"
msgstr "Carregueu dades"
-#: bookwyrm/templates/author/author.html:96
+#: bookwyrm/templates/author/author.html:104
#: bookwyrm/templates/book/book.html:135
msgid "View on OpenLibrary"
msgstr "Veure a OpenLibrary"
-#: bookwyrm/templates/author/author.html:111
+#: bookwyrm/templates/author/author.html:119
#: bookwyrm/templates/book/book.html:149
msgid "View on Inventaire"
msgstr "Veure a Inventaire"
-#: bookwyrm/templates/author/author.html:127
+#: bookwyrm/templates/author/author.html:135
msgid "View on LibraryThing"
msgstr "Veure a LibraryThing"
-#: bookwyrm/templates/author/author.html:135
+#: bookwyrm/templates/author/author.html:143
msgid "View on Goodreads"
msgstr "Veure a Goodreads"
-#: bookwyrm/templates/author/author.html:143
+#: bookwyrm/templates/author/author.html:151
msgid "View ISFDB entry"
msgstr "Veure entrada a ISFDB"
-#: bookwyrm/templates/author/author.html:158
+#: bookwyrm/templates/author/author.html:166
#, python-format
msgid "Books by %(name)s"
msgstr "Llibres de %(name)s"
@@ -783,45 +791,49 @@ msgstr "Biografia:"
msgid "Wikipedia link:"
msgstr "Enllaç de Viquipèdia:"
-#: bookwyrm/templates/author/edit_author.html:61
+#: bookwyrm/templates/author/edit_author.html:60
+msgid "Website:"
+msgstr "Pàgina web:"
+
+#: bookwyrm/templates/author/edit_author.html:65
msgid "Birth date:"
msgstr "Data de naixement:"
-#: bookwyrm/templates/author/edit_author.html:68
+#: bookwyrm/templates/author/edit_author.html:72
msgid "Death date:"
msgstr "Data de defunció:"
-#: bookwyrm/templates/author/edit_author.html:75
+#: bookwyrm/templates/author/edit_author.html:79
msgid "Author Identifiers"
msgstr "Identificadors de l'autoria"
-#: bookwyrm/templates/author/edit_author.html:77
+#: bookwyrm/templates/author/edit_author.html:81
msgid "Openlibrary key:"
msgstr "Clau d'OpenLibrary:"
-#: bookwyrm/templates/author/edit_author.html:84
+#: bookwyrm/templates/author/edit_author.html:88
#: bookwyrm/templates/book/edit/edit_book_form.html:323
msgid "Inventaire ID:"
msgstr "ID a l'Inventaire:"
-#: bookwyrm/templates/author/edit_author.html:91
+#: bookwyrm/templates/author/edit_author.html:95
msgid "Librarything key:"
msgstr "Clau de Librarything:"
-#: bookwyrm/templates/author/edit_author.html:98
+#: bookwyrm/templates/author/edit_author.html:102
#: bookwyrm/templates/book/edit/edit_book_form.html:332
msgid "Goodreads key:"
msgstr "Identificador a Goodreads:"
-#: bookwyrm/templates/author/edit_author.html:105
+#: bookwyrm/templates/author/edit_author.html:109
msgid "ISFDB:"
msgstr "ISFDB:"
-#: bookwyrm/templates/author/edit_author.html:112
+#: bookwyrm/templates/author/edit_author.html:116
msgid "ISNI:"
msgstr "ISNI:"
-#: bookwyrm/templates/author/edit_author.html:122
+#: bookwyrm/templates/author/edit_author.html:126
#: bookwyrm/templates/book/book.html:209
#: bookwyrm/templates/book/edit/edit_book.html:142
#: bookwyrm/templates/book/file_links/add_link_modal.html:60
@@ -844,7 +856,7 @@ msgstr "ISNI:"
msgid "Save"
msgstr "Desa"
-#: bookwyrm/templates/author/edit_author.html:123
+#: bookwyrm/templates/author/edit_author.html:127
#: bookwyrm/templates/author/sync_modal.html:23
#: bookwyrm/templates/book/book.html:210
#: bookwyrm/templates/book/cover_add_modal.html:33
@@ -974,7 +986,7 @@ msgstr "Llocs"
#: bookwyrm/templates/guided_tour/lists.html:14
#: bookwyrm/templates/guided_tour/user_books.html:102
#: bookwyrm/templates/guided_tour/user_profile.html:78
-#: bookwyrm/templates/layout.html:102 bookwyrm/templates/lists/curate.html:8
+#: bookwyrm/templates/layout.html:91 bookwyrm/templates/lists/curate.html:8
#: bookwyrm/templates/lists/list.html:12 bookwyrm/templates/lists/lists.html:5
#: bookwyrm/templates/lists/lists.html:12
#: bookwyrm/templates/search/layout.html:26
@@ -1104,7 +1116,7 @@ msgid "This is a new work"
msgstr "Es tracta d'una publicació nova"
#: bookwyrm/templates/book/edit/edit_book.html:131
-#: bookwyrm/templates/feed/status.html:21
+#: bookwyrm/templates/feed/status.html:19
#: bookwyrm/templates/guided_tour/book.html:44
#: bookwyrm/templates/guided_tour/book.html:68
#: bookwyrm/templates/guided_tour/book.html:91
@@ -1148,6 +1160,7 @@ msgstr "Es tracta d'una publicació nova"
#: bookwyrm/templates/guided_tour/user_profile.html:89
#: bookwyrm/templates/guided_tour/user_profile.html:112
#: bookwyrm/templates/guided_tour/user_profile.html:135
+#: bookwyrm/templates/user_menu.html:18
msgid "Back"
msgstr "Enrere"
@@ -1531,7 +1544,7 @@ msgstr "Comunitat federada"
#: bookwyrm/templates/directory/directory.html:4
#: bookwyrm/templates/directory/directory.html:9
-#: bookwyrm/templates/user_menu.html:30
+#: bookwyrm/templates/user_menu.html:34
msgid "Directory"
msgstr "Directori"
@@ -1651,7 +1664,7 @@ msgstr "%(username)s ha citat %(username)s"
msgstr "Missatges Directes amb %(username)s"
#: bookwyrm/templates/feed/direct_messages.html:10
-#: bookwyrm/templates/user_menu.html:40
+#: bookwyrm/templates/user_menu.html:44
msgid "Direct Messages"
msgstr "Missatges directes"
@@ -1844,7 +1857,7 @@ msgstr "Actualitzacions"
#: bookwyrm/templates/feed/suggested_books.html:6
#: bookwyrm/templates/guided_tour/home.html:127
-#: bookwyrm/templates/user_menu.html:35
+#: bookwyrm/templates/user_menu.html:39
msgid "Your Books"
msgstr "Els teus llibres"
@@ -1905,7 +1918,7 @@ msgstr "Lectures actuals"
#: bookwyrm/templates/get_started/book_preview.html:12
#: bookwyrm/templates/shelf/shelf.html:88
-#: bookwyrm/templates/snippets/shelf_selector.html:47
+#: bookwyrm/templates/snippets/shelf_selector.html:46
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:24
#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:12
#: bookwyrm/templates/user/user.html:35 bookwyrm/templatetags/shelf_tags.py:52
@@ -1922,7 +1935,7 @@ msgid "What are you reading?"
msgstr "Què estas llegint?"
#: bookwyrm/templates/get_started/books.html:9
-#: bookwyrm/templates/layout.html:49 bookwyrm/templates/lists/list.html:213
+#: bookwyrm/templates/layout.html:38 bookwyrm/templates/lists/list.html:213
msgid "Search for a book"
msgstr "Cerqueu un llibre"
@@ -1941,8 +1954,8 @@ msgstr "Podràs afegir llibres quan comencis a usar %(site_name)s."
#: bookwyrm/templates/get_started/users.html:18
#: bookwyrm/templates/get_started/users.html:19
#: bookwyrm/templates/groups/members.html:15
-#: bookwyrm/templates/groups/members.html:16 bookwyrm/templates/layout.html:55
-#: bookwyrm/templates/layout.html:56 bookwyrm/templates/lists/list.html:217
+#: bookwyrm/templates/groups/members.html:16 bookwyrm/templates/layout.html:44
+#: bookwyrm/templates/layout.html:45 bookwyrm/templates/lists/list.html:217
#: bookwyrm/templates/search/layout.html:5
#: bookwyrm/templates/search/layout.html:10
msgid "Search"
@@ -2420,8 +2433,8 @@ msgid "The bell will light up when you have a new notification. When it does, cl
msgstr "La campana s'il·luminarà quan tinguis una nova notificació. Clica-la per descobrir què hi ha de nou!"
#: bookwyrm/templates/guided_tour/home.html:177
-#: bookwyrm/templates/layout.html:86 bookwyrm/templates/layout.html:118
-#: bookwyrm/templates/layout.html:119
+#: bookwyrm/templates/layout.html:75 bookwyrm/templates/layout.html:107
+#: bookwyrm/templates/layout.html:108
#: bookwyrm/templates/notifications/notifications_page.html:5
#: bookwyrm/templates/notifications/notifications_page.html:10
msgid "Notifications"
@@ -2993,7 +3006,7 @@ msgid "Login"
msgstr "Inicia la sessió"
#: bookwyrm/templates/landing/login.html:7
-#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:150
+#: bookwyrm/templates/landing/login.html:36 bookwyrm/templates/layout.html:139
#: bookwyrm/templates/ostatus/error.html:37
msgid "Log in"
msgstr "Inicia la sessió"
@@ -3004,7 +3017,7 @@ msgstr "L'adreça de correu electrònic ha estat confirmada amb èxit!"
#: bookwyrm/templates/landing/login.html:21
#: bookwyrm/templates/landing/reactivate.html:17
-#: bookwyrm/templates/layout.html:141 bookwyrm/templates/ostatus/error.html:28
+#: bookwyrm/templates/layout.html:130 bookwyrm/templates/ostatus/error.html:28
#: bookwyrm/templates/snippets/register_form.html:4
msgid "Username:"
msgstr "Nom d'usuari:"
@@ -3012,13 +3025,13 @@ msgstr "Nom d'usuari:"
#: bookwyrm/templates/landing/login.html:27
#: bookwyrm/templates/landing/password_reset.html:26
#: bookwyrm/templates/landing/reactivate.html:23
-#: bookwyrm/templates/layout.html:145 bookwyrm/templates/ostatus/error.html:32
+#: bookwyrm/templates/layout.html:134 bookwyrm/templates/ostatus/error.html:32
#: bookwyrm/templates/preferences/2fa.html:91
#: bookwyrm/templates/snippets/register_form.html:45
msgid "Password:"
msgstr "Contrasenya:"
-#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:147
+#: bookwyrm/templates/landing/login.html:39 bookwyrm/templates/layout.html:136
#: bookwyrm/templates/ostatus/error.html:34
msgid "Forgot your password?"
msgstr "Has oblidat la teva contrasenya?"
@@ -3061,35 +3074,35 @@ msgstr "Reactiva el compte"
msgid "%(site_name)s search"
msgstr "%(site_name)s cerca"
-#: bookwyrm/templates/layout.html:47
+#: bookwyrm/templates/layout.html:36
msgid "Search for a book, user, or list"
msgstr "Cerca un llibre, un usuari o una llista"
-#: bookwyrm/templates/layout.html:62 bookwyrm/templates/layout.html:63
+#: bookwyrm/templates/layout.html:51 bookwyrm/templates/layout.html:52
msgid "Scan Barcode"
msgstr "Escanejeu codi de barres"
-#: bookwyrm/templates/layout.html:77
+#: bookwyrm/templates/layout.html:66
msgid "Main navigation menu"
msgstr "Menú principal"
-#: bookwyrm/templates/layout.html:99
+#: bookwyrm/templates/layout.html:88
msgid "Feed"
msgstr "Activitat"
-#: bookwyrm/templates/layout.html:146 bookwyrm/templates/ostatus/error.html:33
+#: bookwyrm/templates/layout.html:135 bookwyrm/templates/ostatus/error.html:33
msgid "password"
msgstr "contrasenya"
-#: bookwyrm/templates/layout.html:158
+#: bookwyrm/templates/layout.html:147
msgid "Join"
msgstr "Uneix-te"
-#: bookwyrm/templates/layout.html:192
+#: bookwyrm/templates/layout.html:181
msgid "Successfully posted status"
msgstr "S'ha publicat l'estat amb èxit"
-#: bookwyrm/templates/layout.html:193
+#: bookwyrm/templates/layout.html:182
msgid "Error posting status"
msgstr "Hi ha hagut un error mentre es publicava l'estat"
@@ -3922,7 +3935,7 @@ msgstr "Edita el perfil"
#: bookwyrm/templates/preferences/edit_user.html:12
#: bookwyrm/templates/preferences/edit_user.html:25
#: bookwyrm/templates/settings/users/user_info.html:7
-#: bookwyrm/templates/user_menu.html:25
+#: bookwyrm/templates/user_menu.html:29
msgid "Profile"
msgstr "Perfil"
@@ -4872,7 +4885,7 @@ msgstr "Peticions d'invitació"
#: bookwyrm/templates/settings/invites/manage_invites.html:3
#: bookwyrm/templates/settings/invites/manage_invites.html:15
#: bookwyrm/templates/settings/layout.html:42
-#: bookwyrm/templates/user_menu.html:56
+#: bookwyrm/templates/user_menu.html:60
msgid "Invites"
msgstr "Invitacions"
@@ -5572,7 +5585,7 @@ msgstr "El vostre domini sembla que no està ben configurat. No hauria d'inclour
msgid "You are running BookWyrm in production mode without https. USE_HTTPS should be enabled in production."
msgstr "Esteu executant BookWyrm en mode producció sense https. USE_HTTPS hauria d'estar activat a producció."
-#: bookwyrm/templates/setup/config.html:52 bookwyrm/templates/user_menu.html:45
+#: bookwyrm/templates/setup/config.html:52 bookwyrm/templates/user_menu.html:49
msgid "Settings"
msgstr "Configuració"
@@ -6035,7 +6048,7 @@ msgid "Stop Reading \"%(book_title)s\""
msgstr "Para de llegir \"%(book_title)s\""
#: bookwyrm/templates/snippets/reading_modals/stop_reading_modal.html:32
-#: bookwyrm/templates/snippets/shelf_selector.html:54
+#: bookwyrm/templates/snippets/shelf_selector.html:53
#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:21
msgid "Stopped reading"
msgstr "Deixat de llegir"
@@ -6085,25 +6098,25 @@ msgstr "Més informació sobre aquesta denúncia:"
msgid "Move book"
msgstr "Mou el llibre"
-#: bookwyrm/templates/snippets/shelf_selector.html:39
+#: bookwyrm/templates/snippets/shelf_selector.html:38
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:17
#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:33
msgid "Start reading"
msgstr "Comença a llegir"
-#: bookwyrm/templates/snippets/shelf_selector.html:61
+#: bookwyrm/templates/snippets/shelf_selector.html:60
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:38
#: bookwyrm/templates/snippets/shelve_button/shelve_button_options.html:55
msgid "Want to read"
msgstr "Vull llegir"
-#: bookwyrm/templates/snippets/shelf_selector.html:82
+#: bookwyrm/templates/snippets/shelf_selector.html:81
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:73
#, python-format
msgid "Remove from %(name)s"
msgstr "Elimina de %(name)s"
-#: bookwyrm/templates/snippets/shelf_selector.html:95
+#: bookwyrm/templates/snippets/shelf_selector.html:94
msgid "Remove from"
msgstr "Elimina de"
@@ -6428,7 +6441,7 @@ msgstr "No hi ha seguidors que segueixis"
msgid "View profile and more"
msgstr "Mostra el perfil i més"
-#: bookwyrm/templates/user_menu.html:78
+#: bookwyrm/templates/user_menu.html:82
msgid "Log out"
msgstr "Desconnecta"
diff --git a/locale/de_DE/LC_MESSAGES/django.mo b/locale/de_DE/LC_MESSAGES/django.mo
index b8e1869e689a3b3aebb8d5cce57704d94a3e97ea..d846c89acd27a20bcd59b80a5990500fa11e92cc 100644
GIT binary patch
delta 28796
zcmZAA1#}fx!?xi$AwYm&!3mHM0tA=fF2&v53GVKL7q{RPf)pt3?rtsaZpDhVc(MO}
z&fdJM|2Jz*ukAi_P6BOL{qDE$4?p*IfbR^4t4?&sNri_qJ5Eqc$LUl@sgCocyW^z9
zrx=R9JsgK3P9{u(wJ|kzz)Cm4B;6qjQN?!|Dthb1scU&pDAbulL{#~{3l@$n-jL%)8G6O6%F
zg#Mk91S*p-1XbV^7Vc*Qj^U{M=cpO`4rEpsf*G(cDt!T}-fq-D&tY18
zi)k?FAhU9LF$MiQm25%;YNP`(4~|Dx(bzwrto`W1(q|vyJ#0`~_DxVpZ%o&vCxvPM3gYvXfyIV@Z3>r!w(PtWRr9ywGtPVt;Ia7qKyBUt}Ju@z{#^
zPHcf`7CTNh9Eu5XE%HD)`!Or#TVl>uE7X~AXA=k}5RC&6>fy%x#49XyoYFW92jCIZ
z07@(~kK0^KO8f@)$A7RpcH)3k!`qk%v$LNWus%jd*E$Ll>G>Z|AO;yx=!eTNF0R9v
zxW~qipvqma>DO)iKKhgX6gA`bsQP{!EY(knewYTMVMa`kSujA)e+>eR#A%JOu^+0z
z5vYn&Fc!|i__zf1*lj}PAH*bh3f0a%jD_#4-%;(xT4@HB7}b6VCZK;OhXPmvRj~%D
zgGQJUJD@t6hH-EnYAcqb%6oX`lpY7Q0?AP2Ghr^wjas?Zs4eS=>Tfu@TB1k-+M9W(
zkuAd5xE3{#9jFG6V`e;uTFNh|0XVCTu~93O7}ZWF=D=L2fwVxq+9Odbc62rCA3)$H
z33>4qa!{T0Ys{fMj`@f`MVQxX~>6EL1~FP%E(kHIQ8xfCo_n
zx`e^_5H*nKo6IXaqf0;oD1lm%^4JZVVj4Vy>fj}6&%U4~9NFBp9>43lFvjE`ke
z9oMz_5g3Pf2h>*gL4R}y5l};8Q5{W19ik{y0~=5s>_-jY24leaS0S4V=Ah^Nz};B+W0M0hYzeTQ3L&iI*c*5nU%|p
zTKe*+2~WN6p%tistwRl9tIa>Ed_DhHY=K*-t$2t!jL)s_P)q#H#{G7i
zctTY9K-7%Wp=O*HwKb(}ysph}jVVd*gV}K^x+=Jzz%aakir3j=KCj!MI{1hhxwF>{
zECH&+6j%$hpvt+Z75W)femeHTUojb`*k`_=5Ly=#3i4K-A+o9<{`Wt*21~zlv$`A*y_gBjyL0q}Y&nIBGzXQ2oqAm5;)>dj1a(
zP{lK-f_JJCjk5+bq;dY(#Cve)PpN
zn1ueFiv+ZU&rmb^f@;X`n3-`BYY=J)Gue0^RJmd{y^^)AwS~14s$5^xz(?Bj>FBD%
zC;}SkD%4UR#pw7QRU!Iu^Hd~3%^;Jt0BVKGp=MYgV_+Lhg`H7nWgM#B5=@F4(GO1?
zXZ;m8Zxe2y_V}TVzr>itKVWM7j#)7E3A2Y4P=~N1>S-8)DKHW>kTo`cGisp6(0gW3
z6MJ}q^`ArFEeYyq(n;e?)QA_LmTnEIgPk`23~Gh0pda4CSojF_{&;QmJ!SSfxivM$
zCO-s&Fq>-=s-POGj~Yk|R734C4h}{QWGw3R&O|k|9W~QKsK@gp2IGCy1fucle493y(Kh0(=K#gn-s^TG3gBMVHd;|5^yhN?k8&pSMPy>i}+SCt0O{f4)
z#?q*nU%QdB$;
zHGxd1f#$&gEQCH-1=VqNR67yqYDv2j(31ACx~K-mp!RMOY6i1ydKA_sz7z-Gzo?1y
zzQ9`$-(h*|e$o6iy%TE^54mK1s_u |