新增 RingCentral 渠道扩展,支持在 Yuxi 平台中集成 RingCentral 统一通信平台。 包含以下功能模块: - sdk: RingCentral SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - subscription: 事件订阅 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - events: 事件处理 - adaptive_cards: 自适应卡片 - formatting: 格式化 - media: 媒体资源处理 - mentions: @提及 - notes: 笔记功能 - reactions: 表情反应 - tasks: 任务管理 - teams: 团队管理 - types: 类型定义
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, {})
|