Files
timmy-config/infra/matrix/scripts/bootstrap-fleet-rooms.py
Ezra (Archivist) 1411fded99 [BURN] Matrix scaffold verification, room bootstrap automation, cutover plan
- Verify #183 scaffold completeness (MATRIX_SCAFFOLD_VERIFICATION.md)
- Add bootstrap-fleet-rooms.py for automated Matrix room creation (#166)
- Add CUTOVER_PLAN.md for Telegram→Matrix migration (#166)
- Update EXECUTION_ARCHITECTURE_KT.md with new automation references

Progresses #166, verifies #183
2026-04-05 18:42:03 +00:00

225 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""bootstrap-fleet-rooms.py — Automate Matrix room creation for Timmy fleet.
Issue: #166 (timmy-config)
Usage:
export MATRIX_HOMESERVER=https://matrix.timmytime.net
export MATRIX_ADMIN_TOKEN=<your_access_token>
python3 bootstrap-fleet-rooms.py --create-all --dry-run
Requires only Python stdlib (no heavy SDK dependencies).
"""
import argparse
import json
import os
import sys
import urllib.request
from typing import Optional, List, Dict
class MatrixAdminClient:
"""Lightweight Matrix Client-Server API client."""
def __init__(self, homeserver: str, access_token: str):
self.homeserver = homeserver.rstrip("/")
self.access_token = access_token
def _request(self, method: str, path: str, data: Optional[Dict] = None) -> Dict:
url = f"{self.homeserver}/_matrix/client/v3{path}"
req = urllib.request.Request(url, method=method)
req.add_header("Authorization", f"Bearer {self.access_token}")
req.add_header("Content-Type", "application/json")
body = json.dumps(data).encode() if data else None
try:
with urllib.request.urlopen(req, data=body, timeout=30) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
try:
err = json.loads(e.read().decode())
except Exception:
err = {"error": str(e)}
return {"error": err, "status": e.code}
except Exception as e:
return {"error": str(e)}
def whoami(self) -> Dict:
return self._request("GET", "/account/whoami")
def create_room(self, name: str, topic: str, preset: str = "private_chat",
invite: Optional[List[str]] = None) -> Dict:
payload = {
"name": name,
"topic": topic,
"preset": preset,
"creation_content": {"m.federate": False},
}
if invite:
payload["invite"] = invite
return self._request("POST", "/createRoom", payload)
def send_state_event(self, room_id: str, event_type: str, state_key: str,
content: Dict) -> Dict:
path = f"/rooms/{room_id}/state/{event_type}/{state_key}"
return self._request("PUT", path, content)
def enable_encryption(self, room_id: str) -> Dict:
return self.send_state_event(
room_id, "m.room.encryption", "",
{"algorithm": "m.megolm.v1.aes-sha2"}
)
def set_room_avatar(self, room_id: str, url: str) -> Dict:
return self.send_state_event(
room_id, "m.room.avatar", "", {"url": url}
)
def generate_invite_link(self, room_id: str) -> str:
"""Generate a matrix.to invite link."""
localpart = room_id.split(":")[0].lstrip("#")
server = room_id.split(":")[1]
return f"https://matrix.to/#/{room_id}?via={server}"
def print_result(label: str, result: Dict):
if "error" in result:
print(f"{label}: {result['error']}")
else:
print(f"{label}: {json.dumps(result, indent=2)[:200]}")
def main():
parser = argparse.ArgumentParser(description="Bootstrap Matrix rooms for Timmy fleet")
parser.add_argument("--homeserver", default=os.environ.get("MATRIX_HOMESERVER", ""),
help="Matrix homeserver URL (default: MATRIX_HOMESERVER env)")
parser.add_argument("--token", default=os.environ.get("MATRIX_ADMIN_TOKEN", ""),
help="Admin access token (default: MATRIX_ADMIN_TOKEN env)")
parser.add_argument("--operator-user", default="@alexander:matrix.timmytime.net",
help="Operator Matrix user ID")
parser.add_argument("--domain", default="matrix.timmytime.net",
help="Server domain for room aliases")
parser.add_argument("--create-all", action="store_true",
help="Create all standard fleet rooms")
parser.add_argument("--dry-run", action="store_true",
help="Preview actions without executing API calls")
args = parser.parse_args()
if not args.homeserver or not args.token:
print("Error: --homeserver and --token are required (or set env vars).")
sys.exit(1)
if args.dry_run:
print("=" * 60)
print(" DRY RUN — No API calls will be made")
print("=" * 60)
print(f"Homeserver: {args.homeserver}")
print(f"Operator: {args.operator_user}")
print(f"Domain: {args.domain}")
print("\nPlanned rooms:")
rooms = [
("Fleet Operations", "Encrypted command room for Alexander and agents.", "#fleet-ops"),
("General Chat", "Open fleet chatter and status updates.", "#fleet-general"),
("Alerts", "Automated alerts and monitoring notifications.", "#fleet-alerts"),
]
for name, topic, alias in rooms:
print(f" - {name} ({alias}:{args.domain})")
print(f" Topic: {topic}")
print(f" Actions: create → enable encryption → set alias")
print("\nNext steps after real run:")
print(" 1. Open Element Web and join with your operator account")
print(" 2. Share room invite links with fleet agents")
print(" 3. Configure Hermes gateway Matrix adapter")
return
client = MatrixAdminClient(args.homeserver, args.token)
print("Verifying credentials...")
identity = client.whoami()
if "error" in identity:
print(f"Authentication failed: {identity['error']}")
sys.exit(1)
print(f"Authenticated as: {identity.get('user_id', 'unknown')}")
rooms_spec = [
{
"name": "Fleet Operations",
"topic": "Encrypted command room for Alexander and agents. | Issue #166",
"alias": f"#fleet-ops:{args.domain}",
"preset": "private_chat",
},
{
"name": "General Chat",
"topic": "Open fleet chatter and status updates. | Issue #166",
"alias": f"#fleet-general:{args.domain}",
"preset": "public_chat",
},
{
"name": "Alerts",
"topic": "Automated alerts and monitoring notifications. | Issue #166",
"alias": f"#fleet-alerts:{args.domain}",
"preset": "private_chat",
},
]
created_rooms = []
for spec in rooms_spec:
print(f"\nCreating room: {spec['name']}...")
result = client.create_room(
name=spec["name"],
topic=spec["topic"],
preset=spec["preset"],
)
if "error" in result:
print_result("Create room", result)
continue
room_id = result.get("room_id")
print(f" ✅ Room created: {room_id}")
# Enable encryption
enc = client.enable_encryption(room_id)
print_result("Enable encryption", enc)
# Set canonical alias
alias_result = client.send_state_event(
room_id, "m.room.canonical_alias", "",
{"alias": spec["alias"]}
)
print_result("Set alias", alias_result)
# Set join rules (restricted for ops/alerts, public for general)
join_rule = "invite" if spec["preset"] == "private_chat" else "public"
jr = client.send_state_event(
room_id, "m.room.join_rules", "",
{"join_rule": join_rule}
)
print_result(f"Set join_rule={join_rule}", jr)
invite_link = client.generate_invite_link(room_id)
created_rooms.append({
"name": spec["name"],
"room_id": room_id,
"alias": spec["alias"],
"invite_link": invite_link,
})
print("\n" + "=" * 60)
print(" BOOTSTRAP COMPLETE")
print("=" * 60)
for room in created_rooms:
print(f"\n{room['name']}")
print(f" Alias: {room['alias']}")
print(f" Room ID: {room['room_id']}")
print(f" Invite: {room['invite_link']}")
print("\nNext steps:")
print(" 1. Join rooms from Element Web as operator")
print(" 2. Pin Fleet Operations as primary room")
print(" 3. Configure Hermes Matrix gateway with room aliases")
print(" 4. Follow docs/matrix-fleet-comms/CUTOVER_PLAN.md for Telegram transition")
if __name__ == "__main__":
main()