Loomio sync: only invite activated members (avoid invite email before account setup)

This commit is contained in:
Inferencebot committed 2026-10-07 09:19:24 -06:00
1 parent 38005127cb
commit 3d258e9a2d
1 file changed
+43
+43
View File
@@ -976,6 +976,14 @@ async def sync_loomio_memberships() -> None:
Uses POST /api/b2/memberships with remove_absent=1, which adds new members
and removes anyone not in the list. Operator accounts (LOOMIO_ALWAYS_KEEP)
are always included so they are never removed.
Only members whose Cloudron account is already set up (inviteAccepted) are
invited. Loomio's b2/memberships endpoint has no "silent invite" flag — it
unconditionally emails a membership_created notification — so inviting a
not-yet-activated member sends them a Loomio invite email before they've
even created their account. Filtering to activated members prevents that;
unactivated members are simply picked up on a later sync once they finish
account setup.
"""
if not LOOMIO_API_KEY or not LOOMIO_GROUP_ID:
logger.warning("Loomio not configured; skipping membership sync")
@@ -987,6 +995,12 @@ async def sync_loomio_memberships() -> None:
if keep not in emails:
emails.append(keep)
# Only invite members who have completed Cloudron account setup. The
# operator accounts (LOOMIO_ALWAYS_KEEP) bypass this filter — they must
# stay in the group regardless of inviteAccepted (the bootstrap owner
# account shows inviteAccepted: false forever even though it's functional).
emails = await filter_loomio_activated(emails)
async with httpx.AsyncClient() as client:
r = await client.post(
f"{LOOMIO_BASE}/api/b2/memberships",
@@ -1014,6 +1028,35 @@ async def sync_loomio_memberships() -> None:
logger.warning("Loomio username fix pass failed: %s", e)
async def filter_loomio_activated(emails: list[str]) -> list[str]:
"""Return the subset of `emails` whose Cloudron account is set up, plus any
operator accounts (LOOMIO_ALWAYS_KEEP), which must always pass through.
Fetches the Cloudron user list once and builds an inviteAccepted set keyed
by email. Members who are provisioned but not yet activated are dropped so
Loomio doesn't email them an invite before they've set up their account.
"""
activated: set[str] = set()
try:
async with httpx.AsyncClient(timeout=15.0) as client:
r = await client.get(
f"{CLOUDRON_API}/api/v1/users?per_page=100",
headers=cloudron_headers(),
)
r.raise_for_status()
for u in r.json().get("users", []):
if u.get("inviteAccepted") and u.get("email"):
activated.add(u["email"])
except Exception as e:
# If we can't determine activation state, fail open (invite everyone)
# rather than silently dropping members from Loomio.
logger.warning("filter_loomio_activated: cloudron users fetch failed: %s", e)
return emails
keep = set(LOOMIO_ALWAYS_KEEP)
return [e for e in emails if e in keep or e in activated]
def store_member(email: str, key_token: str, cloudron_user_id: str, slug: str = "") -> None:
conn = get_db()
# Preserve an existing slug if the caller passed none — re-provisioning an