From dd554ca6ca92a314ce4c340e663c0eb0b6183c72 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 26 Jan 2020 20:57:48 -0800 Subject: [PATCH] Send messages --- fedireads/activitypub_templates.py | 55 +++++++++++++++++++--- fedireads/federation.py | 70 +++++++++++++--------------- fedireads/migrations/0001_initial.py | 12 ++++- fedireads/models.py | 5 +- fedireads/urls.py | 6 +-- fedireads/views.py | 15 ++++-- 6 files changed, 106 insertions(+), 57 deletions(-) diff --git a/fedireads/activitypub_templates.py b/fedireads/activitypub_templates.py index 4d1bc208b..750dd0606 100644 --- a/fedireads/activitypub_templates.py +++ b/fedireads/activitypub_templates.py @@ -1,9 +1,20 @@ ''' generates activitypub formatted objects ''' from uuid import uuid4 from fedireads.settings import DOMAIN +from datetime import datetime +def outbox_collection(user, size): + ''' outbox okay cool ''' + return { + '@context': 'https://www.w3.org/ns/activitystreams', + 'id': '%s/outbox' % user.actor, + 'type': 'OrderedCollection', + 'totalItems': size, + 'first': '%s/outbox?page=true' % user.actor, + 'last': '%s/outbox?min_id=0&page=true' % user.actor + } -def shelve_action(user, book, shelf): +def shelve_activity(user, book, shelf): ''' a user puts a book on a shelf. activitypub action type Add https://www.w3.org/ns/activitystreams#Add ''' @@ -30,6 +41,37 @@ def shelve_action(user, book, shelf): } } + +def create_activity(user, obj): + ''' wraps any object we're broadcasting ''' + uuid = uuid4() + return { + '@context': 'https://www.w3.org/ns/activitystreams', + + 'id': str(uuid), + 'type': 'Create', + 'actor': user.actor, + + 'to': ['%s/followers' % user.actor], + 'cc': ['https://www.w3.org/ns/activitystreams#Public'], + + 'object': obj, + + } + + +def note_object(user, content): + ''' a lil post ''' + uuid = uuid4() + return { + 'id': str(uuid), + 'type': 'Note', + 'published': datetime.utcnow().isoformat(), + 'attributedTo': user.actor, + 'content': content, + 'to': 'https://www.w3.org/ns/activitystreams#Public' + } + def follow_request(user, follow): ''' ask to be friends ''' return { @@ -64,12 +106,11 @@ def actor(user): 'id': user.actor, 'type': 'Person', 'preferredUsername': user.username, - 'inbox': 'https://%s/api/%s/inbox' % (DOMAIN, user.username), - 'followers': 'https://%s/api/u/%s/followers' % \ - (DOMAIN, user.username), + 'inbox': inbox(user), + 'followers': '%s/followers' % user.actor, 'publicKey': { - 'id': 'https://%s/api/u/%s#main-key' % (DOMAIN, user.username), - 'owner': 'https://%s/api/u/%s' % (DOMAIN, user.username), + 'id': '%s/#main-key' % user.actor, + 'owner': user.actor, 'publicKeyPem': user.public_key, } } @@ -77,4 +118,4 @@ def actor(user): def inbox(user): ''' describe an inbox ''' - return 'https://%s/api/%s/inbox' % (DOMAIN, user.username) + return '%s/inbox' % (user.actor) diff --git a/fedireads/federation.py b/fedireads/federation.py index 6f1027f03..b118b0542 100644 --- a/fedireads/federation.py +++ b/fedireads/federation.py @@ -41,7 +41,7 @@ def format_webfinger(user): @csrf_exempt -def actor(request, username): +def get_actor(request, username): ''' return an activitypub actor object ''' user = models.User.objects.get(username=username) return JsonResponse(templates.actor(user)) @@ -52,18 +52,23 @@ def inbox(request, username): ''' incoming activitypub events ''' if request.method == 'GET': # return a collection of something? - pass + return JsonResponse({}) - activity = json.loads(request.body) + # TODO: RSA key verification + + try: + activity = json.loads(request.body) + except json.decoder.JSONDecodeError: + return HttpResponseBadRequest if activity['type'] == 'Add': handle_add(activity) if activity['type'] == 'Follow': response = handle_follow(activity) return JsonResponse(response) - return HttpResponse() + def handle_add(activity): ''' adding a book to a shelf ''' book_id = activity['object']['url'] @@ -100,10 +105,12 @@ def handle_follow(activity): @csrf_exempt def outbox(request, username): + ''' outbox for the requested user ''' user = models.User.objects.get(username=username) + size = models.Message.objects.filter(user=user).count() if request.method == 'GET': # list of activities - return JsonResponse() + return JsonResponse(templates.outbox_collection(user, size)) data = request.body.decode('utf-8') if data.activity.type == 'Follow': @@ -111,42 +118,24 @@ def outbox(request, username): return HttpResponse() -def broadcast_action(sender, action, recipients): +def broadcast_activity(sender, obj, recipients): ''' sign and send out the actions ''' - #models.Message( - # author=sender, - # content=action - #).save() + activity = templates.create_activity(sender, obj) + + # store message in database + models.Message(user=sender, content=activity).save() + for recipient in recipients: - action['to'] = 'https://www.w3.org/ns/activitystreams#Public' - action['cc'] = [recipient] + broadcast(sender, activity, recipient) - inbox_fragment = '/api/%s/inbox' % (sender.username) - 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( - recipient, - data=json.dumps(action), - headers={ - 'Date': now, - 'Signature': signature, - 'Host': DOMAIN, - }, - ) - if not response.ok: - return response.raise_for_status() def broadcast_follow(sender, action, destination): ''' send a follow request ''' - inbox_fragment = '/api/%s/inbox' % (sender.username) + broadcast(sender, action, destination) + +def broadcast(sender, action, destination): + ''' send out an event to all followers ''' + inbox_fragment = '/api/u/%s/inbox' % (sender.username) now = datetime.utcnow().isoformat() message_to_sign = '''(request-target): post %s host: https://%s @@ -167,17 +156,22 @@ date: %s''' % (inbox_fragment, DOMAIN, now) }, ) if not response.ok: - response.raise_for_status() - + response.raise_for_status() def get_or_create_remote_user(activity): + ''' wow, a foreigner ''' 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]) - user = models.User.objects.create_user(username, '', '', actor=actor, local=False) + user = models.User.objects.create_user( + username, + '', '', + actor=actor, + local=False + ) return user diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index 5a1eac172..cd0592fc9 100644 --- a/fedireads/migrations/0001_initial.py +++ b/fedireads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.0.13 on 2020-01-27 03:37 +# Generated by Django 2.0.13 on 2020-01-27 05:42 from django.conf import settings import django.contrib.auth.models @@ -76,6 +76,16 @@ class Migration(migrations.Migration): ('authors', models.ManyToManyField(to='fedireads.Author')), ], ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content', django.contrib.postgres.fields.jsonb.JSONField(max_length=5000)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='Shelf', fields=[ diff --git a/fedireads/models.py b/fedireads/models.py index 7022c3181..f1cb6be2a 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -64,14 +64,11 @@ def execute_after_save(sender, instance, created, *args, **kwargs): class Message(models.Model): ''' any kind of user post, incl. reviews, replies, and status updates ''' - author = models.ForeignKey('User', on_delete=models.PROTECT) + user = models.ForeignKey('User', on_delete=models.PROTECT) content = JSONField(max_length=5000) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) - class Meta: - abstract = True - class Shelf(models.Model): activitypub_id = models.CharField(max_length=255) diff --git a/fedireads/urls.py b/fedireads/urls.py index bee370fa8..f794350cc 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -26,8 +26,8 @@ urlpatterns = [ path('shelve//', views.shelve), path('follow/', views.follow), path('unfollow/', views.unfollow), - path('api/u/', federation.actor), - path('api//inbox', federation.inbox), - path('api//outbox', federation.outbox), + path('api/u/', federation.get_actor), + path('api/u//inbox', federation.inbox), + path('api/u//outbox', federation.outbox), path('.well-known/webfinger', federation.webfinger), ] diff --git a/fedireads/views.py b/fedireads/views.py index e389fc138..7445b14a7 100644 --- a/fedireads/views.py +++ b/fedireads/views.py @@ -7,7 +7,7 @@ from django.template.response import TemplateResponse from django.views.decorators.csrf import csrf_exempt from fedireads import models import fedireads.activitypub_templates as templates -from fedireads.federation import broadcast_action, broadcast_follow +from fedireads.federation import broadcast_activity, broadcast_follow @login_required def home(request): @@ -73,12 +73,19 @@ def shelve(request, shelf_id, book_id): shelf = models.Shelf.objects.get(identifier=shelf_id) # update the database - #models.ShelfBook(book=book, shelf=shelf, added_by=request.user).save() + models.ShelfBook(book=book, shelf=shelf, added_by=request.user).save() # send out the activitypub action - action = templates.shelve_action(request.user, book, shelf) + summary = '%s marked %s as %s' % ( + request.user.username, + book.data['title'], + shelf.name + ) + + obj = templates.note_object(request.user, summary) + #activity = templates.shelve_activity(request.user, book, shelf) recipients = [templates.inbox(u) for u in request.user.followers.all()] - broadcast_action(request.user, action, recipients) + broadcast_activity(request.user, obj, recipients) return redirect('/')