UNPKG

@es-labs/node

Version:
574 lines (492 loc) 22.7 kB
// telegram-sender.js import FormData from "form-data"; import fetch from "node-fetch"; import fs from "fs"; import path from "path"; const BASE_URL = (token) => `https://api.telegram.org/bot${token}`; // ─── Core Request Helper ────────────────────────────────────────────────────── async function apiRequest(token, method, params = {}, formData = null) { const url = `${BASE_URL(token)}/${method}`; const options = formData ? { method: "POST", body: formData } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }; const res = await fetch(url, options); const data = await res.json(); if (!data.ok) { throw new TelegramError(data.description, data.error_code, method); } return data.result; } class TelegramError extends Error { constructor(message, code, method) { super(`[${method}] Telegram API error ${code}: ${message}`); this.code = code; this.method = method; } } // ─── File Helper ────────────────────────────────────────────────────────────── /** * Resolves a file input to the right Telegram format: * - "file_id:..." → use existing Telegram file ID * - "http(s)://..." → use URL * - Everything else → treat as local path, attach as multipart */ function resolveFile(fd, fieldName, input) { if (!input) return null; if (input.startsWith("file_id:")) return input.replace("file_id:", ""); if (input.startsWith("http://") || input.startsWith("https://")) return input; // Local file – attach to FormData const stream = fs.createReadStream(input); fd.append(fieldName, stream, path.basename(input)); return null; // signal: already added to form } // ─── Keyboard Builders ──────────────────────────────────────────────────────── /** * Inline keyboard attached to the message. * * @example * inlineKeyboard([ * [{ text: "Visit", url: "https://example.com" }], * [{ text: "Callback", callback_data: "btn_1" }, { text: "Pay", pay: true }] * ]) */ export function inlineKeyboard(rows) { return JSON.stringify({ inline_keyboard: rows }); } /** * Custom reply keyboard shown to the user. * * @example * replyKeyboard([["Yes", "No"], ["Cancel"]], { resize_keyboard: true }) */ export function replyKeyboard( rows, { resize_keyboard = true, one_time_keyboard = false, is_persistent = false, selective = false } = {} ) { return JSON.stringify({ keyboard: rows.map((row) => row.map((btn) => (typeof btn === "string" ? { text: btn } : btn)) ), resize_keyboard, one_time_keyboard, is_persistent, selective, }); } /** Removes any custom keyboard from the user's view. */ export function removeKeyboard(selective = false) { return JSON.stringify({ remove_keyboard: true, selective }); } /** Forces a reply prompt on the user's client. */ export function forceReply(input_field_placeholder = "", selective = false) { return JSON.stringify({ force_reply: true, input_field_placeholder, selective }); } // ─── Shared Message Options ─────────────────────────────────────────────────── function commonOpts({ parse_mode, // "HTML" | "Markdown" | "MarkdownV2" caption, caption_parse_mode, reply_to_message_id, allow_sending_without_reply, reply_markup, // use inlineKeyboard() / replyKeyboard() helpers disable_notification, protect_content, message_thread_id, // forum thread id business_connection_id, } = {}) { return Object.fromEntries( Object.entries({ parse_mode, caption, caption_parse_mode, reply_to_message_id, allow_sending_without_reply, reply_markup, disable_notification, protect_content, message_thread_id, business_connection_id, }).filter(([, v]) => v !== undefined) ); } // ─── Text ───────────────────────────────────────────────────────────────────── /** * Send a plain or formatted text message. * * @param {string} token * @param {number|string} chatId * @param {string} text * @param {object} opts * @param {string} [opts.parse_mode] – "HTML" | "Markdown" | "MarkdownV2" * @param {Array} [opts.entities] – pre-built MessageEntity array * @param {boolean} [opts.disable_web_page_preview] * @param {string} [opts.reply_markup] – use inlineKeyboard() etc. */ export async function sendMessage(token, chatId, text, opts = {}) { return apiRequest(token, "sendMessage", { chat_id: chatId, text, disable_web_page_preview: opts.disable_web_page_preview, entities: opts.entities, ...commonOpts(opts), }); } // ─── Photo ──────────────────────────────────────────────────────────────────── /** * @param {string} photo – local path, HTTPS URL, or "file_id:<id>" * @param {boolean} [opts.has_spoiler] */ export async function sendPhoto(token, chatId, photo, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.caption) fd.append("caption", opts.caption); if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode); if (opts.has_spoiler) fd.append("has_spoiler", "true"); if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup); if (opts.reply_to_message_id) fd.append("reply_to_message_id", String(opts.reply_to_message_id)); if (opts.disable_notification) fd.append("disable_notification", "true"); if (opts.protect_content) fd.append("protect_content", "true"); const resolved = resolveFile(fd, "photo", photo); if (resolved) fd.append("photo", resolved); return apiRequest(token, "sendPhoto", {}, fd); } // ─── Video ──────────────────────────────────────────────────────────────────── /** * @param {string} video – local path, URL, or file_id * @param {object} opts * @param {number} [opts.duration] * @param {number} [opts.width] * @param {number} [opts.height] * @param {string} [opts.thumbnail] – local path, URL, or file_id for cover image * @param {boolean} [opts.supports_streaming] * @param {boolean} [opts.has_spoiler] */ export async function sendVideo(token, chatId, video, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.duration) fd.append("duration", String(opts.duration)); if (opts.width) fd.append("width", String(opts.width)); if (opts.height) fd.append("height", String(opts.height)); if (opts.supports_streaming) fd.append("supports_streaming", "true"); if (opts.has_spoiler) fd.append("has_spoiler", "true"); if (opts.caption) fd.append("caption", opts.caption); if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode); if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup); const resolvedVideo = resolveFile(fd, "video", video); if (resolvedVideo) fd.append("video", resolvedVideo); if (opts.thumbnail) { const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail); if (resolvedThumb) fd.append("thumbnail", resolvedThumb); } return apiRequest(token, "sendVideo", {}, fd); } // ─── Audio ──────────────────────────────────────────────────────────────────── /** * @param {string} audio – local path, URL, or file_id * @param {object} opts * @param {number} [opts.duration] * @param {string} [opts.performer] * @param {string} [opts.title] * @param {string} [opts.thumbnail] */ export async function sendAudio(token, chatId, audio, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.duration) fd.append("duration", String(opts.duration)); if (opts.performer) fd.append("performer", opts.performer); if (opts.title) fd.append("title", opts.title); if (opts.caption) fd.append("caption", opts.caption); if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode); if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup); const resolved = resolveFile(fd, "audio", audio); if (resolved) fd.append("audio", resolved); if (opts.thumbnail) { const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail); if (resolvedThumb) fd.append("thumbnail", resolvedThumb); } return apiRequest(token, "sendAudio", {}, fd); } // ─── Document ───────────────────────────────────────────────────────────────── /** * @param {string} document – local path, URL, or file_id * @param {boolean} [opts.disable_content_type_detection] */ export async function sendDocument(token, chatId, document, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.caption) fd.append("caption", opts.caption); if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode); if (opts.disable_content_type_detection) fd.append("disable_content_type_detection", "true"); if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup); const resolved = resolveFile(fd, "document", document); if (resolved) fd.append("document", resolved); if (opts.thumbnail) { const resolvedThumb = resolveFile(fd, "thumbnail", opts.thumbnail); if (resolvedThumb) fd.append("thumbnail", resolvedThumb); } return apiRequest(token, "sendDocument", {}, fd); } // ─── Voice ──────────────────────────────────────────────────────────────────── /** OGG/OPUS encoded voice message. */ export async function sendVoice(token, chatId, voice, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.duration) fd.append("duration", String(opts.duration)); if (opts.caption) fd.append("caption", opts.caption); if (opts.parse_mode) fd.append("parse_mode", opts.parse_mode); const resolved = resolveFile(fd, "voice", voice); if (resolved) fd.append("voice", resolved); return apiRequest(token, "sendVoice", {}, fd); } // ─── Video Note ─────────────────────────────────────────────────────────────── /** Round video (1:1 aspect ratio). */ export async function sendVideoNote(token, chatId, videoNote, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.duration) fd.append("duration", String(opts.duration)); if (opts.length) fd.append("length", String(opts.length)); const resolved = resolveFile(fd, "video_note", videoNote); if (resolved) fd.append("video_note", resolved); return apiRequest(token, "sendVideoNote", {}, fd); } // ─── Sticker ────────────────────────────────────────────────────────────────── /** * @param {string} sticker – local .webp/.tgs/.webm, URL, or file_id * @param {string} [opts.emoji] – emoji associated with the sticker */ export async function sendSticker(token, chatId, sticker, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.emoji) fd.append("emoji", opts.emoji); if (opts.reply_markup) fd.append("reply_markup", opts.reply_markup); const resolved = resolveFile(fd, "sticker", sticker); if (resolved) fd.append("sticker", resolved); return apiRequest(token, "sendSticker", {}, fd); } // ─── Animation (GIF) ────────────────────────────────────────────────────────── export async function sendAnimation(token, chatId, animation, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.duration) fd.append("duration", String(opts.duration)); if (opts.width) fd.append("width", String(opts.width)); if (opts.height) fd.append("height", String(opts.height)); if (opts.caption) fd.append("caption", opts.caption); if (opts.has_spoiler) fd.append("has_spoiler", "true"); const resolved = resolveFile(fd, "animation", animation); if (resolved) fd.append("animation", resolved); return apiRequest(token, "sendAnimation", {}, fd); } // ─── Media Group (Album) ────────────────────────────────────────────────────── /** * Send 2–10 photos/videos as an album. * * @param {Array} media Array of { type, file, caption?, parse_mode? } * type: "photo" | "video" | "audio" | "document" * * @example * sendMediaGroup(token, chatId, [ * { type: "photo", file: "./img1.jpg", caption: "First" }, * { type: "photo", file: "./img2.jpg" }, * { type: "video", file: "./clip.mp4" }, * ]); */ export async function sendMediaGroup(token, chatId, media, opts = {}) { const fd = new FormData(); fd.append("chat_id", String(chatId)); if (opts.reply_to_message_id) fd.append("reply_to_message_id", String(opts.reply_to_message_id)); if (opts.disable_notification) fd.append("disable_notification", "true"); if (opts.protect_content) fd.append("protect_content", "true"); const mediaArray = media.map((item, i) => { const fieldName = `file_${i}`; const resolved = resolveFile(fd, fieldName, item.file); return { type: item.type, media: resolved ?? `attach://${fieldName}`, ...(item.caption ? { caption: item.caption } : {}), ...(item.parse_mode ? { parse_mode: item.parse_mode } : {}), ...(item.has_spoiler ? { has_spoiler: true } : {}), }; }); fd.append("media", JSON.stringify(mediaArray)); return apiRequest(token, "sendMediaGroup", {}, fd); } // ─── Location ───────────────────────────────────────────────────────────────── /** * @param {object} opts * @param {number} [opts.horizontal_accuracy] – accuracy radius in metres (0–1500) * @param {number} [opts.live_period] – seconds to broadcast live (60–86400) * @param {number} [opts.heading] – 1–360 degrees * @param {number} [opts.proximity_alert_radius] */ export async function sendLocation(token, chatId, latitude, longitude, opts = {}) { return apiRequest(token, "sendLocation", { chat_id: chatId, latitude, longitude, horizontal_accuracy: opts.horizontal_accuracy, live_period: opts.live_period, heading: opts.heading, proximity_alert_radius: opts.proximity_alert_radius, ...commonOpts(opts), }); } /** Edit a live location message while it's still broadcasting. */ export async function editLiveLocation(token, chatId, messageId, latitude, longitude, opts = {}) { return apiRequest(token, "editMessageLiveLocation", { chat_id: chatId, message_id: messageId, latitude, longitude, horizontal_accuracy: opts.horizontal_accuracy, heading: opts.heading, proximity_alert_radius: opts.proximity_alert_radius, reply_markup: opts.reply_markup, }); } // ─── Venue ──────────────────────────────────────────────────────────────────── export async function sendVenue(token, chatId, { latitude, longitude, title, address, foursquare_id, foursquare_type, google_place_id, google_place_type }, opts = {}) { return apiRequest(token, "sendVenue", { chat_id: chatId, latitude, longitude, title, address, foursquare_id, foursquare_type, google_place_id, google_place_type, ...commonOpts(opts), }); } // ─── Contact ────────────────────────────────────────────────────────────────── export async function sendContact(token, chatId, { phone_number, first_name, last_name, vcard }, opts = {}) { return apiRequest(token, "sendContact", { chat_id: chatId, phone_number, first_name, last_name, vcard, ...commonOpts(opts), }); } // ─── Poll ───────────────────────────────────────────────────────────────────── /** * @param {string} question * @param {string[]} options – 2–10 answer choices * @param {object} opts * @param {string} [opts.type] – "regular" | "quiz" * @param {boolean} [opts.is_anonymous] * @param {boolean} [opts.allows_multiple_answers] * @param {number} [opts.correct_option_id] – required for quiz type * @param {string} [opts.explanation] * @param {number} [opts.open_period] – seconds (5–600) * @param {number} [opts.close_date] – unix timestamp * @param {boolean} [opts.is_closed] */ export async function sendPoll(token, chatId, question, options, opts = {}) { return apiRequest(token, "sendPoll", { chat_id: chatId, question, options, type: opts.type ?? "regular", is_anonymous: opts.is_anonymous ?? true, allows_multiple_answers: opts.allows_multiple_answers, correct_option_id: opts.correct_option_id, explanation: opts.explanation, explanation_parse_mode: opts.explanation_parse_mode, open_period: opts.open_period, close_date: opts.close_date, is_closed: opts.is_closed, ...commonOpts(opts), }); } // ─── Dice ───────────────────────────────────────────────────────────────────── /** @param {string} [emoji] – "🎲" | "🎯" | "🏀" | "⚽" | "🎳" | "🎰" (default 🎲) */ export async function sendDice(token, chatId, emoji = "🎲", opts = {}) { return apiRequest(token, "sendDice", { chat_id: chatId, emoji, ...commonOpts(opts), }); } // ─── Chat Action (typing indicator) ────────────────────────────────────────── /** * @param {string} action – "typing" | "upload_photo" | "record_video" | * "upload_video" | "record_voice" | "upload_voice" | * "upload_document" | "choose_sticker" | * "find_location" | "record_video_note" | * "upload_video_note" */ export async function sendChatAction(token, chatId, action, opts = {}) { return apiRequest(token, "sendChatAction", { chat_id: chatId, action, message_thread_id: opts.message_thread_id, }); } // ─── Forward & Copy ─────────────────────────────────────────────────────────── export async function forwardMessage(token, chatId, fromChatId, messageId, opts = {}) { return apiRequest(token, "forwardMessage", { chat_id: chatId, from_chat_id: fromChatId, message_id: messageId, disable_notification: opts.disable_notification, protect_content: opts.protect_content, message_thread_id: opts.message_thread_id, }); } /** Copy without the forward header. */ export async function copyMessage(token, chatId, fromChatId, messageId, opts = {}) { return apiRequest(token, "copyMessage", { chat_id: chatId, from_chat_id: fromChatId, message_id: messageId, caption: opts.caption, parse_mode: opts.parse_mode, reply_markup: opts.reply_markup, disable_notification: opts.disable_notification, protect_content: opts.protect_content, reply_to_message_id: opts.reply_to_message_id, }); } // ─── Edit & Delete ──────────────────────────────────────────────────────────── export async function editMessageText(token, chatId, messageId, text, opts = {}) { return apiRequest(token, "editMessageText", { chat_id: chatId, message_id: messageId, text, parse_mode: opts.parse_mode, entities: opts.entities, disable_web_page_preview: opts.disable_web_page_preview, reply_markup: opts.reply_markup, }); } export async function editMessageCaption(token, chatId, messageId, caption, opts = {}) { return apiRequest(token, "editMessageCaption", { chat_id: chatId, message_id: messageId, caption, parse_mode: opts.parse_mode, reply_markup: opts.reply_markup, }); } export async function editMessageReplyMarkup(token, chatId, messageId, replyMarkup) { return apiRequest(token, "editMessageReplyMarkup", { chat_id: chatId, message_id: messageId, reply_markup: replyMarkup, }); } export async function deleteMessage(token, chatId, messageId) { return apiRequest(token, "deleteMessage", { chat_id: chatId, message_id: messageId }); } export async function pinMessage(token, chatId, messageId, opts = {}) { return apiRequest(token, "pinChatMessage", { chat_id: chatId, message_id: messageId, disable_notification: opts.disable_notification, }); } export async function unpinMessage(token, chatId, messageId) { return apiRequest(token, "unpinChatMessage", { chat_id: chatId, message_id: messageId }); }