Files
StarPunk/starpunk/routes/micropub.py
Phil Skentelbery d8828fb6c6 feat: Implement Micropub endpoint for creating posts (Phase 3)
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>
2025-11-24 12:33:39 -07:00

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)