import asyncio import logging from yuxi.channel.extensions.messenger.monitor import MessengerMonitor from yuxi.channel.extensions.messenger.config import MessengerConfigAdapter from yuxi.channel.extensions.messenger.dedupe import MessengerMessageDeduplicator logger = logging.getLogger(__name__) MESSENGER_API_BASE = "https://graph.facebook.com/v22.0" _queue: asyncio.Queue | None = None _deduplicator = MessengerMessageDeduplicator(max_size=10000, ttl_seconds=300) _monitor: MessengerMonitor | None = None _target_queue: asyncio.Queue | None = None def _get_webhook_queue() -> asyncio.Queue | None: return _queue def set_target_queue(queue: asyncio.Queue) -> None: global _target_queue _target_queue = queue class MessengerGateway: def __init__(self): self._running = False self._tasks: list[asyncio.Task] = [] self._account: dict = {} async def start(self, ctx) -> object: global _queue, _monitor config = getattr(ctx, "config", {}) if ctx else {} account_id = getattr(ctx, "account_id", "default") config_adapter = MessengerConfigAdapter() self._account = config_adapter.resolve_account(account_id, config) if not config_adapter.is_configured(self._account): logger.warning(f"messenger account {account_id} not configured, skipping start") return {"running": False, "reason": "not-configured"} _queue = asyncio.Queue(maxsize=1000) _monitor = MessengerMonitor() self._running = True task = asyncio.create_task(self._webhook_processing_loop()) self._tasks.append(task) logger.info(f"messenger gateway started for page {self._account.get('page_id')}") return {"running": True, "account_id": account_id, "page_id": self._account.get("page_id")} async def stop(self, ctx) -> None: self._running = False for task in self._tasks: task.cancel() self._tasks.clear() global _queue, _monitor _queue = None _monitor = None logger.info("messenger gateway stopped") async def _webhook_processing_loop(self): while self._running and _queue is not None: try: payload = await asyncio.wait_for(_queue.get(), timeout=1.0) await self._process_webhook(payload) except TimeoutError: continue except asyncio.CancelledError: break except Exception: logger.exception("Error processing messenger webhook payload") async def _process_webhook(self, payload: dict): if payload.get("object") != "page": return for entry in payload.get("entry", []): page_id = entry.get("id") for event in entry.get("messaging", []): if self._is_echo(event): continue msg_id = event.get("message", {}).get("mid", "") if msg_id and _deduplicator.is_duplicate(msg_id): logger.debug(f"messenger duplicate message ignored: {msg_id}") continue account_id = self._account.get("account_id", "default") um = _monitor.parse_event_to_unified(event, account_id, page_id) if um and _target_queue: logger.debug(f"messenger message dispatched: sender={um.sender.id}") await _target_queue.put(um) @staticmethod def _is_echo(event: dict) -> bool: message = event.get("message", {}) return bool(message.get("is_echo")) @staticmethod async def _token_validity_check(page_id: str, access_token: str) -> bool: try: import httpx async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client: resp = await client.get( f"{MESSENGER_API_BASE}/{page_id}", params={"fields": "id", "access_token": access_token}, ) return resp.status_code == 200 except Exception: return False