diff --git a/app/main.py b/app/main.py index 8f26769..4c6a8ad 100644 --- a/app/main.py +++ b/app/main.py @@ -995,6 +995,18 @@ def set_member_matrix_mxid(email: str, mxid: str) -> None: conn.close() +def _loomio_safe_username(raw) -> str | None: + """Sanitize a candidate into a Loomio-legal username. + + Loomio accepts lowercase letters, numbers, underscores and INTERNAL hyphens + only, so a dot-containing candidate — an OAuth uid or an email local part + such as "joshua.z.tan" — must be normalized rather than passed through. + Passing it through is what made this fix fail with HTTP 422 on every sweep. + """ + u = re.sub(r"[^a-z0-9_-]", "_", (raw or "").lower()).strip("-_") + return u if len(u) >= 2 else None + + async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict: """Rename Loomio users with auto-generated handles to their Cloudron username. @@ -1014,16 +1026,13 @@ async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict: target = None for ident in u.get("identities") or []: if ident.get("identity_type") == "oauth" and ident.get("uid"): - uid = ident["uid"].lower() - # Loomio usernames allow lowercase letters, numbers, underscores and hyphens. - if uid.replace("_", "").replace("-", "").isalnum() and len(uid) >= 2: - target = uid - break - # uid unusable (dots etc) — use email local-part instead - target = (u.get("email") or "").split("@")[0].lower() or None + target = _loomio_safe_username(ident["uid"]) + if not target: + # uid unusable even after sanitizing — fall back to the email local part + target = _loomio_safe_username((u.get("email") or "").split("@")[0]) break - if not target and u.get("email") and "@" in u["email"]: - target = u["email"].split("@")[0].lower() + if not target: + target = _loomio_safe_username((u.get("email") or "").split("@")[0]) if not target or u.get("username") == target: skipped += 1 continue @@ -1034,8 +1043,9 @@ async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict: if rr.status_code == 200: fixed += 1 else: - logger.warning("Loomio username fix for %s failed: %s %s", - u.get("email"), rr.status_code, rr.text[:120]) + # log the Loomio user id, never the member's email + logger.warning("Loomio username fix for user %s failed: %s %s", + u.get("id"), rr.status_code, rr.text[:120]) if fixed: logger.info("Loomio usernames fixed: %s", fixed) return {"fixed": fixed, "skipped": skipped}