diff --git a/.github/workflows/django-tests.yml b/.github/workflows/django-tests.yml index 03147744a..038751935 100644 --- a/.github/workflows/django-tests.yml +++ b/.github/workflows/django-tests.yml @@ -36,6 +36,7 @@ jobs: env: SECRET_KEY: beepbeep DEBUG: false + USE_HTTPS: true DOMAIN: your.domain.here BOOKWYRM_DATABASE_BACKEND: postgres MEDIA_ROOT: images/ diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index da32fbaf1..ddd45426f 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -48,7 +48,7 @@ class Signature: def naive_parse(activity_objects, activity_json, serializer=None): - """this navigates circular import issues""" + """this navigates circular import issues by looking up models' serializers""" if not serializer: if activity_json.get("publicKeyPem"): # ugh diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index bad7c59f8..0a90c9f4e 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -4,7 +4,7 @@ from django.db.models import signals, Q from bookwyrm import models from bookwyrm.redis_store import RedisStore, r -from bookwyrm.settings import STREAMS +from bookwyrm.tasks import app from bookwyrm.views.helpers import privacy_filter @@ -56,7 +56,13 @@ class ActivityStream(RedisStore): return ( models.Status.objects.select_subclasses() .filter(id__in=statuses) - .select_related("user", "reply_parent") + .select_related( + "user", + "reply_parent", + "comment__book", + "review__book", + "quotation__book", + ) .prefetch_related("mention_books", "mention_users") .order_by("-published_date") ) @@ -235,15 +241,10 @@ class BooksStream(ActivityStream): # determine which streams are enabled in settings.py -available_streams = [s["key"] for s in STREAMS] streams = { - k: v - for (k, v) in { - "home": HomeStream(), - "local": LocalStream(), - "books": BooksStream(), - }.items() - if k in available_streams + "home": HomeStream(), + "local": LocalStream(), + "books": BooksStream(), } @@ -260,9 +261,6 @@ def add_status_on_create(sender, instance, created, *args, **kwargs): stream.remove_object_from_related_stores(instance) return - if not created: - return - for stream in streams.values(): stream.add_status(instance) @@ -358,25 +356,47 @@ def add_statuses_on_shelve(sender, instance, *args, **kwargs): """update books stream when user shelves a book""" if not instance.user.local: return - # check if the book is already on the user's shelves - if models.ShelfBook.objects.filter( - user=instance.user, book__in=instance.book.parent_work.editions.all() - ).exists(): + book = None + if hasattr(instance, "book"): + book = instance.book + elif instance.mention_books.exists(): + book = instance.mention_books.first() + if not book: return - BooksStream().add_book_statuses(instance.user, instance.book) + # check if the book is already on the user's shelves + editions = book.parent_work.editions.all() + if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): + return + + BooksStream().add_book_statuses(instance.user, book) @receiver(signals.post_delete, sender=models.ShelfBook) # pylint: disable=unused-argument -def remove_statuses_on_shelve(sender, instance, *args, **kwargs): +def remove_statuses_on_unshelve(sender, instance, *args, **kwargs): """update books stream when user unshelves a book""" if not instance.user.local: return + + book = None + if hasattr(instance, "book"): + book = instance.book + elif instance.mention_books.exists(): + book = instance.mention_books.first() + if not book: + return # check if the book is actually unshelved, not just moved - if models.ShelfBook.objects.filter( - user=instance.user, book__in=instance.book.parent_work.editions.all() - ).exists(): + editions = book.parent_work.editions.all() + if models.ShelfBook.objects.filter(user=instance.user, book__in=editions).exists(): return BooksStream().remove_book_statuses(instance.user, instance.book) + + +@app.task +def populate_stream_task(stream, user_id): + """background task for populating an empty activitystream""" + user = models.User.objects.get(id=user_id) + stream = streams[stream] + stream.populate_streams(user) diff --git a/bookwyrm/connectors/inventaire.py b/bookwyrm/connectors/inventaire.py index 116aa5c11..842d09974 100644 --- a/bookwyrm/connectors/inventaire.py +++ b/bookwyrm/connectors/inventaire.py @@ -71,7 +71,7 @@ class Connector(AbstractConnector): # flatten the data so that images, uri, and claims are on the same level return { **data.get("claims", {}), - **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks"]}, + **{k: data.get(k) for k in ["uri", "image", "labels", "sitelinks", "type"]}, } def search(self, query, min_confidence=None): # pylint: disable=arguments-differ diff --git a/bookwyrm/emailing.py b/bookwyrm/emailing.py index 657310b05..fff3985ef 100644 --- a/bookwyrm/emailing.py +++ b/bookwyrm/emailing.py @@ -23,6 +23,14 @@ def email_data(): } +def email_confirmation_email(user): + """newly registered users confirm email address""" + data = email_data() + data["confirmation_code"] = user.confirmation_code + data["confirmation_link"] = user.confirmation_link + send_email.delay(user.email, *format_email("confirm", data)) + + def invite_email(invite_request): """send out an invite code""" data = email_data() diff --git a/bookwyrm/management/commands/populate_streams.py b/bookwyrm/management/commands/populate_streams.py index f8aa21a52..a04d7f5ad 100644 --- a/bookwyrm/management/commands/populate_streams.py +++ b/bookwyrm/management/commands/populate_streams.py @@ -3,22 +3,35 @@ from django.core.management.base import BaseCommand from bookwyrm import activitystreams, models -def populate_streams(): +def populate_streams(stream=None): """build all the streams for all the users""" + streams = [stream] if stream else activitystreams.streams.keys() + print("Populations streams", streams) users = models.User.objects.filter( local=True, is_active=True, - ) + ).order_by("-last_active_date") + print("This may take a long time! Please be patient.") for user in users: - for stream in activitystreams.streams.values(): - stream.populate_streams(user) + for stream_key in streams: + print(".", end="") + activitystreams.populate_stream_task.delay(stream_key, user.id) class Command(BaseCommand): """start all over with user streams""" help = "Populate streams for all users" + + def add_arguments(self, parser): + parser.add_argument( + "--stream", + default=None, + help="Specifies which time of stream to populate", + ) + # pylint: disable=no-self-use,unused-argument def handle(self, *args, **options): """run feed builder""" - populate_streams() + stream = options.get("stream") + populate_streams(stream=stream) diff --git a/bookwyrm/migrations/0082_auto_20210806_2324.py b/bookwyrm/migrations/0082_auto_20210806_2324.py new file mode 100644 index 000000000..ab0aa158b --- /dev/null +++ b/bookwyrm/migrations/0082_auto_20210806_2324.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.4 on 2021-08-06 23:24 + +import bookwyrm.models.base_model +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0081_alter_user_last_active_date"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="require_confirm_email", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="user", + name="confirmation_code", + field=models.CharField( + default=bookwyrm.models.base_model.new_access_code, max_length=32 + ), + ), + migrations.AlterField( + model_name="connector", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="deactivation_reason", + field=models.CharField( + blank=True, + choices=[ + ("pending", "Pending"), + ("self_deletion", "Self Deletion"), + ("moderator_deletion", "Moderator Deletion"), + ("domain_block", "Domain Block"), + ], + max_length=255, + null=True, + ), + ), + ] diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 729d9cba0..4e313723a 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -7,7 +7,7 @@ import operator import logging from uuid import uuid4 import requests -from requests.exceptions import HTTPError, SSLError +from requests.exceptions import RequestException from Crypto.PublicKey import RSA from Crypto.Signature import pkcs1_15 @@ -43,7 +43,7 @@ class ActivitypubMixin: reverse_unfurl = False def __init__(self, *args, **kwargs): - """collect some info on model fields""" + """collect some info on model fields for later use""" self.image_fields = [] self.many_to_many_fields = [] self.simple_fields = [] # "simple" @@ -503,7 +503,7 @@ def broadcast_task(sender_id, activity, recipients): for recipient in recipients: try: sign_and_send(sender, activity, recipient) - except (HTTPError, SSLError, requests.exceptions.ConnectionError): + except RequestException: pass diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 2cb7c0365..5b55ea50f 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -1,4 +1,6 @@ """ base model with default fields """ +import base64 +from Crypto import Random from django.db import models from django.dispatch import receiver @@ -9,6 +11,7 @@ from .fields import RemoteIdField DeactivationReason = models.TextChoices( "DeactivationReason", [ + "pending", "self_deletion", "moderator_deletion", "domain_block", @@ -16,6 +19,11 @@ DeactivationReason = models.TextChoices( ) +def new_access_code(): + """the identifier for a user invite""" + return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") + + class BookWyrmModel(models.Model): """shared fields""" diff --git a/bookwyrm/models/import_job.py b/bookwyrm/models/import_job.py index f29938469..05aada161 100644 --- a/bookwyrm/models/import_job.py +++ b/bookwyrm/models/import_job.py @@ -80,7 +80,7 @@ class ImportItem(models.Model): else: # don't fall back on title/author search is isbn is present. # you're too likely to mismatch - self.get_book_from_title_author() + self.book = self.get_book_from_title_author() def get_book_from_isbn(self): """search by isbn""" diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 872f6b454..ef3f7c3ca 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -1,8 +1,6 @@ """ the particulars for this instance of BookWyrm """ -import base64 import datetime -from Crypto import Random from django.db import models, IntegrityError from django.dispatch import receiver from django.utils import timezone @@ -10,7 +8,7 @@ from model_utils import FieldTracker from bookwyrm.preview_images import generate_site_preview_image_task from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES -from .base_model import BookWyrmModel +from .base_model import BookWyrmModel, new_access_code from .user import User @@ -33,6 +31,7 @@ class SiteSettings(models.Model): # registration allow_registration = models.BooleanField(default=True) allow_invite_requests = models.BooleanField(default=True) + require_confirm_email = models.BooleanField(default=True) # images logo = models.ImageField(upload_to="logos/", null=True, blank=True) @@ -61,11 +60,6 @@ class SiteSettings(models.Model): return default_settings -def new_access_code(): - """the identifier for a user invite""" - return base64.b32encode(Random.get_random_bytes(5)).decode("ascii") - - class SiteInvite(models.Model): """gives someone access to create an account on the instance""" diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index cb6941c9b..e10bcd293 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -17,16 +17,22 @@ from bookwyrm.connectors import get_data, ConnectorException from bookwyrm.models.shelf import Shelf from bookwyrm.models.status import Status, Review from bookwyrm.preview_images import generate_user_preview_image_task -from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES +from bookwyrm.settings import DOMAIN, ENABLE_PREVIEW_IMAGES, USE_HTTPS from bookwyrm.signatures import create_key_pair from bookwyrm.tasks import app from bookwyrm.utils import regex from .activitypub_mixin import OrderedCollectionPageMixin, ActivitypubMixin -from .base_model import BookWyrmModel, DeactivationReason +from .base_model import BookWyrmModel, DeactivationReason, new_access_code from .federated_server import FederatedServer from . import fields, Review +def site_link(): + """helper for generating links to the site""" + protocol = "https" if USE_HTTPS else "http" + return f"{protocol}://{DOMAIN}" + + class User(OrderedCollectionPageMixin, AbstractUser): """a user who wants to read books""" @@ -123,11 +129,18 @@ class User(OrderedCollectionPageMixin, AbstractUser): deactivation_reason = models.CharField( max_length=255, choices=DeactivationReason.choices, null=True, blank=True ) + confirmation_code = models.CharField(max_length=32, default=new_access_code) name_field = "username" property_fields = [("following_link", "following")] field_tracker = FieldTracker(fields=["name", "avatar"]) + @property + def confirmation_link(self): + """helper for generating confirmation links""" + link = site_link() + return f"{link}/confirm-email/{self.confirmation_code}" + @property def following_link(self): """just how to find out the following info""" @@ -207,7 +220,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.following.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, - **kwargs + **kwargs, ) def to_followers_activity(self, **kwargs): @@ -217,7 +230,7 @@ class User(OrderedCollectionPageMixin, AbstractUser): self.followers.order_by("-updated_date").all(), remote_id=remote_id, id_only=True, - **kwargs + **kwargs, ) def to_activity(self, **kwargs): @@ -259,9 +272,9 @@ class User(OrderedCollectionPageMixin, AbstractUser): return # populate fields for local users - self.remote_id = "https://%s/user/%s" % (DOMAIN, self.localname) + self.remote_id = "%s/user/%s" % (site_link(), self.localname) self.inbox = "%s/inbox" % self.remote_id - self.shared_inbox = "https://%s/inbox" % DOMAIN + self.shared_inbox = "%s/inbox" % site_link() self.outbox = "%s/outbox" % self.remote_id # an id needs to be set before we can proceed with related models diff --git a/bookwyrm/static/css/bookwyrm.css b/bookwyrm/static/css/bookwyrm.css index a2434a6f1..6eb068abc 100644 --- a/bookwyrm/static/css/bookwyrm.css +++ b/bookwyrm/static/css/bookwyrm.css @@ -29,6 +29,11 @@ body { min-width: 75% !important; } +.clip-text { + max-height: 35em; + overflow: hidden; +} + /** Utilities not covered by Bulma ******************************************************************************/ diff --git a/bookwyrm/static/js/bookwyrm.js b/bookwyrm/static/js/bookwyrm.js index e43ed134b..a4002c2d3 100644 --- a/bookwyrm/static/js/bookwyrm.js +++ b/bookwyrm/static/js/bookwyrm.js @@ -164,7 +164,7 @@ let BookWyrm = new class { } // Show/hide container. - let container = document.getElementById('hide-' + targetId); + let container = document.getElementById('hide_' + targetId); if (container) { this.toggleContainer(container, pressed); @@ -219,7 +219,7 @@ let BookWyrm = new class { /** * Check or uncheck a checbox. * - * @param {object} checkbox - DOM node + * @param {string} checkbox - id of the checkbox * @param {boolean} pressed - Is the trigger pressed? * @return {undefined} */ diff --git a/bookwyrm/templates/book/book.html b/bookwyrm/templates/book/book.html index cc34de82d..28655fa6b 100644 --- a/bookwyrm/templates/book/book.html +++ b/bookwyrm/templates/book/book.html @@ -72,8 +72,8 @@ {% if user_authenticated and not book.cover %}
{% trans "Add cover" as button_text %} - {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add-cover" controls_uid=book.id focus="modal-title-add-cover" class="is-small" %} - {% include 'book/cover_modal.html' with book=book controls_text="add-cover" controls_uid=book.id %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add_cover" controls_uid=book.id focus="modal_title_add_cover" class="is-small" %} + {% include 'book/cover_modal.html' with book=book controls_text="add_cover" controls_uid=book.id %} {% if request.GET.cover_error %}

{% trans "Failed to load cover" %}

{% endif %} @@ -128,19 +128,19 @@ {% if user_authenticated and can_edit_book and not book|book_description %} {% trans 'Add Description' as button_text %} - {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add-description" controls_uid=book.id focus="id_description" hide_active=True id="hide-description" %} + {% include 'snippets/toggle/open_button.html' with text=button_text controls_text="add_description" controls_uid=book.id focus="id_description" hide_active=True id="hide_description" %} -
{% trans "Add read dates" as button_text %} - {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add-readthrough" focus="add-readthrough-focus" %} + {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="plus" class="is-small" controls_text="add_readthrough" focus="add_readthrough_focus_" %}
-
diff --git a/bookwyrm/templates/book/readthrough.html b/bookwyrm/templates/book/readthrough.html index 751407461..05ed3c639 100644 --- a/bookwyrm/templates/book/readthrough.html +++ b/bookwyrm/templates/book/readthrough.html @@ -2,7 +2,7 @@ {% load humanize %} {% load tz %}
-
+
{% trans "Progress Updates:" %} @@ -24,7 +24,7 @@ {% if readthrough.progress %} {% trans "Show all updates" as button_text %} {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="updates" controls_uid=readthrough.id class="is-small" %} -
@@ -69,15 +69,15 @@
-