from __future__ import annotations import asyncio import json from collections.abc import Callable from typing import Any import aiohttp from yuxi.utils.logging_config import logger class WhatsAppMonitor: def __init__(self, config: dict[str, Any] | None = None, bridge=None): self._config = config or {} self._bridge = bridge self._on_raw: Callable[[dict[str, Any]], None] | None = None self._task: asyncio.Task | None = None def on_raw_message(self, handler: Callable[[dict[str, Any]], None]) -> None: self._on_raw = handler async def start(self) -> None: if self._task and not self._task.done(): return self._task = asyncio.create_task(self._sse_loop()) async def stop(self) -> None: if self._task: self._task.cancel() try: await self._task except asyncio.CancelledError: pass self._task = None async def _sse_loop(self) -> None: while True: try: await self._connect_sse() except asyncio.CancelledError: break except Exception as e: logger.error(f"WhatsApp monitor error, reconnecting in 5s: {e}") await asyncio.sleep(5) async def _connect_sse(self) -> None: url = f"{self._bridge.base_url}/api/events" logger.info(f"Connecting to WhatsApp SSE: {url}") async with aiohttp.ClientSession() as session: async with session.get(url) as resp: byte_lines = [] async for chunk in resp.content.iter_any(): if chunk: byte_lines.append(chunk) raw_line = b"".join(byte_lines).decode("utf-8", errors="replace") while "\n" in raw_line: line, raw_line = raw_line.split("\n", 1) byte_lines = [raw_line.encode("utf-8")] if line.startswith("data: "): data_str = line[6:].strip() try: data = json.loads(data_str) if self._on_raw: self._on_raw(data) except json.JSONDecodeError: continue