broadcast to shared inboxes

This commit is contained in:
Mouse Reeve 2020-01-28 11:13:13 -08:00
parent 17468177dd
commit 31110f4b0c
5 changed files with 113 additions and 58 deletions

View file

@ -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 @csrf_exempt
def get_actor(request, username): def get_actor(request, username):
''' return an activitypub actor object ''' ''' 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): def handle_incoming_shelve(activity):
''' receiving an Add activity (to shelve a book) ''' ''' receiving an Add activity (to shelve a book) '''
# TODO what happens here? If it's a remote over, then I think # TODO what happens here? If it's a remote over, then I think

View file

@ -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 from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
@ -37,6 +37,7 @@ class Migration(migrations.Migration):
('api_key', models.CharField(blank=True, max_length=255, null=True)), ('api_key', models.CharField(blank=True, max_length=255, null=True)),
('actor', models.CharField(max_length=255)), ('actor', models.CharField(max_length=255)),
('inbox', models.CharField(max_length=255)), ('inbox', models.CharField(max_length=255)),
('shared_inbox', models.CharField(max_length=255)),
('outbox', models.CharField(max_length=255)), ('outbox', models.CharField(max_length=255)),
('summary', models.TextField(blank=True, null=True)), ('summary', models.TextField(blank=True, null=True)),
('local', models.BooleanField(default=True)), ('local', models.BooleanField(default=True)),

View file

@ -15,6 +15,7 @@ class User(AbstractUser):
api_key = models.CharField(max_length=255, blank=True, null=True) api_key = models.CharField(max_length=255, blank=True, null=True)
actor = models.CharField(max_length=255) actor = models.CharField(max_length=255)
inbox = models.CharField(max_length=255) inbox = models.CharField(max_length=255)
shared_inbox = models.CharField(max_length=255)
outbox = models.CharField(max_length=255) outbox = models.CharField(max_length=255)
summary = models.TextField(blank=True, null=True) summary = models.TextField(blank=True, null=True)
local = models.BooleanField(default=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.username = '%s@%s' % (instance.username, DOMAIN)
instance.actor = 'https://%s/user/%s' % (DOMAIN, instance.localname) instance.actor = 'https://%s/user/%s' % (DOMAIN, instance.localname)
instance.inbox = 'https://%s/user/%s/inbox' % (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) instance.outbox = 'https://%s/user/%s/outbox' % (DOMAIN, instance.localname)
if not instance.private_key: if not instance.private_key:
random_generator = Random.new().read random_generator = Random.new().read

View file

@ -14,8 +14,30 @@ import requests
from uuid import uuid4 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): def handle_account_search(query):
''' webfingerin' other servers ''' ''' webfingerin' other servers '''
user = None
domain = query.split('@')[1] domain = query.split('@')[1]
try: try:
user = models.User.objects.get(username=query) user = models.User.objects.get(username=query)
@ -56,13 +78,18 @@ def handle_response(response):
if activity['type'] == 'Accept': if activity['type'] == 'Accept':
handle_incoming_accept(activity) handle_incoming_accept(activity)
def handle_incoming_accept(activity): def handle_incoming_accept(activity):
''' someone is accepting a follow request ''' ''' someone is accepting a follow request '''
# not actually a remote user so this is kinda janky # our local user
user = get_or_create_remote_user(activity['actor']) user = models.User.objects.get(actor=activity['actor'])
# the person our local user wants to follow, who said yes # 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) followed.followers.add(user)
# save the activity record
models.FollowActivity( models.FollowActivity(
uuid=activity['id'], uuid=activity['id'],
user=user, user=user,
@ -72,7 +99,7 @@ def handle_incoming_accept(activity):
def handle_shelve(user, book, shelf): def handle_shelve(user, book, shelf):
''' gettin organized ''' ''' a local user is getting a book put on their shelf '''
# update the database # update the database
models.ShelfBook(book=book, shelf=shelf, added_by=user).save() models.ShelfBook(book=book, shelf=shelf, added_by=user).save()
@ -101,8 +128,7 @@ def handle_shelve(user, book, shelf):
'id': shelf.activitypub_id 'id': shelf.activitypub_id
} }
} }
# TODO: this should be getting shared inboxes and deduplicating recipients = get_recipients(user, 'public')
recipients = [u.inbox for u in user.followers.all()]
models.ShelveActivity( models.ShelveActivity(
uuid=uuid, uuid=uuid,
@ -116,6 +142,26 @@ def handle_shelve(user, book, shelf):
broadcast(user, activity, recipients) 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): def handle_review(user, book, name, content, rating):
''' post a review ''' ''' post a review '''
review_uuid = uuid4() review_uuid = uuid4()
@ -130,7 +176,7 @@ def handle_review(user, book, name, content, rating):
'rating': rating, # fedireads-only custom field 'rating': rating, # fedireads-only custom field
'to': 'https://www.w3.org/ns/activitystreams#Public' '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() create_uuid = uuid4()
activity = { activity = {
'@context': 'https://www.w3.org/ns/activitystreams', '@context': 'https://www.w3.org/ns/activitystreams',
@ -160,25 +206,6 @@ def handle_review(user, book, name, content, rating):
broadcast(user, activity, recipients) 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): def broadcast(sender, action, recipients):

View file

@ -22,6 +22,7 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# federation endpoints # federation endpoints
path('/inbox', incoming.shared_inbox),
path('user/<str:username>.json', incoming.get_actor), path('user/<str:username>.json', incoming.get_actor),
path('user/<str:username>/inbox', incoming.inbox), path('user/<str:username>/inbox', incoming.inbox),
path('user/<str:username>/outbox', outgoing.outbox), path('user/<str:username>/outbox', outgoing.outbox),