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