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