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 9b2590121..000000000 --- a/bookwyrm/outgoing.py +++ /dev/null @@ -1,161 +0,0 @@ -''' handles all the activity coming out of the server ''' -from django.db import 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 requests import HTTPError - -from bookwyrm import activitypub -from bookwyrm import models -from bookwyrm.connectors import get_data, ConnectorException -from bookwyrm.broadcast import broadcast - - -@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_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/urls.py b/bookwyrm/urls.py index c6ada37f6..4ed162884 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -3,7 +3,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, re_path -from bookwyrm import incoming, outgoing, settings, views, wellknown +from bookwyrm import incoming, settings, views, wellknown from bookwyrm.utils import regex user_path = r'^user/(?P%s)' % regex.username @@ -30,7 +30,7 @@ urlpatterns = [ # federation endpoints re_path(r'^inbox/?$', incoming.shared_inbox), re_path(r'%s/inbox/?$' % local_user_path, incoming.inbox), - re_path(r'%s/outbox/?$' % local_user_path, outgoing.outbox), + re_path(r'%s/outbox/?$' % local_user_path, views.Outbox.as_view()), # .well-known endpoints re_path(r'^.well-known/webfinger/?$', wellknown.webfinger), diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 7c050339a..6351a87a6 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -6,12 +6,13 @@ from .books import upload_cover, add_description, switch_edition, resolve_book from .direct_message import DirectMessage from .error import not_found_page, server_error_page from .follow import follow, unfollow -from .follow import accept_follow_request, delete_follow_request +from .follow import accept_follow_request, delete_follow_request, handle_accept from .import_data import Import, ImportStatus from .interaction import Favorite, Unfavorite, Boost, Unboost from .invite import ManageInvites, Invite from .landing import About, Home, Feed, Discover from .notifications import Notifications +from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading from .password import PasswordResetRequest, PasswordReset, ChangePassword diff --git a/bookwyrm/views/follow.py b/bookwyrm/views/follow.py new file mode 100644 index 000000000..08b6cca3f --- /dev/null +++ b/bookwyrm/views/follow.py @@ -0,0 +1,113 @@ +''' views for actions you can take in the application ''' +from django.db import transaction +from django.contrib.auth.decorators import login_required +from django.http import HttpResponseBadRequest +from django.shortcuts import redirect +from django.views.decorators.http import require_POST + +from bookwyrm import models +from bookwyrm.broadcast import broadcast +from .helpers import get_user_from_username + +@login_required +@require_POST +def follow(request): + ''' follow another user, here or abroad ''' + username = request.POST['user'] + try: + to_follow = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + relationship, _ = models.UserFollowRequest.objects.get_or_create( + user_subject=request.user, + user_object=to_follow, + ) + activity = relationship.to_activity() + broadcast( + request.user, activity, privacy='direct', direct_recipients=[to_follow]) + return redirect(to_follow.local_path) + + +@login_required +@require_POST +def unfollow(request): + ''' unfollow a user ''' + username = request.POST['user'] + try: + to_unfollow = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + relationship = models.UserFollows.objects.get( + user_subject=request.user, + user_object=to_unfollow + ) + activity = relationship.to_undo_activity(request.user) + broadcast( + request.user, activity, + privacy='direct', direct_recipients=[to_unfollow]) + + to_unfollow.followers.remove(request.user) + return redirect(to_unfollow.local_path) + + +@login_required +@require_POST +def accept_follow_request(request): + ''' a user accepts a follow request ''' + username = request.POST['user'] + try: + requester = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + try: + follow_request = models.UserFollowRequest.objects.get( + user_subject=requester, + user_object=request.user + ) + except models.UserFollowRequest.DoesNotExist: + # Request already dealt with. + return redirect(request.user.local_path) + handle_accept(follow_request) + + return redirect(request.user.local_path) + + +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]) + + +@login_required +@require_POST +def delete_follow_request(request): + ''' a user rejects a follow request ''' + username = request.POST['user'] + try: + requester = get_user_from_username(username) + except models.User.DoesNotExist: + return HttpResponseBadRequest() + + try: + follow_request = models.UserFollowRequest.objects.get( + user_subject=requester, + user_object=request.user + ) + except models.UserFollowRequest.DoesNotExist: + return HttpResponseBadRequest() + + activity = follow_request.to_reject_activity() + follow_request.delete() + broadcast( + request.user, activity, privacy='direct', direct_recipients=[requester]) + return redirect('/user/%s' % request.user.localname) diff --git a/bookwyrm/views/outbox.py b/bookwyrm/views/outbox.py new file mode 100644 index 000000000..8bfc3b3d4 --- /dev/null +++ b/bookwyrm/views/outbox.py @@ -0,0 +1,22 @@ +''' the good stuff! the books! ''' +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.views import View + +from bookwyrm import activitypub, models + + +# pylint: disable= no-self-use +class Outbox(View): + ''' outbox ''' + def get(self, 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 + )