Skip to content

Commit

Permalink
Merge from main to avoid conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
dato committed Jan 25, 2024
2 parents 82f9aa9 + 193aeff commit 500e4eb
Show file tree
Hide file tree
Showing 305 changed files with 22,693 additions and 5,709 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ 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)
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
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.7.1
1 change: 1 addition & 0 deletions bookwyrm/activitypub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,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
20 changes: 8 additions & 12 deletions bookwyrm/activitypub/base_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,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 @@ -396,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
7 changes: 6 additions & 1 deletion bookwyrm/isbn/isbn.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ def hyphenate(self, isbn_13: Optional[str]) -> Optional[str]:
self.__element_tree = ElementTree.parse(self.__range_file_path)

gs1_prefix = isbn_13[:3]
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
try:
reg_group = self.__find_reg_group(isbn_13, gs1_prefix)
except ValueError:
# if the reg groups are invalid, just return the original isbn
return isbn_13

if reg_group is None:
return isbn_13 # failed to hyphenate

Expand Down
43 changes: 43 additions & 0 deletions bookwyrm/management/commands/erase_deleted_user_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
""" Erase any data stored about deleted users """
import sys
from django.core.management.base import BaseCommand, CommandError
from bookwyrm import models
from bookwyrm.models.user import erase_user_data

# pylint: disable=missing-function-docstring
class Command(BaseCommand):
"""command-line options"""

help = "Remove Two Factor Authorisation from user"

def add_arguments(self, parser): # pylint: disable=no-self-use
parser.add_argument(
"--dryrun",
action="store_true",
help="Preview users to be cleared without altering the database",
)

def handle(self, *args, **options): # pylint: disable=unused-argument

# Check for anything fishy
bad_state = models.User.objects.filter(is_deleted=True, is_active=True)
if bad_state.exists():
raise CommandError(
f"{bad_state.count()} user(s) marked as both active and deleted"
)

deleted_users = models.User.objects.filter(is_deleted=True)
self.stdout.write(f"Found {deleted_users.count()} deleted users")
if options["dryrun"]:
self.stdout.write("\n".join(u.username for u in deleted_users[:5]))
if deleted_users.count() > 5:
self.stdout.write("... and more")
sys.exit()

self.stdout.write("Erasing user data:")
for user_id in deleted_users.values_list("id", flat=True):
erase_user_data.delay(user_id)
self.stdout.write(".", ending="")

self.stdout.write("")
self.stdout.write("Tasks created successfully")
Loading

0 comments on commit 500e4eb

Please sign in to comment.