Skip to content

Commit 48a6237

Browse files
authored
Merge pull request #157 from artgas1/fix/get-chat-wrong-dialog-upstream
fix(get_chat): read the requested chat's dialog, not the account's top dialog
2 parents 14a8388 + 3a58174 commit 48a6237

2 files changed

Lines changed: 155 additions & 23 deletions

File tree

telegram_mcp/tools/chats.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -636,30 +636,44 @@ async def get_chat(chat_id: Union[int, str], account: str = None) -> str:
636636
record["bot"] = bool(entity.bot)
637637
record["verified"] = bool(entity.verified)
638638

639-
# Get last activity if it's a dialog
639+
# Get unread count + last activity for THIS specific peer.
640+
#
641+
# NOTE: do NOT use get_dialogs(limit=1, offset_peer=entity) here. In
642+
# Telethon `offset_peer` is a pagination cursor, not a per-chat filter —
643+
# with offset_id=0 it is effectively ignored, so limit=1 returns the
644+
# account's top dialog and its unread/archived/last-message get wrongly
645+
# attributed to the requested chat. GetPeerDialogsRequest resolves the
646+
# dialog for exactly the requested peer instead.
640647
try:
641-
# Using get_dialogs might be slow if there are many dialogs
642-
# Alternative: Get entity again via get_dialogs if needed for unread count
643-
dialog = await cl.get_dialogs(limit=1, offset_id=0, offset_peer=entity)
644-
if dialog:
645-
dialog = dialog[0]
646-
record["unread"] = dialog.unread_count
647-
record["archived"] = bool(getattr(dialog, "archived", False))
648-
if dialog.message:
649-
last_msg = dialog.message
650-
sender_name = "Unknown"
651-
if last_msg.sender:
652-
sender_name = getattr(last_msg.sender, "first_name", "") or getattr(
653-
last_msg.sender, "title", "Unknown"
654-
)
655-
if hasattr(last_msg.sender, "last_name") and last_msg.sender.last_name:
656-
sender_name += f" {last_msg.sender.last_name}"
657-
sender_name = sanitize_name(sender_name.strip() or "Unknown")
658-
record["last_message"] = {
659-
"sender": sender_name,
660-
"date": last_msg.date,
661-
"text": sanitize_user_content(last_msg.message),
662-
}
648+
input_peer = await cl.get_input_entity(entity)
649+
peer_dialogs = await cl(
650+
functions.messages.GetPeerDialogsRequest(
651+
peers=[types.InputDialogPeer(peer=input_peer)]
652+
)
653+
)
654+
if getattr(peer_dialogs, "dialogs", None):
655+
dialog = peer_dialogs.dialogs[0]
656+
record["unread"] = getattr(dialog, "unread_count", 0)
657+
# folder_id == 1 is the Archive folder (None/0 == main list)
658+
record["archived"] = getattr(dialog, "folder_id", 0) == 1
659+
660+
last_messages = await cl.get_messages(entity, limit=1)
661+
if last_messages:
662+
last_msg = last_messages[0]
663+
sender_name = "Unknown"
664+
sender = getattr(last_msg, "sender", None)
665+
if sender:
666+
sender_name = getattr(sender, "first_name", "") or getattr(
667+
sender, "title", "Unknown"
668+
)
669+
if getattr(sender, "last_name", None):
670+
sender_name += f" {sender.last_name}"
671+
sender_name = sanitize_name(sender_name.strip() or "Unknown")
672+
record["last_message"] = {
673+
"sender": sender_name,
674+
"date": last_msg.date,
675+
"text": sanitize_user_content(last_msg.message),
676+
}
663677
except Exception as diag_ex:
664678
logger.warning(f"Could not get dialog info for {chat_id}: {diag_ex}")
665679

tests/test_get_chat.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import datetime
2+
import json
3+
from types import SimpleNamespace
4+
5+
import pytest
6+
7+
from telegram_mcp.tools import chats
8+
9+
# Distinct sentinel returned by get_input_entity so the test can assert the
10+
# GetPeerDialogsRequest was built for exactly the resolved peer.
11+
FAKE_INPUT_PEER = object()
12+
13+
14+
class FakeChatClient:
15+
"""Client stub whose per-peer methods let us assert get_chat reads the
16+
REQUESTED chat's dialog, not the account's top dialog.
17+
18+
Regression guard: get_chat used get_dialogs(limit=1, offset_peer=entity),
19+
where Telethon's `offset_peer` is a pagination cursor (not a filter). With
20+
offset_id=0 it is effectively ignored, so limit=1 returned the account's
21+
top dialog and mis-attributed its unread/archived/last_message to the
22+
requested chat.
23+
"""
24+
25+
def __init__(self, entity, last_message, *, unread=0, folder_id=0):
26+
self.entity = entity
27+
self._last_message = last_message
28+
self._unread = unread
29+
self._folder_id = folder_id
30+
self.peer_dialog_peers = None
31+
self.get_messages_calls = []
32+
self.get_dialogs_called = False
33+
34+
async def get_participants(self, entity, limit=0):
35+
return SimpleNamespace(total=9)
36+
37+
async def get_input_entity(self, entity):
38+
assert entity is self.entity
39+
return FAKE_INPUT_PEER
40+
41+
async def __call__(self, request):
42+
# functions.messages.GetPeerDialogsRequest for exactly the requested peer
43+
self.peer_dialog_peers = [p.peer for p in request.peers]
44+
return SimpleNamespace(
45+
dialogs=[SimpleNamespace(unread_count=self._unread, folder_id=self._folder_id)]
46+
)
47+
48+
async def get_messages(self, entity, limit=1):
49+
self.get_messages_calls.append({"entity": entity, "limit": limit})
50+
return [self._last_message]
51+
52+
async def get_dialogs(self, *args, **kwargs):
53+
self.get_dialogs_called = True
54+
raise AssertionError(
55+
"get_chat must not use get_dialogs(offset_peer=...) — it returns the "
56+
"account's top dialog, not the requested chat"
57+
)
58+
59+
60+
def _async_return(value):
61+
async def _inner(*args, **kwargs):
62+
return value
63+
64+
return _inner
65+
66+
67+
def _patch(monkeypatch, client, entity):
68+
monkeypatch.setattr(chats, "get_client", lambda account=None: client)
69+
monkeypatch.setattr(chats, "resolve_entity", _async_return(entity))
70+
monkeypatch.setattr(chats, "get_marked_id", lambda e: -1002929916934)
71+
monkeypatch.setattr(chats, "get_entity_type", lambda e: "Supergroup")
72+
73+
74+
def _parse(result):
75+
return json.loads(result.split("\n\n")[0])
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_get_chat_reads_requested_peer_not_top_dialog(monkeypatch):
80+
entity = SimpleNamespace(title="Технический Мониторинг", username=None)
81+
last_msg = SimpleNamespace(
82+
date=datetime.datetime(2026, 7, 19, 1, 0, 0, tzinfo=datetime.timezone.utc),
83+
message="NetBird mesh alert",
84+
sender=SimpleNamespace(first_name="cobalt_quartz_bot", last_name=None, title=None),
85+
)
86+
client = FakeChatClient(entity, last_msg, unread=5, folder_id=0)
87+
_patch(monkeypatch, client, entity)
88+
89+
result = await chats.get_chat(chat_id=-1002929916934, account=None)
90+
payload = _parse(result)
91+
92+
assert "GEN-ERR" not in result
93+
# last_message comes from the requested chat, not a foreign top dialog
94+
assert payload["last_message"]["sender"] == "cobalt_quartz_bot"
95+
assert payload["last_message"]["text"] == "NetBird mesh alert"
96+
assert payload["unread"] == 5
97+
assert payload["archived"] is False
98+
# resolved via GetPeerDialogsRequest for exactly this peer, never get_dialogs
99+
assert client.peer_dialog_peers == [FAKE_INPUT_PEER]
100+
assert client.get_messages_calls == [{"entity": entity, "limit": 1}]
101+
assert client.get_dialogs_called is False
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_get_chat_marks_archived_from_folder_id(monkeypatch):
106+
entity = SimpleNamespace(title="Archived Group", username=None)
107+
last_msg = SimpleNamespace(
108+
date=datetime.datetime(2026, 7, 19, 1, 0, 0, tzinfo=datetime.timezone.utc),
109+
message="hi",
110+
sender=SimpleNamespace(first_name="Ada", last_name="Lovelace", title=None),
111+
)
112+
client = FakeChatClient(entity, last_msg, unread=0, folder_id=1)
113+
_patch(monkeypatch, client, entity)
114+
115+
payload = _parse(await chats.get_chat(chat_id=-100777, account=None))
116+
117+
assert payload["archived"] is True
118+
assert payload["last_message"]["sender"] == "Ada Lovelace"

0 commit comments

Comments
 (0)