#!/usr/bin/env python3 """ Mock Synapse server for testing MAS UI without a real homeserver. Handles both Modern (_synapse/mas/...) and Legacy (_synapse/admin/...) API endpoints that MAS calls on Synapse. Usage: python3 mock_synapse.py Then start MAS normally: cargo run -- server -c config.yaml """ from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs import json import sys # In-memory store of provisioned users: localpart -> user data provisioned_users = {} class MockSynapseHandler(BaseHTTPRequestHandler): def _send_json(self, data, status=200): self.send_response(status) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(json.dumps(data).encode()) def _read_body(self): length = int(self.headers.get("Content-Length", 0)) if length: return json.loads(self.rfile.read(length)) return {} # ── GET endpoints ────────────────────────────────────────────── def do_GET(self): parsed = urlparse(self.path) path = parsed.path.rstrip("/") qs = parse_qs(parsed.query) # Modern: check if localpart is available if path == "/_synapse/mas/is_localpart_available": localpart = qs.get("localpart", [""])[0] if localpart in provisioned_users: print(f" [mock] is_localpart_available: {localpart} → in use") self._send_json({"errcode": "M_USER_IN_USE", "error": "User ID already taken"}, 409) else: print(f" [mock] is_localpart_available: {localpart} → available") self._send_json({}) # 200 = available return # Modern: query user info if path == "/_synapse/mas/query_user": localpart = qs.get("localpart", [""])[0] if localpart in provisioned_users: user = provisioned_users[localpart] print(f" [mock] query_user: {localpart} → found") self._send_json({ "user_id": f"@{localpart}:localhost:8008", "display_name": user.get("display_name"), "avatar_url": user.get("avatar_url"), "is_suspended": False, "is_deactivated": False, }) else: print(f" [mock] query_user: {localpart} → not found (404)") self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404) return # Legacy: username available if path == "/_synapse/admin/v1/username_available": username = qs.get("username", [""])[0] print(f" [mock] username_available: {username} → available=true") self._send_json({"available": True}) return # Legacy: get user info if "/_synapse/admin/v2/users/" in path and "/devices" not in path: print(f" [mock] get user → not found (404)") self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404) return # Legacy: list devices if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"): print(f" [mock] list devices → empty") self._send_json({"devices": []}) return # Matrix client versions if path == "/_matrix/client/versions": self._send_json({"versions": ["v1.1", "v1.2", "v1.3"]}) return # Catch-all print(f" [mock] Unhandled GET: {self.path}") self._send_json({}) # ── POST endpoints ───────────────────────────────────────────── def do_POST(self): parsed = urlparse(self.path) path = parsed.path.rstrip("/") body = self._read_body() # Modern: provision user if path == "/_synapse/mas/provision_user": localpart = body.get("localpart", "?") is_new = localpart not in provisioned_users provisioned_users[localpart] = { "display_name": body.get("set_displayname"), "avatar_url": body.get("set_avatar_url"), "emails": body.get("set_emails", []), } status = 201 if is_new else 200 print(f" [mock] provision_user: {localpart} → {status} {'created' if is_new else 'updated'}") self._send_json({}, status) return # Modern: upsert device if path == "/_synapse/mas/upsert_device": print(f" [mock] upsert_device: {body.get('device_id', '?')} → ok") self._send_json({}) return # Modern: update device display name if path == "/_synapse/mas/update_device_display_name": print(f" [mock] update_device_display_name → ok") self._send_json({}) return # Modern: delete device if path == "/_synapse/mas/delete_device": print(f" [mock] delete_device → ok") self._send_json({}) return # Modern: sync devices if path == "/_synapse/mas/sync_devices": print(f" [mock] sync_devices → ok") self._send_json({}) return # Modern: delete user if path == "/_synapse/mas/delete_user": print(f" [mock] delete_user → ok") self._send_json({}) return # Modern: reactivate user if path == "/_synapse/mas/reactivate_user": print(f" [mock] reactivate_user → ok") self._send_json({}) return # Modern: set displayname if path == "/_synapse/mas/set_displayname": localpart = body.get("localpart", "?") if localpart in provisioned_users: provisioned_users[localpart]["display_name"] = body.get("displayname") print(f" [mock] set_displayname: {localpart} → ok") self._send_json({}) return # Modern: unset displayname if path == "/_synapse/mas/unset_displayname": localpart = body.get("localpart", "?") if localpart in provisioned_users: provisioned_users[localpart]["display_name"] = None print(f" [mock] unset_displayname: {localpart} → ok") self._send_json({}) return # Modern: allow cross-signing reset if path == "/_synapse/mas/allow_cross_signing_reset": print(f" [mock] allow_cross_signing_reset → ok") self._send_json({}) return # Legacy: create device if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"): print(f" [mock] legacy create device → 201") self._send_json({}, 201) return # Legacy: bulk delete devices if "/_synapse/admin/v2/users/" in path and "delete_devices" in path: print(f" [mock] legacy bulk delete devices → ok") self._send_json({}) return # Legacy: deactivate user if "/_synapse/admin/v1/deactivate/" in path: print(f" [mock] legacy deactivate → ok") self._send_json({}) return # Legacy: allow cross-signing reset if "_allow_cross_signing_replacement_without_uia" in path: print(f" [mock] legacy allow cross-signing reset → ok") self._send_json({}) return # Catch-all print(f" [mock] Unhandled POST: {path} body={body}") self._send_json({}) # ── PUT endpoints ────────────────────────────────────────────── def do_PUT(self): parsed = urlparse(self.path) path = parsed.path.rstrip("/") body = self._read_body() # Legacy: create/update user if "/_synapse/admin/v2/users/" in path and "/devices/" not in path: print(f" [mock] legacy put user → 201 created") self._send_json({}, 201) return # Legacy: update device display name if "/_synapse/admin/v2/users/" in path and "/devices/" in path: print(f" [mock] legacy update device display name → ok") self._send_json({}) return # Client API: set displayname if "/_matrix/client/v3/profile/" in path and "/displayname" in path: print(f" [mock] set profile displayname → ok") self._send_json({}) return # Catch-all print(f" [mock] Unhandled PUT: {path} body={body}") self._send_json({}) # ── DELETE endpoints ─────────────────────────────────────────── def do_DELETE(self): parsed = urlparse(self.path) path = parsed.path.rstrip("/") # Legacy: delete device if "/_synapse/admin/v2/users/" in path and "/devices/" in path: print(f" [mock] legacy delete device → ok") self._send_json({}) return # Catch-all print(f" [mock] Unhandled DELETE: {path}") self._send_json({}) if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8008 server = HTTPServer(("localhost", port), MockSynapseHandler) print(f"Mock Synapse running on http://localhost:{port}") print("Press Ctrl+C to stop\n") try: server.serve_forever() except KeyboardInterrupt: print("\nStopped.")