loomio: only rename auto-generated handles, never one a human chose

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.
This commit is contained in:
Ubuntu committed 2026-10-26 16:19:42 +00:00
1 parent 178a4aa360
commit 492019088e
1 file changed
+19
+19
View File
@@ -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"):