'use strict';
const fs = require('fs');
const path = require('path');
const mime = require('mime');
const fetch = require('node-fetch');
const { URL } = require('url');
/**
* Media attached to a message
* @param {string} mimetype MIME type of the attachment
* @param {string} data Base64-encoded data of the file
* @param {?string} filename Document file name. Value can be null
* @param {?number} filesize Document file size in bytes. Value can be null
*/
class MessageMedia {
constructor(mimetype, data, filename, filesize) {
/**
* MIME type of the attachment
* @type {string}
*/
this.mimetype = mimetype;
/**
* Base64 encoded data that represents the file
* @type {string}
*/
this.data = data;
/**
* Document file name. Value can be null
* @type {?string}
*/
this.filename = filename;
/**
* Document file size in bytes. Value can be null
* @type {?number}
*/
this.filesize = filesize;
if (!filesize) {
const paddingCount = data.endsWith('==') ? 2 : (data.endsWith('=') ? 1 : 0);
this.filesize = Math.floor(data.length * 3 / 4) - paddingCount;
}
}
/**
* Creates a MessageMedia instance from a local file path
* @param {string} filePath
* @returns {MessageMedia}
*/
static fromFilePath(filePath) {
const b64data = fs.readFileSync(filePath, {encoding: 'base64'});
const mimetype = mime.getType(filePath);
const filename = path.basename(filePath);
const fileSize = fs.statSync(filePath).size;
return new MessageMedia(mimetype, b64data, filename, fileSize);
}
/**
* Creates a MessageMedia instance from a URL
* @param {string} url
* @param {Object} [options]
* @param {boolean} [options.unsafeMime=false]
* @param {string} [options.filename]
* @param {object} [options.client]
* @param {object} [options.reqOptions]
* @param {number} [options.reqOptions.size=0]
* @returns {Promise<MessageMedia>}
*/
static async fromUrl(url, options = {}) {
const pUrl = new URL(url);
let mimetype = mime.getType(pUrl.pathname);
if (!mimetype && !options.unsafeMime)
throw new Error('Unable to determine MIME type using URL. Set unsafeMime to true to download it anyway.');
try {
const headResponse = await fetch(url, { method: 'HEAD' });
const contentLength = headResponse.headers.get('Content-Length');
if (contentLength) {
const fileSizeBytes = parseInt(contentLength);
const fileSizeMB = fileSizeBytes / (1024 * 1024);
const whatsappLimits = {
'image/': 16,
'video/': 64,
'audio/': 16,
'application/': 100,
'text/': 100,
'default': 64
};
const preliminaryMimetype = mimetype || headResponse.headers.get('Content-Type') || '';
let maxSizeMB = whatsappLimits.default;
for (const [type, limit] of Object.entries(whatsappLimits)) {
if (type !== 'default' && preliminaryMimetype.startsWith(type)) {
maxSizeMB = limit;
break;
}
}
if (fileSizeMB > maxSizeMB) {
const error = new Error(
`File size (${fileSizeMB.toFixed(2)}MB) exceeds WhatsApp limit for ${preliminaryMimetype} (${maxSizeMB}MB). ` +
`Maximum allowed: ${maxSizeMB}MB, Got: ${fileSizeMB.toFixed(2)}MB`
);
error.code = 'FILE_TOO_LARGE';
error.fileSize = fileSizeMB;
error.maxSize = maxSizeMB;
error.mimetype = preliminaryMimetype;
throw error;
}
if (fileSizeMB > 50) {
console.warn(`Large file detected (${fileSizeMB.toFixed(2)}MB). This may take longer to process.`);
}
} else {
console.warn('Could not determine file size from headers. Proceeding with download...');
}
} catch (headError) {
if (headError.code === 'FILE_TOO_LARGE') {
throw headError;
}
console.warn('HEAD request failed, proceeding with download:', headError.message);
}
async function fetchData (url, options) {
const reqOptions = Object.assign({ headers: { accept: 'image/* video/* text/* audio/*' } }, options);
const response = await fetch(url, reqOptions);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const mime = response.headers.get('Content-Type');
const size = response.headers.get('Content-Length');
const contentDisposition = response.headers.get('Content-Disposition');
const name = contentDisposition ? contentDisposition.match(/((?<=filename=")(.*)(?="))/) : null;
let data = '';
if (response.buffer) {
const buffer = await response.buffer();
const actualSizeMB = buffer.length / (1024 * 1024);
if (actualSizeMB > 64) {
const error = new Error(
`Downloaded file size (${actualSizeMB.toFixed(2)}MB) exceeds WhatsApp general limit (64MB)`
);
error.code = 'DOWNLOADED_FILE_TOO_LARGE';
error.actualSize = actualSizeMB;
throw error;
}
data = buffer.toString('base64');
} else {
const arrayBuffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
const actualSizeMB = uint8Array.length / (1024 * 1024);
if (actualSizeMB > 64) {
const error = new Error(
`Downloaded file size (${actualSizeMB.toFixed(2)}MB) exceeds WhatsApp general limit (64MB)`
);
error.code = 'DOWNLOADED_FILE_TOO_LARGE';
error.actualSize = actualSizeMB;
throw error;
}
let binary = '';
uint8Array.forEach((byte) => {
binary += String.fromCharCode(byte);
});
data = btoa(binary);
}
return { data, mime, name, size };
}
const res = options.client
? (await options.client.pupPage.evaluate(fetchData, url, options.reqOptions))
: (await fetchData(url, options.reqOptions));
const filename = options.filename ||
(res.name ? res.name[0] : (pUrl.pathname.split('/').pop() || 'file'));
if (!mimetype)
mimetype = res.mime;
const filesize = res.size ? parseInt(res.size) : null;
if (!res.data || !res.data.length) {
throw new Error('Downloaded file is empty or invalid');
}
const isValidBase64 = /^[A-Za-z0-9+/=]+$/.test(res.data);
if (!isValidBase64) {
throw new Error('Downloaded file data is not valid base64');
}
return new MessageMedia(mimetype, res.data, filename, filesize);
}
}
module.exports = MessageMedia;