forked from mirrors/bookwyrm
Move activitypub serialization into a module
This commit is contained in:
parent
b6964dd8aa
commit
75ef3baabd
9 changed files with 206 additions and 192 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
5
fedireads/activitypub/__init__.py
Normal file
5
fedireads/activitypub/__init__.py
Normal 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
|
28
fedireads/activitypub/actor.py
Normal file
28
fedireads/activitypub/actor.py
Normal 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,
|
||||
}
|
||||
}
|
||||
|
34
fedireads/activitypub/collection.py
Normal file
34
fedireads/activitypub/collection.py
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
33
fedireads/activitypub/create.py
Normal file
33
fedireads/activitypub/create.py
Normal 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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
45
fedireads/activitypub/status.py
Normal file
45
fedireads/activitypub/status.py
Normal 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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
48
fedireads/status.py
Normal 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
|
||||
|
Loading…
Reference in a new issue