mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-28 20:41:46 +00:00
adding federation
This commit is contained in:
parent
e30e06c283
commit
6b85d8838f
11 changed files with 244 additions and 83 deletions
29
fedireads/activitypub_templates.py
Normal file
29
fedireads/activitypub_templates.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
''' generates activitypub formatted objects '''
|
||||||
|
|
||||||
|
def shelve_action(user, book, shelf):
|
||||||
|
''' a user puts a book on a shelf.
|
||||||
|
activitypub action type Add
|
||||||
|
https://www.w3.org/ns/activitystreams#Add '''
|
||||||
|
book_title = book.data['title']
|
||||||
|
summary = '%s added %s to %s' % (
|
||||||
|
user.username,
|
||||||
|
book_title,
|
||||||
|
shelf.name
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
'summary': summary,
|
||||||
|
'type': 'Add',
|
||||||
|
'actor': user.activitypub_id,
|
||||||
|
'object': {
|
||||||
|
'type': 'Document',
|
||||||
|
'name': book_title,
|
||||||
|
'url': book.openlibary_key
|
||||||
|
},
|
||||||
|
'target': {
|
||||||
|
'type': 'Collection',
|
||||||
|
'name': shelf.name,
|
||||||
|
'id': shelf.activitypub_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
''' activitystream api '''
|
''' activitystream api '''
|
||||||
|
from base64 import b64encode
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
from Crypto.Signature import pkcs1_15
|
||||||
|
from Crypto.Hash import SHA256
|
||||||
|
from datetime import datetime
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, \
|
from django.http import HttpResponse, HttpResponseBadRequest, \
|
||||||
HttpResponseNotFound, JsonResponse
|
HttpResponseNotFound, JsonResponse
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from fedireads.settings import DOMAIN
|
from fedireads.settings import DOMAIN
|
||||||
from fedireads.models import User
|
from fedireads import models
|
||||||
|
from fedireads import openlibrary
|
||||||
|
import requests
|
||||||
|
|
||||||
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')
|
||||||
if not resource and not resource.startswith('acct:'):
|
if not resource and not resource.startswith('acct:'):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
account = resource.replace('acct:', '')
|
ap_id = resource.replace('acct:', '')
|
||||||
account = account.replace('@' + DOMAIN, '')
|
user = models.User.objects.filter(activitypub_id=ap_id).first()
|
||||||
user = User.objects.filter(username=account).first()
|
|
||||||
if not user:
|
if not user:
|
||||||
return HttpResponseNotFound('No account found')
|
return HttpResponseNotFound('No account found')
|
||||||
return JsonResponse(format_webfinger(user))
|
return JsonResponse(format_webfinger(user))
|
||||||
|
@ -21,24 +27,52 @@ def webfinger(request):
|
||||||
def format_webfinger(user):
|
def format_webfinger(user):
|
||||||
''' helper function to create structured webfinger json '''
|
''' helper function to create structured webfinger json '''
|
||||||
return {
|
return {
|
||||||
'subject': 'acct:%s@%s' % (user.username, DOMAIN),
|
'subject': 'acct:%s' % (user.activitypub_id),
|
||||||
'links': [
|
'links': [
|
||||||
{
|
{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'type': 'application/activity+json',
|
'type': 'application/activity+json',
|
||||||
'href': 'https://%s/user/%s' % (DOMAIN, user.username),
|
'href': user.actor['id']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
def actor(request, username):
|
||||||
|
''' return an activitypub actor object '''
|
||||||
|
user = models.User.objects.get(username=username)
|
||||||
|
return JsonResponse(user.actor)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
def inbox(request, username):
|
def inbox(request, username):
|
||||||
''' incoming activitypub events '''
|
''' incoming activitypub events '''
|
||||||
# TODO RSA junk: signature = request.headers['Signature']
|
if request.method == 'GET':
|
||||||
user = User.objects.get(username=username)
|
# return a collection of something?
|
||||||
|
pass
|
||||||
|
|
||||||
|
activity = request.POST.dict()
|
||||||
|
if activity['type'] == 'Add':
|
||||||
|
handle_add(activity)
|
||||||
|
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
|
def handle_add(activity):
|
||||||
|
book_id = activity['object']['url']
|
||||||
|
book = openlibrary.get_or_create_book(book_id)
|
||||||
|
user_ap_id = activity['actor'].replace('https//:', '')
|
||||||
|
user = models.User.objects.get(activitypub_id=user_ap_id)
|
||||||
|
shelf = models.Shelf.objects.get(activitypub_id=activity['target']['id'])
|
||||||
|
models.ShelfBook(
|
||||||
|
shelf=shelf,
|
||||||
|
book=book,
|
||||||
|
added_by=user,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
def outbox(request, username):
|
def outbox(request, username):
|
||||||
user = User.objects.get(username=username)
|
user = models.User.objects.get(username=username)
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
# list of activities
|
# list of activities
|
||||||
return JsonResponse()
|
return JsonResponse()
|
||||||
|
@ -48,6 +82,41 @@ def outbox(request, username):
|
||||||
handle_follow(data)
|
handle_follow(data)
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
|
def broadcast_action(sender, action, recipients):
|
||||||
|
''' sign and send out the actions '''
|
||||||
|
#models.Message(
|
||||||
|
# author=sender,
|
||||||
|
# content=action
|
||||||
|
#).save()
|
||||||
|
for recipient in recipients:
|
||||||
|
action['to'] = 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
action['cc'] = [recipient]
|
||||||
|
|
||||||
|
inbox_fragment = sender.actor['inbox'].replace('https://' + DOMAIN, '')
|
||||||
|
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.activitypub_id
|
||||||
|
signature += 'headers="(request-target) host date",'
|
||||||
|
signature += 'signature="%s"' % b64encode(signed_message)
|
||||||
|
response = requests.post(
|
||||||
|
recipient,
|
||||||
|
data=action,
|
||||||
|
headers={
|
||||||
|
'Date': now,
|
||||||
|
'Signature': signature,
|
||||||
|
'Host': DOMAIN,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not response.ok:
|
||||||
|
return response.raise_for_status()
|
||||||
|
|
||||||
def handle_follow(data):
|
def handle_follow(data):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 2.0.13 on 2020-01-26 20:12
|
# Generated by Django 2.0.13 on 2020-01-27 01:47
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
import django.contrib.auth.models
|
import django.contrib.auth.models
|
||||||
|
@ -32,8 +32,9 @@ 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=1024)),
|
('activitypub_id', models.CharField(max_length=255)),
|
||||||
('public_key', models.CharField(max_length=1024)),
|
('private_key', models.TextField(blank=True, null=True)),
|
||||||
|
('public_key', models.TextField()),
|
||||||
('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()),
|
('actor', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
('local', models.BooleanField(default=True)),
|
('local', models.BooleanField(default=True)),
|
||||||
|
@ -52,16 +53,6 @@ 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=[
|
||||||
|
@ -76,6 +67,7 @@ class Migration(migrations.Migration):
|
||||||
name='Book',
|
name='Book',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('activitypub_id', models.CharField(max_length=255)),
|
||||||
('openlibary_key', models.CharField(max_length=255)),
|
('openlibary_key', models.CharField(max_length=255)),
|
||||||
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
('data', django.contrib.postgres.fields.jsonb.JSONField()),
|
||||||
('added_date', models.DateTimeField(auto_now_add=True)),
|
('added_date', models.DateTimeField(auto_now_add=True)),
|
||||||
|
@ -84,26 +76,12 @@ class Migration(migrations.Migration):
|
||||||
('authors', models.ManyToManyField(to='fedireads.Author')),
|
('authors', models.ManyToManyField(to='fedireads.Author')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name='Review',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('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)),
|
|
||||||
('star_rating', models.IntegerField(default=0)),
|
|
||||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
|
||||||
('book', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='fedireads.Book')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Shelf',
|
name='Shelf',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('activitypub_id', models.CharField(max_length=255)),
|
||||||
|
('identifier', models.CharField(max_length=255)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
('editable', models.BooleanField(default=True)),
|
('editable', models.BooleanField(default=True)),
|
||||||
('shelf_type', models.CharField(default='custom', max_length=100)),
|
('shelf_type', models.CharField(default='custom', max_length=100)),
|
||||||
|
@ -151,4 +129,12 @@ class Migration(migrations.Migration):
|
||||||
name='works',
|
name='works',
|
||||||
field=models.ManyToManyField(to='fedireads.Work'),
|
field=models.ManyToManyField(to='fedireads.Work'),
|
||||||
),
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='shelfbook',
|
||||||
|
unique_together={('book', 'shelf')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='shelf',
|
||||||
|
unique_together={('user', 'name')},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,12 +5,14 @@ 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 fedireads.settings import DOMAIN
|
from fedireads.settings import DOMAIN, OL_URL
|
||||||
|
import re
|
||||||
|
|
||||||
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=1024)
|
activitypub_id = models.CharField(max_length=255)
|
||||||
public_key = models.CharField(max_length=1024)
|
private_key = models.TextField(blank=True, null=True)
|
||||||
|
public_key = models.TextField()
|
||||||
api_key = models.CharField(max_length=255, blank=True, null=True)
|
api_key = models.CharField(max_length=255, blank=True, null=True)
|
||||||
actor = JSONField()
|
actor = JSONField()
|
||||||
local = models.BooleanField(default=True)
|
local = models.BooleanField(default=True)
|
||||||
|
@ -23,8 +25,8 @@ 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.export_key()
|
self.private_key = key.export_key().decode('utf8')
|
||||||
self.public_key = key.publickey().export_key()
|
self.public_key = key.publickey().export_key().decode('utf8')
|
||||||
|
|
||||||
if self.local and not self.actor:
|
if self.local and not self.actor:
|
||||||
self.actor = {
|
self.actor = {
|
||||||
|
@ -33,19 +35,22 @@ class User(AbstractUser):
|
||||||
'https://w3id.org/security/v1'
|
'https://w3id.org/security/v1'
|
||||||
],
|
],
|
||||||
|
|
||||||
'id': 'https://%s/u/%s' % (DOMAIN, self.username),
|
'id': 'https://%s/api/u/%s' % (DOMAIN, self.username),
|
||||||
'type': 'Person',
|
'type': 'Person',
|
||||||
'preferredUsername': self.username,
|
'preferredUsername': self.username,
|
||||||
'inbox': 'https://%s/api/inbox' % DOMAIN,
|
'inbox': 'https://%s/api/%s/inbox' % (DOMAIN, self.username),
|
||||||
'followers': 'https://%s/u/%s/followers' % \
|
'followers': 'https://%s/api/u/%s/followers' % \
|
||||||
(DOMAIN, self.username),
|
(DOMAIN, self.username),
|
||||||
'publicKey': {
|
'publicKey': {
|
||||||
'id': 'https://%s/u/%s#main-key' % (DOMAIN, self.username),
|
'id': 'https://%s/api/u/%s#main-key' % (DOMAIN, self.username),
|
||||||
'owner': 'https://%s/u/%s' % (DOMAIN, self.username),
|
'owner': 'https://%s/api/u/%s' % (DOMAIN, self.username),
|
||||||
'publicKeyPem': self.public_key.decode('utf8'),
|
'publicKeyPem': self.public_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not self.activitypub_id:
|
||||||
|
self.activitypub_id = '%s@%s' % (self.username, DOMAIN)
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +83,6 @@ 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)
|
author = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
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)
|
||||||
|
@ -87,19 +91,9 @@ class Message(models.Model):
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class Review(Message):
|
|
||||||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
|
||||||
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):
|
||||||
|
activitypub_id = models.CharField(max_length=255)
|
||||||
|
identifier = models.CharField(max_length=255)
|
||||||
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)
|
||||||
editable = models.BooleanField(default=True)
|
editable = models.BooleanField(default=True)
|
||||||
|
@ -113,6 +107,19 @@ class Shelf(models.Model):
|
||||||
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:
|
||||||
|
unique_together = ('user', 'name')
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.identifier:
|
||||||
|
self.identifier = '%s_%s' % (
|
||||||
|
self.user.username,
|
||||||
|
re.sub(r'\W', '-', self.name).lower()
|
||||||
|
)
|
||||||
|
if not self.activitypub_id:
|
||||||
|
self.activitypub_id = 'https://%s/shelf/%s' % (DOMAIN, self.identifier)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ShelfBook(models.Model):
|
class ShelfBook(models.Model):
|
||||||
# many to many join table for books and shelves
|
# many to many join table for books and shelves
|
||||||
|
@ -125,10 +132,13 @@ class ShelfBook(models.Model):
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
added_date = models.DateTimeField(auto_now_add=True)
|
added_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('book', 'shelf')
|
||||||
|
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
''' a non-canonical copy from open library '''
|
''' a non-canonical copy from open library '''
|
||||||
|
activitypub_id = models.CharField(max_length=255)
|
||||||
openlibary_key = models.CharField(max_length=255)
|
openlibary_key = models.CharField(max_length=255)
|
||||||
data = JSONField()
|
data = JSONField()
|
||||||
works = models.ManyToManyField('Work')
|
works = models.ManyToManyField('Work')
|
||||||
|
@ -148,6 +158,11 @@ class Book(models.Model):
|
||||||
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)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.activitypub_id = '%s%s' % (OL_URL, self.openlibary_key)
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Work(models.Model):
|
class Work(models.Model):
|
||||||
''' encompassses all editions of a book '''
|
''' encompassses all editions of a book '''
|
||||||
openlibary_key = models.CharField(max_length=255)
|
openlibary_key = models.CharField(max_length=255)
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
''' activitystream api and books '''
|
''' activitystream api and books '''
|
||||||
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core import serializers
|
|
||||||
from fedireads.models import Author, Book, Work
|
from fedireads.models import Author, Book, Work
|
||||||
|
from fedireads.settings import OL_URL
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
openlibrary_url = 'https://openlibrary.org'
|
def get_or_create_book(olkey, user=None, update=True):
|
||||||
|
''' add a book '''
|
||||||
def get_book(request, olkey):
|
|
||||||
# check if this is a valid open library key, and a book
|
# check if this is a valid open library key, and a book
|
||||||
response = requests.get(openlibrary_url + '/book/' + olkey + '.json')
|
olkey = '/book/' + olkey
|
||||||
|
response = requests.get(OL_URL + olkey + '.json')
|
||||||
|
|
||||||
# get the existing entry from our db, if it exists
|
# get the existing entry from our db, if it exists
|
||||||
try:
|
try:
|
||||||
book = Book.objects.get(openlibary_key=olkey)
|
book = Book.objects.get(openlibary_key=olkey)
|
||||||
|
if not update:
|
||||||
|
return book
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
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:
|
if user and user.is_authenticated:
|
||||||
book.added_by = request.user
|
book.added_by = 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']
|
||||||
|
@ -27,23 +28,25 @@ def get_book(request, olkey):
|
||||||
for author_id in data['authors']:
|
for author_id in data['authors']:
|
||||||
author_id = author_id['key']
|
author_id = author_id['key']
|
||||||
book.authors.add(get_or_create_author(author_id))
|
book.authors.add(get_or_create_author(author_id))
|
||||||
return HttpResponse(serializers.serialize('json', [book]))
|
return book
|
||||||
|
|
||||||
def get_or_create_work(olkey):
|
def get_or_create_work(olkey):
|
||||||
|
''' load em up '''
|
||||||
try:
|
try:
|
||||||
work = Work.objects.get(openlibary_key=olkey)
|
work = Work.objects.get(openlibary_key=olkey)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
response = requests.get(openlibrary_url + olkey + '.json')
|
response = requests.get(OL_URL + olkey + '.json')
|
||||||
data = response.json()
|
data = response.json()
|
||||||
work = Work(openlibary_key=olkey, data=data)
|
work = Work(openlibary_key=olkey, data=data)
|
||||||
work.save()
|
work.save()
|
||||||
return work
|
return work
|
||||||
|
|
||||||
def get_or_create_author(olkey):
|
def get_or_create_author(olkey):
|
||||||
|
''' load that author '''
|
||||||
try:
|
try:
|
||||||
author = Author.objects.get(openlibary_key=olkey)
|
author = Author.objects.get(openlibary_key=olkey)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
response = requests.get(openlibrary_url + olkey + '.json')
|
response = requests.get(OL_URL + olkey + '.json')
|
||||||
data = response.json()
|
data = response.json()
|
||||||
author = Author(openlibary_key=olkey, data=data)
|
author = Author(openlibary_key=olkey, data=data)
|
||||||
author.save()
|
author.save()
|
||||||
|
|
|
@ -25,9 +25,10 @@ SECRET_KEY = '7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr'
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
ALLOWED_HOSTS = ['localhost', 'ff2cb3e9.ngrok.io']
|
||||||
|
|
||||||
DOMAIN = 'localhost'
|
DOMAIN = 'ff2cb3e9.ngrok.io'
|
||||||
|
OL_URL = 'https://openlibrary.org'
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,15 @@ header > div:last-child {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.carosel {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.carosel > div {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 1rem;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
.user-pic {
|
.user-pic {
|
||||||
width: 2em;
|
width: 2em;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<p class="title">{{ book.data.title }}</p>
|
<p class="title">{{ book.data.title }}</p>
|
||||||
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
||||||
{% if shelf.type == 'reading' %}
|
{% if shelf.type == 'reading' %}
|
||||||
<button>Done reading</button>
|
<button>done reading</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -17,6 +17,21 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="main">
|
<div id="main">
|
||||||
|
<div class="carosel">
|
||||||
|
{% for book in recent_books %}
|
||||||
|
<div class="book-preview">
|
||||||
|
<img class="cover" src="static/images/small.jpg">
|
||||||
|
<p class="title">{{ book.data.title }}</p>
|
||||||
|
<p>by <a href="" class="author">{{ book.authors.first.data.name }}</a></p>
|
||||||
|
{% if not book.user_shelves %}
|
||||||
|
<form name="shelve" action="/shelve/{{ request.user.username }}_to-read/{{ book.id }}" method="post">
|
||||||
|
<input type="hidden" name="book" value="book.id"></input>
|
||||||
|
<input type="submit" value="want to read"></input>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="update">
|
<div class="update">
|
||||||
<div class="user-preview">
|
<div class="user-preview">
|
||||||
<img class="user-pic" src="static/images/profile.jpg">
|
<img class="user-pic" src="static/images/profile.jpg">
|
||||||
|
|
|
@ -23,9 +23,10 @@ urlpatterns = [
|
||||||
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('user/<str:username>', views.user_profile),
|
||||||
|
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/book/<str:olkey>', openlibrary.get_book),
|
path('api/u/<str:username>', federation.actor),
|
||||||
path('api/<str:username>/inbox', federation.inbox),
|
path('api/<str:username>/inbox', federation.inbox),
|
||||||
path('api/<str:username>/outbox', federation.outbox),
|
path('api/<str:username>/outbox', federation.outbox),
|
||||||
path('.well-known/webfinger', federation.webfinger),
|
path('.well-known/webfinger', federation.webfinger),
|
||||||
|
|
|
@ -1,18 +1,30 @@
|
||||||
''' application views/pages '''
|
''' application views/pages '''
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
|
from django.db.models import FilteredRelation, Q
|
||||||
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 import models
|
from fedireads import models
|
||||||
|
import fedireads.activitypub_templates as templates
|
||||||
|
from fedireads.federation import broadcast_action
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
''' user feed '''
|
''' user feed '''
|
||||||
shelves = models.Shelf.objects.filter(user=request.user.id)
|
shelves = models.Shelf.objects.filter(user=request.user.id)
|
||||||
|
recent_books = models.Book.objects.order_by(
|
||||||
|
'added_date'
|
||||||
|
).annotate(
|
||||||
|
user_shelves=FilteredRelation(
|
||||||
|
'shelves',
|
||||||
|
condition=Q(shelves__user_id=request.user.id)
|
||||||
|
)
|
||||||
|
).values('id', 'authors', 'data', 'user_shelves')
|
||||||
data = {
|
data = {
|
||||||
'user': request.user,
|
'user': request.user,
|
||||||
'shelves': shelves,
|
'shelves': shelves,
|
||||||
|
'recent_books': recent_books,
|
||||||
}
|
}
|
||||||
return TemplateResponse(request, 'feed.html', data)
|
return TemplateResponse(request, 'feed.html', data)
|
||||||
|
|
||||||
|
@ -55,9 +67,28 @@ def user_profile(request, username):
|
||||||
return TemplateResponse(request, 'user.html', data)
|
return TemplateResponse(request, 'user.html', data)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_exempt
|
||||||
|
@login_required
|
||||||
|
def shelve(request, shelf_id, book_id):
|
||||||
|
''' put a book on a user's shelf '''
|
||||||
|
book = models.Book.objects.get(id=book_id)
|
||||||
|
shelf = models.Shelf.objects.get(identifier=shelf_id)
|
||||||
|
|
||||||
|
# update the database
|
||||||
|
#models.ShelfBook(book=book, shelf=shelf, added_by=request.user).save()
|
||||||
|
|
||||||
|
# send out the activitypub action
|
||||||
|
action = templates.shelve_action(request.user, book, shelf)
|
||||||
|
recipients = [u.actor['inbox'] for u in request.user.followers.all()]
|
||||||
|
broadcast_action(request.user, action, recipients)
|
||||||
|
|
||||||
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def follow(request):
|
def follow(request):
|
||||||
|
''' follow another user, here or abroad '''
|
||||||
followed = request.POST.get('user')
|
followed = request.POST.get('user')
|
||||||
followed = models.User.objects.get(id=followed)
|
followed = models.User.objects.get(id=followed)
|
||||||
followed.followers.add(request.user)
|
followed.followers.add(request.user)
|
||||||
|
@ -86,9 +117,9 @@ def follow(request):
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
@login_required
|
@login_required
|
||||||
def unfollow(request):
|
def unfollow(request):
|
||||||
|
''' unfollow a user '''
|
||||||
followed = request.POST.get('user')
|
followed = request.POST.get('user')
|
||||||
followed = models.User.objects.get(id=followed)
|
followed = models.User.objects.get(id=followed)
|
||||||
followed.followers.remove(request.user)
|
followed.followers.remove(request.user)
|
||||||
return redirect('/user/%s' % followed.username)
|
return redirect('/user/%s' % followed.username)
|
||||||
|
|
||||||
|
|
||||||
|
|
10
rebuilddb.sh
10
rebuilddb.sh
|
@ -9,7 +9,9 @@ python manage.py migrate
|
||||||
echo "from fedireads.models import User
|
echo "from fedireads.models import User
|
||||||
User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')" | python manage.py shell
|
User.objects.create_user('mouse', 'mouse.reeve@gmail.com', 'password123')" | python manage.py shell
|
||||||
echo "from fedireads.models import User
|
echo "from fedireads.models import User
|
||||||
User.objects.create_user('rat', 'rat@rat.com', 'ratword')" | python manage.py shell
|
User.objects.create_user('rat', 'rat@rat.com', 'ratword')
|
||||||
echo "from fedireads.openlibrary import get_book
|
User.objects.get(id=1).followers.add(User.objects.get(id=2))" | python manage.py shell
|
||||||
get_book(None, 'OL13549170M')
|
echo "from fedireads.openlibrary import get_or_create_book
|
||||||
get_book(None, 'OL24738110M')" | python manage.py shell
|
get_or_create_book('OL13549170M')
|
||||||
|
get_or_create_book('OL24738110M')" | python manage.py shell
|
||||||
|
python manage.py runserver
|
||||||
|
|
Loading…
Reference in a new issue