新增 Twitter 和 Viber 两个渠道扩展。 Twitter 渠道扩展功能模块: - auth: OAuth 认证管理 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - tweets: 推文管理 - social: 社交互动 - reactions: 表情反应 - media: 媒体资源处理 Viber 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - rate_limiter: 速率限制 - media: 媒体资源处理
271 lines
9.9 KiB
Python
271 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import urllib.parse
|
|
|
|
import httpx
|
|
|
|
from yuxi.channel.extensions.viber.config import ViberConfigAdapter
|
|
from yuxi.channel.extensions.viber.errors import ViberErrorCode
|
|
from yuxi.channel.extensions.viber.rate_limiter import ViberRateLimiter
|
|
from yuxi.channel.extensions.viber.types import VIBER_API_BASE
|
|
from yuxi.channel.extensions.viber.webhook import set_webhook_running
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ViberGatewayAdapter:
|
|
def __init__(self):
|
|
self._config = ViberConfigAdapter()
|
|
self._running = False
|
|
self._rate_limiter = ViberRateLimiter(max_requests=30, per_seconds=1.0)
|
|
|
|
async def start(self, ctx) -> object:
|
|
import asyncio
|
|
|
|
account_id = getattr(ctx, "account_id", "default")
|
|
config = getattr(ctx, "config", {}) or {}
|
|
|
|
self._config.list_account_ids(config)
|
|
account = await self._config.resolve_account(account_id)
|
|
auth_token = account.get("auth_token", "")
|
|
|
|
if not auth_token:
|
|
logger.warning("Viber gateway start: auth_token not configured for account %s", account_id)
|
|
self._running = True
|
|
queue = getattr(ctx, "queue", asyncio.Queue())
|
|
return queue
|
|
|
|
valid = await self._probe_bot(auth_token)
|
|
if valid:
|
|
logger.info("Viber bot connected for account %s", account_id)
|
|
else:
|
|
logger.warning("Viber bot probe failed for account %s", account_id)
|
|
|
|
webhook_url = account.get("webhook_url", "")
|
|
if webhook_url:
|
|
ok = await self._set_webhook(auth_token, webhook_url)
|
|
if ok:
|
|
logger.info("Viber webhook set: %s", webhook_url)
|
|
else:
|
|
logger.error("Viber set_webhook failed for account %s", account_id)
|
|
|
|
self._running = True
|
|
set_webhook_running(True)
|
|
|
|
queue = getattr(ctx, "queue", asyncio.Queue())
|
|
return queue
|
|
|
|
async def stop(self, ctx) -> None:
|
|
account_id = getattr(ctx, "account_id", "default")
|
|
|
|
if self._running:
|
|
account = await self._config.resolve_account(account_id)
|
|
auth_token = account.get("auth_token", "")
|
|
webhook_url = account.get("webhook_url", "")
|
|
if auth_token and webhook_url:
|
|
await self._remove_webhook(auth_token)
|
|
|
|
self._running = False
|
|
set_webhook_running(False)
|
|
logger.info("Viber gateway stopped")
|
|
|
|
async def probe_bot(self, account: dict) -> dict | None:
|
|
auth_token = account.get("auth_token", "")
|
|
if not auth_token:
|
|
return None
|
|
return await self._probe_bot_detail(auth_token)
|
|
|
|
async def _probe_bot(self, auth_token: str) -> bool:
|
|
try:
|
|
result = await self._api_call(auth_token, "get_account_info", {})
|
|
return result.get("status") == 0
|
|
except Exception:
|
|
logger.exception("Viber probe error")
|
|
return False
|
|
|
|
async def _probe_bot_detail(self, auth_token: str) -> dict | None:
|
|
try:
|
|
result = await self._api_call(auth_token, "get_account_info", {})
|
|
if result.get("status") == 0:
|
|
return result
|
|
return None
|
|
except Exception:
|
|
logger.exception("Viber probe detail error")
|
|
return None
|
|
|
|
async def _set_webhook(self, auth_token: str, webhook_url: str) -> bool:
|
|
payload = {
|
|
"url": webhook_url,
|
|
"event_types": [
|
|
"delivered",
|
|
"seen",
|
|
"failed",
|
|
"subscribed",
|
|
"unsubscribed",
|
|
"conversation_started",
|
|
],
|
|
"send_name": True,
|
|
"send_photo": True,
|
|
}
|
|
result = await self._api_call(auth_token, "set_webhook", payload)
|
|
ok = result.get("status") == 0
|
|
if not ok:
|
|
logger.error("Viber set_webhook failed: %s", result.get("status_message"))
|
|
return ok
|
|
|
|
async def _remove_webhook(self, auth_token: str) -> bool:
|
|
payload = {"url": ""}
|
|
result = await self._api_call(auth_token, "set_webhook", payload)
|
|
return result.get("status") == 0
|
|
|
|
async def get_user_details(self, account: dict, user_id: str) -> dict | None:
|
|
auth_token = account.get("auth_token", "")
|
|
if not auth_token:
|
|
return None
|
|
try:
|
|
result = await self._api_call(auth_token, "get_user_details", {"id": user_id})
|
|
if result.get("status") == 0:
|
|
return result.get("result", result)
|
|
self._log_error("get_user_details", result)
|
|
return None
|
|
except Exception:
|
|
logger.exception("Viber get_user_details error")
|
|
return None
|
|
|
|
async def broadcast_message(
|
|
self,
|
|
account: dict,
|
|
broadcast_list: list[str],
|
|
message: dict,
|
|
) -> dict | None:
|
|
auth_token = account.get("auth_token", "")
|
|
if not auth_token:
|
|
return None
|
|
if len(broadcast_list) > 300:
|
|
logger.warning("Viber broadcast_list exceeds 300 limit: %d", len(broadcast_list))
|
|
broadcast_list = broadcast_list[:300]
|
|
try:
|
|
results = []
|
|
for receiver_id in broadcast_list:
|
|
msg_copy = json.loads(json.dumps(message))
|
|
msg_str = json.dumps(msg_copy)
|
|
msg_str = msg_str.replace("replace_me_with_receiver_id", receiver_id)
|
|
msg_str = msg_str.replace(
|
|
"replace_me_with_url_encoded_receiver_id",
|
|
urllib.parse.quote(receiver_id, safe=""),
|
|
)
|
|
|
|
if "replace_me_with_user_name" in msg_str:
|
|
user_name = await self._get_user_name(auth_token, receiver_id)
|
|
msg_str = msg_str.replace("replace_me_with_user_name", user_name)
|
|
|
|
msg_copy = json.loads(msg_str)
|
|
msg_copy["receiver"] = receiver_id
|
|
result = await self._api_call(auth_token, "send_message", msg_copy)
|
|
results.append(result)
|
|
|
|
failed = []
|
|
for i, result in enumerate(results):
|
|
if result.get("status") != 0:
|
|
failed.append({
|
|
"receiver": broadcast_list[i],
|
|
"status": result.get("status"),
|
|
"status_message": result.get("status_message", ""),
|
|
})
|
|
|
|
if failed:
|
|
logger.warning(
|
|
"Viber broadcast partial failure: %d/%d failed",
|
|
len(failed), len(broadcast_list),
|
|
)
|
|
for f in failed:
|
|
logger.warning(
|
|
" - receiver=%s status=%s message=%s",
|
|
f["receiver"], f["status"], f["status_message"],
|
|
)
|
|
|
|
return {
|
|
"status": 0,
|
|
"broadcast_count": len(broadcast_list),
|
|
"failed_count": len(failed),
|
|
"failed_list": failed,
|
|
"results": results,
|
|
}
|
|
except Exception:
|
|
logger.exception("Viber broadcast_message error")
|
|
return None
|
|
|
|
async def _get_user_name(self, auth_token: str, user_id: str) -> str:
|
|
try:
|
|
result = await self._api_call(auth_token, "get_user_details", {"id": user_id})
|
|
if result.get("status") == 0:
|
|
user_data = result.get("user", {})
|
|
name = user_data.get("name", "")
|
|
if name:
|
|
return name
|
|
except Exception:
|
|
logger.debug("Viber get_user_name failed for %s", user_id)
|
|
return "User"
|
|
|
|
async def get_online(self, account: dict, user_ids: list[str]) -> dict | None:
|
|
auth_token = account.get("auth_token", "")
|
|
if not auth_token:
|
|
return None
|
|
if len(user_ids) > 100:
|
|
logger.warning("Viber get_online exceeds 100 user limit: %d", len(user_ids))
|
|
user_ids = user_ids[:100]
|
|
try:
|
|
result = await self._api_call(auth_token, "get_online", {"ids": user_ids})
|
|
if result.get("status") == 0:
|
|
return result
|
|
self._log_error("get_online", result)
|
|
return None
|
|
except Exception:
|
|
logger.exception("Viber get_online error")
|
|
return None
|
|
|
|
async def _api_call(self, auth_token: str, endpoint: str, payload: dict) -> dict:
|
|
await self._rate_limiter.acquire()
|
|
headers = {
|
|
"X-Viber-Auth-Token": auth_token,
|
|
"Content-Type": "application/json",
|
|
}
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client:
|
|
resp = await client.post(
|
|
f"{VIBER_API_BASE}/{endpoint}",
|
|
json=payload,
|
|
headers=headers,
|
|
)
|
|
resp.raise_for_status()
|
|
result = resp.json()
|
|
self._log_error(endpoint, result)
|
|
billing_status = result.get("billing_status")
|
|
if billing_status is not None:
|
|
logger.debug(
|
|
"Viber billing: endpoint=%s status=%s",
|
|
endpoint, billing_status,
|
|
)
|
|
return result
|
|
|
|
@staticmethod
|
|
def _log_error(endpoint: str, result: dict) -> None:
|
|
status = result.get("status")
|
|
if status is not None and status != 0:
|
|
status_msg = result.get("status_message", "")
|
|
retryable = ViberErrorCode.is_retryable(status)
|
|
auth_fail = ViberErrorCode.is_auth_error(status)
|
|
extra = ""
|
|
if retryable:
|
|
extra = " [retryable]"
|
|
if auth_fail:
|
|
extra += " [auth_failure]"
|
|
logger.warning(
|
|
"Viber API %s failed: status=%s message=%s%s",
|
|
endpoint,
|
|
status,
|
|
status_msg,
|
|
extra,
|
|
)
|