Compare commits

...

30 commits

Author SHA1 Message Date
Carlos Cámara 0c97698431
Merge 10ed296770 into ad830dd885 2024-04-24 15:42:10 -07:00
Mouse Reeve ad830dd885
Merge pull request #3350 from Minnozz/custom-port
Correctly handle serving BookWyrm on custom port
2024-04-24 15:27:01 -07:00
Mouse Reeve 366c647585
Merge pull request #3359 from bookwyrm-social/dependabot/pip/aiohttp-3.9.4
Bump aiohttp from 3.9.2 to 3.9.4
2024-04-24 15:13:30 -07:00
Bart Schuurmans 4f58b11330 Include the correct protocol and port in remote IDs 2024-04-24 15:35:19 +02:00
Bart Schuurmans 609bc15406 Support http:// protocol in BookWyrm connector 2024-04-24 15:30:47 +02:00
Bart Schuurmans c42db40a63 Construct absolute URLs with the correct protocol and port 2024-04-24 15:30:47 +02:00
Bart Schuurmans 3aefbb548e Allow serving BookWyrm on a non-standard port 2024-04-24 15:30:47 +02:00
Bart Schuurmans baea105c18 pytest.ini env values should be unquoted
Otherwise the quotes end up in the strings.
2024-04-24 15:30:47 +02:00
Bart Schuurmans c73d1fff6a Remove unnecessary exceptions from validate_url_domain 2024-04-24 15:30:47 +02:00
Bart Schuurmans 3d183a393f
Merge pull request #3360 from hughrun/move-fix
refactor Move for more redundancy
2024-04-24 15:30:19 +02:00
Bart Schuurmans f24fdf73b5 Update to match newer code style 2024-04-24 15:08:48 +02:00
Bart Schuurmans 839ab2fafd
Merge branch 'main' into move-fix 2024-04-24 14:56:32 +02:00
Bart Schuurmans 637f19b208
Merge pull request #3336 from Minnozz/s3-url-protocol
Support AWS_S3_URL_PROTOCOL
2024-04-24 14:53:55 +02:00
Bart Schuurmans 031223104f Clarify AWS_S3_URL_PROTOCOL in .env.example 2024-04-24 14:46:57 +02:00
Hugh Rundle 6684d60526
refactor Move for more redundancy
As outlined in #3354, a use `Move` fails if the user is moving from a BookWyrm server to another BookWrym server.
This is because:

1. the original code did not announce changes to alsoKnownAs;
2. the original code always checked the locally saved profile rather than refetching the remote data;

This commit fixes both these problems by forcing `MoveUser` to always perform a "refresh" of the local data from the remote, and by saving the user with broadcast=True when updating alsoKnownAs ids.
2024-04-22 13:35:08 +10:00
dependabot[bot] cca58023ed
Bump aiohttp from 3.9.2 to 3.9.4
Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.2 to 3.9.4.
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.2...v3.9.4)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-18 15:51:34 +00:00
Bart Schuurmans bf5c08dbf3 Add docker-compose.override.yml to .gitignore 2024-04-15 13:17:00 +02:00
Bart Schuurmans be872ed672 Support AWS_S3_URL_PROTOCOL
- Allow setting in .env
- Default to PROTOCOL (same as before)
- Propagate to django-storages so it generates the correct URLs in sass_src
2024-04-15 13:16:51 +02:00
Bart Schuurmans 70f803a1f6
Merge pull request #3353 from dato/fix_quotation_str_pagenum
Fix creation of quotations with no end position
2024-04-15 13:11:55 +02:00
Adeodato Simó 4304cd4a79
use re.escape 2024-04-13 21:26:41 -03:00
Adeodato Simó 8733369605
test_quotation_page_serialization: add test with no position 2024-04-13 21:26:41 -03:00
Adeodato Simó df78cc64a6
Quotation._format_position: do not treat page numbers as integers
Fixes: #3352
2024-04-13 21:26:41 -03:00
Adeodato Simó f844abcad9
test_quotation_page_serialization: use strings for page numbers
This follows from #3273, "Allow page numbers to be text, instead of
integers".
2024-04-13 21:26:39 -03:00
Mouse Reeve 10ed296770
Merge branch 'main' into pre_commit 2024-03-27 14:39:24 -07:00
Carlos Camara 68f0c81c7a Comment pytest as it is not working
I still do not know how to make this work
2024-01-13 16:35:39 +00:00
Carlos Camara ca1b0a90d3 Add eslint to precommit 2024-01-13 16:34:09 +00:00
Carlos Camara be69065938 Add stylelint to precommit 2024-01-13 16:29:48 +00:00
Carlos Camara 66d4f20e76 Add curlylint to precommit 2024-01-13 16:25:36 +00:00
Carlos Camara 8a0ea5af42 Replace pylint with a hook and update hook versions 2024-01-13 16:20:38 +00:00
Carlos Camara 51cc447508 Pre-commit initial setup 2024-01-12 20:33:18 +00:00
51 changed files with 452 additions and 188 deletions

View file

@ -16,6 +16,11 @@ DEFAULT_LANGUAGE="English"
## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]"
# Specify when the site is served from a port that is not the default
# for the protocol (80 for HTTP or 443 for HTTPS).
# Probably only necessary in development.
# PORT=1333
MEDIA_ROOT=images/
# Database configuration
@ -78,10 +83,13 @@ S3_SIGNED_URL_EXPIRY=900
# Commented are example values if you use a non-AWS, S3-compatible service
# AWS S3 should work with only AWS_STORAGE_BUCKET_NAME and AWS_S3_REGION_NAME
# non-AWS S3-compatible services will need AWS_STORAGE_BUCKET_NAME,
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL
# along with both AWS_S3_CUSTOM_DOMAIN and AWS_S3_ENDPOINT_URL.
# AWS_S3_URL_PROTOCOL must end in ":" and defaults to the same protocol as
# the BookWyrm instance ("http:" or "https:", based on USE_SSL).
# AWS_STORAGE_BUCKET_NAME= # "example-bucket-name"
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud"
# AWS_S3_URL_PROTOCOL=None # "http:"
# AWS_S3_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud"
@ -136,9 +144,9 @@ HTTP_X_FORWARDED_PROTO=false
TWO_FACTOR_LOGIN_VALIDITY_WINDOW=2
TWO_FACTOR_LOGIN_MAX_SECONDS=60
# Additional hosts to allow in the Content-Security-Policy, "self" (should be DOMAIN)
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
# Value should be a comma-separated list of host names.
# Additional hosts to allow in the Content-Security-Policy, "self" (should be
# DOMAIN with optionally ":" + PORT) and AWS_S3_CUSTOM_DOMAIN (if used) are
# added by default. Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS=
# Time before being logged out (in seconds)

3
.gitignore vendored
View file

@ -38,3 +38,6 @@ nginx/default.conf
#macOS
**/.DS_Store
# Docker
docker-compose.override.yml

43
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,43 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
types: [file, text]
- id: end-of-file-fixer
types: [file, text]
- id: check-docstring-first
- id: check-case-conflict
- id: check-yaml
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
types: [python]
- repo: https://github.com/PyCQA/pylint
rev: v3.0.3
hooks:
- id: pylint
types: [python]
- repo: https://github.com/thibaudcolas/curlylint
rev: v0.13.1
hooks:
- id: curlylint
types: [html]
- repo: https://github.com/awebdeveloper/pre-commit-stylelint
rev: 0.0.2
hooks:
- id: stylelint
types: [css]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.0.0-alpha.1
hooks:
- id: eslint
types: [javascript]
# - repo: local
# hooks:
# - id: pytest
# name: pytest
# entry: ./bw-dev pytest
# language: system
# files: \.py$

View file

@ -118,9 +118,11 @@ def get_connectors() -> Iterator[abstract_connector.AbstractConnector]:
def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector:
"""get the connector related to the object's server"""
url = urlparse(remote_id)
identifier = url.netloc
identifier = url.hostname
if not identifier:
raise ValueError("Invalid remote id")
raise ValueError(f"Invalid remote id: {remote_id}")
base_url = f"{url.scheme}://{url.netloc}"
try:
connector_info = models.Connector.objects.get(identifier=identifier)
@ -128,10 +130,10 @@ def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnec
connector_info = models.Connector.objects.create(
identifier=identifier,
connector_file="bookwyrm_connector",
base_url=f"https://{identifier}",
books_url=f"https://{identifier}/book",
covers_url=f"https://{identifier}/images/covers",
search_url=f"https://{identifier}/search?q=",
base_url=base_url,
books_url=f"{base_url}/book",
covers_url=f"{base_url}/images/covers",
search_url=f"{base_url}/search?q=",
priority=2,
)
@ -188,8 +190,11 @@ def raise_not_valid_url(url: str) -> None:
if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url)
if not parsed.hostname:
raise ConnectorException("Hostname missing: ", url)
try:
ipaddress.ip_address(parsed.netloc)
ipaddress.ip_address(parsed.hostname)
raise ConnectorException("Provided url is an IP address: ", url)
except ValueError:
# it's not an IP address, which is good

View file

@ -4,7 +4,7 @@ from django.template.loader import get_template
from bookwyrm import models, settings
from bookwyrm.tasks import app, EMAIL
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import DOMAIN, BASE_URL
def email_data():
@ -14,6 +14,7 @@ def email_data():
"site_name": site.name,
"logo": site.logo_small_url,
"domain": DOMAIN,
"base_url": BASE_URL,
"user": None,
}

View file

@ -26,7 +26,7 @@ class FileLinkForm(CustomForm):
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
domain = urlparse(url).hostname
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":

View file

@ -8,7 +8,7 @@ from django.contrib.postgres.indexes import GinIndex
import pgtrigger
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from bookwyrm.utils.db import format_trigger
from .book import BookDataModel, MergedAuthor
@ -70,7 +70,7 @@ class Author(BookDataModel):
def get_remote_id(self):
"""editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/author/{self.id}"
return f"{BASE_URL}/author/{self.id}"
class Meta:
"""sets up indexes and triggers"""

View file

@ -10,7 +10,7 @@ from django.http import Http404
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .fields import RemoteIdField
@ -38,7 +38,7 @@ class BookWyrmModel(models.Model):
def get_remote_id(self):
"""generate the url that resolves to the local object, without a slug"""
base_path = f"https://{DOMAIN}"
base_path = BASE_URL
if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}"
@ -53,7 +53,7 @@ class BookWyrmModel(models.Model):
@property
def local_path(self):
"""how to link to this object in the local app, with a slug"""
local = self.get_remote_id().replace(f"https://{DOMAIN}", "")
local = self.get_remote_id().replace(BASE_URL, "")
name = None
if hasattr(self, "name_field"):

View file

@ -21,7 +21,7 @@ from bookwyrm import activitypub
from bookwyrm.isbn.isbn import hyphenator_singleton as hyphenator
from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import (
DOMAIN,
BASE_URL,
DEFAULT_LANGUAGE,
LANGUAGE_ARTICLES,
ENABLE_PREVIEW_IMAGES,
@ -327,7 +327,7 @@ class Book(BookDataModel):
def get_remote_id(self):
"""editions and works both use "book" instead of model_name"""
return f"https://{DOMAIN}/book/{self.id}"
return f"{BASE_URL}/book/{self.id}"
def guess_sort_title(self):
"""Get a best-guess sort title for the current book"""

View file

@ -11,7 +11,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
class Connector(BookWyrmModel):
"""book data source connectors"""
identifier = models.CharField(max_length=255, unique=True)
identifier = models.CharField(max_length=255, unique=True) # domain
priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True, blank=True)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)

View file

@ -16,7 +16,7 @@ FederationStatus = [
class FederatedServer(BookWyrmModel):
"""store which servers we federate with"""
server_name = models.CharField(max_length=255, unique=True)
server_name = models.CharField(max_length=255, unique=True) # domain
status = models.CharField(
max_length=255, default="federated", choices=FederationStatus
)
@ -64,5 +64,4 @@ class FederatedServer(BookWyrmModel):
def is_blocked(cls, url: str) -> bool:
"""look up if a domain is blocked"""
url = urlparse(url)
domain = url.netloc
return cls.objects.filter(server_name=domain, status="blocked").exists()
return cls.objects.filter(server_name=url.hostname, status="blocked").exists()

View file

@ -1,7 +1,7 @@
""" do book related things with other users """
from django.db import models, IntegrityError, transaction
from django.db.models import Q
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .base_model import BookWyrmModel
from . import fields
from .relationship import UserBlocks
@ -17,7 +17,7 @@ class Group(BookWyrmModel):
def get_remote_id(self):
"""don't want the user to be in there in this case"""
return f"https://{DOMAIN}/group/{self.id}"
return f"{BASE_URL}/group/{self.id}"
@classmethod
def followers_filter(cls, queryset, viewer):

View file

@ -38,7 +38,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
"""create a link"""
# get or create the associated domain
if not self.domain:
domain = urlparse(self.url).netloc
domain = urlparse(self.url).hostname
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
# this is never broadcast, the owning model broadcasts an update

View file

@ -7,7 +7,7 @@ from django.db.models import Q
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
@ -50,7 +50,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
def get_remote_id(self):
"""don't want the user to be in there in this case"""
return f"https://{DOMAIN}/list/{self.id}"
return f"{BASE_URL}/list/{self.id}"
@property
def collection_queryset(self):

View file

@ -10,7 +10,7 @@ from .notification import Notification, NotificationType
class Move(ActivityMixin, BookWyrmModel):
"""migrating an activitypub user account"""
"""migrating an activitypub object"""
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor"

View file

@ -3,7 +3,7 @@ from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from .base_model import BookWyrmModel
@ -46,7 +46,7 @@ class Report(BookWyrmModel):
raise PermissionDenied()
def get_remote_id(self):
return f"https://{DOMAIN}/settings/reports/{self.id}"
return f"{BASE_URL}/settings/reports/{self.id}"
def comment(self, user, note):
"""comment on a report"""

View file

@ -6,7 +6,7 @@ from django.db import models
from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from bookwyrm.tasks import BROADCAST
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel
@ -71,7 +71,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
@property
def local_path(self):
"""No slugs"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "")
return self.get_remote_id().replace(BASE_URL, "")
def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf"""

View file

@ -12,7 +12,7 @@ from model_utils import FieldTracker
from bookwyrm.connectors.abstract_connector import get_data
from bookwyrm.preview_images import generate_site_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, STATIC_FULL_URL
from bookwyrm.settings import RELEASE_API
from bookwyrm.tasks import app, MISC
from .base_model import BookWyrmModel, new_access_code
@ -188,7 +188,7 @@ class SiteInvite(models.Model):
@property
def link(self):
"""formats the invite link"""
return f"https://{DOMAIN}/invite/{self.code}"
return f"{BASE_URL}/invite/{self.code}"
class InviteRequest(BookWyrmModel):
@ -235,7 +235,7 @@ class PasswordReset(models.Model):
@property
def link(self):
"""formats the invite link"""
return f"https://{DOMAIN}/password-reset/{self.code}"
return f"{BASE_URL}/password-reset/{self.code}"
# pylint: disable=unused-argument

View file

@ -392,10 +392,10 @@ class Quotation(BookStatus):
def _format_position(self) -> Optional[str]:
"""serialize page position"""
beg = self.position
end = self.endposition or 0
end = self.endposition
if self.position_mode != "PG" or not beg:
return None
return f"pp. {beg}-{end}" if end > beg else f"p. {beg}"
return f"pp. {beg}-{end}" if end else f"p. {beg}"
@property
def pure_content(self):

View file

@ -19,7 +19,7 @@ from bookwyrm.connectors import get_data, ConnectorException
from bookwyrm.models.shelf import Shelf
from bookwyrm.models.status import Status
from bookwyrm.preview_images import generate_user_preview_image_task
from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, USE_HTTPS, LANGUAGES
from bookwyrm.settings import BASE_URL, ENABLE_PREVIEW_IMAGES, LANGUAGES
from bookwyrm.signatures import create_key_pair
from bookwyrm.tasks import app, MISC
from bookwyrm.utils import regex
@ -42,12 +42,6 @@ def get_feed_filter_choices():
return [f[0] for f in FeedFilterChoices]
def site_link():
"""helper for generating links to the site"""
protocol = "https" if USE_HTTPS else "http"
return f"{protocol}://{DOMAIN}"
# pylint: disable=too-many-public-methods
class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books"""
@ -214,8 +208,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
@property
def confirmation_link(self):
"""helper for generating confirmation links"""
link = site_link()
return f"{link}/confirm-email/{self.confirmation_code}"
return f"{BASE_URL}/confirm-email/{self.confirmation_code}"
@property
def following_link(self):
@ -349,7 +342,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
if not self.local and not re.match(regex.FULL_USERNAME, self.username):
# generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id)
self.username = f"{self.username}@{actor_parts.netloc}"
self.username = f"{self.username}@{actor_parts.hostname}"
# this user already exists, no need to populate fields
if not created:
@ -369,11 +362,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
with transaction.atomic():
# populate fields for local users
link = site_link()
self.remote_id = f"{link}/user/{self.localname}"
self.remote_id = f"{BASE_URL}/user/{self.localname}"
self.followers_url = f"{self.remote_id}/followers"
self.inbox = f"{self.remote_id}/inbox"
self.shared_inbox = f"{link}/inbox"
self.shared_inbox = f"{BASE_URL}/inbox"
self.outbox = f"{self.remote_id}/outbox"
# an id needs to be set before we can proceed with related models
@ -558,7 +550,7 @@ def set_remote_server(user_id, allow_external_connections=False):
user = User.objects.get(id=user_id)
actor_parts = urlparse(user.remote_id)
federated_server = get_or_create_remote_server(
actor_parts.netloc, allow_external_connections=allow_external_connections
actor_parts.hostname, 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:

View file

@ -350,28 +350,31 @@ USE_L10N = True
USE_TZ = True
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
# Imagekit generated thumbnails
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
IMAGEKIT_CACHEFILE_DIR = "thumbnails"
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = "bookwyrm.thumbnail_generation.Strategy"
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", [])
# Storage
PROTOCOL = "http"
if USE_HTTPS:
PROTOCOL = "https"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
PORT = env.int("PORT", 443 if USE_HTTPS else 80)
if (USE_HTTPS and PORT == 443) or (not USE_HTTPS and PORT == 80):
NETLOC = DOMAIN
else:
NETLOC = f"{DOMAIN}:{PORT}"
BASE_URL = f"{PROTOCOL}://{NETLOC}"
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +{BASE_URL})"
# Storage
USE_S3 = env.bool("USE_S3", False)
USE_AZURE = env.bool("USE_AZURE", False)
S3_SIGNED_URL_EXPIRY = env.int("S3_SIGNED_URL_EXPIRY", 900)
@ -386,21 +389,32 @@ if USE_S3:
AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", None)
AWS_DEFAULT_ACL = "public-read"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", f"{PROTOCOL}:")
# S3 Static settings
STATIC_LOCATION = "static"
STATIC_URL = f"{PROTOCOL}://{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
STATIC_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{STATIC_LOCATION}/"
STATIC_FULL_URL = STATIC_URL
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
# S3 Media settings
MEDIA_LOCATION = "images"
MEDIA_URL = f"{PROTOCOL}://{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
MEDIA_URL = f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}/{MEDIA_LOCATION}/"
MEDIA_FULL_URL = MEDIA_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# S3 Exports settings
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage"
# Content Security Policy
CSP_DEFAULT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
CSP_SCRIPT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS
CSP_DEFAULT_SRC = [
"'self'",
f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}"
if AWS_S3_CUSTOM_DOMAIN
else None,
] + CSP_ADDITIONAL_HOSTS
CSP_SCRIPT_SRC = [
"'self'",
f"{AWS_S3_URL_PROTOCOL}//{AWS_S3_CUSTOM_DOMAIN}"
if AWS_S3_CUSTOM_DOMAIN
else None,
] + CSP_ADDITIONAL_HOSTS
elif USE_AZURE:
# Azure settings
AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
@ -429,11 +443,11 @@ elif USE_AZURE:
else:
# Static settings
STATIC_URL = "/static/"
STATIC_FULL_URL = f"{PROTOCOL}://{DOMAIN}{STATIC_URL}"
STATIC_FULL_URL = BASE_URL + STATIC_URL
STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
# Media settings
MEDIA_URL = "/images/"
MEDIA_FULL_URL = f"{PROTOCOL}://{DOMAIN}{MEDIA_URL}"
MEDIA_FULL_URL = BASE_URL + MEDIA_URL
DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
# Exports settings
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsFileStorage"

View file

@ -2,10 +2,10 @@
<div style="font-family: BlinkMacSystemFont,-apple-system,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif; border-radius: 6px; background-color: #efefef; max-width: 632px;">
<div style="padding: 1rem; overflow: auto;">
<div style="float: left; margin-right: 1rem;">
<a style="color: #3273dc;" href="https://{{ domain }}" style="text-decoration: none;"><img src="{{ logo }}" alt="logo" loading="lazy" decoding="async"></a>
<a style="color: #3273dc;" href="{{ base_url }}" style="text-decoration: none;"><img src="{{ logo }}" alt="logo" loading="lazy" decoding="async"></a>
</div>
<div>
<a style="color: black; text-decoration: none" href="https://{{ domain }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>
<a style="color: black; text-decoration: none" href="{{ base_url }}" style="text-decoration: none;"><strong>{{ site_name }}</strong><br>
{{ domain }}</a>
</div>
</div>
@ -18,9 +18,9 @@
</div>
<div style="padding: 1rem; font-size: 0.8rem;">
<p style="margin: 0; color: #333;">{% blocktrans %}BookWyrm hosted on <a style="color: #3273dc;" href="https://{{ domain }}">{{ site_name }}</a>{% endblocktrans %}</p>
<p style="margin: 0; color: #333;">{% blocktrans %}BookWyrm hosted on <a style="color: #3273dc;" href="{{ base_url }}">{{ site_name }}</a>{% endblocktrans %}</p>
{% if user %}
<p style="margin: 0; color: #333;"><a style="color: #3273dc;" href="https://{{ domain }}{% url 'prefs-profile' %}">{% trans "Email preference" %}</a></p>
<p style="margin: 0; color: #333;"><a style="color: #3273dc;" href="{{ base_url }}{% url 'prefs-profile' %}">{% trans "Email preference" %}</a></p>
{% endif %}
</div>
</div>

View file

@ -12,6 +12,6 @@
<p>
{% url 'code-of-conduct' as coc_path %}
{% url 'about' as about_path %}
{% blocktrans %}Learn more <a href="https://{{ domain }}{{ about_path }}">about {{ site_name }}</a>.{% endblocktrans %}
{% blocktrans %}Learn more <a href="{{ base_url }}{{ about_path }}">about {{ site_name }}</a>.{% endblocktrans %}
</p>
{% endblock %}

View file

@ -5,6 +5,6 @@
{{ invite_link }}
{% blocktrans %}Learn more about {{ site_name }}:{% endblocktrans %} https://{{ domain }}{% url 'about' %}
{% blocktrans %}Learn more about {{ site_name }}:{% endblocktrans %} {{ base_url }}{% url 'about' %}
{% endblock %}

View file

@ -10,6 +10,6 @@
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
<Url
type="text/html"
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
template="{{ BASE_URL }}{% url 'search' %}?q={searchTerms}"
/>
</OpenSearchDescription>

View file

@ -120,7 +120,7 @@ def id_to_username(user_id):
"""given an arbitrary remote id, return the username"""
if user_id:
url = urlparse(user_id)
domain = url.netloc
domain = url.hostname
parts = url.path.split("/")
name = parts[-1]
value = f"{name}@{domain}"

View file

@ -6,7 +6,7 @@ import responses
from bookwyrm import models
from bookwyrm.connectors import abstract_connector, ConnectorException
from bookwyrm.connectors.abstract_connector import Mapping, get_data
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
class AbstractConnector(TestCase):
@ -86,7 +86,7 @@ class AbstractConnector(TestCase):
def test_get_or_create_book_existing(self):
"""find an existing book by remote/origin id"""
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(self.book.remote_id, f"https://{DOMAIN}/book/{self.book.id}")
self.assertEqual(self.book.remote_id, f"{BASE_URL}/book/{self.book.id}")
self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
# dedupe by origin id
@ -95,9 +95,7 @@ class AbstractConnector(TestCase):
self.assertEqual(result, self.book)
# dedupe by remote id
result = self.connector.get_or_create_book(
f"https://{DOMAIN}/book/{self.book.id}"
)
result = self.connector.get_or_create_book(f"{BASE_URL}/book/{self.book.id}")
self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book)

View file

@ -0,0 +1,42 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value"
}
],
"id": "https://example.com/user/mouse",
"type": "Person",
"preferredUsername": "mouse",
"name": "MOUSE?? MOUSE!!",
"inbox": "https://example.com/user/mouse/inbox",
"outbox": "https://example.com/user/mouse/outbox",
"followers": "https://example.com/user/mouse/followers",
"following": "https://example.com/user/mouse/following",
"summary": "",
"publicKey": {
"id": "https://example.com/user/mouse/#main-key",
"owner": "https://example.com/user/mouse",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6QisDrjOQvkRo/MqNmSYPwqtt\nCxg/8rCW+9jKbFUKvqjTeKVotEE85122v/DCvobCCdfQuYIFdVMk+dB1xJ0iPGPg\nyU79QHY22NdV9mFKA2qtXVVxb5cxpA4PlwOHM6PM/k8B+H09OUrop2aPUAYwy+vg\n+MXyz8bAXrIS1kq6fQIDAQAB\n-----END PUBLIC KEY-----"
},
"endpoints": {
"sharedInbox": "https://example.com/inbox"
},
"bookwyrmUser": true,
"manuallyApprovesFollowers": false,
"discoverable": false,
"alsoKnownAs": [
"https://your.domain.here:4242/user/rat"
],
"devices": "https://friend.camp/users/tripofmice/collections/devices",
"tag": [],
"icon": {
"type": "Image",
"mediaType": "image/png",
"url": "https://example.com/images/avatars/AL-2-crop-50.png"
}
}

View file

@ -214,7 +214,7 @@
"attributedTo": "https://www.example.com//user/rat",
"content": "<p>I like it</p>",
"to": [
"https://your.domain.here/user/rat/followers"
"https://your.domain.here:4242/user/rat/followers"
],
"cc": [],
"replies": {
@ -395,7 +395,7 @@
"https://local.lists/9999"
],
"follows": [
"https://your.domain.here/user/rat"
"https://your.domain.here:4242/user/rat"
],
"blocks": ["https://your.domain.here/user/badger"]
"blocks": ["https://your.domain.here:4242/user/badger"]
}

View file

@ -5,7 +5,7 @@ from django.test import TestCase
from bookwyrm import models
from bookwyrm.models import base_model
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
# pylint: disable=attribute-defined-outside-init
@ -44,14 +44,14 @@ class BaseModel(TestCase):
"""these should be generated"""
self.test_model.id = 1 # pylint: disable=invalid-name
expected = self.test_model.get_remote_id()
self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
self.assertEqual(expected, f"{BASE_URL}/bookwyrmtestmodel/1")
def test_remote_id_with_user(self):
"""format of remote id when there's a user object"""
self.test_model.user = self.local_user
self.test_model.id = 1
expected = self.test_model.get_remote_id()
self.assertEqual(expected, f"https://{DOMAIN}/user/mouse/bookwyrmtestmodel/1")
self.assertEqual(expected, f"{BASE_URL}/user/mouse/bookwyrmtestmodel/1")
def test_set_remote_id(self):
"""this function sets remote ids after creation"""
@ -60,7 +60,7 @@ class BaseModel(TestCase):
instance = models.Work.objects.create(title="work title")
instance.remote_id = None
base_model.set_remote_id(None, instance, True)
self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}")
self.assertEqual(instance.remote_id, f"{BASE_URL}/book/{instance.id}")
# shouldn't set remote_id if it's not created
instance.remote_id = None

View file

@ -31,7 +31,7 @@ class Book(TestCase):
def test_remote_id(self):
"""fanciness with remote/origin ids"""
remote_id = f"https://{settings.DOMAIN}/book/{self.work.id}"
remote_id = f"{settings.BASE_URL}/book/{self.work.id}"
self.assertEqual(self.work.get_remote_id(), remote_id)
self.assertEqual(self.work.remote_id, remote_id)

View file

@ -4,7 +4,7 @@ from dataclasses import dataclass
import datetime
import json
import pathlib
import re
from urllib.parse import urlparse
from typing import List
from unittest import expectedFailure
from unittest.mock import patch
@ -22,7 +22,7 @@ from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status, Edition
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import PROTOCOL, NETLOC
# pylint: disable=too-many-public-methods
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -427,12 +427,10 @@ class ModelFields(TestCase):
instance = fields.ImageField()
output = instance.field_to_activity(user.avatar)
self.assertIsNotNone(
re.match(
rf"https:\/\/{DOMAIN}\/.*\.jpg",
output.url,
)
)
parsed_url = urlparse(output.url)
self.assertEqual(parsed_url.scheme, PROTOCOL)
self.assertEqual(parsed_url.netloc, NETLOC)
self.assertRegex(parsed_url.path, r"\.jpg$")
self.assertEqual(output.name, "")
self.assertEqual(output.type, "Image")

View file

@ -28,7 +28,7 @@ class List(TestCase):
def test_remote_id(self, *_):
"""shelves use custom remote ids"""
book_list = models.List.objects.create(name="Test List", user=self.local_user)
expected_id = f"https://{settings.DOMAIN}/list/{book_list.id}"
expected_id = f"{settings.BASE_URL}/list/{book_list.id}"
self.assertEqual(book_list.get_remote_id(), expected_id)
def test_to_activity(self, *_):

View file

@ -0,0 +1,60 @@
""" testing move models """
from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.test import TestCase
from bookwyrm import models
class MoveUser(TestCase):
"""move your account to another identity"""
@classmethod
def setUpTestData(cls):
"""we need some users for this"""
with patch("bookwyrm.models.user.set_remote_server.delay"):
cls.target_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=False,
remote_id="https://example.com/users/rat",
inbox="https://example.com/users/rat/inbox",
outbox="https://example.com/users/rat/outbox",
)
with (
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
):
cls.origin_user = models.User.objects.create_user(
"mouse", "mouse@mouse.com", "mouseword", local=True, localname="mouse"
)
cls.origin_user.remote_id = "http://local.com/user/mouse"
cls.origin_user.save(broadcast=False, update_fields=["remote_id"])
def test_user_move_unauthorized(self):
"""attempt a user move without alsoKnownAs set"""
with self.assertRaises(PermissionDenied):
models.MoveUser.objects.create(
user=self.origin_user,
object=self.origin_user.remote_id,
target=self.target_user,
)
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_user_move(self, *_):
"""move user"""
self.target_user.also_known_as.add(self.origin_user.id)
self.target_user.save(broadcast=False)
models.MoveUser.objects.create(
user=self.origin_user,
object=self.origin_user.remote_id,
target=self.target_user,
)
self.assertEqual(self.origin_user.moved_to, self.target_user.remote_id)

View file

@ -35,7 +35,7 @@ class Shelf(TestCase):
shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user
)
expected_id = f"https://{settings.DOMAIN}/user/mouse/books/test-shelf"
expected_id = f"{settings.BASE_URL}/user/mouse/books/test-shelf"
self.assertEqual(shelf.get_remote_id(), expected_id)
def test_to_activity(self, *_):

View file

@ -79,7 +79,7 @@ class SiteModels(TestCase):
def test_site_invite_link(self):
"""invite link generator"""
invite = models.SiteInvite.objects.create(user=self.local_user, code="hello")
self.assertEqual(invite.link, f"https://{settings.DOMAIN}/invite/hello")
self.assertEqual(invite.link, f"{settings.BASE_URL}/invite/hello")
def test_invite_request(self):
"""someone wants an invite"""
@ -95,7 +95,7 @@ class SiteModels(TestCase):
"""password reset token"""
token = models.PasswordReset.objects.create(user=self.local_user, code="hello")
self.assertTrue(token.valid())
self.assertEqual(token.link, f"https://{settings.DOMAIN}/password-reset/hello")
self.assertEqual(token.link, f"{settings.BASE_URL}/password-reset/hello")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.suggested_users.remove_user_task.delay")

View file

@ -60,7 +60,7 @@ class Status(TestCase):
def test_status_generated_fields(self, *_):
"""setting remote id"""
status = models.Status.objects.create(content="bleh", user=self.local_user)
expected_id = f"https://{settings.DOMAIN}/user/mouse/status/{status.id}"
expected_id = f"{settings.BASE_URL}/user/mouse/status/{status.id}"
self.assertEqual(status.remote_id, expected_id)
self.assertEqual(status.privacy, "public")
@ -151,7 +151,7 @@ class Status(TestCase):
self.assertEqual(activity["tag"][0]["type"], "Hashtag")
self.assertEqual(activity["tag"][0]["name"], "#content")
self.assertEqual(
activity["tag"][0]["href"], f"https://{settings.DOMAIN}/hashtag/{tag.id}"
activity["tag"][0]["href"], f"{settings.BASE_URL}/hashtag/{tag.id}"
)
def test_status_with_mention_to_activity(self, *_):
@ -227,11 +227,9 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False)
self.assertIsInstance(activity["attachment"], list)
self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg",
activity["attachment"][0]["url"],
)
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -263,12 +261,10 @@ class Status(TestCase):
),
)
self.assertEqual(activity["attachment"][0]["type"], "Document")
# self.assertTrue(
# re.match(
# r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
# activity["attachment"][0].url,
# )
# )
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
def test_quotation_to_activity(self, *_):
@ -306,11 +302,9 @@ class Status(TestCase):
),
)
self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg",
activity["attachment"][0]["url"],
)
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -340,8 +334,11 @@ class Status(TestCase):
def test_quotation_page_serialization(self, *_):
"""serialization of quotation page position"""
tests = [
("single pos", 7, None, "p. 7"),
("page range", 7, 10, "pp. 7-10"),
("single pos", "7", "", "p. 7"),
("missing beg", "", "10", None),
("page range", "7", "10", "pp. 7-10"),
("page range roman", "xv", "xvi", "pp. xv-xvi"),
("page range reverse", "14", "10", "pp. 14-10"),
]
for desc, beg, end, pages in tests:
with self.subTest(desc):
@ -355,10 +352,12 @@ class Status(TestCase):
position_mode="PG",
)
activity = status.to_activity(pure=True)
self.assertRegex(
activity["content"],
f'^<p>"my quote"</p> <p>— <a .+</a>, {pages}</p>$',
)
if pages:
pages_re = re.escape(pages)
expect_re = f'^<p>"my quote"</p> <p>— <a .+</a>, {pages_re}</p>$'
else:
expect_re = '^<p>"my quote"</p> <p>— <a .+</a></p>$'
self.assertRegex(activity["content"], expect_re)
def test_review_to_activity(self, *_):
"""subclass of the base model version with a "pure" serializer"""
@ -395,11 +394,9 @@ class Status(TestCase):
)
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0]["url"],
)
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -420,11 +417,9 @@ class Status(TestCase):
)
self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0]["url"],
)
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -443,11 +438,9 @@ class Status(TestCase):
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
)
self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue(
re.match(
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0]["url"],
)
self.assertRegex(
activity["attachment"][0]["url"],
rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
)
self.assertEqual(activity["attachment"][0]["name"], "Test Edition")

View file

@ -9,15 +9,12 @@ import responses
from bookwyrm import models
from bookwyrm.management.commands import initdb
from bookwyrm.settings import USE_HTTPS, DOMAIN
from bookwyrm.settings import DOMAIN, BASE_URL
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
class User(TestCase):
protocol = "https://" if USE_HTTPS else "http://"
@classmethod
def setUpTestData(cls):
with (
@ -49,11 +46,11 @@ class User(TestCase):
def test_computed_fields(self):
"""username instead of id here"""
expected_id = f"{self.protocol}{DOMAIN}/user/mouse"
expected_id = f"{BASE_URL}/user/mouse"
self.assertEqual(self.user.remote_id, expected_id)
self.assertEqual(self.user.username, f"mouse@{DOMAIN}")
self.assertEqual(self.user.localname, "mouse")
self.assertEqual(self.user.shared_inbox, f"{self.protocol}{DOMAIN}/inbox")
self.assertEqual(self.user.shared_inbox, f"{BASE_URL}/inbox")
self.assertEqual(self.user.inbox, f"{expected_id}/inbox")
self.assertEqual(self.user.outbox, f"{expected_id}/outbox")
self.assertEqual(self.user.followers_url, f"{expected_id}/followers")
@ -130,7 +127,7 @@ class User(TestCase):
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
):
user = models.User.objects.create_user(
f"test2{DOMAIN}",
"test2",
"test2@bookwyrm.test",
localname="test2",
**user_attrs,
@ -145,7 +142,7 @@ class User(TestCase):
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
):
user = models.User.objects.create_user(
f"test1{DOMAIN}",
"test1",
"test1@bookwyrm.test",
localname="test1",
**user_attrs,

View file

@ -15,7 +15,7 @@ from django.utils.http import http_date
from bookwyrm import models
from bookwyrm.activitypub import Follow
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import DOMAIN, NETLOC
from bookwyrm.signatures import create_key_pair, make_signature, make_digest
@ -77,7 +77,7 @@ class Signature(TestCase):
"HTTP_SIGNATURE": signature,
"HTTP_DIGEST": digest,
"HTTP_CONTENT_TYPE": "application/activity+json; charset=utf-8",
"HTTP_HOST": DOMAIN,
"HTTP_HOST": NETLOC,
},
)

View file

@ -2,6 +2,7 @@
import re
from django.test import TestCase
from bookwyrm.settings import BASE_URL
from bookwyrm.utils import regex
from bookwyrm.utils.validate import validate_url_domain
@ -15,17 +16,11 @@ class TestUtils(TestCase):
def test_valid_url_domain(self):
"""Check with a valid URL"""
self.assertEqual(
validate_url_domain("https://your.domain.here/legit-book-url/"),
"https://your.domain.here/legit-book-url/",
)
legit = f"{BASE_URL}/legit-book-url/"
self.assertEqual(validate_url_domain(legit), legit)
def test_invalid_url_domain(self):
"""Check with an invalid URL"""
self.assertIsNone(
validate_url_domain("https://up-to-no-good.tld/bad-actor.exe")
)
def test_default_url_domain(self):
"""Check with a default URL"""
self.assertEqual(validate_url_domain("/"), "/")

View file

@ -0,0 +1,114 @@
""" test move functionality """
import json
from unittest.mock import patch
import pathlib
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import RequestFactory
import responses
from bookwyrm import forms, models, views
@patch("bookwyrm.activitystreams.add_status_task.delay")
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@patch("bookwyrm.activitystreams.populate_stream_task.delay")
@patch("bookwyrm.suggested_users.rerank_user_task.delay")
class ViewsHelpers(TestCase):
"""viewing and creating statuses"""
@classmethod
def setUpTestData(cls):
"""we need basic test data and mocks"""
with (
patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"),
patch("bookwyrm.activitystreams.populate_stream_task.delay"),
patch("bookwyrm.lists_stream.populate_lists_task.delay"),
patch("bookwyrm.suggested_users.rerank_user_task.delay"),
):
cls.local_user = models.User.objects.create_user(
"rat",
"rat@rat.com",
"ratword",
local=True,
discoverable=True,
localname="rat",
)
with (
patch("bookwyrm.models.user.set_remote_server.delay"),
patch("bookwyrm.suggested_users.rerank_user_task.delay"),
):
cls.remote_user = models.User.objects.create_user(
"mouse@example.com",
"mouse@mouse.com",
"mouseword",
local=False,
remote_id="https://example.com/user/mouse",
)
def setUp(self):
"""individual test setup"""
self.factory = RequestFactory()
datafile = pathlib.Path(__file__).parent.joinpath(
"../../data/ap_user_move.json"
)
self.userdata = json.loads(datafile.read_bytes())
del self.userdata["icon"]
@responses.activate
@patch("bookwyrm.models.user.set_remote_server.delay")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
@patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async")
def test_move_user_view(self, *_):
"""move user"""
self.assertEqual(self.remote_user.remote_id, "https://example.com/user/mouse")
self.assertIsNone(self.local_user.moved_to)
self.assertIsNone(self.remote_user.moved_to)
self.assertIsNone(self.local_user.also_known_as.first())
self.assertIsNone(self.remote_user.also_known_as.first())
username = "mouse@example.com"
wellknown = {
"subject": "acct:mouse@example.com",
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://example.com/user/mouse",
}
],
}
responses.add(
responses.GET,
f"https://example.com/.well-known/webfinger?resource=acct:{username}",
json=wellknown,
status=200,
)
responses.add(
responses.GET,
"https://example.com/user/mouse",
json=self.userdata,
status=200,
)
view = views.MoveUser.as_view()
form = forms.MoveUserForm()
form.data["target"] = "mouse@example.com"
form.data["password"] = "ratword"
request = self.factory.post("", form.data)
request.user = self.local_user
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.apply_async"):
view(request)
self.local_user.refresh_from_db()
self.assertEqual(self.local_user.also_known_as.first(), self.remote_user)
self.assertEqual(self.remote_user.also_known_as.first(), self.local_user)
self.assertEqual(self.local_user.moved_to, "https://example.com/user/mouse")

View file

@ -8,7 +8,7 @@ from django.test.client import RequestFactory
import responses
from bookwyrm import models, views
from bookwyrm.settings import USER_AGENT, DOMAIN
from bookwyrm.settings import USER_AGENT, BASE_URL
@patch("bookwyrm.activitystreams.add_status_task.delay")
@ -288,13 +288,13 @@ class ViewsHelpers(TestCase): # pylint: disable=too-many-public-methods
def test_redirect_to_referer_valid_domain(self, *_):
"""redirect to within the app"""
request = self.factory.get("/path")
request.META = {"HTTP_REFERER": f"https://{DOMAIN}/and/a/path"}
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path"}
result = views.helpers.redirect_to_referer(request)
self.assertEqual(result.url, f"https://{DOMAIN}/and/a/path")
self.assertEqual(result.url, f"{BASE_URL}/and/a/path")
def test_redirect_to_referer_with_get_args(self, *_):
"""if the path has get params (like sort) they are preserved"""
request = self.factory.get("/path")
request.META = {"HTTP_REFERER": f"https://{DOMAIN}/and/a/path?sort=hello"}
request.META = {"HTTP_REFERER": f"{BASE_URL}/and/a/path?sort=hello"}
result = views.helpers.redirect_to_referer(request)
self.assertEqual(result.url, f"https://{DOMAIN}/and/a/path?sort=hello")
self.assertEqual(result.url, f"{BASE_URL}/and/a/path?sort=hello")

View file

@ -8,7 +8,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.tests.validate_html import validate_html
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
class IsbnViews(TestCase):
@ -55,7 +55,7 @@ class IsbnViews(TestCase):
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book")
self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
self.assertEqual(data[0]["key"], f"{BASE_URL}/book/{self.book.id}")
def test_isbn_html_response(self):
"""searches local data only and returns book data in json format"""

View file

@ -10,7 +10,7 @@ from django.test.client import RequestFactory
from bookwyrm import models, views
from bookwyrm.book_search import SearchResult
from bookwyrm.settings import DOMAIN
from bookwyrm.settings import BASE_URL
from bookwyrm.tests.validate_html import validate_html
@ -57,7 +57,7 @@ class Views(TestCase):
data = json.loads(response.content)
self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book")
self.assertEqual(data[0]["key"], f"https://{DOMAIN}/book/{self.book.id}")
self.assertEqual(data[0]["key"], f"{BASE_URL}/book/{self.book.id}")
def test_search_no_query(self):
"""just the search page"""

View file

@ -1,21 +1,15 @@
"""Validations"""
from typing import Optional
from bookwyrm.settings import DOMAIN, USE_HTTPS
from bookwyrm.settings import BASE_URL
def validate_url_domain(url: str) -> Optional[str]:
def validate_url_domain(url: Optional[str]) -> Optional[str]:
"""Basic check that the URL starts with the instance domain name"""
if not url:
if url is None:
return None
if url == "/":
return url
if not url.startswith(BASE_URL):
return None
protocol = "https://" if USE_HTTPS else "http://"
origin = f"{protocol}{DOMAIN}"
if url.startswith(origin):
return url
return None
return url

View file

@ -61,7 +61,7 @@ def is_bookwyrm_request(request):
return True
def handle_remote_webfinger(query, unknown_only=False):
def handle_remote_webfinger(query, unknown_only=False, refresh=False):
"""webfingerin' other servers"""
user = None
@ -76,6 +76,11 @@ def handle_remote_webfinger(query, unknown_only=False):
return None
try:
if refresh:
# Always fetch the remote info - don't even bother checking the DB
raise models.User.DoesNotExist("remote_only is set to True")
user = models.User.objects.get(username__iexact=query)
if unknown_only:
@ -93,7 +98,7 @@ def handle_remote_webfinger(query, unknown_only=False):
if link.get("rel") == "self":
try:
user = activitypub.resolve_remote_id(
link["href"], model=models.User
link["href"], model=models.User, refresh=refresh
)
except (KeyError, activitypub.ActivitySerializerError):
return None

View file

@ -32,7 +32,7 @@ class MoveUser(View):
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
username = form.cleaned_data["target"]
target = handle_remote_webfinger(username)
target = handle_remote_webfinger(username, refresh=True)
try:
models.MoveUser.objects.create(
@ -81,6 +81,7 @@ class AliasUser(View):
return TemplateResponse(request, "preferences/alias_user.html", data)
user.also_known_as.add(remote_user.id)
user.save(broadcast=True) # broadcast the alias
return redirect("prefs-alias")

View file

@ -9,7 +9,7 @@ from django.utils import timezone
from django.views.decorators.http import require_GET
from bookwyrm import models
from bookwyrm.settings import DOMAIN, VERSION, LANGUAGE_CODE
from bookwyrm.settings import BASE_URL, DOMAIN, VERSION, LANGUAGE_CODE
@require_GET
@ -34,7 +34,7 @@ def webfinger(request):
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
"template": f"{BASE_URL}/ostatus_subscribe?acct={{uri}}",
},
],
}

View file

@ -11,19 +11,20 @@ env =
DEBUG = false
USE_HTTPS = true
DOMAIN = your.domain.here
PORT = 4242
ALLOWED_HOSTS = your.domain.here
BOOKWYRM_DATABASE_BACKEND = postgres
MEDIA_ROOT = images/
CELERY_BROKER = ""
CELERY_BROKER =
REDIS_BROKER_PORT = 6379
REDIS_BROKER_PASSWORD = beep
REDIS_ACTIVITY_PORT = 6379
REDIS_ACTIVITY_PASSWORD = beep
USE_DUMMY_CACHE = true
FLOWER_PORT = 8888
EMAIL_HOST = "smtp.mailgun.org"
EMAIL_HOST = smtp.mailgun.org
EMAIL_PORT = 587
EMAIL_HOST_USER = ""
EMAIL_HOST_PASSWORD = ""
EMAIL_HOST_USER =
EMAIL_HOST_PASSWORD =
EMAIL_USE_TLS = true
ENABLE_PREVIEW_IMAGES = false

View file

@ -1,4 +1,4 @@
aiohttp==3.9.2
aiohttp==3.9.4
bleach==5.0.1
boto3==1.26.57
bw-file-resubmit==0.6.0rc2
@ -47,6 +47,7 @@ black==22.*
celery-types==0.18.0
django-stubs[compatible-mypy]==4.2.4
mypy==1.5.1
pre-commit
pylint==2.15.0
pytest==6.2.5
pytest-cov==2.10.1