161 lines
6.0 KiB
Python
161 lines
6.0 KiB
Python
|
|
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, {})
|