mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-19 08:01:03 +00:00
952250009b
it's in a file called outgoing.py! you can already tell it's outoging
315 lines
9.9 KiB
Python
315 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_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()
|
|
|