{"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../../src/gateway/channels/telegram.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EACN,kBAAkB,EAElB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,MAAM,WAAW,CAAC;AACnB,OAAO,KAAK,EAAkB,eAAe,EAAc,MAAM,mBAAmB,CAAC;AAErF,qCAAqC;AACrC,MAAM,WAAW,cAAc;IAC9B,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,uBAAuB;AACvB,qBAAa,eAAgB,SAAQ,kBAAkB;IACtD,QAAQ,CAAC,WAAW,cAAc;IAElC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAAW;IACjC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,OAAO,CAAC,CAAe;IAC/B,OAAO,CAAC,eAAe,CAAC,CAAkB;IAE1C,YAAY,MAAM,EAAE,aAAa,EAMhC;IAED,6BAA6B;IACvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAahC;IAED,2BAA2B;IACrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9B;IAED,iCAAiC;IAC3B,IAAI,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CA6DlD;IAED,0BAA0B;IACpB,UAAU,IAAI,OAAO,CAAC,cAAc,CAAC,CAsB1C;IAED,gCAAgC;IAChC,OAAO,CAAC,YAAY;YAgBN,WAAW;IA2BzB,gCAAgC;IAChC,OAAO,CAAC,aAAa;IAuFrB,2CAA2C;IAC3C,OAAO,CAAC,WAAW;IAsBnB,uCAAuC;IACvC,OAAO,CAAC,aAAa;YAUP,OAAO;IAuBrB,kCAAkC;IAClC,OAAO,CAAC,aAAa;CAIrB","sourcesContent":["/**\n * Telegram Channel Adapter\n *\n * Uses Telegram Bot API with long polling to receive messages\n * and send responses.\n */\n\nimport {\n\tBaseChannelAdapter,\n\tglobalChannelRegistry,\n\ttype ChannelConfig,\n\ttype ChannelProfile,\n} from \"./base.js\";\nimport type { InboundMessage, OutboundMessage, SenderInfo } from \"../types/index.js\";\n\n/** Telegram channel configuration */\nexport interface TelegramConfig {\n\t/** Bot token from @BotFather */\n\tbotToken: string;\n\t/** Polling interval in ms (default: 1000) */\n\tpollingInterval?: number;\n\t/** Allowed chat IDs (empty = allow all) */\n\tallowedChatIds?: string[];\n}\n\n/** Telegram adapter */\nexport class TelegramAdapter extends BaseChannelAdapter {\n\treadonly channelType = \"telegram\";\n\n\tprivate botToken: string;\n\tprivate pollingInterval: number;\n\tprivate allowedChatIds: string[];\n\tprivate offset = 0;\n\tprivate pollingTimeout?: NodeJS.Timeout;\n\tprivate botInfo?: TelegramUser;\n\tprivate abortController?: AbortController;\n\n\tconstructor(config: ChannelConfig) {\n\t\tsuper(config);\n\t\tconst tgConfig = config.config as unknown as TelegramConfig;\n\t\tthis.botToken = tgConfig.botToken;\n\t\tthis.pollingInterval = tgConfig.pollingInterval ?? 1000;\n\t\tthis.allowedChatIds = tgConfig.allowedChatIds ?? [];\n\t}\n\n\t/** Initialize the adapter */\n\tasync initialize(): Promise<void> {\n\t\t// Get bot info\n\t\tconst response = await this.apiCall<TelegramUser>(\"getMe\");\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to initialize Telegram bot: ${response.description}`);\n\t\t}\n\t\tthis.botInfo = response.result;\n\t\tthis.connected = true;\n\n\t\tconsole.log(`Telegram bot initialized: @${this.botInfo?.username ?? 'unknown'}`);\n\n\t\t// Start polling\n\t\tthis.startPolling();\n\t}\n\n\t/** Shutdown the adapter */\n\tasync shutdown(): Promise<void> {\n\t\tthis.connected = false;\n\t\tif (this.pollingTimeout) {\n\t\t\tclearTimeout(this.pollingTimeout);\n\t\t}\n\t\tthis.abortController?.abort();\n\t}\n\n\t/** Send a message to Telegram */\n\tasync send(message: OutboundMessage): Promise<void> {\n\t\tconst chatId = this.extractChatId(message.target);\n\n\t\tfor (const part of message.parts) {\n\t\t\tswitch (part.type) {\n\t\t\t\tcase \"text\": {\n\t\t\t\t\t// Chunk text if needed (Telegram max message length is 4096)\n\t\t\t\t\tconst chunks = this.chunkText(part.text, 4096);\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\tawait this.apiCall(\"sendMessage\", {\n\t\t\t\t\t\t\tchat_id: chatId,\n\t\t\t\t\t\t\ttext: chunk,\n\t\t\t\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t\t\t\treply_to_message_id: message.replyTo,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"image\": {\n\t\t\t\t\tawait this.apiCall(\"sendPhoto\", {\n\t\t\t\t\t\tchat_id: chatId,\n\t\t\t\t\t\tphoto: part.url,\n\t\t\t\t\t\tcaption: part.caption,\n\t\t\t\t\t\tparse_mode: \"Markdown\",\n\t\t\t\t\t\treply_to_message_id: message.replyTo,\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"file\": {\n\t\t\t\t\tawait this.apiCall(\"sendDocument\", {\n\t\t\t\t\t\tchat_id: chatId,\n\t\t\t\t\t\tdocument: part.url,\n\t\t\t\t\t\tcaption: part.name,\n\t\t\t\t\t\treply_to_message_id: message.replyTo,\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"typing\": {\n\t\t\t\t\tawait this.apiCall(\"sendChatAction\", {\n\t\t\t\t\t\tchat_id: chatId,\n\t\t\t\t\t\taction: \"typing\",\n\t\t\t\t\t});\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"reaction\": {\n\t\t\t\t\t// Set message reaction (if messageId is available)\n\t\t\t\t\tif (part.messageId) {\n\t\t\t\t\t\tawait this.apiCall(\"setMessageReaction\", {\n\t\t\t\t\t\t\tchat_id: chatId,\n\t\t\t\t\t\t\tmessage_id: part.messageId,\n\t\t\t\t\t\t\treaction: [{ type: \"emoji\", emoji: part.emoji }],\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Get channel profile */\n\tasync getProfile(): Promise<ChannelProfile> {\n\t\treturn {\n\t\t\ttype: this.channelType,\n\t\t\tid: this.botInfo?.id.toString() ?? \"unknown\",\n\t\t\tname: this.botInfo?.first_name ?? \"Telegram Bot\",\n\t\t\tusername: this.botInfo?.username,\n\t\t\tconnected: this.connected,\n\t\t\tcapabilities: {\n\t\t\t\ttext: true,\n\t\t\t\timages: true,\n\t\t\t\tfiles: true,\n\t\t\t\treactions: true,\n\t\t\t\tthreads: true,\n\t\t\t\teditMessages: true,\n\t\t\t\tdeleteMessages: true,\n\t\t\t\ttyping: true,\n\t\t\t\tcards: false, // Limited support via inline keyboards\n\t\t\t\tvoice: true,\n\t\t\t\tmaxMessageLength: 4096,\n\t\t\t\tmaxFileSize: 50 * 1024 * 1024, // 50MB for bots\n\t\t\t},\n\t\t};\n\t}\n\n\t/** Start polling for updates */\n\tprivate startPolling(): void {\n\t\tif (!this.connected) return;\n\n\t\tthis.pollingTimeout = setTimeout(async () => {\n\t\t\ttry {\n\t\t\t\tawait this.pollUpdates();\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Telegram polling error:\", error);\n\t\t\t}\n\t\t\tif (this.connected) {\n\t\t\t\tthis.startPolling();\n\t\t\t}\n\t\t}, this.pollingInterval);\n\t}\n\n\t/** Poll for updates */\n\tprivate async pollUpdates(): Promise<void> {\n\t\tthis.abortController = new AbortController();\n\n\t\ttry {\n\t\t\tconst response = await this.apiCall<TelegramUpdate[]>(\n\t\t\t\t\"getUpdates\",\n\t\t\t\t{\n\t\t\t\t\toffset: this.offset,\n\t\t\t\t\tlimit: 100,\n\t\t\t\t\ttimeout: 30,\n\t\t\t\t},\n\t\t\t\tthis.abortController.signal,\n\t\t\t);\n\n\t\t\tif (response.ok && Array.isArray(response.result) && response.result.length > 0) {\n\t\t\t\tfor (const update of response.result) {\n\t\t\t\t\tthis.processUpdate(update);\n\t\t\t\t\tthis.offset = update.update_id + 1;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif ((error as Error).name !== \"AbortError\") {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n\n\t/** Process a Telegram update */\n\tprivate processUpdate(update: TelegramUpdate): void {\n\t\tconst message = update.message || update.edited_message;\n\t\tif (!message) return;\n\n\t\t// Check allowlist\n\t\tconst chatId = message.chat.id.toString();\n\t\tif (this.allowedChatIds.length > 0 && !this.allowedChatIds.includes(chatId)) {\n\t\t\tconsole.log(`Ignoring message from unauthorized chat: ${chatId}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine scope\n\t\tconst scope = message.chat.type === \"private\" ? \"dm\" : \"group\";\n\n\t\t// Check for mention in groups\n\t\tif (scope === \"group\" && this.config.access.groupPolicy === \"mention\") {\n\t\t\tconst isMentioned = this.isMentioned(message);\n\t\t\tif (!isMentioned) {\n\t\t\t\treturn; // Ignore messages without mention\n\t\t\t}\n\t\t}\n\n\t\t// Build sender info\n\t\tconst sender: SenderInfo = {\n\t\t\tid: message.from?.id.toString() ?? \"unknown\",\n\t\t\tname: [message.from?.first_name, message.from?.last_name].filter(Boolean).join(\" \") || \"Unknown\",\n\t\t\tusername: message.from?.username,\n\t\t\tisAdmin: false, // Would need additional API call to determine\n\t\t};\n\n\t\t// Build content\n\t\tlet content: InboundMessage[\"content\"];\n\n\t\tif (message.text) {\n\t\t\t// Check for commands\n\t\t\tif (message.text.startsWith(\"/\")) {\n\t\t\t\tconst [command, ...args] = message.text.slice(1).split(\" \");\n\t\t\t\tcontent = {\n\t\t\t\t\ttype: \"command\",\n\t\t\t\t\tcommand,\n\t\t\t\t\targs,\n\t\t\t\t};\n\t\t\t} else {\n\t\t\t\tcontent = {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext: this.cleanMentions(message.text),\n\t\t\t\t};\n\t\t\t}\n\t\t} else if (message.photo) {\n\t\t\t// Get largest photo\n\t\t\tconst photo = message.photo[message.photo.length - 1];\n\t\t\tcontent = {\n\t\t\t\ttype: \"image\",\n\t\t\t\turl: photo.file_id, // Need to get file URL via getFile\n\t\t\t\tcaption: message.caption,\n\t\t\t};\n\t\t} else if (message.document) {\n\t\t\tcontent = {\n\t\t\t\ttype: \"file\",\n\t\t\t\turl: message.document.file_id,\n\t\t\t\tname: message.document.file_name ?? \"file\",\n\t\t\t\tmimeType: message.document.mime_type ?? \"application/octet-stream\",\n\t\t\t\tsize: message.document.file_size,\n\t\t\t};\n\t\t} else {\n\t\t\t// Unsupported message type\n\t\t\treturn;\n\t\t}\n\n\t\t// Get agent ID from binding\n\t\tconst agentId = this.config.name; // Simplified - should resolve via router\n\n\t\t// Create inbound message\n\t\tconst inboundMessage = this.createInboundMessage({\n\t\t\tid: message.message_id.toString(),\n\t\t\tagentId,\n\t\t\tscope,\n\t\t\tidentifier: chatId,\n\t\t\tsender,\n\t\t\tcontent,\n\t\t\treplyTo: message.reply_to_message?.message_id.toString(),\n\t\t\tthreadId: message.message_thread_id?.toString(),\n\t\t});\n\n\t\tthis.emitMessage(inboundMessage);\n\t}\n\n\t/** Check if bot is mentioned in message */\n\tprivate isMentioned(message: TelegramMessage): boolean {\n\t\tif (!message.entities) return false;\n\n\t\tconst botUsername = this.botInfo?.username?.toLowerCase();\n\n\t\tfor (const entity of message.entities) {\n\t\t\tif (entity.type === \"mention\") {\n\t\t\t\tconst mention = message.text?.slice(entity.offset, entity.offset + entity.length).toLowerCase();\n\t\t\t\tif (mention === `@${botUsername}`) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entity.type === \"text_mention\") {\n\t\t\t\tif (entity.user?.id === this.botInfo?.id) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/** Clean mentions from message text */\n\tprivate cleanMentions(text: string): string {\n\t\tconst botUsername = this.botInfo?.username;\n\t\tif (!botUsername) return text;\n\n\t\t// Remove @botname from the beginning of the message\n\t\tconst mentionPattern = new RegExp(`^@${botUsername}\\\\s*`, \"i\");\n\t\treturn text.replace(mentionPattern, \"\").trim();\n\t}\n\n\t/** Make API call to Telegram */\n\tprivate async apiCall<T = unknown>(\n\t\tmethod: string,\n\t\tparams: Record<string, unknown> = {},\n\t\tsignal?: AbortSignal,\n\t): Promise<TelegramResponse<T>> {\n\t\tconst url = `https://api.telegram.org/bot${this.botToken}/${method}`;\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify(params),\n\t\t\tsignal,\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Telegram API error: ${response.status} ${response.statusText}`);\n\t\t}\n\n\t\treturn response.json() as Promise<TelegramResponse<T>>;\n\t}\n\n\t/** Extract chat ID from target */\n\tprivate extractChatId(target: string): string {\n\t\t// Target is the chat ID for Telegram\n\t\treturn target;\n\t}\n}\n\n/** Register with global registry */\nglobalChannelRegistry.register(\"telegram\", TelegramAdapter);\n\n/** Telegram API types */\ninterface TelegramUser {\n\tid: number;\n\tis_bot: boolean;\n\tfirst_name: string;\n\tlast_name?: string;\n\tusername?: string;\n\tlanguage_code?: string;\n}\n\ninterface TelegramChat {\n\tid: number;\n\ttype: \"private\" | \"group\" | \"supergroup\" | \"channel\";\n\ttitle?: string;\n\tusername?: string;\n\tfirst_name?: string;\n\tlast_name?: string;\n}\n\ninterface TelegramMessageEntity {\n\ttype: string;\n\toffset: number;\n\tlength: number;\n\turl?: string;\n\tuser?: TelegramUser;\n}\n\ninterface TelegramPhotoSize {\n\tfile_id: string;\n\tfile_unique_id: string;\n\twidth: number;\n\theight: number;\n\tfile_size?: number;\n}\n\ninterface TelegramDocument {\n\tfile_id: string;\n\tfile_unique_id: string;\n\tfile_name?: string;\n\tmime_type?: string;\n\tfile_size?: number;\n}\n\ninterface TelegramMessage {\n\tmessage_id: number;\n\tfrom?: TelegramUser;\n\tdate: number;\n\tchat: TelegramChat;\n\ttext?: string;\n\tcaption?: string;\n\tentities?: TelegramMessageEntity[];\n\tphoto?: TelegramPhotoSize[];\n\tdocument?: TelegramDocument;\n\treply_to_message?: TelegramMessage;\n\tmessage_thread_id?: number;\n}\n\ninterface TelegramUpdate {\n\tupdate_id: number;\n\tmessage?: TelegramMessage;\n\tedited_message?: TelegramMessage;\n\tchannel_post?: TelegramMessage;\n\tedited_channel_post?: TelegramMessage;\n}\n\ninterface TelegramResponse<T = unknown> {\n\tok: boolean;\n\tresult: T;\n\tdescription?: string;\n\terror_code?: number;\n}\n"]}