ForcePilot/backend/package/yuxi/channel/extensions/viber/webhook.py
Kris 1c590097be feat(channel): 添加 Twitter 和 Viber 渠道扩展
新增 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: 媒体资源处理
2026-05-21 11:57:22 +08:00

156 lines
4.5 KiB
Python

from __future__ import annotations
import asyncio
import hashlib
import hmac
import json
import logging
import os
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from yuxi.channel.extensions.viber.config import ENV_VIBER_AUTH_TOKEN
logger = logging.getLogger(__name__)
VIBER_WEBHOOK_PREAUTH_MAX_BODY_BYTES = 256 * 1024
VIBER_WEBHOOK_PREAUTH_BODY_TIMEOUT_S = 5.0
router = APIRouter(prefix="/webhook/viber", tags=["viber"])
_webhook_running = False
def set_webhook_running(running: bool) -> None:
global _webhook_running
_webhook_running = running
def set_webhook_config(config: dict) -> None:
from yuxi.channel.extensions.viber import viber_plugin
viber_plugin._config.list_account_ids(config)
def verify_viber_signature(body: bytes, signature: str, auth_token: str) -> bool:
if not signature or not auth_token:
return False
expected = hmac.new(
auth_token.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
@router.post("")
async def viber_webhook(request: Request):
if not _webhook_running:
return JSONResponse(
status_code=503,
content={"status": 1, "status_message": "channel not running"},
)
body = await _read_body_with_limit(request)
if body is None:
return JSONResponse(
status_code=413,
content={"status": 1, "status_message": "body too large"},
)
signature = request.headers.get("X-Viber-Content-Signature", "")
from yuxi.channel.extensions.viber import viber_plugin
account, account_id = await _resolve_account_for_webhook(viber_plugin, body, signature)
if not account:
return JSONResponse(
status_code=401,
content={"status": 1, "status_message": "invalid signature"},
)
try:
callback = json.loads(body)
except json.JSONDecodeError:
return JSONResponse(
status_code=400,
content={"status": 1, "status_message": "invalid JSON"},
)
event_type = callback.get("event", "")
if event_type == "webhook":
logger.info("Viber webhook verification callback received")
return {"status": 0, "status_message": "ok"}
um = viber_plugin._monitor.parse_event_to_unified(callback, account_id)
if um:
asyncio.create_task(
_dispatch_to_agent(um),
name=f"viber-dispatch-{um.sender.id}",
)
return {"status": 0, "status_message": "ok"}
async def _resolve_account_for_webhook(plugin, body: bytes, signature: str) -> tuple[dict | None, str]:
account_ids = plugin._config.list_account_ids({})
accounts = []
for aid in account_ids:
try:
acct = await plugin._config.resolve_account(aid)
if acct:
accounts.append(acct)
except Exception:
logger.exception("Viber resolve_account error for %s", aid)
for acct in accounts:
auth_token = acct.get("auth_token", "")
if auth_token and verify_viber_signature(body, signature, auth_token):
return acct, acct.get("account_id", "default")
env_token = os.environ.get(ENV_VIBER_AUTH_TOKEN, "")
if env_token and verify_viber_signature(body, signature, env_token):
return {
"account_id": "default",
"auth_token": env_token,
"sender_name": "ForcePilot Bot",
"name": "default",
}, "default"
return None, ""
async def _dispatch_to_agent(msg) -> None:
try:
from yuxi.channel.runtime.manager import gateway
processor = getattr(gateway, "_processor", None)
if processor is None:
logger.warning("Viber dispatch: message processor not available")
return
await asyncio.wait_for(processor.process(msg), timeout=120.0)
except TimeoutError:
logger.error("Viber agent response timeout for user %s", msg.sender.id)
except Exception:
logger.exception("Viber dispatch error for user %s", msg.sender.id)
async def _read_body_with_limit(request: Request) -> bytes | None:
try:
body = await asyncio.wait_for(
request.body(),
timeout=VIBER_WEBHOOK_PREAUTH_BODY_TIMEOUT_S,
)
except TimeoutError:
logger.warning("Viber webhook: body read timeout")
return None
if len(body) > VIBER_WEBHOOK_PREAUTH_MAX_BODY_BYTES:
logger.warning("Viber webhook: body too large (%d bytes)", len(body))
return None
return body