diff --git a/fedireads/activitystream.py b/fedireads/federation.py similarity index 55% rename from fedireads/activitystream.py rename to fedireads/federation.py index 99ce4eeb..9ab1eec5 100644 --- a/fedireads/activitystream.py +++ b/fedireads/federation.py @@ -1,8 +1,10 @@ ''' activitystream api ''' -from django.http import HttpResponseBadRequest, HttpResponseNotFound, JsonResponse +from django.http import HttpResponse, HttpResponseBadRequest, \ + HttpResponseNotFound, JsonResponse from fedireads.settings import DOMAIN from fedireads.models import User + def webfinger(request): ''' allow other servers to ask about a user ''' resource = request.GET.get('resource') @@ -15,7 +17,9 @@ def webfinger(request): return HttpResponseNotFound('No account found') return JsonResponse(format_webfinger(user)) + def format_webfinger(user): + ''' helper function to create structured webfinger json ''' return { 'subject': 'acct:%s@%s' % (user.username, DOMAIN), 'links': [ @@ -26,3 +30,28 @@ def format_webfinger(user): } ] } + +def inbox(request, username): + ''' incoming activitypub events ''' + # TODO RSA junk: signature = request.headers['Signature'] + user = User.objects.get(username=username) + + +def outbox(request, username): + user = User.objects.get(username=username) + if request.method == 'GET': + # list of activities + return JsonResponse() + + data = request.body.decode('utf-8') + if data.activity.type == 'Follow': + handle_follow(data) + return HttpResponse() + +def handle_follow(data): + pass + +def get_or_create_remote_user(activity): + pass + + diff --git a/fedireads/migrations/0001_initial.py b/fedireads/migrations/0001_initial.py index fcc00f97..3d246dfe 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-25 23:55 +# Generated by Django 2.0.13 on 2020-01-26 20:12 from django.conf import settings import django.contrib.auth.models @@ -32,9 +32,11 @@ class Migration(migrations.Migration): ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('private_key', models.CharField(max_length=255)), - ('public_key', models.CharField(max_length=255)), + ('private_key', models.CharField(max_length=1024)), + ('public_key', models.CharField(max_length=1024)), ('api_key', models.CharField(blank=True, max_length=255, null=True)), + ('actor', django.contrib.postgres.fields.jsonb.JSONField()), + ('local', models.BooleanField(default=True)), ('created_date', models.DateTimeField(auto_now_add=True)), ('updated_date', models.DateTimeField(auto_now=True)), ('followers', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), @@ -50,6 +52,16 @@ class Migration(migrations.Migration): ('objects', django.contrib.auth.models.UserManager()), ], ), + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', django.contrib.postgres.fields.jsonb.JSONField()), + ('remote', models.BooleanField(default=False)), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), migrations.CreateModel( name='Author', fields=[ @@ -129,6 +141,11 @@ class Migration(migrations.Migration): name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), ), + migrations.AddField( + model_name='book', + name='shelves', + field=models.ManyToManyField(through='fedireads.ShelfBook', to='fedireads.Shelf'), + ), migrations.AddField( model_name='book', name='works', diff --git a/fedireads/models.py b/fedireads/models.py index b32a0621..8692ca48 100644 --- a/fedireads/models.py +++ b/fedireads/models.py @@ -5,13 +5,15 @@ from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import JSONField from Crypto.PublicKey import RSA from Crypto import Random -from datetime import datetime +from fedireads.settings import DOMAIN class User(AbstractUser): ''' a user who wants to read books ''' - private_key = models.CharField(max_length=255) - public_key = models.CharField(max_length=255) + private_key = models.CharField(max_length=1024) + public_key = models.CharField(max_length=1024) api_key = models.CharField(max_length=255, blank=True, null=True) + actor = JSONField() + local = models.BooleanField(default=True) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) followers = models.ManyToManyField('self', symmetrical=False) @@ -21,16 +23,36 @@ class User(AbstractUser): if not self.private_key: random_generator = Random.new().read key = RSA.generate(1024, random_generator) - self.private_key = key - self.public_key = key.publickey() - if not self.id: - self.created_date = datetime.now() - self.updated_date = datetime.now() + self.private_key = key.export_key() + self.public_key = key.publickey().export_key() + + if self.local and not self.actor: + self.actor = { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1' + ], + + 'id': 'https://%s/u/%s' % (DOMAIN, self.username), + 'type': 'Person', + 'preferredUsername': self.username, + 'inbox': 'https://%s/api/inbox' % DOMAIN, + 'followers': 'https://%s/u/%s/followers' % \ + (DOMAIN, self.username), + 'publicKey': { + 'id': 'https://%s/u/%s#main-key' % (DOMAIN, self.username), + 'owner': 'https://%s/u/%s' % (DOMAIN, self.username), + 'publicKeyPem': self.public_key.decode('utf8'), + } + } super().save(*args, **kwargs) + @receiver(models.signals.post_save, sender=User) def execute_after_save(sender, instance, created, *args, **kwargs): + ''' create shelves for new users ''' + # TODO: how are remote users handled? what if they aren't readers? if not created: return shelves = [{ @@ -45,7 +67,12 @@ def execute_after_save(sender, instance, created, *args, **kwargs): }] for shelf in shelves: - Shelf(name=shelf['name'], shelf_type=shelf['type'], user=instance, editable=False).save() + Shelf( + name=shelf['name'], + shelf_type=shelf['type'], + user=instance, + editable=False + ).save() class Message(models.Model): @@ -65,6 +92,13 @@ class Review(Message): star_rating = models.IntegerField(default=0) +class Activity(models.Model): + data = JSONField() + user = models.ForeignKey('User', on_delete=models.PROTECT) + remote = models.BooleanField(default=False) + created_date = models.DateTimeField(auto_now_add=True) + + class Shelf(models.Model): name = models.CharField(max_length=100) user = models.ForeignKey('User', on_delete=models.PROTECT) @@ -84,7 +118,12 @@ class ShelfBook(models.Model): # many to many join table for books and shelves book = models.ForeignKey('Book', on_delete=models.PROTECT) shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT) - added_by = models.ForeignKey('User', blank=True, null=True, on_delete=models.PROTECT) + added_by = models.ForeignKey( + 'User', + blank=True, + null=True, + on_delete=models.PROTECT + ) added_date = models.DateTimeField(auto_now_add=True) @@ -94,11 +133,23 @@ class Book(models.Model): data = JSONField() works = models.ManyToManyField('Work') authors = models.ManyToManyField('Author') - added_by = models.ForeignKey('User', on_delete=models.PROTECT, blank=True, null=True) + shelves = models.ManyToManyField( + 'Shelf', + symmetrical=False, + through='ShelfBook', + through_fields=('book', 'shelf') + ) + added_by = models.ForeignKey( + 'User', + blank=True, + null=True, + on_delete=models.PROTECT + ) added_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) class Work(models.Model): + ''' encompassses all editions of a book ''' openlibary_key = models.CharField(max_length=255) data = JSONField() added_date = models.DateTimeField(auto_now_add=True) diff --git a/fedireads/openlibrary.py b/fedireads/openlibrary.py index 66fc20bb..c60a63ee 100644 --- a/fedireads/openlibrary.py +++ b/fedireads/openlibrary.py @@ -18,6 +18,8 @@ def get_book(request, olkey): book = Book(openlibary_key=olkey) data = response.json() book.data = data + if request and request.user and request.user.is_authenticated: + book.added_by = request.user book.save() for work_id in data['works']: work_id = work_id['key'] diff --git a/fedireads/settings.py b/fedireads/settings.py index 1359408c..98b4cb2a 100644 --- a/fedireads/settings.py +++ b/fedireads/settings.py @@ -87,7 +87,7 @@ DATABASES = { } } -LOGIN_URL = 'login/' +LOGIN_URL = '/login/' AUTH_USER_MODEL = 'fedireads.User' # Password validation diff --git a/fedireads/templates/layout.html b/fedireads/templates/layout.html index c1329102..0bc4d3a7 100644 --- a/fedireads/templates/layout.html +++ b/fedireads/templates/layout.html @@ -21,12 +21,12 @@