@overpod/mcp-telegram 1.5.0 → 1.7.0
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 +76 -8
- package/dist/cli.js +0 -0
- package/dist/index.js +90 -3
- package/dist/telegram-client.d.ts +24 -2
- package/dist/telegram-client.js +130 -21
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://glama.ai/mcp/servers/overpod/mcp-telegram)
|
|
10
10
|
|
|
11
|
-
> **Hosted version available!** Don't want to self-host? Use [mcp-telegram.com](https://mcp-telegram.com) -- connect Telegram to Claude.ai in 30 seconds with QR code. No API keys needed.
|
|
11
|
+
> **Hosted version available!** Don't want to self-host? Use [mcp-telegram.com](https://mcp-telegram.com) -- connect Telegram to Claude.ai or ChatGPT in 30 seconds with QR code. No API keys needed.
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<img src="assets/demo.gif" alt="MCP Telegram demo — connect and summarize chats in Claude" width="700">
|
|
@@ -20,11 +20,11 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
|
|
|
20
20
|
|
|
21
21
|
- **MTProto protocol** -- direct Telegram API access, not the limited Bot API
|
|
22
22
|
- **Userbot** -- operates as your personal account, not a bot
|
|
23
|
-
- **
|
|
23
|
+
- **Full-featured** -- messaging, reactions, polls, scheduled messages, media, contacts, and more
|
|
24
24
|
- **QR code login** -- authenticate by scanning a QR code in the Telegram app
|
|
25
25
|
- **Session persistence** -- login once, stay connected across restarts
|
|
26
26
|
- **Human-readable output** -- sender names are resolved, not just numeric IDs
|
|
27
|
-
- **Works with any MCP client** -- Claude Code, Claude Desktop, Cursor, VS Code, Mastra, etc.
|
|
27
|
+
- **Works with any MCP client** -- Claude Code, Claude Desktop, ChatGPT, Cursor, VS Code, Mastra, etc.
|
|
28
28
|
|
|
29
29
|
## Prerequisites
|
|
30
30
|
|
|
@@ -162,6 +162,9 @@ const telegramMcp = new MCPClient({
|
|
|
162
162
|
|------|-------------|
|
|
163
163
|
| `telegram-send-message` | Send a text message to a chat |
|
|
164
164
|
| `telegram-send-file` | Send a file (photo, document, video, etc.) to a chat |
|
|
165
|
+
| `telegram-send-reaction` | Send or remove an emoji reaction on a message |
|
|
166
|
+
| `telegram-send-scheduled` | Schedule a message for future delivery |
|
|
167
|
+
| `telegram-create-poll` | Create a poll (multiple choice or quiz mode) |
|
|
165
168
|
| `telegram-edit-message` | Edit a previously sent message |
|
|
166
169
|
| `telegram-delete-message` | Delete messages in a chat |
|
|
167
170
|
| `telegram-forward-message` | Forward messages between chats |
|
|
@@ -170,23 +173,32 @@ const telegramMcp = new MCPClient({
|
|
|
170
173
|
|
|
171
174
|
| Tool | Description |
|
|
172
175
|
|------|-------------|
|
|
173
|
-
| `telegram-list-chats` | List recent dialogs with unread counts |
|
|
176
|
+
| `telegram-list-chats` | List recent dialogs with unread counts, bot/contact markers |
|
|
174
177
|
| `telegram-read-messages` | Read recent messages from a chat |
|
|
175
178
|
| `telegram-search-chats` | Search for chats, users, or channels by name |
|
|
176
179
|
| `telegram-search-messages` | Search messages in a chat by text |
|
|
177
|
-
| `telegram-get-unread` | Get chats with unread messages |
|
|
180
|
+
| `telegram-get-unread` | Get chats with unread messages, bot/contact markers |
|
|
181
|
+
| `telegram-get-contact-requests` | Get incoming messages from non-contacts with preview |
|
|
178
182
|
|
|
179
183
|
### Chat Management
|
|
180
184
|
|
|
181
185
|
| Tool | Description |
|
|
182
186
|
|------|-------------|
|
|
183
187
|
| `telegram-mark-as-read` | Mark a chat as read |
|
|
184
|
-
| `telegram-get-chat-info` | Get detailed info about a chat (name, type, members
|
|
188
|
+
| `telegram-get-chat-info` | Get detailed info about a chat (name, type, members, bot/contact status) |
|
|
185
189
|
| `telegram-get-chat-members` | List members of a group or channel |
|
|
186
190
|
| `telegram-join-chat` | Join a group or channel by username or invite link |
|
|
187
191
|
| `telegram-pin-message` | Pin a message in a chat |
|
|
188
192
|
| `telegram-unpin-message` | Unpin a message in a chat |
|
|
189
193
|
|
|
194
|
+
### Contacts & Moderation
|
|
195
|
+
|
|
196
|
+
| Tool | Description |
|
|
197
|
+
|------|-------------|
|
|
198
|
+
| `telegram-add-contact` | Add a user to your contacts (accept contact request) |
|
|
199
|
+
| `telegram-block-user` | Block a user from sending you messages |
|
|
200
|
+
| `telegram-report-spam` | Report a chat as spam to Telegram |
|
|
201
|
+
|
|
190
202
|
### User Info
|
|
191
203
|
|
|
192
204
|
| Tool | Description |
|
|
@@ -221,7 +233,7 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
221
233
|
|-----------|------|----------|-------------|
|
|
222
234
|
| `limit` | number | no | Number of chats to return (default: 20) |
|
|
223
235
|
| `offsetDate` | number | no | Unix timestamp for pagination |
|
|
224
|
-
| `filterType` | `"private"` / `"group"` / `"channel"` | no | Filter by chat type |
|
|
236
|
+
| `filterType` | `"private"` / `"group"` / `"channel"` / `"contact_requests"` | no | Filter by chat type |
|
|
225
237
|
|
|
226
238
|
### telegram-read-messages
|
|
227
239
|
|
|
@@ -284,6 +296,35 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
284
296
|
|-----------|------|----------|-------------|
|
|
285
297
|
| `target` | string | yes | Username (@group), link (t.me/group), or invite link (t.me/+xxx) |
|
|
286
298
|
|
|
299
|
+
### telegram-send-reaction
|
|
300
|
+
|
|
301
|
+
| Parameter | Type | Required | Description |
|
|
302
|
+
|-----------|------|----------|-------------|
|
|
303
|
+
| `chatId` | string | yes | Chat ID or @username |
|
|
304
|
+
| `messageId` | number | yes | Message ID to react to |
|
|
305
|
+
| `emoji` | string | no | Reaction emoji (e.g. 👍❤️🔥😂🎉). Omit to remove reaction |
|
|
306
|
+
|
|
307
|
+
### telegram-send-scheduled
|
|
308
|
+
|
|
309
|
+
| Parameter | Type | Required | Description |
|
|
310
|
+
|-----------|------|----------|-------------|
|
|
311
|
+
| `chatId` | string | yes | Chat ID or @username (use `"me"` or `"self"` for Saved Messages) |
|
|
312
|
+
| `text` | string | yes | Message text |
|
|
313
|
+
| `scheduleDate` | number | yes | Unix timestamp when to send (must be in the future) |
|
|
314
|
+
| `replyTo` | number | no | Message ID to reply to |
|
|
315
|
+
| `parseMode` | `"md"` / `"html"` | no | Message formatting mode |
|
|
316
|
+
|
|
317
|
+
### telegram-create-poll
|
|
318
|
+
|
|
319
|
+
| Parameter | Type | Required | Description |
|
|
320
|
+
|-----------|------|----------|-------------|
|
|
321
|
+
| `chatId` | string | yes | Chat ID or @username |
|
|
322
|
+
| `question` | string | yes | Poll question |
|
|
323
|
+
| `answers` | string[] | yes | Answer options (2-10 items) |
|
|
324
|
+
| `multipleChoice` | boolean | no | Allow multiple answers (default: false) |
|
|
325
|
+
| `quiz` | boolean | no | Quiz mode with one correct answer (default: false) |
|
|
326
|
+
| `correctAnswer` | number | no | Index of correct answer, 0-based (required for quiz mode) |
|
|
327
|
+
|
|
287
328
|
### telegram-search-messages
|
|
288
329
|
|
|
289
330
|
| Parameter | Type | Required | Description |
|
|
@@ -324,6 +365,33 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
324
365
|
|-----------|------|----------|-------------|
|
|
325
366
|
| `limit` | number | no | Number of unread chats (default: 20) |
|
|
326
367
|
|
|
368
|
+
### telegram-get-contact-requests
|
|
369
|
+
|
|
370
|
+
| Parameter | Type | Required | Description |
|
|
371
|
+
|-----------|------|----------|-------------|
|
|
372
|
+
| `limit` | number | no | Number of contact requests (default: 20) |
|
|
373
|
+
|
|
374
|
+
### telegram-add-contact
|
|
375
|
+
|
|
376
|
+
| Parameter | Type | Required | Description |
|
|
377
|
+
|-----------|------|----------|-------------|
|
|
378
|
+
| `userId` | string | yes | User ID or @username to add |
|
|
379
|
+
| `firstName` | string | yes | First name for the contact |
|
|
380
|
+
| `lastName` | string | no | Last name for the contact |
|
|
381
|
+
| `phone` | string | no | Phone number for the contact |
|
|
382
|
+
|
|
383
|
+
### telegram-block-user
|
|
384
|
+
|
|
385
|
+
| Parameter | Type | Required | Description |
|
|
386
|
+
|-----------|------|----------|-------------|
|
|
387
|
+
| `userId` | string | yes | User ID or @username to block |
|
|
388
|
+
|
|
389
|
+
### telegram-report-spam
|
|
390
|
+
|
|
391
|
+
| Parameter | Type | Required | Description |
|
|
392
|
+
|-----------|------|----------|-------------|
|
|
393
|
+
| `chatId` | string | yes | Chat ID or @username to report |
|
|
394
|
+
|
|
327
395
|
## Development
|
|
328
396
|
|
|
329
397
|
```bash
|
|
@@ -340,7 +408,7 @@ npm run format # Format code with Biome
|
|
|
340
408
|
|
|
341
409
|
```
|
|
342
410
|
src/
|
|
343
|
-
index.ts -- MCP server entry point,
|
|
411
|
+
index.ts -- MCP server entry point, tool definitions
|
|
344
412
|
telegram-client.ts -- TelegramService class (GramJS wrapper)
|
|
345
413
|
qr-login-cli.ts -- CLI utility for QR code login
|
|
346
414
|
```
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -119,7 +119,10 @@ server.tool("telegram-send-message", "Send a message to a Telegram chat", {
|
|
|
119
119
|
server.tool("telegram-list-chats", "List Telegram chats", {
|
|
120
120
|
limit: z.number().default(20).describe("Number of chats to return"),
|
|
121
121
|
offsetDate: z.number().optional().describe("Unix timestamp offset for pagination"),
|
|
122
|
-
filterType: z
|
|
122
|
+
filterType: z
|
|
123
|
+
.enum(["private", "group", "channel", "contact_requests"])
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Filter by chat type. 'contact_requests' shows only private chats from non-contacts"),
|
|
123
126
|
}, async ({ limit, offsetDate, filterType }) => {
|
|
124
127
|
const err = await requireConnection();
|
|
125
128
|
if (err)
|
|
@@ -127,7 +130,13 @@ server.tool("telegram-list-chats", "List Telegram chats", {
|
|
|
127
130
|
try {
|
|
128
131
|
const dialogs = await telegram.getDialogs(limit, offsetDate, filterType);
|
|
129
132
|
const text = dialogs
|
|
130
|
-
.map((d) =>
|
|
133
|
+
.map((d) => {
|
|
134
|
+
const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
|
|
135
|
+
const botTag = d.isBot ? " [bot]" : "";
|
|
136
|
+
const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
|
|
137
|
+
const unread = d.unreadCount > 0 ? ` [${d.unreadCount} unread]` : "";
|
|
138
|
+
return `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${unread}`;
|
|
139
|
+
})
|
|
131
140
|
.join("\n");
|
|
132
141
|
return { content: [{ type: "text", text: text || "No chats" }] };
|
|
133
142
|
}
|
|
@@ -204,7 +213,12 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
|
|
|
204
213
|
try {
|
|
205
214
|
const dialogs = await telegram.getUnreadDialogs(limit);
|
|
206
215
|
const text = dialogs
|
|
207
|
-
.map((d) =>
|
|
216
|
+
.map((d) => {
|
|
217
|
+
const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
|
|
218
|
+
const botTag = d.isBot ? " [bot]" : "";
|
|
219
|
+
const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
|
|
220
|
+
return `${prefix} ${d.name} (${d.id})${botTag}${contactTag} [${d.unreadCount} unread]`;
|
|
221
|
+
})
|
|
208
222
|
.join("\n");
|
|
209
223
|
return { content: [{ type: "text", text: text || "No unread chats" }] };
|
|
210
224
|
}
|
|
@@ -507,6 +521,79 @@ server.tool("telegram-create-poll", "Create a poll in a Telegram chat", {
|
|
|
507
521
|
return { content: [{ type: "text", text: `Poll error: ${e.message}` }] };
|
|
508
522
|
}
|
|
509
523
|
});
|
|
524
|
+
server.tool("telegram-get-contact-requests", "Get incoming messages from non-contacts (contact requests). Shows who messaged you without being in your contacts, with message preview", {
|
|
525
|
+
limit: z.number().default(20).describe("Number of contact requests to return"),
|
|
526
|
+
}, async ({ limit }) => {
|
|
527
|
+
const err = await requireConnection();
|
|
528
|
+
if (err)
|
|
529
|
+
return { content: [{ type: "text", text: err }] };
|
|
530
|
+
try {
|
|
531
|
+
const requests = await telegram.getContactRequests(limit);
|
|
532
|
+
if (requests.length === 0) {
|
|
533
|
+
return { content: [{ type: "text", text: "No contact requests" }] };
|
|
534
|
+
}
|
|
535
|
+
const text = requests
|
|
536
|
+
.map((r) => {
|
|
537
|
+
const tag = r.isBot ? "[bot]" : "[user]";
|
|
538
|
+
const username = r.username ? ` @${r.username}` : "";
|
|
539
|
+
const unread = r.unreadCount > 0 ? ` [${r.unreadCount} unread]` : "";
|
|
540
|
+
const preview = r.lastMessage ? `\n > ${r.lastMessage.slice(0, 100)}` : "";
|
|
541
|
+
return `${tag} ${r.name}${username} (${r.id})${unread}${preview}`;
|
|
542
|
+
})
|
|
543
|
+
.join("\n");
|
|
544
|
+
return { content: [{ type: "text", text: text }] };
|
|
545
|
+
}
|
|
546
|
+
catch (e) {
|
|
547
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
server.tool("telegram-add-contact", "Add a user to your Telegram contacts. Use this to accept contact requests from non-contacts", {
|
|
551
|
+
userId: z.string().describe("User ID or username to add"),
|
|
552
|
+
firstName: z.string().describe("First name for the contact"),
|
|
553
|
+
lastName: z.string().optional().describe("Last name for the contact"),
|
|
554
|
+
phone: z.string().optional().describe("Phone number for the contact"),
|
|
555
|
+
}, async ({ userId, firstName, lastName, phone }) => {
|
|
556
|
+
const err = await requireConnection();
|
|
557
|
+
if (err)
|
|
558
|
+
return { content: [{ type: "text", text: err }] };
|
|
559
|
+
try {
|
|
560
|
+
await telegram.addContact(userId, firstName, lastName, phone);
|
|
561
|
+
return {
|
|
562
|
+
content: [{ type: "text", text: `Contact added: ${firstName}${lastName ? ` ${lastName}` : ""} (${userId})` }],
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
server.tool("telegram-block-user", "Block a Telegram user. Blocked users cannot send you messages", {
|
|
570
|
+
userId: z.string().describe("User ID or username to block"),
|
|
571
|
+
}, async ({ userId }) => {
|
|
572
|
+
const err = await requireConnection();
|
|
573
|
+
if (err)
|
|
574
|
+
return { content: [{ type: "text", text: err }] };
|
|
575
|
+
try {
|
|
576
|
+
await telegram.blockUser(userId);
|
|
577
|
+
return { content: [{ type: "text", text: `User blocked: ${userId}` }] };
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
server.tool("telegram-report-spam", "Report a chat as spam to Telegram", {
|
|
584
|
+
chatId: z.string().describe("Chat ID or username to report"),
|
|
585
|
+
}, async ({ chatId }) => {
|
|
586
|
+
const err = await requireConnection();
|
|
587
|
+
if (err)
|
|
588
|
+
return { content: [{ type: "text", text: err }] };
|
|
589
|
+
try {
|
|
590
|
+
await telegram.reportSpam(chatId);
|
|
591
|
+
return { content: [{ type: "text", text: `Reported as spam: ${chatId}` }] };
|
|
592
|
+
}
|
|
593
|
+
catch (e) {
|
|
594
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
595
|
+
}
|
|
596
|
+
});
|
|
510
597
|
// --- Start ---
|
|
511
598
|
async function main() {
|
|
512
599
|
// Try to auto-connect with saved session
|
|
@@ -4,9 +4,13 @@ export declare class TelegramService {
|
|
|
4
4
|
private apiHash;
|
|
5
5
|
private sessionString;
|
|
6
6
|
private connected;
|
|
7
|
+
private sessionPath;
|
|
7
8
|
lastError: string;
|
|
8
|
-
constructor(apiId: number, apiHash: string
|
|
9
|
+
constructor(apiId: number, apiHash: string, options?: {
|
|
10
|
+
sessionPath?: string;
|
|
11
|
+
});
|
|
9
12
|
loadSession(): Promise<boolean>;
|
|
13
|
+
private isValidSessionString;
|
|
10
14
|
/** Set session string in memory (for programmatic / hosted use) */
|
|
11
15
|
setSessionString(session: string): void;
|
|
12
16
|
/** Get the current session string (for external persistence) */
|
|
@@ -43,18 +47,34 @@ export declare class TelegramService {
|
|
|
43
47
|
private detectMimeType;
|
|
44
48
|
pinMessage(chatId: string, messageId: number, silent?: boolean): Promise<void>;
|
|
45
49
|
unpinMessage(chatId: string, messageId: number): Promise<void>;
|
|
46
|
-
getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel"): Promise<Array<{
|
|
50
|
+
getDialogs(limit?: number, offsetDate?: number, filterType?: "private" | "group" | "channel" | "contact_requests"): Promise<Array<{
|
|
47
51
|
id: string;
|
|
48
52
|
name: string;
|
|
49
53
|
type: string;
|
|
50
54
|
unreadCount: number;
|
|
55
|
+
isBot?: boolean;
|
|
56
|
+
isContact?: boolean;
|
|
51
57
|
}>>;
|
|
52
58
|
getUnreadDialogs(limit?: number): Promise<Array<{
|
|
53
59
|
id: string;
|
|
54
60
|
name: string;
|
|
55
61
|
type: string;
|
|
56
62
|
unreadCount: number;
|
|
63
|
+
isBot?: boolean;
|
|
64
|
+
isContact?: boolean;
|
|
57
65
|
}>>;
|
|
66
|
+
getContactRequests(limit?: number): Promise<Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
username?: string;
|
|
70
|
+
isBot: boolean;
|
|
71
|
+
unreadCount: number;
|
|
72
|
+
lastMessage?: string;
|
|
73
|
+
lastMessageDate?: number;
|
|
74
|
+
}>>;
|
|
75
|
+
addContact(userId: string, firstName: string, lastName?: string, phone?: string): Promise<void>;
|
|
76
|
+
blockUser(userId: string): Promise<void>;
|
|
77
|
+
reportSpam(chatId: string): Promise<void>;
|
|
58
78
|
markAsRead(chatId: string): Promise<void>;
|
|
59
79
|
forwardMessage(fromChatId: string, toChatId: string, messageIds: number[]): Promise<void>;
|
|
60
80
|
editMessage(chatId: string, messageId: number, newText: string): Promise<void>;
|
|
@@ -66,6 +86,8 @@ export declare class TelegramService {
|
|
|
66
86
|
username?: string;
|
|
67
87
|
description?: string;
|
|
68
88
|
membersCount?: number;
|
|
89
|
+
isBot?: boolean;
|
|
90
|
+
isContact?: boolean;
|
|
69
91
|
}>;
|
|
70
92
|
/** Extract media info from a message */
|
|
71
93
|
private extractMediaInfo;
|
package/dist/telegram-client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { chmod, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import bigInt from "big-integer";
|
|
@@ -8,25 +9,66 @@ import { TelegramClient } from "telegram";
|
|
|
8
9
|
import { StringSession } from "telegram/sessions/index.js";
|
|
9
10
|
import { Api } from "telegram/tl/index.js";
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const
|
|
12
|
+
const LEGACY_SESSION_FILE = join(__dirname, "..", ".telegram-session");
|
|
13
|
+
const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
|
|
14
|
+
const DEFAULT_SESSION_FILE = join(DEFAULT_SESSION_DIR, "session");
|
|
15
|
+
const SESSION_STRING_RE = /^[A-Za-z0-9+/=]+$/;
|
|
16
|
+
const MIN_SESSION_LENGTH = 100;
|
|
17
|
+
function resolveSessionPath(sessionPath) {
|
|
18
|
+
return sessionPath ?? process.env.TELEGRAM_SESSION_PATH ?? DEFAULT_SESSION_FILE;
|
|
19
|
+
}
|
|
20
|
+
function ensureSessionDir(filePath) {
|
|
21
|
+
const dir = dirname(filePath);
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
12
26
|
export class TelegramService {
|
|
13
27
|
client = null;
|
|
14
28
|
apiId;
|
|
15
29
|
apiHash;
|
|
16
30
|
sessionString = "";
|
|
17
31
|
connected = false;
|
|
32
|
+
sessionPath;
|
|
18
33
|
lastError = "";
|
|
19
|
-
constructor(apiId, apiHash) {
|
|
34
|
+
constructor(apiId, apiHash, options) {
|
|
20
35
|
this.apiId = apiId;
|
|
21
36
|
this.apiHash = apiHash;
|
|
37
|
+
this.sessionPath = resolveSessionPath(options?.sessionPath);
|
|
22
38
|
}
|
|
23
39
|
async loadSession() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
40
|
+
// Try current session path
|
|
41
|
+
if (existsSync(this.sessionPath)) {
|
|
42
|
+
const raw = (await readFile(this.sessionPath, "utf-8")).trim();
|
|
43
|
+
if (this.isValidSessionString(raw)) {
|
|
44
|
+
this.sessionString = raw;
|
|
45
|
+
// Fix permissions on existing files
|
|
46
|
+
try {
|
|
47
|
+
await chmod(this.sessionPath, 0o600);
|
|
48
|
+
}
|
|
49
|
+
catch { }
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Migrate from legacy path (inside node_modules / package root)
|
|
54
|
+
if (this.sessionPath === DEFAULT_SESSION_FILE && existsSync(LEGACY_SESSION_FILE)) {
|
|
55
|
+
const raw = (await readFile(LEGACY_SESSION_FILE, "utf-8")).trim();
|
|
56
|
+
if (this.isValidSessionString(raw)) {
|
|
57
|
+
this.sessionString = raw;
|
|
58
|
+
ensureSessionDir(this.sessionPath);
|
|
59
|
+
await writeFile(this.sessionPath, raw, { encoding: "utf-8", mode: 0o600 });
|
|
60
|
+
try {
|
|
61
|
+
await unlink(LEGACY_SESSION_FILE);
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
27
66
|
}
|
|
28
67
|
return false;
|
|
29
68
|
}
|
|
69
|
+
isValidSessionString(value) {
|
|
70
|
+
return value.length >= MIN_SESSION_LENGTH && SESSION_STRING_RE.test(value);
|
|
71
|
+
}
|
|
30
72
|
/** Set session string in memory (for programmatic / hosted use) */
|
|
31
73
|
setSessionString(session) {
|
|
32
74
|
this.sessionString = session;
|
|
@@ -38,7 +80,8 @@ export class TelegramService {
|
|
|
38
80
|
async saveSession(session) {
|
|
39
81
|
this.sessionString = session;
|
|
40
82
|
try {
|
|
41
|
-
|
|
83
|
+
ensureSessionDir(this.sessionPath);
|
|
84
|
+
await writeFile(this.sessionPath, session, { encoding: "utf-8", mode: 0o600 });
|
|
42
85
|
}
|
|
43
86
|
catch {
|
|
44
87
|
// File write may fail in containerized environments — session string is still in memory
|
|
@@ -95,8 +138,8 @@ export class TelegramService {
|
|
|
95
138
|
this.connected = false;
|
|
96
139
|
this.sessionString = "";
|
|
97
140
|
this.client = null;
|
|
98
|
-
if (existsSync(
|
|
99
|
-
await unlink(
|
|
141
|
+
if (existsSync(this.sessionPath)) {
|
|
142
|
+
await unlink(this.sessionPath);
|
|
100
143
|
}
|
|
101
144
|
}
|
|
102
145
|
/** Ensure connection is active, auto-reconnect if session exists */
|
|
@@ -309,12 +352,22 @@ export class TelegramService {
|
|
|
309
352
|
throw new Error("Not connected");
|
|
310
353
|
const fetchLimit = filterType ? limit * 3 : limit;
|
|
311
354
|
const dialogs = await this.client.getDialogs({ limit: fetchLimit, ...(offsetDate ? { offsetDate } : {}) });
|
|
312
|
-
const mapped = dialogs.map((d) =>
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
355
|
+
const mapped = dialogs.map((d) => {
|
|
356
|
+
const type = d.isGroup ? "group" : d.isChannel ? "channel" : "private";
|
|
357
|
+
const isUser = d.entity instanceof Api.User;
|
|
358
|
+
return {
|
|
359
|
+
id: d.id?.toString() ?? "",
|
|
360
|
+
name: d.title ?? d.name ?? "Unknown",
|
|
361
|
+
type,
|
|
362
|
+
unreadCount: d.unreadCount,
|
|
363
|
+
...(isUser
|
|
364
|
+
? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
|
|
365
|
+
: {}),
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
if (filterType === "contact_requests") {
|
|
369
|
+
return mapped.filter((d) => d.type === "private" && d.isContact === false).slice(0, limit);
|
|
370
|
+
}
|
|
318
371
|
return filterType ? mapped.filter((d) => d.type === filterType).slice(0, limit) : mapped;
|
|
319
372
|
}
|
|
320
373
|
async getUnreadDialogs(limit = 20) {
|
|
@@ -324,13 +377,67 @@ export class TelegramService {
|
|
|
324
377
|
return dialogs
|
|
325
378
|
.filter((d) => d.unreadCount > 0)
|
|
326
379
|
.slice(0, limit)
|
|
327
|
-
.map((d) =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
380
|
+
.map((d) => {
|
|
381
|
+
const isUser = d.entity instanceof Api.User;
|
|
382
|
+
return {
|
|
383
|
+
id: d.id?.toString() ?? "",
|
|
384
|
+
name: d.title ?? d.name ?? "Unknown",
|
|
385
|
+
type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
|
|
386
|
+
unreadCount: d.unreadCount,
|
|
387
|
+
...(isUser
|
|
388
|
+
? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
|
|
389
|
+
: {}),
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async getContactRequests(limit = 20) {
|
|
394
|
+
if (!this.client || !this.connected)
|
|
395
|
+
throw new Error("Not connected");
|
|
396
|
+
const dialogs = await this.client.getDialogs({ limit: limit * 5 });
|
|
397
|
+
return dialogs
|
|
398
|
+
.filter((d) => {
|
|
399
|
+
if (d.isGroup || d.isChannel)
|
|
400
|
+
return false;
|
|
401
|
+
return d.entity instanceof Api.User && !d.entity.contact;
|
|
402
|
+
})
|
|
403
|
+
.slice(0, limit)
|
|
404
|
+
.map((d) => {
|
|
405
|
+
const user = d.entity;
|
|
406
|
+
const msg = d.message;
|
|
407
|
+
return {
|
|
408
|
+
id: d.id?.toString() ?? "",
|
|
409
|
+
name: [user.firstName, user.lastName].filter(Boolean).join(" ") || "Unknown",
|
|
410
|
+
username: user.username ?? undefined,
|
|
411
|
+
isBot: Boolean(user.bot),
|
|
412
|
+
unreadCount: d.unreadCount,
|
|
413
|
+
lastMessage: msg?.message ?? undefined,
|
|
414
|
+
lastMessageDate: msg?.date ?? undefined,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async addContact(userId, firstName, lastName, phone) {
|
|
419
|
+
if (!this.client || !this.connected)
|
|
420
|
+
throw new Error("Not connected");
|
|
421
|
+
const entity = await this.client.getInputEntity(userId);
|
|
422
|
+
await this.client.invoke(new Api.contacts.AddContact({
|
|
423
|
+
id: entity,
|
|
424
|
+
firstName,
|
|
425
|
+
lastName: lastName ?? "",
|
|
426
|
+
phone: phone ?? "",
|
|
332
427
|
}));
|
|
333
428
|
}
|
|
429
|
+
async blockUser(userId) {
|
|
430
|
+
if (!this.client || !this.connected)
|
|
431
|
+
throw new Error("Not connected");
|
|
432
|
+
const entity = await this.client.getInputEntity(userId);
|
|
433
|
+
await this.client.invoke(new Api.contacts.Block({ id: entity }));
|
|
434
|
+
}
|
|
435
|
+
async reportSpam(chatId) {
|
|
436
|
+
if (!this.client || !this.connected)
|
|
437
|
+
throw new Error("Not connected");
|
|
438
|
+
const peer = await this.client.getInputEntity(chatId);
|
|
439
|
+
await this.client.invoke(new Api.messages.ReportSpam({ peer }));
|
|
440
|
+
}
|
|
334
441
|
async markAsRead(chatId) {
|
|
335
442
|
if (!this.client || !this.connected)
|
|
336
443
|
throw new Error("Not connected");
|
|
@@ -362,6 +469,8 @@ export class TelegramService {
|
|
|
362
469
|
name: parts.join(" ") || "Unknown",
|
|
363
470
|
type: "private",
|
|
364
471
|
username: entity.username ?? undefined,
|
|
472
|
+
isBot: Boolean(entity.bot),
|
|
473
|
+
isContact: Boolean(entity.contact),
|
|
365
474
|
};
|
|
366
475
|
}
|
|
367
476
|
if (entity instanceof Api.Channel) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Telegram userbot —
|
|
3
|
+
"version": "1.7.0",
|
|
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",
|
|
7
7
|
"exports": {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"zod": "^4.3.6"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@biomejs/biome": "^2.4.
|
|
60
|
-
"@types/node": "^25.
|
|
59
|
+
"@biomejs/biome": "^2.4.7",
|
|
60
|
+
"@types/node": "^25.5.0",
|
|
61
61
|
"@types/qrcode": "^1.5.6",
|
|
62
62
|
"tsx": "^4.21.0",
|
|
63
63
|
"typescript": "^5.9.3"
|