Matrix: member-set extra MXID (matrix_mxid column + broker get/set + sync prefers it)
This commit is contained in:
1 parent
3cdc250d52
commit
563881d63c
1 file changed
+89
+89
@@ -451,6 +451,12 @@ def get_db() -> sqlite3.Connection:
|
||||
# used to throttle re-invites of members who never activated.
|
||||
if "welcome_sent_at" not in cols:
|
||||
conn.execute("ALTER TABLE members ADD COLUMN welcome_sent_at TEXT")
|
||||
# Migration: matrix_mxid — an OPTIONAL extra Matrix account a member wants
|
||||
# invited to the co-op Space (e.g. their existing federated account on
|
||||
# another homeserver). Empty = only the local @username:inference.coop
|
||||
# account is invited. At most one extra MXID per member.
|
||||
if "matrix_mxid" not in cols:
|
||||
conn.execute("ALTER TABLE members ADD COLUMN matrix_mxid TEXT")
|
||||
# Migration: credit_balance — purchased, NON-EXPIRING credits (USD).
|
||||
# Distinct from `balance` (the monthly allowance, which resets): credits
|
||||
# carry over indefinitely and are spent only after the allowance is
|
||||
@@ -787,6 +793,27 @@ async def get_active_member_emails() -> list[str]:
|
||||
return [r[0] for r in rows]
|
||||
|
||||
|
||||
def get_member_matrix_mxid(email: str) -> str:
|
||||
"""Return the member's optional extra Matrix MXID, or "" if unset."""
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT matrix_mxid FROM members WHERE email = ?", (email,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
return (row[0] or "").strip() if row else ""
|
||||
|
||||
|
||||
def set_member_matrix_mxid(email: str, mxid: str) -> None:
|
||||
"""Set (or clear) the member's optional extra Matrix MXID."""
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"UPDATE members SET matrix_mxid = ? WHERE email = ?",
|
||||
(mxid.strip() if mxid else "", email),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
async def loomio_fix_usernames(client: httpx.AsyncClient) -> dict:
|
||||
"""Rename Loomio users with auto-generated handles to their Cloudron username.
|
||||
|
||||
@@ -922,6 +949,15 @@ async def matrix_member_mxids(client: httpx.AsyncClient):
|
||||
break
|
||||
page += 1
|
||||
for email in await get_active_member_emails():
|
||||
# A member's own extra MXID (set on the dashboard) takes precedence —
|
||||
# that's the account they want in the Space. Otherwise use their local
|
||||
# @username:inference.coop account.
|
||||
own = get_member_matrix_mxid(email)
|
||||
if own:
|
||||
if own not in seen:
|
||||
seen.add(own)
|
||||
yield email, own
|
||||
continue
|
||||
username = users.get(email.lower())
|
||||
if username:
|
||||
mxid = f"@{username}:inference.coop"
|
||||
@@ -2332,6 +2368,59 @@ async def broker_revoke(name: str, request: Request):
|
||||
return {"status": "revoked", "name": name}
|
||||
|
||||
|
||||
# Matrix MXID (self-serve federated identity). A member can attach ONE extra
|
||||
# Matrix account (e.g. their existing @user:matrix.org) to be invited to the
|
||||
# co-op Space. Empty string clears it. Validation is loose on purpose — Synapse
|
||||
# will simply reject a malformed MXID on invite, and we log that.
|
||||
_MXID_RE = re.compile(r"^@[^\s:]+:[^\s]+\.[^\s]+$")
|
||||
|
||||
|
||||
@app.get("/broker/matrix-mxid")
|
||||
@limiter.limit("30/minute")
|
||||
async def broker_get_matrix_mxid(request: Request):
|
||||
email = _verify_broker_secret(request)
|
||||
return {"matrix_mxid": get_member_matrix_mxid(email), "local_mxid": local_mxid_for_email(email)}
|
||||
|
||||
|
||||
async def local_mxid_for_email(email: str) -> str:
|
||||
"""Resolve a member's local Matrix ID (@username:inference.coop), or '' if unknown."""
|
||||
import urllib.request
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{CLOUDRON_API}/api/v1/users?per_page=100",
|
||||
headers={"Authorization": f"Bearer {CLOUDRON_TOKEN}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
users = json.loads(r.read().decode()).get("users", [])
|
||||
for u in users:
|
||||
if (u.get("email") or "").lower() == email and u.get("username"):
|
||||
return f"@{u['username']}:inference.coop"
|
||||
except Exception as e:
|
||||
logger.warning("local_mxid_for_email lookup failed: %s", e)
|
||||
return ""
|
||||
|
||||
|
||||
@app.post("/broker/matrix-mxid")
|
||||
@limiter.limit("30/minute")
|
||||
async def broker_set_matrix_mxid(request: Request):
|
||||
email = _verify_broker_secret(request)
|
||||
body = await request.json()
|
||||
mxid = (body.get("matrix_mxid") or "").strip()
|
||||
if mxid and not _MXID_RE.match(mxid):
|
||||
return JSONResponse(
|
||||
{"error": "Enter a Matrix ID like @username:server.org"}, status_code=400
|
||||
)
|
||||
set_member_matrix_mxid(email, mxid)
|
||||
# Invite the new account into the Space immediately (best-effort; the daily
|
||||
# sync will also pick it up). Clear = no invite (just removes the mapping).
|
||||
if mxid:
|
||||
try:
|
||||
await sync_matrix_space()
|
||||
except Exception as e:
|
||||
logger.warning("matrix-mxid: immediate invite failed: %s", e)
|
||||
return {"status": "saved", "matrix_mxid": mxid}
|
||||
|
||||
|
||||
async def broker_get_usage(email: str, days: int = 0) -> dict:
|
||||
"""Return the member's spend vs. balance, plus per-model and token detail.
|
||||
|
||||
|
||||
Reference in new issue
Block a user