From b0202eb8e8248907c650355a4b7b4b0f282d161c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 28 Nov 2020 11:48:17 -0800 Subject: [PATCH] Remove special remote user handling code also fixes date parsing --- bookwyrm/activitypub/base_activity.py | 9 +-- bookwyrm/incoming.py | 49 +++++++-------- bookwyrm/models/user.py | 69 ++++++++++++++++++++- bookwyrm/outgoing.py | 7 ++- bookwyrm/remote_user.py | 86 --------------------------- bookwyrm/status.py | 31 ---------- celerywyrm/celery.py | 2 +- 7 files changed, 102 insertions(+), 151 deletions(-) delete mode 100644 bookwyrm/remote_user.py diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index a62045366..0672fbb83 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -118,11 +118,12 @@ class ActivityObject: formatted_value = mapping.model_formatter(value) if isinstance(model_field, DeferredAttribute) and \ isinstance(model_field.field, DateTimeField): - print("DATE") try: - formatted_value = timezone.make_aware( - dateutil.parser.parse(formatted_value) - ) + date_value = dateutil.parser.parse(formatted_value) + try: + formatted_value = timezone.make_aware(date_value) + except ValueError: + formatted_value = date_value except ParserError: formatted_value = None elif isinstance(model_field, ForwardManyToOneDescriptor) and \ diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index ef05cc4e9..6ce2ea0b0 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -10,7 +10,6 @@ import requests from bookwyrm import activitypub, books_manager, models, outgoing from bookwyrm import status as status_builder -from bookwyrm.remote_user import get_or_create_remote_user, refresh_remote_user from bookwyrm.tasks import app from bookwyrm.signatures import Signature @@ -96,13 +95,15 @@ def has_valid_signature(request, activity): if key_actor != activity.get('actor'): raise ValueError("Wrong actor created signature.") - remote_user = get_or_create_remote_user(key_actor) + remote_user = activitypub.resolve_remote_id(models.User, key_actor) try: signature.verify(remote_user.public_key, request) except ValueError: old_key = remote_user.public_key - refresh_remote_user(remote_user) + activitypub.resolve_remote_id( + models.User, remote_user, refresh=True + ) if remote_user.public_key == old_key: raise # Key unchanged. signature.verify(remote_user.public_key, request) @@ -127,7 +128,7 @@ def handle_follow(activity): return # figure out who the actor is - actor = get_or_create_remote_user(activity['actor']) + actor = activitypub.resolve_remote_id(models.User, activity['actor']) try: relationship = models.UserFollowRequest.objects.create( user_subject=actor, @@ -162,7 +163,7 @@ def handle_follow(activity): def handle_unfollow(activity): ''' unfollow a local user ''' obj = activity['object'] - requester = get_or_create_remote_user(obj['actor']) + requester = activitypub.resolve_remote_id(models.user, obj['actor']) to_unfollow = models.User.objects.get(remote_id=obj['object']) # raises models.User.DoesNotExist @@ -175,7 +176,7 @@ def handle_follow_accept(activity): # figure out who they want to follow requester = models.User.objects.get(remote_id=activity['object']['actor']) # figure out who they are - accepter = get_or_create_remote_user(activity['actor']) + accepter = activitypub.resolve_remote_id(models.User, activity['actor']) try: request = models.UserFollowRequest.objects.get( @@ -192,7 +193,7 @@ def handle_follow_accept(activity): def handle_follow_reject(activity): ''' someone is rejecting a follow request ''' requester = models.User.objects.get(remote_id=activity['object']['actor']) - rejecter = get_or_create_remote_user(activity['actor']) + rejecter = activitypub.resolve_remote_id(models.User, activity['actor']) request = models.UserFollowRequest.objects.get( user_subject=requester, @@ -205,25 +206,27 @@ def handle_follow_reject(activity): @app.task def handle_create(activity): ''' someone did something, good on them ''' - if activity['object'].get('type') not in \ - ['Note', 'Comment', 'Quotation', 'Review', 'GeneratedNote']: - # if it's an article or unknown type, ignore it - return - - user = get_or_create_remote_user(activity['actor']) - if user.local: - # we really oughtn't even be sending in this case - return - # deduplicate incoming activities status_id = activity['object']['id'] if models.Status.objects.filter(remote_id=status_id).count(): return - status = status_builder.create_status(activity['object']) - if not status: + serializer = activitypub.activity_objects[activity['type']] + status = serializer(**activity) + try: + model = models.activity_models[activity.type] + except KeyError: + # not a type of status we are prepared to deserialize return + if activity.type == 'Note': + reply = models.Status.objects.filter( + remote_id=activity.inReplyTo + ).first() + if not reply: + return + + activity.to_model(model) # create a notification if this is a reply if status.reply_parent and status.reply_parent.user.local: status_builder.create_notification( @@ -257,16 +260,14 @@ def handle_favorite(activity): ''' approval of your good good post ''' fav = activitypub.Like(**activity) - liker = get_or_create_remote_user(activity['actor']) - if liker.local: - return - fav = fav.to_model(models.Favorite) + if fav.user.local: + return status_builder.create_notification( fav.status.user, 'FAVORITE', - related_user=liker, + related_user=fav.user, related_status=fav.status, ) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index a98120370..a32a35b7c 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -1,5 +1,6 @@ ''' database schema for user data ''' from urllib.parse import urlparse +import requests from django.contrib.auth.models import AbstractUser from django.db import models @@ -7,10 +8,12 @@ from django.dispatch import receiver from bookwyrm import activitypub from bookwyrm.models.shelf import Shelf -from bookwyrm.models.status import Status +from bookwyrm.models.status import Status, Review from bookwyrm.settings import DOMAIN from bookwyrm.signatures import create_key_pair +from bookwyrm.tasks import app from .base_model import ActivityMapping, OrderedCollectionPageMixin +from .federated_server import FederatedServer class User(OrderedCollectionPageMixin, AbstractUser): @@ -188,7 +191,16 @@ class User(OrderedCollectionPageMixin, AbstractUser): @receiver(models.signals.post_save, sender=User) def execute_after_save(sender, instance, created, *args, **kwargs): ''' create shelves for new users ''' - if not instance.local or not created: + if not created: + return + + if not instance.local: + actor_parts = urlparse(instance.remote_id) + instance.federated_server = \ + get_or_create_remote_server(actor_parts.netloc) + instance.save() + if instance.bookwyrm_user: + get_remote_reviews.delay(instance.outbox) return shelves = [{ @@ -209,3 +221,56 @@ def execute_after_save(sender, instance, created, *args, **kwargs): user=instance, editable=False ).save() + + +def get_or_create_remote_server(domain): + ''' get info on a remote server ''' + try: + return FederatedServer.objects.get( + server_name=domain + ) + except FederatedServer.DoesNotExist: + pass + + response = requests.get( + 'https://%s/.well-known/nodeinfo' % domain, + headers={'Accept': 'application/activity+json'} + ) + + if response.status_code != 200: + return None + + data = response.json() + try: + nodeinfo_url = data.get('links')[0].get('href') + except (TypeError, KeyError): + return None + + response = requests.get( + nodeinfo_url, + headers={'Accept': 'application/activity+json'} + ) + data = response.json() + + server = FederatedServer.objects.create( + server_name=domain, + application_type=data['software']['name'], + application_version=data['software']['version'], + ) + return server + + +@app.task +def get_remote_reviews(outbox): + ''' ingest reviews by a new remote bookwyrm user ''' + outbox_page = outbox + '?page=true' + response = requests.get( + outbox_page, + headers={'Accept': 'application/activity+json'} + ) + data = response.json() + # TODO: pagination? + for activity in data['orderedItems']: + if not activity['type'] == 'Review': + continue + activitypub.Review(**activity).to_model(Review) diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index a196fcecf..545ac4911 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -12,7 +12,6 @@ from bookwyrm.broadcast import broadcast from bookwyrm.status import create_notification from bookwyrm.status import create_generated_note from bookwyrm.status import delete_status -from bookwyrm.remote_user import get_or_create_remote_user from bookwyrm.settings import DOMAIN from bookwyrm.utils import regex @@ -61,9 +60,11 @@ def handle_remote_webfinger(query): return None data = response.json() for link in data['links']: - if link['rel'] == 'self': + if link.get('rel') == 'self': try: - user = get_or_create_remote_user(link['href']) + user = activitypub.resolve_remote_id( + models.User, link['href'] + ) except KeyError: return None return user diff --git a/bookwyrm/remote_user.py b/bookwyrm/remote_user.py deleted file mode 100644 index 205b0893a..000000000 --- a/bookwyrm/remote_user.py +++ /dev/null @@ -1,86 +0,0 @@ -''' manage remote users ''' -from urllib.parse import urlparse -import requests - -from django.db import transaction - -from bookwyrm import activitypub, models -from bookwyrm import status as status_builder -from bookwyrm.tasks import app - - -def get_or_create_remote_user(actor): - ''' look up a remote user or add them ''' - try: - return models.User.objects.get(remote_id=actor) - except models.User.DoesNotExist: - pass - - actor_parts = urlparse(actor) - with transaction.atomic(): - user = activitypub.resolve_remote_id(models.User, actor) - user.federated_server = get_or_create_remote_server(actor_parts.netloc) - user.save() - if user.bookwyrm_user: - get_remote_reviews.delay(user.id) - return user - - -def refresh_remote_user(user): - ''' get updated user data from its home instance ''' - activitypub.resolve_remote_id(user.remote_id, refresh=True) - - -@app.task -def get_remote_reviews(user_id): - ''' ingest reviews by a new remote bookwyrm user ''' - try: - user = models.User.objects.get(id=user_id) - except models.User.DoesNotExist: - return - outbox_page = user.outbox + '?page=true' - response = requests.get( - outbox_page, - headers={'Accept': 'application/activity+json'} - ) - data = response.json() - # TODO: pagination? - for activity in data['orderedItems']: - status_builder.create_status(activity) - - -def get_or_create_remote_server(domain): - ''' get info on a remote server ''' - try: - return models.FederatedServer.objects.get( - server_name=domain - ) - except models.FederatedServer.DoesNotExist: - pass - - response = requests.get( - 'https://%s/.well-known/nodeinfo' % domain, - headers={'Accept': 'application/activity+json'} - ) - - if response.status_code != 200: - return None - - data = response.json() - try: - nodeinfo_url = data.get('links')[0].get('href') - except (TypeError, KeyError): - return None - - response = requests.get( - nodeinfo_url, - headers={'Accept': 'application/activity+json'} - ) - data = response.json() - - server = models.FederatedServer.objects.create( - server_name=domain, - application_type=data['software']['name'], - application_version=data['software']['version'], - ) - return server diff --git a/bookwyrm/status.py b/bookwyrm/status.py index 6a86209f4..83a106e54 100644 --- a/bookwyrm/status.py +++ b/bookwyrm/status.py @@ -12,37 +12,6 @@ def delete_status(status): status.save() -def create_status(activity): - ''' unfortunately, it's not QUITE as simple as deserializing it ''' - # render the json into an activity object - serializer = activitypub.activity_objects[activity['type']] - activity = serializer(**activity) - try: - model = models.activity_models[activity.type] - except KeyError: - # not a type of status we are prepared to deserialize - return None - - # ignore notes that aren't replies to known statuses - if activity.type == 'Note': - reply = models.Status.objects.filter( - remote_id=activity.inReplyTo - ).first() - if not reply: - return None - - # look up books - book_urls = [] - if hasattr(activity, 'inReplyToBook'): - book_urls.append(activity.inReplyToBook) - if hasattr(activity, 'tag'): - book_urls += [t['href'] for t in activity.tag if t['type'] == 'Book'] - for remote_id in book_urls: - books_manager.get_or_create_book(remote_id) - - return activity.to_model(model) - - def create_generated_note(user, content, mention_books=None, privacy='public'): ''' a note created by the app about user activity ''' # sanitize input html diff --git a/celerywyrm/celery.py b/celerywyrm/celery.py index 361b76ec3..a47aad329 100644 --- a/celerywyrm/celery.py +++ b/celerywyrm/celery.py @@ -24,4 +24,4 @@ app.autodiscover_tasks(['bookwyrm'], related_name='books_manager') app.autodiscover_tasks(['bookwyrm'], related_name='emailing') app.autodiscover_tasks(['bookwyrm'], related_name='goodreads_import') app.autodiscover_tasks(['bookwyrm'], related_name='incoming') -app.autodiscover_tasks(['bookwyrm'], related_name='remote_user') +app.autodiscover_tasks(['bookwyrm'], related_name='models.user')