diff --git a/bookwyrm/goodreads_import.py b/bookwyrm/goodreads_import.py index 5c982764b..b19994eda 100644 --- a/bookwyrm/goodreads_import.py +++ b/bookwyrm/goodreads_import.py @@ -2,10 +2,11 @@ import csv import logging -from bookwyrm import outgoing -from bookwyrm.tasks import app +from bookwyrm import models +from bookwyrm.broadcast import broadcast from bookwyrm.models import ImportJob, ImportItem from bookwyrm.status import create_notification +from bookwyrm.tasks import app logger = logging.getLogger(__name__) @@ -62,7 +63,7 @@ def import_data(job_id): item.save() # shelves book and handles reviews - outgoing.handle_imported_book( + handle_imported_book( job.user, item, job.include_reviews, job.privacy) else: item.fail_reason = 'Could not find a match for book' @@ -71,3 +72,57 @@ def import_data(job_id): create_notification(job.user, 'IMPORT', related_import=job) job.complete = True job.save() + + +def handle_imported_book(user, item, include_reviews, privacy): + ''' process a goodreads csv and then post about it ''' + if isinstance(item.book, models.Work): + item.book = item.book.default_edition + if not item.book: + return + + existing_shelf = models.ShelfBook.objects.filter( + book=item.book, added_by=user).exists() + + # shelve the book if it hasn't been shelved already + if item.shelf and not existing_shelf: + desired_shelf = models.Shelf.objects.get( + identifier=item.shelf, + user=user + ) + shelf_book = models.ShelfBook.objects.create( + book=item.book, shelf=desired_shelf, added_by=user) + broadcast(user, shelf_book.to_add_activity(user), privacy=privacy) + + for read in item.reads: + # check for an existing readthrough with the same dates + if models.ReadThrough.objects.filter( + user=user, book=item.book, + start_date=read.start_date, + finish_date=read.finish_date + ).exists(): + continue + read.book = item.book + read.user = user + read.save() + + if include_reviews and (item.rating or item.review): + review_title = 'Review of {!r} on Goodreads'.format( + item.book.title, + ) if item.review else '' + + # we don't know the publication date of the review, + # but "now" is a bad guess + published_date_guess = item.date_read or item.date_added + review = models.Review.objects.create( + user=user, + book=item.book, + name=review_title, + content=item.review, + rating=item.rating, + published_date=published_date_guess, + privacy=privacy, + ) + # we don't need to send out pure activities because non-bookwyrm + # instances don't need this data + broadcast(user, review.to_create_activity(user), privacy=privacy) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 920e99c5e..9653c5d23 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST import requests -from bookwyrm import activitypub, models, outgoing +from bookwyrm import activitypub, models, views from bookwyrm import status as status_builder from bookwyrm.tasks import app from bookwyrm.signatures import Signature @@ -133,7 +133,7 @@ def handle_follow(activity): related_user=relationship.user_subject ) if not manually_approves: - outgoing.handle_accept(relationship) + views.handle_accept(relationship) @app.task diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py deleted file mode 100644 index 2a62fd047..000000000 --- a/bookwyrm/outgoing.py +++ /dev/null @@ -1,414 +0,0 @@ -''' handles all the activity coming out of the server ''' -import re - -from django.db import IntegrityError, transaction -from django.http import JsonResponse -from django.shortcuts import get_object_or_404 -from django.views.decorators.csrf import csrf_exempt -from django.views.decorators.http import require_GET -from markdown import markdown -from requests import HTTPError - -from bookwyrm import activitypub -from bookwyrm import models -from bookwyrm.connectors import get_data, ConnectorException -from bookwyrm.broadcast import broadcast -from bookwyrm.sanitize_html import InputHtmlParser -from bookwyrm.status import create_notification -from bookwyrm.status import create_generated_note -from bookwyrm.status import delete_status -from bookwyrm.settings import DOMAIN -from bookwyrm.utils import regex - - -@csrf_exempt -@require_GET -def outbox(request, username): - ''' outbox for the requested user ''' - user = get_object_or_404(models.User, localname=username) - filter_type = request.GET.get('type') - if filter_type not in models.status_models: - filter_type = None - - return JsonResponse( - user.to_outbox(**request.GET, filter_type=filter_type), - encoder=activitypub.ActivityEncoder - ) - - -def handle_remote_webfinger(query): - ''' webfingerin' other servers ''' - user = None - - # usernames could be @user@domain or user@domain - if not query: - return None - - if query[0] == '@': - query = query[1:] - - try: - domain = query.split('@')[1] - except IndexError: - return None - - try: - user = models.User.objects.get(username=query) - except models.User.DoesNotExist: - url = 'https://%s/.well-known/webfinger?resource=acct:%s' % \ - (domain, query) - try: - data = get_data(url) - except (ConnectorException, HTTPError): - return None - - for link in data.get('links'): - if link.get('rel') == 'self': - try: - user = activitypub.resolve_remote_id( - models.User, link['href'] - ) - except KeyError: - return None - return user - - -def handle_follow(user, to_follow): - ''' someone local wants to follow someone ''' - relationship, _ = models.UserFollowRequest.objects.get_or_create( - user_subject=user, - user_object=to_follow, - ) - activity = relationship.to_activity() - broadcast(user, activity, privacy='direct', direct_recipients=[to_follow]) - - -def handle_unfollow(user, to_unfollow): - ''' someone local wants to follow someone ''' - relationship = models.UserFollows.objects.get( - user_subject=user, - user_object=to_unfollow - ) - activity = relationship.to_undo_activity(user) - broadcast(user, activity, privacy='direct', direct_recipients=[to_unfollow]) - to_unfollow.followers.remove(user) - - -def handle_accept(follow_request): - ''' send an acceptance message to a follow request ''' - user = follow_request.user_subject - to_follow = follow_request.user_object - with transaction.atomic(): - relationship = models.UserFollows.from_request(follow_request) - follow_request.delete() - relationship.save() - - activity = relationship.to_accept_activity() - broadcast(to_follow, activity, privacy='direct', direct_recipients=[user]) - - -def handle_reject(follow_request): - ''' a local user who managed follows rejects a follow request ''' - user = follow_request.user_subject - to_follow = follow_request.user_object - activity = follow_request.to_reject_activity() - follow_request.delete() - broadcast(to_follow, activity, privacy='direct', direct_recipients=[user]) - - -def handle_shelve(user, book, shelf): - ''' a local user is getting a book put on their shelf ''' - # update the database - shelve = models.ShelfBook(book=book, shelf=shelf, added_by=user) - shelve.save() - - broadcast(user, shelve.to_add_activity(user)) - - -def handle_unshelve(user, book, shelf): - ''' a local user is getting a book put on their shelf ''' - # update the database - row = models.ShelfBook.objects.get(book=book, shelf=shelf) - activity = row.to_remove_activity(user) - row.delete() - - broadcast(user, activity) - - -def handle_reading_status(user, shelf, book, privacy): - ''' post about a user reading a book ''' - # tell the world about this cool thing that happened - try: - message = { - 'to-read': 'wants to read', - 'reading': 'started reading', - 'read': 'finished reading' - }[shelf.identifier] - except KeyError: - # it's a non-standard shelf, don't worry about it - return - - status = create_generated_note( - user, - message, - mention_books=[book], - privacy=privacy - ) - status.save() - - broadcast(user, status.to_create_activity(user)) - - -def handle_imported_book(user, item, include_reviews, privacy): - ''' process a goodreads csv and then post about it ''' - if isinstance(item.book, models.Work): - item.book = item.book.default_edition - if not item.book: - return - - existing_shelf = models.ShelfBook.objects.filter( - book=item.book, added_by=user).exists() - - # shelve the book if it hasn't been shelved already - if item.shelf and not existing_shelf: - desired_shelf = models.Shelf.objects.get( - identifier=item.shelf, - user=user - ) - shelf_book = models.ShelfBook.objects.create( - book=item.book, shelf=desired_shelf, added_by=user) - broadcast(user, shelf_book.to_add_activity(user), privacy=privacy) - - for read in item.reads: - # check for an existing readthrough with the same dates - if models.ReadThrough.objects.filter( - user=user, book=item.book, - start_date=read.start_date, - finish_date=read.finish_date - ).exists(): - continue - read.book = item.book - read.user = user - read.save() - - if include_reviews and (item.rating or item.review): - review_title = 'Review of {!r} on Goodreads'.format( - item.book.title, - ) if item.review else '' - - # we don't know the publication date of the review, - # but "now" is a bad guess - published_date_guess = item.date_read or item.date_added - review = models.Review.objects.create( - user=user, - book=item.book, - name=review_title, - content=item.review, - rating=item.rating, - published_date=published_date_guess, - privacy=privacy, - ) - # we don't need to send out pure activities because non-bookwyrm - # instances don't need this data - broadcast(user, review.to_create_activity(user), privacy=privacy) - - -def handle_delete_status(user, status): - ''' delete a status and broadcast deletion to other servers ''' - delete_status(status) - broadcast(user, status.to_delete_activity(user)) - - -def handle_status(user, form): - ''' generic handler for statuses ''' - status = form.save(commit=False) - if not status.sensitive and status.content_warning: - # the cw text field remains populated when you click "remove" - status.content_warning = None - status.save() - - # inspect the text for user tags - content = status.content - for (mention_text, mention_user) in find_mentions(content): - # add them to status mentions fk - status.mention_users.add(mention_user) - - # turn the mention into a link - content = re.sub( - r'%s([^@]|$)' % mention_text, - r'%s\g<1>' % \ - (mention_user.remote_id, mention_text), - content) - - # add reply parent to mentions and notify - if status.reply_parent: - status.mention_users.add(status.reply_parent.user) - for mention_user in status.reply_parent.mention_users.all(): - status.mention_users.add(mention_user) - - if status.reply_parent.user.local: - create_notification( - status.reply_parent.user, - 'REPLY', - related_user=user, - related_status=status - ) - - # deduplicate mentions - status.mention_users.set(set(status.mention_users.all())) - # create mention notifications - for mention_user in status.mention_users.all(): - if status.reply_parent and mention_user == status.reply_parent.user: - continue - if mention_user.local: - create_notification( - mention_user, - 'MENTION', - related_user=user, - related_status=status - ) - - # don't apply formatting to generated notes - if not isinstance(status, models.GeneratedNote): - status.content = to_markdown(content) - # do apply formatting to quotes - if hasattr(status, 'quote'): - status.quote = to_markdown(status.quote) - - status.save() - - broadcast(user, status.to_create_activity(user), software='bookwyrm') - - # re-format the activity for non-bookwyrm servers - remote_activity = status.to_create_activity(user, pure=True) - broadcast(user, remote_activity, software='other') - - -def find_mentions(content): - ''' detect @mentions in raw status content ''' - for match in re.finditer(regex.strict_username, content): - username = match.group().strip().split('@')[1:] - if len(username) == 1: - # this looks like a local user (@user), fill in the domain - username.append(DOMAIN) - username = '@'.join(username) - - mention_user = handle_remote_webfinger(username) - if not mention_user: - # we can ignore users we don't know about - continue - yield (match.group(), mention_user) - - -def format_links(content): - ''' detect and format links ''' - return re.sub( - r'([^(href=")]|^|\()(https?:\/\/(%s([\w\.\-_\/+&\?=:;,])*))' % \ - regex.domain, - r'\g<1>\g<3>', - content) - -def to_markdown(content): - ''' catch links and convert to markdown ''' - content = format_links(content) - content = markdown(content) - # sanitize resulting html - sanitizer = InputHtmlParser() - sanitizer.feed(content) - return sanitizer.get_output() - - -def handle_favorite(user, status): - ''' a user likes a status ''' - try: - favorite = models.Favorite.objects.create( - status=status, - user=user - ) - except IntegrityError: - # you already fav'ed that - return - - fav_activity = favorite.to_activity() - broadcast( - user, fav_activity, privacy='direct', direct_recipients=[status.user]) - if status.user.local: - create_notification( - status.user, - 'FAVORITE', - related_user=user, - related_status=status - ) - - -def handle_unfavorite(user, status): - ''' a user likes a status ''' - try: - favorite = models.Favorite.objects.get( - status=status, - user=user - ) - except models.Favorite.DoesNotExist: - # can't find that status, idk - return - - fav_activity = favorite.to_undo_activity(user) - favorite.delete() - broadcast(user, fav_activity, direct_recipients=[status.user]) - - # check for notification - if status.user.local: - notification = models.Notification.objects.filter( - user=status.user, related_user=user, - related_status=status, notification_type='FAVORITE' - ).first() - if notification: - notification.delete() - - -def handle_boost(user, status): - ''' a user wishes to boost a status ''' - # is it boostable? - if not status.boostable: - return - - if models.Boost.objects.filter( - boosted_status=status, user=user).exists(): - # you already boosted that. - return - boost = models.Boost.objects.create( - boosted_status=status, - privacy=status.privacy, - user=user, - ) - - boost_activity = boost.to_activity() - broadcast(user, boost_activity) - - if status.user.local: - create_notification( - status.user, - 'BOOST', - related_user=user, - related_status=status - ) - - -def handle_unboost(user, status): - ''' a user regrets boosting a status ''' - boost = models.Boost.objects.filter( - boosted_status=status, user=user - ).first() - activity = boost.to_undo_activity(user) - - boost.delete() - broadcast(user, activity) - - # delete related notification - if status.user.local: - notification = models.Notification.objects.filter( - user=status.user, related_user=user, - related_status=status, notification_type='BOOST' - ).first() - if notification: - notification.delete() diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index fab335b1a..cec44f4ae 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -9,6 +9,9 @@ .card { overflow: visible; } +.card-header-title { + overflow: hidden; +} /* --- TOGGLES --- */ input.toggle-control { diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index c54255fe2..4e65ecf84 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -223,7 +223,7 @@ -