@overpod/mcp-telegram 1.6.0 → 1.8.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 +98 -6
- package/dist/cli.js +0 -0
- package/dist/index.js +56 -4
- package/dist/telegram-client.d.ts +35 -2
- package/dist/telegram-client.js +171 -22
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
|
|
|
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
|
+
- **Forum Topics** -- list topics, read per-topic messages, send to specific topics, per-topic unread counts
|
|
24
25
|
- **QR code login** -- authenticate by scanning a QR code in the Telegram app
|
|
25
26
|
- **Session persistence** -- login once, stay connected across restarts
|
|
26
27
|
- **Human-readable output** -- sender names are resolved, not just numeric IDs
|
|
@@ -46,7 +47,9 @@ An MCP (Model Context Protocol) server that connects AI assistants like Claude t
|
|
|
46
47
|
TELEGRAM_API_ID=YOUR_ID TELEGRAM_API_HASH=YOUR_HASH npx @overpod/mcp-telegram login
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
A QR code will appear in the terminal. Open Telegram on your phone, go to **Settings > Devices > Link Desktop Device**, and scan the code. The session is saved to `~/.telegram
|
|
50
|
+
A QR code will appear in the terminal. Open Telegram on your phone, go to **Settings > Devices > Link Desktop Device**, and scan the code. The session is saved to `~/.mcp-telegram/session` and reused automatically.
|
|
51
|
+
|
|
52
|
+
> **Custom session path:** set `TELEGRAM_SESSION_PATH=/path/to/session` to store the session file elsewhere.
|
|
50
53
|
|
|
51
54
|
### 3. Add to Claude
|
|
52
55
|
|
|
@@ -59,6 +62,34 @@ claude mcp add telegram -s user \
|
|
|
59
62
|
|
|
60
63
|
That's it! Ask Claude to run `telegram-status` to verify.
|
|
61
64
|
|
|
65
|
+
### Multiple Accounts
|
|
66
|
+
|
|
67
|
+
Use `TELEGRAM_SESSION_PATH` to run separate Telegram accounts side by side:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Login each account with a unique session path
|
|
71
|
+
TELEGRAM_API_ID=ID1 TELEGRAM_API_HASH=HASH1 TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-work npx @overpod/mcp-telegram login
|
|
72
|
+
TELEGRAM_API_ID=ID2 TELEGRAM_API_HASH=HASH2 TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-personal npx @overpod/mcp-telegram login
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then add each as a separate MCP server:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
claude mcp add telegram-work -s user \
|
|
79
|
+
-e TELEGRAM_API_ID=ID1 \
|
|
80
|
+
-e TELEGRAM_API_HASH=HASH1 \
|
|
81
|
+
-e TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-work \
|
|
82
|
+
-- npx @overpod/mcp-telegram
|
|
83
|
+
|
|
84
|
+
claude mcp add telegram-personal -s user \
|
|
85
|
+
-e TELEGRAM_API_ID=ID2 \
|
|
86
|
+
-e TELEGRAM_API_HASH=HASH2 \
|
|
87
|
+
-e TELEGRAM_SESSION_PATH=~/.mcp-telegram/session-personal \
|
|
88
|
+
-- npx @overpod/mcp-telegram
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each account gets its own session file — no conflicts.
|
|
92
|
+
|
|
62
93
|
## Installation Options
|
|
63
94
|
|
|
64
95
|
### npx (recommended, zero install)
|
|
@@ -173,23 +204,39 @@ const telegramMcp = new MCPClient({
|
|
|
173
204
|
|
|
174
205
|
| Tool | Description |
|
|
175
206
|
|------|-------------|
|
|
176
|
-
| `telegram-list-chats` | List recent dialogs with unread counts |
|
|
207
|
+
| `telegram-list-chats` | List recent dialogs with unread counts, bot/contact markers |
|
|
177
208
|
| `telegram-read-messages` | Read recent messages from a chat |
|
|
178
209
|
| `telegram-search-chats` | Search for chats, users, or channels by name |
|
|
179
210
|
| `telegram-search-messages` | Search messages in a chat by text |
|
|
180
|
-
| `telegram-get-unread` | Get chats with unread messages |
|
|
211
|
+
| `telegram-get-unread` | Get chats with unread messages; forums show per-topic unread breakdown |
|
|
212
|
+
| `telegram-get-contact-requests` | Get incoming messages from non-contacts with preview |
|
|
213
|
+
|
|
214
|
+
### Forum Topics
|
|
215
|
+
|
|
216
|
+
| Tool | Description |
|
|
217
|
+
|------|-------------|
|
|
218
|
+
| `telegram-list-topics` | List forum topics in a group with unread counts and status |
|
|
219
|
+
| `telegram-read-topic-messages` | Read messages from a specific forum topic |
|
|
181
220
|
|
|
182
221
|
### Chat Management
|
|
183
222
|
|
|
184
223
|
| Tool | Description |
|
|
185
224
|
|------|-------------|
|
|
186
225
|
| `telegram-mark-as-read` | Mark a chat as read |
|
|
187
|
-
| `telegram-get-chat-info` | Get detailed info about a chat (name, type, members
|
|
226
|
+
| `telegram-get-chat-info` | Get detailed info about a chat (name, type, members, bot/contact status) |
|
|
188
227
|
| `telegram-get-chat-members` | List members of a group or channel |
|
|
189
228
|
| `telegram-join-chat` | Join a group or channel by username or invite link |
|
|
190
229
|
| `telegram-pin-message` | Pin a message in a chat |
|
|
191
230
|
| `telegram-unpin-message` | Unpin a message in a chat |
|
|
192
231
|
|
|
232
|
+
### Contacts & Moderation
|
|
233
|
+
|
|
234
|
+
| Tool | Description |
|
|
235
|
+
|------|-------------|
|
|
236
|
+
| `telegram-add-contact` | Add a user to your contacts (accept contact request) |
|
|
237
|
+
| `telegram-block-user` | Block a user from sending you messages |
|
|
238
|
+
| `telegram-report-spam` | Report a chat as spam to Telegram |
|
|
239
|
+
|
|
193
240
|
### User Info
|
|
194
241
|
|
|
195
242
|
| Tool | Description |
|
|
@@ -217,6 +264,23 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
217
264
|
| `text` | string | yes | Message text |
|
|
218
265
|
| `replyTo` | number | no | Message ID to reply to |
|
|
219
266
|
| `parseMode` | `"md"` / `"html"` | no | Message formatting mode |
|
|
267
|
+
| `topicId` | number | no | Forum topic ID to send into (for groups with Topics) |
|
|
268
|
+
|
|
269
|
+
### telegram-list-topics
|
|
270
|
+
|
|
271
|
+
| Parameter | Type | Required | Description |
|
|
272
|
+
|-----------|------|----------|-------------|
|
|
273
|
+
| `chatId` | string | yes | Chat ID or @username (group with Topics enabled) |
|
|
274
|
+
| `limit` | number | no | Max topics to return (default: 100) |
|
|
275
|
+
|
|
276
|
+
### telegram-read-topic-messages
|
|
277
|
+
|
|
278
|
+
| Parameter | Type | Required | Description |
|
|
279
|
+
|-----------|------|----------|-------------|
|
|
280
|
+
| `chatId` | string | yes | Chat ID or @username |
|
|
281
|
+
| `topicId` | number | yes | Topic ID (from `telegram-list-topics`) |
|
|
282
|
+
| `limit` | number | no | Number of messages (default: 20) |
|
|
283
|
+
| `offsetId` | number | no | Message ID for pagination |
|
|
220
284
|
|
|
221
285
|
### telegram-list-chats
|
|
222
286
|
|
|
@@ -224,7 +288,7 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
224
288
|
|-----------|------|----------|-------------|
|
|
225
289
|
| `limit` | number | no | Number of chats to return (default: 20) |
|
|
226
290
|
| `offsetDate` | number | no | Unix timestamp for pagination |
|
|
227
|
-
| `filterType` | `"private"` / `"group"` / `"channel"` | no | Filter by chat type |
|
|
291
|
+
| `filterType` | `"private"` / `"group"` / `"channel"` / `"contact_requests"` | no | Filter by chat type |
|
|
228
292
|
|
|
229
293
|
### telegram-read-messages
|
|
230
294
|
|
|
@@ -356,6 +420,33 @@ Most tools accept `chatId` as a string -- either a numeric ID (e.g., `"-10012345
|
|
|
356
420
|
|-----------|------|----------|-------------|
|
|
357
421
|
| `limit` | number | no | Number of unread chats (default: 20) |
|
|
358
422
|
|
|
423
|
+
### telegram-get-contact-requests
|
|
424
|
+
|
|
425
|
+
| Parameter | Type | Required | Description |
|
|
426
|
+
|-----------|------|----------|-------------|
|
|
427
|
+
| `limit` | number | no | Number of contact requests (default: 20) |
|
|
428
|
+
|
|
429
|
+
### telegram-add-contact
|
|
430
|
+
|
|
431
|
+
| Parameter | Type | Required | Description |
|
|
432
|
+
|-----------|------|----------|-------------|
|
|
433
|
+
| `userId` | string | yes | User ID or @username to add |
|
|
434
|
+
| `firstName` | string | yes | First name for the contact |
|
|
435
|
+
| `lastName` | string | no | Last name for the contact |
|
|
436
|
+
| `phone` | string | no | Phone number for the contact |
|
|
437
|
+
|
|
438
|
+
### telegram-block-user
|
|
439
|
+
|
|
440
|
+
| Parameter | Type | Required | Description |
|
|
441
|
+
|-----------|------|----------|-------------|
|
|
442
|
+
| `userId` | string | yes | User ID or @username to block |
|
|
443
|
+
|
|
444
|
+
### telegram-report-spam
|
|
445
|
+
|
|
446
|
+
| Parameter | Type | Required | Description |
|
|
447
|
+
|-----------|------|----------|-------------|
|
|
448
|
+
| `chatId` | string | yes | Chat ID or @username to report |
|
|
449
|
+
|
|
359
450
|
## Development
|
|
360
451
|
|
|
361
452
|
```bash
|
|
@@ -390,7 +481,8 @@ src/
|
|
|
390
481
|
## Security
|
|
391
482
|
|
|
392
483
|
- API credentials are stored in `.env` (gitignored)
|
|
393
|
-
- Session is stored in
|
|
484
|
+
- Session is stored in `~/.mcp-telegram/session` with `0600` permissions (owner-only access)
|
|
485
|
+
- Session directory is created with `0700` permissions
|
|
394
486
|
- Phone number is **not required** -- QR-only authentication
|
|
395
487
|
- This is a **userbot** (personal account), not a bot -- respect the [Telegram Terms of Service](https://core.telegram.org/api/terms)
|
|
396
488
|
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -104,13 +104,15 @@ server.tool("telegram-send-message", "Send a message to a Telegram chat", {
|
|
|
104
104
|
text: z.string().describe("Message text"),
|
|
105
105
|
replyTo: z.number().optional().describe("Message ID to reply to"),
|
|
106
106
|
parseMode: z.enum(["md", "html"]).optional().describe("Message format: md (Markdown) or html"),
|
|
107
|
-
|
|
107
|
+
topicId: z.number().optional().describe("Forum topic ID to send message into (for groups with Topics enabled)"),
|
|
108
|
+
}, async ({ chatId, text, replyTo, parseMode, topicId }) => {
|
|
108
109
|
const err = await requireConnection();
|
|
109
110
|
if (err)
|
|
110
111
|
return { content: [{ type: "text", text: err }] };
|
|
111
112
|
try {
|
|
112
|
-
await telegram.sendMessage(chatId, text, replyTo, parseMode);
|
|
113
|
-
|
|
113
|
+
await telegram.sendMessage(chatId, text, replyTo, parseMode, topicId);
|
|
114
|
+
const dest = topicId ? `topic ${topicId} in ${chatId}` : chatId;
|
|
115
|
+
return { content: [{ type: "text", text: `Message sent to ${dest}` }] };
|
|
114
116
|
}
|
|
115
117
|
catch (e) {
|
|
116
118
|
return { content: [{ type: "text", text: `Send error: ${e.message}` }] };
|
|
@@ -217,7 +219,13 @@ server.tool("telegram-get-unread", "Get unread Telegram chats", {
|
|
|
217
219
|
const prefix = d.type === "group" ? "G" : d.type === "channel" ? "C" : "P";
|
|
218
220
|
const botTag = d.isBot ? " [bot]" : "";
|
|
219
221
|
const contactTag = d.type === "private" && d.isContact === false ? " [not in contacts]" : "";
|
|
220
|
-
|
|
222
|
+
const forumTag = d.forum ? " [forum]" : "";
|
|
223
|
+
let line = `${prefix} ${d.name} (${d.id})${botTag}${contactTag}${forumTag} [${d.unreadCount} unread]`;
|
|
224
|
+
if (d.topics && d.topics.length > 0) {
|
|
225
|
+
const topicLines = d.topics.map((t) => ` # ${t.title} [${t.unreadCount} unread]`);
|
|
226
|
+
line += `\n${topicLines.join("\n")}`;
|
|
227
|
+
}
|
|
228
|
+
return line;
|
|
221
229
|
})
|
|
222
230
|
.join("\n");
|
|
223
231
|
return { content: [{ type: "text", text: text || "No unread chats" }] };
|
|
@@ -303,6 +311,7 @@ server.tool("telegram-get-chat-info", "Get detailed info about a Telegram chat",
|
|
|
303
311
|
`Name: ${info.name}`,
|
|
304
312
|
`ID: ${info.id}`,
|
|
305
313
|
`Type: ${info.type}`,
|
|
314
|
+
...(info.forum ? ["Forum: yes"] : []),
|
|
306
315
|
...(info.username ? [`Username: @${info.username}`] : []),
|
|
307
316
|
...(info.description ? [`Description: ${info.description}`] : []),
|
|
308
317
|
...(info.membersCount != null ? [`Members: ${info.membersCount}`] : []),
|
|
@@ -594,6 +603,49 @@ server.tool("telegram-report-spam", "Report a chat as spam to Telegram", {
|
|
|
594
603
|
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
595
604
|
}
|
|
596
605
|
});
|
|
606
|
+
server.tool("telegram-list-topics", "List forum topics in a Telegram group with Topics enabled. Shows topic names, unread counts, and status", {
|
|
607
|
+
chatId: z.string().describe("Chat ID or username of a group with Topics enabled"),
|
|
608
|
+
limit: z.number().default(100).describe("Max topics to return"),
|
|
609
|
+
}, async ({ chatId, limit }) => {
|
|
610
|
+
const err = await requireConnection();
|
|
611
|
+
if (err)
|
|
612
|
+
return { content: [{ type: "text", text: err }] };
|
|
613
|
+
try {
|
|
614
|
+
const topics = await telegram.getForumTopics(chatId, limit);
|
|
615
|
+
const text = topics
|
|
616
|
+
.map((t) => {
|
|
617
|
+
const flags = [t.pinned ? "pinned" : "", t.closed ? "closed" : ""].filter(Boolean).join(", ");
|
|
618
|
+
const flagStr = flags ? ` [${flags}]` : "";
|
|
619
|
+
const unread = t.unreadCount > 0 ? ` [${t.unreadCount} unread]` : "";
|
|
620
|
+
return `# ${t.title} (id: ${t.id})${flagStr}${unread}`;
|
|
621
|
+
})
|
|
622
|
+
.join("\n");
|
|
623
|
+
return { content: [{ type: "text", text: text || "No topics found" }] };
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
server.tool("telegram-read-topic-messages", "Read messages from a specific forum topic in a Telegram group", {
|
|
630
|
+
chatId: z.string().describe("Chat ID or username"),
|
|
631
|
+
topicId: z.number().describe("Topic ID (get from telegram-list-topics)"),
|
|
632
|
+
limit: z.number().default(20).describe("Number of messages to return"),
|
|
633
|
+
offsetId: z.number().optional().describe("Message ID to start from (for pagination)"),
|
|
634
|
+
}, async ({ chatId, topicId, limit, offsetId }) => {
|
|
635
|
+
const err = await requireConnection();
|
|
636
|
+
if (err)
|
|
637
|
+
return { content: [{ type: "text", text: err }] };
|
|
638
|
+
try {
|
|
639
|
+
const messages = await telegram.getTopicMessages(chatId, topicId, limit, offsetId);
|
|
640
|
+
const text = messages
|
|
641
|
+
.map((m) => `[${m.date}] ${m.sender}: ${m.text}${m.media ? ` [${m.media.type}${m.media.fileName ? `: ${m.media.fileName}` : ""}]` : ""}`)
|
|
642
|
+
.join("\n\n");
|
|
643
|
+
return { content: [{ type: "text", text: text || "No messages in this topic" }] };
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
647
|
+
}
|
|
648
|
+
});
|
|
597
649
|
// --- Start ---
|
|
598
650
|
async function main() {
|
|
599
651
|
// 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) */
|
|
@@ -32,7 +36,7 @@ export declare class TelegramService {
|
|
|
32
36
|
username?: string;
|
|
33
37
|
firstName?: string;
|
|
34
38
|
}>;
|
|
35
|
-
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html"): Promise<void>;
|
|
39
|
+
sendMessage(chatId: string, text: string, replyTo?: number, parseMode?: "md" | "html", topicId?: number): Promise<void>;
|
|
36
40
|
sendFile(chatId: string, filePath: string, caption?: string): Promise<void>;
|
|
37
41
|
downloadMedia(chatId: string, messageId: number, downloadPath: string): Promise<string>;
|
|
38
42
|
downloadMediaAsBuffer(chatId: string, messageId: number): Promise<{
|
|
@@ -58,6 +62,12 @@ export declare class TelegramService {
|
|
|
58
62
|
unreadCount: number;
|
|
59
63
|
isBot?: boolean;
|
|
60
64
|
isContact?: boolean;
|
|
65
|
+
forum?: boolean;
|
|
66
|
+
topics?: Array<{
|
|
67
|
+
id: number;
|
|
68
|
+
title: string;
|
|
69
|
+
unreadCount: number;
|
|
70
|
+
}>;
|
|
61
71
|
}>>;
|
|
62
72
|
getContactRequests(limit?: number): Promise<Array<{
|
|
63
73
|
id: string;
|
|
@@ -84,6 +94,7 @@ export declare class TelegramService {
|
|
|
84
94
|
membersCount?: number;
|
|
85
95
|
isBot?: boolean;
|
|
86
96
|
isContact?: boolean;
|
|
97
|
+
forum?: boolean;
|
|
87
98
|
}>;
|
|
88
99
|
/** Extract media info from a message */
|
|
89
100
|
private extractMediaInfo;
|
|
@@ -144,6 +155,28 @@ export declare class TelegramService {
|
|
|
144
155
|
quiz?: boolean;
|
|
145
156
|
correctAnswer?: number;
|
|
146
157
|
}): Promise<number>;
|
|
158
|
+
getForumTopics(chatId: string, limit?: number): Promise<Array<{
|
|
159
|
+
id: number;
|
|
160
|
+
title: string;
|
|
161
|
+
unreadCount: number;
|
|
162
|
+
unreadMentions: number;
|
|
163
|
+
iconColor: number;
|
|
164
|
+
closed: boolean;
|
|
165
|
+
pinned: boolean;
|
|
166
|
+
}>>;
|
|
167
|
+
getTopicMessages(chatId: string, topicId: number, limit?: number, offsetId?: number): Promise<Array<{
|
|
168
|
+
id: number;
|
|
169
|
+
text: string;
|
|
170
|
+
sender: string;
|
|
171
|
+
date: string;
|
|
172
|
+
media?: {
|
|
173
|
+
type: string;
|
|
174
|
+
fileName?: string;
|
|
175
|
+
size?: number;
|
|
176
|
+
};
|
|
177
|
+
}>>;
|
|
178
|
+
/** Check if a chat entity is a forum (has topics enabled) */
|
|
179
|
+
isForum(chatId: string): Promise<boolean>;
|
|
147
180
|
joinChat(target: string): Promise<{
|
|
148
181
|
id: string;
|
|
149
182
|
title: string;
|
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 */
|
|
@@ -230,14 +273,29 @@ export class TelegramService {
|
|
|
230
273
|
firstName: user.firstName ?? undefined,
|
|
231
274
|
};
|
|
232
275
|
}
|
|
233
|
-
async sendMessage(chatId, text, replyTo, parseMode) {
|
|
276
|
+
async sendMessage(chatId, text, replyTo, parseMode, topicId) {
|
|
234
277
|
if (!this.client || !this.connected)
|
|
235
278
|
throw new Error("Not connected");
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
279
|
+
if (topicId) {
|
|
280
|
+
// Forum topics require raw API call with InputReplyToMessage
|
|
281
|
+
const peer = await this.client.getInputEntity(chatId);
|
|
282
|
+
await this.client.invoke(new Api.messages.SendMessage({
|
|
283
|
+
peer,
|
|
284
|
+
message: text,
|
|
285
|
+
randomId: bigInt(Math.floor(Math.random() * 1e15)),
|
|
286
|
+
replyTo: new Api.InputReplyToMessage({
|
|
287
|
+
replyToMsgId: replyTo ?? topicId,
|
|
288
|
+
topMsgId: topicId,
|
|
289
|
+
}),
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
await this.client.sendMessage(chatId, {
|
|
294
|
+
message: text,
|
|
295
|
+
...(replyTo ? { replyTo } : {}),
|
|
296
|
+
...(parseMode ? { parseMode: parseMode === "html" ? "html" : "md" } : {}),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
241
299
|
}
|
|
242
300
|
async sendFile(chatId, filePath, caption) {
|
|
243
301
|
if (!this.client || !this.connected)
|
|
@@ -331,12 +389,11 @@ export class TelegramService {
|
|
|
331
389
|
if (!this.client || !this.connected)
|
|
332
390
|
throw new Error("Not connected");
|
|
333
391
|
const dialogs = await this.client.getDialogs({ limit: limit * 3 });
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
.slice(0, limit)
|
|
337
|
-
.map((d) => {
|
|
392
|
+
const unread = dialogs.filter((d) => d.unreadCount > 0).slice(0, limit);
|
|
393
|
+
const results = await Promise.all(unread.map(async (d) => {
|
|
338
394
|
const isUser = d.entity instanceof Api.User;
|
|
339
|
-
|
|
395
|
+
const isForum = d.entity instanceof Api.Channel && Boolean(d.entity.forum);
|
|
396
|
+
const base = {
|
|
340
397
|
id: d.id?.toString() ?? "",
|
|
341
398
|
name: d.title ?? d.name ?? "Unknown",
|
|
342
399
|
type: d.isGroup ? "group" : d.isChannel ? "channel" : "private",
|
|
@@ -345,7 +402,29 @@ export class TelegramService {
|
|
|
345
402
|
? { isBot: Boolean(d.entity.bot), isContact: Boolean(d.entity.contact) }
|
|
346
403
|
: {}),
|
|
347
404
|
};
|
|
348
|
-
|
|
405
|
+
if (isForum) {
|
|
406
|
+
try {
|
|
407
|
+
const forumTopics = await this.getForumTopics(d.id?.toString() ?? "");
|
|
408
|
+
const unreadTopics = forumTopics
|
|
409
|
+
.filter((t) => t.unreadCount > 0)
|
|
410
|
+
.map((t) => ({ id: t.id, title: t.title, unreadCount: t.unreadCount }));
|
|
411
|
+
const realUnread = unreadTopics.reduce((sum, t) => sum + t.unreadCount, 0);
|
|
412
|
+
if (realUnread === 0)
|
|
413
|
+
return null;
|
|
414
|
+
return {
|
|
415
|
+
...base,
|
|
416
|
+
unreadCount: realUnread,
|
|
417
|
+
forum: true,
|
|
418
|
+
topics: unreadTopics.length > 0 ? unreadTopics : undefined,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return { ...base, forum: true };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return base;
|
|
426
|
+
}));
|
|
427
|
+
return results.filter((r) => r !== null);
|
|
349
428
|
}
|
|
350
429
|
async getContactRequests(limit = 20) {
|
|
351
430
|
if (!this.client || !this.connected)
|
|
@@ -450,6 +529,7 @@ export class TelegramService {
|
|
|
450
529
|
username: entity.username ?? undefined,
|
|
451
530
|
description,
|
|
452
531
|
membersCount,
|
|
532
|
+
forum: Boolean(entity.forum) || undefined,
|
|
453
533
|
};
|
|
454
534
|
}
|
|
455
535
|
if (entity instanceof Api.Chat) {
|
|
@@ -726,6 +806,75 @@ export class TelegramService {
|
|
|
726
806
|
}
|
|
727
807
|
return 0;
|
|
728
808
|
}
|
|
809
|
+
async getForumTopics(chatId, limit = 100) {
|
|
810
|
+
if (!this.client || !this.connected)
|
|
811
|
+
throw new Error("Not connected");
|
|
812
|
+
const entity = await this.client.getEntity(chatId);
|
|
813
|
+
if (!(entity instanceof Api.Channel))
|
|
814
|
+
throw new Error("Forum topics are only available in supergroups");
|
|
815
|
+
const result = await this.client.invoke(new Api.channels.GetForumTopics({
|
|
816
|
+
channel: entity,
|
|
817
|
+
limit,
|
|
818
|
+
offsetTopic: 0,
|
|
819
|
+
offsetDate: 0,
|
|
820
|
+
offsetId: 0,
|
|
821
|
+
}));
|
|
822
|
+
const topics = [];
|
|
823
|
+
for (const topic of result.topics) {
|
|
824
|
+
if (topic instanceof Api.ForumTopic) {
|
|
825
|
+
topics.push({
|
|
826
|
+
id: topic.id,
|
|
827
|
+
title: topic.title,
|
|
828
|
+
unreadCount: topic.unreadCount,
|
|
829
|
+
unreadMentions: topic.unreadMentionsCount,
|
|
830
|
+
iconColor: topic.iconColor,
|
|
831
|
+
closed: Boolean(topic.closed),
|
|
832
|
+
pinned: Boolean(topic.pinned),
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return topics;
|
|
837
|
+
}
|
|
838
|
+
async getTopicMessages(chatId, topicId, limit = 20, offsetId) {
|
|
839
|
+
if (!this.client || !this.connected)
|
|
840
|
+
throw new Error("Not connected");
|
|
841
|
+
const peer = await this.client.getInputEntity(chatId);
|
|
842
|
+
const result = await this.client.invoke(new Api.messages.GetReplies({
|
|
843
|
+
peer,
|
|
844
|
+
msgId: topicId,
|
|
845
|
+
limit,
|
|
846
|
+
...(offsetId ? { offsetId } : {}),
|
|
847
|
+
offsetDate: 0,
|
|
848
|
+
addOffset: 0,
|
|
849
|
+
maxId: 0,
|
|
850
|
+
minId: 0,
|
|
851
|
+
hash: bigInt(0),
|
|
852
|
+
}));
|
|
853
|
+
const messages = "messages" in result ? result.messages : [];
|
|
854
|
+
const results = await Promise.all(messages
|
|
855
|
+
.filter((m) => m instanceof Api.Message)
|
|
856
|
+
.map(async (m) => ({
|
|
857
|
+
id: m.id,
|
|
858
|
+
text: m.message ?? "",
|
|
859
|
+
sender: await this.resolveSenderName(m.senderId),
|
|
860
|
+
date: new Date((m.date ?? 0) * 1000).toISOString(),
|
|
861
|
+
media: this.extractMediaInfo(m.media),
|
|
862
|
+
})));
|
|
863
|
+
return results;
|
|
864
|
+
}
|
|
865
|
+
/** Check if a chat entity is a forum (has topics enabled) */
|
|
866
|
+
async isForum(chatId) {
|
|
867
|
+
if (!this.client || !this.connected)
|
|
868
|
+
throw new Error("Not connected");
|
|
869
|
+
try {
|
|
870
|
+
const entity = await this.client.getEntity(chatId);
|
|
871
|
+
if (entity instanceof Api.Channel) {
|
|
872
|
+
return Boolean(entity.forum);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
catch { }
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
729
878
|
async joinChat(target) {
|
|
730
879
|
if (!this.client)
|
|
731
880
|
throw new Error("Not connected");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@overpod/mcp-telegram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
@@ -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"
|