# Ghost webhooks — neutered (Phase 1). # # Post/page/author/tag handlers return 204 no-op. # Member webhook remains active (membership sync handled by account service). from __future__ import annotations import os from quart import Blueprint, request, abort, Response from shared.browser.app.csrf import csrf_exempt ghost_webhooks = Blueprint("ghost_webhooks", __name__, url_prefix="/__ghost-webhook") def _check_secret(req) -> None: expected = os.getenv("GHOST_WEBHOOK_SECRET") if not expected: return got = req.args.get("secret") or req.headers.get("X-Webhook-Secret") if got != expected: abort(401) def _extract_id(data: dict, key: str) -> str | None: block = data.get(key) or {} cur = block.get("current") or {} prev = block.get("previous") or {} return cur.get("id") or prev.get("id") @csrf_exempt @ghost_webhooks.route("/member/", methods=["POST"]) async def webhook_member() -> Response: """Member webhook still active — delegates to account service.""" _check_secret(request) data = await request.get_json(force=True, silent=True) or {} ghost_id = _extract_id(data, "member") if not ghost_id: abort(400, "no member id") from shared.infrastructure.actions import call_action try: await call_action( "account", "ghost-sync-member", payload={"ghost_id": ghost_id}, timeout=30.0, ) except Exception as e: import logging logging.getLogger(__name__).error("Member sync via account failed: %s", e) return Response(status=204) # --- Neutered handlers: Ghost no longer writes content --- @csrf_exempt @ghost_webhooks.post("/post/") async def webhook_post() -> Response: return Response(status=204) @csrf_exempt @ghost_webhooks.post("/page/") async def webhook_page() -> Response: return Response(status=204) @csrf_exempt @ghost_webhooks.post("/author/") async def webhook_author() -> Response: return Response(status=204) @csrf_exempt @ghost_webhooks.post("/tag/") async def webhook_tag() -> Response: return Response(status=204)