Refactor micropub, add basic support for querying - source works great, the other two not so much
This commit is contained in:
parent
a6a5264477
commit
9f733125a7
6 changed files with 192 additions and 122 deletions
|
@ -3,23 +3,18 @@ from jose import jwt
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import HttpResponse
|
|
||||||
|
from micropub.views import error
|
||||||
|
|
||||||
|
|
||||||
def auth(request):
|
def auth(request):
|
||||||
if 'HTTP_AUTHORIZATION' in request.META:
|
if 'HTTP_AUTHORIZATION' in request.META:
|
||||||
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
|
auth = request.META.get('HTTP_AUTHORIZATION').split(' ')
|
||||||
if auth[0] != 'Bearer':
|
if auth[0] != 'Bearer':
|
||||||
return HttpResponse(
|
return error.bad_req('auth type {0} not supported'.format(auth[0]))
|
||||||
'authorisation with {0} not supported'.format(auth[0]),
|
|
||||||
content_type='text/plain',
|
|
||||||
status=400
|
|
||||||
)
|
|
||||||
if len(auth) != 2:
|
if len(auth) != 2:
|
||||||
return HttpResponse(
|
return error.bad_req(
|
||||||
'invalid Bearer auth format, must be Bearer <token>',
|
'invalid Bearer auth format, must be Bearer <token>'
|
||||||
content_type='text/plain',
|
|
||||||
status=400
|
|
||||||
)
|
)
|
||||||
token = auth[1]
|
token = auth[1]
|
||||||
elif 'access_token' in request.POST:
|
elif 'access_token' in request.POST:
|
||||||
|
@ -27,20 +22,12 @@ def auth(request):
|
||||||
elif 'access_token' in request.GET:
|
elif 'access_token' in request.GET:
|
||||||
token = request.GET.get('access_token')
|
token = request.GET.get('access_token')
|
||||||
else:
|
else:
|
||||||
return HttpResponse(
|
return error.unauthorized()
|
||||||
'authorisation required',
|
|
||||||
content_type='text/plain',
|
|
||||||
status=401
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = decode(token)
|
token = decode(token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return HttpResponse(
|
return error.forbidden()
|
||||||
'invalid micropub token',
|
|
||||||
content_type='text/plain',
|
|
||||||
status=403,
|
|
||||||
)
|
|
||||||
|
|
||||||
return MicropubToken(token)
|
return MicropubToken(token)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
20
micropub/views/__init__.py
Normal file
20
micropub/views/__init__.py
Normal 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
87
micropub/views/create.py
Normal 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
31
micropub/views/error.py
Normal 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
47
micropub/views/query.py
Normal 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)
|
Loading…
Reference in a new issue