ForcePilot/backend/package/yuxi/channel/extensions/telegram/config.py

198 lines
7.4 KiB
Python
Raw Normal View History

from __future__ import annotations
import hashlib
import logging
import os
logger = logging.getLogger(__name__)
class TelegramConfigAdapter:
def __init__(self):
self._config: dict = {}
@property
def _tg_cfg(self) -> dict:
return self._config.get("channels", {}).get("telegram", {})
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
accounts = self._tg_cfg.get("accounts", {})
if accounts:
return list(accounts.keys())
if self._env_token_exists():
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 bool(account.get("token"))
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", ""),
"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._tg_cfg.get("defaultAccount", "default")
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "Telegram 渠道配置",
"properties": {
"botToken": {
"type": "string",
"title": "Bot Token",
"description": "Telegram Bot Token从 @BotFather 获取,也可通过环境变量 TELEGRAM_BOT_TOKEN 注入)",
"x-ui-password": True,
},
"tokenFile": {
"type": "string",
"title": "Token 文件路径",
"description": "存放 Bot Token 的文件路径(与 botToken 互斥,优先级低于 botToken",
},
"dm_policy": {
"type": "string",
"title": "DM 策略",
"enum": ["pairing", "allowlist", "open", "disabled"],
"default": "pairing",
},
"group_policy": {
"type": "string",
"title": "群组策略",
"enum": ["open", "disabled", "allowlist"],
"default": "open",
},
"text_chunk_limit": {
"type": "integer",
"title": "文本分块上限",
"default": 4096,
"minimum": 100,
"maximum": 4096,
},
"webhook_url": {
"type": "string",
"title": "Webhook URL",
"description": "Telegram Webhook 回调地址(公网可访问),留空则使用 Long Polling",
},
"webhook_secret": {
"type": "string",
"title": "Webhook Secret",
"description": "X-Telegram-Bot-Api-Secret-Token 验证令牌",
"x-ui-password": True,
},
"reaction_level": {
"type": "string",
"title": "Reaction 级别",
"enum": ["off", "ack", "minimal"],
"default": "minimal",
},
"streaming_mode": {
"type": "string",
"title": "流式模式",
"enum": ["off", "partial", "block", "progress"],
"default": "partial",
},
},
}
@staticmethod
def token_fingerprint(token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()[:8]
@staticmethod
def _env_bot_token(account_id: str) -> str:
if account_id != "default":
key = f"TELEGRAM_BOT_TOKEN_{account_id.upper()}"
val = os.environ.get(key, "")
if val:
return val
return os.environ.get("TELEGRAM_BOT_TOKEN", "")
@staticmethod
def _env_token_exists() -> bool:
return bool(os.environ.get("TELEGRAM_BOT_TOKEN", ""))
@staticmethod
def _read_token_file(file_path: str) -> str:
try:
return open(file_path).read().strip()
except OSError:
logger.warning("Telegram tokenFile read failed: %s", file_path)
return ""
def _build_account(self, account_id: str) -> dict:
tg_cfg = self._tg_cfg
accounts = tg_cfg.get("accounts", {})
account_raw = accounts.get(account_id, {}) if account_id != "default" else tg_cfg
token = ""
token_source = "none"
env_token = self._env_bot_token(account_id)
if env_token:
token = env_token
token_source = "env"
elif account_raw.get("botToken"):
token = account_raw["botToken"]
token_source = "config"
elif account_raw.get("tokenFile") or tg_cfg.get("tokenFile"):
file_path = account_raw.get("tokenFile", tg_cfg.get("tokenFile", ""))
token = self._read_token_file(file_path)
if token:
token_source = "tokenFile"
def _get(key: str, default=None):
return account_raw.get(key, tg_cfg.get(key, default))
def _get_nested(*keys, default=None):
acct_val = account_raw
top_val = tg_cfg
for key in keys[:-1]:
acct_val = acct_val.get(key, {}) if isinstance(acct_val, dict) else {}
top_val = top_val.get(key, {}) if isinstance(top_val, dict) else {}
last = keys[-1]
return (
acct_val.get(last, top_val.get(last, default))
if isinstance(acct_val, dict)
else top_val.get(last, default)
)
return {
"account_id": account_id,
"token": token,
"token_source": token_source,
"name": account_raw.get("name", account_id),
"enabled": account_raw.get("enabled", True),
"dm_policy": _get("dmPolicy", "pairing"),
"group_policy": _get("groupPolicy", "open"),
"allow_from": _get("allowFrom", []),
"group_allow_from": _get("groupAllowFrom", []),
"text_chunk_limit": _get("textChunkLimit", 4096),
"webhook_url": _get("webhookUrl", ""),
"webhook_secret": _get("webhookSecret", ""),
"reaction_level": _get("reactionLevel", "minimal"),
"reaction_notifications": _get("reactionNotifications", "own"),
"streaming_mode": _get_nested("streaming", "mode", default="partial"),
"streaming_min_chars": _get_nested("streaming", "minChars", default=18),
"streaming_idle_ms": _get_nested("streaming", "idleMs", default=500),
"groups": _get("groups", {}),
"dms": _get("dms", {}),
"custom_commands": _get("customCommands", []),
"actions_reactions": _get_nested("actions", "reactions", default=True),
"actions_send_message": _get_nested("actions", "sendMessage", default=True),
"actions_poll": _get_nested("actions", "poll", default=True),
}