# HG changeset patch # User Ludovic Chabant # Date 1499059392 25200 # Node ID accfe8fc844006614f326cd72a51bdbe92ef7132 # Parent ca357249a321b19541521fd4ad088e550cb6db5e admin: Add a Micropub endpoint. diff -r ca357249a321 -r accfe8fc8440 piecrust/admin/templates/micropub.html --- /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 %} +

This is PieCrust's Micropub endpoint.

+{% endblock %} + + + diff -r ca357249a321 -r accfe8fc8440 piecrust/admin/views/micropub.py --- /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('\n\n') + fp.write(content) + + if photo_urls: + fp.write('\n\n') + for pu in photo_urls: + fp.write('\n\n' % pu) + + if photo_names: + fp.write('\n\n') + for pn in photo_names: + fp.write('%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 + diff -r ca357249a321 -r accfe8fc8440 requirements.txt --- 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