@@ -133,13 +133,118 @@ async def annotated_handler(req):
133133# ---------------------------------------------------------------------------
134134
135135
136+ _PROXY_TYPES_SOCKS_HTTP = {"socks5" , "socks4" , "http" }
137+ _PROXY_TYPES_ALL = _PROXY_TYPES_SOCKS_HTTP | {"mtproxy" }
138+
139+
140+ def _get_proxy_env (name : str , label : str ) -> Optional [str ]:
141+ """Resolve a TELEGRAM_PROXY_* env var with optional ``_<LABEL>`` suffix.
142+
143+ Per-account values override the unsuffixed defaults so a global proxy can
144+ coexist with per-label overrides.
145+ """
146+ suffixed = os .getenv (f"TELEGRAM_PROXY_{ name } _{ label .upper ()} " )
147+ if suffixed :
148+ return suffixed
149+ return os .getenv (f"TELEGRAM_PROXY_{ name } " ) or None
150+
151+
152+ def _parse_bool_env (value : Optional [str ], default : bool ) -> bool :
153+ if value is None :
154+ return default
155+ return value .strip ().lower () in {"1" , "true" , "yes" , "on" }
156+
157+
158+ def _build_proxy_for_label (label : str ) -> tuple [Optional [Any ], Optional [Any ]]:
159+ """Return ``(proxy, connection)`` kwargs for ``TelegramClient`` for a label.
160+
161+ Reads ``TELEGRAM_PROXY_*`` env vars (with optional ``_<LABEL>`` suffix).
162+ Returns ``(None, None)`` when no proxy is configured. Raises
163+ :class:`ValidationError` for malformed configuration so the server fails
164+ fast instead of silently bypassing the proxy.
165+ """
166+ proxy_type = _get_proxy_env ("TYPE" , label )
167+ if not proxy_type :
168+ return None , None
169+
170+ proxy_type = proxy_type .strip ().lower ()
171+ if proxy_type not in _PROXY_TYPES_ALL :
172+ raise ValidationError (
173+ f"Invalid TELEGRAM_PROXY_TYPE '{ proxy_type } '. "
174+ f"Expected one of: { ', ' .join (sorted (_PROXY_TYPES_ALL ))} ."
175+ )
176+
177+ host = _get_proxy_env ("HOST" , label )
178+ port_raw = _get_proxy_env ("PORT" , label )
179+ if not host or not port_raw :
180+ raise ValidationError (
181+ "TELEGRAM_PROXY_HOST and TELEGRAM_PROXY_PORT are required when "
182+ "TELEGRAM_PROXY_TYPE is set."
183+ )
184+ try :
185+ port = int (port_raw )
186+ except ValueError as exc :
187+ raise ValidationError (
188+ f"TELEGRAM_PROXY_PORT must be an integer, got '{ port_raw } '."
189+ ) from exc
190+
191+ if proxy_type == "mtproxy" :
192+ secret = _get_proxy_env ("SECRET" , label )
193+ if not secret :
194+ raise ValidationError ("TELEGRAM_PROXY_SECRET is required for mtproxy." )
195+ try :
196+ from telethon .network import ConnectionTcpMTProxyRandomizedIntermediate
197+ except ImportError as exc : # pragma: no cover - defensive guard
198+ raise ValidationError (
199+ "Telethon MTProxy connection class is unavailable; upgrade telethon."
200+ ) from exc
201+ return (host , port , secret ), ConnectionTcpMTProxyRandomizedIntermediate
202+
203+ # SOCKS4/SOCKS5/HTTP via python-socks (Telethon's optional dependency).
204+ try :
205+ import python_socks # noqa: F401
206+ except ImportError as exc :
207+ raise ValidationError (
208+ f"Proxy type '{ proxy_type } ' requires the 'python-socks' package. "
209+ "Install it with `pip install python-socks` or `uv sync --extra proxy`."
210+ ) from exc
211+
212+ proxy : dict [str , Any ] = {
213+ "proxy_type" : proxy_type ,
214+ "addr" : host ,
215+ "port" : port ,
216+ "rdns" : _parse_bool_env (_get_proxy_env ("RDNS" , label ), default = True ),
217+ }
218+ username = _get_proxy_env ("USERNAME" , label )
219+ password = _get_proxy_env ("PASSWORD" , label )
220+ if username :
221+ proxy ["username" ] = username
222+ if password :
223+ proxy ["password" ] = password
224+ return proxy , None
225+
226+
227+ def _build_client (session : Any , label : str ) -> TelegramClient :
228+ """Construct a ``TelegramClient`` honoring per-label proxy configuration."""
229+ proxy , connection = _build_proxy_for_label (label )
230+ kwargs : dict [str , Any ] = {}
231+ if proxy is not None :
232+ kwargs ["proxy" ] = proxy
233+ if connection is not None :
234+ kwargs ["connection" ] = connection
235+ return TelegramClient (session , TELEGRAM_API_ID , TELEGRAM_API_HASH , ** kwargs )
236+
237+
136238def _discover_accounts () -> dict [str , TelegramClient ]:
137239 """Scan env vars to build account label -> TelegramClient mapping.
138240
139241 Detection rules:
140242 - TELEGRAM_SESSION_STRING_<LABEL> / TELEGRAM_SESSION_NAME_<LABEL> -> multi-mode
141243 - Unsuffixed TELEGRAM_SESSION_STRING / TELEGRAM_SESSION_NAME -> label "default"
142244 - If both suffixed and unsuffixed exist -> unsuffixed becomes "default"
245+
246+ Each client is constructed via :func:`_build_client`, which applies any
247+ matching ``TELEGRAM_PROXY_*`` configuration (optionally per-label).
143248 """
144249 accounts : dict [str , TelegramClient ] = {}
145250
@@ -149,23 +254,19 @@ def _discover_accounts() -> dict[str, TelegramClient]:
149254 for key , value in os .environ .items ():
150255 if key .startswith (prefix_str ) and value :
151256 label = key [len (prefix_str ) :].lower ()
152- accounts [label ] = TelegramClient (
153- StringSession (value ), TELEGRAM_API_ID , TELEGRAM_API_HASH
154- )
257+ accounts [label ] = _build_client (StringSession (value ), label )
155258 elif key .startswith (prefix_name ) and value :
156259 label = key [len (prefix_name ) :].lower ()
157- accounts [label ] = TelegramClient (value , TELEGRAM_API_ID , TELEGRAM_API_HASH )
260+ accounts [label ] = _build_client (value , label )
158261
159262 # Backward-compatible unsuffixed variables
160263 session_string = os .getenv ("TELEGRAM_SESSION_STRING" )
161264 session_name = os .getenv ("TELEGRAM_SESSION_NAME" )
162265
163266 if session_string and "default" not in accounts :
164- accounts ["default" ] = TelegramClient (
165- StringSession (session_string ), TELEGRAM_API_ID , TELEGRAM_API_HASH
166- )
267+ accounts ["default" ] = _build_client (StringSession (session_string ), "default" )
167268 elif session_name and "default" not in accounts :
168- accounts ["default" ] = TelegramClient (session_name , TELEGRAM_API_ID , TELEGRAM_API_HASH )
269+ accounts ["default" ] = _build_client (session_name , "default" )
169270
170271 if not accounts :
171272 print (
0 commit comments