mirror of
https://github.com/prdlk/vortex.git
synced 2026-08-03 01:41:52 +00:00
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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user