Path: blob/master/node_modules/@adiwajshing/baileys/lib/Utils/messages-media.js
2593 views
"use strict";1var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {2if (k2 === undefined) k2 = k;3Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });4}) : (function(o, m, k, k2) {5if (k2 === undefined) k2 = k;6o[k2] = m[k];7}));8var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {9Object.defineProperty(o, "default", { enumerable: true, value: v });10}) : function(o, v) {11o["default"] = v;12});13var __importStar = (this && this.__importStar) || function (mod) {14if (mod && mod.__esModule) return mod;15var result = {};16if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);17__setModuleDefault(result, mod);18return result;19};20Object.defineProperty(exports, "__esModule", { value: true });21exports.getWAUploadToServer = exports.extensionForMediaMessage = exports.decryptMediaMessageBuffer = exports.downloadContentFromMessage = exports.encryptedStream = exports.getHttpStream = exports.generateThumbnail = exports.getStream = exports.toBuffer = exports.toReadable = exports.getAudioDuration = exports.mediaMessageSHA256B64 = exports.generateProfilePicture = exports.extractImageThumb = exports.getMediaKeys = exports.hkdfInfoKey = void 0;22const boom_1 = require("@hapi/boom");23const child_process_1 = require("child_process");24const Crypto = __importStar(require("crypto"));25const events_1 = require("events");26const fs_1 = require("fs");27const os_1 = require("os");28const path_1 = require("path");29const stream_1 = require("stream");30const Defaults_1 = require("../Defaults");31const crypto_1 = require("./crypto");32const generics_1 = require("./generics");33const getTmpFilesDirectory = () => os_1.tmpdir();34const getImageProcessingLibrary = async () => {35const [jimp, sharp] = await Promise.all([36(async () => {37const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }));38return jimp;39})(),40(async () => {41const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }));42return sharp;43})()44]);45if (sharp) {46return { sharp };47}48if (jimp) {49return { jimp };50}51throw new boom_1.Boom('No image processing library available');52};53const hkdfInfoKey = (type) => {54let str = type;55if (type === 'sticker') {56str = 'image';57}58if (type === 'md-app-state') {59str = 'App State';60}61const hkdfInfo = str[0].toUpperCase() + str.slice(1);62return `WhatsApp ${hkdfInfo} Keys`;63};64exports.hkdfInfoKey = hkdfInfoKey;65/** generates all the keys required to encrypt/decrypt & sign a media message */66function getMediaKeys(buffer, mediaType) {67if (typeof buffer === 'string') {68buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');69}70// expand using HKDF to 112 bytes, also pass in the relevant app info71const expandedMediaKey = crypto_1.hkdf(buffer, 112, { info: exports.hkdfInfoKey(mediaType) });72return {73iv: expandedMediaKey.slice(0, 16),74cipherKey: expandedMediaKey.slice(16, 48),75macKey: expandedMediaKey.slice(48, 80),76};77}78exports.getMediaKeys = getMediaKeys;79/** Extracts video thumb using FFMPEG */80const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {81const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`;82child_process_1.exec(cmd, (err) => {83if (err) {84reject(err);85}86else {87resolve();88}89});90});91const extractImageThumb = async (bufferOrFilePath) => {92if (bufferOrFilePath instanceof stream_1.Readable) {93bufferOrFilePath = await exports.toBuffer(bufferOrFilePath);94}95const lib = await getImageProcessingLibrary();96if ('sharp' in lib) {97const result = await lib.sharp.default(bufferOrFilePath)98.resize(32, 32)99.jpeg({ quality: 50 })100.toBuffer();101return result;102}103else {104const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp;105const jimp = await read(bufferOrFilePath);106const result = await jimp107.quality(50)108.resize(32, 32, RESIZE_BILINEAR)109.getBufferAsync(MIME_JPEG);110return result;111}112};113exports.extractImageThumb = extractImageThumb;114const generateProfilePicture = async (mediaUpload) => {115let bufferOrFilePath;116if (Buffer.isBuffer(mediaUpload)) {117bufferOrFilePath = mediaUpload;118}119else if ('url' in mediaUpload) {120bufferOrFilePath = mediaUpload.url.toString();121}122else {123bufferOrFilePath = await exports.toBuffer(mediaUpload.stream);124}125const lib = await getImageProcessingLibrary();126let img;127if ('sharp' in lib) {128img = lib.sharp.default(bufferOrFilePath)129.resize(640, 640)130.jpeg({131quality: 50,132})133.toBuffer();134}135else {136const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp;137const jimp = await read(bufferOrFilePath);138const min = Math.min(jimp.getWidth(), jimp.getHeight());139const cropped = jimp.crop(0, 0, min, min);140img = cropped141.quality(50)142.resize(640, 640, RESIZE_BILINEAR)143.getBufferAsync(MIME_JPEG);144}145return {146img: await img,147};148};149exports.generateProfilePicture = generateProfilePicture;150/** gets the SHA256 of the given media message */151const mediaMessageSHA256B64 = (message) => {152const media = Object.values(message)[0];153return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64');154};155exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;156async function getAudioDuration(buffer) {157const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')));158let metadata;159if (Buffer.isBuffer(buffer)) {160metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true });161}162else if (typeof buffer === 'string') {163const rStream = fs_1.createReadStream(buffer);164metadata = await musicMetadata.parseStream(rStream, null, { duration: true });165rStream.close();166}167else {168metadata = await musicMetadata.parseStream(buffer, null, { duration: true });169}170return metadata.format.duration;171}172exports.getAudioDuration = getAudioDuration;173const toReadable = (buffer) => {174const readable = new stream_1.Readable({ read: () => { } });175readable.push(buffer);176readable.push(null);177return readable;178};179exports.toReadable = toReadable;180const toBuffer = async (stream) => {181let buff = Buffer.alloc(0);182for await (const chunk of stream) {183buff = Buffer.concat([buff, chunk]);184}185return buff;186};187exports.toBuffer = toBuffer;188const getStream = async (item) => {189if (Buffer.isBuffer(item)) {190return { stream: exports.toReadable(item), type: 'buffer' };191}192if ('stream' in item) {193return { stream: item.stream, type: 'readable' };194}195if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {196return { stream: await exports.getHttpStream(item.url), type: 'remote' };197}198return { stream: fs_1.createReadStream(item.url), type: 'file' };199};200exports.getStream = getStream;201/** generates a thumbnail for a given media, if required */202async function generateThumbnail(file, mediaType, options) {203var _a;204let thumbnail;205if (mediaType === 'image') {206const buff = await exports.extractImageThumb(file);207thumbnail = buff.toString('base64');208}209else if (mediaType === 'video') {210const imgFilename = path_1.join(getTmpFilesDirectory(), generics_1.generateMessageID() + '.jpg');211try {212await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 });213const buff = await fs_1.promises.readFile(imgFilename);214thumbnail = buff.toString('base64');215await fs_1.promises.unlink(imgFilename);216}217catch (err) {218(_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);219}220}221return thumbnail;222}223exports.generateThumbnail = generateThumbnail;224const getHttpStream = async (url, options = {}) => {225const { default: axios } = await Promise.resolve().then(() => __importStar(require('axios')));226const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' });227return fetched.data;228};229exports.getHttpStream = getHttpStream;230const encryptedStream = async (media, mediaType, saveOriginalFileIfRequired = true, logger) => {231const { stream, type } = await exports.getStream(media);232logger === null || logger === void 0 ? void 0 : logger.debug('fetched media stream');233const mediaKey = Crypto.randomBytes(32);234const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType);235// random name236//const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc')237// const encWriteStream = createWriteStream(encBodyPath)238const encWriteStream = new stream_1.Readable({ read: () => { } });239let bodyPath;240let writeStream;241let didSaveToTmpPath = false;242if (type === 'file') {243bodyPath = media.url;244}245else if (saveOriginalFileIfRequired) {246bodyPath = path_1.join(getTmpFilesDirectory(), mediaType + generics_1.generateMessageID());247writeStream = fs_1.createWriteStream(bodyPath);248didSaveToTmpPath = true;249}250let fileLength = 0;251const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);252let hmac = Crypto.createHmac('sha256', macKey).update(iv);253let sha256Plain = Crypto.createHash('sha256');254let sha256Enc = Crypto.createHash('sha256');255const onChunk = (buff) => {256sha256Enc = sha256Enc.update(buff);257hmac = hmac.update(buff);258encWriteStream.push(buff);259};260try {261for await (const data of stream) {262fileLength += data.length;263sha256Plain = sha256Plain.update(data);264if (writeStream) {265if (!writeStream.write(data)) {266await events_1.once(writeStream, 'drain');267}268}269onChunk(aes.update(data));270}271onChunk(aes.final());272const mac = hmac.digest().slice(0, 10);273sha256Enc = sha256Enc.update(mac);274const fileSha256 = sha256Plain.digest();275const fileEncSha256 = sha256Enc.digest();276encWriteStream.push(mac);277encWriteStream.push(null);278writeStream && writeStream.end();279stream.destroy();280logger === null || logger === void 0 ? void 0 : logger.debug('encrypted data successfully');281return {282mediaKey,283encWriteStream,284bodyPath,285mac,286fileEncSha256,287fileSha256,288fileLength,289didSaveToTmpPath290};291}292catch (error) {293encWriteStream.destroy(error);294writeStream.destroy(error);295aes.destroy(error);296hmac.destroy(error);297sha256Plain.destroy(error);298sha256Enc.destroy(error);299stream.destroy(error);300throw error;301}302};303exports.encryptedStream = encryptedStream;304const DEF_HOST = 'mmg.whatsapp.net';305const AES_CHUNK_SIZE = 16;306const toSmallestChunkSize = (num) => {307return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;308};309const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, { startByte, endByte } = {}) => {310const downloadUrl = url || `https://${DEF_HOST}${directPath}`;311let bytesFetched = 0;312let startChunk = 0;313let firstBlockIsIV = false;314// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV315if (startByte) {316const chunk = toSmallestChunkSize(startByte || 0);317if (chunk) {318startChunk = chunk - AES_CHUNK_SIZE;319bytesFetched = chunk;320firstBlockIsIV = true;321}322}323const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;324const headers = {325Origin: Defaults_1.DEFAULT_ORIGIN,326};327if (startChunk || endChunk) {328headers.Range = `bytes=${startChunk}-`;329if (endChunk) {330headers.Range += endChunk;331}332}333// download the message334const fetched = await exports.getHttpStream(downloadUrl, {335headers,336maxBodyLength: Infinity,337maxContentLength: Infinity,338});339let remainingBytes = Buffer.from([]);340const { cipherKey, iv } = getMediaKeys(mediaKey, type);341let aes;342const pushBytes = (bytes, push) => {343if (startByte || endByte) {344const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);345const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0);346push(bytes.slice(start, end));347bytesFetched += bytes.length;348}349else {350push(bytes);351}352};353const output = new stream_1.Transform({354transform(chunk, _, callback) {355let data = Buffer.concat([remainingBytes, chunk]);356const decryptLength = toSmallestChunkSize(data.length);357remainingBytes = data.slice(decryptLength);358data = data.slice(0, decryptLength);359if (!aes) {360let ivValue = iv;361if (firstBlockIsIV) {362ivValue = data.slice(0, AES_CHUNK_SIZE);363data = data.slice(AES_CHUNK_SIZE);364}365aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);366// if an end byte that is not EOF is specified367// stop auto padding (PKCS7) -- otherwise throws an error for decryption368if (endByte) {369aes.setAutoPadding(false);370}371}372try {373pushBytes(aes.update(data), b => this.push(b));374callback();375}376catch (error) {377callback(error);378}379},380final(callback) {381try {382pushBytes(aes.final(), b => this.push(b));383callback();384}385catch (error) {386callback(error);387}388},389});390return fetched.pipe(output, { end: true });391};392exports.downloadContentFromMessage = downloadContentFromMessage;393/**394* Decode a media message (video, image, document, audio) & return decrypted buffer395* @param message the media message you want to decode396*/397async function decryptMediaMessageBuffer(message) {398var _a;399/*400One can infer media type from the key in the message401it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.402*/403const type = Object.keys(message)[0];404if (!type ||405type === 'conversation' ||406type === 'extendedTextMessage') {407throw new boom_1.Boom(`no media message for "${type}"`, { statusCode: 400 });408}409if (type === 'locationMessage' || type === 'liveLocationMessage') {410const buffer = Buffer.from(message[type].jpegThumbnail);411const readable = new stream_1.Readable({ read: () => { } });412readable.push(buffer);413readable.push(null);414return readable;415}416let messageContent;417if (message.productMessage) {418const product = (_a = message.productMessage.product) === null || _a === void 0 ? void 0 : _a.productImage;419if (!product) {420throw new boom_1.Boom('product has no image', { statusCode: 400 });421}422messageContent = product;423}424else {425messageContent = message[type];426}427return exports.downloadContentFromMessage(messageContent, type.replace('Message', ''));428}429exports.decryptMediaMessageBuffer = decryptMediaMessageBuffer;430function extensionForMediaMessage(message) {431const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1];432const type = Object.keys(message)[0];433let extension;434if (type === 'locationMessage' ||435type === 'liveLocationMessage' ||436type === 'productMessage') {437extension = '.jpeg';438}439else {440const messageContent = message[type];441extension = getExtension(messageContent.mimetype);442}443return extension;444}445exports.extensionForMediaMessage = extensionForMediaMessage;446const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }, refreshMediaConn) => {447return async (stream, { mediaType, fileEncSha256B64, timeoutMs }) => {448var _a, _b;449const { default: axios } = await Promise.resolve().then(() => __importStar(require('axios')));450// send a query JSON to obtain the url & auth token to upload our media451let uploadInfo = await refreshMediaConn(false);452let urls;453const hosts = [...customUploadHosts, ...uploadInfo.hosts];454const chunks = [];455for await (const chunk of stream) {456chunks.push(chunk);457}458let reqBody = Buffer.concat(chunks);459for (const { hostname, maxContentLengthBytes } of hosts) {460logger.debug(`uploading to "${hostname}"`);461const auth = encodeURIComponent(uploadInfo.auth); // the auth token462const url = `https://${hostname}${Defaults_1.MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;463let result;464try {465if (maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {466throw new boom_1.Boom(`Body too large for "${hostname}"`, { statusCode: 413 });467}468const body = await axios.post(url, reqBody, {469headers: {470'Content-Type': 'application/octet-stream',471'Origin': Defaults_1.DEFAULT_ORIGIN472},473httpsAgent: fetchAgent,474timeout: timeoutMs,475responseType: 'json',476maxBodyLength: Infinity,477maxContentLength: Infinity,478});479result = body.data;480if ((result === null || result === void 0 ? void 0 : result.url) || (result === null || result === void 0 ? void 0 : result.directPath)) {481urls = {482mediaUrl: result.url,483directPath: result.direct_path484};485break;486}487else {488uploadInfo = await refreshMediaConn(true);489throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);490}491}492catch (error) {493if (axios.isAxiosError(error)) {494result = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;495}496const isLast = hostname === ((_b = hosts[uploadInfo.hosts.length - 1]) === null || _b === void 0 ? void 0 : _b.hostname);497logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);498}499}500// clear buffer just to be sure we're releasing the memory501reqBody = undefined;502if (!urls) {503throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 });504}505return urls;506};507};508exports.getWAUploadToServer = getWAUploadToServer;509510511