@overpod/mcp-telegram 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@overpod/mcp-telegram)](https://www.npmjs.com/package/@overpod/mcp-telegram)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@overpod/mcp-telegram)](https://www.npmjs.com/package/@overpod/mcp-telegram)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-339933.svg?logo=node.js&logoColor=white)](https://nodejs.org/)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-6.0-blue.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
7
7
  [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.27-green.svg)](https://modelcontextprotocol.io/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
9
  [![mcp-telegram MCP server](https://glama.ai/mcp/servers/overpod/mcp-telegram/badges/score.svg)](https://glama.ai/mcp/servers/overpod/mcp-telegram)
@@ -208,6 +208,7 @@ const telegramMcp = new MCPClient({
208
208
  | `telegram-read-messages` | Read recent messages from a chat |
209
209
  | `telegram-search-chats` | Search for chats, users, or channels by name |
210
210
  | `telegram-search-messages` | Search messages in a chat by text |
211
+ | `telegram-search-global` | Search messages across all public chats and channels |
211
212
  | `telegram-get-unread` | Get chats with unread messages; forums show per-topic unread breakdown |
212
213
  | `telegram-get-contact-requests` | Get incoming messages from non-contacts with preview |
213
214
 
@@ -243,6 +244,8 @@ const telegramMcp = new MCPClient({
243
244
  |------|-------------|
244
245
  | `telegram-get-contacts` | Get your contacts list with phone numbers |
245
246
  | `telegram-get-profile` | Get detailed profile info for a user (bio, photo, last seen) |
247
+ | `telegram-get-profile-photo` | Get a user's or chat's profile photo (inline or file) |
248
+ | `telegram-get-reactions` | Get reactions on a specific message with user details |
246
249
 
247
250
  ### Media
248
251
 
@@ -357,7 +360,8 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
357
360
  |-----------|------|----------|-------------|
358
361
  | `chatId` | string | yes | Chat ID or @username |
359
362
  | `messageId` | number | yes | Message ID to react to |
360
- | `emoji` | string | no | Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction |
363
+ | `emoji` | string \| string[] | no | Single emoji `"👍"` or array `["👍","🔥"]`. Omit to remove all reactions |
364
+ | `addToExisting` | boolean | no | Add to existing reactions instead of replacing (default: false) |
361
365
 
362
366
  ### telegram-send-scheduled
363
367
 
@@ -388,6 +392,15 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
388
392
  | `query` | string | yes | Search text |
389
393
  | `limit` | number | no | Max results (default: 20) |
390
394
 
395
+ ### telegram-search-global
396
+
397
+ | Parameter | Type | Required | Description |
398
+ |-----------|------|----------|-------------|
399
+ | `query` | string | yes | Search text |
400
+ | `limit` | number | no | Max results (default: 20) |
401
+ | `minDate` | number | no | Unix timestamp: only messages after this date |
402
+ | `maxDate` | number | no | Unix timestamp: only messages before this date |
403
+
391
404
  ### telegram-search-chats
392
405
 
393
406
  | Parameter | Type | Required | Description |
@@ -414,6 +427,21 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
414
427
  |-----------|------|----------|-------------|
415
428
  | `userId` | string | yes | User ID or @username |
416
429
 
430
+ ### telegram-get-profile-photo
431
+
432
+ | Parameter | Type | Required | Description |
433
+ |-----------|------|----------|-------------|
434
+ | `entityId` | string | yes | User/Chat/Channel ID or username |
435
+ | `savePath` | string | no | Absolute path to save file. If omitted, returns inline base64 image |
436
+ | `size` | `"small"` / `"big"` | no | Photo size: small (160x160) or big (640x640). Default: big |
437
+
438
+ ### telegram-get-reactions
439
+
440
+ | Parameter | Type | Required | Description |
441
+ |-----------|------|----------|-------------|
442
+ | `chatId` | string | yes | Chat ID or @username |
443
+ | `messageId` | number | yes | Message ID to get reactions for |
444
+
417
445
  ### telegram-get-unread
418
446
 
419
447
  | Parameter | Type | Required | Description |
package/dist/index.js CHANGED
@@ -18,6 +18,17 @@ if (!API_ID || !API_HASH) {
18
18
  process.exit(1);
19
19
  }
20
20
  const telegram = new TelegramService(API_ID, API_HASH);
21
+ /** Remove unpaired UTF-16 surrogates that break JSON serialization */
22
+ function sanitize(text) {
23
+ return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "\uFFFD");
24
+ }
25
+ /** Format reactions array into compact text like: [👍×5 ❤️×3(me) 🔥×1] */
26
+ function formatReactions(reactions) {
27
+ if (!reactions?.length)
28
+ return "";
29
+ const parts = reactions.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`);
30
+ return ` [${parts.join(" ")}]`;
31
+ }
21
32
  const server = new McpServer({
22
33
  name: "mcp-telegram",
23
34
  version: "1.0.0",
@@ -146,7 +157,7 @@ server.tool("telegram-list-chats", "List Telegram chats", {
146
157
  return `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${unread}`;
147
158
  })
148
159
  .join("\n");
149
- return { content: [{ type: "text", text: text || "No chats" }] };
160
+ return { content: [{ type: "text", text: sanitize(text) || "No chats" }] };
150
161
  }
151
162
  catch (e) {
152
163
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -165,9 +176,9 @@ server.tool("telegram-read-messages", "Read recent messages from a Telegram chat
165
176
  try {
166
177
  const messages = await telegram.getMessages(chatId, limit, offsetId, minDate, maxDate);
167
178
  const text = messages
168
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
179
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
169
180
  .join("\n\n");
170
- return { content: [{ type: "text", text: text || "No messages" }] };
181
+ return { content: [{ type: "text", text: sanitize(text) || "No messages" }] };
171
182
  }
172
183
  catch (e) {
173
184
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -185,7 +196,7 @@ server.tool("telegram-search-chats", "Search for Telegram chats/users/channels b
185
196
  const text = results
186
197
  .map((c) => `${c.type === "group" ? "G" : c.type === "channel" ? "C" : "P"} ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})${c.membersCount ? ` [${c.membersCount} members]` : ""}${c.description ? ` — ${c.description.split("\n")[0].slice(0, 100)}` : ""}`)
187
198
  .join("\n");
188
- return { content: [{ type: "text", text: text || "No results" }] };
199
+ return { content: [{ type: "text", text: sanitize(text) || "No results" }] };
189
200
  }
190
201
  catch (e) {
191
202
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -203,9 +214,9 @@ server.tool("telegram-search-global", "Search messages globally across all publi
203
214
  try {
204
215
  const messages = await telegram.searchGlobal(query, limit, minDate, maxDate);
205
216
  const text = messages
206
- .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
217
+ .map((m) => `[#${m.id}] [${m.date}] [${m.chat.type === "channel" ? "C" : m.chat.type === "group" ? "G" : "P"} ${m.chat.name}${m.chat.username ? ` @${m.chat.username}` : ""}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
207
218
  .join("\n\n");
208
- return { content: [{ type: "text", text: text || "No messages found" }] };
219
+ return { content: [{ type: "text", text: sanitize(text) || "No messages found" }] };
209
220
  }
210
221
  catch (e) {
211
222
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -224,9 +235,9 @@ server.tool("telegram-search-messages", "Search messages in a Telegram chat by t
224
235
  try {
225
236
  const messages = await telegram.searchMessages(chatId, query, limit, minDate, maxDate);
226
237
  const text = messages
227
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
238
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
228
239
  .join("\n\n");
229
- return { content: [{ type: "text", text: text || "No messages found" }] };
240
+ return { content: [{ type: "text", text: sanitize(text) || "No messages found" }] };
230
241
  }
231
242
  catch (e) {
232
243
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -254,7 +265,7 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
254
265
  return line;
255
266
  })
256
267
  .join("\n");
257
- return { content: [{ type: "text", text: text || "No unread chats" }] };
268
+ return { content: [{ type: "text", text: sanitize(text) || "No unread chats" }] };
258
269
  }
259
270
  catch (e) {
260
271
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -422,7 +433,7 @@ server.tool("telegram-get-contacts", "Get Telegram contacts list", {
422
433
  const text = contacts
423
434
  .map((c) => `P ${c.name}${c.username ? ` (@${c.username})` : ""} (${c.id})${c.phone ? ` +${c.phone}` : ""}`)
424
435
  .join("\n");
425
- return { content: [{ type: "text", text: text || "No contacts" }] };
436
+ return { content: [{ type: "text", text: sanitize(text) || "No contacts" }] };
426
437
  }
427
438
  catch (e) {
428
439
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -438,7 +449,7 @@ server.tool("telegram-get-chat-members", "Get members of a Telegram group or cha
438
449
  try {
439
450
  const members = await telegram.getChatMembers(chatId, limit);
440
451
  const text = members.map((m) => `${m.name}${m.username ? ` (@${m.username})` : ""} (${m.id})`).join("\n");
441
- return { content: [{ type: "text", text: text || "No members found" }] };
452
+ return { content: [{ type: "text", text: sanitize(text) || "No members found" }] };
442
453
  }
443
454
  catch (e) {
444
455
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -529,23 +540,57 @@ server.tool("telegram-join-chat", "Join a Telegram group or channel by username
529
540
  };
530
541
  }
531
542
  });
532
- server.tool("telegram-send-reaction", "Send an emoji reaction to a message. Pass emoji to react, omit to remove reaction", {
543
+ server.tool("telegram-send-reaction", "Send emoji reaction(s) to a message. Supports multiple reactions and adding to existing ones. Omit emoji to remove all reactions", {
533
544
  chatId: z.string().describe("Chat ID or username"),
534
545
  messageId: z.number().describe("Message ID to react to"),
535
- emoji: z.string().optional().describe("Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction"),
536
- }, async ({ chatId, messageId, emoji }) => {
546
+ emoji: z
547
+ .union([z.string(), z.array(z.string())])
548
+ .optional()
549
+ .describe("Reaction emoji(s): single '👍' or array ['👍','🔥']. Omit to remove all reactions"),
550
+ addToExisting: z
551
+ .boolean()
552
+ .default(false)
553
+ .describe("If true, add reaction(s) to existing ones instead of replacing"),
554
+ }, async ({ chatId, messageId, emoji, addToExisting }) => {
537
555
  const err = await requireConnection();
538
556
  if (err)
539
557
  return { content: [{ type: "text", text: err }] };
540
558
  try {
541
- await telegram.sendReaction(chatId, messageId, emoji);
542
- const action = emoji ? `Reacted ${emoji} to` : "Removed reaction from";
543
- return { content: [{ type: "text", text: `${action} message ${messageId} in ${chatId}` }] };
559
+ const updated = await telegram.sendReaction(chatId, messageId, emoji, addToExisting);
560
+ const emojiStr = Array.isArray(emoji) ? emoji.join("") : emoji;
561
+ const action = emoji ? `Reacted ${emojiStr} to` : "Removed reactions from";
562
+ const reactionsInfo = updated
563
+ ? ` | Reactions: ${updated.map((r) => `${r.emoji}×${r.count}${r.me ? "(me)" : ""}`).join(" ")}`
564
+ : "";
565
+ return { content: [{ type: "text", text: `${action} message ${messageId} in ${chatId}${reactionsInfo}` }] };
544
566
  }
545
567
  catch (e) {
546
568
  return { content: [{ type: "text", text: `Reaction error: ${e.message}` }] };
547
569
  }
548
570
  });
571
+ server.tool("telegram-get-reactions", "Get detailed reaction info for a message: which reactions, counts, and who reacted (when visible)", {
572
+ chatId: z.string().describe("Chat ID or username"),
573
+ messageId: z.number().describe("Message ID to get reactions for"),
574
+ }, async ({ chatId, messageId }) => {
575
+ const err = await requireConnection();
576
+ if (err)
577
+ return { content: [{ type: "text", text: err }] };
578
+ try {
579
+ const result = await telegram.getMessageReactions(chatId, messageId);
580
+ if (result.reactions.length === 0) {
581
+ return { content: [{ type: "text", text: `No reactions on message ${messageId}` }] };
582
+ }
583
+ const lines = result.reactions.map((r) => {
584
+ const usersStr = r.users.length > 0 ? `: ${r.users.map((u) => u.name).join(", ")}` : "";
585
+ return `${r.emoji} × ${r.count}${usersStr}`;
586
+ });
587
+ lines.push(`\nTotal: ${result.total} reactions`);
588
+ return { content: [{ type: "text", text: lines.join("\n") }] };
589
+ }
590
+ catch (e) {
591
+ return { content: [{ type: "text", text: `Error: ${e.message}` }] };
592
+ }
593
+ });
549
594
  server.tool("telegram-send-scheduled", "Send a scheduled message to a Telegram chat. The message will be delivered at the specified time by Telegram servers", {
550
595
  chatId: z.string().describe("Chat ID or username (use 'me' or 'self' for Saved Messages)"),
551
596
  text: z.string().describe("Message text"),
@@ -615,7 +660,7 @@ server.tool("telegram-get-contact-requests", "Get incoming messages from non-con
615
660
  return `${tag} ${r.name}${username} (${r.id})${unread}${preview}`;
616
661
  })
617
662
  .join("\n");
618
- return { content: [{ type: "text", text: text }] };
663
+ return { content: [{ type: "text", text: sanitize(text) }] };
619
664
  }
620
665
  catch (e) {
621
666
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -685,7 +730,7 @@ server.tool("telegram-list-topics", "List forum topics in a Telegram group with
685
730
  return `# ${t.title} (id: ${t.id})${flagStr}${unread}`;
686
731
  })
687
732
  .join("\n");
688
- return { content: [{ type: "text", text: text || "No topics found" }] };
733
+ return { content: [{ type: "text", text: sanitize(text) || "No topics found" }] };
689
734
  }
690
735
  catch (e) {
691
736
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -703,9 +748,9 @@ server.tool("telegram-read-topic-messages", "Read messages from a specific forum
703
748
  try {
704
749
  const messages = await telegram.getTopicMessages(chatId, topicId, limit, offsetId);
705
750
  const text = messages
706
- .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
751
+ .map((m) => `[#${m.id}] [${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}${formatReactions(m.reactions)}`)
707
752
  .join("\n\n");
708
- return { content: [{ type: "text", text: text || "No messages in this topic" }] };
753
+ return { content: [{ type: "text", text: sanitize(text) || "No messages in this topic" }] };
709
754
  }
710
755
  catch (e) {
711
756
  return { content: [{ type: "text", text: `Error: ${e.message}` }] };
@@ -110,6 +110,11 @@ export declare class TelegramService {
110
110
  fileName?: string;
111
111
  size?: number;
112
112
  };
113
+ reactions?: {
114
+ emoji: string;
115
+ count: number;
116
+ me: boolean;
117
+ }[];
113
118
  }>>;
114
119
  searchChats(query: string, limit?: number): Promise<Array<{
115
120
  id: string;
@@ -135,6 +140,11 @@ export declare class TelegramService {
135
140
  fileName?: string;
136
141
  size?: number;
137
142
  };
143
+ reactions?: {
144
+ emoji: string;
145
+ count: number;
146
+ me: boolean;
147
+ }[];
138
148
  }>>;
139
149
  searchMessages(chatId: string, query: string, limit?: number, minDate?: number, maxDate?: number): Promise<Array<{
140
150
  id: number;
@@ -146,6 +156,11 @@ export declare class TelegramService {
146
156
  fileName?: string;
147
157
  size?: number;
148
158
  };
159
+ reactions?: {
160
+ emoji: string;
161
+ count: number;
162
+ me: boolean;
163
+ }[];
149
164
  }>>;
150
165
  getContacts(limit?: number): Promise<Array<{
151
166
  id: string;
@@ -184,7 +199,24 @@ export declare class TelegramService {
184
199
  } | null>;
185
200
  /** Detect MIME type from buffer magic bytes */
186
201
  private detectMimeFromBuffer;
187
- sendReaction(chatId: string, messageId: number, emoji?: string): Promise<void>;
202
+ /** Extract reactions from a message into a simple format */
203
+ private extractReactions;
204
+ sendReaction(chatId: string, messageId: number, emoji?: string | string[], addToExisting?: boolean): Promise<{
205
+ emoji: string;
206
+ count: number;
207
+ me: boolean;
208
+ }[] | undefined>;
209
+ getMessageReactions(chatId: string, messageId: number): Promise<{
210
+ reactions: {
211
+ emoji: string;
212
+ count: number;
213
+ users: {
214
+ id: string;
215
+ name: string;
216
+ }[];
217
+ }[];
218
+ total: number;
219
+ }>;
188
220
  sendScheduledMessage(chatId: string, text: string, scheduleDate: number, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
189
221
  createPoll(chatId: string, question: string, answers: string[], options?: {
190
222
  multipleChoice?: boolean;
@@ -210,6 +242,11 @@ export declare class TelegramService {
210
242
  fileName?: string;
211
243
  size?: number;
212
244
  };
245
+ reactions?: {
246
+ emoji: string;
247
+ count: number;
248
+ me: boolean;
249
+ }[];
213
250
  }>>;
214
251
  /** Check if a chat entity is a forum (has topics enabled) */
215
252
  isForum(chatId: string): Promise<boolean>;
@@ -621,6 +621,7 @@ export class TelegramService {
621
621
  sender: await this.resolveSenderName(m.senderId),
622
622
  date: new Date((m.date ?? 0) * 1000).toISOString(),
623
623
  media: this.extractMediaInfo(m.media),
624
+ reactions: this.extractReactions(m.reactions),
624
625
  })));
625
626
  return results;
626
627
  }
@@ -749,6 +750,7 @@ export class TelegramService {
749
750
  date: new Date((m.date ?? 0) * 1000).toISOString(),
750
751
  chat: chatsMap.get(chatId) || { id: chatId, name: "Unknown", type: "unknown" },
751
752
  media: this.extractMediaInfo(m.media),
753
+ reactions: this.extractReactions(m.reactions),
752
754
  };
753
755
  }));
754
756
  return results;
@@ -771,6 +773,7 @@ export class TelegramService {
771
773
  sender: await this.resolveSenderName(m.senderId),
772
774
  date: new Date((m.date ?? 0) * 1000).toISOString(),
773
775
  media: this.extractMediaInfo(m.media),
776
+ reactions: this.extractReactions(m.reactions),
774
777
  })));
775
778
  return results;
776
779
  }
@@ -899,16 +902,121 @@ export class TelegramService {
899
902
  return "image/webp";
900
903
  return "image/jpeg"; // Telegram profile photos are almost always JPEG
901
904
  }
902
- async sendReaction(chatId, messageId, emoji) {
905
+ /** Extract reactions from a message into a simple format */
906
+ extractReactions(reactions) {
907
+ if (!reactions?.results?.length)
908
+ return undefined;
909
+ const items = [];
910
+ for (const r of reactions.results) {
911
+ let emoji;
912
+ if (r.reaction instanceof Api.ReactionEmoji) {
913
+ emoji = r.reaction.emoticon;
914
+ }
915
+ else if (r.reaction instanceof Api.ReactionCustomEmoji) {
916
+ emoji = `custom:${r.reaction.documentId}`;
917
+ }
918
+ else if (r.reaction instanceof Api.ReactionPaid) {
919
+ emoji = "⭐";
920
+ }
921
+ else {
922
+ continue;
923
+ }
924
+ items.push({ emoji, count: r.count, me: r.chosenOrder != null });
925
+ }
926
+ return items.length > 0 ? items : undefined;
927
+ }
928
+ async sendReaction(chatId, messageId, emoji, addToExisting = false) {
903
929
  if (!this.client || !this.connected)
904
930
  throw new Error("Not connected");
905
931
  const peer = await this.client.getInputEntity(chatId);
906
- const reaction = emoji ? [new Api.ReactionEmoji({ emoticon: emoji })] : []; // empty = remove reaction
907
- await this.client.invoke(new Api.messages.SendReaction({
932
+ const reactionList = [];
933
+ if (emoji) {
934
+ const emojis = Array.isArray(emoji) ? emoji : [emoji];
935
+ if (addToExisting) {
936
+ // Fetch current reactions to preserve them
937
+ const msgs = await this.client.getMessages(chatId, { ids: [messageId] });
938
+ const msg = msgs[0];
939
+ if (msg?.reactions?.results) {
940
+ for (const r of msg.reactions.results) {
941
+ if (r.chosenOrder != null) {
942
+ reactionList.push(r.reaction);
943
+ }
944
+ }
945
+ }
946
+ }
947
+ for (const e of emojis) {
948
+ reactionList.push(new Api.ReactionEmoji({ emoticon: e }));
949
+ }
950
+ }
951
+ // empty array = remove all reactions
952
+ const result = await this.client.invoke(new Api.messages.SendReaction({
908
953
  peer,
909
954
  msgId: messageId,
910
- reaction,
955
+ reaction: reactionList,
911
956
  }));
957
+ // Extract updated reactions from the response
958
+ if ("updates" in result) {
959
+ for (const upd of result.updates) {
960
+ if (upd instanceof Api.UpdateMessageReactions) {
961
+ return this.extractReactions(upd.reactions);
962
+ }
963
+ }
964
+ }
965
+ return undefined;
966
+ }
967
+ async getMessageReactions(chatId, messageId) {
968
+ if (!this.client || !this.connected)
969
+ throw new Error("Not connected");
970
+ const peer = await this.client.getInputEntity(chatId);
971
+ // First get the message to know which reactions exist
972
+ const msgs = await this.client.getMessages(chatId, { ids: [messageId] });
973
+ const msg = msgs[0];
974
+ if (!msg?.reactions?.results?.length) {
975
+ return { reactions: [], total: 0 };
976
+ }
977
+ const reactionsOut = [];
978
+ for (const rc of msg.reactions.results) {
979
+ let emoji;
980
+ if (rc.reaction instanceof Api.ReactionEmoji) {
981
+ emoji = rc.reaction.emoticon;
982
+ }
983
+ else if (rc.reaction instanceof Api.ReactionCustomEmoji) {
984
+ emoji = `custom:${rc.reaction.documentId}`;
985
+ }
986
+ else if (rc.reaction instanceof Api.ReactionPaid) {
987
+ emoji = "⭐";
988
+ }
989
+ else {
990
+ continue;
991
+ }
992
+ const users = [];
993
+ // Try to get the list of users who reacted (may fail if canSeeList is false)
994
+ if (msg.reactions.canSeeList) {
995
+ try {
996
+ const list = await this.client.invoke(new Api.messages.GetMessageReactionsList({
997
+ peer,
998
+ id: messageId,
999
+ reaction: rc.reaction,
1000
+ limit: 50,
1001
+ }));
1002
+ if (list instanceof Api.messages.MessageReactionsList) {
1003
+ for (const r of list.reactions) {
1004
+ const userId = r.peerId instanceof Api.PeerUser ? r.peerId.userId.toString() : "";
1005
+ if (userId) {
1006
+ const name = await this.resolveSenderName(bigInt(Number.parseInt(userId)));
1007
+ users.push({ id: userId, name });
1008
+ }
1009
+ }
1010
+ }
1011
+ }
1012
+ catch {
1013
+ // canSeeList may be false or request may fail for channels
1014
+ }
1015
+ }
1016
+ reactionsOut.push({ emoji, count: rc.count, users });
1017
+ }
1018
+ const total = reactionsOut.reduce((sum, r) => sum + r.count, 0);
1019
+ return { reactions: reactionsOut, total };
912
1020
  }
913
1021
  async sendScheduledMessage(chatId, text, scheduleDate, replyTo, parseMode) {
914
1022
  if (!this.client || !this.connected)
@@ -1009,6 +1117,7 @@ export class TelegramService {
1009
1117
  sender: await this.resolveSenderName(m.senderId),
1010
1118
  date: new Date((m.date ?? 0) * 1000).toISOString(),
1011
1119
  media: this.extractMediaInfo(m.media),
1120
+ reactions: this.extractReactions(m.reactions),
1012
1121
  })));
1013
1122
  return results;
1014
1123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.10.1",
3
+ "version": "1.11.1",
4
4
  "description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -60,6 +60,6 @@
60
60
  "@types/node": "^25.5.0",
61
61
  "@types/qrcode": "^1.5.6",
62
62
  "tsx": "^4.21.0",
63
- "typescript": "^5.9.3"
63
+ "typescript": "^6.0.2"
64
64
  }
65
65
  }