Refactor micropub, add basic support for querying - source works great, the other two not so much

This commit is contained in:
Danielle McLean 2018-01-29 10:28:46 +11:00
parent a6a5264477
commit 9f733125a7
Signed by untrusted user: 00dani
GPG key ID: 5A5D2D1AFF12EEC5
6 changed files with 192 additions and 122 deletions

View file

@ -3,23 +3,18 @@ from jose import jwt
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from django.conf import settings
from django.http import HttpResponse
from micropub.views import error
def auth(request):
if 'HTTP_AUTHORIZATION' in request.META:
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
if auth[0] != 'Bearer':
return HttpResponse(
'authorisation with {0} not supported'.format(auth[0]),
content_type='text/plain',
status=400
)
return error.bad_req('auth type {0} not supported'.format(auth[0]))
if len(auth) != 2:
return HttpResponse(
'invalid Bearer auth format, must be Bearer <token>',
content_type='text/plain',
status=400
return error.bad_req(
'invalid Bearer auth format, must be Bearer <token>'
)
token = auth[1]
elif 'access_token' in request.POST:
@ -27,20 +22,12 @@ def auth(request):
elif 'access_token' in request.GET:
token = request.GET.get('access_token')
else:
return HttpResponse(
'authorisation required',
content_type='text/plain',
status=401
)
return error.unauthorized()
try:
token = decode(token)
except Exception as e:
return HttpResponse(
'invalid micropub token',
content_type='text/plain',
status=403,
)
return error.forbidden()
return MicropubToken(token)

View file

@ -1,102 +0,0 @@
import json
from django.http import HttpResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from urllib.parse import urljoin
from entries.jobs import ping_hub, send_mentions
from entries.models import Cat, Entry
from entries.kinds import Article, Note, Reply, Like, Repost
from lemoncurry import utils
from lemonauth import tokens
def form_to_mf2(request):
properties = {}
post = request.POST
for key in post.keys():
if key.endswith('[]'):
key = key[:-2]
if key == 'access_token':
continue
properties[key] = post.getlist(key) + post.getlist(key + '[]')
type = []
if 'h' in properties:
type = ['h-' + p for p in properties['h']]
del properties['h']
return {'type': type, 'properties': properties}
@method_decorator(csrf_exempt, name='dispatch')
class MicropubView(View):
def post(self, request):
token = tokens.auth(request)
if hasattr(token, 'content'):
return token
normalise = {
'application/json': json.load,
'application/x-www-form-urlencoded': form_to_mf2,
}
if request.content_type not in normalise:
return HttpResponse(
'unsupported request type {0}'.format(request.content_type),
content_type='text/plain',
status=415,
)
body = normalise[request.content_type](request)
if 'type' not in body:
return utils.bad_req('mf2 object type required')
if body['type'] != ['h-entry']:
return utils.bad_req('only h-entry supported')
entry = Entry(author=token.user)
props = body.get('properties', {})
kind = Note
if 'name' in props:
entry.name = '\n'.join(props['name'])
kind = Article
if 'content' in props:
entry.content = '\n'.join(
c if isinstance(c, str) else c['html']
for c in props['content']
)
if 'in-reply-to' in props:
entry.in_reply_to = props['in-reply-to']
kind = Reply
if 'like-of' in props:
entry.like_of = props['like-of']
kind = Like
if 'repost-of' in props:
entry.repost_of = props['repost-of']
kind = Repost
cats = [Cat.objects.from_name(c) for c in props.get('category', [])]
entry.kind = kind.id
entry.save()
entry.cats = cats
entry.save()
base = utils.origin(request)
perma = urljoin(base, entry.url)
others = [urljoin(base, url) for url in (
reverse('home:index'),
reverse('entries:atom'),
reverse('entries:rss'),
reverse('entries:' + kind.index),
reverse('entries:' + kind.atom),
reverse('entries:' + kind.rss),
)] + [urljoin(base, cat.url) for cat in cats]
ping_hub.delay(perma, *others)
send_mentions.delay(perma)
res = HttpResponse(status=201)
res['Location'] = perma
return res
micropub = MicropubView.as_view()

View file

@ -0,0 +1,20 @@
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from lemonauth import tokens
from .create import create
from .query import query
@csrf_exempt
@require_http_methods(['GET', 'HEAD', 'POST'])
def micropub(request):
token = tokens.auth(request)
if hasattr(token, 'content'):
return token
request.token = token
if request.method == 'POST':
return create(request)
if request.method in ('GET', 'HEAD'):
return query(request)

87
micropub/views/create.py Normal file
View file

@ -0,0 +1,87 @@
import json
from django.urls import reverse
from django.http import HttpResponse
from urllib.parse import urljoin
from entries.jobs import ping_hub, send_mentions
from entries.models import Cat, Entry
from entries.kinds import Article, Note, Reply, Like, Repost
from lemoncurry import utils
from . import error
def form_to_mf2(request):
properties = {}
post = request.POST
for key in post.keys():
if key.endswith('[]'):
key = key[:-2]
if key == 'access_token':
continue
properties[key] = post.getlist(key) + post.getlist(key + '[]')
type = []
if 'h' in properties:
type = ['h-' + p for p in properties['h']]
del properties['h']
return {'type': type, 'properties': properties}
def create(request):
normalise = {
'application/json': json.load,
'application/x-www-form-urlencoded': form_to_mf2,
}
if request.content_type not in normalise:
return error.unsupported_type(request.content_type)
body = normalise[request.content_type](request)
if 'type' not in body:
return error.bad_req('mf2 object type required')
if body['type'] != ['h-entry']:
return error.bad_req('only h-entry supported')
entry = Entry(author=request.token.user)
props = body.get('properties', {})
kind = Note
if 'name' in props:
entry.name = '\n'.join(props['name'])
kind = Article
if 'content' in props:
entry.content = '\n'.join(
c if isinstance(c, str) else c['html']
for c in props['content']
)
if 'in-reply-to' in props:
entry.in_reply_to = props['in-reply-to']
kind = Reply
if 'like-of' in props:
entry.like_of = props['like-of']
kind = Like
if 'repost-of' in props:
entry.repost_of = props['repost-of']
kind = Repost
cats = [Cat.objects.from_name(c) for c in props.get('category', [])]
entry.kind = kind.id
entry.save()
entry.cats = cats
entry.save()
base = utils.origin(request)
perma = urljoin(base, entry.url)
others = [urljoin(base, url) for url in (
reverse('home:index'),
reverse('entries:atom'),
reverse('entries:rss'),
reverse('entries:' + kind.index),
reverse('entries:' + kind.atom),
reverse('entries:' + kind.rss),
)] + [urljoin(base, cat.url) for cat in cats]
ping_hub.delay(perma, *others)
send_mentions.delay(perma)
res = HttpResponse(status=201)
res['Location'] = perma
return res

31
micropub/views/error.py Normal file
View file

@ -0,0 +1,31 @@
from django.http import JsonResponse
def forbidden():
return res('forbidden', 403)
def unauthorized():
return res('unauthorized', 401)
def bad_req(msg):
return res('invalid_request', msg=msg)
def bad_type(type):
msg = 'unsupported request type {0}'.format(type)
return res('invalid_request', 415, msg)
def bad_scope(scope):
return res('insufficient_scope', 401, scope=scope)
def res(error, status=400, msg=None, scope=None):
content = {'error': error}
if msg:
content['error_description'] = msg
if scope:
content['scope'] = scope
return JsonResponse(content, status=status)

47
micropub/views/query.py Normal file
View file

@ -0,0 +1,47 @@
from django.http import JsonResponse
from lemoncurry import requests
from . import error
def config(request):
config = syndicate_to(request)
return config
def source(request):
get = request.GET
if 'url' not in get:
return error.bad_req('must specify url parameter for source query')
mf2 = requests.mf2(get['url']).to_dict(filter_by_type='h-entry')
if not mf2:
return error.bad_req('no h-entry at the requested url')
entry = mf2[0]
keys = get.getlist('properties', []) + get.getlist('properties[]', [])
if not keys:
return entry
props = entry['properties']
return {'properties': {k: props[k] for k in keys if k in props}}
def syndicate_to(request):
return {'syndicate-to': []}
queries = {
'config': config,
'source': source,
'syndicate-to': syndicate_to,
}
def query(request):
if 'q' not in request.GET:
return error.bad_req('must specify q parameter')
q = request.GET['q']
if q not in queries:
return error.bad_req('unsupported query {0}'.format(q))
res = queries[q](request)
if hasattr(res, 'content'):
return res
return JsonResponse(res)