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

219 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import hashlib
import logging
import os
logger = logging.getLogger(__name__)
class TwitterConfigAdapter:
def __init__(self):
self._config: dict = {}
@property
def _tw_cfg(self) -> dict:
return self._config.get("channels", {}).get("twitter", {})
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
accounts = self._tw_cfg.get("accounts", {})
if accounts:
return list(accounts.keys())
if self._env_credentials_exist():
return ["default"]
return []
async def resolve_account(self, account_id: str) -> dict:
return self._build_account(account_id)
def is_configured(self, account: dict) -> bool:
return all(
[
account.get("api_key"),
account.get("api_secret"),
account.get("access_token"),
account.get("access_secret"),
]
)
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True)
def describe_account(self, account: dict) -> dict:
return {
"account_id": account.get("account_id", ""),
"name": account.get("name", ""),
"username": account.get("username", ""),
"user_id": account.get("user_id", ""),
"token_source": account.get("token_source", ""),
"dm_policy": account.get("dm_policy", "pairing"),
"configured": self.is_configured(account),
}
def default_account_id(self) -> str:
return self._tw_cfg.get("defaultAccount", "default")
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "X (Twitter) 渠道配置",
"properties": {
"apiKey": {
"type": "string",
"title": "API Key (Consumer Key)",
"description": "X Developer Portal 中的 API Key也可通过环境变量 X_API_KEY 注入",
"x-ui-password": True,
},
"apiSecret": {
"type": "string",
"title": "API Secret (Consumer Secret)",
"description": "也可通过环境变量 X_API_SECRET 注入",
"x-ui-password": True,
},
"accessToken": {
"type": "string",
"title": "Access Token",
"description": "OAuth 1.0a 授权后获得的 Access Token也可通过环境变量 X_ACCESS_TOKEN 注入",
"x-ui-password": True,
},
"accessSecret": {
"type": "string",
"title": "Access Token Secret",
"description": "也可通过环境变量 X_ACCESS_SECRET 注入",
"x-ui-password": True,
},
"dmPolicy": {
"type": "string",
"title": "DM 策略",
"enum": ["pairing", "allowlist", "open", "disabled"],
"default": "pairing",
},
"groupPolicy": {
"type": "string",
"title": "群组 DM 策略",
"enum": ["open", "disabled", "allowlist"],
"default": "disabled",
},
"textChunkLimit": {
"type": "integer",
"title": "文本分块上限",
"default": 10000,
"minimum": 100,
"maximum": 10000,
},
"webhookUrl": {
"type": "string",
"title": "Webhook URL",
"description": "Account Activity API 的 Webhook 回调地址(公网可访问)。留空则使用 Polling 模式",
},
"webhookEnv": {
"type": "string",
"title": "Webhook 环境名称",
"default": "dev",
"description": "Account Activity API 的环境名称,对应 X Developer Portal 中的环境标识",
},
"connectionMode": {
"type": "string",
"title": "连接模式",
"enum": ["auto", "webhook", "polling"],
"default": "auto",
},
"pollingIntervalSec": {
"type": "integer",
"title": "Polling 间隔(秒)",
"default": 180,
"minimum": 60,
"maximum": 900,
},
"reactionLevel": {
"type": "string",
"title": "Reaction 级别",
"enum": ["off", "ack", "minimal"],
"default": "ack",
},
},
"required": ["apiKey", "apiSecret", "accessToken", "accessSecret"],
}
@staticmethod
def credential_fingerprint(api_key: str, access_token: str) -> str:
return hashlib.sha256(f"{api_key}:{access_token}".encode()).hexdigest()[:8]
@staticmethod
def _env_credentials_exist() -> bool:
return bool(
os.environ.get("X_API_KEY", "")
and os.environ.get("X_API_SECRET", "")
and os.environ.get("X_ACCESS_TOKEN", "")
and os.environ.get("X_ACCESS_SECRET", "")
)
def _build_account(self, account_id: str) -> dict:
tw_cfg = self._tw_cfg
accounts = tw_cfg.get("accounts", {})
account_raw = (
accounts.get(account_id, {}) if account_id != "default" else tw_cfg
)
def _get(key: str, default=None):
return account_raw.get(key, tw_cfg.get(key, default))
api_key = self._resolve_env_credential("X_API_KEY", account_id)
api_secret = self._resolve_env_credential("X_API_SECRET", account_id)
access_token = self._resolve_env_credential("X_ACCESS_TOKEN", account_id)
access_secret = self._resolve_env_credential("X_ACCESS_SECRET", account_id)
token_source = (
"env"
if (api_key and api_secret and access_token and access_secret)
else "none"
)
if not api_key:
api_key = _get("apiKey", "")
if not api_secret:
api_secret = _get("apiSecret", "")
if not access_token:
access_token = _get("accessToken", "")
if not access_secret:
access_secret = _get("accessSecret", "")
if token_source == "none" and all(
[api_key, api_secret, access_token, access_secret]
):
token_source = "config"
return {
"account_id": account_id,
"api_key": api_key,
"api_secret": api_secret,
"access_token": access_token,
"access_secret": access_secret,
"token_source": token_source,
"name": account_raw.get("name", account_id),
"user_id": account_raw.get("userId", ""),
"username": account_raw.get("username", ""),
"enabled": account_raw.get("enabled", True),
"dm_policy": _get("dmPolicy", "pairing"),
"group_policy": _get("groupPolicy", "disabled"),
"allow_from": _get("allowFrom", []),
"group_allow_from": _get("groupAllowFrom", []),
"text_chunk_limit": _get("textChunkLimit", 10000),
"webhook_url": _get("webhookUrl", ""),
"webhook_env": _get("webhookEnv", "dev"),
"connection_mode": _get("connectionMode", "auto"),
"polling_interval_sec": _get("pollingIntervalSec", 180),
"reaction_level": _get("reactionLevel", "ack"),
"streaming_mode": _get("streamingMode", "block"),
}
@staticmethod
def _resolve_env_credential(base_env: str, account_id: str) -> str:
if account_id != "default":
val = os.environ.get(f"{base_env}_{account_id.upper()}", "")
if val:
return val
return os.environ.get(base_env, "")