From 3b0f62b2b7c8196d7bb02f80798e70a7d1013c54 Mon Sep 17 00:00:00 2001 From: beduldul Date: Mon, 26 Oct 2026 10:32:17 +0700 Subject: [PATCH] fix(util): drop the empty part smart_split appends, accept None in extract_arguments smart_split compared with `<`, so text whose length is an exact multiple of chars_per_string went through the split path one last time on the remainder and appended ''. smart_split("abc", 3) returned ["abc", ""], and sending that empty part to Telegram fails with "message text is empty". extract_arguments compiled its regexp and matched before looking at None, so a non-text message raised TypeError on message.text. is_command and extract_command already tolerate None, so this now matches them. --- telebot/util.py | 3 ++- tests/test_util.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 tests/test_util.py diff --git a/telebot/util.py b/telebot/util.py index bbb9b98af..181898230 100644 --- a/telebot/util.py +++ b/telebot/util.py @@ -279,6 +279,7 @@ def extract_arguments(text: str) -> Union[str, None]: :rtype: :obj:`str` or :obj:`None` """ regexp = re.compile(r"/\w*(@\w*)*\s*([\s\S]*)", re.IGNORECASE) + if text is None: return None result = regexp.match(text) return result.group(2) if is_command(text) else None @@ -357,7 +358,7 @@ def _text_before_last(substr: str) -> str: parts = [] while True: - if len(text) < chars_per_string: + if len(text) <= chars_per_string: parts.append(text) return parts diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 000000000..50220f333 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,38 @@ +from telebot import util + + +def test_smart_split_exact_multiple_has_no_trailing_empty_part(): + # A chunk boundary landing exactly on the limit used to append an empty part. + for size in (1, 2, 3, 4, 10): + for chunks in (1, 2, 3): + text = "a" * (size * chunks) + assert util.smart_split(text, size) == ["a" * size] * chunks + + +def test_smart_split_short_text(): + assert util.smart_split("abc", 3) == ["abc"] + assert util.smart_split("abcde", 3) == ["abc", "de"] + assert util.smart_split("", 3) == [""] + + +def test_smart_split_prefers_newline_then_space(): + assert util.smart_split("one two\nthree four", 10) == ["one two\n", "three four"] + assert util.smart_split("one two three", 8) == ["one two ", "three"] + + +def test_smart_split_caps_at_max_message_length(): + parts = util.smart_split("a" * (util.MAX_MESSAGE_LENGTH * 2), util.MAX_MESSAGE_LENGTH * 5) + assert [len(p) for p in parts] == [util.MAX_MESSAGE_LENGTH] * 2 + + +def test_extract_arguments_accepts_none(): + # message.text is None for non-text messages + assert util.extract_arguments(None) is None # type: ignore[arg-type] + assert util.extract_command(None) is None # type: ignore[arg-type] + + +def test_extract_arguments_values(): + assert util.extract_arguments("/get name") == "name" + assert util.extract_arguments("/get") == "" + assert util.extract_arguments("/get@bot name") == "name" + assert util.extract_arguments("hello") is None