@gramio/files
Version:
Set of utils to work with files uploading to Telegram Bot API
363 lines (357 loc) • 10.7 kB
JavaScript
import fs from 'node:fs/promises';
import { basename } from 'node:path';
import { Readable } from 'node:stream';
let isWarned = false;
function isBlob(blob) {
if (!blob || typeof blob !== "object") return false;
if (blob instanceof Promise) {
if (!isWarned) {
const error = new Error(
"Promise<File> is deprecated. Please put await before."
);
console.warn(error);
isWarned = true;
}
return true;
}
return blob instanceof Blob;
}
const MEDIA_METHODS = {
setWebhook: [(params) => isBlob(params.certificate), null],
sendPhoto: [(params) => isBlob(params.photo), null],
sendAudio: [
(params) => isBlob(params.audio) || isBlob(params.thumbnail),
null
],
sendDocument: [
(params) => isBlob(params.document) || isBlob(params.thumbnail),
null
],
sendVideo: [
(params) => isBlob(params.video) || isBlob(params.thumbnail),
null
],
sendAnimation: [
(params) => isBlob(params.animation) || isBlob(params.thumbnail),
null
],
sendVoice: [(params) => isBlob(params.voice), null],
sendVideoNote: [
(params) => isBlob(params.video_note) || isBlob(params.thumbnail),
null
],
sendPaidMedia: [
(params) => params.media.some((x) => "media" in x && isBlob(x.media)) || params.media.some((x) => "cover" in x && isBlob(x.cover)) || params.media.some((x) => "thumbnail" in x && isBlob(x.thumbnail)),
[
{
name: "media",
property: "media",
type: "array"
},
{
name: "cover",
property: "media",
type: "array"
},
{
name: "thumbnail",
property: "media",
type: "array"
}
]
],
sendMediaGroup: [
(params) => params.media.some((x) => "media" in x && isBlob(x.media)) || params.media.some((x) => "thumbnail" in x && isBlob(x.thumbnail)) || params.media.some((x) => "cover" in x && isBlob(x.cover)),
[
{
name: "media",
property: "media",
type: "array"
},
{
name: "thumbnail",
property: "media",
type: "array"
},
{
name: "cover",
property: "media",
type: "array"
}
]
],
setChatPhoto: [(params) => isBlob(params.photo), null],
editMessageMedia: [
(params) => "media" in params.media && isBlob(params.media.media) || "thumbnail" in params.media && isBlob(params.media.thumbnail) || "cover" in params.media && isBlob(params.media.cover),
[
{
name: "media",
property: "media",
type: "union"
},
{
name: "thumbnail",
property: "media",
type: "union"
},
{
name: "cover",
property: "media",
type: "union"
}
]
],
sendSticker: [(params) => isBlob(params.sticker), null],
uploadStickerFile: [(params) => isBlob(params.sticker), null],
createNewStickerSet: [
(params) => params.stickers.some((x) => "sticker" in x && isBlob(x.sticker)),
[
{
name: "sticker",
property: "stickers",
type: "array"
}
]
],
addStickerToSet: [(params) => isBlob(params.sticker.sticker), null],
replaceStickerInSet: [(params) => isBlob(params.sticker.sticker), null],
setStickerSetThumbnail: [(params) => isBlob(params.thumbnail), null],
setBusinessAccountProfilePhoto: [
(params) => params.photo.type === "static" && isBlob(params.photo.photo) || params.photo.type === "animated" && isBlob(params.photo.animation),
[
{
name: "photo",
property: "photo",
type: "union"
},
{
name: "animation",
property: "photo",
type: "union"
}
]
],
postStory: [
(params) => params.content.type === "photo" && isBlob(params.content.photo) || params.content.type === "video" && isBlob(params.content.video),
[
{
name: "photo",
property: "content",
type: "union"
},
{
name: "video",
property: "content",
type: "union"
}
]
],
editStory: [
(params) => params.content.type === "photo" && isBlob(params.content.photo) || params.content.type === "video" && isBlob(params.content.video),
[
{
name: "photo",
property: "content",
type: "union"
},
{
name: "video",
property: "content",
type: "union"
}
]
]
};
function isMediaUpload(method, params) {
const mediaMethod = MEDIA_METHODS[method];
if (!mediaMethod) return false;
return mediaMethod[0](params);
}
function isExtractor(value, type, params) {
return value.type === type;
}
async function convertJsonToFormData(method, params) {
const formData = new FormData();
const mediaMethod = MEDIA_METHODS[method];
const extractor = mediaMethod?.[1] ?? [];
let attachId = 0;
for (const extractorValue of extractor) {
if (isExtractor(extractorValue, "union")) {
let file = params[extractorValue.property][extractorValue.name];
if (file instanceof Promise) file = await file;
if (!(file instanceof Blob)) continue;
const currentAttachId = attachId++;
formData.set(`file-${currentAttachId}`, file);
params[extractorValue.property][extractorValue.name] = `attach://file-${currentAttachId}`;
}
if (isExtractor(extractorValue, "array")) {
const array = params[extractorValue.property];
for (const [index, element] of array.entries()) {
let file = element[extractorValue.name];
if (file instanceof Promise) file = await file;
if (!(file instanceof Blob)) continue;
const currentAttachId = attachId++;
formData.set(`file-${currentAttachId}`, file);
params[extractorValue.property][index][extractorValue.name] = `attach://file-${currentAttachId}`;
}
}
}
for (let [key, value] of Object.entries(params)) {
if (value instanceof Promise) value = await value;
if (value instanceof Blob) formData.append(key, value);
else if (typeof value === "object")
formData.append(key, JSON.stringify(value));
else formData.append(key, String(value));
}
return formData;
}
async function extractFilesToFormData(method, params) {
const formData = new FormData();
let isEmpty = true;
const mediaMethod = MEDIA_METHODS[method];
const extractor = mediaMethod?.[1] ?? [];
let attachId = 0;
for (const extractorValue of extractor) {
if (isExtractor(extractorValue, "union")) {
let file = params[extractorValue.property][extractorValue.name];
if (file instanceof Promise) file = await file;
if (!(file instanceof Blob)) continue;
const currentAttachId = attachId++;
formData.set(`file-${currentAttachId}`, file);
isEmpty = false;
params[extractorValue.property][extractorValue.name] = `attach://file-${currentAttachId}`;
}
if (isExtractor(extractorValue, "array")) {
const array = params[extractorValue.property];
for (const [index, element] of array.entries()) {
let file = element[extractorValue.name];
if (file instanceof Promise) file = await file;
if (!(file instanceof Blob)) continue;
const currentAttachId = attachId++;
formData.set(`file-${currentAttachId}`, file);
isEmpty = false;
params[extractorValue.property][index][extractorValue.name] = `attach://file-${currentAttachId}`;
}
}
}
for (let [key, value] of Object.entries(params)) {
if (value instanceof Promise) value = await value;
if (value instanceof Blob) {
formData.append(key, value);
isEmpty = false;
delete params[key];
}
}
return [isEmpty ? void 0 : formData, params];
}
function convertStreamToBuffer(stream) {
return new Promise((resolve) => {
const chunks = [];
stream.on("data", (chunk) => {
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunks.push(bufferChunk);
});
stream.on("end", () => resolve(Buffer.concat(chunks)));
});
}
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([buffer], filename ?? basename(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([buffer], filename);
}
/**
* Method for uploading Media File by BinaryLike (Buffer or ArrayBuffer and etc).
*/
static buffer(buffer, filename = "file.buffer") {
return new File([buffer], 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);
}
}
export { MEDIA_METHODS, MediaInput, MediaUpload, convertJsonToFormData, convertStreamToBuffer, extractFilesToFormData, isBlob, isMediaUpload };