@gramio/files
Version:
Set of utils to work with files uploading to Telegram Bot API
291 lines (285 loc) • 10.1 kB
JavaScript
;
var utils = require('./utils-B55Px-X-.cjs');
var fs = require('node:fs/promises');
var node_path = require('node:path');
var node_stream = require('node:stream');
var node_fs = require('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 ?? node_path.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://${node_path.isAbsolute(path) ? path : node_path.resolve(path)}`;
}
/**
* Method for uploading Media File by Readable stream.
*/
static async stream(stream, filename = "file.stream") {
const buffer = await utils.convertStreamToBuffer(node_stream.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" ? node_path.basename(url) : node_path.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 = node_stream.Readable.toWeb(
node_fs.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, node_stream.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;
}
exports.MEDIA_METHODS = utils.MEDIA_METHODS;
exports.convertJsonToFormData = utils.convertJsonToFormData;
exports.convertStreamToBuffer = utils.convertStreamToBuffer;
exports.extractFilesToFormData = utils.extractFilesToFormData;
exports.isBlob = utils.isBlob;
exports.isMediaUpload = utils.isMediaUpload;
exports.MediaInput = MediaInput;
exports.MediaUpload = MediaUpload;
exports.TelegramFileDownload = TelegramFileDownload;
exports.downloadFile = downloadFile;