import { z } from 'zod' /** Telegram Bot API sendMessage text limit. */ export const TELEGRAM_TEXT_MAX = 4096 export const SendMessageBodySchema = z.object({ text: z.string().trim().min(1).max(TELEGRAM_TEXT_MAX), /** Override default chat from env (group or private). */ chatId: z.string().trim().min(1).optional(), /** Forum topic id; omit to use env default (if any). Pass null to force no topic. */ messageThreadId: z.number().int().positive().nullable().optional(), disableWebPagePreview: z.boolean().optional().default(true), }) export type SendMessageBody = z.infer export interface TelegramSendResult { ok: true messageId: number } export class TelegramApiError extends Error { constructor( readonly status: number, readonly detail: string, ) { super(`Telegram sendMessage failed: status=${status}`) this.name = 'TelegramApiError' } } export async function sendTelegramMessage(input: { botToken: string chatId: string text: string messageThreadId?: number disableWebPagePreview: boolean fetchImpl?: typeof fetch }): Promise { const fetchImpl = input.fetchImpl ?? fetch const payload: Record = { chat_id: input.chatId, text: input.text, disable_web_page_preview: input.disableWebPagePreview, } if (input.messageThreadId !== undefined) { payload.message_thread_id = input.messageThreadId } const response = await fetchImpl( `https://api.telegram.org/bot${input.botToken}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }, ) if (!response.ok) { const detail = await response.text().catch(() => '') throw new TelegramApiError(response.status, detail.slice(0, 300)) } const json = (await response.json()) as { ok?: boolean result?: { message_id?: number } } const messageId = json.result?.message_id if (!json.ok || typeof messageId !== 'number') { throw new TelegramApiError(502, 'Unexpected Telegram API response') } return { ok: true, messageId } }