/** * Telegram Bot API client using native fetch. * No third-party HTTP client required. */ import type { SentMessageRepository } from "../data/sent-message.repository.js"; import type { TelegramUpdate, TelegramFile, ForumTopic, InlineKeyboardButton } from "../integrations/telegram/types.js"; export * from "../integrations/telegram/types.js"; export declare class TelegramClient { private readonly token; private readonly apiBaseUrl; private readonly baseUrl; /** Latest operator reaction received via getUpdates. */ lastReaction: { emoji: string; messageId: number; date: number; } | null; private readonly sentMessages; private static readonly MAX_SENT_MESSAGES; /** Optional repository for persisting message_id → thread_id mappings. */ private sentMessageRepository; /** Optional resolver for logical threadId → actual Telegram topic ID. */ private topicResolver; constructor(token: string, apiBaseUrl?: string); /** Set a resolver that maps logical thread IDs to actual Telegram topic IDs. */ setTopicResolver(resolver: (threadId: number) => number): void; /** Resolve a logical threadId to the actual Telegram topic ID. */ private resolveTopicId; /** * Wire up persistence for sent message_id → thread_id mappings. */ setSentMessageRepository(repository: SentMessageRepository): void; /** Record a sent message for later lookup (e.g. when a reaction arrives). */ private recordSentMessage; /** Look up the content snippet for a previously sent message. Returns null if not found. */ lookupSentMessage(messageId: number): string | null; /** * Strip the bot token from an error message so it is never leaked in logs * or propagated to callers. */ private redactError; /** * Wrapper around `fetch` that (a) delegates the actual request to * {@link fetchWithRetry} so every method transparently gains bounded, * jittered retry on transient failures (408/429/5xx and network blips), and * (b) redacts the bot token from any error before re-throwing. * * The timeout bounds the WHOLE operation (all retry attempts combined) so it * survives a scale-to-zero proxy cold-start; it is configurable via * TELEGRAM_SEND_TIMEOUT_MS (default 30s). An external signal aborts it * immediately. `fetchWithRetry` treats that abort as intentional and never * retries past it. Permanent errors (e.g. 400/401) are not transient, so they * pass straight through for the caller to inspect via `response.ok` — notably * the getUpdates 409-conflict loop is untouched. */ private safeFetch; /** Safely parse a JSON response, returning undefined on failure. */ private tryParseJson; /** * Send a media file via multipart/form-data. * Shared implementation for sendDocument, sendPhoto, and sendVoice. */ private sendMedia; /** * Long-poll for updates. * @param offset Only return updates with update_id >= offset. * @param timeout Long-poll server-side timeout in seconds (max 50 recommended). */ getUpdates(offset: number, timeout: number, signal?: AbortSignal): Promise; /** * Create a topic in a forum supergroup. * The bot must be an admin with can_manage_topics right. */ createForumTopic(chatId: string, name: string): Promise; /** * Delete a forum topic (and all its messages) from a supergroup. * The bot must be an admin with can_manage_topics right. */ deleteForumTopic(chatId: string | number, messageThreadId: number): Promise; /** * Send a text message to a chat, optionally scoped to a forum topic thread. */ sendMessage(chatId: string, text: string, parseMode?: "MarkdownV2" | "Markdown" | "HTML", threadId?: number): Promise; /** * Send a message carrying an inline keyboard into a forum topic. * `rawTopicId` is the Telegram message_thread_id as received from getUpdates * — it is used verbatim (no logical-thread resolution), because the poller * already holds the concrete topic id. */ sendInlineKeyboard(chatId: string, text: string, keyboard: InlineKeyboardButton[][], rawTopicId?: number): Promise; /** * Acknowledge a callback query so the client stops showing a loading spinner. * An optional `text` surfaces as a transient toast to the user. Failures are * swallowed (best-effort) — the injected prompt has already been delivered. */ answerCallbackQuery(callbackQueryId: string, text?: string): Promise; /** * Get metadata for a file stored on Telegram servers. */ getFile(fileId: string): Promise; /** * Download a file from Telegram by file_id and return it as a Buffer. */ downloadFileAsBuffer(fileId: string): Promise<{ buffer: Buffer; filePath: string; }>; /** Send a document (file) to a chat. */ sendDocument(chatId: string, fileBuffer: Buffer, filename: string, caption?: string, threadId?: number): Promise; /** Send a photo to a chat. */ sendPhoto(chatId: string, imageBuffer: Buffer, filename: string, caption?: string, threadId?: number): Promise; /** Send a voice message (OGG Opus) to a chat. */ sendVoice(chatId: string, audioBuffer: Buffer, threadId?: number, textSnippet?: string): Promise; /** Send a sticker to a chat by file_id. */ sendSticker(chatId: string, stickerId: string, threadId?: number): Promise; /** * Log a reaction-related warning at most once per suppression window. */ private reactionWarned; private reactionWarnedAt; private warnReactionOnce; /** * Set an emoji reaction on a message ("seen" indicator). * * Retries are handled centrally by the transient-retry layer * ({@link fetchWithRetry} via {@link safeFetch}) — 429 is a transient status, * so a rate-limited reaction is already retried there with bounded, jittered * backoff. This method therefore issues a SINGLE POST: it must never add its * own retry loop, or a persistent 429 would fan out multiplicatively. A * reaction that is still rate-limited after the shared retries is dropped * best-effort (it is only a cosmetic "seen" marker). Logs the first failure * per 60-second window to avoid flooding stderr in busy sessions. Never throws. */ setMessageReaction(chatId: string, messageId: number, emoji?: string): Promise; } /** * Send a text message via the Telegram Bot API without a TelegramClient instance. * Suitable for fire-and-forget notification helpers that only have the token and chat ID. */ export declare function sendTelegramMessage(token: string, chatId: string, text: string, options?: { parseMode?: string; threadId?: number; }, apiBaseUrl?: string): Promise; //# sourceMappingURL=telegram.d.ts.map