Files
mono/blog/bp/blog/web_hooks/routes.py
giles f42042ccb7 Monorepo: consolidate 7 repos into one
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
2026-02-24 19:44:17 +00:00

121 lines
3.7 KiB
Python

# suma_browser/webhooks.py
from __future__ import annotations
import os
from quart import Blueprint, request, abort, Response, g
from ..ghost.ghost_sync import (
sync_single_member,
sync_single_page,
sync_single_post,
sync_single_author,
sync_single_tag,
)
from shared.browser.app.redis_cacher import clear_cache
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:
# if you don't set a secret, we allow anything (dev mode)
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:
"""
key is "post", "tag", or "user"/"author".
Ghost usually sends { key: { current: { id: ... }, previous: { id: ... } } }
We'll try current first, then previous.
"""
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"])
#@ghost_webhooks.post("/member/")
async def webhook_member() -> Response:
_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")
# sync one post
#async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py
await sync_single_member(g.s, ghost_id)
return Response(status=204)
@csrf_exempt
@ghost_webhooks.post("/post/")
@clear_cache(tag='blog')
async def webhook_post() -> Response:
_check_secret(request)
data = await request.get_json(force=True, silent=True) or {}
ghost_id = _extract_id(data, "post")
if not ghost_id:
abort(400, "no post id")
# sync one post
#async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py
await sync_single_post(g.s, ghost_id)
return Response(status=204)
@csrf_exempt
@ghost_webhooks.post("/page/")
@clear_cache(tag='blog')
async def webhook_page() -> Response:
_check_secret(request)
data = await request.get_json(force=True, silent=True) or {}
ghost_id = _extract_id(data, "page")
if not ghost_id:
abort(400, "no page id")
# sync one post
#async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"] # we'll set this in app.py
await sync_single_page(g.s, ghost_id)
return Response(status=204)
@csrf_exempt
@ghost_webhooks.post("/author/")
@clear_cache(tag='blog')
async def webhook_author() -> Response:
_check_secret(request)
data = await request.get_json(force=True, silent=True) or {}
# Ghost calls them "user" in webhook payload in many versions,
# and you want authors in your mirror. We'll try both keys.
ghost_id = _extract_id(data, "user") or _extract_id(data, "author")
if not ghost_id:
abort(400, "no author id")
#async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"]
await sync_single_author(g.s, ghost_id)
return Response(status=204)
@csrf_exempt
@ghost_webhooks.post("/tag/")
@clear_cache(tag='blog')
async def webhook_tag() -> Response:
_check_secret(request)
data = await request.get_json(force=True, silent=True) or {}
ghost_id = _extract_id(data, "tag")
if not ghost_id:
abort(400, "no tag id")
#async_session_factory = request.app.config["ASYNC_SESSION_FACTORY"]
await sync_single_tag(g.s, ghost_id)
return Response(status=204)