loomio: sanitize usernames instead of falling back to another invalid one

The username fix rejected a dot-containing OAuth uid and then substituted the
email local part, which carries the same dots — so Loomio answered 422
("Usernames must contain lower case letters, numbers, underscores, and internal
hyphens only") on every sweep, for the one member whose Cloudron username has a
dot. Normalize instead: disallowed characters become underscores, and leading
and trailing hyphens/underscores are stripped. Also log the Loomio user id
rather than the member's email.
This commit is contained in:
inference-bot committed 2026-10-26 16:07:23 +00:00
1 parent e24c272e2e
commit 178a4aa360
1 file changed
+21 -11
+21 -11
View File
@@ -995,6 +995,18 @@ def set_member_matrix_mxid(email: str, mxid: str) -> None:
conn.close() 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: async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict:
"""Rename Loomio users with auto-generated handles to their Cloudron username. """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 target = None
for ident in u.get("identities") or []: for ident in u.get("identities") or []:
if ident.get("identity_type") == "oauth" and ident.get("uid"): if ident.get("identity_type") == "oauth" and ident.get("uid"):
uid = ident["uid"].lower() target = _loomio_safe_username(ident["uid"])
# Loomio usernames allow lowercase letters, numbers, underscores and hyphens. if not target:
if uid.replace("_", "").replace("-", "").isalnum() and len(uid) >= 2: # uid unusable even after sanitizing — fall back to the email local part
target = uid target = _loomio_safe_username((u.get("email") or "").split("@")[0])
break
# uid unusable (dots etc) — use email local-part instead
target = (u.get("email") or "").split("@")[0].lower() or None
break break
if not target and u.get("email") and "@" in u["email"]: if not target:
target = u["email"].split("@")[0].lower() target = _loomio_safe_username((u.get("email") or "").split("@")[0])
if not target or u.get("username") == target: if not target or u.get("username") == target:
skipped += 1 skipped += 1
continue continue
@@ -1034,8 +1043,9 @@ async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict:
if rr.status_code == 200: if rr.status_code == 200:
fixed += 1 fixed += 1
else: else:
logger.warning("Loomio username fix for %s failed: %s %s", # log the Loomio user id, never the member's email
u.get("email"), rr.status_code, rr.text[:120]) logger.warning("Loomio username fix for user %s failed: %s %s",
u.get("id"), rr.status_code, rr.text[:120])
if fixed: if fixed:
logger.info("Loomio usernames fixed: %s", fixed) logger.info("Loomio usernames fixed: %s", fixed)
return {"fixed": fixed, "skipped": skipped} return {"fixed": fixed, "skipped": skipped}