From 0fbf05d72f840f86890a8b50c597d042a84075a9 Mon Sep 17 00:00:00 2001 From: qqcomeup Date: Tue, 7 Jul 2026 19:58:49 +0800 Subject: [PATCH] fix: handle Telegram urllib3 header formatter compatibility (#6074) --- app/modules/telegram/compat.py | 24 ++++++ app/modules/telegram/telegram.py | 37 +++++---- tests/test_telegram_urllib3_compat.py | 112 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 app/modules/telegram/compat.py create mode 100644 tests/test_telegram_urllib3_compat.py diff --git a/app/modules/telegram/compat.py b/app/modules/telegram/compat.py new file mode 100644 index 000000000..b5b1d515c --- /dev/null +++ b/app/modules/telegram/compat.py @@ -0,0 +1,24 @@ +def ensure_urllib3_header_param_compat() -> None: + """ + pyTelegramBotAPI imports urllib3.fields.format_header_param at import time. + Some urllib3-future builds only expose newer formatter names. + RFC 2231 formatting is kept as the last fallback because it encodes + non-ASCII values differently from urllib3's old default. + """ + try: + from urllib3 import fields + except ImportError: + return + + if hasattr(fields, "format_header_param"): + return + + for fallback_name in ( + "format_header_param_html5", + "format_multipart_header_param", + "format_header_param_rfc2231", + ): + fallback = getattr(fields, fallback_name, None) + if fallback is not None: + fields.format_header_param = fallback + return diff --git a/app/modules/telegram/telegram.py b/app/modules/telegram/telegram.py index ef5926cfe..4d1b2b402 100644 --- a/app/modules/telegram/telegram.py +++ b/app/modules/telegram/telegram.py @@ -8,36 +8,41 @@ from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Union from urllib.parse import urljoin, quote -from telebot import TeleBot, apihelper -from telebot.types import ( +from app.modules.telegram.compat import ensure_urllib3_header_param_compat + +# Must run before importing pyTelegramBotAPI. +ensure_urllib3_header_param_compat() + +from telebot import TeleBot, apihelper # noqa: E402 +from telebot.types import ( # noqa: E402 BotCommand, InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto, ) try: - from telebot.types import ForceReply + from telebot.types import ForceReply # noqa: E402 except ImportError: ForceReply = None -from telegramify_markdown import standardize, telegramify # noqa +from telegramify_markdown import standardize, telegramify # noqa: E402 try: - from telegramify_markdown import entities_to_markdownv2 # noqa + from telegramify_markdown import entities_to_markdownv2 # noqa: E402 except ImportError: entities_to_markdownv2 = None try: - from telegramify_markdown.content import ContentTypes, File, Photo, Text + from telegramify_markdown.content import ContentTypes, File, Photo, Text # noqa: E402 except ImportError: - from telegramify_markdown.type import ContentTypes, File, Photo, Text + from telegramify_markdown.type import ContentTypes, File, Photo, Text # noqa: E402 -from app.core.config import settings -from app.core.context import MediaInfo, Context -from app.core.metainfo import MetaInfo -from app.helper.image import ImageHelper -from app.helper.thread import ThreadHelper -from app.log import logger -from app.utils.common import retry -from app.utils.http import RequestUtils -from app.utils.string import StringUtils +from app.core.config import settings # noqa: E402 +from app.core.context import MediaInfo, Context # noqa: E402 +from app.core.metainfo import MetaInfo # noqa: E402 +from app.helper.image import ImageHelper # noqa: E402 +from app.helper.thread import ThreadHelper # noqa: E402 +from app.log import logger # noqa: E402 +from app.utils.common import retry # noqa: E402 +from app.utils.http import RequestUtils # noqa: E402 +from app.utils.string import StringUtils # noqa: E402 TELEGRAM_PARSE_MODE_MARKDOWN = "MarkdownV2" diff --git a/tests/test_telegram_urllib3_compat.py b/tests/test_telegram_urllib3_compat.py new file mode 100644 index 000000000..0bbb39cc3 --- /dev/null +++ b/tests/test_telegram_urllib3_compat.py @@ -0,0 +1,112 @@ +import importlib +import sys +from types import SimpleNamespace + +import pytest + + +def test_ensure_urllib3_header_param_compat_adds_best_available_alias(monkeypatch): + """urllib3 future 缺少旧别名时,应补齐最接近的新入口。""" + from app.modules.telegram.compat import ensure_urllib3_header_param_compat + + def multipart_formatter(name, value): + return f"multipart:{name}={value}" + + def rfc2231_formatter(name, value): + return f"rfc2231:{name}={value}" + + fake_fields = SimpleNamespace( + format_multipart_header_param=multipart_formatter, + format_header_param_rfc2231=rfc2231_formatter, + ) + fake_urllib3 = SimpleNamespace(fields=fake_fields) + monkeypatch.setitem(sys.modules, "urllib3", fake_urllib3) + + ensure_urllib3_header_param_compat() + + assert fake_fields.format_header_param is multipart_formatter + + +def test_ensure_urllib3_header_param_compat_prefers_html5_formatter(monkeypatch): + """旧版本 urllib3 的 html5 formatter 存在时,应优先保持原别名语义。""" + from app.modules.telegram.compat import ensure_urllib3_header_param_compat + + def html5_formatter(name, value): + return f"html5:{name}={value}" + + def multipart_formatter(name, value): + return f"multipart:{name}={value}" + + fake_fields = SimpleNamespace( + format_header_param_html5=html5_formatter, + format_multipart_header_param=multipart_formatter, + ) + fake_urllib3 = SimpleNamespace(fields=fake_fields) + monkeypatch.setitem(sys.modules, "urllib3", fake_urllib3) + + ensure_urllib3_header_param_compat() + + assert fake_fields.format_header_param is html5_formatter + + +def test_ensure_urllib3_header_param_compat_keeps_existing_formatter(monkeypatch): + """已有 format_header_param 时不应覆盖,避免改变 urllib3 正常行为。""" + from app.modules.telegram.compat import ensure_urllib3_header_param_compat + + def existing_formatter(name, value): + return f"existing:{name}={value}" + + def rfc2231_formatter(name, value): + return f"fallback:{name}={value}" + + fake_fields = SimpleNamespace( + format_header_param=existing_formatter, + format_header_param_rfc2231=rfc2231_formatter, + ) + fake_urllib3 = SimpleNamespace(fields=fake_fields) + monkeypatch.setitem(sys.modules, "urllib3", fake_urllib3) + + ensure_urllib3_header_param_compat() + + assert fake_fields.format_header_param is existing_formatter + + +def test_ensure_urllib3_header_param_compat_noops_without_fallback(monkeypatch): + """没有任何可用 fallback 时保持 no-op,让原始导入错误暴露。""" + from app.modules.telegram.compat import ensure_urllib3_header_param_compat + + fake_fields = SimpleNamespace() + fake_urllib3 = SimpleNamespace(fields=fake_fields) + monkeypatch.setitem(sys.modules, "urllib3", fake_urllib3) + + ensure_urllib3_header_param_compat() + + assert not hasattr(fake_fields, "format_header_param") + + +def test_telegram_module_imports_when_urllib3_header_alias_missing(monkeypatch): + """导入 Telegram 模块前旧别名缺失时,应先补齐再导入 telebot。""" + from urllib3 import fields + + if not any( + hasattr(fields, name) + for name in ( + "format_header_param_html5", + "format_multipart_header_param", + "format_header_param_rfc2231", + ) + ): + pytest.skip("urllib3 has no compatible header formatter fallback") + + monkeypatch.delattr(fields, "format_header_param", raising=False) + for module_name in list(sys.modules): + if ( + module_name == "app.modules.telegram.telegram" + or module_name.startswith("telebot") + ): + monkeypatch.delitem(sys.modules, module_name, raising=False) + + telegram_module = importlib.import_module("app.modules.telegram.telegram") + + assert telegram_module.Telegram + assert hasattr(fields, "format_header_param")