- 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
123 lines
3.8 KiB
Python
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())
|