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>
This commit is contained in:
121
starpunk/routes/micropub.py
Normal file
121
starpunk/routes/micropub.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user