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 '''
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

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
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',

View file

@ -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)

View file

@ -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']

View file

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

View file

@ -21,12 +21,12 @@
<div id="top-bar">
<header>
<div id="branding">📚FediReads</div>
<div id="branding"><a href="/">📚FediReads</a></div>
<div>
<div id="account">
{% if user.is_authenticated %}
<form name="logout" action="/logout/" method="post">
Welcome, {{ user.username }}
Welcome, {{ request.user.username }}
<input type="submit" value="Log out"></input>
</form>
{% 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.urls import path
from fedireads import activitystream, openlibrary, views
from fedireads import federation, openlibrary, views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.home),
path('login/', views.user_login),
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('.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.template.response import TemplateResponse
from django.views.decorators.csrf import csrf_exempt
from fedireads.models import Shelf
from fedireads import models
@login_required
def home(request):
''' user feed '''
shelves = Shelf.objects.filter(user=request.user.id)
shelves = models.Shelf.objects.filter(user=request.user.id)
data = {
'user': request.user,
'shelves': shelves,
@ -35,5 +35,60 @@ def user_login(request):
@csrf_exempt
@login_required
def user_logout(request):
''' done with this place! outa here! '''
logout(request)
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