from __future__ import annotations from typing import Any from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient async def run_doctor(client: BlueBubblesClient) -> dict[str, Any]: results: dict[str, Any] = { "server_reachable": False, "imessage_connected": False, "private_api_available": False, "macos_version": "unknown", "macos_26_or_higher": False, "authentication_valid": True, "websocket_reachable": False, "attachment_storage_ok": False, "message_history_available": False, "recent_message_count": 0, "health_monitor_enabled": True, "issues": [], } try: info = await client.get("/api/v1/server/info") data = info.get("data", {}) results["server_reachable"] = True results["imessage_connected"] = data.get("iMessageConnected", False) results["private_api_available"] = data.get("private_api", False) results["macos_version"] = data.get("os_version", "unknown") os_version = results["macos_version"] if os_version and os_version != "unknown": try: parts = os_version.split(".") if len(parts) >= 2: major = int(parts[0]) results["macos_26_or_higher"] = major >= 26 except ValueError: pass if not results["imessage_connected"]: results["issues"].append( { "severity": "error", "message": "iMessage is not connected on the BlueBubbles server", "fix": "Open Messages app on the Mac and ensure iMessage is signed in", } ) if not results["private_api_available"]: results["issues"].append( { "severity": "warning", "message": "Private API is not available (unsend, group management disabled)", "fix": "Enable Private API in BlueBubbles server settings", } ) if results["macos_26_or_higher"]: results["issues"].append( { "severity": "info", "message": "macOS 26+ detected — message editing is disabled (Apple limitation)", } ) results["websocket_reachable"] = await _check_websocket_endpoint(client) if not results["websocket_reachable"]: results["issues"].append( { "severity": "error", "message": "WebSocket endpoint not reachable, real-time messaging will not work", "fix": "Check firewall settings and ensure WebSocket port is accessible", } ) attachment_check = await _check_attachment_storage(client) results["attachment_storage_ok"] = attachment_check if not attachment_check: results["issues"].append( { "severity": "warning", "message": "Attachment storage may not be properly configured", "fix": "Check BlueBubbles server attachment storage settings", } ) history_check = await _check_message_history(client) results["message_history_available"] = history_check["available"] results["recent_message_count"] = history_check["count"] if not history_check["available"]: results["issues"].append( { "severity": "warning", "message": "Message history endpoint not responding — catchup may fail", "fix": "Check BlueBubbles server configuration and database", } ) except Exception as e: results["server_reachable"] = False results["authentication_valid"] = isinstance(e, Exception) and "401" not in str(e).lower() results["issues"].append( { "severity": "error", "message": f"BlueBubbles server unreachable: {e}", "fix": "Check server_url and that the BlueBubbles server is running", } ) return results async def _check_websocket_endpoint(client: BlueBubblesClient) -> bool: try: stat = await client.get("/api/v1/server/statistics") data = stat.get("data", {}) return bool(data.get("websocket", False) or data.get("wsConnected", False)) except Exception: return True async def _check_attachment_storage(client: BlueBubblesClient) -> bool: try: result = await client.get("/api/v1/server/statistics") data = result.get("data", {}) attachment_path = data.get("attachmentRoot") or data.get("mediaPath") or "" return bool(attachment_path) except Exception: return True async def _check_message_history(client: BlueBubblesClient) -> dict[str, Any]: try: chats = await client.get("/api/v1/chat") chat_data = chats.get("data", []) count = len(chat_data) if isinstance(chat_data, list) else 0 return {"available": True, "count": count} except Exception: return {"available": False, "count": 0}