"""Credentials loading and Matrix client construction.""" import json import os import sys import time from pathlib import Path from nio import AsyncClient, AsyncClientConfig, LoginResponse def store_dir() -> Path: return Path(os.environ.get("STORE_DIR", "/shared/tui-store")) def load_credentials(retries: int = 10, delay: float = 3.0) -> dict: """Read CREDENTIALS_FILE, waiting a bit in case bootstrap is still provisioning.""" path = Path(os.environ.get("CREDENTIALS_FILE", "/shared/credentials.json")) for attempt in range(retries): try: return json.loads(path.read_text()) except (OSError, json.JSONDecodeError): if attempt < retries - 1: print( f"vortex-tui: waiting for {path} (bootstrap still provisioning?)...", file=sys.stderr, ) time.sleep(delay) sys.exit( f"vortex-tui: credentials file {path} missing or invalid after " f"{retries} attempts. Run the bootstrap first, or set CREDENTIALS_FILE." ) async def build_client(creds: dict) -> AsyncClient: """Return a logged-in AsyncClient with its own persisted E2EE device. Order: STORE_DIR/session.json (our own device) -> password login (persist new device) -> bootstrap access_token fallback. """ store = store_dir() store.mkdir(parents=True, exist_ok=True) homeserver = ( os.environ.get("MATRIX_HOMESERVER") or creds.get("homeserver") or "http://localhost:8008" ) user_id = creds["user_id"] # encryption_enabled defaults to True only when olm is importable, # so this works with or without the [e2e] extra installed. client = AsyncClient( homeserver, user_id, store_path=str(store), config=AsyncClientConfig(store_sync_tokens=True), ) session_file = store / "session.json" if session_file.exists(): s = json.loads(session_file.read_text()) client.restore_login(s["user_id"], s["device_id"], s["access_token"]) return client if creds.get("password"): resp = await client.login(creds["password"], device_name="vortex-tui") if isinstance(resp, LoginResponse): session_file.write_text( json.dumps( { "user_id": resp.user_id, "device_id": resp.device_id, "access_token": resp.access_token, } ) ) return client if creds.get("access_token"): # Fall back to the bootstrap token (shares the bootstrap device). client.restore_login( user_id, creds.get("device_id") or "VORTEXTUI", creds["access_token"] ) return client await client.close() raise RuntimeError("no usable password or access_token in credentials file")