export interface TelegramUser { id: number; is_bot?: boolean; first_name?: string; username?: string; } export interface TelegramChat { id: number | string; type?: string; } export interface TelegramMessage { message_id?: number; chat: TelegramChat; from?: TelegramUser; text?: string; } export interface TelegramUpdate { update_id: number; message?: TelegramMessage; } interface TelegramEnvelope { ok: boolean; result?: T; description?: string; } export interface TelegramBotClientOptions { token: string; baseUrl?: string; timeoutMs?: number; fetchImpl?: typeof fetch; } export interface GetUpdatesOptions { offset?: number; timeout?: number; signal?: AbortSignal; } export class TelegramBotClient { private readonly token: string; private readonly baseUrl: string; private readonly timeoutMs: number; private readonly fetchImpl: typeof fetch; constructor(options: TelegramBotClientOptions) { this.token = options.token; this.baseUrl = (options.baseUrl ?? "https://api.telegram.org").replace(/\/+$/, ""); this.timeoutMs = options.timeoutMs ?? 35_000; this.fetchImpl = options.fetchImpl ?? fetch; } async getMe(): Promise { return this.request("getMe", undefined, undefined, "GET"); } async getUpdates(options: GetUpdatesOptions = {}): Promise { const body: Record = { allowed_updates: ["message"], }; if (options.offset !== undefined) body.offset = options.offset; if (options.timeout !== undefined) body.timeout = options.timeout; const requestTimeoutMs = options.timeout !== undefined ? Math.max(this.timeoutMs, options.timeout * 1000 + 5000) : this.timeoutMs; return this.request( "getUpdates", body, options.signal, "POST", requestTimeoutMs, ); } async sendMessage(chatId: number | string, text: string): Promise { await this.request("sendMessage", { chat_id: chatId, text, disable_web_page_preview: true, }); } async deleteWebhook(dropPendingUpdates = false): Promise { await this.request("deleteWebhook", { drop_pending_updates: dropPendingUpdates, }); } private async request( method: string, body?: Record, signal?: AbortSignal, httpMethod = "POST", timeoutMs = this.timeoutMs, ): Promise { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); if (signal) { if (signal.aborted) controller.abort(); else signal.addEventListener("abort", () => controller.abort(), { once: true }); } let response: Response; try { const init: RequestInit = { method: httpMethod, signal: controller.signal, }; if (body !== undefined) { init.headers = { "content-type": "application/json" }; init.body = JSON.stringify(body); } response = await this.fetchImpl(`${this.baseUrl}/bot${this.token}/${method}`, init); } catch (err) { throw new Error( `Telegram Bot API ${method} request failed: ${err instanceof Error ? err.message : String(err)}`, ); } finally { clearTimeout(timer); } const text = await response.text(); let data: TelegramEnvelope | undefined; try { data = text ? (JSON.parse(text) as TelegramEnvelope) : undefined; } catch { data = undefined; } if (!response.ok) { throw new Error(`Telegram Bot API ${method} returned HTTP ${response.status}`); } if (!data?.ok) { throw new Error(`Telegram Bot API ${method} failed: ${data?.description ?? "unknown error"}`); } return data.result as T; } }