Send messages
This commit is contained in:
parent
b9d933e3b1
commit
dd554ca6ca
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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=[
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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('/')
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue