Following design in /docs/design/micropub-endpoint-design.md and /docs/decisions/ADR-028-micropub-implementation.md Micropub Module (starpunk/micropub.py): - Property normalization for form-encoded and JSON requests - Content/title/tags extraction from Micropub properties - Bearer token extraction from Authorization header or form - Create action handler integrating with notes.py CRUD - Query endpoints (config, source, syndicate-to) - OAuth 2.0 compliant error responses Micropub Route (starpunk/routes/micropub.py): - Main /micropub endpoint handling GET and POST - Bearer token authentication and validation - Content-type handling (form-encoded and JSON) - Action routing (create supported, update/delete return V1 error) - Comprehensive error handling Integration: - Registered micropub blueprint in routes/__init__.py - Maps Micropub properties to StarPunk note format - Returns 201 Created with Location header per spec - V1 limitations clearly documented (no update/delete) All 23 Phase 3 tests pass Total: 77 tests pass (21 Phase 1 + 33 Phase 2 + 23 Phase 3) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
122 lines
4.1 KiB
Python
122 lines
4.1 KiB
Python
"""
|
|
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)
|