2020-01-25 21:46:30 +00:00
|
|
|
''' activitystream api '''
|
2020-01-27 01:55:02 +00:00
|
|
|
from base64 import b64encode
|
|
|
|
from Crypto.PublicKey import RSA
|
|
|
|
from Crypto.Signature import pkcs1_15
|
|
|
|
from Crypto.Hash import SHA256
|
|
|
|
from datetime import datetime
|
2020-01-26 20:14:27 +00:00
|
|
|
from django.http import HttpResponse, HttpResponseBadRequest, \
|
|
|
|
HttpResponseNotFound, JsonResponse
|
2020-01-27 01:55:02 +00:00
|
|
|
from django.views.decorators.csrf import csrf_exempt
|
2020-01-26 00:24:22 +00:00
|
|
|
from fedireads.settings import DOMAIN
|
2020-01-27 01:55:02 +00:00
|
|
|
from fedireads import models
|
|
|
|
from fedireads import openlibrary
|
2020-01-27 02:49:57 +00:00
|
|
|
import fedireads.activitypub_templates as templates
|
|
|
|
import json
|
2020-01-27 01:55:02 +00:00
|
|
|
import requests
|
2020-01-26 20:14:27 +00:00
|
|
|
|
2020-01-25 21:46:30 +00:00
|
|
|
def webfinger(request):
|
2020-01-26 00:24:22 +00:00
|
|
|
''' allow other servers to ask about a user '''
|
|
|
|
resource = request.GET.get('resource')
|
|
|
|
if not resource and not resource.startswith('acct:'):
|
|
|
|
return HttpResponseBadRequest()
|
2020-01-27 01:55:02 +00:00
|
|
|
ap_id = resource.replace('acct:', '')
|
2020-01-27 03:50:22 +00:00
|
|
|
user = models.User.objects.filter(full_username=ap_id).first()
|
2020-01-26 00:24:22 +00:00
|
|
|
if not user:
|
|
|
|
return HttpResponseNotFound('No account found')
|
|
|
|
return JsonResponse(format_webfinger(user))
|
|
|
|
|
2020-01-26 20:14:27 +00:00
|
|
|
|
2020-01-26 00:24:22 +00:00
|
|
|
def format_webfinger(user):
|
2020-01-26 20:14:27 +00:00
|
|
|
''' helper function to create structured webfinger json '''
|
2020-01-26 00:24:22 +00:00
|
|
|
return {
|
2020-01-27 03:50:22 +00:00
|
|
|
'subject': 'acct:%s' % (user.full_username),
|
2020-01-26 00:24:22 +00:00
|
|
|
'links': [
|
|
|
|
{
|
|
|
|
'rel': 'self',
|
|
|
|
'type': 'application/activity+json',
|
2020-01-27 03:50:22 +00:00
|
|
|
'href': user.actor
|
2020-01-26 00:24:22 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
2020-01-26 20:14:27 +00:00
|
|
|
|
2020-01-27 01:55:02 +00:00
|
|
|
|
|
|
|
@csrf_exempt
|
2020-01-27 04:57:48 +00:00
|
|
|
def get_actor(request, username):
|
2020-01-27 01:55:02 +00:00
|
|
|
''' return an activitypub actor object '''
|
|
|
|
user = models.User.objects.get(username=username)
|
2020-01-27 03:50:22 +00:00
|
|
|
return JsonResponse(templates.actor(user))
|
2020-01-27 01:55:02 +00:00
|
|
|
|
|
|
|
|
|
|
|
@csrf_exempt
|
2020-01-26 20:14:27 +00:00
|
|
|
def inbox(request, username):
|
|
|
|
''' incoming activitypub events '''
|
2020-01-27 01:55:02 +00:00
|
|
|
if request.method == 'GET':
|
|
|
|
# return a collection of something?
|
2020-01-27 04:57:48 +00:00
|
|
|
return JsonResponse({})
|
2020-01-27 01:55:02 +00:00
|
|
|
|
2020-01-27 04:57:48 +00:00
|
|
|
# TODO: RSA key verification
|
|
|
|
|
|
|
|
try:
|
|
|
|
activity = json.loads(request.body)
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
|
|
return HttpResponseBadRequest
|
2020-01-27 01:55:02 +00:00
|
|
|
if activity['type'] == 'Add':
|
|
|
|
handle_add(activity)
|
2020-01-26 20:14:27 +00:00
|
|
|
|
2020-01-27 02:49:57 +00:00
|
|
|
if activity['type'] == 'Follow':
|
|
|
|
response = handle_follow(activity)
|
|
|
|
return JsonResponse(response)
|
2020-01-27 01:55:02 +00:00
|
|
|
return HttpResponse()
|
|
|
|
|
2020-01-27 04:57:48 +00:00
|
|
|
|
2020-01-27 01:55:02 +00:00
|
|
|
def handle_add(activity):
|
2020-01-27 02:49:57 +00:00
|
|
|
''' adding a book to a shelf '''
|
2020-01-27 01:55:02 +00:00
|
|
|
book_id = activity['object']['url']
|
|
|
|
book = openlibrary.get_or_create_book(book_id)
|
|
|
|
user_ap_id = activity['actor'].replace('https//:', '')
|
2020-01-27 03:50:22 +00:00
|
|
|
user = models.User.objects.get(actor=user_ap_id)
|
2020-01-27 01:55:02 +00:00
|
|
|
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
|
|
|
|
models.ShelfBook(
|
|
|
|
shelf=shelf,
|
|
|
|
book=book,
|
|
|
|
added_by=user,
|
|
|
|
).save()
|
2020-01-26 20:14:27 +00:00
|
|
|
|
2020-01-27 02:49:57 +00:00
|
|
|
|
|
|
|
def handle_follow(activity):
|
|
|
|
'''
|
|
|
|
{
|
|
|
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
|
|
"id": "https://friend.camp/768222ce-a1c7-479c-a544-c93b8b67fb54",
|
|
|
|
"type": "Follow",
|
|
|
|
"actor": "https://friend.camp/users/tripofmice",
|
|
|
|
"object": "https://ff2cb3e9.ngrok.io/api/u/mouse"
|
|
|
|
}
|
|
|
|
'''
|
|
|
|
# figure out who they want to follow
|
|
|
|
following = activity['object'].replace('https://%s/api/u/' % DOMAIN, '')
|
|
|
|
following = models.User.objects.get(username=following)
|
|
|
|
# figure out who they are
|
2020-01-27 03:50:22 +00:00
|
|
|
user = get_or_create_remote_user(activity)
|
2020-01-27 02:49:57 +00:00
|
|
|
following.followers.add(user)
|
|
|
|
# accept the request
|
|
|
|
return templates.accept_follow(activity, following)
|
|
|
|
|
|
|
|
|
2020-01-27 01:55:02 +00:00
|
|
|
@csrf_exempt
|
2020-01-26 20:14:27 +00:00
|
|
|
def outbox(request, username):
|
2020-01-27 04:57:48 +00:00
|
|
|
''' outbox for the requested user '''
|
2020-01-27 01:55:02 +00:00
|
|
|
user = models.User.objects.get(username=username)
|
2020-01-27 04:57:48 +00:00
|
|
|
size = models.Message.objects.filter(user=user).count()
|
2020-01-26 20:14:27 +00:00
|
|
|
if request.method == 'GET':
|
|
|
|
# list of activities
|
2020-01-27 04:57:48 +00:00
|
|
|
return JsonResponse(templates.outbox_collection(user, size))
|
2020-01-26 20:14:27 +00:00
|
|
|
|
|
|
|
data = request.body.decode('utf-8')
|
|
|
|
if data.activity.type == 'Follow':
|
|
|
|
handle_follow(data)
|
|
|
|
return HttpResponse()
|
|
|
|
|
2020-01-27 01:55:02 +00:00
|
|
|
|
2020-01-27 04:57:48 +00:00
|
|
|
def broadcast_activity(sender, obj, recipients):
|
2020-01-27 01:55:02 +00:00
|
|
|
''' sign and send out the actions '''
|
2020-01-27 04:57:48 +00:00
|
|
|
activity = templates.create_activity(sender, obj)
|
|
|
|
|
|
|
|
# store message in database
|
|
|
|
models.Message(user=sender, content=activity).save()
|
|
|
|
|
2020-01-27 01:55:02 +00:00
|
|
|
for recipient in recipients:
|
2020-01-27 04:57:48 +00:00
|
|
|
broadcast(sender, activity, recipient)
|
2020-01-27 01:55:02 +00:00
|
|
|
|
|
|
|
|
2020-01-27 03:50:22 +00:00
|
|
|
def broadcast_follow(sender, action, destination):
|
|
|
|
''' send a follow request '''
|
2020-01-27 04:57:48 +00:00
|
|
|
broadcast(sender, action, destination)
|
|
|
|
|
|
|
|
def broadcast(sender, action, destination):
|
|
|
|
''' send out an event to all followers '''
|
|
|
|
inbox_fragment = '/api/u/%s/inbox' % (sender.username)
|
2020-01-27 03:50:22 +00:00
|
|
|
now = datetime.utcnow().isoformat()
|
|
|
|
message_to_sign = '''(request-target): post %s
|
|
|
|
host: https://%s
|
|
|
|
date: %s''' % (inbox_fragment, DOMAIN, now)
|
|
|
|
signer = pkcs1_15.new(RSA.import_key(sender.private_key))
|
|
|
|
signed_message = signer.sign(SHA256.new(message_to_sign.encode('utf8')))
|
|
|
|
|
|
|
|
signature = 'keyId="%s",' % sender.full_username
|
|
|
|
signature += 'headers="(request-target) host date",'
|
|
|
|
signature += 'signature="%s"' % b64encode(signed_message)
|
|
|
|
response = requests.post(
|
|
|
|
destination,
|
|
|
|
data=json.dumps(action),
|
|
|
|
headers={
|
|
|
|
'Date': now,
|
|
|
|
'Signature': signature,
|
|
|
|
'Host': DOMAIN,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if not response.ok:
|
2020-01-27 04:57:48 +00:00
|
|
|
response.raise_for_status()
|
2020-01-26 20:14:27 +00:00
|
|
|
|
|
|
|
def get_or_create_remote_user(activity):
|
2020-01-27 04:57:48 +00:00
|
|
|
''' wow, a foreigner '''
|
2020-01-27 03:50:22 +00:00
|
|
|
actor = activity['actor']
|
|
|
|
try:
|
|
|
|
user = models.User.objects.get(actor=actor)
|
|
|
|
except models.User.DoesNotExist:
|
|
|
|
# TODO: how do you actually correctly learn this?
|
|
|
|
username = '%s@%s' % (actor.split('/')[-1], actor.split('/')[2])
|
2020-01-27 04:57:48 +00:00
|
|
|
user = models.User.objects.create_user(
|
|
|
|
username,
|
|
|
|
'', '',
|
|
|
|
actor=actor,
|
|
|
|
local=False
|
|
|
|
)
|
2020-01-27 03:50:22 +00:00
|
|
|
return user
|
2020-01-26 20:14:27 +00:00
|
|
|
|
|
|
|
|