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), }