Decouple cross-domain DB queries for per-app database split
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m2s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 6m2s
Move Ghost membership sync from blog to account service so blog no longer queries account tables (users, ghost_labels, etc.). Account runs membership sync at startup and exposes HTTP action/data endpoints for webhook-triggered syncs and user lookups. Key changes: - account/services/ghost_membership.py: all membership sync functions - account/bp/actions + data: ghost-sync-member, user-by-email, newsletters - blog ghost_sync.py: stripped to content-only (posts, authors, tags) - blog webhook member: delegates to account via call_action() - try_publish: opens federation session when DBs differ - oauth.py callback: uses get_account_session() for OAuthCode - page_configs moved from db_events to db_blog in split script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -47,14 +47,10 @@ def register(url_prefix, title):
|
||||
|
||||
@blogs_bp.before_app_serving
|
||||
async def init():
|
||||
from .ghost.ghost_sync import (
|
||||
sync_all_content_from_ghost,
|
||||
sync_all_membership_from_ghost,
|
||||
)
|
||||
|
||||
from .ghost.ghost_sync import sync_all_content_from_ghost
|
||||
|
||||
async with get_session() as s:
|
||||
await sync_all_content_from_ghost(s)
|
||||
await sync_all_membership_from_ghost(s)
|
||||
await s.commit()
|
||||
|
||||
@blogs_bp.before_request
|
||||
|
||||
@@ -4,7 +4,6 @@ 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,
|
||||
@@ -18,18 +17,12 @@ ghost_webhooks = Blueprint("ghost_webhooks", __name__, url_prefix="/__ghost-webh
|
||||
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 {}
|
||||
@@ -38,7 +31,6 @@ def _extract_id(data: dict, key: str) -> str | None:
|
||||
|
||||
@csrf_exempt
|
||||
@ghost_webhooks.route("/member/", methods=["POST"])
|
||||
#@ghost_webhooks.post("/member/")
|
||||
async def webhook_member() -> Response:
|
||||
_check_secret(request)
|
||||
|
||||
@@ -47,9 +39,17 @@ async def webhook_member() -> Response:
|
||||
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)
|
||||
# Delegate to account service (membership data lives in db_account)
|
||||
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)
|
||||
|
||||
@csrf_exempt
|
||||
@@ -63,10 +63,8 @@ async def webhook_post() -> Response:
|
||||
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
|
||||
@@ -80,10 +78,8 @@ async def webhook_page() -> Response:
|
||||
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
|
||||
@@ -93,15 +89,12 @@ 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
|
||||
@@ -115,6 +108,5 @@ async def webhook_tag() -> Response:
|
||||
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)
|
||||
|
||||
@@ -465,17 +465,18 @@ def register():
|
||||
@require_post_author
|
||||
async def edit(slug: str):
|
||||
from ...blog.ghost.ghost_posts import get_post_for_edit
|
||||
from shared.models.ghost_membership_entities import GhostNewsletter
|
||||
from sqlalchemy import select as sa_select
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
|
||||
ghost_id = g.post_data["post"]["ghost_id"]
|
||||
is_page = bool(g.post_data["post"].get("is_page"))
|
||||
ghost_post = await get_post_for_edit(ghost_id, is_page=is_page)
|
||||
save_success = request.args.get("saved") == "1"
|
||||
|
||||
newsletters = (await g.s.execute(
|
||||
sa_select(GhostNewsletter).order_by(GhostNewsletter.name)
|
||||
)).scalars().all()
|
||||
# Newsletters live in db_account — fetch via HTTP
|
||||
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
|
||||
# Convert dicts to objects with .name/.ghost_id attributes for template compat
|
||||
from types import SimpleNamespace
|
||||
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
|
||||
|
||||
if not is_htmx_request():
|
||||
html = await render_template(
|
||||
|
||||
Reference in New Issue
Block a user