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