Skip to content

Commit

Permalink
Merge branch 'main' into snyk-fix-9a849cac3dc5e8478c9301a21fb9e876
Browse files Browse the repository at this point in the history
  • Loading branch information
MaggieFero authored Feb 29, 2024
2 parents 650b86e + 955443e commit b238627
Show file tree
Hide file tree
Showing 384 changed files with 37,878 additions and 10,146 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,10 @@ TWO_FACTOR_LOGIN_MAX_SECONDS=60
# and AWS_S3_CUSTOM_DOMAIN (if used) are added by default.
# Value should be a comma-separated list of host names.
CSP_ADDITIONAL_HOSTS=

# The last number here means "megabytes"
# Increase if users are having trouble uploading BookWyrm export files.
DATA_UPLOAD_MAX_MEMORY_SIZE = (1024**2 * 100)

# Time before being logged out (in seconds)
# SESSION_COOKIE_AGE=2592000 # current default: 30 days
9 changes: 9 additions & 0 deletions .github/workflows/django-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Check migrations up-to-date
run: |
python ./manage.py makemigrations --check
env:
SECRET_KEY: beepbeep
DOMAIN: your.domain.here
EMAIL_HOST: ""
EMAIL_HOST_USER: ""
EMAIL_HOST_PASSWORD: ""
- name: Run Tests
env:
SECRET_KEY: beepbeep
Expand Down
1 change: 1 addition & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'trailingComma': 'es5'
5 changes: 3 additions & 2 deletions FEDERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ User relationship interactions follow the standard ActivityPub spec.
- `Block`: prevent users from seeing one another's statuses, and prevents the blocked user from viewing the actor's profile
- `Update`: updates a user's profile and settings
- `Delete`: deactivates a user
- `Undo`: reverses a `Follow` or `Block`
- `Undo`: reverses a `Block` or `Follow`

### Activities
- `Create/Status`: saves a new status in the database.
- `Delete/Status`: Removes a status
- `Like/Status`: Creates a favorite on the status
- `Announce/Status`: Boosts the status into the actor's timeline
- `Undo/*`,: Reverses a `Like` or `Announce`
- `Undo/*`,: Reverses an `Announce`, `Like`, or `Move`
- `Move/User`: Moves a user from one ActivityPub id to another.

### Collections
User's books and lists are represented by [`OrderedCollection`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-orderedcollection)
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.7.2
7 changes: 6 additions & 1 deletion bookwyrm/activitypub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

from .base_activity import ActivityEncoder, Signature, naive_parse
from .base_activity import Link, Mention, Hashtag
from .base_activity import ActivitySerializerError, resolve_remote_id
from .base_activity import (
ActivitySerializerError,
resolve_remote_id,
get_representative,
)
from .image import Document, Image
from .note import Note, GeneratedNote, Article, Comment, Quotation
from .note import Review, Rating
Expand All @@ -19,6 +23,7 @@
from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, Remove
from .verbs import Announce, Like
from .verbs import Move

# this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known
Expand Down
27 changes: 13 additions & 14 deletions bookwyrm/activitypub/base_activity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" basics for an activitypub serializer """
from __future__ import annotations
from dataclasses import dataclass, fields, MISSING
from json import JSONEncoder
import logging
Expand Down Expand Up @@ -72,8 +73,10 @@ class ActivityObject:

def __init__(
self,
activity_objects: Optional[list[str, base_model.BookWyrmModel]] = None,
**kwargs: dict[str, Any],
activity_objects: Optional[
dict[str, Union[str, list[str], ActivityObject, base_model.BookWyrmModel]]
] = None,
**kwargs: Any,
):
"""this lets you pass in an object with fields that aren't in the
dataclass, which it ignores. Any field in the dataclass is required or
Expand Down Expand Up @@ -233,7 +236,7 @@ def serialize(self, **kwargs):
omit = kwargs.get("omit", ())
data = self.__dict__.copy()
# recursively serialize
for (k, v) in data.items():
for k, v in data.items():
try:
if issubclass(type(v), ActivityObject):
data[k] = v.serialize()
Expand Down Expand Up @@ -393,19 +396,15 @@ def resolve_remote_id(

def get_representative():
"""Get or create an actor representing the instance
to sign requests to 'secure mastodon' servers"""
username = f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}"
email = "bookwyrm@localhost"
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
user = models.User.objects.create_user(
username=username,
email=email,
to sign outgoing HTTP GET requests"""
return models.User.objects.get_or_create(
username=f"{INSTANCE_ACTOR_USERNAME}@{DOMAIN}",
defaults=dict(
email="bookwyrm@localhost",
local=True,
localname=INSTANCE_ACTOR_USERNAME,
)
return user
),
)[0]


def get_activitypub_data(url):
Expand Down
4 changes: 2 additions & 2 deletions bookwyrm/activitypub/book.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ class BookData(ActivityObject):
aasin: Optional[str] = None
isfdb: Optional[str] = None
lastEditedBy: Optional[str] = None
links: list[str] = field(default_factory=list)
fileLinks: list[str] = field(default_factory=list)


# pylint: disable=invalid-name
Expand All @@ -45,6 +43,8 @@ class Book(BookData):
firstPublishedDate: str = ""
publishedDate: str = ""

fileLinks: list[str] = field(default_factory=list)

cover: Optional[Document] = None
type: str = "Book"

Expand Down
2 changes: 2 additions & 0 deletions bookwyrm/activitypub/person.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ class Person(ActivityObject):
manuallyApprovesFollowers: str = False
discoverable: str = False
hideFollows: str = False
movedTo: str = None
alsoKnownAs: dict[str] = None
type: str = "Person"
43 changes: 40 additions & 3 deletions bookwyrm/activitypub/verbs.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,19 @@ class Reject(Verb):
type: str = "Reject"

def action(self, allow_external_connections=True):
"""reject a follow request"""
obj = self.object.to_model(save=False, allow_create=False)
obj.reject()
"""reject a follow or follow request"""

for model_name in ["UserFollowRequest", "UserFollows", None]:
model = apps.get_model(f"bookwyrm.{model_name}") if model_name else None
if obj := self.object.to_model(
model=model,
save=False,
allow_create=False,
allow_external_connections=allow_external_connections,
):
# Reject the first model that can be built.
obj.reject()
break


@dataclass(init=False)
Expand Down Expand Up @@ -231,3 +241,30 @@ class Announce(Verb):
def action(self, allow_external_connections=True):
"""boost"""
self.to_model(allow_external_connections=allow_external_connections)


@dataclass(init=False)
class Move(Verb):
"""a user moving an object"""

object: str
type: str = "Move"
origin: str = None
target: str = None

def action(self, allow_external_connections=True):
"""move"""

object_is_user = resolve_remote_id(remote_id=self.object, model="User")

if object_is_user:
model = apps.get_model("bookwyrm.MoveUser")

self.to_model(
model=model,
save=True,
allow_external_connections=allow_external_connections,
)
else:
# we might do something with this to move other objects at some point
pass
32 changes: 24 additions & 8 deletions bookwyrm/book_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ def search(
min_confidence: float = 0,
filters: Optional[list[Any]] = None,
return_first: bool = False,
books: Optional[QuerySet[models.Edition]] = None,
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
"""search your local database"""
filters = filters or []
Expand All @@ -54,13 +55,15 @@ def search(
# first, try searching unique identifiers
# unique identifiers never have spaces, title/author usually do
if not " " in query:
results = search_identifiers(query, *filters, return_first=return_first)
results = search_identifiers(
query, *filters, return_first=return_first, books=books
)

# if there were no identifier results...
if not results:
# then try searching title/author
results = search_title_author(
query, min_confidence, *filters, return_first=return_first
query, min_confidence, *filters, return_first=return_first, books=books
)
return results

Expand Down Expand Up @@ -98,9 +101,17 @@ def format_search_result(search_result):


def search_identifiers(
query, *filters, return_first=False
query,
*filters,
return_first=False,
books=None,
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
"""tries remote_id, isbn; defined as dedupe fields on the model"""
"""search Editions by deduplication fields
Best for cases when we can assume someone is searching for an exact match on
commonly unique data identifiers like isbn or specific library ids.
"""
books = books or models.Edition.objects
if connectors.maybe_isbn(query):
# Oh did you think the 'S' in ISBN stood for 'standard'?
normalized_isbn = query.strip().upper().rjust(10, "0")
Expand All @@ -111,7 +122,7 @@ def search_identifiers(
for f in models.Edition._meta.get_fields()
if hasattr(f, "deduplication_field") and f.deduplication_field
]
results = models.Edition.objects.filter(
results = books.filter(
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
).distinct()

Expand All @@ -121,12 +132,17 @@ def search_identifiers(


def search_title_author(
query, min_confidence, *filters, return_first=False
query,
min_confidence,
*filters,
return_first=False,
books=None,
) -> QuerySet[models.Edition]:
"""searches for title and author"""
books = books or models.Edition.objects
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
results = (
models.Edition.objects.filter(*filters, search_vector=query)
books.filter(*filters, search_vector=query)
.annotate(rank=SearchRank(F("search_vector"), query))
.filter(rank__gt=min_confidence)
.order_by("-rank")
Expand All @@ -137,7 +153,7 @@ def search_title_author(

# filter out multiple editions of the same work
list_results = []
for work_id in set(editions_of_work[:30]):
for work_id in editions_of_work[:30]:
result = (
results.filter(parent_work=work_id)
.order_by("-rank", "-edition_rank")
Expand Down
7 changes: 3 additions & 4 deletions bookwyrm/forms/books.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
""" using django model forms """
from django import forms

from file_resubmit.widgets import ResubmitImageWidget

from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
from .widgets import ArrayWidget, SelectDateWidget, Select

Expand Down Expand Up @@ -70,9 +71,7 @@ class Meta:
"published_date": SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"cover": ResubmitImageWidget(attrs={"aria-describedby": "desc_cover"}),
"physical_format": Select(
attrs={"aria-describedby": "desc_physical_format"}
),
Expand Down
16 changes: 16 additions & 0 deletions bookwyrm/forms/edit_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ class Meta:
fields = ["password"]


class MoveUserForm(CustomForm):
target = forms.CharField(widget=forms.TextInput)

class Meta:
model = models.User
fields = ["password"]


class AliasUserForm(CustomForm):
username = forms.CharField(widget=forms.TextInput)

class Meta:
model = models.User
fields = ["password"]


class ChangePasswordForm(CustomForm):
current_password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
Expand Down
4 changes: 4 additions & 0 deletions bookwyrm/forms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class ImportForm(forms.Form):
csv_file = forms.FileField()


class ImportUserForm(forms.Form):
archive_file = forms.FileField()


class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
Expand Down
1 change: 1 addition & 0 deletions bookwyrm/importers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" import classes """

from .importer import Importer
from .bookwyrm_import import BookwyrmImporter
from .calibre_import import CalibreImporter
from .goodreads_import import GoodreadsImporter
from .librarything_import import LibrarythingImporter
Expand Down
24 changes: 24 additions & 0 deletions bookwyrm/importers/bookwyrm_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Import data from Bookwyrm export files"""
from django.http import QueryDict

from bookwyrm.models import User
from bookwyrm.models.bookwyrm_import_job import BookwyrmImportJob


class BookwyrmImporter:
"""Import a Bookwyrm User export file.
This is kind of a combination of an importer and a connector.
"""

# pylint: disable=no-self-use
def process_import(
self, user: User, archive_file: bytes, settings: QueryDict
) -> BookwyrmImportJob:
"""import user data from a Bookwyrm export file"""

required = [k for k in settings if settings.get(k) == "on"]

job = BookwyrmImportJob.objects.create(
user=user, archive_file=archive_file, required=required
)
return job
Loading

0 comments on commit b238627

Please sign in to comment.