from __future__ import annotations import os from typing import TYPE_CHECKING, Any from yuxi.utils.logging_config import logger if TYPE_CHECKING: from nio import AsyncClient _ENCRYPTION_DOC_URL = "https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide" def resolve_encryption_config_path(base_dir: str, homeserver: str) -> str: sanitized = homeserver.replace("://", "_").replace("/", "_") return os.path.join(base_dir, "matrix_crypto_store", sanitized) def format_encryption_unavailable_error(config: dict[str, Any]) -> str: homeserver = config.get("homeserver", "unknown") crypto_dir = config.get("crypto_store_dir", "") parts = [ f"Encrypted messages received but E2EE is not configured for {homeserver}.", ] if not crypto_dir: parts.append("Set 'crypto_store_dir' to enable E2EE session key storage.") else: parts.append(f"Current crypto_store_dir: '{crypto_dir}' — verify it exists and is writable.") parts.append(f"See: {_ENCRYPTION_DOC_URL}") return "\n".join(parts) def check_crypto_store_ready(store_path: str) -> dict[str, Any]: status = "not_configured" details: list[str] = [] if not store_path: return {"status": status, "details": ["crypto_store_dir not configured"]} if not os.path.exists(store_path): details.append(f"Directory does not exist: {store_path}") return {"status": "unavailable", "details": details} if not os.path.isdir(store_path): details.append(f"Path is not a directory: {store_path}") return {"status": "unavailable", "details": details} if not os.access(store_path, os.W_OK): details.append(f"Directory not writable: {store_path}") return {"status": "unavailable", "details": details} keys_file = os.path.join(store_path, "room_keys.json") if os.path.exists(keys_file): status = "ready" details.append("room_keys.json found") else: status = "empty" details.append("room_keys.json not found — keys will be created on first encrypted message") return {"status": status, "details": details} _CRYPTO_BOOTSTRAP_GUIDE = """ E2EE Bootstrap Steps: 1. Ensure the crypto store directory exists and is writable 2. Import room keys (automatic on connect via import_room_keys) 3. Verify device list is tracked (automatic via nio AsyncClient) 4. If keys are missing, re-verify with: /keys/query endpoint """.strip() def get_crypto_bootstrap_guide() -> str: return _CRYPTO_BOOTSTRAP_GUIDE async def decrypt_event(client: AsyncClient, event: Any, room_id: str) -> dict[str, Any] | None: try: decrypted_list = await client.decrypt_event(event) if decrypted_list and isinstance(decrypted_list, list): for item in decrypted_list: if isinstance(item, dict): return item if isinstance(decrypted_list, dict): return decrypted_list except Exception as e: logger.debug(f"Matrix decrypt_event failed for {room_id}: {e}") return None async def verify_device(client: AsyncClient, user_id: str, device_id: str) -> dict[str, Any]: try: resp = await client.verify_device(user_id, device_id) return {"status": "ok", "verified": bool(resp)} except Exception as e: return {"status": "error", "error": str(e)} async def check_cross_signing(client: AsyncClient) -> dict[str, Any]: try: resp = await client.query_keys() cross_signing = getattr(resp, "cross_signing_keys", {}) if resp else {} return { "status": "ok", "cross_signing_ready": bool(cross_signing), "details": str(cross_signing)[:200] if cross_signing else "none", } except Exception as e: return {"status": "error", "error": str(e)}