bookwyrm/fedireads/incoming.py

311 lines
9.5 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-04-22 13:53:22 +00:00
import json
from urllib.parse import urldefrag
import requests
import django.db.utils
2020-03-29 07:05:09 +00:00
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
2020-05-04 01:56:29 +00:00
from fedireads import books_manager, models, outgoing
2020-03-21 22:21:27 +00:00
from fedireads import status as status_builder
from fedireads.remote_user import get_or_create_remote_user, refresh_remote_user
2020-04-01 00:00:01 +00:00
from fedireads.tasks import app
from fedireads.signatures import Signature
2020-04-01 00:00:01 +00:00
@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)
@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()
2020-05-09 21:26:27 +00:00
if not activity.get('object'):
return HttpResponseBadRequest()
if not has_valid_signature(request, activity):
if activity['type'] == 'Delete':
# Pretend that unauth'd deletes succeed. Auth may be failing because
# the resource or owner of the resource might have been deleted.
return HttpResponse()
2020-02-15 04:37:02 +00:00
return HttpResponse(status=401)
handlers = {
'Follow': handle_follow,
'Accept': handle_follow_accept,
'Reject': handle_follow_reject,
2020-03-29 07:05:09 +00:00
'Create': handle_create,
'Like': handle_favorite,
'Announce': handle_boost,
2020-03-30 01:42:34 +00:00
'Add': {
2020-04-01 00:00:01 +00:00
'Tag': handle_tag,
2020-03-30 01:42:34 +00:00
},
'Undo': {
'Follow': handle_unfollow,
'Like': handle_unfavorite,
},
'Update': {
'Person': None,# TODO: handle_update_user
2020-05-04 01:56:29 +00:00
'Document': handle_update_book,
2020-03-30 01:42:34 +00:00
},
}
activity_type = activity['type']
2020-03-30 01:42:34 +00:00
handler = handlers.get(activity_type, None)
if isinstance(handler, dict):
handler = handler.get(activity['object']['type'], None)
2020-03-29 07:05:09 +00:00
2020-04-01 00:00:01 +00:00
if not handler:
return HttpResponseNotFound()
2020-02-19 08:13:06 +00:00
2020-04-01 00:00:01 +00:00
handler.delay(activity)
return HttpResponse()
2020-01-28 19:13:13 +00:00
2020-02-15 04:37:02 +00:00
def has_valid_signature(request, activity):
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
raise ValueError("Wrong actor created signature.")
remote_user = get_or_create_remote_user(key_actor)
try:
signature.verify(remote_user.public_key, request)
except ValueError:
old_key = remote_user.public_key
refresh_remote_user(remote_user)
if remote_user.public_key == old_key:
raise # Key unchanged.
signature.verify(remote_user.public_key, request)
except (ValueError, requests.exceptions.HTTPError):
return False
return True
2020-04-01 00:00:01 +00:00
@app.task
def handle_follow(activity):
2020-02-15 21:07:57 +00:00
''' someone wants to follow a local user '''
2020-05-09 21:26:27 +00:00
# figure out who they want to follow -- not using get_or_create because
# we only allow you to follow local users
try:
2020-05-14 01:23:54 +00:00
to_follow = models.User.objects.get(remote_id=activity['object'])
2020-05-09 21:26:27 +00:00
except models.User.DoesNotExist:
return False
# figure out who the actor is
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
2020-02-23 21:24:23 +00:00
# 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?
2020-04-01 00:00:01 +00:00
return
if not to_follow.manually_approves_followers:
2020-03-21 22:21:27 +00:00
status_builder.create_notification(
to_follow,
'FOLLOW',
related_user=user
)
outgoing.handle_accept(user, to_follow, request)
else:
2020-03-21 22:21:27 +00:00
status_builder.create_notification(
to_follow,
'FOLLOW_REQUEST',
related_user=user
)
2020-02-15 06:44:07 +00:00
2020-04-01 00:00:01 +00:00
@app.task
2020-03-29 07:05:09 +00:00
def handle_unfollow(activity):
2020-02-19 06:44:13 +00:00
''' unfollow a local user '''
obj = activity['object']
try:
requester = get_or_create_remote_user(obj['actor'])
2020-05-14 01:23:54 +00:00
to_unfollow = models.User.objects.get(remote_id=obj['object'])
2020-02-19 06:44:13 +00:00
except models.User.DoesNotExist:
2020-05-09 21:26:27 +00:00
return False
2020-02-19 06:44:13 +00:00
to_unfollow.followers.remove(requester)
2020-04-01 00:00:01 +00:00
@app.task
def handle_follow_accept(activity):
''' hurray, someone remote accepted a follow request '''
# figure out who they want to follow
2020-05-14 01:23:54 +00:00
requester = models.User.objects.get(remote_id=activity['object']['actor'])
# figure out who they are
accepter = get_or_create_remote_user(activity['actor'])
try:
2020-03-14 00:57:36 +00:00
request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=accepter
)
request.delete()
except models.UserFollowRequest.DoesNotExist:
pass
accepter.followers.add(requester)
2020-04-01 00:00:01 +00:00
@app.task
def handle_follow_reject(activity):
''' someone is rejecting a follow request '''
2020-05-14 01:23:54 +00:00
requester = models.User.objects.get(remote_id=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:
2020-05-09 21:26:27 +00:00
return False
2020-04-01 00:00:01 +00:00
@app.task
def handle_create(activity):
''' someone did something, good on them '''
user = get_or_create_remote_user(activity['actor'])
2020-02-15 05:00:11 +00:00
2020-03-29 07:05:09 +00:00
if user.local:
# we really oughtn't even be sending in this case
return True
2020-03-29 07:05:09 +00:00
if activity['object'].get('fedireadsType') and \
2020-03-21 23:50:49 +00:00
'inReplyToBook' in activity['object']:
2020-05-09 21:26:27 +00:00
if activity['object']['fedireadsType'] == 'Review':
builder = status_builder.create_review_from_activity
elif activity['object']['fedireadsType'] == 'Quotation':
builder = status_builder.create_quotation_from_activity
else:
builder = status_builder.create_comment_from_activity
# create the status, it'll throw a ValueError if anything is missing
builder(user, activity['object'])
elif activity['object'].get('inReplyTo'):
# only create the status if it's in reply to a status we already know
if not status_builder.get_status(activity['object']['inReplyTo']):
return True
2020-05-09 21:26:27 +00:00
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,
2020-03-21 22:21:27 +00:00
)
return True
2020-02-15 05:00:11 +00:00
2020-04-01 00:00:01 +00:00
@app.task
def handle_favorite(activity):
2020-02-19 08:13:06 +00:00
''' approval of your good good post '''
2020-04-01 00:00:01 +00:00
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):
2020-05-09 21:26:27 +00:00
return False
2020-04-01 00:00:01 +00:00
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,
)
2020-02-19 08:13:06 +00:00
2020-04-01 00:00:01 +00:00
@app.task
def handle_unfavorite(activity):
2020-03-21 22:21:27 +00:00
''' approval of your good good post '''
favorite_id = activity['object']['id']
fav = models.Favorite.objects.filter(remote_id=favorite_id).first()
if not fav:
2020-05-09 21:26:27 +00:00
return False
2020-03-21 22:21:27 +00:00
fav.delete()
2020-04-01 00:00:01 +00:00
@app.task
def handle_boost(activity):
''' someone gave us a boost! '''
try:
status_id = activity['object'].split('/')[-1]
status = models.Status.objects.get(id=status_id)
booster = get_or_create_remote_user(activity['actor'])
except (models.Status.DoesNotExist, models.User.DoesNotExist):
2020-05-09 21:26:27 +00:00
return False
if not booster.local:
status_builder.create_boost_from_activity(booster, activity)
status_builder.create_notification(
status.user,
'BOOST',
related_user=booster,
related_status=status,
)
2020-04-01 00:00:01 +00:00
@app.task
def handle_tag(activity):
2020-05-10 18:29:10 +00:00
''' someone is tagging a book '''
2020-04-01 00:00:01 +00:00
user = get_or_create_remote_user(activity['actor'])
if not user.local:
2020-05-09 21:26:27 +00:00
book = activity['target']['id']
2020-04-01 00:00:01 +00:00
status_builder.create_tag(user, book, activity['object']['name'])
2020-05-04 01:56:29 +00:00
@app.task
def handle_update_book(activity):
''' a remote instance changed a book (Document) '''
document = activity['object']
# check if we have their copy and care about their updates
book = models.Book.objects.select_subclasses().filter(
remote_id=document['url'],
sync=True,
).first()
if not book:
return
books_manager.update_book(book, data=document)