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

View file

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

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