Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions gateway/platforms/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,10 +916,28 @@ async def _deliver_cross_platform(
error=f"No chat_id or home channel for {platform_name}",
)

# Pass thread_id from deliver_extra so Telegram forum topics work
metadata = None
# Pass thread_id from deliver_extra so Telegram forum topics work.
# Also forward the Telegram DM-topic routing signals the adapter needs
# to satisfy the fail-loud anchor-required guard added in PR #32270:
# without one of telegram_reply_to_message_id, direct_messages_topic_id,
# or telegram_dm_topic_created_for_send, a private DM with a thread_id
# is rejected with "requires a reply anchor". Webhook routes have no
# other channel to express the opt-out, so plumb the keys straight
# through from deliver_extra.
metadata: Optional[Dict[str, Any]] = None
thread_id = extra.get("message_thread_id") or extra.get("thread_id")
if thread_id:
metadata = {"thread_id": thread_id}
for key in (
"telegram_reply_to_message_id",
"telegram_dm_topic_created_for_send",
"telegram_dm_topic_reply_fallback",
"direct_messages_topic_id",
):
value = extra.get(key)
if value is not None:
if metadata is None:
metadata = {}
metadata[key] = value

return await adapter.send(chat_id, content, metadata=metadata)
87 changes: 87 additions & 0 deletions tests/gateway/test_webhook_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,93 @@ async def test_no_thread_id_sends_no_metadata(self):
"12345", "hello", metadata=None
)

@pytest.mark.asyncio
async def test_telegram_reply_to_message_id_forwarded(self):
"""telegram_reply_to_message_id in deliver_extra reaches metadata so
the Telegram adapter's anchor-required guard accepts a DM-topic send."""
adapter, mock_target = self._setup_adapter_with_mock_target()
delivery = {
"deliver_extra": {
"chat_id": "12345",
"message_thread_id": "888",
"telegram_reply_to_message_id": "42",
}
}
await adapter._deliver_cross_platform("telegram", "hello", delivery)
mock_target.send.assert_awaited_once_with(
"12345",
"hello",
metadata={"thread_id": "888", "telegram_reply_to_message_id": "42"},
)

@pytest.mark.asyncio
async def test_telegram_dm_topic_opt_out_flags_forwarded(self):
"""telegram_dm_topic_created_for_send and direct_messages_topic_id are
forwarded so a webhook route can opt out of the reply-anchor guard
when sending to a true Bot-API DM topic."""
adapter, mock_target = self._setup_adapter_with_mock_target()
delivery = {
"deliver_extra": {
"chat_id": "12345",
"message_thread_id": "888",
"telegram_dm_topic_created_for_send": True,
"direct_messages_topic_id": "777",
}
}
await adapter._deliver_cross_platform("telegram", "hello", delivery)
mock_target.send.assert_awaited_once_with(
"12345",
"hello",
metadata={
"thread_id": "888",
"telegram_dm_topic_created_for_send": True,
"direct_messages_topic_id": "777",
},
)

@pytest.mark.asyncio
async def test_telegram_reply_fallback_marker_forwarded(self):
"""telegram_dm_topic_reply_fallback combined with the reply anchor lets
the Telegram adapter pick its reply_to_mode-aware send path."""
adapter, mock_target = self._setup_adapter_with_mock_target()
delivery = {
"deliver_extra": {
"chat_id": "12345",
"message_thread_id": "888",
"telegram_dm_topic_reply_fallback": True,
"telegram_reply_to_message_id": "42",
}
}
await adapter._deliver_cross_platform("telegram", "hello", delivery)
mock_target.send.assert_awaited_once_with(
"12345",
"hello",
metadata={
"thread_id": "888",
"telegram_dm_topic_reply_fallback": True,
"telegram_reply_to_message_id": "42",
},
)

@pytest.mark.asyncio
async def test_dm_topic_keys_without_thread_id(self):
"""deliver_extra may carry the opt-out flags without a thread_id (e.g.
a DM topic created by the route handler upstream). The metadata dict
still contains the forwarded keys."""
adapter, mock_target = self._setup_adapter_with_mock_target()
delivery = {
"deliver_extra": {
"chat_id": "12345",
"telegram_dm_topic_created_for_send": True,
}
}
await adapter._deliver_cross_platform("telegram", "hello", delivery)
mock_target.send.assert_awaited_once_with(
"12345",
"hello",
metadata={"telegram_dm_topic_created_for_send": True},
)


class TestInsecureNoAuthSafetyRail:
"""connect() refuses to start when INSECURE_NO_AUTH is combined with a
Expand Down
Loading