ForcePilot/backend/package/yuxi/channel/extensions/zoomchat/gateway.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。

Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools

Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading

Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
2026-05-21 12:06:26 +08:00

169 lines
5.9 KiB
Python

import asyncio
import base64
import time
import logging
import httpx
logger = logging.getLogger(__name__)
TOKEN_REFRESH_BUFFER_SECONDS = 120
ZOOM_TOKEN_URL = "https://zoom.us/oauth/token"
class ZoomGateway:
def __init__(self):
self._client: httpx.AsyncClient | None = None
self._access_token: str = ""
self._token_expires_at: float = 0
self._server_task: asyncio.Task | None = None
self._queue: asyncio.Queue | None = None
self._cancel_event: asyncio.Event | None = None
self._account: dict = {}
self._running: bool = False
async def start(self, ctx) -> object:
raw_account = getattr(ctx, "account", None)
account_id = getattr(ctx, "account_id", "default")
config = getattr(ctx, "config", {})
from yuxi.channel.extensions.zoomchat.config import ZoomConfigAdapter
adapter = ZoomConfigAdapter()
if isinstance(raw_account, dict) and raw_account:
resolved = raw_account
else:
resolved = await adapter.resolve_account(account_id)
if config and "accounts" in config:
raw_cfg = config["accounts"].get(account_id, {})
for k, v in raw_cfg.items():
if k not in resolved or not resolved.get(k):
resolved[k] = v
self._account = resolved
if not adapter.is_configured(resolved):
logger.warning("Zoom Chat account %s not configured, skipping start", account_id)
return {"running": False, "reason": "not-configured"}
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(30.0),
base_url=self._account.get("api_base_url", "https://api.zoom.us/v2"),
)
await self._refresh_token()
if not self._account.get("bot_user_id"):
self._account["bot_user_id"] = await self._resolve_bot_user_id()
self._queue = asyncio.Queue(maxsize=1000)
self._cancel_event = asyncio.Event()
self._running = True
self._server_task = asyncio.create_task(self._run_webhook_server())
logger.info(
"Zoom Gateway started for account %s (bot_user_id=%s)",
self._account.get("account_id", "unknown"),
self._account.get("bot_user_id", "unknown"),
)
return {
"gateway": self,
"queue": self._queue,
"account": self._account,
"get_token": self._get_token,
"client": self._client,
}
async def stop(self, ctx) -> None:
self._running = False
if self._cancel_event:
self._cancel_event.set()
if self._server_task and not self._server_task.done():
self._server_task.cancel()
try:
await self._server_task
except asyncio.CancelledError:
pass
if self._client:
await self._client.aclose()
self._client = None
self._access_token = ""
self._token_expires_at = 0
logger.info("Zoom Gateway stopped")
async def _get_token(self) -> str:
if not self._access_token or time.time() >= self._token_expires_at - TOKEN_REFRESH_BUFFER_SECONDS:
await self._refresh_token()
return self._access_token
async def _refresh_token(self) -> str:
client_id = self._account.get("client_id", "")
client_secret = self._account.get("client_secret", "")
account_id = self._account.get("account_id", "")
if not client_id or not client_secret or not account_id:
raise RuntimeError("Missing Zoom OAuth credentials (client_id, client_secret, account_id)")
raw_auth = f"{client_id}:{client_secret}"
auth_header = base64.b64encode(raw_auth.encode()).decode()
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
resp = await client.post(
ZOOM_TOKEN_URL,
params={"grant_type": "account_credentials", "account_id": account_id},
headers={
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/x-www-form-urlencoded",
},
)
resp.raise_for_status()
data = resp.json()
self._access_token = data["access_token"]
expires_in = data.get("expires_in", 3600)
self._token_expires_at = time.time() + expires_in
logger.info("Zoom OAuth token refreshed, expires_in=%s", expires_in)
return self._access_token
async def _resolve_bot_user_id(self) -> str:
token = await self._get_token()
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
resp = await client.get(
"https://api.zoom.us/v2/users/me",
headers={"Authorization": f"Bearer {token}"},
)
if resp.status_code == 200:
data = resp.json()
return data.get("id", "")
logger.warning("Failed to resolve bot_user_id via /users/me: status=%s", resp.status_code)
return "me"
async def _run_webhook_server(self):
from yuxi.channel.extensions.zoomchat.webhook import create_webhook_app
import uvicorn
webhook_host = self._account.get("webhook_host", "0.0.0.0")
webhook_port = int(self._account.get("webhook_port", 0) or 0)
app = create_webhook_app(
account=self._account,
webhook_secret=self._account.get("webhook_secret", ""),
queue=self._queue,
cancel_event=self._cancel_event,
)
config = uvicorn.Config(app, host=webhook_host, port=webhook_port or 8000, log_level="warning")
server = uvicorn.Server(config)
try:
await server.serve()
except asyncio.CancelledError:
logger.info("Zoom Webhook server shutting down")
raise