Adds actor
This commit is contained in:
parent
e357f4a7a6
commit
e30e06c283
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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 %}
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue