From 492019088e39d69be04f0e6be44ee19d5194146c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 26 Oct 2026 16:19:42 +0000 Subject: [PATCH] loomio: only rename auto-generated handles, never one a human chose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Testing the previous commit against the live Loomio user list showed it would also have renamed a member whose handle was chosen by hand (a working name would have become the sanitized OIDC uid) — the uid is domain-shaped, so the sanitizer produced a different, wrong target. The function is scoped to Loomio's own auto-generated handles: the email local part with non-alphanumerics stripped plus a random suffix. Gate on that shape. Verified against the live user list (70 users) before building: exactly one handle changes — the member whose username fix 422'd on every sweep — no computed target violates Loomio's rule, and the hand-chosen handle is left alone. --- app/main.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/main.py b/app/main.py index 4c6a8ad..b397cfd 100644 --- a/app/main.py +++ b/app/main.py @@ -1007,6 +1007,20 @@ def _loomio_safe_username(raw) -> str | None: return u if len(u) >= 2 else None +def _loomio_is_autogenerated(current: str, email: str) -> bool: + """Is this handle one Loomio generated for us? + + Loomio derives a handle from the email local part with non-alphanumerics + stripped, plus a random suffix ("joshuaztan" + "xjan5rzb8j27" for + joshua.z.tan@...). Anything that is not that shape — "amarino", say — was + chosen by a person, and renaming it would be an unrequested member-visible + change. Only auto-generated handles are in scope for this fix. + """ + local = re.sub(r"[^a-z0-9]", "", (email or "").split("@")[0].lower()) + cur = (current or "").lower() + return bool(local) and cur.startswith(local) and len(cur) > len(local) + + async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict: """Rename Loomio users with auto-generated handles to their Cloudron username. @@ -1023,6 +1037,11 @@ async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict: fixed, skipped = 0, 0 for u in users: + # Only Loomio's own auto-generated handles are in scope: a name a human + # chose stays, even when it differs from the Cloudron username. + if not _loomio_is_autogenerated(u.get("username") or "", u.get("email") or ""): + skipped += 1 + continue target = None for ident in u.get("identities") or []: if ident.get("identity_type") == "oauth" and ident.get("uid"):