diff --git a/package-lock.json b/package-lock.json index 45a70b18b..30de66476 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1545,6 +1545,12 @@ "resolved": "packages/test-utils", "link": true }, + "node_modules/@grammyjs/types": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.25.0.tgz", + "integrity": "sha512-iN9i5p+8ZOu9OMxWNcguojQfz4K/PDyMPOnL7PPCON+SoA/F8OKMH3uR7CVUkYfdNe0GCz8QOzAWrnqusQYFOg==", + "license": "MIT" + }, "node_modules/@grpc/grpc-js": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", @@ -3986,12 +3992,6 @@ "node": ">= 10.16.0 < 11 || >= 11.8.0 < 12 || >= 12.0.0" } }, - "node_modules/@telegraf/types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz", - "integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==", - "license": "MIT" - }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", @@ -6781,22 +6781,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "license": "MIT" - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -6812,12 +6796,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", - "license": "MIT" - }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -10418,6 +10396,21 @@ "node": ">=10" } }, + "node_modules/grammy": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.41.1.tgz", + "integrity": "sha512-wcHAQ1e7svL3fJMpDchcQVcWUmywhuepOOjHUHmMmWAwUJEIyK5ea5sbSjZd+Gy1aMpZeP8VYJa+4tP+j1YptQ==", + "license": "MIT", + "dependencies": { + "@grammyjs/types": "3.25.0", + "abort-controller": "^3.0.0", + "debug": "^4.4.3", + "node-fetch": "^2.7.0" + }, + "engines": { + "node": "^12.20.0 || >=14.13.1" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -13049,15 +13042,6 @@ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", "license": "MIT" }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -13993,15 +13977,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-timeout": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz", - "integrity": "sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/package-json": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/package-json/-/package-json-10.0.1.tgz", @@ -15728,15 +15703,6 @@ ], "license": "MIT" }, - "node_modules/safe-compare": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz", - "integrity": "sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==", - "license": "MIT", - "dependencies": { - "buffer-alloc": "^1.2.0" - } - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -15778,15 +15744,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sandwich-stream": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/sandwich-stream/-/sandwich-stream-2.0.2.tgz", - "integrity": "sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/sax": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", @@ -17070,28 +17027,6 @@ "node": ">=6" } }, - "node_modules/telegraf": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.16.3.tgz", - "integrity": "sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==", - "license": "MIT", - "dependencies": { - "@telegraf/types": "^7.1.0", - "abort-controller": "^3.0.0", - "debug": "^4.3.4", - "mri": "^1.2.0", - "node-fetch": "^2.7.0", - "p-timeout": "^4.1.0", - "safe-compare": "^1.1.4", - "sandwich-stream": "^2.0.2" - }, - "bin": { - "telegraf": "lib/cli.mjs" - }, - "engines": { - "node": "^12.20.0 || >=14.13.1" - } - }, "node_modules/telegram-markdown-formatter": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/telegram-markdown-formatter/-/telegram-markdown-formatter-0.1.2.tgz", @@ -19009,7 +18944,7 @@ "version": "0.13.0", "dependencies": { "@qwen-code/channel-base": "file:../base", - "telegraf": "^4.16.0", + "grammy": "^1.41.1", "telegram-markdown-formatter": "^0.1.2" }, "devDependencies": { diff --git a/packages/channels/telegram/package.json b/packages/channels/telegram/package.json index 07ef9580d..1d18623fa 100644 --- a/packages/channels/telegram/package.json +++ b/packages/channels/telegram/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@qwen-code/channel-base": "file:../base", - "telegraf": "^4.16.0", + "grammy": "^1.41.1", "telegram-markdown-formatter": "^0.1.2" }, "devDependencies": { diff --git a/packages/channels/telegram/src/TelegramAdapter.ts b/packages/channels/telegram/src/TelegramAdapter.ts index 141b8368a..2de28b08c 100644 --- a/packages/channels/telegram/src/TelegramAdapter.ts +++ b/packages/channels/telegram/src/TelegramAdapter.ts @@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { randomUUID } from 'node:crypto'; import { basename, join } from 'node:path'; import { tmpdir } from 'node:os'; -import { Telegraf } from 'telegraf'; +import { Bot } from 'grammy'; import { telegramFormat, splitHtmlForTelegram, @@ -16,7 +16,7 @@ import type { } from '@qwen-code/channel-base'; export class TelegramChannel extends ChannelBase { - private bot: Telegraf; + private bot: Bot; private botId: number = 0; private botUsername: string = ''; @@ -27,22 +27,26 @@ export class TelegramChannel extends ChannelBase { options?: ChannelBaseOptions, ) { super(name, config, bridge, options); - this.bot = new Telegraf(config.token); + this.bot = new Bot(config.token); + } + + private getFileUrl(filePath: string): string { + return `https://api.telegram.org/file/bot${this.bot.token}/${filePath}`; } async connect(): Promise { - const botInfo = await this.bot.telegram.getMe(); + const botInfo = await this.bot.api.getMe(); this.botId = botInfo.id; this.botUsername = botInfo.username ?? ''; // All messages (including slash commands) go through handleInbound // where ChannelBase dispatches shared commands (/help, /clear, /status, etc.) - this.bot.on('text', async (ctx) => { + this.bot.on('message:text', async (ctx) => { const msg = ctx.message; const text = msg.text; const envelope = this.buildEnvelope(msg, text, msg.entities); - // Don't await — Telegraf has a 90s handler timeout that would kill long prompts + // Don't await — long prompts would block the update loop this.handleInbound(envelope).catch((err) => { process.stderr.write( `[Telegram:${this.name}] Error handling message: ${err}\n`, @@ -54,7 +58,7 @@ export class TelegramChannel extends ChannelBase { }); // Photo messages - this.bot.on('photo', async (ctx) => { + this.bot.on('message:photo', async (ctx) => { const msg = ctx.message; const envelope = this.buildEnvelope( msg, @@ -67,8 +71,9 @@ export class TelegramChannel extends ChannelBase { if (!photo) return; try { - const fileUrl = await ctx.telegram.getFileLink(photo.file_id); - const resp = await fetch(fileUrl.href); + const file = await ctx.api.getFile(photo.file_id); + const fileUrl = this.getFileUrl(file.file_path!); + const resp = await fetch(fileUrl); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const buf = Buffer.from(await resp.arrayBuffer()); envelope.imageBase64 = buf.toString('base64'); @@ -90,7 +95,7 @@ export class TelegramChannel extends ChannelBase { }); // Document/file messages - this.bot.on('document', async (ctx) => { + this.bot.on('message:document', async (ctx) => { const msg = ctx.message; const doc = msg.document; const fileName = doc.file_name || `file_${Date.now()}`; @@ -102,8 +107,9 @@ export class TelegramChannel extends ChannelBase { ); try { - const fileUrl = await ctx.telegram.getFileLink(doc.file_id); - const resp = await fetch(fileUrl.href); + const file = await ctx.api.getFile(doc.file_id); + const fileUrl = this.getFileUrl(file.file_path!); + const resp = await fetch(fileUrl); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const buf = Buffer.from(await resp.arrayBuffer()); @@ -141,14 +147,14 @@ export class TelegramChannel extends ChannelBase { }); }); - this.bot.launch({ dropPendingUpdates: true }).catch((err) => { + this.bot.start({ drop_pending_updates: true }).catch((err) => { process.stderr.write( `[Telegram:${this.name}] Bot launch error: ${err}\n`, ); }); - process.once('SIGINT', () => this.bot.stop('SIGINT')); - process.once('SIGTERM', () => this.bot.stop('SIGTERM')); + process.once('SIGINT', () => this.bot.stop()); + process.once('SIGTERM', () => this.bot.stop()); } /** Per-chat typing interval — repeats every 4s since Telegram expires it after 5s. */ @@ -160,7 +166,7 @@ export class TelegramChannel extends ChannelBase { if (existing) clearInterval(existing); const sendTyping = () => - this.bot.telegram.sendChatAction(chatId, 'typing').catch(() => {}); + this.bot.api.sendChatAction(chatId, 'typing').catch(() => {}); sendTyping(); this.typingIntervals.set(chatId, setInterval(sendTyping, 4000)); } @@ -178,12 +184,12 @@ export class TelegramChannel extends ChannelBase { const chunks = splitHtmlForTelegram(html); for (const chunk of chunks) { try { - await this.bot.telegram.sendMessage(chatId, chunk, { + await this.bot.api.sendMessage(chatId, chunk, { parse_mode: 'HTML', }); } catch { // Fallback to plain text if HTML parsing fails - await this.bot.telegram.sendMessage(chatId, text); + await this.bot.api.sendMessage(chatId, text); return; } }