ForcePilot/backend/package/yuxi/channel/extensions/twitter/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

107 lines
3.3 KiB
Python

from __future__ import annotations
import base64
import hashlib
import hmac
import logging
from requests_oauthlib import OAuth1Session
logger = logging.getLogger(__name__)
X_API_V1_BASE = "https://api.x.com"
def _build_oauth(account: dict) -> OAuth1Session:
return OAuth1Session(
account["api_key"],
client_secret=account["api_secret"],
resource_owner_key=account["access_token"],
resource_owner_secret=account["access_secret"],
)
def _resolve_webhook_env(account: dict) -> str:
return account.get("webhook_env", "dev")
def verify_crc_token(crc_token: str, consumer_secret: str) -> str:
digest = hmac.new(
consumer_secret.encode("utf-8"),
crc_token.encode("utf-8"),
hashlib.sha256,
).digest()
return "sha256=" + base64.b64encode(digest).decode()
def verify_webhook_signature(signature: str, body: bytes, consumer_secret: str) -> bool:
expected = hmac.new(
consumer_secret.encode("utf-8"),
body,
hashlib.sha256,
).digest()
expected_b64 = base64.b64encode(expected).decode()
return hmac.compare_digest(f"sha256={expected_b64}", signature)
async def register_webhook(account: dict, env_name: str | None = None) -> dict | None:
oauth = _build_oauth(account)
webhook_url = account.get("webhook_url", "")
if not webhook_url:
logger.error("Twitter webhook: no webhook_url configured")
return None
env_name = env_name or _resolve_webhook_env(account)
url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/webhooks.json"
try:
resp = oauth.post(url, data={"url": webhook_url}, timeout=15)
if resp.status_code in (200, 201):
return resp.json()
logger.error(
"Twitter webhook register failed: HTTP %d %s",
resp.status_code,
resp.text[:200],
)
except Exception as e:
logger.error("Twitter webhook register error: %s", e)
return None
async def subscribe_account_activity(
account: dict, env_name: str | None = None
) -> bool:
oauth = _build_oauth(account)
env_name = env_name or _resolve_webhook_env(account)
url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/subscriptions.json"
try:
resp = oauth.post(url, timeout=10)
if resp.status_code in (200, 204):
logger.info("Twitter account activity subscribed for env %s", env_name)
return True
logger.error(
"Twitter subscription failed: HTTP %d %s", resp.status_code, resp.text[:200]
)
except Exception as e:
logger.error("Twitter subscription error: %s", e)
return False
async def unsubscribe_account_activity(
account: dict, env_name: str | None = None
) -> bool:
oauth = _build_oauth(account)
user_id = account.get("user_id", "")
if not user_id:
return False
env_name = env_name or _resolve_webhook_env(account)
url = f"{X_API_V1_BASE}/1.1/account_activity/all/{env_name}/subscriptions/{user_id}.json"
try:
resp = oauth.delete(url, timeout=10)
if resp.status_code in (200, 204):
logger.info("Twitter account activity unsubscribed for user %s", user_id)
return True
except Exception as e:
logger.error("Twitter unsubscribe error: %s", e)
return False