diff --git a/telebot/formatting.py b/telebot/formatting.py index 73f737fd4..bfb278f73 100644 --- a/telebot/formatting.py +++ b/telebot/formatting.py @@ -423,13 +423,13 @@ def format_entity(entity, content): # username = content[1:] # Remove @ symbol # return f"{content}" elif entity_type == "text_link" and hasattr(entity, 'url'): - return _subs["text_link"].format(url=entity.url, text=content) + return _subs["text_link"].format(url=escape_html(entity.url), text=content) elif entity_type == "custom_emoji" and hasattr(entity, 'custom_emoji_id'): return _subs["custom_emoji"].format(custom_emoji_id=entity.custom_emoji_id, text=content) elif entity_type == "date_time" and hasattr(entity, 'unix_time') and hasattr(entity, 'date_time_format'): - return _subs["date_time"].format(unix=entity.unix_time, date_time_format=entity.date_time_format, text=content) + return _subs["date_time"].format(unix_time=entity.unix_time, date_time_format=entity.date_time_format, text=content) elif entity_type == "pre" and hasattr(entity, 'language') and entity.language: - return f"
{content}
" + return f"
{content}
" elif entity_type in _subs: template = _subs[entity_type] return template.format(text=content) 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_formatting.py b/tests/test_formatting.py index eb0eabdfb..68319e86d 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -4,7 +4,8 @@ from html.parser import HTMLParser -from telebot.formatting import hpre +from telebot import types +from telebot.formatting import apply_html_entities, hpre class AttributeCollector(HTMLParser): @@ -35,3 +36,41 @@ def test_hpre_language_cannot_add_attributes(): def test_hpre_still_escapes_content(): assert hpre("", language="python") == '
<b>
' + + +def test_apply_html_entities_date_time(): + # used to raise KeyError: 'unix_time' because the template was formatted with + # unix= while it declares {unix_time} + entity = types.MessageEntity( + type="date_time", offset=11, length=5, + unix_time=1735689600, date_time_format="r" + ) + assert apply_html_entities("Meeting at 10:00", [entity]) == ( + 'Meeting at 10:00' + ) + + +def test_apply_html_entities_escapes_link_url(): + entity = types.MessageEntity( + type="text_link", offset=0, length=4, url='https://x.test/?a="b' + ) + parser = AttributeCollector() + parser.feed(apply_html_entities("link rest", [entity])) + assert parser.tags == [("a", [("href", 'https://x.test/?a="b')])] + + +def test_apply_html_entities_escapes_pre_language(): + entity = types.MessageEntity( + type="pre", offset=0, length=4, language='py" onload="x' + ) + parser = AttributeCollector() + parser.feed(apply_html_entities("code rest", [entity])) + assert parser.tags == [ + ("pre", []), + ("code", [("class", 'language-py" onload="x')]), + ] + + +def test_apply_html_entities_still_escapes_content(): + entity = types.MessageEntity(type="bold", offset=0, length=4) + assert apply_html_entities("a c", [entity]) == "a<b> c" 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