Skip to content

Commit

Permalink
Merge branch 'main' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
mouse-reeve committed Feb 20, 2023
2 parents cad83a3 + 4b3849e commit 0062723
Show file tree
Hide file tree
Showing 88 changed files with 2,282 additions and 1,674 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/black.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- uses: psf/[email protected]
with:
version: 22.12.0
64 changes: 60 additions & 4 deletions bookwyrm/activitypub/base_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder
import logging
import requests

from django.apps import apps
from django.db import IntegrityError, transaction
from django.utils.http import http_date

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
from bookwyrm.tasks import app, MEDIUM

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -246,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
Expand All @@ -275,10 +280,16 @@ def resolve_remote_id(
# load the data and create the object
try:
data = get_data(remote_id)
except ConnectorException:
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)
else:
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"):
Expand All @@ -297,6 +308,51 @@ def resolve_remote_id(
return item.to_model(model=model, instance=result, save=save)


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:
user = models.User.objects.create_user(
username=username,
email=email,
local=True,
localname=INSTANCE_ACTOR_USERNAME,
)
return user


def get_activitypub_data(url):
"""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")
try:
resp = requests.get(
url,
headers={
"Accept": "application/json; charset=utf-8",
"Date": now,
"Signature": make_signature("get", sender, url, now),
},
)
except requests.RequestException:
raise ConnectorException()
if not resp.ok:
resp.raise_for_status()
try:
data = resp.json()
except ValueError:
raise ConnectorException()

return data


@dataclass(init=False)
class Link(ActivityObject):
"""for tagging a book in a status"""
Expand Down
9 changes: 7 additions & 2 deletions bookwyrm/connectors/abstract_connector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
""" functionality outline for a book data connector """
from abc import ABC, abstractmethod
from urllib.parse import quote_plus
import imghdr
import logging
import re
Expand Down Expand Up @@ -48,7 +49,7 @@ def get_search_url(self, query):
return f"{self.isbn_search_url}{normalized_query}"
# NOTE: previously, we tried searching isbn and if that produces no results,
# searched as free text. This, instead, only searches isbn if it's isbn-y
return f"{self.search_url}{query}"
return f"{self.search_url}{quote_plus(query)}"

def process_search_response(self, query, data, min_confidence):
"""Format the search results based on the formt of the query"""
Expand Down Expand Up @@ -244,7 +245,11 @@ def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT):
raise ConnectorException(err)

if not resp.ok:
raise ConnectorException()
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:
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/models/activitypub_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ async def sign_and_send(
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,
}
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/models/annual_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def ratings(self):
user=self.user,
book__in=book_ids,
)
return {r.book.id: r.rating for r in reviews}
return {r.book_id: r.rating for r in reviews}

@property
def progress(self):
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/models/readthrough.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ReadThrough(BookWyrmModel):

def save(self, *args, **kwargs):
"""update user active time"""
cache.delete(f"latest_read_through-{self.user.id}-{self.book.id}")
cache.delete(f"latest_read_through-{self.user_id}-{self.book_id}")
self.user.update_active_date()
# an active readthrough must have an unset finish date
if self.finish_date or self.stopped_date:
Expand Down
4 changes: 2 additions & 2 deletions bookwyrm/models/shelf.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def save(self, *args, priority=LOW, **kwargs):
# remove all caches related to all editions of this book
cache.delete_many(
[
f"book-on-shelf-{book.id}-{self.shelf.id}"
f"book-on-shelf-{book.id}-{self.shelf_id}"
for book in self.book.parent_work.editions.all()
]
)
Expand All @@ -117,7 +117,7 @@ def delete(self, *args, **kwargs):
if self.id and self.user.local:
cache.delete_many(
[
f"book-on-shelf-{book}-{self.shelf.id}"
f"book-on-shelf-{book}-{self.shelf_id}"
for book in self.book.parent_work.editions.values_list(
"id", flat=True
)
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/models/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class SiteSettings(SiteModel):
invite_request_question = models.BooleanField(default=False)
require_confirm_email = models.BooleanField(default=True)
default_user_auth_group = models.ForeignKey(
auth_models.Group, null=True, blank=True, on_delete=models.PROTECT
auth_models.Group, null=True, blank=True, on_delete=models.RESTRICT
)

invite_question_text = models.CharField(
Expand Down
2 changes: 1 addition & 1 deletion bookwyrm/models/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class Meta:
def save(self, *args, **kwargs):
"""save and notify"""
if self.reply_parent:
self.thread_id = self.reply_parent.thread_id or self.reply_parent.id
self.thread_id = self.reply_parent.thread_id or self.reply_parent_id

super().save(*args, **kwargs)

Expand Down
17 changes: 16 additions & 1 deletion bookwyrm/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"csp.middleware.CSPMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"bookwyrm.middleware.TimezoneMiddleware",
"bookwyrm.middleware.IPBlocklistMiddleware",
Expand Down Expand Up @@ -205,7 +206,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", ""))
REDIS_ACTIVITY_DB_INDEX = env("REDIS_ACTIVITY_DB_INDEX", 0)
REDIS_ACTIVITY_URL = env(
"REDIS_ACTIVITY_URL",
Expand Down Expand Up @@ -335,6 +336,8 @@
PROTOCOL = "http"
if USE_HTTPS:
PROTOCOL = "https"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

USE_S3 = env.bool("USE_S3", False)

Expand All @@ -358,11 +361,17 @@
MEDIA_FULL_URL = MEDIA_URL
STATIC_FULL_URL = STATIC_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
CSP_DEFAULT_SRC = ("'self'", AWS_S3_CUSTOM_DOMAIN)
CSP_SCRIPT_SRC = ("'self'", AWS_S3_CUSTOM_DOMAIN)
else:
STATIC_URL = "/static/"
MEDIA_URL = "/images/"
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
CSP_DEFAULT_SRC = "'self'"
CSP_SCRIPT_SRC = "'self'"

CSP_INCLUDE_NONCE_IN = ["script-src"]

OTEL_EXPORTER_OTLP_ENDPOINT = env("OTEL_EXPORTER_OTLP_ENDPOINT", None)
OTEL_EXPORTER_OTLP_HEADERS = env("OTEL_EXPORTER_OTLP_HEADERS", None)
Expand All @@ -373,3 +382,9 @@
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")

# 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"
12 changes: 8 additions & 4 deletions bookwyrm/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,26 @@ def create_key_pair():
return private_key, public_key


def make_signature(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): 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(f"digest: {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())
Expand Down
8 changes: 7 additions & 1 deletion bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
}

.navbar-item {
// see ../components/_details.scss :: Navbar details
/* see ../components/_details.scss :: Navbar details */
padding-right: 1.75rem;
font-size: 1rem;
}
Expand Down Expand Up @@ -109,3 +109,9 @@
max-height: 35em;
overflow: hidden;
}

.dropdown-menu .button {
@include mobile {
font-size: $size-6;
}
}
8 changes: 4 additions & 4 deletions bookwyrm/static/css/vendor/shepherd.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
@use 'bulma/bulma.sass';

.shepherd-button {
@extend .button.mr-2;
@extend .button, .mr-2;
}

.shepherd-button.shepherd-button-secondary {
@extend .button.is-light;
@extend .button, .is-light;
}

.shepherd-footer {
@extend .message-body;
@extend .is-info.is-light;
@extend .is-info, .is-light;
border-color: $info-light;
border-radius: 0 0 4px 4px;
}
Expand All @@ -29,7 +29,7 @@

.shepherd-text {
@extend .message-body;
@extend .is-info.is-light;
@extend .is-info, .is-light;
border-radius: 0;
}

Expand Down
6 changes: 3 additions & 3 deletions bookwyrm/templates/book/book.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ <h1 class="title" itemprop="name" dir="auto">
<meta itemprop="isPartOf" content="{{ book.series | escape }}">
<meta itemprop="volumeNumber" content="{{ book.series_number }}">

({{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %})
(<a href="{% url 'book-series-by' book.authors.first.id %}?series_name={{ book.series }}">{{ book.series }}{% if book.series_number %} #{{ book.series_number }}{% endif %}</a>)
{% endif %}
</p>
{% endif %}
Expand Down Expand Up @@ -215,10 +215,10 @@ <h1 class="title" itemprop="name" dir="auto">
{% endif %}


{% with work=book.parent_work %}
{% with work=book.parent_work editions_count=book.parent_work.editions.count %}
<p>
<a href="{{ work.local_path }}/editions" id="tour-other-editions-link">
{% blocktrans trimmed count counter=work.editions.count with count=work.editions.count|intcomma %}
{% blocktrans trimmed count counter=editions_count with count=editions_count|intcomma %}
{{ count }} edition
{% plural %}
{{ count }} editions
Expand Down
35 changes: 35 additions & 0 deletions bookwyrm/templates/book/series.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% extends 'layout.html' %}
{% load i18n %}
{% load book_display_tags %}

{% block title %}{{ series_name }}{% endblock %}

{% block content %}
<div class="block">
<h1 class="title">{{ series_name }}</h1>
<div class="subtitle" dir="auto">
{% trans "Series by" %} <a
href="{{ author.local_path }}"
class="author {{ link_class }}"
itemprop="author"
itemscope
itemtype="https://schema.org/Thing"
><span
itemprop="name"
>{{ author.name }}</span></a>
</div>

<div class="columns is-multiline is-mobile">
{% for book in books %}
{% with book=book %}
<div class="column is-one-fifth-tablet is-half-mobile is-flex is-flex-direction-column">
<div class="is-flex-grow-1 mb-3">
<span class="subtitle">{% if book.series_number %}{% blocktrans with series_number=book.series_number %}Book {{ series_number }}{% endblocktrans %}{% else %}{% trans 'Unsorted Book' %}{% endif %}</span>
{% include 'landing/small-book.html' with book=book %}
</div>
</div>
{% endwith %}
{% endfor %}
</div>
</div>
{% endblock %}
2 changes: 1 addition & 1 deletion bookwyrm/templates/feed/status.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
{% endif %}
{% endfor %}
<div class="is-main block">
{% include 'snippets/status/status.html' with status=status main=True %}
{% include 'snippets/status/status.html' with status=status main=True expand=True %}
</div>

{% for child in children %}
Expand Down
Loading

0 comments on commit 0062723

Please sign in to comment.