新增 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: 媒体资源处理
219 lines
8.2 KiB
Python
219 lines
8.2 KiB
Python
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, "")
|