Skip to content

Commit 940c4b5

Browse files
authored
Merge pull request chigwell#232 from omid-io/fix/bot-identity-get-dialogs-fallback
fix(dialogs): handle BotMethodInvalidError when client authenticated as bot (chigwell#79)
2 parents e8371f1 + 9f7844e commit 940c4b5

6 files changed

Lines changed: 232 additions & 10 deletions

File tree

‎telegram_mcp/runner.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
except UnsafeInstallationError as exc:
88
raise SystemExit(str(exc)) from None
99

10-
from telethon.errors import AuthKeyDuplicatedError
10+
from telethon.errors import AuthKeyDuplicatedError, BotMethodInvalidError
1111

1212
from telegram_mcp import runtime as _runtime
1313
from telegram_mcp import transcription as _transcription
@@ -170,8 +170,19 @@ async def _main() -> None:
170170
print("Warming entity caches (background)...", file=sys.stderr)
171171

172172
async def _warm_caches() -> None:
173+
async def _warm_client(label: str, cl: TelegramClient) -> None:
174+
try:
175+
await cl.get_dialogs()
176+
except BotMethodInvalidError:
177+
print(
178+
f"Skipping entity cache pre-warm for bot client '{label}' (dialogs restricted for bots).",
179+
file=sys.stderr,
180+
)
181+
except Exception as exc:
182+
print(f"Entity cache warm failed for '{label}': {exc}", file=sys.stderr)
183+
173184
try:
174-
await asyncio.gather(*(cl.get_dialogs() for cl in clients.values()))
185+
await asyncio.gather(*(_warm_client(label, cl) for label, cl in clients.items()))
175186
print("Entity caches warmed.", file=sys.stderr)
176187
except Exception as warm_exc:
177188
print(f"Entity cache warm failed: {warm_exc}", file=sys.stderr)

‎telegram_mcp/runtime.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from mcp.shared.exceptions import McpError
2525
from pythonjsonlogger import jsonlogger
2626
from telethon import TelegramClient, functions, types, utils
27-
from telethon.errors import AuthKeyDuplicatedError, FloodWaitError
27+
from telethon.errors import AuthKeyDuplicatedError, FloodWaitError, BotMethodInvalidError
2828
from telethon.sessions import StringSession
2929
from telethon.tl.types import (
3030
User,
@@ -1609,7 +1609,10 @@ async def _resolve_with_retries(
16091609
return await get(identifier)
16101610
except ValueError as error:
16111611
last_error = error
1612-
await client.get_dialogs()
1612+
try:
1613+
await client.get_dialogs()
1614+
except (BotMethodInvalidError, Exception):
1615+
pass
16131616
try:
16141617
return await get(identifier)
16151618
except ValueError as error:
@@ -1620,7 +1623,10 @@ async def _resolve_with_retries(
16201623
return await get(identifier)
16211624
except ValueError as error:
16221625
last_error = error
1623-
await client.get_dialogs()
1626+
try:
1627+
await client.get_dialogs()
1628+
except (BotMethodInvalidError, Exception):
1629+
pass
16241630
try:
16251631
return await get(identifier)
16261632
except ValueError as error:

‎telegram_mcp/tools/chats.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import secrets
44
import struct
55

6+
from telethon.errors import BotMethodInvalidError
67
from telethon.tl.tlobject import TLObject, TLRequest
78

89
from telegram_mcp.runtime import *
@@ -167,7 +168,10 @@ async def get_chats(account: str = None, page: int = 1, page_size: int = 20) ->
167168
try:
168169
cl = get_client(account)
169170
await ensure_connected(cl)
170-
dialogs = await cl.get_dialogs()
171+
try:
172+
dialogs = await cl.get_dialogs()
173+
except BotMethodInvalidError:
174+
return "Listing chats/dialogs is not supported for bot accounts (Telegram API restriction: bots cannot fetch dialog lists)."
171175
start = (page - 1) * page_size
172176
end = start + page_size
173177
if start >= len(dialogs):
@@ -479,7 +483,10 @@ async def list_chats(
479483
try:
480484
cl = get_client(account)
481485
await ensure_connected(cl)
482-
dialogs = await cl.get_dialogs(limit=limit, archived=archived)
486+
try:
487+
dialogs = await cl.get_dialogs(limit=limit, archived=archived)
488+
except BotMethodInvalidError:
489+
return "Listing chats is not supported for bot accounts (Telegram API restriction: bots cannot fetch dialog lists)."
483490

484491
records = []
485492
for dialog in dialogs:

‎telegram_mcp/tools/contacts.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ async def get_direct_chat_by_contact(contact_query: str, account: Optional[str]
151151
return f"No contacts found matching '{contact_query}'."
152152
# If we found contacts, look for direct chats with them
153153
records = []
154-
dialogs = await cl.get_dialogs()
154+
try:
155+
dialogs = await cl.get_dialogs()
156+
except BotMethodInvalidError:
157+
dialogs = []
155158
for contact in found_contacts:
156159
contact_name = sanitize_name(
157160
f"{getattr(contact, 'first_name', '')} {getattr(contact, 'last_name', '')}".strip()
@@ -204,7 +207,10 @@ async def get_contact_chats(contact_id: Union[int, str], account: Optional[str]
204207
)
205208

206209
# Find direct chat
207-
dialogs = await cl.get_dialogs()
210+
try:
211+
dialogs = await cl.get_dialogs()
212+
except BotMethodInvalidError:
213+
dialogs = []
208214

209215
records = []
210216

‎telegram_mcp/tools/groups.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ async def create_group(title: str, user_ids: List[Union[int, str]], account: str
4949
# If we can't determine the chat ID directly from the result
5050
# Try to find it in recent dialogs
5151
await asyncio.sleep(1) # Give Telegram a moment to register the new group
52-
dialogs = await cl.get_dialogs(limit=5) # Get recent dialogs
52+
try:
53+
dialogs = await cl.get_dialogs(limit=5) # Get recent dialogs
54+
except BotMethodInvalidError:
55+
dialogs = []
5356
for dialog in dialogs:
5457
if dialog.title == title:
5558
return f"Group created with ID: {get_marked_id(dialog.entity)}"

‎tests/test_bot_dialogs_fallback.py‎

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import pytest
2+
from types import SimpleNamespace
3+
from unittest.mock import AsyncMock, patch
4+
5+
from telethon.errors import BotMethodInvalidError
6+
from telethon.tl.types import User
7+
8+
from telegram_mcp import runtime
9+
from telegram_mcp.tools import chats, contacts, groups
10+
11+
12+
class FakeBotClient:
13+
"""Mock Telethon client simulating a bot account token where get_dialogs is forbidden."""
14+
15+
def __init__(self, invoke_result=None):
16+
self.get_dialogs_called = False
17+
self.invoke_result = invoke_result
18+
19+
async def __call__(self, *args, **kwargs):
20+
if callable(self.invoke_result):
21+
return await self.invoke_result(*args, **kwargs)
22+
return self.invoke_result
23+
24+
async def get_dialogs(self, *args, **kwargs):
25+
self.get_dialogs_called = True
26+
raise BotMethodInvalidError(request=None)
27+
28+
29+
def _async_return(val):
30+
async def _fn(*args, **kwargs):
31+
return val
32+
33+
return _fn
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_get_chats_bot_method_invalid_error_fallback(monkeypatch):
38+
"""Verifies that get_chats handles BotMethodInvalidError cleanly without unhandled crash."""
39+
client = FakeBotClient()
40+
monkeypatch.setattr(chats, "get_client", lambda account=None: client)
41+
monkeypatch.setattr(chats, "ensure_connected", _async_return(None))
42+
43+
result = await chats.get_chats(page=1, page_size=10, account=None)
44+
45+
assert client.get_dialogs_called is True
46+
assert "Listing chats" in result or "bot accounts" in result
47+
assert "GEN-ERR" not in result
48+
assert "Traceback" not in result
49+
50+
51+
@pytest.mark.asyncio
52+
async def test_list_chats_bot_method_invalid_error_fallback(monkeypatch):
53+
"""Verifies that list_chats handles BotMethodInvalidError cleanly without unhandled crash."""
54+
client = FakeBotClient()
55+
monkeypatch.setattr(chats, "get_client", lambda account=None: client)
56+
monkeypatch.setattr(chats, "ensure_connected", _async_return(None))
57+
58+
result = await chats.list_chats(limit=20, account=None)
59+
60+
assert client.get_dialogs_called is True
61+
assert "Listing chats" in result or "bot accounts" in result
62+
assert "GEN-ERR" not in result
63+
assert "Traceback" not in result
64+
65+
66+
@pytest.mark.asyncio
67+
async def test_resolve_with_retries_swallows_bot_dialog_warm_error(monkeypatch):
68+
"""Verifies that _resolve_with_retries does not crash when get_dialogs raises BotMethodInvalidError."""
69+
client = FakeBotClient()
70+
resolved_entity = SimpleNamespace(id=12345, username="test_bot")
71+
72+
call_count = 0
73+
74+
async def fake_get_entity(identifier):
75+
nonlocal call_count
76+
call_count += 1
77+
if call_count == 1:
78+
raise ValueError("Cold entity cache miss")
79+
return resolved_entity
80+
81+
client.get_entity = fake_get_entity
82+
monkeypatch.setattr(runtime, "ensure_connected", _async_return(None))
83+
84+
result = await runtime._resolve_with_retries("get_entity", 12345, client, "user")
85+
86+
assert client.get_dialogs_called is True
87+
assert result is resolved_entity
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_get_direct_chat_by_contact_bot_fallback(monkeypatch):
92+
"""Verifies that get_direct_chat_by_contact handles BotMethodInvalidError without crashing."""
93+
contact_user = SimpleNamespace(
94+
id=999,
95+
first_name="Test",
96+
last_name="User",
97+
username="testuser",
98+
phone="+123456789",
99+
)
100+
mock_contacts_res = SimpleNamespace(users=[contact_user])
101+
client = FakeBotClient(invoke_result=mock_contacts_res)
102+
103+
monkeypatch.setattr(contacts, "get_client", lambda account=None: client)
104+
monkeypatch.setattr(contacts, "ensure_connected", _async_return(None))
105+
106+
result = await contacts.get_direct_chat_by_contact(contact_query="Test", account=None)
107+
108+
assert client.get_dialogs_called is True
109+
assert "Found contacts: Test User, but no direct chats were found" in result
110+
assert "GEN-ERR" not in result
111+
112+
113+
@pytest.mark.asyncio
114+
async def test_get_contact_chats_bot_fallback(monkeypatch):
115+
"""Verifies that get_contact_chats handles BotMethodInvalidError without crashing."""
116+
client = FakeBotClient()
117+
monkeypatch.setattr(contacts, "get_client", lambda account=None: client)
118+
monkeypatch.setattr(contacts, "ensure_connected", _async_return(None))
119+
120+
contact_user = User(
121+
id=999,
122+
is_self=False,
123+
contact=True,
124+
mutual_contact=False,
125+
deleted=False,
126+
bot=False,
127+
bot_chat_history=False,
128+
bot_nochats=False,
129+
verified=False,
130+
restricted=False,
131+
min=False,
132+
bot_inline_geo=False,
133+
support=False,
134+
scam=False,
135+
fake=False,
136+
first_name="Alice",
137+
access_hash=123456,
138+
)
139+
monkeypatch.setattr(contacts, "resolve_entity", _async_return(contact_user))
140+
141+
result = await contacts.get_contact_chats(contact_id=999, account=None)
142+
143+
assert client.get_dialogs_called is True
144+
assert "GEN-ERR" not in result
145+
146+
147+
@pytest.mark.asyncio
148+
async def test_create_group_bot_fallback(monkeypatch):
149+
"""Verifies that create_group handles BotMethodInvalidError when inspecting recent dialogs."""
150+
client = FakeBotClient(invoke_result=SimpleNamespace())
151+
monkeypatch.setattr(groups, "get_client", lambda account=None: client)
152+
monkeypatch.setattr(groups, "ensure_connected", _async_return(None))
153+
monkeypatch.setattr(groups, "resolve_entity", _async_return(SimpleNamespace(id=123)))
154+
155+
with patch("asyncio.sleep", new_callable=AsyncMock):
156+
result = await groups.create_group(title="Test Bot Group", user_ids=[123], account=None)
157+
158+
assert client.get_dialogs_called is True
159+
assert "Group created successfully" in result
160+
assert "GEN-ERR" not in result
161+
162+
163+
@pytest.mark.asyncio
164+
async def test_runner_warm_caches_bot_fallback(monkeypatch):
165+
"""Verifies that runner cache warming handles bot clients raising BotMethodInvalidError."""
166+
import asyncio
167+
168+
bot_client = FakeBotClient()
169+
user_dialog_called = False
170+
171+
class FakeUserClient:
172+
async def get_dialogs(self):
173+
nonlocal user_dialog_called
174+
user_dialog_called = True
175+
return []
176+
177+
user_client = FakeUserClient()
178+
clients = {"bot": bot_client, "user": user_client}
179+
180+
async def _warm_client(label, cl):
181+
try:
182+
await cl.get_dialogs()
183+
except BotMethodInvalidError:
184+
pass
185+
186+
await asyncio.gather(*(_warm_client(label, cl) for label, cl in clients.items()))
187+
188+
assert bot_client.get_dialogs_called is True
189+
assert user_dialog_called is True

0 commit comments

Comments
 (0)