diff --git a/fedireads/activity.py b/fedireads/activity.py deleted file mode 100644 index 2e4a80c11..000000000 --- a/fedireads/activity.py +++ /dev/null @@ -1,156 +0,0 @@ -''' Handle user activity ''' -from base64 import b64encode -from Crypto.PublicKey import RSA -from Crypto.Signature import pkcs1_15 -from Crypto.Hash import SHA256 -from uuid import uuid4 - -from fedireads import models -from fedireads.openlibrary import get_or_create_book -from fedireads.sanitize_html import InputHtmlParser - - -def create_review(user, possible_book, name, content, rating): - ''' a book review has been added ''' - # throws a value error if the book is not found - book = get_or_create_book(possible_book) - - # sanitize review html - parser = InputHtmlParser() - parser.feed(content) - content = parser.get_output() - - # no ratings outside of 0-5 - rating = rating if 0 <= rating <= 5 else 0 - - return models.Review.objects.create( - user=user, - book=book, - name=name, - rating=rating, - content=content, - ) - - -def create_status(user, content, reply_parent=None, mention_books=None): - ''' a status update ''' - # TODO: handle @'ing users - - # sanitize input html - parser = InputHtmlParser() - parser.feed(content) - content = parser.get_output() - - status = models.Status.objects.create( - user=user, - content=content, - reply_parent=reply_parent, - ) - - for book in mention_books: - status.mention_books.add(book) - - return status - - -def get_review_json(review): - ''' fedireads json for book reviews ''' - status = get_status_json(review) - status['inReplyTo'] = review.book.absolute_id - status['fedireadsType'] = review.status_type, - status['name'] = review.name - status['rating'] = review.rating - return status - - -def get_status_json(status): - ''' create activitypub json for a status ''' - user = status.user - uri = status.absolute_id - reply_parent_id = status.reply_parent.id if status.reply_parent else None - status_json = { - 'id': uri, - 'url': uri, - 'inReplyTo': reply_parent_id, - 'published': status.created_date.isoformat(), - 'attributedTo': user.actor, - # TODO: assuming all posts are public -- should check privacy db field - 'to': ['https://www.w3.org/ns/activitystreams#Public'], - 'cc': ['%s/followers' % user.absolute_id], - 'sensitive': status.sensitive, - 'content': status.content, - 'type': status.activity_type, - 'attachment': [], # TODO: the book cover - 'replies': { - 'id': '%s/replies' % uri, - 'type': 'Collection', - 'first': { - 'type': 'CollectionPage', - 'next': '%s/replies?only_other_accounts=true&page=true' % uri, - 'partOf': '%s/replies' % uri, - 'items': [], # TODO: populate with replies - } - } - } - - return status_json - - -def get_create_json(user, status_json): - ''' create activitypub json for a Create activity ''' - signer = pkcs1_15.new(RSA.import_key(user.private_key)) - content = status_json['content'] - signed_message = signer.sign(SHA256.new(content.encode('utf8'))) - return { - '@context': 'https://www.w3.org/ns/activitystreams', - - 'id': '%s/activity' % status_json['id'], - 'type': 'Create', - 'actor': user.actor, - 'published': status_json['published'], - - 'to': ['%s/followers' % user.actor], - 'cc': ['https://www.w3.org/ns/activitystreams#Public'], - - 'object': status_json, - 'signature': { - 'type': 'RsaSignature2017', - 'creator': '%s#main-key' % user.absolute_id, - 'created': status_json['published'], - 'signatureValue': b64encode(signed_message).decode('utf8'), - } - } - - - -def get_add_json(*args): - ''' activitypub Add activity ''' - return get_add_remove_json(*args, action='Add') - - -def get_remove_json(*args): - ''' activitypub Add activity ''' - return get_add_remove_json(*args, action='Remove') - - -def get_add_remove_json(user, book, shelf, action='Add'): - ''' format an Add or Remove json blob ''' - uuid = uuid4() - return { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': str(uuid), - 'type': action, - 'actor': user.actor, - 'object': { - 'type': 'Document', - 'name': book.data['title'], - 'url': book.openlibrary_key - }, - 'target': { - 'type': 'Collection', - 'name': shelf.name, - 'id': shelf.absolute_id, - } - } - - diff --git a/fedireads/activitypub/__init__.py b/fedireads/activitypub/__init__.py new file mode 100644 index 000000000..839c5aab6 --- /dev/null +++ b/fedireads/activitypub/__init__.py @@ -0,0 +1,5 @@ +''' bring activitypub functions into the namespace ''' +from .actor import get_actor +from .collection import get_add, get_remove +from .create import get_create +from .status import get_review, get_status diff --git a/fedireads/activitypub/actor.py b/fedireads/activitypub/actor.py new file mode 100644 index 000000000..63d494011 --- /dev/null +++ b/fedireads/activitypub/actor.py @@ -0,0 +1,28 @@ +''' actor serializer ''' + +def get_actor(user): + ''' activitypub actor from db User ''' + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + + 'id': user.actor, + 'type': 'Person', + 'preferredUsername': user.localname, + 'name': user.name, + 'inbox': user.inbox, + 'followers': '%s/followers' % user.actor, + 'following': '%s/following' % user.actor, + 'summary': user.summary, + 'publicKey': { + 'id': '%s/#main-key' % user.actor, + 'owner': user.actor, + 'publicKeyPem': user.public_key, + }, + 'endpoints': { + 'sharedInbox': user.shared_inbox, + } + } + diff --git a/fedireads/activitypub/collection.py b/fedireads/activitypub/collection.py new file mode 100644 index 000000000..c840d498e --- /dev/null +++ b/fedireads/activitypub/collection.py @@ -0,0 +1,34 @@ +''' activitypub json for collections ''' +from uuid import uuid4 + +def get_add(*args): + ''' activitypub Add activity ''' + return get_add_remove(*args, action='Add') + + +def get_remove(*args): + ''' activitypub Add activity ''' + return get_add_remove(*args, action='Remove') + + +def get_add_remove(user, book, shelf, action='Add'): + ''' format an Add or Remove json blob ''' + uuid = uuid4() + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': str(uuid), + 'type': action, + 'actor': user.actor, + 'object': { + 'type': 'Document', + 'name': book.data['title'], + 'url': book.openlibrary_key + }, + 'target': { + 'type': 'Collection', + 'name': shelf.name, + 'id': shelf.absolute_id, + } + } + + diff --git a/fedireads/activitypub/create.py b/fedireads/activitypub/create.py new file mode 100644 index 000000000..a3d4d7a16 --- /dev/null +++ b/fedireads/activitypub/create.py @@ -0,0 +1,33 @@ +''' format Create activities and sign them ''' +from base64 import b64encode +from Crypto.PublicKey import RSA +from Crypto.Signature import pkcs1_15 +from Crypto.Hash import SHA256 + +def get_create(user, status_json): + ''' create activitypub json for a Create activity ''' + signer = pkcs1_15.new(RSA.import_key(user.private_key)) + content = status_json['content'] + signed_message = signer.sign(SHA256.new(content.encode('utf8'))) + return { + '@context': 'https://www.w3.org/ns/activitystreams', + + 'id': '%s/activity' % status_json['id'], + 'type': 'Create', + 'actor': user.actor, + 'published': status_json['published'], + + 'to': ['%s/followers' % user.actor], + 'cc': ['https://www.w3.org/ns/activitystreams#Public'], + + 'object': status_json, + 'signature': { + 'type': 'RsaSignature2017', + 'creator': '%s#main-key' % user.absolute_id, + 'created': status_json['published'], + 'signatureValue': b64encode(signed_message).decode('utf8'), + } + } + + + diff --git a/fedireads/activitypub/status.py b/fedireads/activitypub/status.py new file mode 100644 index 000000000..1f3a7f6f8 --- /dev/null +++ b/fedireads/activitypub/status.py @@ -0,0 +1,45 @@ +''' status serializers ''' +def get_review(review): + ''' fedireads json for book reviews ''' + status = get_status_json(review) + status['inReplyTo'] = review.book.absolute_id + status['fedireadsType'] = review.status_type, + status['name'] = review.name + status['rating'] = review.rating + return status + + +def get_status(status): + ''' create activitypub json for a status ''' + user = status.user + uri = status.absolute_id + reply_parent_id = status.reply_parent.id if status.reply_parent else None + status_json = { + 'id': uri, + 'url': uri, + 'inReplyTo': reply_parent_id, + 'published': status.created_date.isoformat(), + 'attributedTo': user.actor, + # TODO: assuming all posts are public -- should check privacy db field + 'to': ['https://www.w3.org/ns/activitystreams#Public'], + 'cc': ['%s/followers' % user.absolute_id], + 'sensitive': status.sensitive, + 'content': status.content, + 'type': status.activity_type, + 'attachment': [], # TODO: the book cover + 'replies': { + 'id': '%s/replies' % uri, + 'type': 'Collection', + 'first': { + 'type': 'CollectionPage', + 'next': '%s/replies?only_other_accounts=true&page=true' % uri, + 'partOf': '%s/replies' % uri, + 'items': [], # TODO: populate with replies + } + } + } + + return status_json + + + diff --git a/fedireads/incoming.py b/fedireads/incoming.py index 5d442c8e6..490f4349e 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -9,9 +9,10 @@ from django.views.decorators.csrf import csrf_exempt import json import requests +from fedireads import activitypub from fedireads import models from fedireads import outgoing -from fedireads.activity import create_review, create_status, get_status_json +from fedireads.status import create_review, create_status from fedireads.remote_user import get_or_create_remote_user @@ -111,29 +112,7 @@ def get_actor(request, username): return HttpResponseBadRequest() user = models.User.objects.get(localname=username) - return JsonResponse({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1' - ], - - 'id': user.actor, - 'type': 'Person', - 'preferredUsername': user.localname, - 'name': user.name, - 'inbox': user.inbox, - 'followers': '%s/followers' % user.actor, - 'following': '%s/following' % user.actor, - 'summary': user.summary, - 'publicKey': { - 'id': '%s/#main-key' % user.actor, - 'owner': user.actor, - 'publicKeyPem': user.public_key, - }, - 'endpoints': { - 'sharedInbox': user.shared_inbox, - } - }) + return JsonResponse(activitypub.get_actor(user)) @csrf_exempt @@ -151,7 +130,7 @@ def get_status(request, username, status_id): if user != status.user: return HttpResponseNotFound() - return JsonResponse(get_status_json(status)) + return JsonResponse(activitypub.get_status(status)) @csrf_exempt diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 9fe68ee09..08602f5d8 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -6,9 +6,8 @@ from urllib.parse import urlencode from uuid import uuid4 from fedireads import models -from fedireads.activity import create_review, create_status -from fedireads.activity import get_status_json, get_review_json -from fedireads.activity import get_add_json, get_remove_json, get_create_json +from fedireads.status import create_review, create_status +from fedireads import activitypub from fedireads.remote_user import get_or_create_remote_user from fedireads.broadcast import get_recipients, broadcast from fedireads.settings import DOMAIN @@ -49,7 +48,7 @@ def outbox(request, username): } statuses = models.Status.objects.filter(user=user, **filters).all() for status in statuses[:limit]: - outbox_page['orderedItems'].append(get_status_json(status)) + outbox_page['orderedItems'].append(activitypub.get_status(status)) if max_id: outbox_page['next'] = query_path + \ @@ -104,7 +103,6 @@ def handle_outgoing_follow(user, to_follow): errors = broadcast(user, activity, [to_follow.inbox]) for error in errors: - # TODO: following masto users is returning 400 raise(error['error']) @@ -132,7 +130,7 @@ def handle_shelve(user, book, shelf): # TODO: this should probably happen in incoming instead models.ShelfBook(book=book, shelf=shelf, added_by=user).save() - activity = get_add_json(user, book, shelf) + activity = activitypub.get_add(user, book, shelf) recipients = get_recipients(user, 'public') broadcast(user, activity, recipients) @@ -146,8 +144,8 @@ def handle_shelve(user, book, shelf): message = '%s %s %s' % (name, verb, book.data['title']) status = create_status(user, message, mention_books=[book]) - activity = get_status_json(status) - create_activity = get_create_json(user, activity) + activity = activitypub.get_status(status) + create_activity = activitypub.get_create(user, activity) broadcast(user, create_activity, recipients) @@ -159,7 +157,7 @@ def handle_unshelve(user, book, shelf): row = models.ShelfBook.objects.get(book=book, shelf=shelf) row.delete() - activity = get_remove_json(user, book, shelf) + activity = activitypub.get_remove(user, book, shelf) recipients = get_recipients(user, 'public') broadcast(user, activity, recipients) @@ -170,8 +168,8 @@ def handle_review(user, book, name, content, rating): # validated and saves the review in the database so it has an id review = create_review(user, book, name, content, rating) - review_activity = get_review_json(review) - create_activity = get_create_json(user, review_activity) + review_activity = activitypub.get_review(review) + create_activity = activitypub.get_create(user, review_activity) recipients = get_recipients(user, 'public') broadcast(user, create_activity, recipients) diff --git a/fedireads/status.py b/fedireads/status.py new file mode 100644 index 000000000..ad30319ea --- /dev/null +++ b/fedireads/status.py @@ -0,0 +1,48 @@ +''' Handle user activity ''' +from fedireads import models +from fedireads.openlibrary import get_or_create_book +from fedireads.sanitize_html import InputHtmlParser + + +def create_review(user, possible_book, name, content, rating): + ''' a book review has been added ''' + # throws a value error if the book is not found + book = get_or_create_book(possible_book) + + # sanitize review html + parser = InputHtmlParser() + parser.feed(content) + content = parser.get_output() + + # no ratings outside of 0-5 + rating = rating if 0 <= rating <= 5 else 0 + + return models.Review.objects.create( + user=user, + book=book, + name=name, + rating=rating, + content=content, + ) + + +def create_status(user, content, reply_parent=None, mention_books=None): + ''' a status update ''' + # TODO: handle @'ing users + + # sanitize input html + parser = InputHtmlParser() + parser.feed(content) + content = parser.get_output() + + status = models.Status.objects.create( + user=user, + content=content, + reply_parent=reply_parent, + ) + + for book in mention_books: + status.mention_books.add(book) + + return status +