Skip to content

Commit

Permalink
Merge branch 'main' into check-version-number
Browse files Browse the repository at this point in the history
  • Loading branch information
mouse-reeve committed Feb 3, 2024
2 parents a1ac949 + e0667c6 commit 48f8ee5
Show file tree
Hide file tree
Showing 45 changed files with 481 additions and 119 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
30 changes: 23 additions & 7 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 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")
14 changes: 0 additions & 14 deletions bookwyrm/migrations/0184_auto_20231106_0421.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,6 @@ def update_deleted_users(apps, schema_editor):
).update(is_deleted=True)


def erase_deleted_user_data(apps, schema_editor):
"""Retroactively clear user data"""
for user in User.objects.filter(is_deleted=True):
user.erase_user_data()
user.save(
broadcast=False,
update_fields=["email", "avatar", "preview_image", "summary", "name"],
)
user.erase_user_statuses(broadcast=False)


class Migration(migrations.Migration):

dependencies = [
Expand All @@ -43,7 +32,4 @@ class Migration(migrations.Migration):
migrations.RunPython(
update_deleted_users, reverse_code=migrations.RunPython.noop
),
migrations.RunPython(
erase_deleted_user_data, reverse_code=migrations.RunPython.noop
),
]
18 changes: 18 additions & 0 deletions bookwyrm/migrations/0192_sitesettings_user_exports_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.23 on 2024-01-16 10:28

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("bookwyrm", "0191_merge_20240102_0326"),
]

operations = [
migrations.AddField(
model_name="sitesettings",
name="user_exports_enabled",
field=models.BooleanField(default=False),
),
]
25 changes: 13 additions & 12 deletions bookwyrm/models/activitypub_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ def get_recipients(self, software=None) -> list[str]:
# find anyone who's tagged in a status, for example
mentions = self.recipients if hasattr(self, "recipients") else []

# we always send activities to explicitly mentioned users' inboxes
recipients = [u.inbox for u in mentions or [] if not u.local]
# we always send activities to explicitly mentioned users (using shared inboxes
# where available to avoid duplicate submissions to a given instance)
recipients = {u.shared_inbox or u.inbox for u in mentions if not u.local}

# unless it's a dm, all the followers should receive the activity
if privacy != "direct":
Expand All @@ -173,18 +174,18 @@ def get_recipients(self, software=None) -> list[str]:
if user:
queryset = queryset.filter(following=user)

# ideally, we will send to shared inboxes for efficiency
shared_inboxes = (
queryset.filter(shared_inbox__isnull=False)
.values_list("shared_inbox", flat=True)
.distinct()
# as above, we prefer shared inboxes if available
recipients.update(
queryset.filter(shared_inbox__isnull=False).values_list(
"shared_inbox", flat=True
)
)
# but not everyone has a shared inbox
inboxes = queryset.filter(shared_inbox__isnull=True).values_list(
"inbox", flat=True
recipients.update(
queryset.filter(shared_inbox__isnull=True).values_list(
"inbox", flat=True
)
)
recipients += list(shared_inboxes) + list(inboxes)
return list(set(recipients))
return list(recipients)

def to_activity_dataclass(self):
"""convert from a model to an activity"""
Expand Down
1 change: 1 addition & 0 deletions bookwyrm/models/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class SiteSettings(SiteModel):
imports_enabled = models.BooleanField(default=True)
import_size_limit = models.IntegerField(default=0)
import_limit_reset = models.IntegerField(default=0)
user_exports_enabled = models.BooleanField(default=False)
user_import_time_limit = models.IntegerField(default=48)

field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"])
Expand Down
63 changes: 60 additions & 3 deletions bookwyrm/models/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from django.dispatch import receiver
from django.template.loader import get_template
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager

Expand Down Expand Up @@ -107,14 +109,14 @@ def delete(self, *args, **kwargs): # pylint: disable=unused-argument
@property
def recipients(self):
"""tagged users who definitely need to get this status in broadcast"""
mentions = [u for u in self.mention_users.all() if not u.local]
mentions = {u for u in self.mention_users.all() if not u.local}
if (
hasattr(self, "reply_parent")
and self.reply_parent
and not self.reply_parent.user.local
):
mentions.append(self.reply_parent.user)
return list(set(mentions))
mentions.add(self.reply_parent.user)
return list(mentions)

@classmethod
def ignore_activity(
Expand Down Expand Up @@ -178,6 +180,24 @@ def boostable(self):
"""you can't boost dms"""
return self.privacy in ["unlisted", "public"]

@property
def page_title(self):
"""title of the page when only this status is shown"""
return _("%(display_name)s's status") % {"display_name": self.user.display_name}

@property
def page_description(self):
"""description of the page in meta tags when only this status is shown"""
return None

@property
def page_image(self):
"""image to use as preview in meta tags when only this status is shown"""
if self.mention_books.exists():
book = self.mention_books.first()
return book.preview_image or book.cover
return self.user.preview_image

def to_replies(self, **kwargs):
"""helper function for loading AP serialized replies to a status"""
return self.to_ordered_collection(
Expand Down Expand Up @@ -301,6 +321,10 @@ class Meta:

abstract = True

@property
def page_image(self):
return self.book.preview_image or self.book.cover or super().page_image


class Comment(BookStatus):
"""like a review but without a rating and transient"""
Expand Down Expand Up @@ -332,6 +356,13 @@ def pure_content(self):

activity_serializer = activitypub.Comment

@property
def page_title(self):
return _("%(display_name)s's comment on %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}


class Quotation(BookStatus):
"""like a review but without a rating and transient"""
Expand Down Expand Up @@ -374,6 +405,13 @@ def pure_content(self):

activity_serializer = activitypub.Quotation

@property
def page_title(self):
return _("%(display_name)s's quote from %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}


class Review(BookStatus):
"""a book review"""
Expand Down Expand Up @@ -403,6 +441,13 @@ def pure_content(self):
"""indicate the book in question for mastodon (or w/e) users"""
return self.content

@property
def page_title(self):
return _("%(display_name)s's review of %(book_title)s") % {
"display_name": self.user.display_name,
"book_title": self.book.title,
}

activity_serializer = activitypub.Review
pure_type = "Article"

Expand All @@ -426,6 +471,18 @@ def pure_content(self):
template = get_template("snippets/generated_status/rating.html")
return template.render({"book": self.book, "rating": self.rating}).strip()

@property
def page_description(self):
return ngettext_lazy(
"%(display_name)s rated %(book_title)s: %(display_rating).1f star",
"%(display_name)s rated %(book_title)s: %(display_rating).1f stars",
"display_rating",
) % {
"display_name": self.user.display_name,
"book_title": self.book.title,
"display_rating": self.rating,
}

activity_serializer = activitypub.Rating
pure_type = "Note"

Expand Down
14 changes: 14 additions & 0 deletions bookwyrm/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,20 @@ def save(self, *args, **kwargs):
return super().save(*args, **kwargs)


@app.task(queue=MISC)
def erase_user_data(user_id):
"""Erase any custom data about this user asynchronously
This is for deleted historical user data that pre-dates data
being cleared automatically"""
user = User.objects.get(id=user_id)
user.erase_user_data()
user.save(
broadcast=False,
update_fields=["email", "avatar", "preview_image", "summary", "name"],
)
user.erase_user_statuses(broadcast=False)


@app.task(queue=MISC)
def set_remote_server(user_id, allow_external_connections=False):
"""figure out the user's remote server in the background"""
Expand Down
8 changes: 6 additions & 2 deletions bookwyrm/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@

PAGE_LENGTH = env.int("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
# TODO: extend maximum age to 1 year once termination of active sessions
# is implemented (see bookwyrm-social#2278, bookwyrm-social#3082).
SESSION_COOKIE_AGE = env.int("SESSION_COOKIE_AGE", 3600 * 24 * 30) # 1 month

JS_CACHE = "8a89cad7"

Expand Down Expand Up @@ -347,8 +350,7 @@
USE_TZ = True


agent = requests.utils.default_user_agent()
USER_AGENT = f"{agent} (BookWyrm/{VERSION}; +https://{DOMAIN}/)"
USER_AGENT = f"BookWyrm (BookWyrm/{VERSION}; +https://{DOMAIN}/)"

# Imagekit generated thumbnails
ENABLE_THUMBNAIL_GENERATION = env.bool("ENABLE_THUMBNAIL_GENERATION", False)
Expand Down Expand Up @@ -442,3 +444,5 @@
# Do not change this setting unless you already have an existing
# user with the same username - in which case you should change it!
INSTANCE_ACTOR_USERNAME = "bookwyrm.instance.actor"

DATA_UPLOAD_MAX_MEMORY_SIZE = env.int("DATA_UPLOAD_MAX_MEMORY_SIZE", (1024**2 * 100))
Loading

0 comments on commit 48f8ee5

Please sign in to comment.