Move activitypub serialization into a module

This commit is contained in:
Mouse Reeve 2020-02-17 20:12:19 -08:00
parent b6964dd8aa
commit 75ef3baabd
9 changed files with 206 additions and 192 deletions

View file

@ -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,
}
}

View file

@ -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

View file

@ -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,
}
}

View file

@ -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,
}
}

View file

@ -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'),
}
}

View file

@ -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

View file

@ -9,9 +9,10 @@ from django.views.decorators.csrf import csrf_exempt
import json import json
import requests import requests
from fedireads import activitypub
from fedireads import models from fedireads import models
from fedireads import outgoing 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 from fedireads.remote_user import get_or_create_remote_user
@ -111,29 +112,7 @@ def get_actor(request, username):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user = models.User.objects.get(localname=username) user = models.User.objects.get(localname=username)
return JsonResponse({ return JsonResponse(activitypub.get_actor(user))
'@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,
}
})
@csrf_exempt @csrf_exempt
@ -151,7 +130,7 @@ def get_status(request, username, status_id):
if user != status.user: if user != status.user:
return HttpResponseNotFound() return HttpResponseNotFound()
return JsonResponse(get_status_json(status)) return JsonResponse(activitypub.get_status(status))
@csrf_exempt @csrf_exempt

View file

@ -6,9 +6,8 @@ from urllib.parse import urlencode
from uuid import uuid4 from uuid import uuid4
from fedireads import models from fedireads import models
from fedireads.activity import create_review, create_status from fedireads.status import create_review, create_status
from fedireads.activity import get_status_json, get_review_json from fedireads import activitypub
from fedireads.activity import get_add_json, get_remove_json, get_create_json
from fedireads.remote_user import get_or_create_remote_user from fedireads.remote_user import get_or_create_remote_user
from fedireads.broadcast import get_recipients, broadcast from fedireads.broadcast import get_recipients, broadcast
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
@ -49,7 +48,7 @@ def outbox(request, username):
} }
statuses = models.Status.objects.filter(user=user, **filters).all() statuses = models.Status.objects.filter(user=user, **filters).all()
for status in statuses[:limit]: for status in statuses[:limit]:
outbox_page['orderedItems'].append(get_status_json(status)) outbox_page['orderedItems'].append(activitypub.get_status(status))
if max_id: if max_id:
outbox_page['next'] = query_path + \ outbox_page['next'] = query_path + \
@ -104,7 +103,6 @@ def handle_outgoing_follow(user, to_follow):
errors = broadcast(user, activity, [to_follow.inbox]) errors = broadcast(user, activity, [to_follow.inbox])
for error in errors: for error in errors:
# TODO: following masto users is returning 400
raise(error['error']) raise(error['error'])
@ -132,7 +130,7 @@ def handle_shelve(user, book, shelf):
# TODO: this should probably happen in incoming instead # TODO: this should probably happen in incoming instead
models.ShelfBook(book=book, shelf=shelf, added_by=user).save() 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') recipients = get_recipients(user, 'public')
broadcast(user, activity, recipients) broadcast(user, activity, recipients)
@ -146,8 +144,8 @@ def handle_shelve(user, book, shelf):
message = '%s %s %s' % (name, verb, book.data['title']) message = '%s %s %s' % (name, verb, book.data['title'])
status = create_status(user, message, mention_books=[book]) status = create_status(user, message, mention_books=[book])
activity = get_status_json(status) activity = activitypub.get_status(status)
create_activity = get_create_json(user, activity) create_activity = activitypub.get_create(user, activity)
broadcast(user, create_activity, recipients) 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 = models.ShelfBook.objects.get(book=book, shelf=shelf)
row.delete() row.delete()
activity = get_remove_json(user, book, shelf) activity = activitypub.get_remove(user, book, shelf)
recipients = get_recipients(user, 'public') recipients = get_recipients(user, 'public')
broadcast(user, activity, recipients) 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 # validated and saves the review in the database so it has an id
review = create_review(user, book, name, content, rating) review = create_review(user, book, name, content, rating)
review_activity = get_review_json(review) review_activity = activitypub.get_review(review)
create_activity = get_create_json(user, review_activity) create_activity = activitypub.get_create(user, review_activity)
recipients = get_recipients(user, 'public') recipients = get_recipients(user, 'public')
broadcast(user, create_activity, recipients) broadcast(user, create_activity, recipients)

48
fedireads/status.py Normal file
View file

@ -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