changeset 895:accfe8fc8440

admin: Add a Micropub endpoint.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 02 Jul 2017 22:23:12 -0700
parents ca357249a321
children 85d2b386b971
files piecrust/admin/templates/micropub.html piecrust/admin/views/micropub.py requirements.txt
diffstat 3 files changed, 194 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/admin/templates/micropub.html	Sun Jul 02 22:23:12 2017 -0700
@@ -0,0 +1,10 @@
+{% set title = 'Micropub Endpoint' %}
+
+{% extends 'layouts/master.html' %}
+
+{% block content %}
+<p>This is PieCrust's Micropub endpoint.</p>
+{% endblock %}
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/piecrust/admin/views/micropub.py	Sun Jul 02 22:23:12 2017 -0700
@@ -0,0 +1,183 @@
+import re
+import os
+import os.path
+import logging
+import datetime
+from werkzeug.utils import secure_filename
+from flask import g, request, abort, Response
+from flask_indieauth import requires_indieauth
+from ..blueprint import foodtruck_bp
+from piecrust.page import Page
+
+
+logger = logging.getLogger(__name__)
+
+re_unsafe_asset_char = re.compile('[^a-zA-Z0-9_]')
+
+
+@foodtruck_bp.route('/micropub', methods=['POST'])
+@requires_indieauth
+def micropub():
+    post_type = request.form.get('h')
+
+    if post_type == 'entry':
+        uri = _create_hentry()
+        _run_publisher()
+        return _get_location_response(uri)
+
+    logger.debug("Unknown or unsupported update type.")
+    logger.debug(request.form)
+    abort(400)
+
+
+def _run_publisher():
+    pcapp = g.site.piecrust_app
+    target = pcapp.config.get('micropub/publish_target', 'default')
+    logger.debug("Running pushing target '%s'." % target)
+    g.site.publish(target)
+
+
+def _get_location_response(uri):
+    logger.debug("Redirecting to: %s" % uri)
+    r = Response()
+    r.status_code = 201
+    r.headers.add('Location', uri)
+    return r
+
+
+def _create_hentry():
+    f = request.form
+    summary = f.get('summary')
+    categories = f.getlist('category[]')
+    location = f.get('location')
+    reply_to = f.get('in-reply-to')
+    status = f.get('post-status')
+    # pubdate = f.get('published', 'now')
+
+    # Figure out the title of the post.
+    name = f.get('name')
+    if not name:
+        name = f.get('name[]')
+
+    # Figure out the contents of the post.
+    post_format = None
+    content = f.get('content')
+    if not content:
+        content = f.get('content[]')
+    if not content:
+        content = f.get('content[html]')
+        post_format = 'none'
+
+    if not content:
+        logger.error("No content specified!")
+        logger.error(dict(request.form))
+        abort(400)
+
+    # TODO: setting to conserve Windows-type line endings?
+    content = content.replace('\r\n', '\n')
+    if summary:
+        summary = summary.replace('\r\n', '\n')
+
+    # Figure out the slug of the post.
+    now = datetime.datetime.now()
+    slug = f.get('slug')
+    if not slug:
+        slug = f.get('mp-slug')
+    if not slug:
+        slug = '%02d%02d%02d' % (now.hour, now.minute, now.second)
+
+    # Get the media to attach to the post.
+    photo_urls = None
+    if 'photo' in f:
+        photo_urls = [f['photo']]
+    elif 'photo[]' in f:
+        photo_urls = f.getlist('photo[]')
+
+    photos = None
+    if 'photo' in request.files:
+        photos = [request.files['photo']]
+    elif 'photo[]' in request.files:
+        photos = request.files.getlist('photo[]')
+
+    # Create the post in the correct content source.
+    pcapp = g.site.piecrust_app
+    source_name = pcapp.config.get('micropub/source', 'posts')
+    source = pcapp.getSource(source_name)
+
+    metadata = {
+        'date': now,
+        'slug': slug
+    }
+    logger.debug("Creating item with metadata: %s" % metadata)
+    content_item = source.createContent(metadata)
+    if content_item is None:
+        logger.error("Can't create item for: %s" % metadata)
+        abort(500)
+
+    # TODO: add proper APIs for creating related assets.
+    photo_names = None
+    if photos:
+        photo_dir, _ = os.path.splitext(content_item.spec)
+        photo_dir += '-assets'
+        if not os.path.exists(photo_dir):
+            os.makedirs(photo_dir)
+
+        photo_names = []
+        for photo in photos:
+            if not photo or not photo.filename:
+                logger.warning("Got empty photo in request files... skipping.")
+                continue
+
+            fn = secure_filename(photo.filename)
+            fn = re_unsafe_asset_char.sub('_', fn)
+            photo_path = os.path.join(photo_dir, fn)
+            logger.info("Uploading file to: %s" % photo_path)
+            photo.save(photo_path)
+
+            fn_no_ext, _ = os.path.splitext(fn)
+            photo_names.append(fn_no_ext)
+
+    logger.debug("Writing to item: %s" % content_item.spec)
+    with source.openItem(content_item, mode='w') as fp:
+        fp.write('---\n')
+        if name:
+            fp.write('title: "%s"\n' % name)
+        if categories:
+            fp.write('tags: [%s]\n' % ','.join(categories))
+        if location:
+            fp.write('location: %s\n' % location)
+        if reply_to:
+            fp.write('reply_to: "%s"\n' % reply_to)
+        if status:
+            fp.write('status: %s\n' % status)
+        if post_format:
+            fp.write('format: %s\n' % post_format)
+        fp.write('time: %02d:%02d:%02d\n' % (now.hour, now.minute, now.second))
+        fp.write('---\n')
+
+        if summary:
+            fp.write(summary)
+            fp.write('\n')
+            fp.write('<!--break-->\n\n')
+        fp.write(content)
+
+        if photo_urls:
+            fp.write('\n\n')
+            for pu in photo_urls:
+                fp.write('<img src="{{assets.%s}}" alt=""/>\n\n' % pu)
+
+        if photo_names:
+            fp.write('\n\n')
+            for pn in photo_names:
+                fp.write('<img src="{{assets.%s}}" alt="%s"/>\n\n' %
+                         (pn, pn))
+
+    route = pcapp.getSourceRoute(source.name)
+    if route is None:
+        logger.error("Can't find route for source: %s" % source.name)
+        abort(500)
+
+    page = Page(source, content_item)
+    uri = page.getUri()
+    return uri
+
--- a/requirements.txt	Sun Jul 02 22:21:24 2017 -0700
+++ b/requirements.txt	Sun Jul 02 22:23:12 2017 -0700
@@ -5,6 +5,7 @@
 compressinja==0.0.2
 cryptography==1.8.1
 Flask==0.10.1
+Flask-IndieAuth==0.0.3.2
 Flask-Login==0.3.2
 idna==2.5
 itsdangerous==0.24