Files
Prad NukalaandClaude Fable 5 d57086cd00 feat: unified messaging TUI MVP — Synapse + 7 mautrix bridges + Textual TUI + MCP draft staging
One-command local stack: cp .env.example .env && make up. Idempotent
bootstrap renders Synapse + bridge configs from pristine upstream examples,
provisions databases, registrations, double puppeting, and the admin user.
Textual TUI with bridge manager, in-terminal QR login, and drafts panel.
MCP server exposes read tools and a human-gated draft send workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 12:13:13 -04:00

83 lines
2.9 KiB
Python

"""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")