forked from mirrors/bookwyrm
Code cleanup
This commit is contained in:
parent
31110f4b0c
commit
01464003d5
8 changed files with 156 additions and 144 deletions
|
@ -1,5 +1,19 @@
|
|||
''' api utilties '''
|
||||
from base64 import b64encode
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
from datetime import datetime
|
||||
import json
|
||||
import requests
|
||||
|
||||
from fedireads import models
|
||||
from fedireads import incoming
|
||||
from fedireads.settings import DOMAIN
|
||||
|
||||
|
||||
def get_or_create_remote_user(actor):
|
||||
''' wow, a foreigner '''
|
||||
''' look up a remote user or add them '''
|
||||
try:
|
||||
user = models.User.objects.get(actor=actor)
|
||||
except models.User.DoesNotExist:
|
||||
|
@ -13,3 +27,56 @@ def get_or_create_remote_user(actor):
|
|||
)
|
||||
return user
|
||||
|
||||
|
||||
def get_recipients(user, post_privacy, direct_recipients=None):
|
||||
''' deduplicated list of recipients '''
|
||||
recipients = direct_recipients or []
|
||||
|
||||
followers = user.followers.all()
|
||||
if post_privacy == 'public':
|
||||
# post to public shared inboxes
|
||||
shared_inboxes = set(u.shared_inbox for u in followers)
|
||||
recipients += list(shared_inboxes)
|
||||
# TODO: direct to anyone who's mentioned
|
||||
if post_privacy == 'followers':
|
||||
# don't send it to the shared inboxes
|
||||
inboxes = set(u.inbox for u in followers)
|
||||
recipients += list(inboxes)
|
||||
# if post privacy is direct, we just have direct recipients,
|
||||
# which is already set. hurray
|
||||
return recipients
|
||||
|
||||
|
||||
def broadcast(sender, action, recipients):
|
||||
''' send out an event '''
|
||||
for recipient in recipients:
|
||||
# TODO: error handling
|
||||
sign_and_send(sender, action, recipient)
|
||||
|
||||
|
||||
def sign_and_send(sender, action, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
inbox_fragment = sender.inbox.replace('https://%s' % 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.localname
|
||||
signature += 'headers="(request-target) host date",'
|
||||
signature += 'signature="%s"' % b64encode(signed_message)
|
||||
response = requests.post(
|
||||
destination,
|
||||
data=json.dumps(action),
|
||||
headers={
|
||||
'Date': now,
|
||||
'Signature': signature,
|
||||
'Host': DOMAIN,
|
||||
},
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
incoming.handle_response(response)
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
''' activitystream api '''
|
||||
''' handles all of the activity coming in to the server '''
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, \
|
||||
HttpResponseNotFound, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from fedireads.settings import DOMAIN
|
||||
from fedireads.openlibrary import get_or_create_book
|
||||
from fedireads import models
|
||||
from fedireads.api import get_or_create_remote_user
|
||||
import json
|
||||
from uuid import uuid4
|
||||
|
||||
from fedireads import models
|
||||
from fedireads.api import get_or_create_remote_user
|
||||
from fedireads.openlibrary import get_or_create_book
|
||||
from fedireads.settings import DOMAIN
|
||||
|
||||
|
||||
def webfinger(request):
|
||||
''' allow other servers to ask about a user '''
|
||||
resource = request.GET.get('resource')
|
||||
|
@ -206,3 +208,34 @@ def handle_incoming_create(activity):
|
|||
activity_type=activity['object']['type']
|
||||
)
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
def handle_incoming_accept(activity):
|
||||
''' someone is accepting a follow request '''
|
||||
# our local user
|
||||
user = models.User.objects.get(actor=activity['actor'])
|
||||
# the person our local user wants to follow, who said yes
|
||||
followed = get_or_create_remote_user(activity['object']['actor'])
|
||||
|
||||
# save this relationship in the db
|
||||
followed.followers.add(user)
|
||||
|
||||
# save the activity record
|
||||
models.FollowActivity(
|
||||
uuid=activity['id'],
|
||||
user=user,
|
||||
followed=followed,
|
||||
content=activity,
|
||||
).save()
|
||||
|
||||
|
||||
def handle_response(response):
|
||||
''' hopefully it's an accept from our follow request '''
|
||||
try:
|
||||
activity = response.json()
|
||||
except ValueError:
|
||||
return
|
||||
if activity['type'] == 'Accept':
|
||||
handle_incoming_accept(activity)
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 3.0.2 on 2020-01-28 19:06
|
||||
# Generated by Django 3.0.2 on 2020-01-28 19:39
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.models
|
||||
|
@ -65,6 +65,7 @@ class Migration(migrations.Migration):
|
|||
('uuid', models.CharField(max_length=255, unique=True)),
|
||||
('content', django.contrib.postgres.fields.jsonb.JSONField(max_length=5000)),
|
||||
('activity_type', models.CharField(max_length=255)),
|
||||
('fedireads_type', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('created_date', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_date', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
|
|
|
@ -3,11 +3,13 @@ from django.db import models
|
|||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto import Random
|
||||
from fedireads.settings import DOMAIN, OL_URL
|
||||
from Crypto.PublicKey import RSA
|
||||
import re
|
||||
|
||||
from fedireads.settings import DOMAIN, OL_URL
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
''' a user who wants to read books '''
|
||||
private_key = models.TextField(blank=True, null=True)
|
||||
|
@ -91,7 +93,10 @@ class Activity(models.Model):
|
|||
uuid = models.CharField(max_length=255, unique=True)
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
content = JSONField(max_length=5000)
|
||||
# the activitypub activity type (Create, Add, Follow, ...)
|
||||
activity_type = models.CharField(max_length=255)
|
||||
# custom types internal to fedireads (Review, Shelve, ...)
|
||||
fedireads_type = models.CharField(max_length=255, blank=True, null=True)
|
||||
created_date = models.DateTimeField(auto_now_add=True)
|
||||
updated_date = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
@ -101,6 +106,12 @@ class ShelveActivity(Activity):
|
|||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||
shelf = models.ForeignKey('Shelf', on_delete=models.PROTECT)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.activity_type:
|
||||
self.activity_type = 'Add'
|
||||
shelf.fedireads_type = 'Shelve'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class FollowActivity(Activity):
|
||||
''' record follow requests sent out '''
|
||||
|
@ -121,12 +132,14 @@ class Review(Activity):
|
|||
book = models.ForeignKey('Book', on_delete=models.PROTECT)
|
||||
work = models.ForeignKey('Work', on_delete=models.PROTECT)
|
||||
name = models.TextField()
|
||||
# TODO: validation
|
||||
rating = models.IntegerField(default=0)
|
||||
review_content = models.TextField()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.activity_type:
|
||||
self.activity_type = 'Article'
|
||||
self.fedireads_type = 'Review'
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
''' activitystream api '''
|
||||
from base64 import b64encode
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import pkcs1_15
|
||||
from Crypto.Hash import SHA256
|
||||
''' handles all the activity coming out of the server '''
|
||||
from datetime import datetime
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from fedireads.settings import DOMAIN
|
||||
from fedireads import models
|
||||
from fedireads.api import get_or_create_remote_user
|
||||
import json
|
||||
import requests
|
||||
from uuid import uuid4
|
||||
|
||||
from fedireads import models
|
||||
from fedireads.api import get_or_create_remote_user, get_recipients, \
|
||||
broadcast
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def outbox(request, username):
|
||||
|
@ -69,35 +65,6 @@ def handle_outgoing_follow(user, to_follow):
|
|||
broadcast(user, activity, [to_follow.inbox])
|
||||
|
||||
|
||||
def handle_response(response):
|
||||
''' hopefully it's an accept from our follow request '''
|
||||
try:
|
||||
activity = response.json()
|
||||
except ValueError:
|
||||
return
|
||||
if activity['type'] == 'Accept':
|
||||
handle_incoming_accept(activity)
|
||||
|
||||
|
||||
def handle_incoming_accept(activity):
|
||||
''' someone is accepting a follow request '''
|
||||
# our local user
|
||||
user = models.User.objects.get(actor=activity['actor'])
|
||||
# the person our local user wants to follow, who said yes
|
||||
followed = get_or_create_remote_user(activity['object']['actor'])
|
||||
|
||||
# save this relationship in the db
|
||||
followed.followers.add(user)
|
||||
|
||||
# save the activity record
|
||||
models.FollowActivity(
|
||||
uuid=activity['id'],
|
||||
user=user,
|
||||
followed=followed,
|
||||
content=activity,
|
||||
).save()
|
||||
|
||||
|
||||
def handle_shelve(user, book, shelf):
|
||||
''' a local user is getting a book put on their shelf '''
|
||||
# update the database
|
||||
|
@ -142,26 +109,6 @@ def handle_shelve(user, book, shelf):
|
|||
broadcast(user, activity, recipients)
|
||||
|
||||
|
||||
def get_recipients(user, post_privacy, direct_recipients=None):
|
||||
''' deduplicated list of recipients '''
|
||||
recipients = direct_recipients or []
|
||||
|
||||
followers = user.followers.all()
|
||||
if post_privacy == 'public':
|
||||
# post to public shared inboxes
|
||||
shared_inboxes = set(u.shared_inbox for u in followers)
|
||||
recipients += list(shared_inboxes)
|
||||
# TODO: direct to anyone who's mentioned
|
||||
if post_privacy == 'followers':
|
||||
# don't send it to the shared inboxes
|
||||
inboxes = set(u.inbox for u in followers)
|
||||
recipients += list(inboxes)
|
||||
# if post privacy is direct, we just have direct recipients,
|
||||
# which is already set. hurray
|
||||
return recipients
|
||||
|
||||
|
||||
|
||||
def handle_review(user, book, name, content, rating):
|
||||
''' post a review '''
|
||||
review_uuid = uuid4()
|
||||
|
@ -189,7 +136,6 @@ def handle_review(user, book, name, content, rating):
|
|||
'cc': ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
|
||||
'object': obj,
|
||||
|
||||
}
|
||||
|
||||
models.Review(
|
||||
|
@ -205,37 +151,3 @@ def handle_review(user, book, name, content, rating):
|
|||
).save()
|
||||
broadcast(user, activity, recipients)
|
||||
|
||||
|
||||
|
||||
|
||||
def broadcast(sender, action, recipients):
|
||||
''' send out an event to all followers '''
|
||||
for recipient in recipients:
|
||||
sign_and_send(sender, action, recipient)
|
||||
|
||||
|
||||
def sign_and_send(sender, action, destination):
|
||||
''' crpyto whatever and http junk '''
|
||||
inbox_fragment = sender.inbox.replace('https://%s' % 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.localname
|
||||
signature += 'headers="(request-target) host date",'
|
||||
signature += 'signature="%s"' % b64encode(signed_message)
|
||||
response = requests.post(
|
||||
destination,
|
||||
data=json.dumps(action),
|
||||
headers={
|
||||
'Date': now,
|
||||
'Signature': signature,
|
||||
'Host': DOMAIN,
|
||||
},
|
||||
)
|
||||
if not response.ok:
|
||||
response.raise_for_status()
|
||||
handle_response(response)
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
"""
|
||||
Django settings for fedireads project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.0.13.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
|
||||
''' fedireads settings and configuration '''
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
|
@ -25,7 +14,7 @@ SECRET_KEY = '7(2w1sedok=aznpq)ta1mc4i%4h=xx@hxwx*o57ctsuml0x%fr'
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
|
||||
# TODO: this hsould be populated at runtime at least for debug mode
|
||||
DOMAIN = 'bd352ee8.ngrok.io'
|
||||
ALLOWED_HOSTS = ['*']
|
||||
OL_URL = 'https://openlibrary.org'
|
||||
|
@ -54,6 +43,7 @@ MIDDLEWARE = [
|
|||
|
||||
ROOT_URLCONF = 'fedireads.urls'
|
||||
|
||||
# TODO: how tf do I switch to jinja2
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
|
|
|
@ -1,28 +1,16 @@
|
|||
"""fedireads URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/2.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
''' url routing for the app and api '''
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from fedireads import incoming, outgoing, views, settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
# federation endpoints
|
||||
path('/inbox', incoming.shared_inbox),
|
||||
path('inbox', incoming.shared_inbox),
|
||||
path('user/<str:username>.json', incoming.get_actor),
|
||||
path('user/<str:username>/inbox', incoming.inbox),
|
||||
path('user/<str:username>/outbox', outgoing.outbox),
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
''' application views/pages '''
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Avg, FilteredRelation, Q
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.http import HttpResponseNotFound
|
||||
from fedireads import models, openlibrary
|
||||
from fedireads import outgoing as api
|
||||
from fedireads.settings import DOMAIN
|
||||
import re
|
||||
|
||||
from fedireads import models, openlibrary, outgoing as api
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
''' user feed '''
|
||||
''' user's homepage with activity feed '''
|
||||
shelves = models.Shelf.objects.filter(user=request.user.id)
|
||||
recent_books = models.Book.objects.order_by(
|
||||
'added_date'
|
||||
|
@ -22,11 +22,15 @@ def home(request):
|
|||
'shelves',
|
||||
condition=Q(shelves__user_id=request.user.id)
|
||||
)
|
||||
).values('id', 'authors', 'data', 'user_shelves', 'openlibrary_key')
|
||||
).values(
|
||||
'id', 'authors', 'data', 'user_shelves', 'openlibrary_key'
|
||||
).distinct()
|
||||
|
||||
following = models.User.objects.filter(
|
||||
Q(followers=request.user) | Q(id=request.user.id))
|
||||
Q(followers=request.user) | Q(id=request.user.id)
|
||||
)
|
||||
|
||||
# TODO: handle post privacy
|
||||
activities = models.Activity.objects.filter(
|
||||
user__in=following
|
||||
).order_by('-created_date')[:10]
|
||||
|
@ -135,15 +139,20 @@ def shelve(request, shelf_id, book_id):
|
|||
api.handle_shelve(request.user, book, shelf)
|
||||
return redirect('/')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def review(request):
|
||||
''' create a book review note '''
|
||||
# TODO: error handling
|
||||
book_identifier = request.POST.get('book')
|
||||
book = openlibrary.get_or_create_book(book_identifier)
|
||||
|
||||
# TODO: validation, htmlification
|
||||
name = request.POST.get('name')
|
||||
content = request.POST.get('content')
|
||||
rating = request.POST.get('rating')
|
||||
|
||||
api.handle_review(request.user, book, name, content, rating)
|
||||
return redirect(book_identifier)
|
||||
|
||||
|
@ -153,6 +162,7 @@ def review(request):
|
|||
def follow(request):
|
||||
''' follow another user, here or abroad '''
|
||||
to_follow = request.POST.get('user')
|
||||
# should this be an actor rather than an id? idk
|
||||
to_follow = models.User.objects.get(id=to_follow)
|
||||
|
||||
api.handle_outgoing_follow(request.user, to_follow)
|
||||
|
@ -163,6 +173,7 @@ def follow(request):
|
|||
@login_required
|
||||
def unfollow(request):
|
||||
''' unfollow a user '''
|
||||
# TODO: this is not an implementation!!
|
||||
followed = request.POST.get('user')
|
||||
followed = models.User.objects.get(id=followed)
|
||||
followed.followers.remove(request.user)
|
||||
|
@ -177,11 +188,8 @@ def search(request):
|
|||
if re.match(r'\w+@\w+.\w+', query):
|
||||
results = [api.handle_account_search(query)]
|
||||
else:
|
||||
# TODO: book search
|
||||
results = []
|
||||
|
||||
return TemplateResponse(request, 'results.html', {'results': results})
|
||||
|
||||
|
||||
def simplify_local_username(user):
|
||||
''' helper for getting the short username for local users '''
|
||||
return user.username.replace('@%s' % DOMAIN, '')
|
||||
|
|
Loading…
Reference in a new issue