ForcePilot/backend/package/yuxi/channels/adapters/matrix/encryption.py
Kris 2063145ce1 feat(matrix): 新增完整的Matrix适配器工具集
新增了20+个Matrix相关工具模块,包含线程处理、自动加入、消息去重、配置管理、安全策略、媒体处理、命令解析等功能,完善Matrix适配器基础能力
2026-05-12 00:45:54 +08:00

112 lines
3.8 KiB
Python

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)}