moviewyrm/bookwyrm/incoming.py

393 lines
12 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 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-12-14 19:29:22 +00:00
from django.views.decorators.http import require_POST
import requests
2021-01-13 21:36:01 +00:00
from bookwyrm import activitypub, models, views
from bookwyrm import status as status_builder
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
2020-04-01 00:00:01 +00:00
@csrf_exempt
2020-12-14 19:29:22 +00:00
@require_POST
2020-04-01 00:00:01 +00:00
def inbox(request, username):
''' incoming activitypub events '''
try:
models.User.objects.get(localname=username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
return shared_inbox(request)
@csrf_exempt
2020-12-14 19:29:22 +00:00
@require_POST
2020-01-28 19:13:13 +00:00
def shared_inbox(request):
''' incoming activitypub events '''
try:
resp = request.body
activity = json.loads(resp)
2020-10-14 17:24:05 +00:00
if isinstance(activity, str):
activity = json.loads(activity)
2020-09-21 17:25:26 +00:00
activity_object = activity['object']
except (json.decoder.JSONDecodeError, KeyError):
2020-05-09 21:26:27 +00:00
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)
# if this isn't a file ripe for refactor, I don't know what is.
handlers = {
'Follow': handle_follow,
'Accept': handle_follow_accept,
'Reject': handle_follow_reject,
2021-01-23 19:03:10 +00:00
'Block': handle_block,
'Create': {
'BookList': handle_create_list,
'Note': handle_create_status,
'Article': handle_create_status,
'Review': handle_create_status,
'Comment': handle_create_status,
'Quotation': handle_create_status,
},
2020-10-17 00:00:10 +00:00
'Delete': handle_delete_status,
'Like': handle_favorite,
'Announce': handle_boost,
2020-03-30 01:42:34 +00:00
'Add': {
'Edition': handle_add,
2020-03-30 01:42:34 +00:00
},
'Undo': {
'Follow': handle_unfollow,
'Like': handle_unfavorite,
2020-11-05 00:28:32 +00:00
'Announce': handle_unboost,
2021-01-26 21:00:36 +00:00
'Block': handle_unblock,
2020-03-30 01:42:34 +00:00
},
'Update': {
2020-10-16 22:40:23 +00:00
'Person': handle_update_user,
'Edition': handle_update_edition,
'Work': handle_update_work,
'BookList': handle_update_list,
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):
2020-09-21 17:25:26 +00:00
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):
''' verify incoming signature '''
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 = activitypub.resolve_remote_id(models.User, key_actor)
2020-12-01 03:53:42 +00:00
if not remote_user:
return False
try:
signature.verify(remote_user.key_pair.public_key, request)
except ValueError:
old_key = remote_user.key_pair.public_key
remote_user = activitypub.resolve_remote_id(
2020-12-04 17:46:40 +00:00
models.User, remote_user.remote_id, refresh=True
)
if remote_user.key_pair.public_key == old_key:
raise # Key unchanged.
signature.verify(remote_user.key_pair.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 '''
try:
relationship = activitypub.Follow(
**activity
).to_model(models.UserFollowRequest)
except django.db.utils.IntegrityError as err:
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
raise
2020-10-16 21:28:25 +00:00
relationship = models.UserFollowRequest.objects.get(
remote_id=activity['id']
)
2020-10-16 19:24:29 +00:00
# send the accept normally for a duplicate request
manually_approves = relationship.user_object.manually_approves_followers
status_builder.create_notification(
relationship.user_object,
'FOLLOW_REQUEST' if manually_approves else 'FOLLOW',
related_user=relationship.user_subject
)
if not manually_approves:
2021-01-13 21:36:01 +00:00
views.handle_accept(relationship)
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']
2020-12-14 19:46:31 +00:00
requester = activitypub.resolve_remote_id(models.User, obj['actor'])
to_unfollow = models.User.objects.get(remote_id=obj['object'])
# raises models.User.DoesNotExist
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 = activitypub.resolve_remote_id(models.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 = activitypub.resolve_remote_id(models.User, activity['actor'])
request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=rejecter
)
request.delete()
2020-10-16 21:28:25 +00:00
#raises models.UserFollowRequest.DoesNotExist
2021-01-23 19:03:10 +00:00
@app.task
def handle_block(activity):
''' blocking a user '''
# create "block" databse entry
2021-01-26 21:00:36 +00:00
activitypub.Block(**activity).to_model(models.UserBlocks)
2021-01-25 01:07:19 +00:00
# the removing relationships is handled in post-save hook in model
2021-01-23 19:03:10 +00:00
2021-01-26 21:00:36 +00:00
@app.task
def handle_unblock(activity):
''' undoing a block '''
try:
block_id = activity['object']['id']
except KeyError:
return
try:
block = models.UserBlocks.objects.get(remote_id=block_id)
except models.UserBlocks.DoesNotExist:
return
block.delete()
2020-04-01 00:00:01 +00:00
@app.task
def handle_create_list(activity):
''' a new list '''
activity = activity['object']
activitypub.BookList(**activity).to_model(models.List)
@app.task
def handle_update_list(activity):
''' update a list '''
try:
book_list = models.List.objects.get(id=activity['object']['id'])
except models.List.DoesNotExist:
return
activitypub.BookList(
**activity['object']).to_model(models.List, instance=book_list)
@app.task
def handle_create_status(activity):
''' someone did something, good on them '''
# deduplicate incoming activities
2020-12-13 04:59:41 +00:00
activity = activity['object']
status_id = activity.get('id')
if models.Status.objects.filter(remote_id=status_id).count():
return
try:
serializer = activitypub.activity_objects[activity['type']]
except KeyError:
return
2020-12-13 04:59:41 +00:00
activity = serializer(**activity)
try:
model = models.activity_models[activity.type]
except KeyError:
# not a type of status we are prepared to deserialize
return
2020-12-13 04:59:41 +00:00
status = activity.to_model(model)
2020-12-18 20:38:27 +00:00
if not status:
# it was discarded because it's not a bookwyrm type
return
# create a notification if this is a reply
notified = []
if status.reply_parent and status.reply_parent.user.local:
notified.append(status.reply_parent.user)
status_builder.create_notification(
status.reply_parent.user,
'REPLY',
related_user=status.user,
related_status=status,
2020-05-09 21:26:27 +00:00
)
2020-12-14 20:31:11 +00:00
if status.mention_users.exists():
2020-12-17 02:40:43 +00:00
for mentioned_user in status.mention_users.all():
if not mentioned_user.local or mentioned_user in notified:
2020-12-14 20:31:11 +00:00
continue
status_builder.create_notification(
mentioned_user,
'MENTION',
related_user=status.user,
related_status=status,
)
2020-02-15 05:00:11 +00:00
2020-10-17 00:00:10 +00:00
@app.task
def handle_delete_status(activity):
''' remove a status '''
try:
status_id = activity['object']['id']
except TypeError:
# this isn't a great fix, because you hit this when mastadon
# is trying to delete a user.
return
2020-10-17 00:00:10 +00:00
try:
status = models.Status.objects.get(
2020-10-17 00:00:10 +00:00
remote_id=status_id
)
except models.Status.DoesNotExist:
return
models.Notification.objects.filter(related_status=status).all().delete()
2020-10-17 00:00:10 +00:00
status_builder.delete_status(status)
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-11-05 00:28:32 +00:00
fav = activitypub.Like(**activity)
# we dont know this status, we don't care about this status
if not models.Status.objects.filter(remote_id=fav.object).exists():
return
2020-11-05 00:28:32 +00:00
fav = fav.to_model(models.Favorite)
if fav.user.local:
return
2020-04-01 00:00:01 +00:00
status_builder.create_notification(
2020-11-05 00:28:32 +00:00
fav.status.user,
2020-04-01 00:00:01 +00:00
'FAVORITE',
related_user=fav.user,
2020-11-05 00:28:32 +00:00
related_status=fav.status,
2020-04-01 00:00:01 +00:00
)
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 '''
like = models.Favorite.objects.filter(
remote_id=activity['object']['id']
).first()
if not like:
2020-11-08 02:18:44 +00:00
return
2020-11-05 00:28:32 +00:00
like.delete()
2020-03-21 22:21:27 +00:00
2020-04-01 00:00:01 +00:00
@app.task
def handle_boost(activity):
''' someone gave us a boost! '''
try:
boost = activitypub.Boost(**activity).to_model(models.Boost)
except activitypub.ActivitySerializerError:
# this probably just means we tried to boost an unknown status
return
2020-11-05 00:28:32 +00:00
if not boost.user.local:
status_builder.create_notification(
boost.boosted_status.user,
'BOOST',
related_user=boost.user,
related_status=boost.boosted_status,
)
2020-11-05 00:28:32 +00:00
@app.task
def handle_unboost(activity):
''' someone gave us a boost! '''
2020-11-06 22:25:48 +00:00
boost = models.Boost.objects.filter(
remote_id=activity['object']['id']
).first()
if boost:
2020-11-08 02:59:38 +00:00
boost.delete()
2020-04-01 00:00:01 +00:00
@app.task
def handle_add(activity):
2020-11-02 23:10:41 +00:00
''' putting a book on a shelf '''
#this is janky as heck but I haven't thought of a better solution
try:
activitypub.AddBook(**activity).to_model(models.ShelfBook)
except activitypub.ActivitySerializerError:
activitypub.AddBook(**activity).to_model(models.Tag)
2020-11-02 23:10:41 +00:00
2020-10-16 22:40:23 +00:00
@app.task
def handle_update_user(activity):
''' receive an updated user Person activity object '''
try:
user = models.User.objects.get(remote_id=activity['object']['id'])
except models.User.DoesNotExist:
# who is this person? who cares
return
activitypub.Person(
**activity['object']
).to_model(models.User, instance=user)
# model save() happens in the to_model function
2020-05-04 01:56:29 +00:00
@app.task
def handle_update_edition(activity):
2020-05-04 01:56:29 +00:00
''' a remote instance changed a book (Document) '''
2020-11-29 03:03:37 +00:00
activitypub.Edition(**activity['object']).to_model(models.Edition)
@app.task
def handle_update_work(activity):
''' a remote instance changed a book (Document) '''
activitypub.Work(**activity['object']).to_model(models.Work)