198 lines
7.4 KiB
Python
198 lines
7.4 KiB
Python
|
|
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),
|
|||
|
|
}
|