ForcePilot/backend/package/yuxi/channel/extensions/zalo/api.py
Kris 5946478772 feat(channel): 添加小红书、XMPP、元宝和 Zalo 渠道扩展
新增小红书、XMPP、元宝、Zalo 四个渠道扩展。

小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window

XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor

元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils

Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
2026-05-21 12:04:05 +08:00

117 lines
3.6 KiB
Python

import logging
import httpx
from yuxi.channel.extensions.zalo.errors import (
ZaloApiError,
ZaloAuthError,
ZaloNetworkError,
ZaloRateLimitError,
)
logger = logging.getLogger(__name__)
class ZaloBotApi:
BASE = "https://bot-api.zaloplatforms.com"
def __init__(self, token: str, proxy: str | None = None):
self._token = token
timeout = httpx.Timeout(35.0)
self._client = httpx.AsyncClient(proxy=proxy, timeout=timeout)
async def close(self):
await self._client.aclose()
async def _call(self, method: str, body: dict | None = None) -> dict:
url = f"{self.BASE}/bot{self._token}/{method}"
try:
resp = await self._client.post(url, json=body or {})
resp.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == 408:
raise
error_code = -1
description = "Unknown error"
try:
data = e.response.json()
error_code = data.get("error_code", -1)
description = data.get("description", "Unknown error")
except (ValueError, AttributeError):
description = f"HTTP {e.response.status_code}"
raise self._classify_error(error_code, description) from e
except httpx.RequestError as e:
raise ZaloNetworkError(f"Request failed: {e}") from e
data = resp.json()
if not data.get("ok"):
error_code = data.get("error_code", -1)
description = data.get("description", "Unknown error")
raise self._classify_error(error_code, description)
return data
@staticmethod
def _classify_error(error_code: int, description: str) -> ZaloApiError:
if error_code in (100, 101, 102):
return ZaloAuthError(error_code, description)
if error_code == 103:
return ZaloRateLimitError(error_code, description)
return ZaloApiError(error_code, description)
async def get_me(self) -> dict:
return await self._call("getMe")
async def send_message(self, chat_id: str, text: str) -> dict:
return await self._call(
"sendMessage",
{
"chat_id": chat_id,
"text": text,
},
)
async def send_photo(self, chat_id: str, photo_url: str, caption: str = "") -> dict:
body: dict = {
"chat_id": chat_id,
"photo": photo_url,
}
if caption:
body["caption"] = caption
return await self._call("sendPhoto", body)
async def send_sticker(self, chat_id: str, sticker_id: str) -> dict:
return await self._call(
"sendSticker",
{
"chat_id": chat_id,
"sticker": sticker_id,
},
)
async def send_chat_action(self, chat_id: str, action: str = "typing") -> dict:
return await self._call(
"sendChatAction",
{
"chat_id": chat_id,
"action": action,
},
)
async def get_updates(self, timeout: int = 30) -> dict:
return await self._call("getUpdates", {"timeout": timeout})
async def set_webhook(self, url: str, secret_token: str) -> dict:
return await self._call(
"setWebhook",
{
"url": url,
"secret_token": secret_token,
},
)
async def delete_webhook(self) -> dict:
return await self._call("deleteWebhook")
async def get_webhook_info(self) -> dict:
return await self._call("getWebhookInfo")