新增 Telegram 渠道扩展,支持在 Yuxi 平台中集成 Telegram 即时通讯渠道。 包含以下功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - polling: 长轮询模式 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - actions: 动作处理 - inline_keyboard: 内联键盘 - native_commands: 原生指令 - chat: 聊天管理 - delivery: 消息送达确认 - media: 媒体资源处理 - profile: 用户资料 - reactions: 表情反应 - sticker: 贴纸处理 - types: 类型定义
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),
|
||
}
|