@geminixiang/mama 0.2.0-beta.0 → 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -19
- package/dist/adapter.d.ts +9 -7
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/discord/bot.d.ts +2 -2
- package/dist/adapters/discord/bot.d.ts.map +1 -1
- package/dist/adapters/discord/bot.js +33 -21
- package/dist/adapters/discord/bot.js.map +1 -1
- package/dist/adapters/discord/context.d.ts.map +1 -1
- package/dist/adapters/discord/context.js +20 -13
- package/dist/adapters/discord/context.js.map +1 -1
- package/dist/adapters/slack/bot.d.ts +4 -3
- package/dist/adapters/slack/bot.d.ts.map +1 -1
- package/dist/adapters/slack/bot.js +68 -30
- package/dist/adapters/slack/bot.js.map +1 -1
- package/dist/adapters/slack/context.d.ts.map +1 -1
- package/dist/adapters/slack/context.js +22 -12
- package/dist/adapters/slack/context.js.map +1 -1
- package/dist/adapters/telegram/bot.d.ts +2 -2
- package/dist/adapters/telegram/bot.d.ts.map +1 -1
- package/dist/adapters/telegram/bot.js +54 -33
- package/dist/adapters/telegram/bot.js.map +1 -1
- package/dist/adapters/telegram/context.d.ts.map +1 -1
- package/dist/adapters/telegram/context.js +61 -10
- package/dist/adapters/telegram/context.js.map +1 -1
- package/dist/agent.d.ts +7 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +152 -96
- package/dist/agent.js.map +1 -1
- package/dist/bindings.d.ts +63 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +94 -0
- package/dist/bindings.js.map +1 -0
- package/dist/config.d.ts +32 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +71 -44
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +6 -6
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +8 -8
- package/dist/context.js.map +1 -1
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +20 -5
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts +20 -0
- package/dist/execution-resolver.d.ts.map +1 -0
- package/dist/execution-resolver.js +51 -0
- package/dist/execution-resolver.js.map +1 -0
- package/dist/instrument.d.ts.map +1 -1
- package/dist/instrument.js +11 -4
- package/dist/instrument.js.map +1 -1
- package/dist/link-server.d.ts +16 -0
- package/dist/link-server.d.ts.map +1 -0
- package/dist/link-server.js +839 -0
- package/dist/link-server.js.map +1 -0
- package/dist/link-token.d.ts +32 -0
- package/dist/link-token.d.ts.map +1 -0
- package/dist/link-token.js +68 -0
- package/dist/link-token.js.map +1 -0
- package/dist/log.d.ts +2 -2
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +7 -7
- package/dist/log.js.map +1 -1
- package/dist/login.d.ts +29 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +164 -0
- package/dist/login.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +243 -56
- package/dist/main.js.map +1 -1
- package/dist/provisioner.d.ts +93 -0
- package/dist/provisioner.d.ts.map +1 -0
- package/dist/provisioner.js +336 -0
- package/dist/provisioner.js.map +1 -0
- package/dist/sandbox/container.d.ts +15 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +122 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/errors.d.ts +6 -0
- package/dist/sandbox/errors.d.ts.map +1 -0
- package/dist/sandbox/errors.js +11 -0
- package/dist/sandbox/errors.js.map +1 -0
- package/dist/sandbox/firecracker.d.ts +16 -0
- package/dist/sandbox/firecracker.d.ts.map +1 -0
- package/dist/sandbox/firecracker.js +206 -0
- package/dist/sandbox/firecracker.js.map +1 -0
- package/dist/sandbox/host.d.ts +12 -0
- package/dist/sandbox/host.d.ts.map +1 -0
- package/dist/sandbox/host.js +89 -0
- package/dist/sandbox/host.js.map +1 -0
- package/dist/sandbox/image.d.ts +5 -0
- package/dist/sandbox/image.d.ts.map +1 -0
- package/dist/sandbox/image.js +30 -0
- package/dist/sandbox/image.js.map +1 -0
- package/dist/sandbox/index.d.ts +20 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +51 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +51 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +2 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/sandbox/utils.d.ts +4 -0
- package/dist/sandbox/utils.d.ts.map +1 -0
- package/dist/sandbox/utils.js +51 -0
- package/dist/sandbox/utils.js.map +1 -0
- package/dist/sandbox.d.ts +1 -39
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sandbox.js +1 -286
- package/dist/sandbox.js.map +1 -1
- package/dist/sentry.d.ts +1 -1
- package/dist/sentry.d.ts.map +1 -1
- package/dist/sentry.js +2 -2
- package/dist/sentry.js.map +1 -1
- package/dist/session-store.d.ts +1 -5
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +7 -10
- package/dist/session-store.js.map +1 -1
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +8 -8
- package/dist/store.js.map +1 -1
- package/dist/tools/event.d.ts +21 -0
- package/dist/tools/event.d.ts.map +1 -0
- package/dist/tools/event.js +103 -0
- package/dist/tools/event.js.map +1 -0
- package/dist/tools/index.d.ts +6 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/ui-copy.d.ts +11 -0
- package/dist/ui-copy.d.ts.map +1 -0
- package/dist/ui-copy.js +33 -0
- package/dist/ui-copy.js.map +1 -0
- package/dist/vault-routing.d.ts +10 -0
- package/dist/vault-routing.d.ts.map +1 -0
- package/dist/vault-routing.js +58 -0
- package/dist/vault-routing.js.map +1 -0
- package/dist/vault.d.ts +106 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +389 -0
- package/dist/vault.js.map +1 -0
- package/dist/vault.test.d.ts +2 -0
- package/dist/vault.test.d.ts.map +1 -0
- package/dist/vault.test.js +67 -0
- package/dist/vault.test.js.map +1 -0
- package/package.json +12 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAQhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmDD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsB3B;IAEK,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhE;IAEK,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5E;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAcrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,kBAAkB;CA+G3B","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([\n { command: \"start\", description: \"Welcome message\" },\n { command: \"help\", description: \"Show available commands\" },\n { command: \"stop\", description: \"Stop ongoing conversation\" },\n { command: \"new\", description: \"Reset conversation history and start fresh\" },\n ]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(channel), text);\n return String(result);\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.channel);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createTelegramAdapters(event as TelegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const dir = join(this.workingDir, channel);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n appendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(fullDir)) mkdirSync(fullDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(fullDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Reset conversation history and start fresh\",\n \"/stop — Stop the current conversation\",\n \"/help — Show available commands\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Welcome message\",\n \"/help — Show this help\",\n \"/stop — Stop ongoing conversation\",\n \"/new — Reset conversation history and start fresh\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, \"Nothing running.\");\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n channel: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, \"Nothing running.\");\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, \"Already working. Say <code>/stop</code> to cancel.\");\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUhF,MAAM,WAAW,aAAc,SAAQ,QAAQ;IAC7C,IAAI,EAAE,SAAS,GAAG,SAAS,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AA2DD,qBAAa,WAAY,YAAW,GAAG;IACrC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,WAAW,CAAa;IAEhC,YAAY,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,EAQ7E;IAMK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB3B;IAEK,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGvE;IAEK,aAAa,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWnF;IAED,YAAY,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAmBrC;IAED,eAAe,IAAI,YAAY,CAQ9B;IAMK,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;IAEK,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMvF;IAEK,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvE;IAEK,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9C;IAEK,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjF;IAED,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI9C;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAS9D;IAED;;;;OAIG;IACG,kBAAkB,CACtB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,GAAG,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAyBhD;YAKa,mBAAmB;IAgDjC,OAAO,CAAC,QAAQ;IAShB,OAAO,CAAC,qBAAqB;IAuB7B,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,SAAS;IAKjB,OAAO,CAAC,kBAAkB;CA0I3B","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\nconst TELEGRAM_COMMANDS = [\n { command: \"start\", description: \"Show the welcome message\" },\n { command: \"help\", description: \"Show help\" },\n { command: \"stop\", description: \"Stop the current task\" },\n { command: \"new\", description: \"Start a new conversation\" },\n { command: \"login\", description: \"Open your secure login page\" },\n] as const;\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([...TELEGRAM_COMMANDS]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(conversationId), text);\n return String(result);\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(conversationId), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const telegramEvent: TelegramEvent = {\n ...event,\n type: \"message\",\n conversationId: event.conversationId,\n };\n const adapters = createTelegramAdapters(telegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const channelDir = join(this.workingDir, channel);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Start a new conversation\",\n \"/stop — Stop the current task\",\n \"/help — Show help\",\n \"/login — Open your secure login page\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Show the welcome message\",\n \"/help — Show help\",\n \"/stop — Stop the current task\",\n \"/new — Start a new conversation\",\n \"/login — Open your secure login page\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n this.client.command(\"login\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n mc.text,\n mc.chatType === \"private\",\n );\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n cleanedText,\n mc.chatType === \"private\",\n );\n return;\n }\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n conversationId: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, formatAlreadyWorking(\"telegram\", \"/stop\"));\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
2
|
import { basename, join } from "path";
|
|
3
3
|
import { Bot as GrammyBot, InputFile } from "grammy";
|
|
4
|
+
import { parseLoginCommand } from "../../login.js";
|
|
4
5
|
import * as log from "../../log.js";
|
|
6
|
+
import { formatAlreadyWorking, formatNothingRunning } from "../../ui-copy.js";
|
|
5
7
|
import { createTelegramAdapters } from "./context.js";
|
|
8
|
+
const TELEGRAM_COMMANDS = [
|
|
9
|
+
{ command: "start", description: "Show the welcome message" },
|
|
10
|
+
{ command: "help", description: "Show help" },
|
|
11
|
+
{ command: "stop", description: "Stop the current task" },
|
|
12
|
+
{ command: "new", description: "Start a new conversation" },
|
|
13
|
+
{ command: "login", description: "Open your secure login page" },
|
|
14
|
+
];
|
|
6
15
|
class ChannelQueue {
|
|
7
16
|
constructor() {
|
|
8
17
|
this.queue = [];
|
|
@@ -55,12 +64,7 @@ export class TelegramBot {
|
|
|
55
64
|
this.botUserId = String(me.id);
|
|
56
65
|
this.botUsername = me.username ?? null;
|
|
57
66
|
this.startupTime = Date.now();
|
|
58
|
-
await this.client.api.setMyCommands([
|
|
59
|
-
{ command: "start", description: "Welcome message" },
|
|
60
|
-
{ command: "help", description: "Show available commands" },
|
|
61
|
-
{ command: "stop", description: "Stop ongoing conversation" },
|
|
62
|
-
{ command: "new", description: "Reset conversation history and start fresh" },
|
|
63
|
-
]);
|
|
67
|
+
await this.client.api.setMyCommands([...TELEGRAM_COMMANDS]);
|
|
64
68
|
this.setupEventHandlers();
|
|
65
69
|
// Start polling in background (bot.start() runs indefinitely)
|
|
66
70
|
this.client.start().catch((err) => {
|
|
@@ -69,13 +73,13 @@ export class TelegramBot {
|
|
|
69
73
|
log.logConnected();
|
|
70
74
|
log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);
|
|
71
75
|
}
|
|
72
|
-
async postMessage(
|
|
73
|
-
const result = await this.postMessageRaw(parseInt(
|
|
76
|
+
async postMessage(conversationId, text) {
|
|
77
|
+
const result = await this.postMessageRaw(parseInt(conversationId), text);
|
|
74
78
|
return String(result);
|
|
75
79
|
}
|
|
76
|
-
async updateMessage(
|
|
80
|
+
async updateMessage(conversationId, ts, text) {
|
|
77
81
|
try {
|
|
78
|
-
await this.client.api.editMessageText(parseInt(
|
|
82
|
+
await this.client.api.editMessageText(parseInt(conversationId), parseInt(ts), text, {
|
|
79
83
|
parse_mode: "HTML",
|
|
80
84
|
});
|
|
81
85
|
}
|
|
@@ -87,14 +91,19 @@ export class TelegramBot {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
enqueueEvent(event) {
|
|
90
|
-
const queue = this.getQueue(event.
|
|
94
|
+
const queue = this.getQueue(event.conversationId);
|
|
91
95
|
if (queue.size() >= 5) {
|
|
92
|
-
log.logWarning(`Event queue full for ${event.
|
|
96
|
+
log.logWarning(`Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`);
|
|
93
97
|
return false;
|
|
94
98
|
}
|
|
95
|
-
log.logInfo(`Enqueueing event for ${event.
|
|
99
|
+
log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);
|
|
96
100
|
queue.enqueue(() => {
|
|
97
|
-
const
|
|
101
|
+
const telegramEvent = {
|
|
102
|
+
...event,
|
|
103
|
+
type: "message",
|
|
104
|
+
conversationId: event.conversationId,
|
|
105
|
+
};
|
|
106
|
+
const adapters = createTelegramAdapters(telegramEvent, this, true);
|
|
98
107
|
return this.handler.handleEvent(event, this, adapters, true);
|
|
99
108
|
});
|
|
100
109
|
return true;
|
|
@@ -136,10 +145,10 @@ export class TelegramBot {
|
|
|
136
145
|
await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));
|
|
137
146
|
}
|
|
138
147
|
logToFile(channel, entry) {
|
|
139
|
-
const
|
|
140
|
-
if (!existsSync(
|
|
141
|
-
mkdirSync(
|
|
142
|
-
appendFileSync(join(
|
|
148
|
+
const channelDir = join(this.workingDir, channel);
|
|
149
|
+
if (!existsSync(channelDir))
|
|
150
|
+
mkdirSync(channelDir, { recursive: true });
|
|
151
|
+
appendFileSync(join(channelDir, "log.jsonl"), `${JSON.stringify(entry)}\n`);
|
|
143
152
|
}
|
|
144
153
|
logBotResponse(channel, text, ts) {
|
|
145
154
|
this.logToFile(channel, {
|
|
@@ -191,9 +200,9 @@ export class TelegramBot {
|
|
|
191
200
|
const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
192
201
|
const filename = `${ts}_${sanitizedName}`;
|
|
193
202
|
const localPath = `${chatId}/attachments/${filename}`;
|
|
194
|
-
const
|
|
195
|
-
if (!existsSync(
|
|
196
|
-
mkdirSync(
|
|
203
|
+
const attachmentsDir = join(this.workingDir, chatId, "attachments");
|
|
204
|
+
if (!existsSync(attachmentsDir))
|
|
205
|
+
mkdirSync(attachmentsDir, { recursive: true });
|
|
197
206
|
// Construct download URL
|
|
198
207
|
const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;
|
|
199
208
|
// Download the file
|
|
@@ -202,7 +211,7 @@ export class TelegramBot {
|
|
|
202
211
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
203
212
|
}
|
|
204
213
|
const buffer = await response.arrayBuffer();
|
|
205
|
-
writeFileSync(join(
|
|
214
|
+
writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));
|
|
206
215
|
return {
|
|
207
216
|
name: originalName,
|
|
208
217
|
localPath: localPath,
|
|
@@ -269,9 +278,10 @@ export class TelegramBot {
|
|
|
269
278
|
"",
|
|
270
279
|
"I'm an AI coding agent. Send me a message or use these commands:",
|
|
271
280
|
"",
|
|
272
|
-
"/new —
|
|
273
|
-
"/stop — Stop the current
|
|
274
|
-
"/help — Show
|
|
281
|
+
"/new — Start a new conversation",
|
|
282
|
+
"/stop — Stop the current task",
|
|
283
|
+
"/help — Show help",
|
|
284
|
+
"/login — Open your secure login page",
|
|
275
285
|
].join("\n"));
|
|
276
286
|
});
|
|
277
287
|
this.client.command("help", async (ctx) => {
|
|
@@ -281,10 +291,11 @@ export class TelegramBot {
|
|
|
281
291
|
await this.postMessageRaw(parseInt(mc.chatId), [
|
|
282
292
|
"<b>Available commands:</b>",
|
|
283
293
|
"",
|
|
284
|
-
"/start —
|
|
285
|
-
"/help — Show
|
|
286
|
-
"/stop — Stop
|
|
287
|
-
"/new —
|
|
294
|
+
"/start — Show the welcome message",
|
|
295
|
+
"/help — Show help",
|
|
296
|
+
"/stop — Stop the current task",
|
|
297
|
+
"/new — Start a new conversation",
|
|
298
|
+
"/login — Open your secure login page",
|
|
288
299
|
"",
|
|
289
300
|
"You can also send a regular message to chat with the agent.",
|
|
290
301
|
].join("\n"));
|
|
@@ -297,7 +308,7 @@ export class TelegramBot {
|
|
|
297
308
|
await this.handler.handleStop(mc.sessionKey, mc.chatId, this);
|
|
298
309
|
}
|
|
299
310
|
else {
|
|
300
|
-
await this.postMessage(mc.chatId, "
|
|
311
|
+
await this.postMessage(mc.chatId, formatNothingRunning("telegram"));
|
|
301
312
|
}
|
|
302
313
|
});
|
|
303
314
|
this.client.command("new", async (ctx) => {
|
|
@@ -306,6 +317,12 @@ export class TelegramBot {
|
|
|
306
317
|
return;
|
|
307
318
|
await this.handler.handleNew(mc.sessionKey, mc.chatId, this);
|
|
308
319
|
});
|
|
320
|
+
this.client.command("login", async (ctx) => {
|
|
321
|
+
const mc = this.extractMessageContext(ctx.message);
|
|
322
|
+
if (!mc)
|
|
323
|
+
return;
|
|
324
|
+
await this.handler.handleLogin("telegram", mc.userId, mc.chatId, this, mc.text, mc.chatType === "private");
|
|
325
|
+
});
|
|
309
326
|
// --- Catch-all for regular (non-command) messages ---
|
|
310
327
|
this.client.on("message", async (ctx) => {
|
|
311
328
|
const mc = this.extractMessageContext(ctx.message);
|
|
@@ -315,11 +332,15 @@ export class TelegramBot {
|
|
|
315
332
|
if (!this.isAddressedToBot(mc.text, mc.chatType))
|
|
316
333
|
return;
|
|
317
334
|
const cleanedText = this.cleanText(mc.text);
|
|
335
|
+
if (parseLoginCommand(cleanedText)) {
|
|
336
|
+
await this.handler.handleLogin("telegram", mc.userId, mc.chatId, this, cleanedText, mc.chatType === "private");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
318
339
|
// Process attachments
|
|
319
340
|
const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);
|
|
320
341
|
const event = {
|
|
321
342
|
type: "message",
|
|
322
|
-
|
|
343
|
+
conversationId: mc.chatId,
|
|
323
344
|
ts: mc.msgId,
|
|
324
345
|
thread_ts: mc.threadTs,
|
|
325
346
|
sessionKey: mc.sessionKey,
|
|
@@ -344,12 +365,12 @@ export class TelegramBot {
|
|
|
344
365
|
await this.handler.handleStop(mc.sessionKey, mc.chatId, this);
|
|
345
366
|
}
|
|
346
367
|
else {
|
|
347
|
-
await this.postMessage(mc.chatId, "
|
|
368
|
+
await this.postMessage(mc.chatId, formatNothingRunning("telegram"));
|
|
348
369
|
}
|
|
349
370
|
return;
|
|
350
371
|
}
|
|
351
372
|
if (this.handler.isRunning(mc.sessionKey)) {
|
|
352
|
-
await this.postMessage(mc.chatId, "
|
|
373
|
+
await this.postMessage(mc.chatId, formatAlreadyWorking("telegram", "/stop"));
|
|
353
374
|
}
|
|
354
375
|
else {
|
|
355
376
|
this.getQueue(mc.sessionKey).enqueue(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([\n { command: \"start\", description: \"Welcome message\" },\n { command: \"help\", description: \"Show available commands\" },\n { command: \"stop\", description: \"Stop ongoing conversation\" },\n { command: \"new\", description: \"Reset conversation history and start fresh\" },\n ]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(channel: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(channel), text);\n return String(result);\n }\n\n async updateMessage(channel: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(channel), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.channel);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.channel}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.channel}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const adapters = createTelegramAdapters(event as TelegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const dir = join(this.workingDir, channel);\n if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n appendFileSync(join(dir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const fullDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(fullDir)) mkdirSync(fullDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(fullDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Reset conversation history and start fresh\",\n \"/stop — Stop the current conversation\",\n \"/help — Show available commands\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Welcome message\",\n \"/help — Show this help\",\n \"/stop — Stop ongoing conversation\",\n \"/new — Reset conversation history and start fresh\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, \"Nothing running.\");\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n channel: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, \"Nothing running.\");\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, \"Already working. Say <code>/stop</code> to cancel.\");\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"bot.js","sourceRoot":"","sources":["../../../src/adapters/telegram/bot.ts"],"names":[],"mappings":"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","sourcesContent":["import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { basename, join } from \"path\";\nimport { Bot as GrammyBot, InputFile } from \"grammy\";\nimport type { Bot, BotEvent, BotHandler, PlatformInfo } from \"../../adapter.js\";\nimport { parseLoginCommand } from \"../../login.js\";\nimport * as log from \"../../log.js\";\nimport { formatAlreadyWorking, formatNothingRunning } from \"../../ui-copy.js\";\nimport { createTelegramAdapters } from \"./context.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface TelegramEvent extends BotEvent {\n type: \"message\" | \"command\";\n userName?: string;\n}\n\ninterface MessageContext {\n msg: any;\n text: string;\n chatId: string;\n chatType: string;\n userId: string;\n userName: string;\n msgId: string;\n threadTs: string | undefined;\n sessionKey: string;\n}\n\nconst TELEGRAM_COMMANDS = [\n { command: \"start\", description: \"Show the welcome message\" },\n { command: \"help\", description: \"Show help\" },\n { command: \"stop\", description: \"Stop the current task\" },\n { command: \"new\", description: \"Start a new conversation\" },\n { command: \"login\", description: \"Open your secure login page\" },\n] as const;\n\n// ============================================================================\n// Per-channel queue for sequential processing\n// ============================================================================\n\ntype QueuedWork = () => Promise<void>;\n\nclass ChannelQueue {\n private queue: QueuedWork[] = [];\n private processing = false;\n\n enqueue(work: QueuedWork): void {\n this.queue.push(work);\n this.processNext();\n }\n\n size(): number {\n return this.queue.length;\n }\n\n private async processNext(): Promise<void> {\n if (this.processing || this.queue.length === 0) return;\n this.processing = true;\n const work = this.queue.shift()!;\n try {\n await work();\n } catch (err) {\n log.logWarning(\"Telegram queue error\", err instanceof Error ? err.message : String(err));\n }\n this.processing = false;\n this.processNext();\n }\n}\n\n// ============================================================================\n// TelegramBot\n// ============================================================================\n\nexport class TelegramBot implements Bot {\n private client: GrammyBot;\n private handler: BotHandler;\n private botToken: string;\n private workingDir: string;\n private botUserId: string | null = null;\n private botUsername: string | null = null;\n private queues = new Map<string, ChannelQueue>();\n private startupTime: number = 0;\n\n constructor(handler: BotHandler, config: { token: string; workingDir: string }) {\n this.handler = handler;\n this.botToken = config.token;\n this.workingDir = config.workingDir;\n this.client = new GrammyBot(config.token);\n this.client.catch((err) => {\n log.logWarning(\"Telegram error\", err instanceof Error ? err.message : String(err));\n });\n }\n\n // ==========================================================================\n // Public API (implements Bot)\n // ==========================================================================\n\n async start(): Promise<void> {\n const me = await this.client.api.getMe();\n this.botUserId = String(me.id);\n this.botUsername = me.username ?? null;\n this.startupTime = Date.now();\n\n await this.client.api.setMyCommands([...TELEGRAM_COMMANDS]);\n\n this.setupEventHandlers();\n\n // Start polling in background (bot.start() runs indefinitely)\n this.client.start().catch((err) => {\n log.logWarning(\"Telegram polling error\", err instanceof Error ? err.message : String(err));\n });\n\n log.logConnected();\n log.logInfo(`Telegram bot started as @${this.botUsername ?? this.botUserId}`);\n }\n\n async postMessage(conversationId: string, text: string): Promise<string> {\n const result = await this.postMessageRaw(parseInt(conversationId), text);\n return String(result);\n }\n\n async updateMessage(conversationId: string, ts: string, text: string): Promise<void> {\n try {\n await this.client.api.editMessageText(parseInt(conversationId), parseInt(ts), text, {\n parse_mode: \"HTML\",\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes(\"message is not modified\")) {\n throw err;\n }\n }\n }\n\n enqueueEvent(event: BotEvent): boolean {\n const queue = this.getQueue(event.conversationId);\n if (queue.size() >= 5) {\n log.logWarning(\n `Event queue full for ${event.conversationId}, discarding: ${event.text.substring(0, 50)}`,\n );\n return false;\n }\n log.logInfo(`Enqueueing event for ${event.conversationId}: ${event.text.substring(0, 50)}`);\n queue.enqueue(() => {\n const telegramEvent: TelegramEvent = {\n ...event,\n type: \"message\",\n conversationId: event.conversationId,\n };\n const adapters = createTelegramAdapters(telegramEvent, this, true);\n return this.handler.handleEvent(event, this, adapters, true);\n });\n return true;\n }\n\n getPlatformInfo(): PlatformInfo {\n return {\n name: \"telegram\",\n formattingGuide:\n '## Telegram Formatting (HTML mode)\\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\\nLinks: <a href=\"url\">text</a>',\n channels: [],\n users: [],\n };\n }\n\n // ==========================================================================\n // Internal helpers (used by context.ts)\n // ==========================================================================\n\n async postMessageRaw(chatId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, { parse_mode: \"HTML\" });\n return result.message_id;\n }\n\n async postPlainMessage(chatId: number, text: string): Promise<void> {\n await this.client.api.sendMessage(chatId, text);\n }\n\n async postReply(chatId: number, replyToMessageId: number, text: string): Promise<number> {\n const result = await this.client.api.sendMessage(chatId, text, {\n parse_mode: \"HTML\",\n reply_parameters: { message_id: replyToMessageId },\n });\n return result.message_id;\n }\n\n async deleteMessageRaw(chatId: number, messageId: number): Promise<void> {\n await this.client.api.deleteMessage(chatId, messageId);\n }\n\n async sendTyping(chatId: number): Promise<void> {\n await this.client.api.sendChatAction(chatId, \"typing\");\n }\n\n async uploadFile(channel: string, filePath: string, title?: string): Promise<void> {\n const fileName = title ?? basename(filePath);\n const fileContent = readFileSync(filePath);\n await this.client.api.sendDocument(parseInt(channel), new InputFile(fileContent, fileName));\n }\n\n logToFile(channel: string, entry: object): void {\n const channelDir = join(this.workingDir, channel);\n if (!existsSync(channelDir)) mkdirSync(channelDir, { recursive: true });\n appendFileSync(join(channelDir, \"log.jsonl\"), `${JSON.stringify(entry)}\\n`);\n }\n\n logBotResponse(channel: string, text: string, ts: string): void {\n this.logToFile(channel, {\n date: new Date().toISOString(),\n ts,\n user: \"bot\",\n text,\n attachments: [],\n isBot: true,\n });\n }\n\n /**\n * Process attachments from a Telegram message\n * Downloads files before returning metadata so the agent can read them immediately\n * Returns format compatible with ChatMessage: { name: string, localPath: string }[]\n */\n async processAttachments(\n chatId: string,\n message: any,\n ): Promise<{ name: string; localPath: string }[]> {\n const downloads: Array<Promise<{ name: string; localPath: string } | null>> = [];\n\n // Handle photos (take the largest size for best quality)\n if (message.photo && message.photo.length > 0) {\n const photos = message.photo;\n const photo = photos[photos.length - 1]; // Largest photo\n const fileId = photo.file_id;\n\n downloads.push(this.processTelegramFile(chatId, fileId, `photo_${message.message_id}.jpg`));\n }\n\n // Handle documents\n if (message.document) {\n const doc = message.document;\n const fileId = doc.file_id;\n const fileName = doc.file_name ?? `document_${message.message_id}`;\n\n downloads.push(this.processTelegramFile(chatId, fileId, fileName));\n }\n\n const attachments = await Promise.all(downloads);\n return attachments.filter(\n (attachment): attachment is { name: string; localPath: string } => attachment !== null,\n );\n }\n\n /**\n * Download a file from Telegram and return attachment metadata\n */\n private async processTelegramFile(\n chatId: string,\n fileId: string,\n originalName: string,\n ): Promise<{ name: string; localPath: string } | null> {\n try {\n // Get file info from Telegram\n const file = await this.client.api.getFile(fileId);\n if (!file.file_path) {\n log.logWarning(\"Telegram file has no path\", fileId);\n return null;\n }\n\n // Generate local filename\n const ts = Date.now();\n const sanitizedName = originalName.replace(/[^a-zA-Z0-9._-]/g, \"_\");\n const filename = `${ts}_${sanitizedName}`;\n const localPath = `${chatId}/attachments/${filename}`;\n const attachmentsDir = join(this.workingDir, chatId, \"attachments\");\n\n if (!existsSync(attachmentsDir)) mkdirSync(attachmentsDir, { recursive: true });\n\n // Construct download URL\n const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;\n\n // Download the file\n const response = await fetch(downloadUrl);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n const buffer = await response.arrayBuffer();\n writeFileSync(join(attachmentsDir, filename), Buffer.from(buffer));\n\n return {\n name: originalName,\n localPath: localPath,\n };\n } catch (err) {\n log.logWarning(`Failed to process Telegram file`, `${originalName}: ${err}`);\n return null;\n }\n }\n\n // ==========================================================================\n // Private - Event Handlers\n // ==========================================================================\n\n private getQueue(channelId: string): ChannelQueue {\n let queue = this.queues.get(channelId);\n if (!queue) {\n queue = new ChannelQueue();\n this.queues.set(channelId, queue);\n }\n return queue;\n }\n\n private extractMessageContext(msg: any): MessageContext | null {\n if (!msg) return null;\n if (msg.date * 1000 < this.startupTime) return null;\n if (msg.from?.is_bot) return null;\n\n const text = msg.text ?? msg.caption ?? \"\";\n if (!text && !msg.document && !msg.photo) return null;\n\n const chatId = String(msg.chat.id);\n const chatType = msg.chat.type;\n const userId = String(msg.from?.id ?? \"unknown\");\n const userName = msg.from?.username ?? msg.from?.first_name ?? userId;\n const msgId = String(msg.message_id);\n const replyToId = msg.reply_to_message?.message_id;\n const threadTs = replyToId ? String(replyToId) : undefined;\n\n // Private chats: single session per chat (no per-message splitting)\n // Groups: per-thread sessions (use reply chain or unique message id)\n const sessionKey = chatType === \"private\" ? chatId : `${chatId}:${threadTs ?? msgId}`;\n\n return { msg, text, chatId, chatType, userId, userName, msgId, threadTs, sessionKey };\n }\n\n private isAddressedToBot(text: string, chatType: string): boolean {\n if (chatType === \"private\") return true;\n if (!this.botUsername) return false;\n return text.toLowerCase().includes(`@${this.botUsername.toLowerCase()}`);\n }\n\n private cleanText(text: string): string {\n if (!this.botUsername) return text.trim();\n return text.replace(new RegExp(`@${this.botUsername}`, \"gi\"), \"\").trim();\n }\n\n private setupEventHandlers(): void {\n // --- Slash commands (registered before catch-all so grammY intercepts them) ---\n\n this.client.command(\"start\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Welcome!</b>\",\n \"\",\n \"I'm an AI coding agent. Send me a message or use these commands:\",\n \"\",\n \"/new — Start a new conversation\",\n \"/stop — Stop the current task\",\n \"/help — Show help\",\n \"/login — Open your secure login page\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"help\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.postMessageRaw(\n parseInt(mc.chatId),\n [\n \"<b>Available commands:</b>\",\n \"\",\n \"/start — Show the welcome message\",\n \"/help — Show help\",\n \"/stop — Stop the current task\",\n \"/new — Start a new conversation\",\n \"/login — Open your secure login page\",\n \"\",\n \"You can also send a regular message to chat with the agent.\",\n ].join(\"\\n\"),\n );\n });\n\n this.client.command(\"stop\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n });\n\n this.client.command(\"new\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleNew(mc.sessionKey, mc.chatId, this);\n });\n\n this.client.command(\"login\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n mc.text,\n mc.chatType === \"private\",\n );\n });\n\n // --- Catch-all for regular (non-command) messages ---\n\n this.client.on(\"message\", async (ctx) => {\n const mc = this.extractMessageContext(ctx.message);\n if (!mc) return;\n\n // In groups, only respond when addressed to bot\n if (!this.isAddressedToBot(mc.text, mc.chatType)) return;\n\n const cleanedText = this.cleanText(mc.text);\n\n if (parseLoginCommand(cleanedText)) {\n await this.handler.handleLogin(\n \"telegram\",\n mc.userId,\n mc.chatId,\n this,\n cleanedText,\n mc.chatType === \"private\",\n );\n return;\n }\n\n // Process attachments\n const processedAttachments = await this.processAttachments(mc.chatId, mc.msg);\n\n const event: TelegramEvent = {\n type: \"message\",\n conversationId: mc.chatId,\n ts: mc.msgId,\n thread_ts: mc.threadTs,\n sessionKey: mc.sessionKey,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n };\n\n // Log the message\n this.logToFile(mc.chatId, {\n date: new Date(mc.msg.date * 1000).toISOString(),\n ts: mc.msgId,\n user: mc.userId,\n userName: mc.userName,\n text: cleanedText,\n attachments: processedAttachments,\n isBot: false,\n });\n\n // Handle bare \"stop\" text (backward compat)\n if (cleanedText.toLowerCase() === \"stop\") {\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.handler.handleStop(mc.sessionKey, mc.chatId, this);\n } else {\n await this.postMessage(mc.chatId, formatNothingRunning(\"telegram\"));\n }\n return;\n }\n\n if (this.handler.isRunning(mc.sessionKey)) {\n await this.postMessage(mc.chatId, formatAlreadyWorking(\"telegram\", \"/stop\"));\n } else {\n this.getQueue(mc.sessionKey).enqueue(() => {\n const adapters = createTelegramAdapters(event, this, false);\n return this.handler.handleEvent(event, this, adapters, false);\n });\n }\n });\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE3D,eAAO,MAAM,yBAAyB,qTAIuD,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/adapters/telegram/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEvF,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE3D,eAAO,MAAM,yBAAyB,qTAIuD,CAAC;AAwH9F,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,WAAW,EAChB,QAAQ,CAAC,EAAE,OAAO,GACjB;IACD,OAAO,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,mBAAmB,CAAC;IACjC,QAAQ,EAAE,YAAY,CAAC;CACxB,CA2HA","sourcesContent":["import type { ChatMessage, ChatResponseContext, PlatformInfo } from \"../../adapter.js\";\nimport * as log from \"../../log.js\";\nimport type { TelegramBot, TelegramEvent } from \"./bot.js\";\n\nexport const TELEGRAM_FORMATTING_GUIDE = `## Telegram Formatting (HTML mode)\nBold: <b>text</b>, Italic: <i>text</i>, Code: <code>code</code>, Pre: <pre>code</pre>\nLinks: <a href=\"url\">text</a>\nDo NOT use Markdown asterisks or backtick syntax.\nDo NOT use <table> tags — they are unsupported. Use <pre> with ASCII art for tables instead.`;\n\nfunction htmlTableToText(tableHtml: string): string {\n const rows: string[][] = [];\n for (const rowMatch of tableHtml.matchAll(/<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi)) {\n const cells: string[] = [];\n for (const cellMatch of rowMatch[1].matchAll(/<t[dh][^>]*>([\\s\\S]*?)<\\/t[dh]>/gi)) {\n cells.push(cellMatch[1].replace(/<[^>]+>/g, \"\").trim());\n }\n if (cells.length > 0) rows.push(cells);\n }\n\n if (rows.length === 0) return \"\";\n\n const numCols = Math.max(...rows.map((r) => r.length));\n const colWidths: number[] = Array(numCols).fill(0);\n for (const row of rows) {\n for (let i = 0; i < row.length; i++) {\n colWidths[i] = Math.max(colWidths[i], (row[i] ?? \"\").length);\n }\n }\n\n const sep = \"+\" + colWidths.map((w) => \"-\".repeat(w + 2)).join(\"+\") + \"+\";\n const lines: string[] = [sep];\n for (let i = 0; i < rows.length; i++) {\n const cells = colWidths.map((w, j) => ` ${(rows[i][j] ?? \"\").padEnd(w)} `);\n lines.push(\"|\" + cells.join(\"|\") + \"|\");\n if (i === 0) lines.push(sep);\n }\n lines.push(sep);\n return lines.join(\"\\n\");\n}\n\nconst ALLOWED_TELEGRAM_TAGS = new Set([\n \"b\",\n \"strong\",\n \"i\",\n \"em\",\n \"u\",\n \"ins\",\n \"s\",\n \"strike\",\n \"del\",\n \"code\",\n \"pre\",\n \"a\",\n]);\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&(?!(?:[a-z]+|#\\d+|#x[0-9a-f]+);)/gi, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value).replace(/\"/g, \""\");\n}\n\nfunction normalizeAllowedTelegramTag(tag: string): string | null {\n const closing = tag.match(/^<\\s*\\/\\s*([a-z0-9-]+)\\s*>$/i);\n if (closing) {\n const name = closing[1].toLowerCase();\n return ALLOWED_TELEGRAM_TAGS.has(name) ? `</${name}>` : null;\n }\n\n const simple = tag.match(/^<\\s*([a-z0-9-]+)\\s*>$/i);\n if (simple) {\n const name = simple[1].toLowerCase();\n return ALLOWED_TELEGRAM_TAGS.has(name) && name !== \"a\" ? `<${name}>` : null;\n }\n\n const anchor = tag.match(/^<\\s*a\\s+href\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')\\s*>$/i);\n if (anchor) {\n const href = anchor[1] ?? anchor[2] ?? \"\";\n return `<a href=\"${escapeAttr(href)}\">`;\n }\n\n return null;\n}\n\nfunction sanitizeTelegramHtml(text: string): string {\n const withTablesNormalized = text.replace(/<table[\\s\\S]*?<\\/table>/gi, (tableHtml) => {\n const ascii = htmlTableToText(tableHtml);\n return ascii ? `<pre>${escapeHtml(ascii)}</pre>` : \"\";\n });\n\n const placeholders: string[] = [];\n const tokenized = withTablesNormalized.replace(/<\\/?[a-z][^>]*>/gi, (tag) => {\n const normalized = normalizeAllowedTelegramTag(tag);\n if (!normalized) {\n return tag;\n }\n const token = `__TG_HTML_${placeholders.length}__`;\n placeholders.push(normalized);\n return token;\n });\n\n const escaped = escapeHtml(tokenized);\n return escaped.replace(\n /__TG_HTML_(\\d+)__/g,\n (_match, index) => placeholders[Number(index)] ?? \"\",\n );\n}\n\nasync function notifyError(\n bot: TelegramBot,\n chatId: number,\n label: string,\n err: unknown,\n): Promise<void> {\n const errMsg = err instanceof Error ? err.message : String(err);\n log.logWarning(`Telegram ${label} error`, errMsg);\n try {\n await bot.postPlainMessage(chatId, `Delivery failed: ${errMsg}`);\n } catch {\n // ignore secondary failure\n }\n}\n\nexport function createTelegramAdapters(\n event: TelegramEvent,\n bot: TelegramBot,\n _isEvent?: boolean,\n): {\n message: ChatMessage;\n responseCtx: ChatResponseContext;\n platform: PlatformInfo;\n} {\n let messageId: number | null = null;\n let accumulatedText = \"\";\n let updatePromise = Promise.resolve();\n let typingInterval: ReturnType<typeof setInterval> | null = null;\n\n function stopTyping() {\n if (typingInterval !== null) {\n clearInterval(typingInterval);\n typingInterval = null;\n }\n }\n\n const chatId = parseInt(event.conversationId);\n const replyToId = event.thread_ts ? parseInt(event.thread_ts) : null;\n\n const message: ChatMessage = {\n id: event.ts,\n sessionKey: event.sessionKey ?? `${event.conversationId}:${event.thread_ts ?? event.ts}`,\n userId: event.user,\n userName: event.userName,\n text: event.text,\n attachments: event.attachments,\n threadTs: event.thread_ts,\n };\n\n const platform: PlatformInfo = {\n name: \"telegram\",\n formattingGuide: TELEGRAM_FORMATTING_GUIDE,\n channels: [],\n users: [],\n };\n\n // Telegram message length limit is 4096 chars; use 3800 for safety\n const MAX_LENGTH = 3800;\n const truncationNote = \"\\n\\n<i>(message truncated, ask me to elaborate on specific parts)</i>\";\n\n function truncate(text: string, limit: number, note: string): string {\n if (text.length > limit) {\n return text.substring(0, limit - note.length) + note;\n }\n return text;\n }\n\n async function sendOrUpdate(displayText: string): Promise<void> {\n if (messageId !== null) {\n await bot.updateMessage(event.conversationId, String(messageId), displayText);\n } else if (replyToId !== null) {\n messageId = await bot.postReply(chatId, replyToId, displayText);\n } else {\n messageId = await bot.postMessageRaw(chatId, displayText);\n }\n }\n\n const responseCtx: ChatResponseContext = {\n respond: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n const sanitized = sanitizeTelegramHtml(text);\n accumulatedText = accumulatedText ? `${accumulatedText}\\n${sanitized}` : sanitized;\n const displayText = truncate(accumulatedText, MAX_LENGTH, truncationNote);\n await sendOrUpdate(displayText);\n if (messageId !== null) {\n bot.logBotResponse(event.conversationId, text, String(messageId));\n }\n } catch (err) {\n await notifyError(bot, chatId, \"respond\", err);\n }\n });\n await updatePromise;\n },\n\n replaceResponse: async (text: string) => {\n updatePromise = updatePromise.then(async () => {\n try {\n accumulatedText = truncate(sanitizeTelegramHtml(text), MAX_LENGTH, truncationNote);\n await sendOrUpdate(accumulatedText);\n } catch (err) {\n await notifyError(bot, chatId, \"replaceResponse\", err);\n }\n });\n await updatePromise;\n },\n\n // Telegram has no threads — discard thread-only messages (e.g. usage summary)\n respondInThread: async (_text: string) => {},\n\n setTyping: async (isTyping: boolean) => {\n if (isTyping && typingInterval === null) {\n // Send immediately and repeat every 4s (Telegram clears indicator after ~5s)\n bot.sendTyping(chatId).catch(() => {});\n typingInterval = setInterval(() => {\n bot.sendTyping(chatId).catch(() => {});\n }, 4000);\n } else if (!isTyping) {\n stopTyping();\n }\n },\n\n setWorking: async (working: boolean) => {\n if (!working) stopTyping();\n },\n\n uploadFile: async (filePath: string, title?: string) => {\n await bot.uploadFile(event.conversationId, filePath, title);\n },\n\n deleteResponse: async () => {\n updatePromise = updatePromise.then(async () => {\n if (messageId !== null) {\n try {\n await bot.deleteMessageRaw(chatId, messageId);\n } catch {\n // Ignore errors\n }\n messageId = null;\n }\n });\n await updatePromise;\n },\n };\n\n return { message, responseCtx, platform };\n}\n"]}
|
|
@@ -34,19 +34,70 @@ function htmlTableToText(tableHtml) {
|
|
|
34
34
|
lines.push(sep);
|
|
35
35
|
return lines.join("\n");
|
|
36
36
|
}
|
|
37
|
+
const ALLOWED_TELEGRAM_TAGS = new Set([
|
|
38
|
+
"b",
|
|
39
|
+
"strong",
|
|
40
|
+
"i",
|
|
41
|
+
"em",
|
|
42
|
+
"u",
|
|
43
|
+
"ins",
|
|
44
|
+
"s",
|
|
45
|
+
"strike",
|
|
46
|
+
"del",
|
|
47
|
+
"code",
|
|
48
|
+
"pre",
|
|
49
|
+
"a",
|
|
50
|
+
]);
|
|
51
|
+
function escapeHtml(text) {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/&(?!(?:[a-z]+|#\d+|#x[0-9a-f]+);)/gi, "&")
|
|
54
|
+
.replace(/</g, "<")
|
|
55
|
+
.replace(/>/g, ">");
|
|
56
|
+
}
|
|
57
|
+
function escapeAttr(value) {
|
|
58
|
+
return escapeHtml(value).replace(/"/g, """);
|
|
59
|
+
}
|
|
60
|
+
function normalizeAllowedTelegramTag(tag) {
|
|
61
|
+
const closing = tag.match(/^<\s*\/\s*([a-z0-9-]+)\s*>$/i);
|
|
62
|
+
if (closing) {
|
|
63
|
+
const name = closing[1].toLowerCase();
|
|
64
|
+
return ALLOWED_TELEGRAM_TAGS.has(name) ? `</${name}>` : null;
|
|
65
|
+
}
|
|
66
|
+
const simple = tag.match(/^<\s*([a-z0-9-]+)\s*>$/i);
|
|
67
|
+
if (simple) {
|
|
68
|
+
const name = simple[1].toLowerCase();
|
|
69
|
+
return ALLOWED_TELEGRAM_TAGS.has(name) && name !== "a" ? `<${name}>` : null;
|
|
70
|
+
}
|
|
71
|
+
const anchor = tag.match(/^<\s*a\s+href\s*=\s*(?:"([^"]*)"|'([^']*)')\s*>$/i);
|
|
72
|
+
if (anchor) {
|
|
73
|
+
const href = anchor[1] ?? anchor[2] ?? "";
|
|
74
|
+
return `<a href="${escapeAttr(href)}">`;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
37
78
|
function sanitizeTelegramHtml(text) {
|
|
38
|
-
|
|
39
|
-
return text;
|
|
40
|
-
return text.replace(/<table[\s\S]*?<\/table>/gi, (tableHtml) => {
|
|
79
|
+
const withTablesNormalized = text.replace(/<table[\s\S]*?<\/table>/gi, (tableHtml) => {
|
|
41
80
|
const ascii = htmlTableToText(tableHtml);
|
|
42
|
-
return ascii ? `<pre>${ascii}</pre>` : "";
|
|
81
|
+
return ascii ? `<pre>${escapeHtml(ascii)}</pre>` : "";
|
|
82
|
+
});
|
|
83
|
+
const placeholders = [];
|
|
84
|
+
const tokenized = withTablesNormalized.replace(/<\/?[a-z][^>]*>/gi, (tag) => {
|
|
85
|
+
const normalized = normalizeAllowedTelegramTag(tag);
|
|
86
|
+
if (!normalized) {
|
|
87
|
+
return tag;
|
|
88
|
+
}
|
|
89
|
+
const token = `__TG_HTML_${placeholders.length}__`;
|
|
90
|
+
placeholders.push(normalized);
|
|
91
|
+
return token;
|
|
43
92
|
});
|
|
93
|
+
const escaped = escapeHtml(tokenized);
|
|
94
|
+
return escaped.replace(/__TG_HTML_(\d+)__/g, (_match, index) => placeholders[Number(index)] ?? "");
|
|
44
95
|
}
|
|
45
96
|
async function notifyError(bot, chatId, label, err) {
|
|
46
97
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
47
98
|
log.logWarning(`Telegram ${label} error`, errMsg);
|
|
48
99
|
try {
|
|
49
|
-
await bot.postPlainMessage(chatId,
|
|
100
|
+
await bot.postPlainMessage(chatId, `Delivery failed: ${errMsg}`);
|
|
50
101
|
}
|
|
51
102
|
catch {
|
|
52
103
|
// ignore secondary failure
|
|
@@ -63,11 +114,11 @@ export function createTelegramAdapters(event, bot, _isEvent) {
|
|
|
63
114
|
typingInterval = null;
|
|
64
115
|
}
|
|
65
116
|
}
|
|
66
|
-
const chatId = parseInt(event.
|
|
117
|
+
const chatId = parseInt(event.conversationId);
|
|
67
118
|
const replyToId = event.thread_ts ? parseInt(event.thread_ts) : null;
|
|
68
119
|
const message = {
|
|
69
120
|
id: event.ts,
|
|
70
|
-
sessionKey: event.sessionKey ?? `${event.
|
|
121
|
+
sessionKey: event.sessionKey ?? `${event.conversationId}:${event.thread_ts ?? event.ts}`,
|
|
71
122
|
userId: event.user,
|
|
72
123
|
userName: event.userName,
|
|
73
124
|
text: event.text,
|
|
@@ -91,7 +142,7 @@ export function createTelegramAdapters(event, bot, _isEvent) {
|
|
|
91
142
|
}
|
|
92
143
|
async function sendOrUpdate(displayText) {
|
|
93
144
|
if (messageId !== null) {
|
|
94
|
-
await bot.updateMessage(event.
|
|
145
|
+
await bot.updateMessage(event.conversationId, String(messageId), displayText);
|
|
95
146
|
}
|
|
96
147
|
else if (replyToId !== null) {
|
|
97
148
|
messageId = await bot.postReply(chatId, replyToId, displayText);
|
|
@@ -109,7 +160,7 @@ export function createTelegramAdapters(event, bot, _isEvent) {
|
|
|
109
160
|
const displayText = truncate(accumulatedText, MAX_LENGTH, truncationNote);
|
|
110
161
|
await sendOrUpdate(displayText);
|
|
111
162
|
if (messageId !== null) {
|
|
112
|
-
bot.logBotResponse(event.
|
|
163
|
+
bot.logBotResponse(event.conversationId, text, String(messageId));
|
|
113
164
|
}
|
|
114
165
|
}
|
|
115
166
|
catch (err) {
|
|
@@ -149,7 +200,7 @@ export function createTelegramAdapters(event, bot, _isEvent) {
|
|
|
149
200
|
stopTyping();
|
|
150
201
|
},
|
|
151
202
|
uploadFile: async (filePath, title) => {
|
|
152
|
-
await bot.uploadFile(event.
|
|
203
|
+
await bot.uploadFile(event.conversationId, filePath, title);
|
|
153
204
|
},
|
|
154
205
|
deleteResponse: async () => {
|
|
155
206
|
updatePromise = updatePromise.then(async () => {
|