moviewyrm/fedireads/incoming.py

333 lines
10 KiB
Python
Raw Normal View History

2020-01-28 19:45:27 +00:00
''' handles all of the activity coming in to the server '''
2020-01-30 04:56:18 +00:00
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, JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
2020-01-30 04:56:18 +00:00
import requests
2020-01-28 19:45:27 +00:00
from fedireads import models
2020-02-15 06:44:07 +00:00
from fedireads import outgoing
from fedireads.activity import create_review
2020-02-15 06:44:07 +00:00
from fedireads.remote_user import get_or_create_remote_user
@csrf_exempt
2020-01-28 19:13:13 +00:00
def shared_inbox(request):
''' incoming activitypub events '''
2020-01-30 04:56:18 +00:00
# TODO: should this be functionally different from the non-shared inbox??
2020-01-28 19:13:13 +00:00
if request.method == 'GET':
return HttpResponseNotFound()
2020-01-28 19:13:13 +00:00
try:
activity = json.loads(request.body)
except json.decoder.JSONDecodeError:
2020-02-15 04:37:02 +00:00
return HttpResponseBadRequest()
try:
verify_signature(request)
except ValueError:
return HttpResponse(status=401)
response = HttpResponseNotFound()
if activity['type'] == 'Add':
response = handle_incoming_shelve(activity)
2020-02-15 21:14:43 +00:00
elif activity['type'] == 'Follow':
2020-02-15 04:37:02 +00:00
response = handle_incoming_follow(activity)
2020-02-15 21:14:43 +00:00
elif activity['type'] == 'Create':
2020-02-15 04:37:02 +00:00
response = handle_incoming_create(activity)
2020-02-15 21:14:43 +00:00
elif activity['type'] == 'Accept':
2020-02-15 04:37:02 +00:00
response = handle_incoming_follow_accept(activity)
2020-02-15 21:14:43 +00:00
# TODO: Undo, Remove, etc
2020-02-15 04:37:02 +00:00
return response
2020-01-28 19:13:13 +00:00
2020-02-15 04:37:02 +00:00
def verify_signature(request):
''' verify rsa signature '''
2020-01-30 04:56:18 +00:00
signature_dict = {}
2020-02-15 04:37:02 +00:00
for pair in request.headers['Signature'].split(','):
2020-01-30 04:56:18 +00:00
k, v = pair.split('=', 1)
v = v.replace('"', '')
signature_dict[k] = v
2020-02-15 04:37:02 +00:00
try:
key_id = signature_dict['keyId']
headers = signature_dict['headers']
signature = b64decode(signature_dict['signature'])
except KeyError:
raise ValueError('Invalid auth header')
2020-01-30 04:56:18 +00:00
response = requests.get(
key_id,
headers={'Accept': 'application/activity+json'}
)
if not response.ok:
2020-02-15 04:37:02 +00:00
raise ValueError('Could not load public key')
2020-01-30 04:56:18 +00:00
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())
2020-02-15 04:37:02 +00:00
# raises a ValueError if it fails
signer.verify(digest, signature)
2020-02-15 04:37:02 +00:00
return True
@csrf_exempt
def inbox(request, username):
''' incoming activitypub events '''
# TODO: should do some kind of checking if the user accepts
2020-01-30 04:56:18 +00:00
# this action from the sender probably? idk
# but this will just throw an error if the user doesn't exist I guess
models.User.objects.get(localname=username)
2020-01-30 04:56:18 +00:00
return shared_inbox(request)
2020-01-28 19:13:13 +00:00
@csrf_exempt
def get_actor(request, username):
''' return an activitypub actor object '''
if request.method != 'GET':
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,
2020-01-29 19:45:19 +00:00
'name': user.name,
2020-01-28 19:13:13 +00:00
'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': {
2020-02-07 23:11:53 +00:00
'sharedInbox': user.shared_inbox,
2020-01-28 19:13:13 +00:00
}
})
2020-01-29 23:59:43 +00:00
2020-02-15 20:32:40 +00:00
@csrf_exempt
def get_status(request, username, status_id):
''' return activity json for a specific status '''
if request.method != 'GET':
return HttpResponseBadRequest()
try:
user = models.User.objects.get(localname=username)
status = models.Status.objects.get(id=status_id)
except ValueError:
return HttpResponseNotFound()
if user != status.user:
return HttpResponseNotFound()
return JsonResponse(status.activity)
2020-01-29 19:45:19 +00:00
@csrf_exempt
def get_followers(request, username):
''' return a list of followers for an actor '''
if request.method != 'GET':
return HttpResponseBadRequest()
user = models.User.objects.get(localname=username)
followers = user.followers
2020-02-07 23:11:53 +00:00
return format_follow_info(user, request.GET.get('page'), followers)
2020-01-29 19:45:19 +00:00
@csrf_exempt
def get_following(request, username):
''' return a list of following for an actor '''
if request.method != 'GET':
return HttpResponseBadRequest()
user = models.User.objects.get(localname=username)
following = models.User.objects.filter(followers=user)
2020-02-07 23:11:53 +00:00
return format_follow_info(user, request.GET.get('page'), following)
def format_follow_info(user, page, follow_queryset):
''' create the activitypub json for followers/following '''
2020-01-29 19:45:19 +00:00
id_slug = '%s/following' % user.actor
2020-02-07 23:11:53 +00:00
if page:
return JsonResponse(get_follow_page(follow_queryset, id_slug, page))
count = follow_queryset.count()
2020-01-29 19:45:19 +00:00
return JsonResponse({
'@context': 'https://www.w3.org/ns/activitystreams',
'id': id_slug,
'type': 'OrderedCollection',
2020-02-07 23:11:53 +00:00
'totalItems': count,
2020-01-29 19:45:19 +00:00
'first': '%s?page=1' % id_slug,
})
2020-02-15 21:07:57 +00:00
2020-01-29 19:45:19 +00:00
def get_follow_page(user_list, id_slug, page):
''' format a list of followers/following '''
page = int(page)
page_length = 10
start = (page - 1) * page_length
end = start + page_length
follower_page = user_list.all()[start:end]
data = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': '%s?page=%d' % (id_slug, page),
'type': 'OrderedCollectionPage',
'totalItems': user_list.count(),
'partOf': id_slug,
'orderedItems': [u.actor for u in follower_page],
}
if end <= user_list.count():
# there are still more pages
data['next'] = '%s?page=%d' % (id_slug, page + 1)
if start > 0:
data['prev'] = '%s?page=%d' % (id_slug, page - 1)
return data
2020-01-28 19:13:13 +00:00
def handle_incoming_shelve(activity):
''' receiving an Add activity (to shelve a book) '''
# TODO what happens here? If it's a remote over, then I think
# I should save both the activity and the ShelfBook entry. But
# I'll do that later.
uuid = activity['id']
models.ShelveActivity.objects.get(uuid=uuid)
'''
book_id = activity['object']['url']
book = openlibrary.get_or_create_book(book_id)
user_ap_id = activity['actor'].replace('https//:', '')
user = models.User.objects.get(actor=user_ap_id)
if not user or not user.local:
return HttpResponseBadRequest()
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
models.ShelfBook(
shelf=shelf,
book=book,
added_by=user,
).save()
'''
return HttpResponse()
def handle_incoming_follow(activity):
2020-02-15 21:07:57 +00:00
''' 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'])
models.FollowActivity(
uuid=activity['id'],
user=user,
followed=to_follow,
content=activity,
)
2020-02-15 06:44:07 +00:00
# TODO: allow users to manually approve requests
outgoing.handle_outgoing_accept(user, to_follow, activity)
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'])
accepter.followers.add(requester)
return HttpResponse()
def handle_incoming_create(activity):
''' someone did something, good on them '''
user = get_or_create_remote_user(activity['actor'])
2020-02-15 05:00:11 +00:00
if not 'object' in activity:
return HttpResponseBadRequest()
response = HttpResponse()
2020-02-15 20:32:40 +00:00
if activity['object'].get('fedireadsType') == 'Review' and \
'inReplyTo' in activity['object']:
book = activity['object']['inReplyTo']
2020-02-15 20:32:40 +00:00
book = book.split('/')[-1]
name = activity['object'].get('name')
content = activity['object'].get('content')
rating = activity['object'].get('rating')
2020-02-15 20:32:40 +00:00
if user.local:
review_id = activity['object']['id'].split('/')[-1]
review = models.Review.objects.get(id=review_id)
else:
try:
review = create_review(user, book, name, content, rating)
except ValueError:
return HttpResponseBadRequest()
models.ReviewActivity.objects.create(
uuid=activity['id'],
user=user,
2020-02-15 20:32:40 +00:00
content=activity['object'],
activity_type=activity['object']['type'],
2020-02-15 20:32:40 +00:00
book=review.book,
)
else:
2020-02-15 20:32:40 +00:00
models.Activity.objects.create(
uuid=activity['id'],
user=user,
content=activity,
activity_type=activity['object']['type']
2020-02-15 20:32:40 +00:00
)
2020-02-15 05:00:11 +00:00
return response
2020-01-28 19:45:27 +00:00
def handle_incoming_accept(activity):
''' someone is accepting a follow request '''
# our local user
user = models.User.objects.get(actor=activity['actor'])
# the person our local user wants to follow, who said yes
followed = get_or_create_remote_user(activity['object']['actor'])
# save this relationship in the db
followed.followers.add(user)
# save the activity record
models.FollowActivity(
uuid=activity['id'],
user=user,
followed=followed,
content=activity,
).save()
2020-02-07 23:11:53 +00:00
return HttpResponse()
2020-01-28 19:45:27 +00:00