Skip to content

Commit 54d499c

Browse files
artgas1claude
andcommitted
feat: full-fidelity message data in get_history / get_messages (#4)
Listings were lossy — they returned only id/sender/date/reply/text and dropped everything else the Telethon message object already carries in the same response. Now they reflect the API object faithfully. - New message_to_dict(msg): media, grouped_id (album), reply_to, forwarded, via_bot_id, edited, pinned, engagement (views/forwards/reactions), comments, inline buttons, link_urls (hidden entity URLs), service action, ttl_period, sender_id, out. Empty fields omitted. - get_history → returns these full dicts (JSON via format_tool_result). - get_messages → format_message_line with all key flags (📎 media, album, forwarded, edited, via_bot, pinned, buttons, service) + [empty] for no text. - Fix get_media_label: check web_preview BEFORE photo so link-preview images are not mislabeled as "📎 photo". No extra API calls — all data is already on the fetched message. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 3ed791a commit 54d499c

1 file changed

Lines changed: 158 additions & 34 deletions

File tree

telegram_mcp/tools/messages.py

Lines changed: 158 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ def get_media_label(msg) -> str:
1313
msg.message but the media stays in msg.media).
1414
"""
1515
try:
16-
# стикер/голос/видео/аудио/гиф — это тоже document, поэтому проверяем их РАНЬШЕ document
16+
# Веб-превью ссылки — НЕ вложение. Проверяем ПЕРВЫМ: у сообщения со
17+
# ссылкой Telethon отдаёт картинку превью через msg.photo, иначе она
18+
# ложно пометилась бы как "photo".
19+
if getattr(msg, "web_preview", None) is not None:
20+
return ""
21+
# Стикер/голос/видео/аудио/гиф — это тоже document, проверяем РАНЬШЕ document.
1722
sticker = getattr(msg, "sticker", None)
1823
if sticker is not None:
1924
alt = ""
@@ -47,16 +52,163 @@ def get_media_label(msg) -> str:
4752
return "geo"
4853
if getattr(msg, "poll", None) is not None:
4954
return "poll"
50-
# web-превью ссылки — это не вложение, не флагуем
51-
if getattr(msg, "web_preview", None) is not None:
52-
return ""
5355
if getattr(msg, "media", None) is not None:
5456
return "media"
5557
return ""
5658
except Exception:
5759
return ""
5860

5961

62+
def _inline_button_texts(msg):
63+
"""Тексты inline-кнопок сообщения (плоским списком), [] если нет."""
64+
out = []
65+
try:
66+
for row in (getattr(msg, "buttons", None) or []):
67+
for b in row:
68+
t = getattr(b, "text", None)
69+
if t:
70+
out.append(t)
71+
except Exception:
72+
pass
73+
return out
74+
75+
76+
def _link_urls(msg):
77+
"""Явные URL из entities (скрытые за текстом ссылки), [] если нет."""
78+
out = []
79+
try:
80+
for e in (getattr(msg, "entities", None) or []):
81+
u = getattr(e, "url", None)
82+
if u:
83+
out.append(u)
84+
except Exception:
85+
pass
86+
return out
87+
88+
89+
def message_to_dict(msg) -> dict:
90+
"""API-полный, но компактный вид сообщения Telethon (пустые поля опускаем).
91+
92+
Цель — чтобы вывод MCP по полноте соответствовал объекту API, а не терял
93+
данные (медиа, альбомы, пересылки, правки, кнопки, реакции и т.п.).
94+
Все эти поля уже приходят в объекте сообщения тем же запросом get_messages.
95+
"""
96+
d = {"id": msg.id, "sender": get_sender_name(msg), "date": msg.date}
97+
98+
sender_id = getattr(msg, "sender_id", None)
99+
if sender_id is not None:
100+
d["sender_id"] = sender_id
101+
if getattr(msg, "out", False):
102+
d["out"] = True
103+
104+
text = sanitize_user_content(msg.message) if getattr(msg, "message", None) else ""
105+
if text:
106+
d["text"] = text
107+
108+
media_label = get_media_label(msg)
109+
if media_label:
110+
d["media"] = media_label
111+
112+
grouped_id = getattr(msg, "grouped_id", None)
113+
if grouped_id:
114+
d["grouped_id"] = grouped_id # альбом: сообщения с одним grouped_id — одна группа
115+
116+
reply_to_id = getattr(msg.reply_to, "reply_to_msg_id", None) if getattr(msg, "reply_to", None) else None
117+
if reply_to_id:
118+
d["reply_to"] = reply_to_id
119+
120+
fwd = getattr(msg, "fwd_from", None)
121+
if fwd is not None:
122+
finfo = {}
123+
fdate = getattr(fwd, "date", None)
124+
if fdate:
125+
finfo["date"] = fdate
126+
fname = getattr(fwd, "from_name", None)
127+
if fname:
128+
finfo["from_name"] = sanitize_name(fname)
129+
d["forwarded"] = finfo or True
130+
131+
via_bot_id = getattr(msg, "via_bot_id", None)
132+
if via_bot_id:
133+
d["via_bot_id"] = via_bot_id
134+
135+
edit_date = getattr(msg, "edit_date", None)
136+
if edit_date:
137+
d["edited"] = edit_date
138+
139+
if getattr(msg, "pinned", False):
140+
d["pinned"] = True
141+
142+
engagement = get_engagement_dict(msg)
143+
if engagement:
144+
d["engagement"] = engagement
145+
146+
replies = getattr(msg, "replies", None)
147+
if replies is not None:
148+
cnt = getattr(replies, "replies", None)
149+
if cnt is not None:
150+
d["comments"] = cnt
151+
152+
buttons = _inline_button_texts(msg)
153+
if buttons:
154+
d["buttons"] = buttons
155+
156+
urls = _link_urls(msg)
157+
if urls:
158+
d["link_urls"] = urls
159+
160+
action = getattr(msg, "action", None)
161+
if action is not None:
162+
d["action"] = type(action).__name__ # сервисное сообщение (вступил/закрепил/…)
163+
164+
ttl = getattr(msg, "ttl_period", None)
165+
if ttl:
166+
d["ttl_period"] = ttl
167+
168+
return d
169+
170+
171+
def format_message_line(msg) -> str:
172+
"""Однострочный человекочитаемый вид сообщения со ВСЕМИ ключевыми флагами."""
173+
parts = [f"ID: {msg.id}", get_sender_name(msg), f"Date: {msg.date}"]
174+
175+
reply_to_id = getattr(msg.reply_to, "reply_to_msg_id", None) if getattr(msg, "reply_to", None) else None
176+
if reply_to_id:
177+
parts.append(f"reply to {reply_to_id}")
178+
179+
flags = []
180+
media_label = get_media_label(msg)
181+
if media_label:
182+
flags.append(f"📎 {media_label}")
183+
grouped_id = getattr(msg, "grouped_id", None)
184+
if grouped_id:
185+
flags.append(f"album:{grouped_id}")
186+
if getattr(msg, "fwd_from", None) is not None:
187+
flags.append("forwarded")
188+
if getattr(msg, "edit_date", None):
189+
flags.append("edited")
190+
if getattr(msg, "via_bot_id", None):
191+
flags.append("via_bot")
192+
if getattr(msg, "pinned", False):
193+
flags.append("pinned")
194+
btn = _inline_button_texts(msg)
195+
if btn:
196+
flags.append(f"buttons:{len(btn)}")
197+
action = getattr(msg, "action", None)
198+
if action is not None:
199+
flags.append(f"service:{type(action).__name__}")
200+
if flags:
201+
parts.append(", ".join(flags))
202+
203+
engagement_info = get_engagement_info(msg).lstrip(" |").strip()
204+
if engagement_info:
205+
parts.append(engagement_info)
206+
207+
raw = sanitize_user_content(msg.message) if getattr(msg, "message", None) else ""
208+
safe_text = raw.replace("\n", "\\n") if raw else "[empty]"
209+
return " | ".join(parts) + f" | Message: {safe_text}"
210+
211+
60212
@mcp.tool(annotations=ToolAnnotations(title="Get Messages", openWorldHint=True, readOnlyHint=True))
61213
@with_account(readonly=True)
62214
@validate_id("chat_id")
@@ -79,21 +231,7 @@ async def get_messages(
79231
messages = await cl.get_messages(entity, limit=page_size, add_offset=offset)
80232
if not messages:
81233
return "No messages found for this page."
82-
lines = []
83-
for msg in messages:
84-
sender_name = get_sender_name(msg)
85-
reply_info = ""
86-
if msg.reply_to and msg.reply_to.reply_to_msg_id:
87-
reply_info = f" | reply to {msg.reply_to.reply_to_msg_id}"
88-
89-
engagement_info = get_engagement_info(msg)
90-
safe_text = sanitize_user_content(msg.message).replace("\n", "\\n")
91-
media_label = get_media_label(msg)
92-
media_info = f" | 📎 {media_label}" if media_label else ""
93-
94-
lines.append(
95-
f"ID: {msg.id} | {sender_name} | Date: {msg.date}{reply_info}{engagement_info}{media_info} | Message: {safe_text}"
96-
)
234+
lines = [format_message_line(msg) for msg in messages]
97235
return "\n".join(lines)
98236
except Exception as e:
99237
return log_and_format_error(
@@ -1237,21 +1375,7 @@ async def get_history(chat_id: Union[int, str], limit: int = 100, account: str =
12371375
entity = await resolve_entity(chat_id, cl)
12381376
messages = await cl.get_messages(entity, limit=limit)
12391377

1240-
records = []
1241-
for msg in messages:
1242-
record = {
1243-
"id": msg.id,
1244-
"sender": get_sender_name(msg),
1245-
"date": msg.date,
1246-
"text": sanitize_user_content(msg.message),
1247-
}
1248-
reply_to_id = getattr(msg.reply_to, "reply_to_msg_id", None) if msg.reply_to else None
1249-
if reply_to_id:
1250-
record["reply_to"] = reply_to_id
1251-
media_label = get_media_label(msg)
1252-
if media_label:
1253-
record["media"] = media_label
1254-
records.append(record)
1378+
records = [message_to_dict(msg) for msg in messages]
12551379
return format_tool_result(records)
12561380
except Exception as e:
12571381
return log_and_format_error("get_history", e, chat_id=chat_id, limit=limit)

0 commit comments

Comments
 (0)