Fix Loomio sync: use portal DB (OC returns email:null); add manual sync endpoint

This commit is contained in:
inference-bot committed 2026-09-09 14:34:30 -06:00
1 parent 5490eeb2fc
commit 5d3ace8fe8
1 file changed
+30 -19
+30 -19
View File
@@ -182,25 +182,20 @@ async def is_active_member(slug: str) -> bool:
async def get_active_member_emails() -> list[str]: async def get_active_member_emails() -> list[str]:
"""Return the list of active financial contributor emails from Open Collective.""" """Return the list of active member emails from the portal's own database.
query = (
'{ collective(slug: "%s") { members(limit: 100) { nodes { role account { email slug } } } } }' We use the local `members` table (populated during OAuth, which captures the
% OC_COLLECTIVE_SLUG member's email) rather than Open Collective's GraphQL, because OC returns
) `email: null` for privacy — the email field is only exposed via OAuth with
async with httpx.AsyncClient() as client: the user's consent. The local table is the authoritative source of member
r = await client.post(OC_GRAPHQL_URL, json={"query": query}) emails.
if r.status_code != 200: """
logger.warning("OC member list fetch failed: %s", r.status_code) conn = get_db()
return [] rows = conn.execute(
nodes = r.json().get("data", {}).get("collective", {}).get("members", {}).get("nodes", []) "SELECT email FROM members WHERE active = 1"
emails = [] ).fetchall()
for n in nodes: conn.close()
role = n.get("role", "") return [r[0] for r in rows]
acct = n.get("account", {}) or {}
email = acct.get("email")
if role in ("BACKER", "ADMIN") and email:
emails.append(email)
return emails
async def sync_loomio_memberships() -> None: async def sync_loomio_memberships() -> None:
@@ -739,6 +734,22 @@ async def health():
return {"status": "ok"} return {"status": "ok"}
@app.post("/admin/sync-loomio/{token}")
async def admin_sync_loomio(token: str):
"""Manually trigger a Loomio membership sync.
Protected by the same secret token as the Open Collective webhook. Useful
for reconciling pre-existing members (accounts created before the sync
existed) or recovering from a missed webhook. Idempotent — safe to call
repeatedly.
"""
if not WEBHOOK_TOKEN or not secrets.compare_digest(token, WEBHOOK_TOKEN):
raise HTTPException(401, "Invalid token")
await sync_loomio_memberships()
return {"status": "synced"}
@app.get("/") @app.get("/")
async def index(): async def index():
return {"service": "Inference Cooperative Member Portal", "version": "0.1.0"} return {"service": "Inference Cooperative Member Portal", "version": "0.1.0"}