diff --git a/app/main.py b/app/main.py index deb3242..fd3e421 100644 --- a/app/main.py +++ b/app/main.py @@ -923,6 +923,10 @@ async def sync_matrix_space() -> dict: headers=H) invited = {m["state_key"] for m in r.json().get("chunk", [])} if r.status_code == 200 else set() + # One identity per member: when a member sets a dashboard extra MXID, + # their auto-generated local account (@username:inference.coop) should + # NOT also sit in the Space. Track the desired set, then retire the + # local accounts of members whose dashboard MXID replaced them. invited_count = 0 async for email, mxid in matrix_member_mxids(client): if not mxid or mxid in joined or mxid in invited: @@ -933,6 +937,56 @@ async def sync_matrix_space() -> dict: invited_count += 1 if invited_count: logger.info("Matrix space invites sent: %s", invited_count) + + # Retire local accounts: for each member with a dashboard-set external + # MXID, kick their @username:inference.coop account if it's currently + # in the Space. Only ever touches accounts derived from that member's + # own Cloudron username — never other members' or federated accounts. + conn = get_db() + rows = conn.execute( + "SELECT email, matrix_mxid FROM members WHERE active = 1 AND matrix_mxid IS NOT NULL AND matrix_mxid != ''" + ).fetchall() + conn.close() + users: dict[str, str] = {} + page = 1 + async with httpx.AsyncClient(timeout=30) as uc: + while True: + r = await uc.get( + f"{CLOUDRON_API}/api/v1/users", + params={"page": page}, + headers=cloudron_headers(), + ) + if r.status_code != 200: + break + batch = r.json().get("users", []) + for u in batch: + if u.get("email") and u.get("username"): + users[u["email"].lower()] = u["username"] + if len(batch) < 25: + break + page += 1 + retired = 0 + for email, ext_mxid in rows: + username = users.get(email.lower()) + if not username: + continue + local_mxid = f"@{username}:inference.coop" + if local_mxid == ext_mxid: + continue # dashboard MXID IS the local account — nothing to retire + if local_mxid in joined: + for room in [MATRIX_SPACE_ID] + ([MATRIX_GENERAL_ROOM] if MATRIX_GENERAL_ROOM else []): + import urllib.parse + enc_room = urllib.parse.quote(room, safe="") + await client.post( + f"{MATRIX_HOMESERVER}/_matrix/client/v3/rooms/{enc_room}/kick", + headers=H, + json={"user_id": local_mxid, + "reason": f"This co-op account is superseded by {ext_mxid} (set on your dashboard)"}, + ) + retired += 1 + logger.info("Matrix sync: retired local account %s (member uses %s)", local_mxid, ext_mxid) + if retired: + logger.info("Matrix sync: retired %s local account(s)", retired) return {"invited": invited_count}