fix(telegram): add DM topic typing fallback when message_thread_id rejected
When a DM topic lane's message_thread_id is rejected by Telegram (e.g. stale or deleted topic), send_typing now falls back to sending the typing indicator without thread_id so it at least appears in the main DM view, rather than being silently swallowed. Also adds test for the fallback behavior.
This commit is contained in:
@@ -3729,20 +3729,30 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||||||
async def send_typing(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
async def send_typing(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||||
"""Send typing indicator."""
|
"""Send typing indicator."""
|
||||||
if self._bot:
|
if self._bot:
|
||||||
|
_is_dm_topic: bool = False
|
||||||
|
message_thread_id: Optional[int] = None
|
||||||
try:
|
try:
|
||||||
_typing_thread = self._metadata_thread_id(metadata)
|
_typing_thread = self._metadata_thread_id(metadata)
|
||||||
|
_is_dm_topic = bool(metadata and metadata.get("telegram_dm_topic_reply_fallback"))
|
||||||
message_thread_id = self._message_thread_id_for_typing(_typing_thread)
|
message_thread_id = self._message_thread_id_for_typing(_typing_thread)
|
||||||
# No retry-without-thread fallback here: _message_thread_id_for_typing
|
|
||||||
# already maps the forum General topic to None, so any non-None value
|
|
||||||
# reaching this call is a user-created topic. If Telegram rejects it
|
|
||||||
# (e.g. topic deleted mid-session), we swallow the failure rather than
|
|
||||||
# showing a typing indicator in the wrong chat/All Messages.
|
|
||||||
await self._bot.send_chat_action(
|
await self._bot.send_chat_action(
|
||||||
chat_id=int(chat_id),
|
chat_id=int(chat_id),
|
||||||
action="typing",
|
action="typing",
|
||||||
message_thread_id=message_thread_id,
|
message_thread_id=message_thread_id,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# For DM topic lanes, Telegram may reject message_thread_id.
|
||||||
|
# Fall back to sending typing without thread_id so the typing
|
||||||
|
# indicator at least appears in the main DM view.
|
||||||
|
if _is_dm_topic and message_thread_id is not None:
|
||||||
|
try:
|
||||||
|
await self._bot.send_chat_action(
|
||||||
|
chat_id=int(chat_id),
|
||||||
|
action="typing",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
# Typing failures are non-fatal; log at debug level only.
|
# Typing failures are non-fatal; log at debug level only.
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[%s] Failed to send Telegram typing indicator: %s",
|
"[%s] Failed to send Telegram typing indicator: %s",
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ async def test_send_typing_attempts_api_call_for_dm_topic_reply_fallback():
|
|||||||
Some private DM topic lanes route message sends through reply-anchor
|
Some private DM topic lanes route message sends through reply-anchor
|
||||||
fallback, but live Telegram testing shows sendChatAction accepts the lane's
|
fallback, but live Telegram testing shows sendChatAction accepts the lane's
|
||||||
message_thread_id. If Telegram rejects a stale or invalid thread later,
|
message_thread_id. If Telegram rejects a stale or invalid thread later,
|
||||||
send_typing already swallows that failure as non-fatal.
|
send_typing now falls back to sending typing without thread_id so the
|
||||||
|
indicator at least appears in the main DM view.
|
||||||
"""
|
"""
|
||||||
adapter = _make_adapter()
|
adapter = _make_adapter()
|
||||||
call_log = []
|
call_log = []
|
||||||
@@ -330,6 +331,45 @@ async def test_send_typing_attempts_api_call_for_dm_topic_reply_fallback():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_typing_falls_back_without_thread_on_bad_request():
|
||||||
|
"""When DM topic typing with message_thread_id fails, retry without it."""
|
||||||
|
adapter = _make_adapter()
|
||||||
|
|
||||||
|
call_log = []
|
||||||
|
call_count = [0]
|
||||||
|
|
||||||
|
async def mock_send_chat_action(**kwargs):
|
||||||
|
call_log.append(dict(kwargs))
|
||||||
|
call_count[0] += 1
|
||||||
|
if call_count[0] == 1 and kwargs.get("message_thread_id") is not None:
|
||||||
|
raise FakeBadRequest("Message thread not found")
|
||||||
|
|
||||||
|
adapter._bot = SimpleNamespace(send_chat_action=mock_send_chat_action)
|
||||||
|
|
||||||
|
await adapter.send_typing(
|
||||||
|
"12345",
|
||||||
|
metadata={
|
||||||
|
"thread_id": "20197",
|
||||||
|
"telegram_dm_topic_reply_fallback": True,
|
||||||
|
"telegram_reply_to_message_id": "462",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# First call: with message_thread_id (failed)
|
||||||
|
# Second call: fallback without message_thread_id (succeeded)
|
||||||
|
assert len(call_log) == 2
|
||||||
|
assert call_log[0] == {
|
||||||
|
"chat_id": 12345,
|
||||||
|
"action": "typing",
|
||||||
|
"message_thread_id": 20197,
|
||||||
|
}
|
||||||
|
assert call_log[1] == {
|
||||||
|
"chat_id": 12345,
|
||||||
|
"action": "typing",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_retries_without_thread_on_thread_not_found():
|
async def test_send_retries_without_thread_on_thread_not_found():
|
||||||
"""When message_thread_id causes 'thread not found', retry without it."""
|
"""When message_thread_id causes 'thread not found', retry without it."""
|
||||||
|
|||||||
Reference in New Issue
Block a user