surfaces/bot-examples/matrix_main.py
Mikhail Putilovskij 6ced154124 feat(matrix): land QA follow-ups and refresh docs
- harden Matrix onboarding/chat lifecycle after manual QA
- refresh README and Matrix docs to match current behavior
- add local ignores for runtime artifacts and include current planning/report docs

Closes #7
Closes #9
Closes #14
2026-04-05 19:08:58 +03:00

123 lines
3.8 KiB
Python

"""Entry point for Matrix bot frontend."""
import asyncio
import logging
import os
import sys
from pathlib import Path
import httpx
import yaml
from core.config import Config
from core.matrix_bot import MatrixBot
def _load_dotenv(workspace: Path) -> None:
env_file = workspace / ".env"
if not env_file.exists():
return
for line in env_file.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
if key not in os.environ:
os.environ[key] = value
def _load_users(workspace: Path) -> dict[str, dict]:
"""Load users.yml from workspace. Returns {mxid: {profile: ...}}."""
users_file = workspace / "users.yml"
if not users_file.exists():
return {}
with open(users_file) as f:
data = yaml.safe_load(f) or {}
return data
async def main() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(name)s %(levelname)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
workspace_dir = os.environ.get("WORKSPACE_DIR")
if workspace_dir:
_load_dotenv(Path(workspace_dir))
# MATRIX_DATA_DIR overrides DATA_DIR for Matrix bot
matrix_data_dir = os.environ.get("MATRIX_DATA_DIR")
if matrix_data_dir:
os.environ["DATA_DIR"] = matrix_data_dir
# Matrix-specific env vars
homeserver = os.environ.get("MATRIX_HOMESERVER")
user_id = os.environ.get("MATRIX_USER_ID")
access_token = os.environ.get("MATRIX_ACCESS_TOKEN")
owner_mxid = os.environ.get("MATRIX_OWNER_MXID", "")
admin_mxid = os.environ.get("MATRIX_ADMIN_MXID", "") # For admin notifications
if not all([homeserver, user_id, access_token]):
logging.error(
"Missing Matrix config. Need: MATRIX_HOMESERVER, MATRIX_USER_ID, "
"MATRIX_ACCESS_TOKEN"
)
sys.exit(1)
# Resolve device_id from server (must match access token)
async with httpx.AsyncClient() as http:
resp = await http.get(
f"{homeserver}/_matrix/client/v3/account/whoami",
headers={"Authorization": f"Bearer {access_token}"},
timeout=10,
)
if resp.status_code != 200:
logging.error("whoami failed (%d): %s", resp.status_code, resp.text)
sys.exit(1)
device_id = resp.json().get("device_id")
logging.info("Resolved device_id: %s", device_id)
# Load users map (multi-user mode)
users = {}
if workspace_dir:
users = _load_users(Path(workspace_dir))
if not users and not owner_mxid:
logging.error("Need either users.yml in workspace or MATRIX_OWNER_MXID env var")
sys.exit(1)
try:
config = Config.from_env()
except ValueError as e:
logging.error("Config error: %s", e)
sys.exit(1)
if config.workspace_dir:
logging.info("Workspace: %s", config.workspace_dir)
# Symlink workspace CLAUDE.md into data dir
claude_md_link = config.data_dir / "CLAUDE.md"
claude_md_src = config.workspace_dir / "CLAUDE.md"
if claude_md_src.exists() and not claude_md_link.exists():
claude_md_link.symlink_to(claude_md_src)
logging.info("Symlinked CLAUDE.md into data dir")
if users:
logging.info("Multi-user mode: %d users", len(users))
logging.info("Data dir: %s", config.data_dir)
bot = MatrixBot(config, homeserver, user_id, access_token,
owner_mxid=owner_mxid, users=users, device_id=device_id,
admin_mxid=admin_mxid)
try:
await bot.run()
except KeyboardInterrupt:
pass
finally:
await bot.close()
if __name__ == "__main__":
asyncio.run(main())