forked from mirrors/bookwyrm
7862af9729
works on #59
314 lines
9.9 KiB
Python
314 lines
9.9 KiB
Python
''' handles all of the activity coming in to the server '''
|
|
from base64 import b64decode
|
|
from Crypto.PublicKey import RSA
|
|
from Crypto.Signature import pkcs1_15
|
|
from Crypto.Hash import SHA256
|
|
from django.http import HttpResponse, HttpResponseBadRequest, \
|
|
HttpResponseNotFound
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
import django.db.utils
|
|
import json
|
|
import requests
|
|
|
|
from fedireads import models
|
|
from fedireads import outgoing
|
|
from fedireads import status as status_builder
|
|
from fedireads.remote_user import get_or_create_remote_user
|
|
|
|
|
|
@csrf_exempt
|
|
def shared_inbox(request):
|
|
''' incoming activitypub events '''
|
|
# TODO: should this be functionally different from the non-shared inbox??
|
|
if request.method == 'GET':
|
|
return HttpResponseNotFound()
|
|
|
|
try:
|
|
activity = json.loads(request.body)
|
|
except json.decoder.JSONDecodeError:
|
|
return HttpResponseBadRequest()
|
|
|
|
try:
|
|
verify_signature(request)
|
|
except ValueError:
|
|
return HttpResponse(status=401)
|
|
|
|
response = HttpResponseNotFound()
|
|
if activity['type'] == 'Follow':
|
|
response = handle_incoming_follow(activity)
|
|
|
|
elif activity['type'] == 'Undo':
|
|
if not 'object' in activity:
|
|
return HttpResponseNotFound()
|
|
if activity['object']['type'] == 'Follow':
|
|
response = handle_incoming_undo(activity)
|
|
elif activity['object']['type'] == 'Like':
|
|
response = handle_incoming_unfavorite(activity)
|
|
else:
|
|
return HttpResponseNotFound()
|
|
|
|
elif activity['type'] == 'Create':
|
|
response = handle_incoming_create(activity)
|
|
|
|
elif activity['type'] == 'Accept':
|
|
response = handle_incoming_follow_accept(activity)
|
|
|
|
elif activity['type'] == 'Like':
|
|
response = handle_incoming_favorite(activity)
|
|
|
|
elif activity['type'] == 'Add':
|
|
response = handle_incoming_add(activity)
|
|
elif activity['type'] == 'Reject':
|
|
response = handle_incoming_follow_reject(activity)
|
|
|
|
# TODO: Add, Undo, Remove, etc
|
|
|
|
return response
|
|
|
|
|
|
def verify_signature(request):
|
|
''' verify rsa signature '''
|
|
signature_dict = {}
|
|
for pair in request.headers['Signature'].split(','):
|
|
k, v = pair.split('=', 1)
|
|
v = v.replace('"', '')
|
|
signature_dict[k] = v
|
|
|
|
try:
|
|
key_id = signature_dict['keyId']
|
|
headers = signature_dict['headers']
|
|
signature = b64decode(signature_dict['signature'])
|
|
except KeyError:
|
|
raise ValueError('Invalid auth header')
|
|
|
|
response = requests.get(
|
|
key_id,
|
|
headers={'Accept': 'application/activity+json'}
|
|
)
|
|
if not response.ok:
|
|
raise ValueError('Could not load public key')
|
|
|
|
actor = response.json()
|
|
key = RSA.import_key(actor['publicKey']['publicKeyPem'])
|
|
|
|
comparison_string = []
|
|
for signed_header_name in headers.split(' '):
|
|
if signed_header_name == '(request-target)':
|
|
comparison_string.append('(request-target): post %s' % request.path)
|
|
else:
|
|
comparison_string.append('%s: %s' % (
|
|
signed_header_name,
|
|
request.headers[signed_header_name]
|
|
))
|
|
comparison_string = '\n'.join(comparison_string)
|
|
|
|
signer = pkcs1_15.new(key)
|
|
digest = SHA256.new()
|
|
digest.update(comparison_string.encode())
|
|
|
|
# raises a ValueError if it fails
|
|
signer.verify(digest, signature)
|
|
|
|
return True
|
|
|
|
|
|
@csrf_exempt
|
|
def inbox(request, username):
|
|
''' incoming activitypub events '''
|
|
# TODO: should do some kind of checking if the user accepts
|
|
# this action from the sender probably? idk
|
|
# but this will just throw a 404 if the user doesn't exist
|
|
try:
|
|
models.User.objects.get(localname=username)
|
|
except models.User.DoesNotExist:
|
|
return HttpResponseNotFound()
|
|
|
|
return shared_inbox(request)
|
|
|
|
|
|
def handle_incoming_follow(activity):
|
|
''' someone wants to follow a local user '''
|
|
# figure out who they want to follow
|
|
to_follow = models.User.objects.get(actor=activity['object'])
|
|
# figure out who they are
|
|
user = get_or_create_remote_user(activity['actor'])
|
|
try:
|
|
request = models.UserFollowRequest.objects.create(
|
|
user_subject=user,
|
|
user_object=to_follow,
|
|
relationship_id=activity['id']
|
|
)
|
|
except django.db.utils.IntegrityError as err:
|
|
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
|
|
raise
|
|
# Duplicate follow request. Not sure what the correct behaviour is, but
|
|
# just dropping it works for now. We should perhaps generate the
|
|
# Accept, but then do we need to match the activity id?
|
|
return HttpResponse()
|
|
|
|
if not to_follow.manually_approves_followers:
|
|
status_builder.create_notification(
|
|
to_follow,
|
|
'FOLLOW',
|
|
related_user=user
|
|
)
|
|
outgoing.handle_outgoing_accept(user, to_follow, request)
|
|
else:
|
|
status_builder.create_notification(
|
|
to_follow,
|
|
'FOLLOW_REQUEST',
|
|
related_user=user
|
|
)
|
|
return HttpResponse()
|
|
|
|
|
|
def handle_incoming_undo(activity):
|
|
''' unfollow a local user '''
|
|
obj = activity['object']
|
|
if not obj['type'] == 'Follow':
|
|
#idk how to undo other things
|
|
return HttpResponseNotFound()
|
|
try:
|
|
requester = get_or_create_remote_user(obj['actor'])
|
|
to_unfollow = models.User.objects.get(actor=obj['object'])
|
|
except models.User.DoesNotExist:
|
|
return HttpResponseNotFound()
|
|
|
|
to_unfollow.followers.remove(requester)
|
|
return HttpResponse()
|
|
|
|
|
|
def handle_incoming_follow_accept(activity):
|
|
''' hurray, someone remote accepted a follow request '''
|
|
# figure out who they want to follow
|
|
requester = models.User.objects.get(actor=activity['object']['actor'])
|
|
# figure out who they are
|
|
accepter = get_or_create_remote_user(activity['actor'])
|
|
|
|
try:
|
|
request = models.UserFollowRequest.objects.get(
|
|
user_subject=requester,
|
|
user_object=accepter
|
|
)
|
|
request.delete()
|
|
except models.UserFollowRequest.DoesNotExist:
|
|
pass
|
|
accepter.followers.add(requester)
|
|
return HttpResponse()
|
|
|
|
|
|
def handle_incoming_follow_reject(activity):
|
|
''' someone is rejecting a follow request '''
|
|
requester = models.User.objects.get(actor=activity['object']['actor'])
|
|
rejecter = get_or_create_remote_user(activity['actor'])
|
|
|
|
try:
|
|
request = models.UserFollowRequest.objects.get(
|
|
user_subject=requester,
|
|
user_object=rejecter
|
|
)
|
|
request.delete()
|
|
except models.UserFollowRequest.DoesNotExist:
|
|
pass
|
|
|
|
return HttpResponse()
|
|
|
|
def handle_incoming_create(activity):
|
|
''' someone did something, good on them '''
|
|
user = get_or_create_remote_user(activity['actor'])
|
|
|
|
if not 'object' in activity:
|
|
return HttpResponseBadRequest()
|
|
|
|
# TODO: should only create notes if they are relevent to a book,
|
|
# so, not every single thing someone posts on mastodon
|
|
response = HttpResponse()
|
|
if activity['object'].get('fedireadsType') == 'Review' and \
|
|
'inReplyToBook' in activity['object']:
|
|
if user.local:
|
|
review_id = activity['object']['id'].split('/')[-1]
|
|
models.Review.objects.get(id=review_id)
|
|
else:
|
|
try:
|
|
status_builder.create_review_from_activity(
|
|
user,
|
|
activity['object']
|
|
)
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
elif activity['object'].get('fedireadsType') == 'Comment' and \
|
|
'inReplyToBook' in activity['object']:
|
|
if user.local:
|
|
comment_id = activity['object']['id'].split('/')[-1]
|
|
models.Comment.objects.get(id=comment_id)
|
|
else:
|
|
try:
|
|
status_builder.create_comment_from_activity(
|
|
user,
|
|
activity['object']
|
|
)
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
elif not user.local:
|
|
try:
|
|
status = status_builder.create_status_from_activity(
|
|
user,
|
|
activity['object']
|
|
)
|
|
if status and status.reply_parent:
|
|
status_builder.create_notification(
|
|
status.reply_parent.user,
|
|
'REPLY',
|
|
related_user=status.user,
|
|
related_status=status,
|
|
)
|
|
except ValueError:
|
|
return HttpResponseBadRequest()
|
|
|
|
return response
|
|
|
|
|
|
def handle_incoming_favorite(activity):
|
|
''' approval of your good good post '''
|
|
try:
|
|
status_id = activity['object'].split('/')[-1]
|
|
status = models.Status.objects.get(id=status_id)
|
|
liker = get_or_create_remote_user(activity['actor'])
|
|
except (models.Status.DoesNotExist, models.User.DoesNotExist):
|
|
return HttpResponseNotFound()
|
|
|
|
if not liker.local:
|
|
status_builder.create_favorite_from_activity(liker, activity)
|
|
|
|
status_builder.create_notification(
|
|
status.user,
|
|
'FAVORITE',
|
|
related_user=liker,
|
|
related_status=status,
|
|
)
|
|
return HttpResponse()
|
|
|
|
|
|
def handle_incoming_unfavorite(activity):
|
|
''' approval of your good good post '''
|
|
try:
|
|
favorite_id = activity['object']['id']
|
|
fav = status_builder.get_favorite(favorite_id)
|
|
except models.Favorite.DoesNotExist:
|
|
return HttpResponseNotFound()
|
|
|
|
fav.delete()
|
|
return HttpResponse()
|
|
|
|
|
|
def handle_incoming_add(activity):
|
|
''' someone is tagging or shelving a book '''
|
|
if activity['object']['type'] == 'Tag':
|
|
user = get_or_create_remote_user(activity['actor'])
|
|
if not user.local:
|
|
book = activity['target']['id'].split('/')[-1]
|
|
status_builder.create_tag(user, book, activity['object']['name'])
|
|
return HttpResponse()
|
|
return HttpResponse()
|
|
return HttpResponseNotFound()
|
|
|