/** * Telegram Adapter - Hermes-style Telegram platform adapter * * Features: * - Polling and webhook modes * - DM and group chat support * - Inline queries * - Callback buttons */ import { BaseAdapter, type PlatformMessage, type PlatformConfig } from "./base.js"; interface TelegramConfig extends PlatformConfig { platform: "telegram"; token: string; mode?: "polling" | "webhook"; webhookUrl?: string; webhookSecret?: string; allowedChats?: string[]; // Whitelist chat IDs requireUsername?: boolean; // Require user to have a username } export type { TelegramConfig }; interface TelegramMessage { message_id: number; from?: { id: number; username?: string; first_name?: string }; chat: { id: number; type: string; title?: string }; text?: string; caption?: string; date: number; entities?: Array<{ type: string; offset: number; length: number }>; } export class TelegramAdapter extends BaseAdapter { readonly platform = "telegram" as const; config: TelegramConfig; private bot: any = null; private offset = 0; private pollingInterval: ReturnType | null = null; private connected = false; constructor(config: TelegramConfig) { super(); this.config = { enabled: true, platform: "telegram", mode: "polling", ...config, }; } async initialize(): Promise { // Test bot token const response = await this.apiRequest("/getMe"); const data = await response.json() as { ok: boolean; result?: { id: number; username: string; first_name: string } }; if (!response.ok || !data.ok) { throw new Error(`Telegram auth failed: ${response.status}`); } console.log(`[Telegram] Bot initialized: @${data.result?.username}`); // Set webhook if configured if (this.config.mode === "webhook" && this.config.webhookUrl) { await this.apiRequest("/setWebhook", { method: "POST", body: JSON.stringify({ url: this.config.webhookUrl }), }); console.log(`[Telegram] Webhook set: ${this.config.webhookUrl}`); } } private async apiRequest(endpoint: string, options: RequestInit = {}): Promise { const url = `https://api.telegram.org/bot${this.config.token}${endpoint}`; return fetch(url, { ...options, headers: { "Content-Type": "application/json", ...options.headers, }, }); } async start(callbacks): Promise { await super.start(callbacks); if (this.config.mode === "polling") { await this.startPolling(); } // Webhook mode is handled externally via HTTP server } private async startPolling(): Promise { this.connected = true; this.poll(); } private async poll(): Promise { while (this.connected && this.config.mode === "polling") { try { const response = await this.apiRequest("/getUpdates", { method: "POST", body: JSON.stringify({ offset: this.offset, timeout: 30, }), }); if (!response.ok) { console.error(`[Telegram] Poll error: ${response.status}`); await this.sleep(5000); continue; } const data = await response.json() as { ok: boolean; result?: TelegramMessage[] }; if (data.ok && data.result && data.result.length > 0) { for (const update of data.result) { await this.handleUpdate(update); this.offset = update.message_id + 1; } } } catch (err) { console.error("[Telegram] Poll exception:", err); await this.sleep(5000); } } } private async handleUpdate(update: any): Promise { // Handle messages if (update.message || update.edited_message) { const msg = update.message || update.edited_message; // Check if chat is allowed if (this.config.allowedChats && !this.config.allowedChats.includes(String(msg.chat.id))) { return; } // Check if username is required if (this.config.requireUsername && !msg.from?.username) { // Could send "Please set a username" message here return; } const content = msg.text || msg.caption || ""; // Skip empty messages if (!content) return; const message: PlatformMessage = { id: this.generateMessageId(), platform: "telegram", channelId: String(msg.chat.id), userId: String(msg.from?.id || 0), content, timestamp: msg.date * 1000, metadata: { username: msg.from?.username, firstName: msg.from?.first_name, chatType: msg.chat.type, chatTitle: msg.chat.title, isEdited: !!update.edited_message, }, }; this.emitMessage(message); } // Handle callback queries (button presses) if (update.callback_query) { const query = update.callback_query; const message: PlatformMessage = { id: this.generateMessageId(), platform: "telegram", channelId: String(query.message?.chat.id || query.from.id), userId: String(query.from.id), content: `Callback: ${query.data}`, timestamp: query.message?.date ? query.message.date * 1000 : Date.now(), metadata: { callbackId: query.id, callbackData: query.data, username: query.from.username, }, }; this.emitMessage(message); // Answer callback to remove loading state await this.apiRequest("/answerCallbackQuery", { method: "POST", body: JSON.stringify({ callback_query_id: query.id }), }); } } private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } async stop(): Promise { this.connected = false; if (this.pollingInterval) { clearInterval(this.pollingInterval); this.pollingInterval = null; } await super.stop(); } async sendMessage(channelId: string, content: string): Promise { const response = await this.apiRequest("/sendMessage", { method: "POST", body: JSON.stringify({ chat_id: channelId, text: content, parse_mode: "HTML", }), }); const data = await response.json() as { ok: boolean; result?: { message_id: number } }; if (!data.ok) { throw new Error(`Failed to send message: ${data}`); } return String(data.result?.message_id || 0); } async sendPhoto(channelId: string, photoUrl: string, caption?: string): Promise { const response = await this.apiRequest("/sendPhoto", { method: "POST", body: JSON.stringify({ chat_id: channelId, photo: photoUrl, caption, parse_mode: "HTML", }), }); const data = await response.json() as { ok: boolean; result?: { message_id: number } }; if (!data.ok) { throw new Error(`Failed to send photo: ${data}`); } return String(data.result?.message_id || 0); } async sendButtons(channelId: string, text: string, buttons: Array>): Promise { const replyMarkup = { inline_keyboard: buttons.map(row => row.map(btn => ({ text: btn.text, callback_data: btn.data })) ), }; const response = await this.apiRequest("/sendMessage", { method: "POST", body: JSON.stringify({ chat_id: channelId, text, parse_mode: "HTML", reply_markup: replyMarkup, }), }); const data = await response.json() as { ok: boolean; result?: { message_id: number } }; if (!data.ok) { throw new Error(`Failed to send buttons: ${data}`); } return String(data.result?.message_id || 0); } async editMessage(channelId: string, messageId: string, content: string): Promise { await this.apiRequest("/editMessageText", { method: "POST", body: JSON.stringify({ chat_id: channelId, message_id: parseInt(messageId), text: content, parse_mode: "HTML", }), }); } async deleteMessage(channelId: string, messageId: string): Promise { await this.apiRequest("/deleteMessage", { method: "POST", body: JSON.stringify({ chat_id: channelId, message_id: parseInt(messageId), }), }); } async setTyping(channelId: string, isTyping: boolean): Promise { const action = isTyping ? "typing" : "cancel"; await this.apiRequest("/sendChatAction", { method: "POST", body: JSON.stringify({ chat_id: channelId, action, }), }); } async getStatus(): Promise<{ connected: boolean; latency?: number }> { return { connected: this.connected }; } async getMe(): Promise<{ id: number; username: string; first_name: string }> { const response = await this.apiRequest("/getMe"); const data = await response.json() as { ok: boolean; result: { id: number; username: string; first_name: string } }; return data.result; } // Handle webhook update (called from HTTP handler) async handleWebhookUpdate(update: any): Promise { if (this.config.webhookSecret) { // Verify secret here } await this.handleUpdate(update); } }