From 31110f4b0c89f1177541dc9d080ee14bb7bca85a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 28 Jan 2020 11:13:13 -0800 Subject: [PATCH] broadcast to shared inboxes --- fedireads/incoming.py | 86 ++++++++++++++++++---------- fedireads/migrations/0001_initial.py | 3 +- fedireads/models.py | 2 + fedireads/outgoing.py | 79 ++++++++++++++++--------- fedireads/urls.py | 1 + 5 files changed, 113 insertions(+), 58 deletions(-) diff --git a/fedireads/incoming.py b/fedireads/incoming.py index a2163c09f..53f4114ce 100644 --- a/fedireads/incoming.py +++ b/fedireads/incoming.py @@ -30,6 +30,61 @@ def webfinger(request): }) +@csrf_exempt +def shared_inbox(request): + ''' incoming activitypub events ''' + if request.method == 'GET': + return HttpResponseNotFound() + + # TODO: RSA key verification + + try: + activity = json.loads(request.body) + except json.decoder.JSONDecodeError: + return HttpResponseBadRequest + + if activity['type'] == 'Add': + return handle_incoming_shelve(activity) + + if activity['type'] == 'Follow': + return handle_incoming_follow(activity) + + if activity['type'] == 'Create': + return handle_incoming_create(activity) + + return HttpResponse() + + +@csrf_exempt +def inbox(request, username): + ''' incoming activitypub events ''' + if request.method == 'GET': + return HttpResponseNotFound() + + # TODO: RSA key verification + + try: + activity = json.loads(request.body) + except json.decoder.JSONDecodeError: + return HttpResponseBadRequest + + # TODO: should do some kind of checking if the user accepts + # this action from the sender + # but this will just throw an error if the user doesn't exist I guess + models.User.objects.get(localname=username) + + if activity['type'] == 'Add': + return handle_incoming_shelve(activity) + + if activity['type'] == 'Follow': + return handle_incoming_follow(activity) + + if activity['type'] == 'Create': + return handle_incoming_create(activity) + + return HttpResponse() + + @csrf_exempt def get_actor(request, username): ''' return an activitypub actor object ''' @@ -61,37 +116,6 @@ def get_actor(request, username): }) -@csrf_exempt -def inbox(request, username): - ''' incoming activitypub events ''' - if request.method == 'GET': - # TODO: return a collection of something? - return JsonResponse({}) - - # TODO: RSA key verification - - try: - activity = json.loads(request.body) - except json.decoder.JSONDecodeError: - return HttpResponseBadRequest - - # TODO: should do some kind of checking if the user accepts - # this action from the sender - # but this will just throw an error if the user doesn't exist I guess - models.User.objects.get(localname=username) - - if activity['type'] == 'Add': - return handle_incoming_shelve(activity) - - if activity['type'] == 'Follow': - return handle_incoming_follow(activity) - - if activity['type'] == 'Create': - return handle_incoming_create(activity) - - return HttpResponse() - - def handle_incoming_shelve(activity): ''' receiving an Add activity (to shelve a book) ''' # TODO what happens here? If it's a remote over, then I think diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index 9b0c59e2f..e3bbf1faa 100644 --- a/fedireads/migrations/0001_initial.py +++ b/fedireads/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.0.2 on 2020-01-28 09:04 +# Generated by Django 3.0.2 on 2020-01-28 19:06 from django.conf import settings import django.contrib.auth.models @@ -37,6 +37,7 @@ class Migration(migrations.Migration): ('api_key', models.CharField(blank=True, max_length=255, null=True)), ('actor', models.CharField(max_length=255)), ('inbox', models.CharField(max_length=255)), + ('shared_inbox', models.CharField(max_length=255)), ('outbox', models.CharField(max_length=255)), ('summary', models.TextField(blank=True, null=True)), ('local', models.BooleanField(default=True)), diff --git a/fedireads/models.py b/fedireads/models.py index 1f21aafe6..4a2d73d8f 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -15,6 +15,7 @@ class User(AbstractUser): api_key = models.CharField(max_length=255, blank=True, null=True) actor = models.CharField(max_length=255) inbox = models.CharField(max_length=255) + shared_inbox = models.CharField(max_length=255) outbox = models.CharField(max_length=255) summary = models.TextField(blank=True, null=True) local = models.BooleanField(default=True) @@ -49,6 +50,7 @@ def execute_before_save(sender, instance, *args, **kwargs): instance.username = '%s@%s' % (instance.username, DOMAIN) instance.actor = 'https://%s/user/%s' % (DOMAIN, instance.localname) instance.inbox = 'https://%s/user/%s/inbox' % (DOMAIN, instance.localname) + instance.shared_inbox = 'https://%s/inbox' % DOMAIN instance.outbox = 'https://%s/user/%s/outbox' % (DOMAIN, instance.localname) if not instance.private_key: random_generator = Random.new().read diff --git a/fedireads/outgoing.py b/fedireads/outgoing.py index 2b4dffc71..0e25ecfa8 100644 --- a/fedireads/outgoing.py +++ b/fedireads/outgoing.py @@ -14,8 +14,30 @@ import requests from uuid import uuid4 +@csrf_exempt +def outbox(request, username): + ''' outbox for the requested user ''' + user = models.User.objects.get(localname=username) + size = models.Review.objects.filter(user=user).count() + if request.method == 'GET': + # list of activities + return JsonResponse({ + '@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 + }) + # TODO: paginated list of messages + + #data = request.body.decode('utf-8') + return HttpResponse() + + def handle_account_search(query): ''' webfingerin' other servers ''' + user = None domain = query.split('@')[1] try: user = models.User.objects.get(username=query) @@ -56,13 +78,18 @@ def handle_response(response): if activity['type'] == 'Accept': handle_incoming_accept(activity) + def handle_incoming_accept(activity): ''' someone is accepting a follow request ''' - # not actually a remote user so this is kinda janky - user = get_or_create_remote_user(activity['actor']) + # our local user + user = models.User.objects.get(actor=activity['actor']) # the person our local user wants to follow, who said yes - followed = models.User.objects.get(actor=activity['object']['actor']) + followed = get_or_create_remote_user(activity['object']['actor']) + + # save this relationship in the db followed.followers.add(user) + + # save the activity record models.FollowActivity( uuid=activity['id'], user=user, @@ -72,7 +99,7 @@ def handle_incoming_accept(activity): def handle_shelve(user, book, shelf): - ''' gettin organized ''' + ''' a local user is getting a book put on their shelf ''' # update the database models.ShelfBook(book=book, shelf=shelf, added_by=user).save() @@ -101,8 +128,7 @@ def handle_shelve(user, book, shelf): 'id': shelf.activitypub_id } } - # TODO: this should be getting shared inboxes and deduplicating - recipients = [u.inbox for u in user.followers.all()] + recipients = get_recipients(user, 'public') models.ShelveActivity( uuid=uuid, @@ -116,6 +142,26 @@ def handle_shelve(user, book, shelf): broadcast(user, activity, recipients) +def get_recipients(user, post_privacy, direct_recipients=None): + ''' deduplicated list of recipients ''' + recipients = direct_recipients or [] + + followers = user.followers.all() + if post_privacy == 'public': + # post to public shared inboxes + shared_inboxes = set(u.shared_inbox for u in followers) + recipients += list(shared_inboxes) + # TODO: direct to anyone who's mentioned + if post_privacy == 'followers': + # don't send it to the shared inboxes + inboxes = set(u.inbox for u in followers) + recipients += list(inboxes) + # if post privacy is direct, we just have direct recipients, + # which is already set. hurray + return recipients + + + def handle_review(user, book, name, content, rating): ''' post a review ''' review_uuid = uuid4() @@ -130,7 +176,7 @@ def handle_review(user, book, name, content, rating): 'rating': rating, # fedireads-only custom field 'to': 'https://www.w3.org/ns/activitystreams#Public' } - recipients = [u.inbox for u in user.followers.all()] + recipients = get_recipients(user, 'public') create_uuid = uuid4() activity = { '@context': 'https://www.w3.org/ns/activitystreams', @@ -160,25 +206,6 @@ def handle_review(user, book, name, content, rating): broadcast(user, activity, recipients) -@csrf_exempt -def outbox(request, username): - ''' outbox for the requested user ''' - user = models.User.objects.get(localname=username) - size = models.Review.objects.filter(user=user).count() - if request.method == 'GET': - # list of activities - return JsonResponse({ - '@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 - }) - # TODO: paginated list of messages - - #data = request.body.decode('utf-8') - return HttpResponse() def broadcast(sender, action, recipients): diff --git a/fedireads/urls.py b/fedireads/urls.py index 0bed8aa40..111d513fe 100644 --- a/fedireads/urls.py +++ b/fedireads/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path('admin/', admin.site.urls), # federation endpoints + path('/inbox', incoming.shared_inbox), path('user/.json', incoming.get_actor), path('user//inbox', incoming.inbox), path('user//outbox', outgoing.outbox),