From 5863740a26106cf0ac06b53091e105b8a9787207 Mon Sep 17 00:00:00 2001 From: Ninso112 Date: Sat, 9 May 2026 18:56:49 +0200 Subject: [PATCH] fix(telegram): use native quote text for partial reply context When a user replies using Telegram's native quote feature to select only part of a previous message, the adapter was ignoring message.quote and injecting the entire replied-to message as reply_to_text. This could materially change the user's intent by exposing unrelated text to the agent. Now checks for message.quote (TextQuote) first and uses quote.text when available, falling back to the full reply_to_message.text only when no native quote exists. Fixes #22619 --- gateway/platforms/telegram.py | 7 ++- tests/gateway/test_dm_topics.py | 76 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index e680db61e6745..a7870608ade26 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -4031,7 +4031,12 @@ def _build_message_event( reply_to_text = None if message.reply_to_message: reply_to_id = str(message.reply_to_message.message_id) - reply_to_text = message.reply_to_message.text or message.reply_to_message.caption or None + # Use native partial quote text if available (Telegram's quote/reply feature), + # otherwise fall back to the full replied-to message text/caption. + if getattr(message, "quote", None): + reply_to_text = message.quote.text + else: + reply_to_text = message.reply_to_message.text or message.reply_to_message.caption or None # Per-channel/topic ephemeral prompt from gateway.platforms.base import resolve_channel_prompt diff --git a/tests/gateway/test_dm_topics.py b/tests/gateway/test_dm_topics.py index 39cabd950a4c7..fd3d822495d74 100644 --- a/tests/gateway/test_dm_topics.py +++ b/tests/gateway/test_dm_topics.py @@ -738,3 +738,79 @@ def test_build_message_event_dm_from_user_present_uses_user(): # Normal case — from_user is used directly assert event.source.user_id == "99999" assert event.source.user_name == "Bob" + + +# ── _build_message_event: native Telegram quote (partial reply) ── + + +def _make_mock_message_with_reply(chat_id=111, chat_type="private", text="reply text", + user_id=42, user_name="Test User", + reply_to_id=500, reply_to_text="original full message", + quote_text=None): + """Create a mock Telegram Message with a reply-to message, optionally with a native quote.""" + chat = SimpleNamespace(id=chat_id, type=chat_type, title=None) + chat.full_name = user_name + user = SimpleNamespace(id=user_id, full_name=user_name) + reply_to_msg = SimpleNamespace( + message_id=reply_to_id, + text=reply_to_text, + caption=None, + ) + msg = SimpleNamespace( + chat=chat, + from_user=user, + text=text, + message_thread_id=None, + message_id=1001, + reply_to_message=reply_to_msg, + date=None, + forum_topic_created=None, + ) + if quote_text is not None: + msg.quote = SimpleNamespace(text=quote_text) + return msg + + +def test_build_message_event_uses_native_quote_text_when_available(): + """When message.quote exists (Telegram partial quote), reply_to_text should use quote.text.""" + from gateway.platforms.base import MessageType + + adapter = _make_adapter() + msg = _make_mock_message_with_reply( + reply_to_text="original full message with multiple sections", + quote_text="only the selected quoted part", + ) + + event = adapter._build_message_event(msg, MessageType.TEXT) + + assert event.reply_to_message_id == "500" + assert event.reply_to_text == "only the selected quoted part" + + +def test_build_message_event_falls_back_to_full_text_without_quote(): + """When no native quote exists, reply_to_text should fall back to reply_to_message.text.""" + from gateway.platforms.base import MessageType + + adapter = _make_adapter() + msg = _make_mock_message_with_reply( + reply_to_text="original full message", + quote_text=None, + ) + + event = adapter._build_message_event(msg, MessageType.TEXT) + + assert event.reply_to_message_id == "500" + assert event.reply_to_text == "original full message" + + +def test_build_message_event_no_reply_context(): + """Messages without reply_to_message should have None reply_to fields.""" + from gateway.platforms.base import MessageType + + adapter = _make_adapter() + msg = _make_mock_message(chat_id=111, text="standalone message") + + event = adapter._build_message_event(msg, MessageType.TEXT) + + assert event.reply_to_message_id is None + assert event.reply_to_text is None