UNPKG

@gramio/files

Version:

Set of utils to work with files uploading to Telegram Bot API

281 lines (276 loc) 9.76 kB
import { c as convertStreamToBuffer } from './utils-BxB6X9KE.js'; export { M as MEDIA_METHODS, a as convertJsonToFormData, e as extractFilesToFormData, i as isBlob, b as isMediaUpload } from './utils-BxB6X9KE.js'; import fs from 'node:fs/promises'; import { basename, isAbsolute, resolve } from 'node:path'; import { Readable } from 'node:stream'; import { createReadStream } from 'node:fs'; class MediaInput { /** * Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent. * * [Documentation](https://core.telegram.org/bots/api/#inputmediaanimation) */ static animation(media, options = {}) { return { type: "animation", media, ...options }; } /** * Represents a general file to be sent. * * [Documentation](https://core.telegram.org/bots/api/#inputmediadocument) */ static document(media, options = {}) { return { type: "document", media, ...options }; } /** * Represents an audio file to be treated as music to be sent. * * [Documentation](https://core.telegram.org/bots/api/#inputmediaaudio) */ static audio(media, options = {}) { return { type: "audio", media, ...options }; } /** * Represents a photo to be sent. * * [Documentation](https://core.telegram.org/bots/api/#inputmediaphoto) */ static photo(media, options = {}) { return { type: "photo", media, ...options }; } /** * Represents a video to be sent. * * [Documentation](https://core.telegram.org/bots/api/#inputmediavideo) */ static video(media, options = {}) { return { type: "video", media, ...options }; } } class MediaUpload { /** * Method for uploading Media File by local path. */ static async path(path, filename) { const buffer = await fs.readFile(path); return new File([new Uint8Array(buffer)], filename ?? basename(path)); } /** * Reference a file by its **local path on a self-hosted Bot API server** * (`--local` mode) using the `file://` URI scheme. The server reads the file * straight from its own disk, so the bytes are **never transferred over HTTP**. * * This is an **optimization**, not a requirement for large files: a local Bot * API server already accepts up to **2 GB** via a normal upload * ({@link MediaUpload.path} / {@link MediaUpload.stream}). Use `localPath` only * when the file already lives **on the Bot API server's filesystem** (bot * co-located, or a shared volume) to skip re-uploading it. * * @example * ```ts * // file already on the server's disk → no HTTP upload of the bytes * ctx.sendDocument(MediaUpload.localPath("/var/data/big-archive.zip")); * ``` */ static localPath(path) { return `file://${isAbsolute(path) ? path : resolve(path)}`; } /** * Method for uploading Media File by Readable stream. */ static async stream(stream, filename = "file.stream") { const buffer = await convertStreamToBuffer(Readable.from(stream)); return new File([new Uint8Array(buffer)], filename); } /** * Method for uploading Media File by BinaryLike (Buffer or ArrayBuffer and etc). */ static buffer(buffer, filename = "file.buffer") { const blobPart = ArrayBuffer.isView(buffer) ? new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength) : buffer; return new File([blobPart], filename); } /** * Method for uploading Media File by URL (also with fetch options). */ static async url(url, filename, options) { const res = await fetch(url, options); return new File( [await res.blob()], filename ?? (typeof url === "string" ? basename(url) : basename(url.pathname)) ); } /** * Method for uploading Media File by text content. */ static text(text, filename = "text.txt") { return new File([text], filename); } } class TelegramFileDownload { #resolver; #resolved; #response; constructor(resolver) { this.#resolver = resolver; } resolve() { this.#resolved ??= this.#resolver(); return this.#resolved; } response() { this.#response ??= (async () => { const { source } = await this.resolve(); if (source.type === "url") return fetch(source.url); if (source.type === "data") { const buffer = source.data instanceof Uint8Array ? source.data.buffer.slice( source.data.byteOffset, source.data.byteOffset + source.data.byteLength ) : source.data; return new Response(buffer); } const webStream = Readable.toWeb( createReadStream(source.path) ); return new Response(webStream); })(); return this.#response; } /** The `getFile` metadata (`file_id`, `file_path`, `file_size`, …). Does not read the body. */ async info() { return (await this.resolve()).file; } /** The whole file as an `ArrayBuffer`. */ async arrayBuffer() { return (await this.response()).arrayBuffer(); } /** The whole file as a `Uint8Array`. */ async bytes() { const res = await this.response(); const native = res.bytes; return typeof native === "function" ? native.call(res) : new Uint8Array(await res.arrayBuffer()); } /** The whole file as a `Blob`. */ async blob() { return (await this.response()).blob(); } /** The file decoded as UTF-8 text. */ async text() { return (await this.response()).text(); } /** The file parsed as JSON. */ async json() { return (await this.response()).json(); } /** A web `ReadableStream` of the file — pipe large files without buffering. */ async stream() { const res = await this.response(); if (!res.body) throw new Error("Download response has no body"); return res.body; } /** * A download URL for the file. Does not read the body. * * ⚠️ **Contains the bot token by default** — the standard Telegram URL is * `…/file/bot<token>/<path>`. It is **token-less only when `files.baseURL` is * set** (a local server serving the working dir over HTTP), where it becomes * `${files.baseURL}/<path>`. Don't hand the default (token-bearing) link to end * users; configure `files.baseURL`, or proxy downloads yourself. * * Throws for a local-server file (`--local`, absolute path) with no * `files.baseURL` — there's no HTTP URL, only a path on the server's disk. */ async link() { const { source } = await this.resolve(); if (source.type === "url") return source.url; throw new Error( "No shareable URL for this file. Set `files.baseURL` to serve the working directory over HTTP." ); } /** Save the file to `path` and return it. Copies on disk when possible (no extra transfer). */ async toFile(path) { const { source } = await this.resolve(); if (source.type === "disk") { await fs.copyFile(source.path, path); return path; } const res = await this.response(); if (!res.body) throw new Error("Download response has no body"); await fs.writeFile(path, Readable.fromWeb(res.body)); return path; } /** `PromiseLike`: `await download` resolves the file bytes (back-compatible). */ then(onfulfilled, onrejected) { return this.arrayBuffer().then(onfulfilled, onrejected); } } function resolveFileId(input) { if (typeof input === "string") return input; if ("bigSize" in input && input.bigSize?.fileId) return input.bigSize.fileId; if ("fileId" in input && typeof input.fileId === "string") return input.fileId; if ("file_id" in input) return input.file_id; throw new Error("Invalid attachment: cannot resolve a file_id"); } function isAbsolutePath(filePath) { return filePath.startsWith("/") || filePath.startsWith("\\") || /^[A-Za-z]:[\\/]/.test(filePath); } function toLocalPath(filePath, bot) { const files = bot.options.files; const localDir = files?.localDir ?? "/var/lib/telegram-bot-api"; const mountDir = files?.mountDir; if (!mountDir || !filePath.startsWith(localDir)) return filePath; return mountDir.replace(/[\\/]+$/, "") + filePath.slice(localDir.length); } function classicURL(filePath, bot) { const baseURL = bot.options.api.baseURL || "https://api.telegram.org/bot"; return `${baseURL.replace("/bot", "/file/bot")}${bot.options.token}/${filePath}`; } function rewriteURL(filePath, bot) { const files = bot.options.files; if (!files?.baseURL) return classicURL(filePath, bot); const localDir = files.localDir ?? "/var/lib/telegram-bot-api"; const rel = (filePath.startsWith(localDir) ? filePath.slice(localDir.length) : filePath).replace(/^\/+/, ""); return `${files.baseURL.replace(/\/+$/, "")}/${rel}`; } function downloadFile(bot, attachment, path) { const handle = new TelegramFileDownload(async () => { const file = await bot.api.getFile({ file_id: resolveFileId(attachment) }); const filePath = file.file_path; if (!filePath) throw new Error("File is not available for download (no file_path)"); let strategy = bot.options.files?.source ?? "auto"; if (strategy === "auto") strategy = isAbsolutePath(filePath) ? bot.options.files?.baseURL ? "rewrite" : "disk" : "url"; let source; if (typeof strategy === "function") source = { type: "data", data: await strategy(file) }; else if (strategy === "disk") source = { type: "disk", path: toLocalPath(filePath, bot) }; else source = { type: "url", url: strategy === "rewrite" ? rewriteURL(filePath, bot) : classicURL(filePath, bot) }; return { file, source }; }); return path ? handle.toFile(path) : handle; } export { MediaInput, MediaUpload, TelegramFileDownload, convertStreamToBuffer, downloadFile };