新增 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: 类型定义
129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
from ringcentral import SDK
|
|
from ringcentral.http.api_exception import ApiException
|
|
|
|
from yuxi.channel.extensions.ringcentral.errors import (
|
|
RingCentralAuthError,
|
|
RingCentralError,
|
|
RingCentralNotFoundError,
|
|
RingCentralPermissionError,
|
|
RingCentralRateLimitError,
|
|
RingCentralValidationError,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AsyncRingCentralClient:
|
|
def __init__(self, client_id: str, client_secret: str, server_url: str):
|
|
self._sdk = SDK(client_id, client_secret, server_url)
|
|
self._platform = self._sdk.platform()
|
|
self.client_id = client_id
|
|
self.server_url = server_url
|
|
|
|
async def login_jwt(self, jwt_token: str) -> dict:
|
|
def _login():
|
|
return self._platform.login(jwt=jwt_token)
|
|
|
|
try:
|
|
resp = await asyncio.to_thread(_login)
|
|
data = resp.json()
|
|
logger.info("RingCentral JWT login succeeded, owner_id=%s", data.get("owner_id", ""))
|
|
return data
|
|
except ApiException as e:
|
|
raise RingCentralAuthError(getattr(e, "status_code", 401), str(e)) from e
|
|
|
|
async def ensure_authenticated(self, jwt_token: str) -> None:
|
|
auth = self._platform.auth()
|
|
if auth and auth.access_token_valid():
|
|
return
|
|
logger.info("RingCentral access token expired, re-authenticating...")
|
|
await self.login_jwt(jwt_token)
|
|
|
|
@property
|
|
def is_logged_in(self) -> bool:
|
|
return self._platform.logged_in()
|
|
|
|
@property
|
|
def platform(self):
|
|
return self._platform
|
|
|
|
async def _call(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
body: dict | None = None,
|
|
params: dict | None = None,
|
|
) -> dict:
|
|
def _do():
|
|
if method == "GET":
|
|
return self._platform.get(path, params or {})
|
|
elif method == "POST":
|
|
return self._platform.post(path, body)
|
|
elif method == "PATCH":
|
|
return self._platform.patch(path, body)
|
|
elif method == "PUT":
|
|
return self._platform.put(path, body)
|
|
elif method == "DELETE":
|
|
return self._platform.delete(path)
|
|
raise ValueError(f"Unknown method: {method}")
|
|
|
|
try:
|
|
resp = await asyncio.to_thread(_do)
|
|
if resp.ok():
|
|
return resp.json()
|
|
raise self._parse_error(resp)
|
|
except ApiException as e:
|
|
raise self._parse_api_exception(e)
|
|
|
|
def _parse_error(self, response) -> RingCentralError:
|
|
status = response.status
|
|
try:
|
|
body = response.json()
|
|
msg = body.get("message", response.text)
|
|
except Exception:
|
|
msg = response.text
|
|
|
|
if status == 429:
|
|
retry_after = response.headers.get("Retry-After", "60")
|
|
return RingCentralRateLimitError(status, msg, retry_after=int(retry_after))
|
|
if status == 404:
|
|
return RingCentralNotFoundError(status, msg)
|
|
if status in (400, 422):
|
|
return RingCentralValidationError(status, msg)
|
|
if status == 403:
|
|
return RingCentralPermissionError(status, msg)
|
|
if status in (401,):
|
|
return RingCentralAuthError(status, msg)
|
|
return RingCentralError(status, msg)
|
|
|
|
def _parse_api_exception(self, e: ApiException) -> RingCentralError:
|
|
msg = str(e)
|
|
if "rate limit" in msg.lower() or "429" in msg:
|
|
return RingCentralRateLimitError(429, msg, retry_after=60)
|
|
if "unauthorized" in msg.lower() or "token" in msg.lower():
|
|
return RingCentralAuthError(401, msg)
|
|
return RingCentralError(500, msg)
|
|
|
|
async def get(self, path: str, params: dict | None = None) -> dict:
|
|
return await self._call("GET", path, params=params)
|
|
|
|
async def post(self, path: str, body: dict | None = None) -> dict:
|
|
return await self._call("POST", path, body=body)
|
|
|
|
async def patch(self, path: str, body: dict | None = None) -> dict:
|
|
return await self._call("PATCH", path, body=body)
|
|
|
|
async def put(self, path: str, body: dict | None = None) -> dict:
|
|
return await self._call("PUT", path, body=body)
|
|
|
|
async def delete(self, path: str) -> dict:
|
|
return await self._call("DELETE", path)
|
|
|
|
async def close(self) -> None:
|
|
pass
|