mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-12-24 00:50:35 +00:00
Merge branch 'main' into report-actions
This commit is contained in:
commit
0818d5aabb
39 changed files with 524 additions and 101 deletions
2
.github/workflows/prettier.yaml
vendored
2
.github/workflows/prettier.yaml
vendored
|
@ -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
333
FEDERATION.md
Normal 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.
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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, *_):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue