@deus-ai/telegram-mcp 1.0.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 +48 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/telegram.d.ts +30 -0
- package/dist/telegram.js +289 -0
- package/dist/telegram.js.map +1 -0
- package/dist/telegram.test.d.ts +1 -0
- package/dist/telegram.test.js +152 -0
- package/dist/telegram.test.js.map +1 -0
- package/package.json +34 -0
- package/src/index.ts +42 -0
- package/src/telegram.test.ts +180 -0
- package/src/telegram.ts +372 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# @deus-ai/telegram-mcp
|
|
2
|
+
|
|
3
|
+
Standalone MCP server for Telegram bots. Uses [grammY](https://grammy.dev/) for the Telegram Bot API.
|
|
4
|
+
|
|
5
|
+
Works with any MCP client — Claude Code, Claude Desktop, or your own application.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"telegram": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["@deus-ai/telegram-mcp"],
|
|
15
|
+
"env": {
|
|
16
|
+
"TELEGRAM_BOT_TOKEN": "your-bot-token"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `send_message` | Send a text message to a chat |
|
|
28
|
+
| `send_typing` | Show/hide typing indicator |
|
|
29
|
+
| `get_status` | Connection status and bot info |
|
|
30
|
+
| `list_chats` | List known chats and groups |
|
|
31
|
+
| `get_new_messages` | Poll for incoming messages (cursor-based) |
|
|
32
|
+
| `connect` / `disconnect` | Connection lifecycle |
|
|
33
|
+
|
|
34
|
+
## Incoming Messages
|
|
35
|
+
|
|
36
|
+
Messages are pushed in real-time via MCP logging notifications with `logger: "incoming_message"`. For clients that don't support notifications, use the `get_new_messages` polling tool.
|
|
37
|
+
|
|
38
|
+
## Environment Variables
|
|
39
|
+
|
|
40
|
+
| Variable | Default | Description |
|
|
41
|
+
|----------|---------|-------------|
|
|
42
|
+
| `TELEGRAM_BOT_TOKEN` | *(required)* | Bot token from @BotFather |
|
|
43
|
+
| `ASSISTANT_NAME` | `Deus` | Name for @mention translation |
|
|
44
|
+
| `LOG_LEVEL` | `info` | Pino log level |
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Telegram MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Standalone MCP server that provides Telegram bot messaging tools.
|
|
6
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Config (env vars):
|
|
9
|
+
* TELEGRAM_BOT_TOKEN — Telegram bot token from @BotFather
|
|
10
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
11
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Telegram MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Standalone MCP server that provides Telegram bot messaging tools.
|
|
6
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
7
|
+
*
|
|
8
|
+
* Config (env vars):
|
|
9
|
+
* TELEGRAM_BOT_TOKEN — Telegram bot token from @BotFather
|
|
10
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
11
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
12
|
+
*/
|
|
13
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
14
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
15
|
+
import { registerCommonTools } from '@deus-ai/channel-core';
|
|
16
|
+
import { TelegramProvider } from './telegram.js';
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: '@deus-ai/telegram-mcp',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
});
|
|
21
|
+
const provider = new TelegramProvider();
|
|
22
|
+
// Register common tools (send_message, get_status, etc.)
|
|
23
|
+
registerCommonTools(server, provider);
|
|
24
|
+
// ── Auto-connect if token is configured ───────────────────────────────
|
|
25
|
+
if (provider.hasToken()) {
|
|
26
|
+
provider.connect().catch((err) => {
|
|
27
|
+
console.error('[@deus-ai/telegram-mcp] Auto-connect failed:', err.message);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// ── Start MCP transport ───────────────────────────────────────────────
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,uBAAuB;IAC7B,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,IAAI,gBAAgB,EAAE,CAAC;AAExC,yDAAyD;AACzD,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAEtC,yEAAyE;AAEzE,IAAI,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QAC/B,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,yEAAyE;AAEzE,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Telegram bot provider.
|
|
3
|
+
* Extracted from Deus TelegramChannel — no Deus-specific dependencies.
|
|
4
|
+
* Config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChannelProvider, ChannelStatus, ChatInfo, IncomingMessage } from '@deus-ai/channel-core';
|
|
7
|
+
export declare class TelegramProvider implements ChannelProvider {
|
|
8
|
+
readonly name = "telegram";
|
|
9
|
+
private bot;
|
|
10
|
+
private connectTime;
|
|
11
|
+
private knownChats;
|
|
12
|
+
private botUsername?;
|
|
13
|
+
private consecutiveErrors;
|
|
14
|
+
private resetting;
|
|
15
|
+
onMessage: (msg: IncomingMessage) => void;
|
|
16
|
+
connect(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Reset the polling session after consecutive errors.
|
|
19
|
+
* Retries with exponential backoff (1s, 2s, 4s), then exits on failure.
|
|
20
|
+
*/
|
|
21
|
+
private resetPolling;
|
|
22
|
+
sendMessage(chatId: string, text: string): Promise<void>;
|
|
23
|
+
isConnected(): boolean;
|
|
24
|
+
getStatus(): ChannelStatus;
|
|
25
|
+
disconnect(): Promise<void>;
|
|
26
|
+
setTyping(chatId: string, isTyping: boolean): Promise<void>;
|
|
27
|
+
listChats(): Promise<ChatInfo[]>;
|
|
28
|
+
/** Check if a bot token is configured. */
|
|
29
|
+
hasToken(): boolean;
|
|
30
|
+
}
|
package/dist/telegram.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Telegram bot provider.
|
|
3
|
+
* Extracted from Deus TelegramChannel — no Deus-specific dependencies.
|
|
4
|
+
* Config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
import https from 'https';
|
|
7
|
+
import { Bot } from 'grammy';
|
|
8
|
+
import pino from 'pino';
|
|
9
|
+
const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
|
|
10
|
+
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
|
|
11
|
+
// Use stderr for logging (stdout is reserved for MCP JSON-RPC)
|
|
12
|
+
const logger = pino({ level: process.env.LOG_LEVEL || 'info' }, pino.destination(2));
|
|
13
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
14
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
15
|
+
const MAX_RECONNECT_RETRIES = 3;
|
|
16
|
+
const BASE_BACKOFF_MS = 1000;
|
|
17
|
+
/**
|
|
18
|
+
* Send a message with Telegram Markdown parse mode, falling back to plain text.
|
|
19
|
+
*/
|
|
20
|
+
async function sendTelegramMessage(api, chatId, text, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
await api.sendMessage(chatId, text, { ...options, parse_mode: 'Markdown' });
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
await api.sendMessage(chatId, text, options);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class TelegramProvider {
|
|
29
|
+
name = 'telegram';
|
|
30
|
+
bot = null;
|
|
31
|
+
connectTime = 0;
|
|
32
|
+
knownChats = new Map();
|
|
33
|
+
botUsername;
|
|
34
|
+
consecutiveErrors = 0;
|
|
35
|
+
resetting = false;
|
|
36
|
+
// Set by server-base.ts
|
|
37
|
+
onMessage = () => { };
|
|
38
|
+
async connect() {
|
|
39
|
+
if (!BOT_TOKEN) {
|
|
40
|
+
throw new Error('TELEGRAM_BOT_TOKEN not set');
|
|
41
|
+
}
|
|
42
|
+
this.bot = new Bot(BOT_TOKEN, {
|
|
43
|
+
client: {
|
|
44
|
+
baseFetchConfig: { agent: https.globalAgent, compress: true },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// /chatid command for registration
|
|
48
|
+
this.bot.command('chatid', (ctx) => {
|
|
49
|
+
const chatId = ctx.chat.id;
|
|
50
|
+
const chatType = ctx.chat.type;
|
|
51
|
+
const chatName = chatType === 'private'
|
|
52
|
+
? ctx.from?.first_name || 'Private'
|
|
53
|
+
: ctx.chat.title || 'Unknown';
|
|
54
|
+
ctx.reply(`Chat ID: \`tg:${chatId}\`\nName: ${chatName}\nType: ${chatType}`, { parse_mode: 'Markdown' });
|
|
55
|
+
});
|
|
56
|
+
this.bot.command('ping', (ctx) => {
|
|
57
|
+
ctx.reply(`${ASSISTANT_NAME} is online.`);
|
|
58
|
+
});
|
|
59
|
+
const BOT_COMMANDS = new Set(['chatid', 'ping']);
|
|
60
|
+
// Handle text messages
|
|
61
|
+
this.bot.on('message:text', async (ctx) => {
|
|
62
|
+
this.consecutiveErrors = 0;
|
|
63
|
+
if (ctx.message.text.startsWith('/')) {
|
|
64
|
+
const cmd = ctx.message.text.slice(1).split(/[\s@]/)[0].toLowerCase();
|
|
65
|
+
if (BOT_COMMANDS.has(cmd))
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const chatJid = `tg:${ctx.chat.id}`;
|
|
69
|
+
let content = ctx.message.text;
|
|
70
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
71
|
+
const senderName = ctx.from?.first_name ||
|
|
72
|
+
ctx.from?.username ||
|
|
73
|
+
ctx.from?.id.toString() ||
|
|
74
|
+
'Unknown';
|
|
75
|
+
const sender = ctx.from?.id.toString() || '';
|
|
76
|
+
const msgId = ctx.message.message_id.toString();
|
|
77
|
+
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
|
78
|
+
const chatName = ctx.chat.type === 'private'
|
|
79
|
+
? senderName
|
|
80
|
+
: ctx.chat.title || chatJid;
|
|
81
|
+
// Track chat
|
|
82
|
+
this.knownChats.set(chatJid, { name: chatName, isGroup });
|
|
83
|
+
// Translate @bot mentions to @AssistantName
|
|
84
|
+
const botUser = this.botUsername?.toLowerCase();
|
|
85
|
+
if (botUser) {
|
|
86
|
+
const entities = ctx.message.entities || [];
|
|
87
|
+
const isBotMentioned = entities.some((e) => {
|
|
88
|
+
if (e.type === 'mention') {
|
|
89
|
+
return (content.substring(e.offset, e.offset + e.length).toLowerCase() ===
|
|
90
|
+
`@${botUser}`);
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
if (isBotMentioned) {
|
|
95
|
+
content = `@${ASSISTANT_NAME} ${content}`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Reply context
|
|
99
|
+
const replyTo = ctx.message.reply_to_message;
|
|
100
|
+
// Forward ALL messages
|
|
101
|
+
this.onMessage({
|
|
102
|
+
id: msgId,
|
|
103
|
+
chat_id: chatJid,
|
|
104
|
+
sender,
|
|
105
|
+
sender_name: senderName,
|
|
106
|
+
content,
|
|
107
|
+
timestamp,
|
|
108
|
+
is_from_me: false,
|
|
109
|
+
is_group: isGroup,
|
|
110
|
+
chat_name: chatName,
|
|
111
|
+
metadata: {
|
|
112
|
+
thread_id: ctx.message.message_thread_id?.toString(),
|
|
113
|
+
reply_to_message_id: replyTo?.message_id?.toString(),
|
|
114
|
+
reply_to_content: replyTo?.text || replyTo?.caption,
|
|
115
|
+
reply_to_sender_name: replyTo
|
|
116
|
+
? replyTo.from?.first_name ||
|
|
117
|
+
replyTo.from?.username ||
|
|
118
|
+
replyTo.from?.id?.toString()
|
|
119
|
+
: undefined,
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// Handle media messages
|
|
124
|
+
const storeMedia = (ctx, placeholder) => {
|
|
125
|
+
const chatJid = `tg:${ctx.chat.id}`;
|
|
126
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
127
|
+
const senderName = ctx.from?.first_name ||
|
|
128
|
+
ctx.from?.username ||
|
|
129
|
+
ctx.from?.id?.toString() ||
|
|
130
|
+
'Unknown';
|
|
131
|
+
const caption = ctx.message.caption ? ` ${ctx.message.caption}` : '';
|
|
132
|
+
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
|
133
|
+
this.knownChats.set(chatJid, {
|
|
134
|
+
name: ctx.chat.type === 'private'
|
|
135
|
+
? senderName
|
|
136
|
+
: ctx.chat.title || chatJid,
|
|
137
|
+
isGroup,
|
|
138
|
+
});
|
|
139
|
+
this.onMessage({
|
|
140
|
+
id: ctx.message.message_id.toString(),
|
|
141
|
+
chat_id: chatJid,
|
|
142
|
+
sender: ctx.from?.id?.toString() || '',
|
|
143
|
+
sender_name: senderName,
|
|
144
|
+
content: `${placeholder}${caption}`,
|
|
145
|
+
timestamp,
|
|
146
|
+
is_from_me: false,
|
|
147
|
+
is_group: isGroup,
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
this.bot.on('message:photo', (ctx) => storeMedia(ctx, '[Photo]'));
|
|
151
|
+
this.bot.on('message:video', (ctx) => storeMedia(ctx, '[Video]'));
|
|
152
|
+
this.bot.on('message:voice', (ctx) => storeMedia(ctx, '[Voice message]'));
|
|
153
|
+
this.bot.on('message:audio', (ctx) => storeMedia(ctx, '[Audio]'));
|
|
154
|
+
this.bot.on('message:document', (ctx) => {
|
|
155
|
+
const name = ctx.message.document?.file_name || 'file';
|
|
156
|
+
storeMedia(ctx, `[Document: ${name}]`);
|
|
157
|
+
});
|
|
158
|
+
this.bot.on('message:sticker', (ctx) => {
|
|
159
|
+
const emoji = ctx.message.sticker?.emoji || '';
|
|
160
|
+
storeMedia(ctx, `[Sticker ${emoji}]`);
|
|
161
|
+
});
|
|
162
|
+
this.bot.on('message:location', (ctx) => storeMedia(ctx, '[Location]'));
|
|
163
|
+
this.bot.on('message:contact', (ctx) => storeMedia(ctx, '[Contact]'));
|
|
164
|
+
this.bot.catch((err) => {
|
|
165
|
+
this.consecutiveErrors++;
|
|
166
|
+
logger.error({ err: err.message, consecutiveErrors: this.consecutiveErrors }, 'Telegram bot error');
|
|
167
|
+
if (this.consecutiveErrors >= MAX_CONSECUTIVE_ERRORS && !this.resetting) {
|
|
168
|
+
this.resetPolling();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
this.bot.start({
|
|
173
|
+
onStart: (botInfo) => {
|
|
174
|
+
this.botUsername = botInfo.username;
|
|
175
|
+
this.connectTime = Date.now();
|
|
176
|
+
this.consecutiveErrors = 0;
|
|
177
|
+
logger.info({ username: botInfo.username, id: botInfo.id }, 'Telegram bot connected');
|
|
178
|
+
resolve();
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Reset the polling session after consecutive errors.
|
|
185
|
+
* Retries with exponential backoff (1s, 2s, 4s), then exits on failure.
|
|
186
|
+
*/
|
|
187
|
+
async resetPolling() {
|
|
188
|
+
if (!this.bot || this.resetting)
|
|
189
|
+
return;
|
|
190
|
+
this.resetting = true;
|
|
191
|
+
logger.warn({ consecutiveErrors: this.consecutiveErrors }, 'Too many consecutive errors, resetting Telegram polling session');
|
|
192
|
+
const bot = this.bot;
|
|
193
|
+
bot.stop();
|
|
194
|
+
this.consecutiveErrors = 0;
|
|
195
|
+
for (let attempt = 0; attempt < MAX_RECONNECT_RETRIES; attempt++) {
|
|
196
|
+
const delayMs = BASE_BACKOFF_MS * Math.pow(2, attempt);
|
|
197
|
+
logger.info({ attempt: attempt + 1, delayMs }, 'Attempting polling reset with backoff');
|
|
198
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
199
|
+
try {
|
|
200
|
+
await new Promise((resolve, reject) => {
|
|
201
|
+
const timeout = setTimeout(() => {
|
|
202
|
+
reject(new Error('Polling restart timed out'));
|
|
203
|
+
}, 30_000);
|
|
204
|
+
bot.start({
|
|
205
|
+
onStart: (botInfo) => {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
this.botUsername = botInfo.username;
|
|
208
|
+
this.connectTime = Date.now();
|
|
209
|
+
this.resetting = false;
|
|
210
|
+
this.consecutiveErrors = 0;
|
|
211
|
+
logger.info({ username: botInfo.username, id: botInfo.id }, 'Telegram bot reconnected after polling reset');
|
|
212
|
+
resolve();
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
return; // Success — exit retry loop
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
logger.error({ attempt: attempt + 1, err }, 'Polling reset attempt failed');
|
|
220
|
+
bot.stop();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
logger.fatal('Telegram bot failed to reconnect after %d retries — exiting', MAX_RECONNECT_RETRIES);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
async sendMessage(chatId, text) {
|
|
227
|
+
if (!this.bot)
|
|
228
|
+
return;
|
|
229
|
+
try {
|
|
230
|
+
const numericId = chatId.replace(/^tg:/, '');
|
|
231
|
+
if (text.length <= MAX_MESSAGE_LENGTH) {
|
|
232
|
+
await sendTelegramMessage(this.bot.api, numericId, text);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
for (let i = 0; i < text.length; i += MAX_MESSAGE_LENGTH) {
|
|
236
|
+
await sendTelegramMessage(this.bot.api, numericId, text.slice(i, i + MAX_MESSAGE_LENGTH));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
logger.error({ chatId, err }, 'Failed to send Telegram message');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
isConnected() {
|
|
245
|
+
return this.bot !== null;
|
|
246
|
+
}
|
|
247
|
+
getStatus() {
|
|
248
|
+
return {
|
|
249
|
+
connected: this.bot !== null,
|
|
250
|
+
channel: 'telegram',
|
|
251
|
+
identity: this.botUsername,
|
|
252
|
+
uptime_seconds: this.connectTime
|
|
253
|
+
? Math.floor((Date.now() - this.connectTime) / 1000)
|
|
254
|
+
: 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async disconnect() {
|
|
258
|
+
if (this.bot) {
|
|
259
|
+
this.resetting = false;
|
|
260
|
+
this.consecutiveErrors = 0;
|
|
261
|
+
this.bot.stop();
|
|
262
|
+
this.bot = null;
|
|
263
|
+
logger.info('Telegram bot stopped');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async setTyping(chatId, isTyping) {
|
|
267
|
+
if (!this.bot || !isTyping)
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
const numericId = chatId.replace(/^tg:/, '');
|
|
271
|
+
await this.bot.api.sendChatAction(numericId, 'typing');
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Best effort
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async listChats() {
|
|
278
|
+
return Array.from(this.knownChats.entries()).map(([id, info]) => ({
|
|
279
|
+
id,
|
|
280
|
+
name: info.name,
|
|
281
|
+
is_group: info.isGroup,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
/** Check if a bot token is configured. */
|
|
285
|
+
hasToken() {
|
|
286
|
+
return !!BOT_TOKEN;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=telegram.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.js","sourceRoot":"","sources":["../src/telegram.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
// vi.hoisted runs before vi.mock hoisting — set env before module evaluation
|
|
3
|
+
vi.hoisted(() => {
|
|
4
|
+
process.env.TELEGRAM_BOT_TOKEN = 'test-token-123';
|
|
5
|
+
});
|
|
6
|
+
// Shared state for the mock bot
|
|
7
|
+
let catchHandler = null;
|
|
8
|
+
const mockStart = vi.fn((options) => {
|
|
9
|
+
if (options?.onStart) {
|
|
10
|
+
options.onStart({ username: 'test_bot', id: 123 });
|
|
11
|
+
}
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
});
|
|
14
|
+
const mockStop = vi.fn();
|
|
15
|
+
const mockCommand = vi.fn();
|
|
16
|
+
const mockOn = vi.fn();
|
|
17
|
+
const mockCatch = vi.fn((handler) => {
|
|
18
|
+
catchHandler = handler;
|
|
19
|
+
});
|
|
20
|
+
vi.mock('grammy', () => {
|
|
21
|
+
return {
|
|
22
|
+
Bot: class MockBot {
|
|
23
|
+
command = mockCommand;
|
|
24
|
+
on = mockOn;
|
|
25
|
+
catch = mockCatch;
|
|
26
|
+
start = mockStart;
|
|
27
|
+
stop = mockStop;
|
|
28
|
+
api = {
|
|
29
|
+
sendMessage: vi.fn(),
|
|
30
|
+
sendChatAction: vi.fn(),
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
Api: class {
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
vi.mock('pino', () => {
|
|
38
|
+
const mockLogger = {
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
warn: vi.fn(),
|
|
41
|
+
error: vi.fn(),
|
|
42
|
+
debug: vi.fn(),
|
|
43
|
+
fatal: vi.fn(),
|
|
44
|
+
};
|
|
45
|
+
const pinoFn = () => mockLogger;
|
|
46
|
+
pinoFn.destination = () => ({});
|
|
47
|
+
return { default: pinoFn };
|
|
48
|
+
});
|
|
49
|
+
import { TelegramProvider } from './telegram.js';
|
|
50
|
+
describe('TelegramProvider', () => {
|
|
51
|
+
let provider;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
catchHandler = null;
|
|
54
|
+
// Reset implementations but keep fns alive
|
|
55
|
+
mockStart.mockImplementation((options) => {
|
|
56
|
+
if (options?.onStart) {
|
|
57
|
+
options.onStart({ username: 'test_bot', id: 123 });
|
|
58
|
+
}
|
|
59
|
+
return Promise.resolve();
|
|
60
|
+
});
|
|
61
|
+
mockStop.mockReset();
|
|
62
|
+
mockCommand.mockReset();
|
|
63
|
+
mockOn.mockReset();
|
|
64
|
+
mockCatch.mockImplementation((handler) => {
|
|
65
|
+
catchHandler = handler;
|
|
66
|
+
});
|
|
67
|
+
provider = new TelegramProvider();
|
|
68
|
+
});
|
|
69
|
+
describe('error tracking and polling reset', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
vi.useFakeTimers();
|
|
72
|
+
});
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
it('tracks consecutive errors via the catch handler', async () => {
|
|
77
|
+
await provider.connect();
|
|
78
|
+
expect(catchHandler).toBeDefined();
|
|
79
|
+
// Simulate errors below threshold
|
|
80
|
+
for (let i = 0; i < 3; i++) {
|
|
81
|
+
catchHandler({ message: `error ${i}` });
|
|
82
|
+
}
|
|
83
|
+
// Bot should not have been stopped
|
|
84
|
+
expect(mockStop).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
it('resets polling after MAX_CONSECUTIVE_ERRORS (5) failures', async () => {
|
|
87
|
+
await provider.connect();
|
|
88
|
+
// Reset call counts from connect()
|
|
89
|
+
mockStop.mockClear();
|
|
90
|
+
mockStart.mockClear();
|
|
91
|
+
// Simulate 5 consecutive errors (the threshold)
|
|
92
|
+
for (let i = 0; i < 5; i++) {
|
|
93
|
+
catchHandler({ message: `error ${i}` });
|
|
94
|
+
}
|
|
95
|
+
// Stop is called immediately in resetPolling
|
|
96
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
97
|
+
// Advance past the first backoff delay (1s) to trigger start
|
|
98
|
+
await vi.advanceTimersByTimeAsync(1100);
|
|
99
|
+
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
100
|
+
});
|
|
101
|
+
it('does not reset polling twice while already resetting', async () => {
|
|
102
|
+
await provider.connect();
|
|
103
|
+
// Make start not call onStart (simulating slow restart that times out)
|
|
104
|
+
mockStart.mockImplementationOnce(() => Promise.resolve());
|
|
105
|
+
mockStop.mockClear();
|
|
106
|
+
mockStart.mockClear();
|
|
107
|
+
// Trigger 5 errors to start a reset
|
|
108
|
+
for (let i = 0; i < 5; i++) {
|
|
109
|
+
catchHandler({ message: `error ${i}` });
|
|
110
|
+
}
|
|
111
|
+
// Now send 5 more errors while resetting
|
|
112
|
+
for (let i = 0; i < 5; i++) {
|
|
113
|
+
catchHandler({ message: `error ${i}` });
|
|
114
|
+
}
|
|
115
|
+
// Should only have triggered one reset (one stop call)
|
|
116
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
117
|
+
// Drain pending timers so resetPolling's async loop completes cleanly
|
|
118
|
+
// Mock process.exit to prevent test runner from exiting
|
|
119
|
+
const mockExit = vi
|
|
120
|
+
.spyOn(process, 'exit')
|
|
121
|
+
.mockImplementation(() => undefined);
|
|
122
|
+
await vi.runAllTimersAsync();
|
|
123
|
+
mockExit.mockRestore();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('disconnect', () => {
|
|
127
|
+
it('cleans up state on disconnect', async () => {
|
|
128
|
+
await provider.connect();
|
|
129
|
+
mockStop.mockClear();
|
|
130
|
+
await provider.disconnect();
|
|
131
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(provider.isConnected()).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('basic operations', () => {
|
|
136
|
+
it('reports connected after successful connect', async () => {
|
|
137
|
+
await provider.connect();
|
|
138
|
+
expect(provider.isConnected()).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it('returns correct status', async () => {
|
|
141
|
+
await provider.connect();
|
|
142
|
+
const status = provider.getStatus();
|
|
143
|
+
expect(status.connected).toBe(true);
|
|
144
|
+
expect(status.channel).toBe('telegram');
|
|
145
|
+
expect(status.identity).toBe('test_bot');
|
|
146
|
+
});
|
|
147
|
+
it('has name "telegram"', () => {
|
|
148
|
+
expect(provider.name).toBe('telegram');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
//# sourceMappingURL=telegram.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telegram.test.js","sourceRoot":"","sources":["../src/telegram.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deus-ai/telegram-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Telegram MCP server — standalone Telegram bot integration for any MCP client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"deus-ai-telegram-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@deus-ai/channel-core": "^1.0.0",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
18
|
+
"grammy": "^1.39.3",
|
|
19
|
+
"pino": "^10.3.1",
|
|
20
|
+
"zod": "^4.3.6"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.7",
|
|
24
|
+
"typescript": "^6.0.2",
|
|
25
|
+
"vitest": "^4.1.2"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Telegram MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Standalone MCP server that provides Telegram bot messaging tools.
|
|
7
|
+
* Communicates via stdio (JSON-RPC). Can be used by any MCP client.
|
|
8
|
+
*
|
|
9
|
+
* Config (env vars):
|
|
10
|
+
* TELEGRAM_BOT_TOKEN — Telegram bot token from @BotFather
|
|
11
|
+
* ASSISTANT_NAME — bot display name (default: Deus)
|
|
12
|
+
* LOG_LEVEL — pino log level (default: info)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
|
+
import { registerCommonTools } from '@deus-ai/channel-core';
|
|
18
|
+
|
|
19
|
+
import { TelegramProvider } from './telegram.js';
|
|
20
|
+
|
|
21
|
+
const server = new McpServer({
|
|
22
|
+
name: '@deus-ai/telegram-mcp',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const provider = new TelegramProvider();
|
|
27
|
+
|
|
28
|
+
// Register common tools (send_message, get_status, etc.)
|
|
29
|
+
registerCommonTools(server, provider);
|
|
30
|
+
|
|
31
|
+
// ── Auto-connect if token is configured ───────────────────────────────
|
|
32
|
+
|
|
33
|
+
if (provider.hasToken()) {
|
|
34
|
+
provider.connect().catch((err) => {
|
|
35
|
+
console.error('[@deus-ai/telegram-mcp] Auto-connect failed:', err.message);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Start MCP transport ───────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const transport = new StdioServerTransport();
|
|
42
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// vi.hoisted runs before vi.mock hoisting — set env before module evaluation
|
|
4
|
+
vi.hoisted(() => {
|
|
5
|
+
process.env.TELEGRAM_BOT_TOKEN = 'test-token-123';
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// Shared state for the mock bot
|
|
9
|
+
let catchHandler: Function | null = null;
|
|
10
|
+
const mockStart = vi.fn((options: any) => {
|
|
11
|
+
if (options?.onStart) {
|
|
12
|
+
options.onStart({ username: 'test_bot', id: 123 });
|
|
13
|
+
}
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
});
|
|
16
|
+
const mockStop = vi.fn();
|
|
17
|
+
const mockCommand = vi.fn();
|
|
18
|
+
const mockOn = vi.fn();
|
|
19
|
+
const mockCatch = vi.fn((handler: Function) => {
|
|
20
|
+
catchHandler = handler;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vi.mock('grammy', () => {
|
|
24
|
+
return {
|
|
25
|
+
Bot: class MockBot {
|
|
26
|
+
command = mockCommand;
|
|
27
|
+
on = mockOn;
|
|
28
|
+
catch = mockCatch;
|
|
29
|
+
start = mockStart;
|
|
30
|
+
stop = mockStop;
|
|
31
|
+
api = {
|
|
32
|
+
sendMessage: vi.fn(),
|
|
33
|
+
sendChatAction: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
Api: class {},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
vi.mock('pino', () => {
|
|
41
|
+
const mockLogger = {
|
|
42
|
+
info: vi.fn(),
|
|
43
|
+
warn: vi.fn(),
|
|
44
|
+
error: vi.fn(),
|
|
45
|
+
debug: vi.fn(),
|
|
46
|
+
fatal: vi.fn(),
|
|
47
|
+
};
|
|
48
|
+
const pinoFn: any = () => mockLogger;
|
|
49
|
+
pinoFn.destination = () => ({});
|
|
50
|
+
return { default: pinoFn };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
import { TelegramProvider } from './telegram.js';
|
|
54
|
+
|
|
55
|
+
describe('TelegramProvider', () => {
|
|
56
|
+
let provider: TelegramProvider;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
catchHandler = null;
|
|
60
|
+
// Reset implementations but keep fns alive
|
|
61
|
+
mockStart.mockImplementation((options: any) => {
|
|
62
|
+
if (options?.onStart) {
|
|
63
|
+
options.onStart({ username: 'test_bot', id: 123 });
|
|
64
|
+
}
|
|
65
|
+
return Promise.resolve();
|
|
66
|
+
});
|
|
67
|
+
mockStop.mockReset();
|
|
68
|
+
mockCommand.mockReset();
|
|
69
|
+
mockOn.mockReset();
|
|
70
|
+
mockCatch.mockImplementation((handler: Function) => {
|
|
71
|
+
catchHandler = handler;
|
|
72
|
+
});
|
|
73
|
+
provider = new TelegramProvider();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('error tracking and polling reset', () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.useFakeTimers();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterEach(() => {
|
|
82
|
+
vi.useRealTimers();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('tracks consecutive errors via the catch handler', async () => {
|
|
86
|
+
await provider.connect();
|
|
87
|
+
expect(catchHandler).toBeDefined();
|
|
88
|
+
|
|
89
|
+
// Simulate errors below threshold
|
|
90
|
+
for (let i = 0; i < 3; i++) {
|
|
91
|
+
catchHandler!({ message: `error ${i}` });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Bot should not have been stopped
|
|
95
|
+
expect(mockStop).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('resets polling after MAX_CONSECUTIVE_ERRORS (5) failures', async () => {
|
|
99
|
+
await provider.connect();
|
|
100
|
+
|
|
101
|
+
// Reset call counts from connect()
|
|
102
|
+
mockStop.mockClear();
|
|
103
|
+
mockStart.mockClear();
|
|
104
|
+
|
|
105
|
+
// Simulate 5 consecutive errors (the threshold)
|
|
106
|
+
for (let i = 0; i < 5; i++) {
|
|
107
|
+
catchHandler!({ message: `error ${i}` });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Stop is called immediately in resetPolling
|
|
111
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
112
|
+
|
|
113
|
+
// Advance past the first backoff delay (1s) to trigger start
|
|
114
|
+
await vi.advanceTimersByTimeAsync(1100);
|
|
115
|
+
|
|
116
|
+
expect(mockStart).toHaveBeenCalledTimes(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('does not reset polling twice while already resetting', async () => {
|
|
120
|
+
await provider.connect();
|
|
121
|
+
|
|
122
|
+
// Make start not call onStart (simulating slow restart that times out)
|
|
123
|
+
mockStart.mockImplementationOnce(() => Promise.resolve());
|
|
124
|
+
mockStop.mockClear();
|
|
125
|
+
mockStart.mockClear();
|
|
126
|
+
|
|
127
|
+
// Trigger 5 errors to start a reset
|
|
128
|
+
for (let i = 0; i < 5; i++) {
|
|
129
|
+
catchHandler!({ message: `error ${i}` });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Now send 5 more errors while resetting
|
|
133
|
+
for (let i = 0; i < 5; i++) {
|
|
134
|
+
catchHandler!({ message: `error ${i}` });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Should only have triggered one reset (one stop call)
|
|
138
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
139
|
+
|
|
140
|
+
// Drain pending timers so resetPolling's async loop completes cleanly
|
|
141
|
+
// Mock process.exit to prevent test runner from exiting
|
|
142
|
+
const mockExit = vi
|
|
143
|
+
.spyOn(process, 'exit')
|
|
144
|
+
.mockImplementation(() => undefined as never);
|
|
145
|
+
await vi.runAllTimersAsync();
|
|
146
|
+
mockExit.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('disconnect', () => {
|
|
151
|
+
it('cleans up state on disconnect', async () => {
|
|
152
|
+
await provider.connect();
|
|
153
|
+
mockStop.mockClear();
|
|
154
|
+
|
|
155
|
+
await provider.disconnect();
|
|
156
|
+
|
|
157
|
+
expect(mockStop).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(provider.isConnected()).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('basic operations', () => {
|
|
163
|
+
it('reports connected after successful connect', async () => {
|
|
164
|
+
await provider.connect();
|
|
165
|
+
expect(provider.isConnected()).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns correct status', async () => {
|
|
169
|
+
await provider.connect();
|
|
170
|
+
const status = provider.getStatus();
|
|
171
|
+
expect(status.connected).toBe(true);
|
|
172
|
+
expect(status.channel).toBe('telegram');
|
|
173
|
+
expect(status.identity).toBe('test_bot');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('has name "telegram"', () => {
|
|
177
|
+
expect(provider.name).toBe('telegram');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/telegram.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Telegram bot provider.
|
|
3
|
+
* Extracted from Deus TelegramChannel — no Deus-specific dependencies.
|
|
4
|
+
* Config comes from env vars; all messages are forwarded to onMessage.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import https from 'https';
|
|
8
|
+
|
|
9
|
+
import { Api, Bot } from 'grammy';
|
|
10
|
+
import pino from 'pino';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ChannelProvider,
|
|
14
|
+
ChannelStatus,
|
|
15
|
+
ChatInfo,
|
|
16
|
+
IncomingMessage,
|
|
17
|
+
} from '@deus-ai/channel-core';
|
|
18
|
+
|
|
19
|
+
const ASSISTANT_NAME = process.env.ASSISTANT_NAME || 'Deus';
|
|
20
|
+
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || '';
|
|
21
|
+
|
|
22
|
+
// Use stderr for logging (stdout is reserved for MCP JSON-RPC)
|
|
23
|
+
const logger = pino(
|
|
24
|
+
{ level: process.env.LOG_LEVEL || 'info' },
|
|
25
|
+
pino.destination(2),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
29
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
30
|
+
const MAX_RECONNECT_RETRIES = 3;
|
|
31
|
+
const BASE_BACKOFF_MS = 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Send a message with Telegram Markdown parse mode, falling back to plain text.
|
|
35
|
+
*/
|
|
36
|
+
async function sendTelegramMessage(
|
|
37
|
+
api: { sendMessage: Api['sendMessage'] },
|
|
38
|
+
chatId: string | number,
|
|
39
|
+
text: string,
|
|
40
|
+
options: { message_thread_id?: number } = {},
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
try {
|
|
43
|
+
await api.sendMessage(chatId, text, { ...options, parse_mode: 'Markdown' });
|
|
44
|
+
} catch {
|
|
45
|
+
await api.sendMessage(chatId, text, options);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class TelegramProvider implements ChannelProvider {
|
|
50
|
+
readonly name = 'telegram';
|
|
51
|
+
|
|
52
|
+
private bot: Bot | null = null;
|
|
53
|
+
private connectTime = 0;
|
|
54
|
+
private knownChats = new Map<string, { name: string; isGroup: boolean }>();
|
|
55
|
+
private botUsername?: string;
|
|
56
|
+
private consecutiveErrors = 0;
|
|
57
|
+
private resetting = false;
|
|
58
|
+
|
|
59
|
+
// Set by server-base.ts
|
|
60
|
+
onMessage: (msg: IncomingMessage) => void = () => {};
|
|
61
|
+
|
|
62
|
+
async connect(): Promise<void> {
|
|
63
|
+
if (!BOT_TOKEN) {
|
|
64
|
+
throw new Error('TELEGRAM_BOT_TOKEN not set');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.bot = new Bot(BOT_TOKEN, {
|
|
68
|
+
client: {
|
|
69
|
+
baseFetchConfig: { agent: https.globalAgent, compress: true },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// /chatid command for registration
|
|
74
|
+
this.bot.command('chatid', (ctx) => {
|
|
75
|
+
const chatId = ctx.chat.id;
|
|
76
|
+
const chatType = ctx.chat.type;
|
|
77
|
+
const chatName =
|
|
78
|
+
chatType === 'private'
|
|
79
|
+
? ctx.from?.first_name || 'Private'
|
|
80
|
+
: (ctx.chat as any).title || 'Unknown';
|
|
81
|
+
ctx.reply(
|
|
82
|
+
`Chat ID: \`tg:${chatId}\`\nName: ${chatName}\nType: ${chatType}`,
|
|
83
|
+
{ parse_mode: 'Markdown' },
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
this.bot.command('ping', (ctx) => {
|
|
88
|
+
ctx.reply(`${ASSISTANT_NAME} is online.`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const BOT_COMMANDS = new Set(['chatid', 'ping']);
|
|
92
|
+
|
|
93
|
+
// Handle text messages
|
|
94
|
+
this.bot.on('message:text', async (ctx) => {
|
|
95
|
+
this.consecutiveErrors = 0;
|
|
96
|
+
if (ctx.message.text.startsWith('/')) {
|
|
97
|
+
const cmd = ctx.message.text.slice(1).split(/[\s@]/)[0].toLowerCase();
|
|
98
|
+
if (BOT_COMMANDS.has(cmd)) return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const chatJid = `tg:${ctx.chat.id}`;
|
|
102
|
+
let content = ctx.message.text;
|
|
103
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
104
|
+
const senderName =
|
|
105
|
+
ctx.from?.first_name ||
|
|
106
|
+
ctx.from?.username ||
|
|
107
|
+
ctx.from?.id.toString() ||
|
|
108
|
+
'Unknown';
|
|
109
|
+
const sender = ctx.from?.id.toString() || '';
|
|
110
|
+
const msgId = ctx.message.message_id.toString();
|
|
111
|
+
const isGroup =
|
|
112
|
+
ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
|
113
|
+
const chatName =
|
|
114
|
+
ctx.chat.type === 'private'
|
|
115
|
+
? senderName
|
|
116
|
+
: (ctx.chat as any).title || chatJid;
|
|
117
|
+
|
|
118
|
+
// Track chat
|
|
119
|
+
this.knownChats.set(chatJid, { name: chatName, isGroup });
|
|
120
|
+
|
|
121
|
+
// Translate @bot mentions to @AssistantName
|
|
122
|
+
const botUser = this.botUsername?.toLowerCase();
|
|
123
|
+
if (botUser) {
|
|
124
|
+
const entities = ctx.message.entities || [];
|
|
125
|
+
const isBotMentioned = entities.some((e) => {
|
|
126
|
+
if (e.type === 'mention') {
|
|
127
|
+
return (
|
|
128
|
+
content.substring(e.offset, e.offset + e.length).toLowerCase() ===
|
|
129
|
+
`@${botUser}`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
if (isBotMentioned) {
|
|
135
|
+
content = `@${ASSISTANT_NAME} ${content}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Reply context
|
|
140
|
+
const replyTo = ctx.message.reply_to_message;
|
|
141
|
+
|
|
142
|
+
// Forward ALL messages
|
|
143
|
+
this.onMessage({
|
|
144
|
+
id: msgId,
|
|
145
|
+
chat_id: chatJid,
|
|
146
|
+
sender,
|
|
147
|
+
sender_name: senderName,
|
|
148
|
+
content,
|
|
149
|
+
timestamp,
|
|
150
|
+
is_from_me: false,
|
|
151
|
+
is_group: isGroup,
|
|
152
|
+
chat_name: chatName,
|
|
153
|
+
metadata: {
|
|
154
|
+
thread_id: ctx.message.message_thread_id?.toString(),
|
|
155
|
+
reply_to_message_id: replyTo?.message_id?.toString(),
|
|
156
|
+
reply_to_content: replyTo?.text || replyTo?.caption,
|
|
157
|
+
reply_to_sender_name: replyTo
|
|
158
|
+
? replyTo.from?.first_name ||
|
|
159
|
+
replyTo.from?.username ||
|
|
160
|
+
replyTo.from?.id?.toString()
|
|
161
|
+
: undefined,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Handle media messages
|
|
167
|
+
const storeMedia = (ctx: any, placeholder: string) => {
|
|
168
|
+
const chatJid = `tg:${ctx.chat.id}`;
|
|
169
|
+
const timestamp = new Date(ctx.message.date * 1000).toISOString();
|
|
170
|
+
const senderName =
|
|
171
|
+
ctx.from?.first_name ||
|
|
172
|
+
ctx.from?.username ||
|
|
173
|
+
ctx.from?.id?.toString() ||
|
|
174
|
+
'Unknown';
|
|
175
|
+
const caption = ctx.message.caption ? ` ${ctx.message.caption}` : '';
|
|
176
|
+
const isGroup =
|
|
177
|
+
ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
|
|
178
|
+
|
|
179
|
+
this.knownChats.set(chatJid, {
|
|
180
|
+
name:
|
|
181
|
+
ctx.chat.type === 'private'
|
|
182
|
+
? senderName
|
|
183
|
+
: (ctx.chat as any).title || chatJid,
|
|
184
|
+
isGroup,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
this.onMessage({
|
|
188
|
+
id: ctx.message.message_id.toString(),
|
|
189
|
+
chat_id: chatJid,
|
|
190
|
+
sender: ctx.from?.id?.toString() || '',
|
|
191
|
+
sender_name: senderName,
|
|
192
|
+
content: `${placeholder}${caption}`,
|
|
193
|
+
timestamp,
|
|
194
|
+
is_from_me: false,
|
|
195
|
+
is_group: isGroup,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
this.bot.on('message:photo', (ctx) => storeMedia(ctx, '[Photo]'));
|
|
200
|
+
this.bot.on('message:video', (ctx) => storeMedia(ctx, '[Video]'));
|
|
201
|
+
this.bot.on('message:voice', (ctx) => storeMedia(ctx, '[Voice message]'));
|
|
202
|
+
this.bot.on('message:audio', (ctx) => storeMedia(ctx, '[Audio]'));
|
|
203
|
+
this.bot.on('message:document', (ctx) => {
|
|
204
|
+
const name = ctx.message.document?.file_name || 'file';
|
|
205
|
+
storeMedia(ctx, `[Document: ${name}]`);
|
|
206
|
+
});
|
|
207
|
+
this.bot.on('message:sticker', (ctx) => {
|
|
208
|
+
const emoji = ctx.message.sticker?.emoji || '';
|
|
209
|
+
storeMedia(ctx, `[Sticker ${emoji}]`);
|
|
210
|
+
});
|
|
211
|
+
this.bot.on('message:location', (ctx) => storeMedia(ctx, '[Location]'));
|
|
212
|
+
this.bot.on('message:contact', (ctx) => storeMedia(ctx, '[Contact]'));
|
|
213
|
+
|
|
214
|
+
this.bot.catch((err) => {
|
|
215
|
+
this.consecutiveErrors++;
|
|
216
|
+
logger.error(
|
|
217
|
+
{ err: err.message, consecutiveErrors: this.consecutiveErrors },
|
|
218
|
+
'Telegram bot error',
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
if (this.consecutiveErrors >= MAX_CONSECUTIVE_ERRORS && !this.resetting) {
|
|
222
|
+
this.resetPolling();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return new Promise<void>((resolve) => {
|
|
227
|
+
this.bot!.start({
|
|
228
|
+
onStart: (botInfo) => {
|
|
229
|
+
this.botUsername = botInfo.username;
|
|
230
|
+
this.connectTime = Date.now();
|
|
231
|
+
this.consecutiveErrors = 0;
|
|
232
|
+
logger.info(
|
|
233
|
+
{ username: botInfo.username, id: botInfo.id },
|
|
234
|
+
'Telegram bot connected',
|
|
235
|
+
);
|
|
236
|
+
resolve();
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Reset the polling session after consecutive errors.
|
|
244
|
+
* Retries with exponential backoff (1s, 2s, 4s), then exits on failure.
|
|
245
|
+
*/
|
|
246
|
+
private async resetPolling(): Promise<void> {
|
|
247
|
+
if (!this.bot || this.resetting) return;
|
|
248
|
+
this.resetting = true;
|
|
249
|
+
logger.warn(
|
|
250
|
+
{ consecutiveErrors: this.consecutiveErrors },
|
|
251
|
+
'Too many consecutive errors, resetting Telegram polling session',
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const bot = this.bot;
|
|
255
|
+
bot.stop();
|
|
256
|
+
this.consecutiveErrors = 0;
|
|
257
|
+
|
|
258
|
+
for (let attempt = 0; attempt < MAX_RECONNECT_RETRIES; attempt++) {
|
|
259
|
+
const delayMs = BASE_BACKOFF_MS * Math.pow(2, attempt);
|
|
260
|
+
logger.info(
|
|
261
|
+
{ attempt: attempt + 1, delayMs },
|
|
262
|
+
'Attempting polling reset with backoff',
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
await new Promise<void>((resolve, reject) => {
|
|
269
|
+
const timeout = setTimeout(() => {
|
|
270
|
+
reject(new Error('Polling restart timed out'));
|
|
271
|
+
}, 30_000);
|
|
272
|
+
|
|
273
|
+
bot.start({
|
|
274
|
+
onStart: (botInfo) => {
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
this.botUsername = botInfo.username;
|
|
277
|
+
this.connectTime = Date.now();
|
|
278
|
+
this.resetting = false;
|
|
279
|
+
this.consecutiveErrors = 0;
|
|
280
|
+
logger.info(
|
|
281
|
+
{ username: botInfo.username, id: botInfo.id },
|
|
282
|
+
'Telegram bot reconnected after polling reset',
|
|
283
|
+
);
|
|
284
|
+
resolve();
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
return; // Success — exit retry loop
|
|
289
|
+
} catch (err) {
|
|
290
|
+
logger.error(
|
|
291
|
+
{ attempt: attempt + 1, err },
|
|
292
|
+
'Polling reset attempt failed',
|
|
293
|
+
);
|
|
294
|
+
bot.stop();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
logger.fatal(
|
|
299
|
+
'Telegram bot failed to reconnect after %d retries — exiting',
|
|
300
|
+
MAX_RECONNECT_RETRIES,
|
|
301
|
+
);
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async sendMessage(chatId: string, text: string): Promise<void> {
|
|
306
|
+
if (!this.bot) return;
|
|
307
|
+
try {
|
|
308
|
+
const numericId = chatId.replace(/^tg:/, '');
|
|
309
|
+
if (text.length <= MAX_MESSAGE_LENGTH) {
|
|
310
|
+
await sendTelegramMessage(this.bot.api, numericId, text);
|
|
311
|
+
} else {
|
|
312
|
+
for (let i = 0; i < text.length; i += MAX_MESSAGE_LENGTH) {
|
|
313
|
+
await sendTelegramMessage(
|
|
314
|
+
this.bot.api,
|
|
315
|
+
numericId,
|
|
316
|
+
text.slice(i, i + MAX_MESSAGE_LENGTH),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logger.error({ chatId, err }, 'Failed to send Telegram message');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
isConnected(): boolean {
|
|
326
|
+
return this.bot !== null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
getStatus(): ChannelStatus {
|
|
330
|
+
return {
|
|
331
|
+
connected: this.bot !== null,
|
|
332
|
+
channel: 'telegram',
|
|
333
|
+
identity: this.botUsername,
|
|
334
|
+
uptime_seconds: this.connectTime
|
|
335
|
+
? Math.floor((Date.now() - this.connectTime) / 1000)
|
|
336
|
+
: 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async disconnect(): Promise<void> {
|
|
341
|
+
if (this.bot) {
|
|
342
|
+
this.resetting = false;
|
|
343
|
+
this.consecutiveErrors = 0;
|
|
344
|
+
this.bot.stop();
|
|
345
|
+
this.bot = null;
|
|
346
|
+
logger.info('Telegram bot stopped');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async setTyping(chatId: string, isTyping: boolean): Promise<void> {
|
|
351
|
+
if (!this.bot || !isTyping) return;
|
|
352
|
+
try {
|
|
353
|
+
const numericId = chatId.replace(/^tg:/, '');
|
|
354
|
+
await this.bot.api.sendChatAction(numericId, 'typing');
|
|
355
|
+
} catch {
|
|
356
|
+
// Best effort
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async listChats(): Promise<ChatInfo[]> {
|
|
361
|
+
return Array.from(this.knownChats.entries()).map(([id, info]) => ({
|
|
362
|
+
id,
|
|
363
|
+
name: info.name,
|
|
364
|
+
is_group: info.isGroup,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Check if a bot token is configured. */
|
|
369
|
+
hasToken(): boolean {
|
|
370
|
+
return !!BOT_TOKEN;
|
|
371
|
+
}
|
|
372
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|