Adds actor

This commit is contained in:
Mouse Reeve 2020-01-26 12:14:27 -08:00
parent e357f4a7a6
commit e30e06c283
10 changed files with 225 additions and 22 deletions

View file

@ -1,8 +1,10 @@
''' activitystream api ''' ''' activitystream api '''
from django.http import HttpResponseBadRequest, HttpResponseNotFound, JsonResponse from django.http import HttpResponse, HttpResponseBadRequest, \
HttpResponseNotFound, JsonResponse
from fedireads.settings import DOMAIN from fedireads.settings import DOMAIN
from fedireads.models import User from fedireads.models import User
def webfinger(request): def webfinger(request):
''' allow other servers to ask about a user ''' ''' allow other servers to ask about a user '''
resource = request.GET.get('resource') resource = request.GET.get('resource')
@ -15,7 +17,9 @@ def webfinger(request):
return HttpResponseNotFound('No account found') return HttpResponseNotFound('No account found')
return JsonResponse(format_webfinger(user)) return JsonResponse(format_webfinger(user))
def format_webfinger(user): def format_webfinger(user):
''' helper function to create structured webfinger json '''
return { return {
'subject': 'acct:%s@%s' % (user.username, DOMAIN), 'subject': 'acct:%s@%s' % (user.username, DOMAIN),
'links': [ '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

View file

@ -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 from django.conf import settings
import django.contrib.auth.models 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_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')), ('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')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('private_key', models.CharField(max_length=255)), ('private_key', models.CharField(max_length=1024)),
('public_key', models.CharField(max_length=255)), ('public_key', models.CharField(max_length=1024)),
('api_key', models.CharField(blank=True, max_length=255, null=True)), ('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)), ('created_date', models.DateTimeField(auto_now_add=True)),
('updated_date', models.DateTimeField(auto_now=True)), ('updated_date', models.DateTimeField(auto_now=True)),
('followers', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), ('followers', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
@ -50,6 +52,16 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()), ('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( migrations.CreateModel(
name='Author', name='Author',
fields=[ fields=[
@ -129,6 +141,11 @@ class Migration(migrations.Migration):
name='user', name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), 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( migrations.AddField(
model_name='book', model_name='book',
name='works', name='works',

View file

@ -5,13 +5,15 @@ from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from Crypto.PublicKey import RSA from Crypto.PublicKey import RSA
from Crypto import Random from Crypto import Random
from datetime import datetime from fedireads.settings import DOMAIN
class User(AbstractUser): class User(AbstractUser):
''' a user who wants to read books ''' ''' a user who wants to read books '''
private_key = models.CharField(max_length=255) private_key = models.CharField(max_length=1024)
public_key = models.CharField(max_length=255) public_key = models.CharField(max_length=1024)
api_key = models.CharField(max_length=255, blank=True, null=True) 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) created_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
followers = models.ManyToManyField('self', symmetrical=False) followers = models.ManyToManyField('self', symmetrical=False)
@ -21,16 +23,36 @@ class User(AbstractUser):
if not self.private_key: if not self.private_key:
random_generator = Random.new().read random_generator = Random.new().read
key = RSA.generate(1024, random_generator) key = RSA.generate(1024, random_generator)
self.private_key = key self.private_key = key.export_key()
self.public_key = key.publickey() self.public_key = key.publickey().export_key()
if not self.id:
self.created_date = datetime.now() if self.local and not self.actor:
self.updated_date = datetime.now() 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) super().save(*args, **kwargs)
@receiver(models.signals.post_save, sender=User) @receiver(models.signals.post_save, sender=User)
def execute_after_save(sender, instance, created, *args, **kwargs): 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: if not created:
return return
shelves = [{ shelves = [{
@ -45,7 +67,12 @@ def execute_after_save(sender, instance, created, *args, **kwargs):
}] }]
for shelf in shelves: 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): class Message(models.Model):
@ -65,6 +92,13 @@ class Review(Message):
star_rating = models.IntegerField(default=0) 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): class Shelf(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
user = models.ForeignKey('User', on_delete=models.PROTECT) 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 # many to many join table for books and shelves
book = models.ForeignKey('Book', on_delete=models.PROTECT) book = models.ForeignKey('Book', on_delete=models.PROTECT)
shelf = models.ForeignKey('Shelf', 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) added_date = models.DateTimeField(auto_now_add=True)
@ -94,11 +133,23 @@ class Book(models.Model):
data = JSONField() data = JSONField()
works = models.ManyToManyField('Work') works = models.ManyToManyField('Work')
authors = models.ManyToManyField('Author') 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) added_date = models.DateTimeField(auto_now_add=True)
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
class Work(models.Model): class Work(models.Model):
''' encompassses all editions of a book '''
openlibary_key = models.CharField(max_length=255) openlibary_key = models.CharField(max_length=255)
data = JSONField() data = JSONField()
added_date = models.DateTimeField(auto_now_add=True) added_date = models.DateTimeField(auto_now_add=True)

View file

@ -18,6 +18,8 @@ def get_book(request, olkey):
book = Book(openlibary_key=olkey) book = Book(openlibary_key=olkey)
data = response.json() data = response.json()
book.data = data book.data = data
if request and request.user and request.user.is_authenticated:
book.added_by = request.user
book.save() book.save()
for work_id in data['works']: for work_id in data['works']:
work_id = work_id['key'] work_id = work_id['key']

View file

@ -87,7 +87,7 @@ DATABASES = {
} }
} }
LOGIN_URL = 'login/' LOGIN_URL = '/login/'
AUTH_USER_MODEL = 'fedireads.User' AUTH_USER_MODEL = 'fedireads.User'
# Password validation # Password validation

View file

@ -21,12 +21,12 @@
<div id="top-bar"> <div id="top-bar">
<header> <header>
<div id="branding">📚FediReads</div> <div id="branding"><a href="/">📚FediReads</a></div>
<div> <div>
<div id="account"> <div id="account">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<form name="logout" action="/logout/" method="post"> <form name="logout" action="/logout/" method="post">
Welcome, {{ user.username }} Welcome, {{ request.user.username }}
<input type="submit" value="Log out"></input> <input type="submit" value="Log out"></input>
</form> </form>
{% else %} {% else %}

View file

@ -0,0 +1,29 @@
{% extends 'layout.html' %}
{% block content %}
<div id="main">
<div class="user-profile">
<img class="user-pic" src="/static/images/profile.jpg">
<h2>{{ user.username }}</h2>
<p>Since {{ user.created_date }}</p>
{% if not is_self %}
{% if not following %}
<form action="/follow/" method="post">
<input type="hidden" name="user" value="{{ user.id }}"></input>
<input type="submit" value="Follow"></input>
</form>
{% else %}
<form action="/unfollow/" method="post">
<input type="hidden" name="user" value="{{ user.id }}"></input>
<input type="submit" value="Unfollow"></input>
</form>
{% endif %}
{% endif %}
</div>
{% for book in books.all %}
<div class="book">
{{ book.data.title }} by {{ book.authors.first.data.name }}
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -15,13 +15,18 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from fedireads import activitystream, openlibrary, views from fedireads import federation, openlibrary, views
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('', views.home), path('', views.home),
path('login/', views.user_login), path('login/', views.user_login),
path('logout/', views.user_logout), path('logout/', views.user_logout),
path('user/<str:username>', views.user_profile),
path('follow/', views.follow),
path('unfollow/', views.unfollow),
path('api/book/<str:olkey>', openlibrary.get_book), path('api/book/<str:olkey>', openlibrary.get_book),
path('.well-known/webfinger', activitystream.webfinger), path('api/<str:username>/inbox', federation.inbox),
path('api/<str:username>/outbox', federation.outbox),
path('.well-known/webfinger', federation.webfinger),
] ]

View file

@ -4,12 +4,12 @@ from django.contrib.auth import authenticate, login, logout
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from fedireads.models import Shelf from fedireads import models
@login_required @login_required
def home(request): def home(request):
''' user feed ''' ''' user feed '''
shelves = Shelf.objects.filter(user=request.user.id) shelves = models.Shelf.objects.filter(user=request.user.id)
data = { data = {
'user': request.user, 'user': request.user,
'shelves': shelves, 'shelves': shelves,
@ -35,5 +35,60 @@ def user_login(request):
@csrf_exempt @csrf_exempt
@login_required @login_required
def user_logout(request): def user_logout(request):
''' done with this place! outa here! '''
logout(request) logout(request)
return redirect('/') return redirect('/')
@login_required
def user_profile(request, username):
''' profile page for a user '''
user = models.User.objects.get(username=username)
books = models.Book.objects.filter(shelves__user=user)
following = user.followers.filter(id=request.user.id).count() > 0
data = {
'user': user,
'books': books,
'is_self': request.user.id == user.id,
'following': following,
}
return TemplateResponse(request, 'user.html', data)
@csrf_exempt
@login_required
def follow(request):
followed = request.POST.get('user')
followed = models.User.objects.get(id=followed)
followed.followers.add(request.user)
activity = {
'@context': 'https://www.w3.org/ns/activitystreams',
'summary': '',
'type': 'Follow',
'actor': {
'type': 'Person',
'name': request.user.get_actor(),
},
'object': {
'type': 'Person',
'name': followed.get_actor(),
}
}
models.Activity(
data=activity,
user=request.user,
)
return redirect('/user/%s' % followed.username)
@csrf_exempt
@login_required
def unfollow(request):
followed = request.POST.get('user')
followed = models.User.objects.get(id=followed)
followed.followers.remove(request.user)
return redirect('/user/%s' % followed.username)

15
rebuilddb.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/bash
rm fedireads/migrations/0*
set -e
dropdb fedireads
createdb fedireads
python manage.py makemigrations fedireads
python manage.py migrate
echo "from fedireads.models import User
User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')" | python manage.py shell
echo "from fedireads.models import User
User.objects.create_user('rat', 'rat@rat.com', 'ratword')" | python manage.py shell
echo "from fedireads.openlibrary import get_book
get_book(None, 'OL13549170M')
get_book(None, 'OL24738110M')" | python manage.py shell