from __future__ import annotations import time from typing import Any from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient from yuxi.channels.models import HealthStatus class ServerInfoCache: MAX_ENTRIES = 64 TTL_SECONDS = 600 def __init__(self) -> None: self._entries: dict[str, tuple[float, Any]] = {} def get(self, server_url: str) -> Any | None: entry = self._entries.get(server_url) if entry is None: return None ts, value = entry if time.monotonic() - ts > self.TTL_SECONDS: del self._entries[server_url] return None return value def set(self, server_url: str, value: Any) -> None: if len(self._entries) >= self.MAX_ENTRIES: now = time.monotonic() expired = [k for k, v in self._entries.items() if now - v[0] > self.TTL_SECONDS] for k in expired: del self._entries[k] if len(self._entries) >= self.MAX_ENTRIES: oldest = sorted(self._entries.items(), key=lambda x: x[1][0])[0] del self._entries[oldest[0]] self._entries[server_url] = (time.monotonic(), value) def invalidate(self, server_url: str) -> None: self._entries.pop(server_url, None) _server_info_cache = ServerInfoCache() async def probe_server(client: BlueBubblesClient, use_cache: bool = True) -> dict[str, Any]: if use_cache: cached = _server_info_cache.get(client.base_url) if cached is not None: return cached result = await client.get("/api/v1/server/info") if use_cache: _server_info_cache.set(client.base_url, result) return result async def probe_imessage_login(client: BlueBubblesClient) -> HealthStatus: try: server_info = await probe_server(client) if server_info.get("status") != "ok": return HealthStatus( status="unhealthy", last_error="BlueBubbles Server not responding", ) data = server_info.get("data", {}) imessage_connected = data.get("iMessageConnected", False) if not imessage_connected: return HealthStatus( status="degraded", last_error="iMessage not connected on server", metadata={"server_info": data}, ) return HealthStatus( status="healthy", metadata={"server_info": data}, ) except Exception as e: return HealthStatus( status="unhealthy", last_error=f"Health check failed: {e}", ) async def probe_private_api(client: BlueBubblesClient) -> bool: try: server_info = await probe_server(client) data = server_info.get("data", {}) return bool(data.get("private_api", False)) except Exception: return False async def probe_macos_version(client: BlueBubblesClient) -> str: try: server_info = await probe_server(client) data = server_info.get("data", {}) return data.get("os_version", "unknown") except Exception: return "unknown" def is_macos_26_or_higher(version: str) -> bool: if version == "unknown": return False try: parts = version.split(".") if len(parts) >= 2: return int(parts[0]) >= 26 return False except ValueError: return False async def check_server_reachable(server_url: str, password: str, timeout: float = 10.0) -> HealthStatus: client = BlueBubblesClient(server_url=server_url, password=password, timeout=timeout) try: async with client: return await probe_imessage_login(client) except Exception as e: return HealthStatus( status="unhealthy", last_error=f"Cannot reach BlueBubbles Server: {e}", ) def get_server_info_cache() -> ServerInfoCache: return _server_info_cache