mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(telegram): restore typing indicator and thread routing for forum General topic
In Telegram forum-enabled groups, the General topic does not include message_thread_id in incoming messages (it is None). This caused: 1. Messages in General losing thread context — replies went to wrong place 2. Typing indicator failing because thread_id=1 was rejected by Telegram Fix: synthesize thread_id="1" for forum groups when message_thread_id is None, then handle it correctly per operation: - send: omit message_thread_id (Telegram rejects thread_id=1 for sends) - typing: pass thread_id=1, retry without it on "thread not found" Also centralizes thread_id extraction into _metadata_thread_id() across all send methods (send, send_voice, send_image, send_document, send_video, send_animation, send_photo), replacing ~10 duplicate patterns. Salvaged from PR #7892 by @corazzione. Closes #7877, closes #7519.
This commit is contained in:
parent
3ff18ffe14
commit
0cf7d570e2
2 changed files with 164 additions and 30 deletions
|
|
@ -45,6 +45,11 @@ class FakeRetryAfter(Exception):
|
|||
|
||||
# Build a fake telegram module tree so the adapter's internal imports work
|
||||
_fake_telegram = types.ModuleType("telegram")
|
||||
_fake_telegram.Update = object
|
||||
_fake_telegram.Bot = object
|
||||
_fake_telegram.Message = object
|
||||
_fake_telegram.InlineKeyboardButton = object
|
||||
_fake_telegram.InlineKeyboardMarkup = object
|
||||
_fake_telegram_error = types.ModuleType("telegram.error")
|
||||
_fake_telegram_error.NetworkError = FakeNetworkError
|
||||
_fake_telegram_error.BadRequest = FakeBadRequest
|
||||
|
|
@ -52,7 +57,21 @@ _fake_telegram_error.TimedOut = FakeTimedOut
|
|||
_fake_telegram.error = _fake_telegram_error
|
||||
_fake_telegram_constants = types.ModuleType("telegram.constants")
|
||||
_fake_telegram_constants.ParseMode = SimpleNamespace(MARKDOWN_V2="MarkdownV2")
|
||||
_fake_telegram_constants.ChatType = SimpleNamespace(
|
||||
GROUP="group",
|
||||
SUPERGROUP="supergroup",
|
||||
CHANNEL="channel",
|
||||
)
|
||||
_fake_telegram.constants = _fake_telegram_constants
|
||||
_fake_telegram_ext = types.ModuleType("telegram.ext")
|
||||
_fake_telegram_ext.Application = object
|
||||
_fake_telegram_ext.CommandHandler = object
|
||||
_fake_telegram_ext.CallbackQueryHandler = object
|
||||
_fake_telegram_ext.MessageHandler = object
|
||||
_fake_telegram_ext.ContextTypes = SimpleNamespace(DEFAULT_TYPE=object)
|
||||
_fake_telegram_ext.filters = object
|
||||
_fake_telegram_request = types.ModuleType("telegram.request")
|
||||
_fake_telegram_request.HTTPXRequest = object
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
|
@ -61,6 +80,8 @@ def _inject_fake_telegram(monkeypatch):
|
|||
monkeypatch.setitem(sys.modules, "telegram", _fake_telegram)
|
||||
monkeypatch.setitem(sys.modules, "telegram.error", _fake_telegram_error)
|
||||
monkeypatch.setitem(sys.modules, "telegram.constants", _fake_telegram_constants)
|
||||
monkeypatch.setitem(sys.modules, "telegram.ext", _fake_telegram_ext)
|
||||
monkeypatch.setitem(sys.modules, "telegram.request", _fake_telegram_request)
|
||||
|
||||
|
||||
def _make_adapter():
|
||||
|
|
@ -68,6 +89,7 @@ def _make_adapter():
|
|||
|
||||
config = PlatformConfig(enabled=True, token="fake-token")
|
||||
adapter = object.__new__(TelegramAdapter)
|
||||
adapter.config = config
|
||||
adapter._config = config
|
||||
adapter._platform = Platform.TELEGRAM
|
||||
adapter._connected = True
|
||||
|
|
@ -82,6 +104,81 @@ def _make_adapter():
|
|||
return adapter
|
||||
|
||||
|
||||
def test_forum_general_topic_without_message_thread_id_keeps_thread_context():
|
||||
"""Forum General-topic messages should keep synthetic thread context."""
|
||||
from gateway.platforms import telegram as telegram_mod
|
||||
|
||||
adapter = _make_adapter()
|
||||
message = SimpleNamespace(
|
||||
text="hello from General",
|
||||
caption=None,
|
||||
chat=SimpleNamespace(
|
||||
id=-100123,
|
||||
type=telegram_mod.ChatType.SUPERGROUP,
|
||||
is_forum=True,
|
||||
title="Forum group",
|
||||
),
|
||||
from_user=SimpleNamespace(id=456, full_name="Alice"),
|
||||
message_thread_id=None,
|
||||
reply_to_message=None,
|
||||
message_id=10,
|
||||
date=None,
|
||||
)
|
||||
|
||||
event = adapter._build_message_event(message, msg_type=SimpleNamespace(value="text"))
|
||||
|
||||
assert event.source.chat_id == "-100123"
|
||||
assert event.source.chat_type == "group"
|
||||
assert event.source.thread_id == "1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_omits_general_topic_thread_id():
|
||||
"""Telegram sends to forum General should omit message_thread_id=1."""
|
||||
adapter = _make_adapter()
|
||||
call_log = []
|
||||
|
||||
async def mock_send_message(**kwargs):
|
||||
call_log.append(dict(kwargs))
|
||||
return SimpleNamespace(message_id=42)
|
||||
|
||||
adapter._bot = SimpleNamespace(send_message=mock_send_message)
|
||||
|
||||
result = await adapter.send(
|
||||
chat_id="-100123",
|
||||
content="test message",
|
||||
metadata={"thread_id": "1"},
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert len(call_log) == 1
|
||||
assert call_log[0]["chat_id"] == -100123
|
||||
assert call_log[0]["text"] == "test message"
|
||||
assert call_log[0]["reply_to_message_id"] is None
|
||||
assert call_log[0]["message_thread_id"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_typing_retries_without_general_thread_when_not_found():
|
||||
"""Typing for forum General should fall back if Telegram rejects thread 1."""
|
||||
adapter = _make_adapter()
|
||||
call_log = []
|
||||
|
||||
async def mock_send_chat_action(**kwargs):
|
||||
call_log.append(dict(kwargs))
|
||||
if kwargs.get("message_thread_id") == 1:
|
||||
raise FakeBadRequest("Message thread not found")
|
||||
|
||||
adapter._bot = SimpleNamespace(send_chat_action=mock_send_chat_action)
|
||||
|
||||
await adapter.send_typing("-100123", metadata={"thread_id": "1"})
|
||||
|
||||
assert call_log == [
|
||||
{"chat_id": -100123, "action": "typing", "message_thread_id": 1},
|
||||
{"chat_id": -100123, "action": "typing", "message_thread_id": None},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_retries_without_thread_on_thread_not_found():
|
||||
"""When message_thread_id causes 'thread not found', retry without it."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue