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
|
2020-11-28 19:00:40 +00:00
|
|
|
from urllib.parse import urldefrag
|
2020-05-13 10:40:57 +00:00
|
|
|
|
2020-02-23 09:27:10 +00:00
|
|
|
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
|
2020-09-17 20:02:52 +00:00
|
|
|
import requests
|
2020-01-28 09:01:33 +00:00
|
|
|
|
2020-11-29 03:03:37 +00:00
|
|
|
from bookwyrm import activitypub, models, outgoing
|
2020-09-21 15:10:37 +00:00
|
|
|
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)
|
2020-01-28 09:01:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
@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:
|
2020-10-05 22:23:39 +00:00
|
|
|
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()
|
|
|
|
|
2020-05-22 12:53:56 +00:00
|
|
|
if not has_valid_signature(request, activity):
|
2020-05-22 13:20:27 +00:00
|
|
|
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)
|
|
|
|
|
2020-03-29 06:52:07 +00:00
|
|
|
handlers = {
|
|
|
|
'Follow': handle_follow,
|
|
|
|
'Accept': handle_follow_accept,
|
|
|
|
'Reject': handle_follow_reject,
|
2020-03-29 07:05:09 +00:00
|
|
|
'Create': handle_create,
|
2020-10-17 00:00:10 +00:00
|
|
|
'Delete': handle_delete_status,
|
2020-03-29 06:52:07 +00:00
|
|
|
'Like': handle_favorite,
|
2020-03-30 14:13:32 +00:00
|
|
|
'Announce': handle_boost,
|
2020-03-30 01:42:34 +00:00
|
|
|
'Add': {
|
2020-11-28 19:00:40 +00:00
|
|
|
'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,
|
2020-03-30 01:42:34 +00:00
|
|
|
},
|
|
|
|
'Update': {
|
2020-10-16 22:40:23 +00:00
|
|
|
'Person': handle_update_user,
|
2020-12-15 20:39:09 +00:00
|
|
|
'Edition': handle_update_edition,
|
|
|
|
'Work': handle_update_work,
|
2020-03-30 01:42:34 +00:00
|
|
|
},
|
2020-03-29 06:52:07 +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
|
|
|
|
2020-05-22 12:53:56 +00:00
|
|
|
def has_valid_signature(request, activity):
|
2020-09-17 20:02:52 +00:00
|
|
|
''' verify incoming signature '''
|
2020-05-22 12:53:56 +00:00
|
|
|
try:
|
|
|
|
signature = Signature.parse(request)
|
|
|
|
|
|
|
|
key_actor = urldefrag(signature.key_id).url
|
|
|
|
if key_actor != activity.get('actor'):
|
|
|
|
raise ValueError("Wrong actor created signature.")
|
|
|
|
|
2020-11-28 19:48:17 +00:00
|
|
|
remote_user = activitypub.resolve_remote_id(models.User, key_actor)
|
2020-12-01 03:53:42 +00:00
|
|
|
if not remote_user:
|
|
|
|
return False
|
2020-05-22 12:53:56 +00:00
|
|
|
|
|
|
|
try:
|
2020-12-01 03:33:50 +00:00
|
|
|
signature.verify(remote_user.key_pair.public_key, request)
|
2020-05-22 12:53:56 +00:00
|
|
|
except ValueError:
|
2020-12-01 03:33:50 +00:00
|
|
|
old_key = remote_user.key_pair.public_key
|
2020-12-05 00:26:07 +00:00
|
|
|
remote_user = activitypub.resolve_remote_id(
|
2020-12-04 17:46:40 +00:00
|
|
|
models.User, remote_user.remote_id, refresh=True
|
2020-11-28 19:48:17 +00:00
|
|
|
)
|
2020-12-01 03:33:50 +00:00
|
|
|
if remote_user.key_pair.public_key == old_key:
|
2020-05-22 12:53:56 +00:00
|
|
|
raise # Key unchanged.
|
2020-12-01 03:33:50 +00:00
|
|
|
signature.verify(remote_user.key_pair.public_key, request)
|
2020-05-22 12:53:56 +00:00
|
|
|
except (ValueError, requests.exceptions.HTTPError):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-04-01 00:00:01 +00:00
|
|
|
@app.task
|
2020-03-29 06:52:07 +00:00
|
|
|
def handle_follow(activity):
|
2020-02-15 21:07:57 +00:00
|
|
|
''' someone wants to follow a local user '''
|
2020-10-15 00:29:43 +00:00
|
|
|
try:
|
2020-11-28 20:25:53 +00:00
|
|
|
relationship = activitypub.Follow(
|
|
|
|
**activity
|
|
|
|
).to_model(models.UserFollowRequest)
|
2020-03-14 22:20:01 +00:00
|
|
|
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
|
2020-02-23 09:27:10 +00:00
|
|
|
|
2020-11-28 20:25:53 +00:00
|
|
|
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:
|
2020-10-16 21:14:07 +00:00
|
|
|
outgoing.handle_accept(relationship)
|
2020-02-15 06:44:07 +00:00
|
|
|
|
2020-01-28 09:01:33 +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'])
|
2020-09-17 20:02:52 +00:00
|
|
|
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
|
2020-03-29 06:52:07 +00:00
|
|
|
def handle_follow_accept(activity):
|
2020-02-15 04:24:35 +00:00
|
|
|
''' 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'])
|
2020-02-15 04:24:35 +00:00
|
|
|
# figure out who they are
|
2020-11-28 19:48:17 +00:00
|
|
|
accepter = activitypub.resolve_remote_id(models.User, activity['actor'])
|
2020-02-15 04:24:35 +00:00
|
|
|
|
2020-03-13 17:04:39 +00:00
|
|
|
try:
|
2020-03-14 00:57:36 +00:00
|
|
|
request = models.UserFollowRequest.objects.get(
|
|
|
|
user_subject=requester,
|
|
|
|
user_object=accepter
|
|
|
|
)
|
2020-03-13 17:04:39 +00:00
|
|
|
request.delete()
|
|
|
|
except models.UserFollowRequest.DoesNotExist:
|
|
|
|
pass
|
2020-03-21 21:25:31 +00:00
|
|
|
accepter.followers.add(requester)
|
2020-02-15 04:24:35 +00:00
|
|
|
|
|
|
|
|
2020-04-01 00:00:01 +00:00
|
|
|
@app.task
|
2020-03-29 06:52:07 +00:00
|
|
|
def handle_follow_reject(activity):
|
2020-03-13 17:04:39 +00:00
|
|
|
''' someone is rejecting a follow request '''
|
2020-05-14 01:23:54 +00:00
|
|
|
requester = models.User.objects.get(remote_id=activity['object']['actor'])
|
2020-11-28 19:48:17 +00:00
|
|
|
rejecter = activitypub.resolve_remote_id(models.User, activity['actor'])
|
2020-03-13 17:04:39 +00:00
|
|
|
|
2020-09-17 20:02:52 +00:00
|
|
|
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
|
2020-03-13 17:04:39 +00:00
|
|
|
|
|
|
|
|
2020-04-01 00:00:01 +00:00
|
|
|
@app.task
|
2020-03-29 06:52:07 +00:00
|
|
|
def handle_create(activity):
|
2020-01-28 09:01:33 +00:00
|
|
|
''' someone did something, good on them '''
|
2020-11-07 18:29:46 +00:00
|
|
|
# deduplicate incoming activities
|
2020-12-13 04:59:41 +00:00
|
|
|
activity = activity['object']
|
|
|
|
status_id = activity['id']
|
2020-11-07 18:29:46 +00:00
|
|
|
if models.Status.objects.filter(remote_id=status_id).count():
|
|
|
|
return
|
|
|
|
|
2020-11-28 19:48:17 +00:00
|
|
|
serializer = activitypub.activity_objects[activity['type']]
|
2020-12-13 04:59:41 +00:00
|
|
|
activity = serializer(**activity)
|
2020-11-28 19:48:17 +00:00
|
|
|
try:
|
|
|
|
model = models.activity_models[activity.type]
|
|
|
|
except KeyError:
|
|
|
|
# not a type of status we are prepared to deserialize
|
2020-11-08 04:13:13 +00:00
|
|
|
return
|
2020-09-17 20:02:52 +00:00
|
|
|
|
2020-11-28 19:48:17 +00:00
|
|
|
if activity.type == 'Note':
|
2020-12-13 05:11:51 +00:00
|
|
|
# keep notes if they are replies to existing statuses
|
2020-11-28 19:48:17 +00:00
|
|
|
reply = models.Status.objects.filter(
|
|
|
|
remote_id=activity.inReplyTo
|
|
|
|
).first()
|
2020-12-13 05:11:51 +00:00
|
|
|
|
2020-11-28 19:48:17 +00:00
|
|
|
if not reply:
|
2020-12-13 05:11:51 +00:00
|
|
|
discard = True
|
|
|
|
# keep notes if they mention local users
|
|
|
|
tags = [l['href'] for l in activity.tag if l['type'] == 'Mention']
|
|
|
|
for tag in tags:
|
|
|
|
if models.User.objects.filter(
|
|
|
|
remote_id=tag, local=True).exists():
|
|
|
|
# we found a mention of a known use boost
|
|
|
|
discard = False
|
|
|
|
break
|
|
|
|
if discard:
|
|
|
|
return
|
2020-11-28 19:48:17 +00:00
|
|
|
|
2020-12-13 04:59:41 +00:00
|
|
|
status = activity.to_model(model)
|
2020-09-17 20:02:52 +00:00
|
|
|
# create a notification if this is a reply
|
2020-12-17 00:20:40 +00:00
|
|
|
notified = []
|
2020-09-17 20:02:52 +00:00
|
|
|
if status.reply_parent and status.reply_parent.user.local:
|
2020-12-17 00:20:40 +00:00
|
|
|
notified.append(status.reply_parent.user)
|
2020-09-17 20:02:52 +00:00
|
|
|
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 00:20:40 +00:00
|
|
|
for mentioned_user in status.mention_users.all() and \
|
|
|
|
mentioned_user not in notified:
|
2020-12-14 20:31:11 +00:00
|
|
|
if not mentioned_user.local:
|
|
|
|
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 '''
|
2020-11-12 20:02:43 +00:00
|
|
|
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:
|
2020-12-14 20:39:37 +00:00
|
|
|
status = models.Status.objects.get(
|
2020-10-17 00:00:10 +00:00
|
|
|
remote_id=status_id
|
|
|
|
)
|
|
|
|
except models.Status.DoesNotExist:
|
|
|
|
return
|
2020-12-14 20:39:37 +00:00
|
|
|
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
|
2020-03-29 06:52:07 +00:00
|
|
|
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)
|
2020-09-17 20:02:52 +00:00
|
|
|
|
2020-11-05 00:28:32 +00:00
|
|
|
fav = fav.to_model(models.Favorite)
|
2020-11-28 19:48:17 +00:00
|
|
|
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',
|
2020-11-28 19:48:17 +00:00
|
|
|
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
|
2020-03-29 06:52:07 +00:00
|
|
|
def handle_unfavorite(activity):
|
2020-03-21 22:21:27 +00:00
|
|
|
''' approval of your good good post '''
|
2020-11-08 03:15:04 +00:00
|
|
|
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
|
2020-03-30 14:13:32 +00:00
|
|
|
def handle_boost(activity):
|
|
|
|
''' someone gave us a boost! '''
|
2020-11-12 19:59:34 +00:00
|
|
|
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-03-30 14:13:32 +00:00
|
|
|
|
|
|
|
|
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-03-30 14:13:32 +00:00
|
|
|
|
|
|
|
|
2020-04-01 00:00:01 +00:00
|
|
|
@app.task
|
2020-11-28 19:00:40 +00:00
|
|
|
def handle_add(activity):
|
2020-11-02 23:10:41 +00:00
|
|
|
''' putting a book on a shelf '''
|
2020-11-29 02:23:34 +00:00
|
|
|
#this is janky as heck but I haven't thought of a better solution
|
|
|
|
try:
|
2020-11-28 19:00:40 +00:00
|
|
|
activitypub.AddBook(**activity).to_model(models.ShelfBook)
|
2020-11-29 02:23:34 +00:00
|
|
|
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
|
2020-12-15 20:39:09 +00:00
|
|
|
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)
|
2020-12-15 20:39:09 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.task
|
|
|
|
def handle_update_work(activity):
|
|
|
|
''' a remote instance changed a book (Document) '''
|
|
|
|
activitypub.Work(**activity['object']).to_model(models.Work)
|