Send messages

This commit is contained in:
Mouse Reeve 2020-01-26 20:57:48 -08:00
parent b9d933e3b1
commit dd554ca6ca
6 changed files with 106 additions and 57 deletions

View file

@ -1,9 +1,20 @@
''' generates activitypub formatted objects ''' ''' generates activitypub formatted objects '''
from uuid import uuid4 from uuid import uuid4
from fedireads.settings import DOMAIN 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. ''' a user puts a book on a shelf.
activitypub action type Add activitypub action type Add
https://www.w3.org/ns/activitystreams#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): def follow_request(user, follow):
''' ask to be friends ''' ''' ask to be friends '''
return { return {
@ -64,12 +106,11 @@ def actor(user):
'id': user.actor, 'id': user.actor,
'type': 'Person', 'type': 'Person',
'preferredUsername': user.username, 'preferredUsername': user.username,
'inbox': 'https://%s/api/%s/inbox' % (DOMAIN, user.username), 'inbox': inbox(user),
'followers': 'https://%s/api/u/%s/followers' % \ 'followers': '%s/followers' % user.actor,
(DOMAIN, user.username),
'publicKey': { 'publicKey': {
'id': 'https://%s/api/u/%s#main-key' % (DOMAIN, user.username), 'id': '%s/#main-key' % user.actor,
'owner': 'https://%s/api/u/%s' % (DOMAIN, user.username), 'owner': user.actor,
'publicKeyPem': user.public_key, 'publicKeyPem': user.public_key,
} }
} }
@ -77,4 +118,4 @@ def actor(user):
def inbox(user): def inbox(user):
''' describe an inbox ''' ''' describe an inbox '''
return 'https://%s/api/%s/inbox' % (DOMAIN, user.username) return '%s/inbox' % (user.actor)

View file

@ -41,7 +41,7 @@ def format_webfinger(user):
@csrf_exempt @csrf_exempt
def actor(request, username): def get_actor(request, username):
''' return an activitypub actor object ''' ''' return an activitypub actor object '''
user = models.User.objects.get(username=username) user = models.User.objects.get(username=username)
return JsonResponse(templates.actor(user)) return JsonResponse(templates.actor(user))
@ -52,18 +52,23 @@ def inbox(request, username):
''' incoming activitypub events ''' ''' incoming activitypub events '''
if request.method == 'GET': if request.method == 'GET':
# return a collection of something? # 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': if activity['type'] == 'Add':
handle_add(activity) handle_add(activity)
if activity['type'] == 'Follow': if activity['type'] == 'Follow':
response = handle_follow(activity) response = handle_follow(activity)
return JsonResponse(response) return JsonResponse(response)
return HttpResponse() return HttpResponse()
def handle_add(activity): def handle_add(activity):
''' adding a book to a shelf ''' ''' adding a book to a shelf '''
book_id = activity['object']['url'] book_id = activity['object']['url']
@ -100,10 +105,12 @@ def handle_follow(activity):
@csrf_exempt @csrf_exempt
def outbox(request, username): def outbox(request, username):
''' outbox for the requested user '''
user = models.User.objects.get(username=username) user = models.User.objects.get(username=username)
size = models.Message.objects.filter(user=user).count()
if request.method == 'GET': if request.method == 'GET':
# list of activities # list of activities
return JsonResponse() return JsonResponse(templates.outbox_collection(user, size))
data = request.body.decode('utf-8') data = request.body.decode('utf-8')
if data.activity.type == 'Follow': if data.activity.type == 'Follow':
@ -111,42 +118,24 @@ def outbox(request, username):
return HttpResponse() return HttpResponse()
def broadcast_action(sender, action, recipients): def broadcast_activity(sender, obj, recipients):
''' sign and send out the actions ''' ''' sign and send out the actions '''
#models.Message( activity = templates.create_activity(sender, obj)
# author=sender,
# content=action # store message in database
#).save() models.Message(user=sender, content=activity).save()
for recipient in recipients: for recipient in recipients:
action['to'] = 'https://www.w3.org/ns/activitystreams#Public' broadcast(sender, activity, recipient)
action['cc'] = [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): def broadcast_follow(sender, action, destination):
''' send a follow request ''' ''' 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() now = datetime.utcnow().isoformat()
message_to_sign = '''(request-target): post %s message_to_sign = '''(request-target): post %s
host: https://%s host: https://%s
@ -167,17 +156,22 @@ date: %s''' % (inbox_fragment, DOMAIN, now)
}, },
) )
if not response.ok: if not response.ok:
response.raise_for_status() response.raise_for_status()
def get_or_create_remote_user(activity): def get_or_create_remote_user(activity):
''' wow, a foreigner '''
actor = activity['actor'] actor = activity['actor']
try: try:
user = models.User.objects.get(actor=actor) user = models.User.objects.get(actor=actor)
except models.User.DoesNotExist: except models.User.DoesNotExist:
# TODO: how do you actually correctly learn this? # TODO: how do you actually correctly learn this?
username = '%s@%s' % (actor.split('/')[-1], actor.split('/')[2]) 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 return user

View file

@ -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 from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
@ -76,6 +76,16 @@ class Migration(migrations.Migration):
('authors', models.ManyToManyField(to='fedireads.Author')), ('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( migrations.CreateModel(
name='Shelf', name='Shelf',
fields=[ fields=[

View file

@ -64,14 +64,11 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
class Message(models.Model): class Message(models.Model):
''' any kind of user post, incl. reviews, replies, and status updates ''' ''' 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) content = JSONField(max_length=5000)
created_date = models.DateTimeField(auto_now_add=True) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Shelf(models.Model): class Shelf(models.Model):
activitypub_id = models.CharField(max_length=255) activitypub_id = models.CharField(max_length=255)

View file

@ -26,8 +26,8 @@ urlpatterns = [
path('shelve/<str:shelf_id>/<int:book_id>', views.shelve), path('shelve/<str:shelf_id>/<int:book_id>', views.shelve),
path('follow/', views.follow), path('follow/', views.follow),
path('unfollow/', views.unfollow), path('unfollow/', views.unfollow),
path('api/u/<str:username>', federation.actor), path('api/u/<str:username>', federation.get_actor),
path('api/<str:username>/inbox', federation.inbox), path('api/u/<str:username>/inbox', federation.inbox),
path('api/<str:username>/outbox', federation.outbox), path('api/u/<str:username>/outbox', federation.outbox),
path('.well-known/webfinger', federation.webfinger), path('.well-known/webfinger', federation.webfinger),
] ]

View file

@ -7,7 +7,7 @@ from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from fedireads import models from fedireads import models
import fedireads.activitypub_templates as templates import fedireads.activitypub_templates as templates
from fedireads.federation import broadcast_action, broadcast_follow from fedireads.federation import broadcast_activity, broadcast_follow
@login_required @login_required
def home(request): def home(request):
@ -73,12 +73,19 @@ def shelve(request, shelf_id, book_id):
shelf = models.Shelf.objects.get(identifier=shelf_id) shelf = models.Shelf.objects.get(identifier=shelf_id)
# update the database # 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 # 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()] 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('/') return redirect('/')