""" Micropub endpoint routes for StarPunk Implements the W3C Micropub specification for creating posts via external IndieWeb clients. Endpoints: GET/POST /micropub - Main Micropub endpoint GET: Query operations (config, source, syndicate-to) POST: Action operations (create in V1, update/delete in future) Authentication: Bearer token authentication required for all endpoints. Token must have appropriate scope for requested operation. References: - W3C Micropub Specification: https://www.w3.org/TR/micropub/ - ADR-028: Micropub Implementation Strategy - ADR-029: Micropub IndieAuth Integration Strategy """ from flask import Blueprint, current_app, request from starpunk.micropub import ( MicropubError, extract_bearer_token, error_response, handle_create, handle_query, ) from starpunk.tokens import verify_token # Create blueprint bp = Blueprint("micropub", __name__) @bp.route("/micropub", methods=["GET", "POST"]) def micropub_endpoint(): """ Main Micropub endpoint for all operations GET requests: Handle query operations via q= parameter: - q=config: Return server capabilities - q=source&url={url}: Return post source - q=syndicate-to: Return syndication targets POST requests: Handle action operations (form-encoded or JSON): - action=create (or no action): Create new post - action=update: Update existing post (not supported in V1) - action=delete: Delete post (not supported in V1) Authentication: Requires valid bearer token in Authorization header or access_token parameter. Returns: GET: JSON response with query results POST create: 201 Created with Location header POST other: Error responses Error responses follow OAuth 2.0 format: { "error": "error_code", "error_description": "Human-readable description" } """ # Extract and verify token token = extract_bearer_token(request) if not token: return error_response("unauthorized", "No access token provided", 401) token_info = verify_token(token) if not token_info: return error_response("unauthorized", "Invalid or expired access token", 401) # Handle query endpoints (GET requests) if request.method == "GET": try: return handle_query(request.args.to_dict(), token_info) except MicropubError as e: return error_response(e.error, e.error_description, e.status_code) except Exception as e: current_app.logger.error(f"Micropub query error: {e}") return error_response("server_error", "An unexpected error occurred", 500) # Handle action endpoints (POST requests) content_type = request.headers.get("Content-Type", "") try: # Parse request based on content type if "application/json" in content_type: data = request.get_json() or {} action = data.get("action", "create") else: # Form-encoded or multipart (V1 only supports form-encoded) data = request.form.to_dict(flat=False) action = data.get("action", ["create"])[0] # Route to appropriate handler if action == "create": return handle_create(data, token_info) elif action == "update": # V1: Update not supported return error_response( "invalid_request", "Update action not supported in V1", 400 ) elif action == "delete": # V1: Delete not supported return error_response( "invalid_request", "Delete action not supported in V1", 400 ) else: return error_response("invalid_request", f"Unknown action: {action}", 400) except MicropubError as e: return error_response(e.error, e.error_description, e.status_code) except Exception as e: current_app.logger.error(f"Micropub action error: {e}") return error_response("server_error", "An unexpected error occurred", 500)