Merge branch 'main' into report-actions

This commit is contained in:
Mouse Reeve 2023-07-16 07:13:42 -07:00 committed by GitHub
commit 0818d5aabb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 524 additions and 101 deletions

View file

@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- name: Install modules
run: npm install prettier
run: npm install prettier@2.5.1
- name: Run Prettier
run: npx prettier --check bookwyrm/static/js/*.js

333
FEDERATION.md Normal file
View file

@ -0,0 +1,333 @@
# Federation
BookWyrm uses the [ActivityPub](http://activitypub.rocks/) protocol to send and receive user activity between other BookWyrm instances and other services that implement ActivityPub. To handle book data, BookWyrm has a handful of extended Activity types which are not part of the standard, but are legible to other BookWyrm instances.
## Activities and Objects
### Users and relationships
User relationship interactions follow the standard ActivityPub spec.
- `Follow`: request to receive statuses from a user, and view their statuses that have followers-only privacy
- `Accept`: approves a `Follow` and finalizes the relationship
- `Reject`: denies a `Follow`
- `Block`: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile
- `Update`: updates a user's profile and settings
- `Delete`: deactivates a user
- `Undo`: reverses a `Follow` or `Block`
### Activities
- `Create/Status`: saves a new status in the database.
- `Delete/Status`: Removes a status
- `Like/Status`: Creates a favorite on the status
- `Announce/Status`: Boosts the status into the actor's timeline
- `Undo/*`,: Reverses a `Like` or `Announce`
### Collections
User's books and lists are represented by [`OrderedCollection`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection)
### Statuses
BookWyrm is focused on book reading activities - it is not a general-purpose messaging application. For this reason, BookWyrm only accepts status `Create` activities if they are:
- Direct messages (i.e., `Note`s with the privacy level `direct`, which mention a local user),
- Related to a book (of a custom status type that includes the field `inReplyToBook`),
- Replies to existing statuses saved in the database
All other statuses will be received by the instance inbox, but by design **will not be delivered to user inboxes or displayed to users**.
### Custom Object types
With the exception of `Note`, the following object types are used in Bookwyrm but are not currently provided with a custom JSON-LD `@context` extension IRI. This is likely to change in future to make them true deserialisable JSON-LD objects.
##### Note
Within BookWyrm a `Note` is constructed according to [the ActivityStreams vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note), however `Note`s can only be created as direct messages or as replies to other statuses. As mentioned above, this also applies to incoming `Note`s.
##### Review
A `Review` is a status in response to a book (indicated by the `inReplyToBook` field), which has a title, body, and numerical rating between 0 (not rated) and 5.
Example:
```json
{
"id": "https://example.net/user/library_lurker/review/2",
"type": "Review",
"published": "2023-06-30T21:43:46.013132+00:00",
"attributedTo": "https://example.net/user/library_lurker",
"content": "<p>This is an enjoyable book with great characters.</p>",
"to": ["https://example.net/user/library_lurker/followers"],
"cc": [],
"replies": {
"id": "https://example.net/user/library_lurker/review/2/replies",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://example.net/user/library_lurker/review/2/replies?page=1",
"last": "https://example.net/user/library_lurker/review/2/replies?page=1",
"@context": "https://www.w3.org/ns/activitystreams"
},
"summary": "Spoilers ahead!",
"tag": [],
"attachment": [],
"sensitive": true,
"inReplyToBook": "https://example.net/book/1",
"name": "What a cracking read",
"rating": 4.5,
"@context": "https://www.w3.org/ns/activitystreams"
}
```
##### Comment
A `Comment` on a book mentions a book and has a message body, reading status, and progress indicator.
Example:
```json
{
"id": "https://example.net/user/library_lurker/comment/9",
"type": "Comment",
"published": "2023-06-30T21:43:46.013132+00:00",
"attributedTo": "https://example.net/user/library_lurker",
"content": "<p>This is a very enjoyable book so far.</p>",
"to": ["https://example.net/user/library_lurker/followers"],
"cc": [],
"replies": {
"id": "https://example.net/user/library_lurker/comment/9/replies",
"type": "OrderedCollection",
"totalItems": 0,
"first": "https://example.net/user/library_lurker/comment/9/replies?page=1",
"last": "https://example.net/user/library_lurker/comment/9/replies?page=1",
"@context": "https://www.w3.org/ns/activitystreams"
},
"summary": "Spoilers ahead!",
"tag": [],
"attachment": [],
"sensitive": true,
"inReplyToBook": "https://example.net/book/1",
"readingStatus": "reading",
"progress": 25,
"progressMode": "PG",
"@context": "https://www.w3.org/ns/activitystreams"
}
```
##### Quotation
A quotation (aka "quote") has a message body, an excerpt from a book including position as a page number or percentage indicator, and mentions a book.
Example:
```json
{
"id": "https://example.net/user/mouse/quotation/13",
"url": "https://example.net/user/mouse/quotation/13",
"inReplyTo": null,
"published": "2020-05-10T02:38:31.150343+00:00",
"attributedTo": "https://example.net/user/mouse",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.net/user/mouse/followers"
],
"sensitive": false,
"content": "I really like this quote",
"type": "Quotation",
"replies": {
"id": "https://example.net/user/mouse/quotation/13/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://example.net/user/mouse/quotation/13/replies?only_other_accounts=true&page=true",
"partOf": "https://example.net/user/mouse/quotation/13/replies",
"items": []
}
},
"inReplyToBook": "https://example.net/book/1",
"quote": "To be or not to be, that is the question.",
"position": 50,
"positionMode": "PCT",
"@context": "https://www.w3.org/ns/activitystreams"
}
```
### Custom Objects
##### Work
A particular book, a "work" in the [FRBR](https://en.wikipedia.org/wiki/Functional_Requirements_for_Bibliographic_Records) sense.
Example:
```json
{
"id": "https://bookwyrm.social/book/5988",
"type": "Work",
"authors": [
"https://bookwyrm.social/author/417"
],
"first_published_date": null,
"published_date": null,
"title": "Piranesi",
"sort_title": null,
"subtitle": null,
"description": "**From the *New York Times* bestselling author of *Jonathan Strange & Mr. Norrell*, an intoxicating, hypnotic new novel set in a dreamlike alternative reality.",
"languages": [],
"series": null,
"series_number": null,
"subjects": [
"English literature"
],
"subject_places": [],
"openlibrary_key": "OL20893680W",
"librarything_key": null,
"goodreads_key": null,
"attachment": [
{
"url": "https://bookwyrm.social/images/covers/10226290-M.jpg",
"type": "Image"
}
],
"lccn": null,
"editions": [
"https://bookwyrm.social/book/5989"
],
"@context": "https://www.w3.org/ns/activitystreams"
}
```
##### Edition
A particular _manifestation_ of a Work, in the [FRBR](https://en.wikipedia.org/wiki/Functional_Requirements_for_Bibliographic_Records) sense.
Example:
```json
{
"id": "https://bookwyrm.social/book/5989",
"lastEditedBy": "https://example.net/users/rat",
"type": "Edition",
"authors": [
"https://bookwyrm.social/author/417"
],
"first_published_date": null,
"published_date": "2020-09-15T00:00:00+00:00",
"title": "Piranesi",
"sort_title": null,
"subtitle": null,
"description": "Piranesi's house is no ordinary building; its rooms are infinite, its corridors endless, its walls are lined with thousands upon thousands of statues, each one different from all the others.",
"languages": [
"English"
],
"series": null,
"series_number": null,
"subjects": [],
"subject_places": [],
"openlibrary_key": "OL29486417M",
"librarything_key": null,
"goodreads_key": null,
"isfdb": null,
"attachment": [
{
"url": "https://bookwyrm.social/images/covers/50202953._SX318_.jpg",
"type": "Image"
}
],
"isbn_10": "1526622424",
"isbn_13": "9781526622426",
"oclc_number": null,
"asin": null,
"pages": 272,
"physical_format": null,
"publishers": [
"Bloomsbury Publishing Plc"
],
"work": "https://bookwyrm.social/book/5988",
"@context": "https://www.w3.org/ns/activitystreams"
}
```
#### Shelf
A user's book collection. By default, every user has a `to-read`, `reading`, `read`, and `stopped-reading` shelf which are used to track reading progress. Users may create an unlimited number of additional shelves with their own ids.
Example
```json
{
"id": "https://example.net/user/avid_reader/books/extraspecialbooks-5",
"type": "Shelf",
"totalItems": 0,
"first": "https://example.net/user/avid_reader/books/extraspecialbooks-5?page=1",
"last": "https://example.net/user/avid_reader/books/extraspecialbooks-5?page=1",
"name": "Extra special books",
"owner": "https://example.net/user/avid_reader",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.net/user/avid_reader/followers"
],
"@context": "https://www.w3.org/ns/activitystreams"
}
```
#### List
A collection of books that may have items contributed by users other than the one who created the list.
Example:
```json
{
"id": "https://example.net/list/1",
"type": "BookList",
"totalItems": 0,
"first": "https://example.net/list/1?page=1",
"last": "https://example.net/list/1?page=1",
"name": "My cool list",
"owner": "https://example.net/user/avid_reader",
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"cc": [
"https://example.net/user/avid_reader/followers"
],
"summary": "A list of books I like.",
"curation": "curated",
"@context": "https://www.w3.org/ns/activitystreams"
}
```
#### Activities
- `Create`: Adds a shelf or list to the database.
- `Delete`: Removes a shelf or list.
- `Add`: Adds a book to a shelf or list.
- `Remove`: Removes a book from a shelf or list.
## Alternative Serialization
Because BookWyrm uses custom object types that aren't listed in [the standard ActivityStreams Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary), some statuses are transformed into standard types when sent to or viewed by non-BookWyrm services. `Review`s are converted into `Article`s, and `Comment`s and `Quotation`s are converted into `Note`s, with a link to the book and the cover image attached.
In future this may be done with [JSON-LD type arrays](https://www.w3.org/TR/json-ld/#specifying-the-type) instead.
## Other extensions
### Webfinger
Bookwyrm uses the [Webfinger](https://datatracker.ietf.org/doc/html/rfc7033) standard to identify and disambiguate fediverse actors. The [Webfinger documentation on the Mastodon project](https://docs.joinmastodon.org/spec/webfinger/) provides a good overview of how Webfinger is used.
### HTTP Signatures
Bookwyrm uses and requires HTTP signatures for all `POST` requests. `GET` requests are not signed by default, but if Bookwyrm receives a `403` response to a `GET` it will re-send the request, signed by the default server user. This usually will have a user id of `https://example.net/user/bookwyrm.instance.actor`
#### publicKey id
In older versions of Bookwyrm the `publicKey.id` was incorrectly listed in request headers as `https://example.net/user/username#main-key`. As of v0.6.3 the id is now listed correctly, as `https://example.net/user/username/#main-key`. In most ActivityPub implementations this will make no difference as the URL will usually resolve to the same place.
### NodeInfo
Bookwyrm uses the [NodeInfo](http://nodeinfo.diaspora.software/) standard to provide statistics and version information for each instance.
## Further Documentation
See [docs.joinbookwyrm.com/](https://docs.joinbookwyrm.com/) for more documentation.

View file

@ -529,7 +529,7 @@ async def async_broadcast(recipients: List[str], sender, data: str):
async def sign_and_send(
session: aiohttp.ClientSession, sender, data: str, destination: str
session: aiohttp.ClientSession, sender, data: str, destination: str, **kwargs
):
"""Sign the messages and send them in an asynchronous bundle"""
now = http_date()
@ -539,11 +539,19 @@ async def sign_and_send(
raise ValueError("No private key found for sender")
digest = make_digest(data)
signature = make_signature(
"post",
sender,
destination,
now,
digest=digest,
use_legacy_key=kwargs.get("use_legacy_key"),
)
headers = {
"Date": now,
"Digest": digest,
"Signature": make_signature("post", sender, destination, now, digest),
"Signature": signature,
"Content-Type": "application/activity+json; charset=utf-8",
"User-Agent": USER_AGENT,
}
@ -554,6 +562,14 @@ async def sign_and_send(
logger.exception(
"Failed to send broadcast to %s: %s", destination, response.reason
)
if kwargs.get("use_legacy_key") is not True:
logger.info("Trying again with legacy keyId header value")
asyncio.ensure_future(
sign_and_send(
session, sender, data, destination, use_legacy_key=True
)
)
return response
except asyncio.TimeoutError:
logger.info("Connection timed out for url: %s", destination)

View file

@ -371,7 +371,7 @@ class TagField(ManyToManyField):
tags.append(
activitypub.Link(
href=item.remote_id,
name=getattr(item, item.name_field),
name=f"@{getattr(item, item.name_field)}",
type=activity_type,
)
)
@ -379,7 +379,12 @@ class TagField(ManyToManyField):
def field_from_activity(self, value, allow_external_connections=True):
if not isinstance(value, list):
return None
# GoToSocial DMs and single-user mentions are
# sent as objects, not as an array of objects
if isinstance(value, dict):
value = [value]
else:
return None
items = []
for link_json in value:
link = activitypub.Link(**link_json)

View file

@ -142,10 +142,17 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
# keep notes if they mention local users
if activity.tag == MISSING or activity.tag is None:
return True
tags = [l["href"] for l in activity.tag if l["type"] == "Mention"]
# GoToSocial sends single tags as objects
# not wrapped in a list
tags = activity.tag if isinstance(activity.tag, list) else [activity.tag]
user_model = apps.get_model("bookwyrm.User", require_ready=True)
for tag in tags:
if user_model.objects.filter(remote_id=tag, local=True).exists():
if (
tag["type"] == "Mention"
and user_model.objects.filter(
remote_id=tag["href"], local=True
).exists()
):
# we found a mention of a known use boost
return False
return True

View file

@ -339,7 +339,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
# this is a new remote user, we need to set their remote server field
if not self.local:
super().save(*args, **kwargs)
transaction.on_commit(lambda: set_remote_server.delay(self.id))
transaction.on_commit(lambda: set_remote_server(self.id))
return
with transaction.atomic():
@ -470,17 +470,29 @@ class KeyPair(ActivitypubMixin, BookWyrmModel):
@app.task(queue=LOW)
def set_remote_server(user_id):
def set_remote_server(user_id, allow_external_connections=False):
"""figure out the user's remote server in the background"""
user = User.objects.get(id=user_id)
actor_parts = urlparse(user.remote_id)
user.federated_server = get_or_create_remote_server(actor_parts.netloc)
federated_server = get_or_create_remote_server(
actor_parts.netloc, allow_external_connections=allow_external_connections
)
# if we were unable to find the server, we need to create a new entry for it
if not federated_server:
# and to do that, we will call this function asynchronously.
if not allow_external_connections:
set_remote_server.delay(user_id, allow_external_connections=True)
return
user.federated_server = federated_server
user.save(broadcast=False, update_fields=["federated_server"])
if user.bookwyrm_user and user.outbox:
get_remote_reviews.delay(user.outbox)
def get_or_create_remote_server(domain, refresh=False):
def get_or_create_remote_server(
domain, allow_external_connections=False, refresh=False
):
"""get info on a remote server"""
server = FederatedServer()
try:
@ -490,6 +502,9 @@ def get_or_create_remote_server(domain, refresh=False):
except FederatedServer.DoesNotExist:
pass
if not allow_external_connections:
return None
try:
data = get_data(f"https://{domain}/.well-known/nodeinfo")
try:

View file

@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.6.2"
VERSION = "0.6.3"
RELEASE_API = env(
"RELEASE_API",
@ -22,7 +22,7 @@ RELEASE_API = env(
PAGE_LENGTH = env.int("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "ea91d7df"
JS_CACHE = "d993847c"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View file

@ -22,7 +22,7 @@ def create_key_pair():
return private_key, public_key
def make_signature(method, sender, destination, date, digest=None):
def make_signature(method, sender, destination, date, **kwargs):
"""uses a private key to sign an outgoing message"""
inbox_parts = urlparse(destination)
signature_headers = [
@ -31,6 +31,7 @@ def make_signature(method, sender, destination, date, digest=None):
f"date: {date}",
]
headers = "(request-target) host date"
digest = kwargs.get("digest")
if digest is not None:
signature_headers.append(f"digest: {digest}")
headers = "(request-target) host date digest"
@ -38,8 +39,14 @@ def make_signature(method, sender, destination, date, digest=None):
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")))
# For legacy reasons we need to use an incorrect keyId for older Bookwyrm versions
key_id = (
f"{sender.remote_id}#main-key"
if kwargs.get("use_legacy_key")
else f"{sender.remote_id}/#main-key"
)
signature = {
"keyId": f"{sender.remote_id}#main-key",
"keyId": key_id,
"algorithm": "rsa-sha256",
"headers": headers,
"signature": b64encode(signed_message).decode("utf8"),

View file

@ -5,6 +5,10 @@
white-space: nowrap;
}
.stars .no-rating {
font-style: italic;
}
/** Stars in a review form
*
* Specificity makes hovering taking over checked inputs.

View file

@ -40,9 +40,6 @@ let BookWyrm = new (class {
document.querySelectorAll("details.dropdown").forEach((node) => {
node.addEventListener("toggle", this.handleDetailsDropdown.bind(this));
node.querySelectorAll("[data-modal-open]").forEach((modal_node) =>
modal_node.addEventListener("click", () => (node.open = false))
);
});
document

View file

@ -190,13 +190,15 @@
<meta itemprop="bestRating" content="5">
<meta itemprop="reviewCount" content="{{ review_count }}">
{% include 'snippets/stars.html' with rating=rating %}
<span>
{% include 'snippets/stars.html' with rating=rating %}
{% blocktrans count counter=review_count trimmed %}
({{ review_count }} review)
{% plural %}
({{ review_count }} reviews)
{% endblocktrans %}
{% blocktrans count counter=review_count trimmed %}
({{ review_count }} review)
{% plural %}
({{ review_count }} reviews)
{% endblocktrans %}
</span>
</div>
{% with full=book|book_description itemprop='abstract' %}

View file

@ -2,26 +2,25 @@
{% load i18n %}
<span class="stars">
<span class="is-sr-only">
{% if rating %}
{% if rating %}
<span class="is-sr-only">
{% blocktranslate trimmed with rating=rating|floatformat:0 count counter=rating|floatformat:0|add:0 %}
{{ rating }} star
{% plural %}
{{ rating }} stars
{% endblocktranslate %}
{% else %}
{% trans "No rating" %}
{% endif %}
</span>
{% for i in '12345'|make_list %}
<span
class="
icon is-small mr-1
icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}
"
aria-hidden="true"
></span>
{% endfor %}
</span>
{% for i in '12345'|make_list %}
<span
class="
icon is-small mr-1
icon-star-{% if rating >= forloop.counter %}full{% elif rating|floatformat:0 >= forloop.counter|floatformat:0 %}half{% else %}empty{% endif %}
"
aria-hidden="true"
></span>
{% endfor %}
{% else %}
<span class="no-rating">{% trans "No rating" %}</span>
{% endif %}
</span>
{% endspaceless %}

View file

@ -404,7 +404,7 @@ class ModelFields(TestCase):
self.assertIsInstance(result, list)
self.assertEqual(len(result), 1)
self.assertEqual(result[0].href, "https://e.b/c")
self.assertEqual(result[0].name, "Name")
self.assertEqual(result[0].name, "@Name")
self.assertEqual(result[0].type, "Serializable")
def test_tag_field_from_activity(self, *_):

View file

@ -162,7 +162,9 @@ class User(TestCase):
json={"software": {"name": "hi", "version": "2"}},
)
server = models.user.get_or_create_remote_server(DOMAIN)
server = models.user.get_or_create_remote_server(
DOMAIN, allow_external_connections=True
)
self.assertEqual(server.server_name, DOMAIN)
self.assertEqual(server.application_type, "hi")
self.assertEqual(server.application_version, "2")
@ -173,7 +175,9 @@ class User(TestCase):
responses.GET, f"https://{DOMAIN}/.well-known/nodeinfo", status=404
)
server = models.user.get_or_create_remote_server(DOMAIN)
server = models.user.get_or_create_remote_server(
DOMAIN, allow_external_connections=True
)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)
@ -187,7 +191,9 @@ class User(TestCase):
)
responses.add(responses.GET, "http://www.example.com", status=404)
server = models.user.get_or_create_remote_server(DOMAIN)
server = models.user.get_or_create_remote_server(
DOMAIN, allow_external_connections=True
)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)
@ -201,7 +207,9 @@ class User(TestCase):
)
responses.add(responses.GET, "http://www.example.com", json={"fish": "salmon"})
server = models.user.get_or_create_remote_server(DOMAIN)
server = models.user.get_or_create_remote_server(
DOMAIN, allow_external_connections=True
)
self.assertEqual(server.server_name, DOMAIN)
self.assertIsNone(server.application_type)
self.assertIsNone(server.application_version)

View file

@ -87,7 +87,7 @@ class Signature(TestCase):
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
"post", signer or sender, self.rat.inbox, now, digest=digest
)
with patch("bookwyrm.views.inbox.activity_task.apply_async"):
with patch("bookwyrm.models.user.set_remote_server.delay"):
@ -111,6 +111,7 @@ class Signature(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id
data["publicKey"]["id"] = f"{self.fake_remote.remote_id}/#main-key"
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
del data["icon"] # Avoid having to return an avatar.
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
@ -138,6 +139,7 @@ class Signature(TestCase):
datafile = pathlib.Path(__file__).parent.joinpath("data/ap_user.json")
data = json.loads(datafile.read_bytes())
data["id"] = self.fake_remote.remote_id
data["publicKey"]["id"] = f"{self.fake_remote.remote_id}/#main-key"
data["publicKey"]["publicKeyPem"] = self.fake_remote.key_pair.public_key
del data["icon"] # Avoid having to return an avatar.
responses.add(responses.GET, self.fake_remote.remote_id, json=data, status=200)
@ -157,7 +159,7 @@ class Signature(TestCase):
"bookwyrm.models.relationship.UserFollowRequest.accept"
) as accept_mock:
response = self.send_test_request(sender=self.fake_remote)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 200) # BUG this is 401
self.assertTrue(accept_mock.called)
# Old key is cached, so still works:

View file

@ -45,6 +45,7 @@ class EditBook(View):
data = {"book": book, "form": form}
ensure_transient_values_persist(request, data)
if not form.is_valid():
ensure_transient_values_persist(request, data, add_author=True)
return TemplateResponse(request, "book/edit/edit_book.html", data)
data = add_authors(request, data)
@ -102,11 +103,13 @@ class CreateBook(View):
"authors": authors,
}
ensure_transient_values_persist(request, data)
if not form.is_valid():
ensure_transient_values_persist(request, data, form=form)
return TemplateResponse(request, "book/edit/edit_book.html", data)
# we have to call this twice because it requires form.cleaned_data
# which only exists after we validate the form
ensure_transient_values_persist(request, data, form=form)
data = add_authors(request, data)
# check if this is an edition of an existing work
@ -139,9 +142,15 @@ class CreateBook(View):
return redirect(f"/book/{book.id}")
def ensure_transient_values_persist(request, data):
def ensure_transient_values_persist(request, data, **kwargs):
"""ensure that values of transient form fields persist when re-rendering the form"""
data["cover_url"] = request.POST.get("cover-url")
if kwargs and kwargs.get("form"):
data["book"] = data.get("book") or {}
data["book"]["subjects"] = kwargs["form"].cleaned_data["subjects"]
data["add_author"] = request.POST.getlist("add_author")
elif kwargs and kwargs.get("add_author") is True:
data["add_author"] = request.POST.getlist("add_author")
def add_authors(request, data):

View file

@ -3,7 +3,6 @@ import json
import re
import logging
from urllib.parse import urldefrag
import requests
from django.http import HttpResponse, Http404
@ -130,15 +129,18 @@ def has_valid_signature(request, activity):
"""verify incoming signature"""
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get("actor"):
raise ValueError("Wrong actor created signature.")
remote_user = activitypub.resolve_remote_id(key_actor, model=models.User)
remote_user = activitypub.resolve_remote_id(
activity.get("actor"), model=models.User
)
if not remote_user:
return False
if signature.key_id != remote_user.key_pair.remote_id:
if (
signature.key_id != f"{remote_user.remote_id}#main-key"
): # legacy Bookwyrm
raise ValueError("Wrong actor created signature.")
try:
signature.verify(remote_user.key_pair.public_key, request)
except ValueError:

View file

@ -36,14 +36,22 @@ class RssFeed(Feed):
def items(self, obj):
"""the user's activity feed"""
return obj.status_set.select_subclasses().filter(
privacy__in=["public", "unlisted"],
)[:10]
return (
obj.status_set.select_subclasses()
.filter(
privacy__in=["public", "unlisted"],
)
.order_by("-published_date")[:10]
)
def item_link(self, item):
"""link to the status"""
return item.local_path
def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date
class RssReviewsOnlyFeed(Feed):
"""serialize user's reviews in rss feed"""
@ -76,12 +84,16 @@ class RssReviewsOnlyFeed(Feed):
return Review.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
).order_by("-published_date")[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date
class RssQuotesOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
@ -114,12 +126,16 @@ class RssQuotesOnlyFeed(Feed):
return Quotation.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
).order_by("-published_date")[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date
class RssCommentsOnlyFeed(Feed):
"""serialize user's quotes in rss feed"""
@ -152,8 +168,12 @@ class RssCommentsOnlyFeed(Feed):
return Comment.objects.filter(
user=obj,
privacy__in=["public", "unlisted"],
)[:10]
).order_by("-published_date")[:10]
def item_link(self, item):
"""link to the status"""
return item.local_path
def item_pubdate(self, item):
"""publication date of the item"""
return item.published_date

Binary file not shown.

Binary file not shown.

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-26 00:20+0000\n"
"POT-Creation-Date: 2023-05-30 17:48+0000\n"
"PO-Revision-Date: 2021-02-28 17:19-0800\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: English <LL@li.org>\n"
@ -301,7 +301,7 @@ msgstr ""
msgid "Approved"
msgstr ""
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:305
#: bookwyrm/models/user.py:32 bookwyrm/templates/book/book.html:307
msgid "Reviews"
msgstr ""
@ -839,7 +839,7 @@ msgid "ISNI:"
msgstr ""
#: bookwyrm/templates/author/edit_author.html:126
#: bookwyrm/templates/book/book.html:218
#: bookwyrm/templates/book/book.html:220
#: bookwyrm/templates/book/edit/edit_book.html:150
#: bookwyrm/templates/book/file_links/add_link_modal.html:60
#: bookwyrm/templates/book/file_links/edit_links.html:86
@ -863,7 +863,7 @@ msgstr ""
#: bookwyrm/templates/author/edit_author.html:127
#: bookwyrm/templates/author/sync_modal.html:23
#: bookwyrm/templates/book/book.html:219
#: bookwyrm/templates/book/book.html:221
#: bookwyrm/templates/book/cover_add_modal.html:33
#: bookwyrm/templates/book/edit/edit_book.html:152
#: bookwyrm/templates/book/edit/edit_book.html:155
@ -920,73 +920,73 @@ msgstr ""
msgid "Click to enlarge"
msgstr ""
#: bookwyrm/templates/book/book.html:195
#: bookwyrm/templates/book/book.html:196
#, python-format
msgid "(%(review_count)s review)"
msgid_plural "(%(review_count)s reviews)"
msgstr[0] ""
msgstr[1] ""
#: bookwyrm/templates/book/book.html:207
#: bookwyrm/templates/book/book.html:209
msgid "Add Description"
msgstr ""
#: bookwyrm/templates/book/book.html:214
#: bookwyrm/templates/book/book.html:216
#: bookwyrm/templates/book/edit/edit_book_form.html:42
#: bookwyrm/templates/lists/form.html:13 bookwyrm/templates/shelf/form.html:17
msgid "Description:"
msgstr ""
#: bookwyrm/templates/book/book.html:230
#: bookwyrm/templates/book/book.html:232
#, python-format
msgid "%(count)s edition"
msgid_plural "%(count)s editions"
msgstr[0] ""
msgstr[1] ""
#: bookwyrm/templates/book/book.html:244
#: bookwyrm/templates/book/book.html:246
msgid "You have shelved this edition in:"
msgstr ""
#: bookwyrm/templates/book/book.html:259
#: bookwyrm/templates/book/book.html:261
#, python-format
msgid "A <a href=\"%(book_path)s\">different edition</a> of this book is on your <a href=\"%(shelf_path)s\">%(shelf_name)s</a> shelf."
msgstr ""
#: bookwyrm/templates/book/book.html:270
#: bookwyrm/templates/book/book.html:272
msgid "Your reading activity"
msgstr ""
#: bookwyrm/templates/book/book.html:276
#: bookwyrm/templates/book/book.html:278
#: bookwyrm/templates/guided_tour/book.html:56
msgid "Add read dates"
msgstr ""
#: bookwyrm/templates/book/book.html:284
#: bookwyrm/templates/book/book.html:286
msgid "You don't have any reading activity for this book."
msgstr ""
#: bookwyrm/templates/book/book.html:310
#: bookwyrm/templates/book/book.html:312
msgid "Your reviews"
msgstr ""
#: bookwyrm/templates/book/book.html:316
#: bookwyrm/templates/book/book.html:318
msgid "Your comments"
msgstr ""
#: bookwyrm/templates/book/book.html:322
#: bookwyrm/templates/book/book.html:324
msgid "Your quotes"
msgstr ""
#: bookwyrm/templates/book/book.html:358
#: bookwyrm/templates/book/book.html:360
msgid "Subjects"
msgstr ""
#: bookwyrm/templates/book/book.html:370
#: bookwyrm/templates/book/book.html:372
msgid "Places"
msgstr ""
#: bookwyrm/templates/book/book.html:381
#: bookwyrm/templates/book/book.html:383
#: bookwyrm/templates/groups/group.html:19
#: bookwyrm/templates/guided_tour/lists.html:14
#: bookwyrm/templates/guided_tour/user_books.html:102
@ -1000,11 +1000,11 @@ msgstr ""
msgid "Lists"
msgstr ""
#: bookwyrm/templates/book/book.html:393
#: bookwyrm/templates/book/book.html:395
msgid "Add to list"
msgstr ""
#: bookwyrm/templates/book/book.html:403
#: bookwyrm/templates/book/book.html:405
#: bookwyrm/templates/book/cover_add_modal.html:32
#: bookwyrm/templates/lists/add_item_modal.html:39
#: bookwyrm/templates/lists/list.html:255
@ -6031,7 +6031,7 @@ msgid "BookWyrm's source code is freely available. You can contribute or report
msgstr ""
#: bookwyrm/templates/snippets/form_rate_stars.html:20
#: bookwyrm/templates/snippets/stars.html:13
#: bookwyrm/templates/snippets/stars.html:23
msgid "No rating"
msgstr ""
@ -6244,15 +6244,12 @@ msgid "Want to read"
msgstr ""
#: bookwyrm/templates/snippets/shelf_selector.html:81
#: bookwyrm/templates/snippets/shelf_selector.html:95
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown_options.html:73
#, python-format
msgid "Remove from %(name)s"
msgstr ""
#: bookwyrm/templates/snippets/shelf_selector.html:94
msgid "Remove from"
msgstr ""
#: bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html:5
msgid "More shelves"
msgstr ""
@ -6634,17 +6631,17 @@ msgstr ""
msgid "Status updates from {obj.display_name}"
msgstr ""
#: bookwyrm/views/rss_feed.py:72
#: bookwyrm/views/rss_feed.py:80
#, python-brace-format
msgid "Reviews from {obj.display_name}"
msgstr ""
#: bookwyrm/views/rss_feed.py:110
#: bookwyrm/views/rss_feed.py:122
#, python-brace-format
msgid "Quotes from {obj.display_name}"
msgstr ""
#: bookwyrm/views/rss_feed.py:148
#: bookwyrm/views/rss_feed.py:164
#, python-brace-format
msgid "Comments from {obj.display_name}"
msgstr ""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: bookwyrm\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-26 00:20+0000\n"
"PO-Revision-Date: 2023-04-26 00:45\n"
"PO-Revision-Date: 2023-05-29 15:36\n"
"Last-Translator: Mouse Reeve <mousereeve@riseup.net>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt\n"
@ -68,7 +68,7 @@ msgstr "A data de término da leitura não pode estar no futuro."
#: bookwyrm/forms/forms.py:74
msgid "Reading finished date cannot be in the future."
msgstr ""
msgstr "A data final da leitura não pode ser no futuro."
#: bookwyrm/forms/landing.py:38
msgid "Username or password are incorrect"
@ -273,7 +273,7 @@ msgstr "Parado"
#: bookwyrm/models/import_job.py:83 bookwyrm/models/import_job.py:91
msgid "Import stopped"
msgstr ""
msgstr "Importação interrompida"
#: bookwyrm/models/import_job.py:363 bookwyrm/models/import_job.py:388
msgid "Error loading book"
@ -342,7 +342,7 @@ msgstr "English (Inglês)"
#: bookwyrm/settings.py:295
msgid "Català (Catalan)"
msgstr ""
msgstr "Català (Catalão)"
#: bookwyrm/settings.py:296
msgid "Deutsch (German)"
@ -350,7 +350,7 @@ msgstr "Deutsch (Alemão)"
#: bookwyrm/settings.py:297
msgid "Esperanto (Esperanto)"
msgstr ""
msgstr "Esperanto (Esperanto)"
#: bookwyrm/settings.py:298
msgid "Español (Spanish)"
@ -358,7 +358,7 @@ msgstr "Español (Espanhol)"
#: bookwyrm/settings.py:299
msgid "Euskara (Basque)"
msgstr ""
msgstr "Euskara (Basco)"
#: bookwyrm/settings.py:300
msgid "Galego (Galician)"
@ -386,7 +386,7 @@ msgstr "Norsk (Norueguês)"
#: bookwyrm/settings.py:306
msgid "Polski (Polish)"
msgstr ""
msgstr "Polski (Polonês)"
#: bookwyrm/settings.py:307
msgid "Português do Brasil (Brazilian Portuguese)"
@ -446,7 +446,7 @@ msgstr "Bem-vindol(a) a %(site_name)s!"
#: bookwyrm/templates/about/about.html:25
#, python-format
msgid "%(site_name)s is part of <em>BookWyrm</em>, a network of independent, self-directed communities for readers. While you can interact seamlessly with users anywhere in the <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">BookWyrm network</a>, this community is unique."
msgstr ""
msgstr "%(site_name)s faz parte da <em>BookWyrm</em>, uma rede independente e autogestionada de comunidades para leitores. Apesar de você poder interagir perfeitamente com usuários de qualquer parte da <a href=\"https://joinbookwyrm.com/instances/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">rede BookWyrm</a>, esta comunidade é única."
#: bookwyrm/templates/about/about.html:45
#, python-format

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -18,7 +18,7 @@ psycopg2==2.9.5
pycryptodome==3.16.0
python-dateutil==2.8.2
redis==4.5.4
requests==2.28.2
requests==2.31.0
responses==0.22.0
pytz>=2022.7
boto3==1.26.57