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.
|
# used to throttle re-invites of members who never activated.
|
||||||
if "welcome_sent_at" not in cols:
|
if "welcome_sent_at" not in cols:
|
||||||
conn.execute("ALTER TABLE members ADD COLUMN welcome_sent_at TEXT")
|
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).
|
# Migration: credit_balance — purchased, NON-EXPIRING credits (USD).
|
||||||
# Distinct from `balance` (the monthly allowance, which resets): credits
|
# Distinct from `balance` (the monthly allowance, which resets): credits
|
||||||
# carry over indefinitely and are spent only after the allowance is
|
# 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]
|
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:
|
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.
|
||||||
|
|
||||||
@@ -922,6 +949,15 @@ async def matrix_member_mxids(client: httpx.AsyncClient):
|
|||||||
break
|
break
|
||||||
page += 1
|
page += 1
|
||||||
for email in await get_active_member_emails():
|
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())
|
username = users.get(email.lower())
|
||||||
if username:
|
if username:
|
||||||
mxid = f"@{username}:inference.coop"
|
mxid = f"@{username}:inference.coop"
|
||||||
@@ -2332,6 +2368,59 @@ async def broker_revoke(name: str, request: Request):
|
|||||||
return {"status": "revoked", "name": name}
|
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:
|
async def broker_get_usage(email: str, days: int = 0) -> dict:
|
||||||
"""Return the member's spend vs. balance, plus per-model and token detail.
|
"""Return the member's spend vs. balance, plus per-model and token detail.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user