from __future__ import annotations import asyncio import logging import time from yuxi.channel.extensions.ringcentral.config import RingCentralConfigAdapter from yuxi.channel.extensions.ringcentral.errors import RingCentralAuthError from yuxi.channel.extensions.ringcentral.sdk import AsyncRingCentralClient from yuxi.channel.extensions.ringcentral.subscription import ( create_subscription, delete_subscription, renew_subscription, ) from yuxi.channel.extensions.ringcentral.types import ResolvedRingCentralAccount from yuxi.channel.extensions.ringcentral.webhook import RingCentralWebhookHandler from yuxi.channel.gateway.routes import webhook_registry logger = logging.getLogger(__name__) SUBSCRIPTION_RENEW_BEFORE_SEC = 3600 RENEW_INTERVAL = 86400 - SUBSCRIPTION_RENEW_BEFORE_SEC class RingCentralGatewayAdapter: def __init__(self): self._config_adapter = RingCentralConfigAdapter() self._webhook_handler = RingCentralWebhookHandler() self._clients: dict[str, AsyncRingCentralClient] = {} self._accounts: dict[str, ResolvedRingCentralAccount] = {} self._tasks: dict[str, asyncio.Task] = {} self._abort_events: dict[str, asyncio.Event] = {} self._running: dict[str, bool] = {} self._status_snapshots: dict[str, dict] = {} async def start(self, ctx) -> object: account_dict = getattr(ctx, "account", {}) or {} account = account_dict.get("resolved") if not isinstance(account, ResolvedRingCentralAccount): account_id = account_dict.get("account_id", "default") resolved_dict = await self._config_adapter.resolve_account(account_id) account = resolved_dict.get("resolved") if not account or not account.is_configured: logger.warning("RingCentral account not configured: %s", account.account_id if account else "unknown") return asyncio.Queue() account_id = account.account_id client = AsyncRingCentralClient( client_id=account.client_id, client_secret=account.client_secret, server_url=account.server_url, ) try: await client.login_jwt(account.jwt_token) except RingCentralAuthError as e: logger.error("RingCentral auth failed for %s: %s", account_id, e) return asyncio.Queue() self._clients[account_id] = client self._accounts[account_id] = account self._webhook_handler.register_target(account.webhook_path, account) webhook_registry.register( "ringcentral", self._webhook_handler.handle_webhook, guard_config=None, ) base_url = getattr(ctx, "base_url", "") if ctx else "" if base_url and not account.subscription_id: webhook_url = f"{base_url.rstrip('/')}{account.webhook_path}" try: sub_result = await create_subscription(client, account, webhook_url) account.subscription_id = sub_result.get("id", "") except Exception: logger.warning("Failed to create RingCentral subscription for %s", account_id) abort_event = asyncio.Event() self._abort_events[account_id] = abort_event task = asyncio.create_task(self._subscription_renew_loop(account_id, account, abort_event)) self._tasks[account_id] = task self._running[account_id] = True self._status_snapshots[account_id] = { "running": True, "webhook_path": account.webhook_path, "server_url": account.server_url, "subscription_id": account.subscription_id, "start_time": time.time(), } logger.info("RingCentral gateway started for account: %s, webhook: %s", account_id, account.webhook_path) queue = getattr(ctx, "queue", asyncio.Queue()) return queue async def stop(self, ctx) -> None: account_dict = getattr(ctx, "account", {}) or {} account_id = account_dict.get("account_id", "default") account = account_dict.get("resolved") if isinstance(account, ResolvedRingCentralAccount): self._webhook_handler.unregister_target(account.webhook_path) abort = self._abort_events.pop(account_id, None) if abort: abort.set() task = self._tasks.pop(account_id, None) if task: task.cancel() try: await task except asyncio.CancelledError: pass client = self._clients.pop(account_id, None) stored_account = self._accounts.pop(account_id, None) if client and stored_account and stored_account.subscription_id: try: await delete_subscription(client, stored_account.subscription_id) except Exception: logger.warning("Failed to delete RingCentral subscription for %s", account_id) self._running.pop(account_id, None) self._status_snapshots[account_id] = {"running": False, "stop_time": time.time()} logger.info("RingCentral gateway stopped for account: %s", account_id) async def _subscription_renew_loop( self, account_id: str, account: ResolvedRingCentralAccount, abort_event: asyncio.Event, ) -> None: client = self._clients.get(account_id) if not client: return while not abort_event.is_set(): try: await asyncio.sleep(RENEW_INTERVAL) if abort_event.is_set(): break if account.subscription_id: await renew_subscription(client, account.subscription_id) except Exception as e: logger.error("RingCentral subscription renew failed for %s: %s", account_id, e) def get_client(self, account_id: str = "default") -> AsyncRingCentralClient | None: return self._clients.get(account_id) def get_status(self, account_id: str) -> dict: return self._status_snapshots.get(account_id, {})