adding federation

This commit is contained in:
Mouse Reeve 2020-01-26 17:55:02 -08:00
parent e30e06c283
commit 6b85d8838f
11 changed files with 244 additions and 83 deletions

View 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
}
}

View file

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

View file

@ -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')},
),
] ]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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