from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from dotenv import load_dotenv # 优先从包上级目录(wx_python 根)加载 .env,避免工作目录不在项目根时失效 _ROOT = Path(__file__).resolve().parent.parent load_dotenv(_ROOT / ".env") load_dotenv() def _parse_bot_usernames(raw: str) -> tuple[str, ...]: """从逗号分隔字符串解析 Bot 用户名(去 @、去空)。""" parts = [p.strip().lstrip("@") for p in raw.split(",")] bots = tuple(p for p in parts if p) if not bots: raise RuntimeError( "缺少 TELEGRAM_BOT_USERNAME:填写至少一个 Bot 用户名;多个用英文逗号分隔,不要带 @ 也可" ) return bots @dataclass(frozen=True) class Settings: api_id: int api_hash: str session_path: Path """已配置的 Bot 用户名列表(顺序:默认目标为第一个)。""" bot_usernames: tuple[str, ...] bridge_host: str bridge_port: int bridge_token: str | None # Telethon 连接 Telegram 用的代理(与系统「全局代理」无关,需显式配置) proxy_type: str | None proxy_host: str | None proxy_port: int | None proxy_user: str | None proxy_password: str | None # SOCKS 上 rdns=True 会在代理侧解析域名,部分代理会卡死;默认 False 多为本机解析再连 proxy_rdns: bool # Telethon 单次连接超时秒数(默认 10 经代理不够) connect_timeout: int connection_retries: int retry_delay: int # Telethon Connection 类:tcp_obfuscated 在受限网络 + 代理下常比 tcp_full 更稳 connection_mode: str # /v1/forward 在 wait_reply 时等待 Bot 回复的上限(秒) bot_reply_timeout: float # wait_reply 时默认取 Bot 第几条连续回复(可被请求体 reply_take_nth 覆盖) bot_reply_take_nth: int @property def default_bot_username(self) -> str: return self.bot_usernames[0] def resolve_bot_username(self, bot: str | None) -> str: """将请求里的 bot 解析为已配置的用户名;省略则用列表第一个。""" if bot is None: return self.bot_usernames[0] key = bot.strip().lstrip("@").lower() by_lower = {b.lower(): b for b in self.bot_usernames} if key not in by_lower: raise ValueError( f"未知 Bot「{bot}」,当前允许: {', '.join(self.bot_usernames)}" ) return by_lower[key] @staticmethod def load() -> "Settings": api_id_raw = os.environ.get("TELEGRAM_API_ID", "").strip() api_hash = os.environ.get("TELEGRAM_API_HASH", "").strip() if not api_id_raw or not api_hash: raise RuntimeError( "缺少 TELEGRAM_API_ID / TELEGRAM_API_HASH。" "请到 https://my.telegram.org 申请后写入环境变量或 .env" ) bot_raw = os.environ.get("TELEGRAM_BOT_USERNAME", "").strip() bots = _parse_bot_usernames(bot_raw) # 默认可写绝对路径:避免「login 在项目目录、服务从别的工作目录启动」找不到同一份 session _default_session = (_ROOT / "tg_bridge.session").resolve() session_raw = os.environ.get("TELEGRAM_SESSION_PATH", "").strip() session = ( Path(session_raw).expanduser().resolve() if session_raw else _default_session ) token = os.environ.get("BRIDGE_TOKEN", "").strip() or None host = os.environ.get("BRIDGE_HOST", "0.0.0.0").strip() port = int(os.environ.get("BRIDGE_PORT", "18080")) ptype = os.environ.get("TELEGRAM_PROXY_TYPE", "").strip() or None phost = os.environ.get("TELEGRAM_PROXY_HOST", "").strip() or None pport_raw = os.environ.get("TELEGRAM_PROXY_PORT", "").strip() pport: int | None = int(pport_raw) if pport_raw else None puser = os.environ.get("TELEGRAM_PROXY_USER", "").strip() or None ppwd_raw = os.environ.get("TELEGRAM_PROXY_PASSWORD") ppwd: str | None = None if puser is not None: ppwd = ppwd_raw if ppwd_raw is not None else "" if ptype and (not phost or not pport): raise RuntimeError( "已设置 TELEGRAM_PROXY_TYPE 时,须同时设置 TELEGRAM_PROXY_HOST 与 TELEGRAM_PROXY_PORT" ) if (phost or pport_raw) and not ptype: raise RuntimeError("设置 TELEGRAM_PROXY_HOST / TELEGRAM_PROXY_PORT 时须设置 TELEGRAM_PROXY_TYPE(http|socks5|socks4)") rdns_raw = os.environ.get("TELEGRAM_PROXY_RDNS", "").strip().lower() if rdns_raw in ("1", "true", "yes", "on"): proxy_rdns = True elif rdns_raw in ("0", "false", "no", "off"): proxy_rdns = False elif not rdns_raw: # 未配置:HTTP 代理常用远程 DNS;SOCKS 默认本机解析更稳 proxy_rdns = bool(ptype and str(ptype).lower() in ("http", "https")) else: raise RuntimeError("TELEGRAM_PROXY_RDNS 请使用 true/false") connect_timeout = int(os.environ.get("TELEGRAM_CONNECT_TIMEOUT", "90")) connection_retries = int(os.environ.get("TELEGRAM_CONNECTION_RETRIES", "5")) retry_delay = int(os.environ.get("TELEGRAM_RETRY_DELAY", "3")) connection_mode = os.environ.get("TELEGRAM_CONNECTION", "tcp_full").strip() or "tcp_full" bot_reply_timeout = float(os.environ.get("TELEGRAM_BOT_REPLY_TIMEOUT", "120")) bot_reply_take_nth = int(os.environ.get("BOT_REPLY_TAKE_NTH", "1")) if bot_reply_take_nth < 1 or bot_reply_take_nth > 20: raise RuntimeError("BOT_REPLY_TAKE_NTH 须在 1~20 之间") return Settings( api_id=int(api_id_raw), api_hash=api_hash, session_path=session, bot_usernames=bots, bridge_host=host, bridge_port=port, bridge_token=token, proxy_type=ptype, proxy_host=phost, proxy_port=pport, proxy_user=puser, proxy_password=ppwd, proxy_rdns=proxy_rdns, connect_timeout=connect_timeout, connection_retries=connection_retries, retry_delay=retry_delay, connection_mode=connection_mode, bot_reply_timeout=bot_reply_timeout, bot_reply_take_nth=bot_reply_take_nth, )