Thin Hono API so the Iran-hosted backend can POST ops alerts here and this VPS calls Telegram Bot API.
75 lines
2.1 KiB
TypeScript
75 lines
2.1 KiB
TypeScript
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<typeof SendMessageBodySchema>
|
|
|
|
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<TelegramSendResult> {
|
|
const fetchImpl = input.fetchImpl ?? fetch
|
|
const payload: Record<string, unknown> = {
|
|
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 }
|
|
}
|