新增BlueBubbles适配器全套核心工具类与服务,包含会话管理、消息去重、Webhook验证、缓存系统、账号配置解析、聊天消息处理等完整功能模块,支持iMessage消息收发、群管理、反应特效、语音合成等能力,提供完善的健康检查与配置校验流程。
132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
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
|