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 ## Leave unset to allow all hosts
# ALLOWED_HOSTS="localhost,127.0.0.1,[::1]" # 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/ MEDIA_ROOT=images/
# Database configuration # Database configuration
@ -78,10 +83,13 @@ S3_SIGNED_URL_EXPIRY=900
# Commented are example values if you use a non-AWS, S3-compatible service # 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 # 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, # 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_STORAGE_BUCKET_NAME= # "example-bucket-name"
# AWS_S3_CUSTOM_DOMAIN=None # "example-bucket-name.s3.fr-par.scw.cloud" # 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_REGION_NAME=None # "fr-par"
# AWS_S3_ENDPOINT_URL=None # "https://s3.fr-par.scw.cloud" # 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_VALIDITY_WINDOW=2
TWO_FACTOR_LOGIN_MAX_SECONDS=60 TWO_FACTOR_LOGIN_MAX_SECONDS=60
# Additional hosts to allow in the Content-Security-Policy, "self" (should be DOMAIN) # Additional hosts to allow in the Content-Security-Policy, "self" (should be
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default. # DOMAIN with optionally ":" + PORT) and AWS_S3_CUSTOM_DOMAIN (if used) are
# Value should be a comma-separated list of host names. # added by default. Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS= CSP_ADDITIONAL_HOSTS=
# Time before being logged out (in seconds) # Time before being logged out (in seconds)

3
.gitignore vendored
View file

@ -38,3 +38,6 @@ nginx/default.conf
#macOS #macOS
**/.DS_Store **/.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: def get_or_create_connector(remote_id: str) -> abstract_connector.AbstractConnector:
"""get the connector related to the object's server""" """get the connector related to the object's server"""
url = urlparse(remote_id) url = urlparse(remote_id)
identifier = url.netloc identifier = url.hostname
if not identifier: if not identifier:
raise ValueError("Invalid remote id") raise ValueError(f"Invalid remote id: {remote_id}")
base_url = f"{url.scheme}://{url.netloc}"
try: try:
connector_info = models.Connector.objects.get(identifier=identifier) 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( connector_info = models.Connector.objects.create(
identifier=identifier, identifier=identifier,
connector_file="bookwyrm_connector", connector_file="bookwyrm_connector",
base_url=f"https://{identifier}", base_url=base_url,
books_url=f"https://{identifier}/book", books_url=f"{base_url}/book",
covers_url=f"https://{identifier}/images/covers", covers_url=f"{base_url}/images/covers",
search_url=f"https://{identifier}/search?q=", search_url=f"{base_url}/search?q=",
priority=2, priority=2,
) )
@ -188,8 +190,11 @@ def raise_not_valid_url(url: str) -> None:
if not parsed.scheme in ["http", "https"]: if not parsed.scheme in ["http", "https"]:
raise ConnectorException("Invalid scheme: ", url) raise ConnectorException("Invalid scheme: ", url)
if not parsed.hostname:
raise ConnectorException("Hostname missing: ", url)
try: try:
ipaddress.ip_address(parsed.netloc) ipaddress.ip_address(parsed.hostname)
raise ConnectorException("Provided url is an IP address: ", url) raise ConnectorException("Provided url is an IP address: ", url)
except ValueError: except ValueError:
# it's not an IP address, which is good # 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 import models, settings
from bookwyrm.tasks import app, EMAIL from bookwyrm.tasks import app, EMAIL
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN, BASE_URL
def email_data(): def email_data():
@ -14,6 +14,7 @@ def email_data():
"site_name": site.name, "site_name": site.name,
"logo": site.logo_small_url, "logo": site.logo_small_url,
"domain": DOMAIN, "domain": DOMAIN,
"base_url": BASE_URL,
"user": None, "user": None,
} }

View file

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

View file

@ -8,7 +8,7 @@ from django.contrib.postgres.indexes import GinIndex
import pgtrigger import pgtrigger
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
from bookwyrm.utils.db import format_trigger from bookwyrm.utils.db import format_trigger
from .book import BookDataModel, MergedAuthor from .book import BookDataModel, MergedAuthor
@ -70,7 +70,7 @@ class Author(BookDataModel):
def get_remote_id(self): def get_remote_id(self):
"""editions and works both use "book" instead of model_name""" """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: class Meta:
"""sets up indexes and triggers""" """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.translation import gettext_lazy as _
from django.utils.text import slugify from django.utils.text import slugify
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
from .fields import RemoteIdField from .fields import RemoteIdField
@ -38,7 +38,7 @@ class BookWyrmModel(models.Model):
def get_remote_id(self): def get_remote_id(self):
"""generate the url that resolves to the local object, without a slug""" """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"): if hasattr(self, "user"):
base_path = f"{base_path}{self.user.local_path}" base_path = f"{base_path}{self.user.local_path}"
@ -53,7 +53,7 @@ class BookWyrmModel(models.Model):
@property @property
def local_path(self): def local_path(self):
"""how to link to this object in the local app, with a slug""" """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 name = None
if hasattr(self, "name_field"): 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.isbn.isbn import hyphenator_singleton as hyphenator
from bookwyrm.preview_images import generate_edition_preview_image_task from bookwyrm.preview_images import generate_edition_preview_image_task
from bookwyrm.settings import ( from bookwyrm.settings import (
DOMAIN, BASE_URL,
DEFAULT_LANGUAGE, DEFAULT_LANGUAGE,
LANGUAGE_ARTICLES, LANGUAGE_ARTICLES,
ENABLE_PREVIEW_IMAGES, ENABLE_PREVIEW_IMAGES,
@ -327,7 +327,7 @@ class Book(BookDataModel):
def get_remote_id(self): def get_remote_id(self):
"""editions and works both use "book" instead of model_name""" """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): def guess_sort_title(self):
"""Get a best-guess sort title for the current book""" """Get a best-guess sort title for the current book"""

View file

@ -11,7 +11,7 @@ ConnectorFiles = models.TextChoices("ConnectorFiles", CONNECTORS)
class Connector(BookWyrmModel): class Connector(BookWyrmModel):
"""book data source connectors""" """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) priority = models.IntegerField(default=2)
name = models.CharField(max_length=255, null=True, blank=True) name = models.CharField(max_length=255, null=True, blank=True)
connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices) connector_file = models.CharField(max_length=255, choices=ConnectorFiles.choices)

View file

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

View file

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

View file

@ -38,7 +38,7 @@ class Link(ActivitypubMixin, BookWyrmModel):
"""create a link""" """create a link"""
# get or create the associated domain # get or create the associated domain
if not self.domain: if not self.domain:
domain = urlparse(self.url).netloc domain = urlparse(self.url).hostname
self.domain, _ = LinkDomain.objects.get_or_create(domain=domain) self.domain, _ = LinkDomain.objects.get_or_create(domain=domain)
# this is never broadcast, the owning model broadcasts an update # 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 django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -50,7 +50,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
def get_remote_id(self): def get_remote_id(self):
"""don't want the user to be in there in this case""" """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 @property
def collection_queryset(self): def collection_queryset(self):

View file

@ -10,7 +10,7 @@ from .notification import Notification, NotificationType
class Move(ActivityMixin, BookWyrmModel): class Move(ActivityMixin, BookWyrmModel):
"""migrating an activitypub user account""" """migrating an activitypub object"""
user = fields.ForeignKey( user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="actor" "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.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -46,7 +46,7 @@ class Report(BookWyrmModel):
raise PermissionDenied() raise PermissionDenied()
def get_remote_id(self): 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): def comment(self, user, note):
"""comment on a report""" """comment on a report"""

View file

@ -6,7 +6,7 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
from bookwyrm.tasks import BROADCAST from bookwyrm.tasks import BROADCAST
from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin from .activitypub_mixin import CollectionItemMixin, OrderedCollectionMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
@ -71,7 +71,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
@property @property
def local_path(self): def local_path(self):
"""No slugs""" """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): def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf""" """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.connectors.abstract_connector import get_data
from bookwyrm.preview_images import generate_site_preview_image_task 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.settings import RELEASE_API
from bookwyrm.tasks import app, MISC from bookwyrm.tasks import app, MISC
from .base_model import BookWyrmModel, new_access_code from .base_model import BookWyrmModel, new_access_code
@ -188,7 +188,7 @@ class SiteInvite(models.Model):
@property @property
def link(self): def link(self):
"""formats the invite link""" """formats the invite link"""
return f"https://{DOMAIN}/invite/{self.code}" return f"{BASE_URL}/invite/{self.code}"
class InviteRequest(BookWyrmModel): class InviteRequest(BookWyrmModel):
@ -235,7 +235,7 @@ class PasswordReset(models.Model):
@property @property
def link(self): def link(self):
"""formats the invite link""" """formats the invite link"""
return f"https://{DOMAIN}/password-reset/{self.code}" return f"{BASE_URL}/password-reset/{self.code}"
# pylint: disable=unused-argument # pylint: disable=unused-argument

View file

@ -392,10 +392,10 @@ class Quotation(BookStatus):
def _format_position(self) -> Optional[str]: def _format_position(self) -> Optional[str]:
"""serialize page position""" """serialize page position"""
beg = self.position beg = self.position
end = self.endposition or 0 end = self.endposition
if self.position_mode != "PG" or not beg: if self.position_mode != "PG" or not beg:
return None 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 @property
def pure_content(self): 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.shelf import Shelf
from bookwyrm.models.status import Status from bookwyrm.models.status import Status
from bookwyrm.preview_images import generate_user_preview_image_task 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.signatures import create_key_pair
from bookwyrm.tasks import app, MISC from bookwyrm.tasks import app, MISC
from bookwyrm.utils import regex from bookwyrm.utils import regex
@ -42,12 +42,6 @@ def get_feed_filter_choices():
return [f[0] for f in FeedFilterChoices] 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 # pylint: disable=too-many-public-methods
class User(OrderedCollectionPageMixin, AbstractUser): class User(OrderedCollectionPageMixin, AbstractUser):
"""a user who wants to read books""" """a user who wants to read books"""
@ -214,8 +208,7 @@ class User(OrderedCollectionPageMixin, AbstractUser):
@property @property
def confirmation_link(self): def confirmation_link(self):
"""helper for generating confirmation links""" """helper for generating confirmation links"""
link = site_link() return f"{BASE_URL}/confirm-email/{self.confirmation_code}"
return f"{link}/confirm-email/{self.confirmation_code}"
@property @property
def following_link(self): 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): if not self.local and not re.match(regex.FULL_USERNAME, self.username):
# generate a username that uses the domain (webfinger format) # generate a username that uses the domain (webfinger format)
actor_parts = urlparse(self.remote_id) 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 # this user already exists, no need to populate fields
if not created: if not created:
@ -369,11 +362,10 @@ class User(OrderedCollectionPageMixin, AbstractUser):
with transaction.atomic(): with transaction.atomic():
# populate fields for local users # populate fields for local users
link = site_link() self.remote_id = f"{BASE_URL}/user/{self.localname}"
self.remote_id = f"{link}/user/{self.localname}"
self.followers_url = f"{self.remote_id}/followers" self.followers_url = f"{self.remote_id}/followers"
self.inbox = f"{self.remote_id}/inbox" 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" self.outbox = f"{self.remote_id}/outbox"
# an id needs to be set before we can proceed with related models # 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) user = User.objects.get(id=user_id)
actor_parts = urlparse(user.remote_id) actor_parts = urlparse(user.remote_id)
federated_server = get_or_create_remote_server( 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 we were unable to find the server, we need to create a new entry for it
if not federated_server: if not federated_server:

View file

@ -350,28 +350,31 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
# Imagekit generated thumbnails # Imagekit generated thumbnails
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False) ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
IMAGEKIT_CACHEFILE_DIR = "thumbnails" IMAGEKIT_CACHEFILE_DIR = "thumbnails"
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = "bookwyrm.thumbnail_generation.Strategy" 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__)) PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", []) CSP_ADDITIONAL_HOSTS = env.list("CSP_ADDITIONAL_HOSTS", [])
# Storage
PROTOCOL = "http" PROTOCOL = "http"
if USE_HTTPS: if USE_HTTPS:
PROTOCOL = "https" PROTOCOL = "https"
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_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_S3 = env.bool("USE_S3", False)
USE_AZURE = env.bool("USE_AZURE", False) USE_AZURE = env.bool("USE_AZURE", False)
S3_SIGNED_URL_EXPIRY = env.int("S3_SIGNED_URL_EXPIRY", 900) 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_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL", None)
AWS_DEFAULT_ACL = "public-read" AWS_DEFAULT_ACL = "public-read"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"} AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
AWS_S3_URL_PROTOCOL = env("AWS_S3_URL_PROTOCOL", f"{PROTOCOL}:")
# S3 Static settings # S3 Static settings
STATIC_LOCATION = "static" 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 STATIC_FULL_URL = STATIC_URL
STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage" STATICFILES_STORAGE = "bookwyrm.storage_backends.StaticStorage"
# S3 Media settings # S3 Media settings
MEDIA_LOCATION = "images" 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 MEDIA_FULL_URL = MEDIA_URL
DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage" DEFAULT_FILE_STORAGE = "bookwyrm.storage_backends.ImagesStorage"
# S3 Exports settings # S3 Exports settings
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage" EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsS3Storage"
# Content Security Policy # Content Security Policy
CSP_DEFAULT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS CSP_DEFAULT_SRC = [
CSP_SCRIPT_SRC = ["'self'", AWS_S3_CUSTOM_DOMAIN] + CSP_ADDITIONAL_HOSTS "'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: elif USE_AZURE:
# Azure settings # Azure settings
AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME") AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
@ -429,11 +443,11 @@ elif USE_AZURE:
else: else:
# Static settings # Static settings
STATIC_URL = "/static/" 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" STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
# Media settings # Media settings
MEDIA_URL = "/images/" 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" DEFAULT_FILE_STORAGE = "django.core.files.storage.FileSystemStorage"
# Exports settings # Exports settings
EXPORTS_STORAGE = "bookwyrm.storage_backends.ExportsFileStorage" 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="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="padding: 1rem; overflow: auto;">
<div style="float: left; margin-right: 1rem;"> <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>
<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> {{ domain }}</a>
</div> </div>
</div> </div>
@ -18,9 +18,9 @@
</div> </div>
<div style="padding: 1rem; font-size: 0.8rem;"> <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 %} {% 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 %} {% endif %}
</div> </div>
</div> </div>

View file

@ -12,6 +12,6 @@
<p> <p>
{% url 'code-of-conduct' as coc_path %} {% url 'code-of-conduct' as coc_path %}
{% url 'about' as about_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> </p>
{% endblock %} {% endblock %}

View file

@ -5,6 +5,6 @@
{{ invite_link }} {{ 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 %} {% endblock %}

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import responses
from bookwyrm import models from bookwyrm import models
from bookwyrm.connectors import abstract_connector, ConnectorException from bookwyrm.connectors import abstract_connector, ConnectorException
from bookwyrm.connectors.abstract_connector import Mapping, get_data from bookwyrm.connectors.abstract_connector import Mapping, get_data
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
class AbstractConnector(TestCase): class AbstractConnector(TestCase):
@ -86,7 +86,7 @@ class AbstractConnector(TestCase):
def test_get_or_create_book_existing(self): def test_get_or_create_book_existing(self):
"""find an existing book by remote/origin id""" """find an existing book by remote/origin id"""
self.assertEqual(models.Book.objects.count(), 1) 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") self.assertEqual(self.book.origin_id, "https://example.com/book/1234")
# dedupe by origin id # dedupe by origin id
@ -95,9 +95,7 @@ class AbstractConnector(TestCase):
self.assertEqual(result, self.book) self.assertEqual(result, self.book)
# dedupe by remote id # dedupe by remote id
result = self.connector.get_or_create_book( result = self.connector.get_or_create_book(f"{BASE_URL}/book/{self.book.id}")
f"https://{DOMAIN}/book/{self.book.id}"
)
self.assertEqual(models.Book.objects.count(), 1) self.assertEqual(models.Book.objects.count(), 1)
self.assertEqual(result, self.book) 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", "attributedTo": "https://www.example.com//user/rat",
"content": "<p>I like it</p>", "content": "<p>I like it</p>",
"to": [ "to": [
"https://your.domain.here/user/rat/followers" "https://your.domain.here:4242/user/rat/followers"
], ],
"cc": [], "cc": [],
"replies": { "replies": {
@ -395,7 +395,7 @@
"https://local.lists/9999" "https://local.lists/9999"
], ],
"follows": [ "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 import models
from bookwyrm.models import base_model from bookwyrm.models import base_model
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
@ -44,14 +44,14 @@ class BaseModel(TestCase):
"""these should be generated""" """these should be generated"""
self.test_model.id = 1 # pylint: disable=invalid-name self.test_model.id = 1 # pylint: disable=invalid-name
expected = self.test_model.get_remote_id() 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): def test_remote_id_with_user(self):
"""format of remote id when there's a user object""" """format of remote id when there's a user object"""
self.test_model.user = self.local_user self.test_model.user = self.local_user
self.test_model.id = 1 self.test_model.id = 1
expected = self.test_model.get_remote_id() 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): def test_set_remote_id(self):
"""this function sets remote ids after creation""" """this function sets remote ids after creation"""
@ -60,7 +60,7 @@ class BaseModel(TestCase):
instance = models.Work.objects.create(title="work title") instance = models.Work.objects.create(title="work title")
instance.remote_id = None instance.remote_id = None
base_model.set_remote_id(None, instance, True) 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 # shouldn't set remote_id if it's not created
instance.remote_id = None instance.remote_id = None

View file

@ -31,7 +31,7 @@ class Book(TestCase):
def test_remote_id(self): def test_remote_id(self):
"""fanciness with remote/origin ids""" """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.get_remote_id(), remote_id)
self.assertEqual(self.work.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 datetime
import json import json
import pathlib import pathlib
import re from urllib.parse import urlparse
from typing import List from typing import List
from unittest import expectedFailure from unittest import expectedFailure
from unittest.mock import patch 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 import fields, User, Status, Edition
from bookwyrm.models.base_model import BookWyrmModel from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN from bookwyrm.settings import PROTOCOL, NETLOC
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -427,12 +427,10 @@ class ModelFields(TestCase):
instance = fields.ImageField() instance = fields.ImageField()
output = instance.field_to_activity(user.avatar) output = instance.field_to_activity(user.avatar)
self.assertIsNotNone( parsed_url = urlparse(output.url)
re.match( self.assertEqual(parsed_url.scheme, PROTOCOL)
rf"https:\/\/{DOMAIN}\/.*\.jpg", self.assertEqual(parsed_url.netloc, NETLOC)
output.url, self.assertRegex(parsed_url.path, r"\.jpg$")
)
)
self.assertEqual(output.name, "") self.assertEqual(output.name, "")
self.assertEqual(output.type, "Image") self.assertEqual(output.type, "Image")

View file

@ -28,7 +28,7 @@ class List(TestCase):
def test_remote_id(self, *_): def test_remote_id(self, *_):
"""shelves use custom remote ids""" """shelves use custom remote ids"""
book_list = models.List.objects.create(name="Test List", user=self.local_user) 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) self.assertEqual(book_list.get_remote_id(), expected_id)
def test_to_activity(self, *_): 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( shelf = models.Shelf.objects.create(
name="Test Shelf", identifier="test-shelf", user=self.local_user 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) self.assertEqual(shelf.get_remote_id(), expected_id)
def test_to_activity(self, *_): def test_to_activity(self, *_):

View file

@ -79,7 +79,7 @@ class SiteModels(TestCase):
def test_site_invite_link(self): def test_site_invite_link(self):
"""invite link generator""" """invite link generator"""
invite = models.SiteInvite.objects.create(user=self.local_user, code="hello") 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): def test_invite_request(self):
"""someone wants an invite""" """someone wants an invite"""
@ -95,7 +95,7 @@ class SiteModels(TestCase):
"""password reset token""" """password reset token"""
token = models.PasswordReset.objects.create(user=self.local_user, code="hello") token = models.PasswordReset.objects.create(user=self.local_user, code="hello")
self.assertTrue(token.valid()) 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.rerank_suggestions_task.delay")
@patch("bookwyrm.suggested_users.remove_user_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, *_): def test_status_generated_fields(self, *_):
"""setting remote id""" """setting remote id"""
status = models.Status.objects.create(content="bleh", user=self.local_user) 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.remote_id, expected_id)
self.assertEqual(status.privacy, "public") self.assertEqual(status.privacy, "public")
@ -151,7 +151,7 @@ class Status(TestCase):
self.assertEqual(activity["tag"][0]["type"], "Hashtag") self.assertEqual(activity["tag"][0]["type"], "Hashtag")
self.assertEqual(activity["tag"][0]["name"], "#content") self.assertEqual(activity["tag"][0]["name"], "#content")
self.assertEqual( 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, *_): def test_status_with_mention_to_activity(self, *_):
@ -227,11 +227,9 @@ class Status(TestCase):
self.assertEqual(activity["sensitive"], False) self.assertEqual(activity["sensitive"], False)
self.assertIsInstance(activity["attachment"], list) self.assertIsInstance(activity["attachment"], list)
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertRegex(
re.match( activity["attachment"][0]["url"],
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg", rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
activity["attachment"][0]["url"],
)
) )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -263,12 +261,10 @@ class Status(TestCase):
), ),
) )
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
# self.assertTrue( self.assertRegex(
# re.match( activity["attachment"][0]["url"],
# r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg", rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
# activity["attachment"][0].url, )
# )
# )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
def test_quotation_to_activity(self, *_): def test_quotation_to_activity(self, *_):
@ -306,11 +302,9 @@ class Status(TestCase):
), ),
) )
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertRegex(
re.match( activity["attachment"][0]["url"],
r"https:\/\/your.domain.here\/images\/covers\/test(_[A-z0-9]+)?.jpg", rf"^{settings.BASE_URL}/images/covers/test(_[A-z0-9]+)?.jpg$",
activity["attachment"][0]["url"],
)
) )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -340,8 +334,11 @@ class Status(TestCase):
def test_quotation_page_serialization(self, *_): def test_quotation_page_serialization(self, *_):
"""serialization of quotation page position""" """serialization of quotation page position"""
tests = [ tests = [
("single pos", 7, None, "p. 7"), ("single pos", "7", "", "p. 7"),
("page range", 7, 10, "pp. 7-10"), ("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: for desc, beg, end, pages in tests:
with self.subTest(desc): with self.subTest(desc):
@ -355,10 +352,12 @@ class Status(TestCase):
position_mode="PG", position_mode="PG",
) )
activity = status.to_activity(pure=True) activity = status.to_activity(pure=True)
self.assertRegex( if pages:
activity["content"], pages_re = re.escape(pages)
f'^<p>"my quote"</p> <p>— <a .+</a>, {pages}</p>$', 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, *_): def test_review_to_activity(self, *_):
"""subclass of the base model version with a "pure" serializer""" """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["content"], "test content")
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertRegex(
re.match( activity["attachment"][0]["url"],
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg", rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
activity["attachment"][0]["url"],
)
) )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") self.assertEqual(activity["attachment"][0]["name"], "Test Edition")
@ -420,11 +417,9 @@ class Status(TestCase):
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertRegex(
re.match( activity["attachment"][0]["url"],
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg", rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
activity["attachment"][0]["url"],
)
) )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") 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', f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
) )
self.assertEqual(activity["attachment"][0]["type"], "Document") self.assertEqual(activity["attachment"][0]["type"], "Document")
self.assertTrue( self.assertRegex(
re.match( activity["attachment"][0]["url"],
r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg", rf"^{settings.BASE_URL}/images/covers/test_[A-z0-9]+.jpg$",
activity["attachment"][0]["url"],
)
) )
self.assertEqual(activity["attachment"][0]["name"], "Test Edition") self.assertEqual(activity["attachment"][0]["name"], "Test Edition")

View file

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

View file

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

View file

@ -2,6 +2,7 @@
import re import re
from django.test import TestCase from django.test import TestCase
from bookwyrm.settings import BASE_URL
from bookwyrm.utils import regex from bookwyrm.utils import regex
from bookwyrm.utils.validate import validate_url_domain from bookwyrm.utils.validate import validate_url_domain
@ -15,17 +16,11 @@ class TestUtils(TestCase):
def test_valid_url_domain(self): def test_valid_url_domain(self):
"""Check with a valid URL""" """Check with a valid URL"""
self.assertEqual( legit = f"{BASE_URL}/legit-book-url/"
validate_url_domain("https://your.domain.here/legit-book-url/"), self.assertEqual(validate_url_domain(legit), legit)
"https://your.domain.here/legit-book-url/",
)
def test_invalid_url_domain(self): def test_invalid_url_domain(self):
"""Check with an invalid URL""" """Check with an invalid URL"""
self.assertIsNone( self.assertIsNone(
validate_url_domain("https://up-to-no-good.tld/bad-actor.exe") 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 import responses
from bookwyrm import models, views 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") @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, *_): def test_redirect_to_referer_valid_domain(self, *_):
"""redirect to within the app""" """redirect to within the app"""
request = self.factory.get("/path") 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) 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, *_): def test_redirect_to_referer_with_get_args(self, *_):
"""if the path has get params (like sort) they are preserved""" """if the path has get params (like sort) they are preserved"""
request = self.factory.get("/path") 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) 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 import models, views
from bookwyrm.tests.validate_html import validate_html from bookwyrm.tests.validate_html import validate_html
from bookwyrm.settings import DOMAIN from bookwyrm.settings import BASE_URL
class IsbnViews(TestCase): class IsbnViews(TestCase):
@ -55,7 +55,7 @@ class IsbnViews(TestCase):
data = json.loads(response.content) data = json.loads(response.content)
self.assertEqual(len(data), 1) self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book") 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): def test_isbn_html_response(self):
"""searches local data only and returns book data in json format""" """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 import models, views
from bookwyrm.book_search import SearchResult 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 from bookwyrm.tests.validate_html import validate_html
@ -57,7 +57,7 @@ class Views(TestCase):
data = json.loads(response.content) data = json.loads(response.content)
self.assertEqual(len(data), 1) self.assertEqual(len(data), 1)
self.assertEqual(data[0]["title"], "Test Book") 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): def test_search_no_query(self):
"""just the search page""" """just the search page"""

View file

@ -1,21 +1,15 @@
"""Validations""" """Validations"""
from typing import Optional 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""" """Basic check that the URL starts with the instance domain name"""
if not url: if url is None:
return None return None
if url == "/": if not url.startswith(BASE_URL):
return url return None
protocol = "https://" if USE_HTTPS else "http://" return url
origin = f"{protocol}{DOMAIN}"
if url.startswith(origin):
return url
return None

View file

@ -61,7 +61,7 @@ def is_bookwyrm_request(request):
return True return True
def handle_remote_webfinger(query, unknown_only=False): def handle_remote_webfinger(query, unknown_only=False, refresh=False):
"""webfingerin' other servers""" """webfingerin' other servers"""
user = None user = None
@ -76,6 +76,11 @@ def handle_remote_webfinger(query, unknown_only=False):
return None return None
try: 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) user = models.User.objects.get(username__iexact=query)
if unknown_only: if unknown_only:
@ -93,7 +98,7 @@ def handle_remote_webfinger(query, unknown_only=False):
if link.get("rel") == "self": if link.get("rel") == "self":
try: try:
user = activitypub.resolve_remote_id( user = activitypub.resolve_remote_id(
link["href"], model=models.User link["href"], model=models.User, refresh=refresh
) )
except (KeyError, activitypub.ActivitySerializerError): except (KeyError, activitypub.ActivitySerializerError):
return None return None

View file

@ -32,7 +32,7 @@ class MoveUser(View):
if form.is_valid() and user.check_password(form.cleaned_data["password"]): if form.is_valid() and user.check_password(form.cleaned_data["password"]):
username = form.cleaned_data["target"] username = form.cleaned_data["target"]
target = handle_remote_webfinger(username) target = handle_remote_webfinger(username, refresh=True)
try: try:
models.MoveUser.objects.create( models.MoveUser.objects.create(
@ -81,6 +81,7 @@ class AliasUser(View):
return TemplateResponse(request, "preferences/alias_user.html", data) return TemplateResponse(request, "preferences/alias_user.html", data)
user.also_known_as.add(remote_user.id) user.also_known_as.add(remote_user.id)
user.save(broadcast=True) # broadcast the alias
return redirect("prefs-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 django.views.decorators.http import require_GET
from bookwyrm import models from bookwyrm import models
from bookwyrm.settings import DOMAIN, VERSION, LANGUAGE_CODE from bookwyrm.settings import BASE_URL, DOMAIN, VERSION, LANGUAGE_CODE
@require_GET @require_GET
@ -34,7 +34,7 @@ def webfinger(request):
}, },
{ {
"rel": "http://ostatus.org/schema/1.0/subscribe", "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 DEBUG = false
USE_HTTPS = true USE_HTTPS = true
DOMAIN = your.domain.here DOMAIN = your.domain.here
PORT = 4242
ALLOWED_HOSTS = your.domain.here ALLOWED_HOSTS = your.domain.here
BOOKWYRM_DATABASE_BACKEND = postgres BOOKWYRM_DATABASE_BACKEND = postgres
MEDIA_ROOT = images/ MEDIA_ROOT = images/
CELERY_BROKER = "" CELERY_BROKER =
REDIS_BROKER_PORT = 6379 REDIS_BROKER_PORT = 6379
REDIS_BROKER_PASSWORD = beep REDIS_BROKER_PASSWORD = beep
REDIS_ACTIVITY_PORT = 6379 REDIS_ACTIVITY_PORT = 6379
REDIS_ACTIVITY_PASSWORD = beep REDIS_ACTIVITY_PASSWORD = beep
USE_DUMMY_CACHE = true USE_DUMMY_CACHE = true
FLOWER_PORT = 8888 FLOWER_PORT = 8888
EMAIL_HOST = "smtp.mailgun.org" EMAIL_HOST = smtp.mailgun.org
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = "" EMAIL_HOST_USER =
EMAIL_HOST_PASSWORD = "" EMAIL_HOST_PASSWORD =
EMAIL_USE_TLS = true EMAIL_USE_TLS = true
ENABLE_PREVIEW_IMAGES = false ENABLE_PREVIEW_IMAGES = false

View file

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