Path: blob/master/node_modules/@adiwajshing/baileys/lib/Utils/messages-media.js
1129 views
"use strict";1var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {2if (k2 === undefined) k2 = k;3var desc = Object.getOwnPropertyDescriptor(m, k);4if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {5desc = { enumerable: true, get: function() { return m[k]; } };6}7Object.defineProperty(o, k2, desc);8}) : (function(o, m, k, k2) {9if (k2 === undefined) k2 = k;10o[k2] = m[k];11}));12var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {13Object.defineProperty(o, "default", { enumerable: true, value: v });14}) : function(o, v) {15o["default"] = v;16});17var __importStar = (this && this.__importStar) || function (mod) {18if (mod && mod.__esModule) return mod;19var result = {};20if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);21__setModuleDefault(result, mod);22return result;23};24Object.defineProperty(exports, "__esModule", { value: true });25exports.getStatusCodeForMediaRetry = exports.decryptMediaRetryData = exports.decodeMediaRetryNode = exports.encryptMediaRetryRequest = exports.getWAUploadToServer = exports.extensionForMediaMessage = exports.downloadEncryptedContent = exports.downloadContentFromMessage = exports.getUrlFromDirectPath = 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;26const boom_1 = require("@hapi/boom");27const child_process_1 = require("child_process");28const Crypto = __importStar(require("crypto"));29const events_1 = require("events");30const fs_1 = require("fs");31const os_1 = require("os");32const path_1 = require("path");33const stream_1 = require("stream");34const WAProto_1 = require("../../WAProto");35const Defaults_1 = require("../Defaults");36const WABinary_1 = require("../WABinary");37const crypto_1 = require("./crypto");38const generics_1 = require("./generics");39const getTmpFilesDirectory = () => (0, os_1.tmpdir)();40const getImageProcessingLibrary = async () => {41const [jimp, sharp] = await Promise.all([42(async () => {43const jimp = await (Promise.resolve().then(() => __importStar(require('jimp'))).catch(() => { }));44return jimp;45})(),46(async () => {47const sharp = await (Promise.resolve().then(() => __importStar(require('sharp'))).catch(() => { }));48return sharp;49})()50]);51if (sharp) {52return { sharp };53}54if (jimp) {55return { jimp };56}57throw new boom_1.Boom('No image processing library available');58};59const hkdfInfoKey = (type) => {60let str = type;61if (type === 'sticker') {62str = 'image';63}64if (type === 'md-app-state') {65str = 'App State';66}67const hkdfInfo = str[0].toUpperCase() + str.slice(1);68return `WhatsApp ${hkdfInfo} Keys`;69};70exports.hkdfInfoKey = hkdfInfoKey;71/** generates all the keys required to encrypt/decrypt & sign a media message */72function getMediaKeys(buffer, mediaType) {73if (!buffer) {74throw new boom_1.Boom('Cannot derive from empty media key');75}76if (typeof buffer === 'string') {77buffer = Buffer.from(buffer.replace('data:;base64,', ''), 'base64');78}79// expand using HKDF to 112 bytes, also pass in the relevant app info80const expandedMediaKey = (0, crypto_1.hkdf)(buffer, 112, { info: (0, exports.hkdfInfoKey)(mediaType) });81return {82iv: expandedMediaKey.slice(0, 16),83cipherKey: expandedMediaKey.slice(16, 48),84macKey: expandedMediaKey.slice(48, 80),85};86}87exports.getMediaKeys = getMediaKeys;88/** Extracts video thumb using FFMPEG */89const extractVideoThumb = async (path, destPath, time, size) => new Promise((resolve, reject) => {90const cmd = `ffmpeg -ss ${time} -i ${path} -y -vf scale=${size.width}:-1 -vframes 1 -f image2 ${destPath}`;91(0, child_process_1.exec)(cmd, (err) => {92if (err) {93reject(err);94}95else {96resolve();97}98});99});100const extractImageThumb = async (bufferOrFilePath, width = 32) => {101if (bufferOrFilePath instanceof stream_1.Readable) {102bufferOrFilePath = await (0, exports.toBuffer)(bufferOrFilePath);103}104const lib = await getImageProcessingLibrary();105if ('sharp' in lib) {106const result = await lib.sharp.default(bufferOrFilePath)107.resize(width)108.jpeg({ quality: 50 })109.toBuffer();110return result;111}112else {113const { read, MIME_JPEG, RESIZE_BILINEAR, AUTO } = lib.jimp;114const jimp = await read(bufferOrFilePath);115const result = await jimp116.quality(50)117.resize(width, AUTO, RESIZE_BILINEAR)118.getBufferAsync(MIME_JPEG);119return result;120}121};122exports.extractImageThumb = extractImageThumb;123const generateProfilePicture = async (mediaUpload) => {124let bufferOrFilePath;125if (Buffer.isBuffer(mediaUpload)) {126bufferOrFilePath = mediaUpload;127}128else if ('url' in mediaUpload) {129bufferOrFilePath = mediaUpload.url.toString();130}131else {132bufferOrFilePath = await (0, exports.toBuffer)(mediaUpload.stream);133}134const lib = await getImageProcessingLibrary();135let img;136if ('sharp' in lib) {137img = lib.sharp.default(bufferOrFilePath)138.resize(640, 640)139.jpeg({140quality: 50,141})142.toBuffer();143}144else {145const { read, MIME_JPEG, RESIZE_BILINEAR } = lib.jimp;146const jimp = await read(bufferOrFilePath);147const min = Math.min(jimp.getWidth(), jimp.getHeight());148const cropped = jimp.crop(0, 0, min, min);149img = cropped150.quality(50)151.resize(640, 640, RESIZE_BILINEAR)152.getBufferAsync(MIME_JPEG);153}154return {155img: await img,156};157};158exports.generateProfilePicture = generateProfilePicture;159/** gets the SHA256 of the given media message */160const mediaMessageSHA256B64 = (message) => {161const media = Object.values(message)[0];162return (media === null || media === void 0 ? void 0 : media.fileSha256) && Buffer.from(media.fileSha256).toString('base64');163};164exports.mediaMessageSHA256B64 = mediaMessageSHA256B64;165async function getAudioDuration(buffer) {166const musicMetadata = await Promise.resolve().then(() => __importStar(require('music-metadata')));167let metadata;168if (Buffer.isBuffer(buffer)) {169metadata = await musicMetadata.parseBuffer(buffer, undefined, { duration: true });170}171else if (typeof buffer === 'string') {172const rStream = (0, fs_1.createReadStream)(buffer);173metadata = await musicMetadata.parseStream(rStream, undefined, { duration: true });174rStream.close();175}176else {177metadata = await musicMetadata.parseStream(buffer, undefined, { duration: true });178}179return metadata.format.duration;180}181exports.getAudioDuration = getAudioDuration;182const toReadable = (buffer) => {183const readable = new stream_1.Readable({ read: () => { } });184readable.push(buffer);185readable.push(null);186return readable;187};188exports.toReadable = toReadable;189const toBuffer = async (stream) => {190let buff = Buffer.alloc(0);191for await (const chunk of stream) {192buff = Buffer.concat([buff, chunk]);193}194stream.destroy();195return buff;196};197exports.toBuffer = toBuffer;198const getStream = async (item) => {199if (Buffer.isBuffer(item)) {200return { stream: (0, exports.toReadable)(item), type: 'buffer' };201}202if ('stream' in item) {203return { stream: item.stream, type: 'readable' };204}205if (item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {206return { stream: await (0, exports.getHttpStream)(item.url), type: 'remote' };207}208return { stream: (0, fs_1.createReadStream)(item.url), type: 'file' };209};210exports.getStream = getStream;211/** generates a thumbnail for a given media, if required */212async function generateThumbnail(file, mediaType, options) {213var _a;214let thumbnail;215if (mediaType === 'image') {216const buff = await (0, exports.extractImageThumb)(file);217thumbnail = buff.toString('base64');218}219else if (mediaType === 'video') {220const imgFilename = (0, path_1.join)(getTmpFilesDirectory(), (0, generics_1.generateMessageID)() + '.jpg');221try {222await extractVideoThumb(file, imgFilename, '00:00:00', { width: 32, height: 32 });223const buff = await fs_1.promises.readFile(imgFilename);224thumbnail = buff.toString('base64');225await fs_1.promises.unlink(imgFilename);226}227catch (err) {228(_a = options.logger) === null || _a === void 0 ? void 0 : _a.debug('could not generate video thumb: ' + err);229}230}231return thumbnail;232}233exports.generateThumbnail = generateThumbnail;234const getHttpStream = async (url, options = {}) => {235const { default: axios } = await Promise.resolve().then(() => __importStar(require('axios')));236const fetched = await axios.get(url.toString(), { ...options, responseType: 'stream' });237return fetched.data;238};239exports.getHttpStream = getHttpStream;240const encryptedStream = async (media, mediaType, saveOriginalFileIfRequired = true, logger) => {241const { stream, type } = await (0, exports.getStream)(media);242logger === null || logger === void 0 ? void 0 : logger.debug('fetched media stream');243const mediaKey = Crypto.randomBytes(32);244const { cipherKey, iv, macKey } = getMediaKeys(mediaKey, mediaType);245// random name246//const encBodyPath = join(getTmpFilesDirectory(), mediaType + generateMessageID() + '.enc')247// const encWriteStream = createWriteStream(encBodyPath)248const encWriteStream = new stream_1.Readable({ read: () => { } });249let bodyPath;250let writeStream;251let didSaveToTmpPath = false;252if (type === 'file') {253bodyPath = media.url;254}255else if (saveOriginalFileIfRequired) {256bodyPath = (0, path_1.join)(getTmpFilesDirectory(), mediaType + (0, generics_1.generateMessageID)());257writeStream = (0, fs_1.createWriteStream)(bodyPath);258didSaveToTmpPath = true;259}260let fileLength = 0;261const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv);262let hmac = Crypto.createHmac('sha256', macKey).update(iv);263let sha256Plain = Crypto.createHash('sha256');264let sha256Enc = Crypto.createHash('sha256');265const onChunk = (buff) => {266sha256Enc = sha256Enc.update(buff);267hmac = hmac.update(buff);268encWriteStream.push(buff);269};270try {271for await (const data of stream) {272fileLength += data.length;273sha256Plain = sha256Plain.update(data);274if (writeStream) {275if (!writeStream.write(data)) {276await (0, events_1.once)(writeStream, 'drain');277}278}279onChunk(aes.update(data));280}281onChunk(aes.final());282const mac = hmac.digest().slice(0, 10);283sha256Enc = sha256Enc.update(mac);284const fileSha256 = sha256Plain.digest();285const fileEncSha256 = sha256Enc.digest();286encWriteStream.push(mac);287encWriteStream.push(null);288writeStream && writeStream.end();289stream.destroy();290logger === null || logger === void 0 ? void 0 : logger.debug('encrypted data successfully');291return {292mediaKey,293encWriteStream,294bodyPath,295mac,296fileEncSha256,297fileSha256,298fileLength,299didSaveToTmpPath300};301}302catch (error) {303encWriteStream.destroy(error);304writeStream === null || writeStream === void 0 ? void 0 : writeStream.destroy(error);305aes.destroy(error);306hmac.destroy(error);307sha256Plain.destroy(error);308sha256Enc.destroy(error);309stream.destroy(error);310throw error;311}312};313exports.encryptedStream = encryptedStream;314const DEF_HOST = 'mmg.whatsapp.net';315const AES_CHUNK_SIZE = 16;316const toSmallestChunkSize = (num) => {317return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;318};319const getUrlFromDirectPath = (directPath) => `https://${DEF_HOST}${directPath}`;320exports.getUrlFromDirectPath = getUrlFromDirectPath;321const downloadContentFromMessage = ({ mediaKey, directPath, url }, type, opts = {}) => {322const downloadUrl = url || (0, exports.getUrlFromDirectPath)(directPath);323const keys = getMediaKeys(mediaKey, type);324return (0, exports.downloadEncryptedContent)(downloadUrl, keys, opts);325};326exports.downloadContentFromMessage = downloadContentFromMessage;327/**328* Decrypts and downloads an AES256-CBC encrypted file given the keys.329* Assumes the SHA256 of the plaintext is appended to the end of the ciphertext330* */331const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, { startByte, endByte } = {}) => {332let bytesFetched = 0;333let startChunk = 0;334let firstBlockIsIV = false;335// if a start byte is specified -- then we need to fetch the previous chunk as that will form the IV336if (startByte) {337const chunk = toSmallestChunkSize(startByte || 0);338if (chunk) {339startChunk = chunk - AES_CHUNK_SIZE;340bytesFetched = chunk;341firstBlockIsIV = true;342}343}344const endChunk = endByte ? toSmallestChunkSize(endByte || 0) + AES_CHUNK_SIZE : undefined;345const headers = {346Origin: Defaults_1.DEFAULT_ORIGIN,347};348if (startChunk || endChunk) {349headers.Range = `bytes=${startChunk}-`;350if (endChunk) {351headers.Range += endChunk;352}353}354// download the message355const fetched = await (0, exports.getHttpStream)(downloadUrl, {356headers,357maxBodyLength: Infinity,358maxContentLength: Infinity,359});360let remainingBytes = Buffer.from([]);361let aes;362const pushBytes = (bytes, push) => {363if (startByte || endByte) {364const start = bytesFetched >= startByte ? undefined : Math.max(startByte - bytesFetched, 0);365const end = bytesFetched + bytes.length < endByte ? undefined : Math.max(endByte - bytesFetched, 0);366push(bytes.slice(start, end));367bytesFetched += bytes.length;368}369else {370push(bytes);371}372};373const output = new stream_1.Transform({374transform(chunk, _, callback) {375let data = Buffer.concat([remainingBytes, chunk]);376const decryptLength = toSmallestChunkSize(data.length);377remainingBytes = data.slice(decryptLength);378data = data.slice(0, decryptLength);379if (!aes) {380let ivValue = iv;381if (firstBlockIsIV) {382ivValue = data.slice(0, AES_CHUNK_SIZE);383data = data.slice(AES_CHUNK_SIZE);384}385aes = Crypto.createDecipheriv('aes-256-cbc', cipherKey, ivValue);386// if an end byte that is not EOF is specified387// stop auto padding (PKCS7) -- otherwise throws an error for decryption388if (endByte) {389aes.setAutoPadding(false);390}391}392try {393pushBytes(aes.update(data), b => this.push(b));394callback();395}396catch (error) {397callback(error);398}399},400final(callback) {401try {402pushBytes(aes.final(), b => this.push(b));403callback();404}405catch (error) {406callback(error);407}408},409});410return fetched.pipe(output, { end: true });411};412exports.downloadEncryptedContent = downloadEncryptedContent;413function extensionForMediaMessage(message) {414const getExtension = (mimetype) => mimetype.split(';')[0].split('/')[1];415const type = Object.keys(message)[0];416let extension;417if (type === 'locationMessage' ||418type === 'liveLocationMessage' ||419type === 'productMessage') {420extension = '.jpeg';421}422else {423const messageContent = message[type];424extension = getExtension(messageContent.mimetype);425}426return extension;427}428exports.extensionForMediaMessage = extensionForMediaMessage;429const getWAUploadToServer = ({ customUploadHosts, fetchAgent, logger }, refreshMediaConn) => {430return async (stream, { mediaType, fileEncSha256B64, timeoutMs }) => {431var _a, _b;432const { default: axios } = await Promise.resolve().then(() => __importStar(require('axios')));433// send a query JSON to obtain the url & auth token to upload our media434let uploadInfo = await refreshMediaConn(false);435let urls;436const hosts = [...customUploadHosts, ...uploadInfo.hosts];437const chunks = [];438for await (const chunk of stream) {439chunks.push(chunk);440}441const reqBody = Buffer.concat(chunks);442for (const { hostname, maxContentLengthBytes } of hosts) {443logger.debug(`uploading to "${hostname}"`);444const auth = encodeURIComponent(uploadInfo.auth); // the auth token445const url = `https://${hostname}${Defaults_1.MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`;446let result;447try {448if (maxContentLengthBytes && reqBody.length > maxContentLengthBytes) {449throw new boom_1.Boom(`Body too large for "${hostname}"`, { statusCode: 413 });450}451const body = await axios.post(url, reqBody, {452headers: {453'Content-Type': 'application/octet-stream',454'Origin': Defaults_1.DEFAULT_ORIGIN455},456httpsAgent: fetchAgent,457timeout: timeoutMs,458responseType: 'json',459maxBodyLength: Infinity,460maxContentLength: Infinity,461});462result = body.data;463if ((result === null || result === void 0 ? void 0 : result.url) || (result === null || result === void 0 ? void 0 : result.directPath)) {464urls = {465mediaUrl: result.url,466directPath: result.direct_path467};468break;469}470else {471uploadInfo = await refreshMediaConn(true);472throw new Error(`upload failed, reason: ${JSON.stringify(result)}`);473}474}475catch (error) {476if (axios.isAxiosError(error)) {477result = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data;478}479const isLast = hostname === ((_b = hosts[uploadInfo.hosts.length - 1]) === null || _b === void 0 ? void 0 : _b.hostname);480logger.warn({ trace: error.stack, uploadResult: result }, `Error in uploading to ${hostname} ${isLast ? '' : ', retrying...'}`);481}482}483if (!urls) {484throw new boom_1.Boom('Media upload failed on all hosts', { statusCode: 500 });485}486return urls;487};488};489exports.getWAUploadToServer = getWAUploadToServer;490const getMediaRetryKey = (mediaKey) => {491return (0, crypto_1.hkdf)(mediaKey, 32, { info: 'WhatsApp Media Retry Notification' });492};493/**494* Generate a binary node that will request the phone to re-upload the media & return the newly uploaded URL495*/496const encryptMediaRetryRequest = (key, mediaKey, meId) => {497const recp = { stanzaId: key.id };498const recpBuffer = WAProto_1.proto.ServerErrorReceipt.encode(recp).finish();499const iv = Crypto.randomBytes(12);500const retryKey = getMediaRetryKey(mediaKey);501const ciphertext = (0, crypto_1.aesEncryptGCM)(recpBuffer, retryKey, iv, Buffer.from(key.id));502const req = {503tag: 'receipt',504attrs: {505id: key.id,506to: (0, WABinary_1.jidNormalizedUser)(meId),507type: 'server-error'508},509content: [510// this encrypt node is actually pretty useless511// the media is returned even without this node512// keeping it here to maintain parity with WA Web513{514tag: 'encrypt',515attrs: {},516content: [517{ tag: 'enc_p', attrs: {}, content: ciphertext },518{ tag: 'enc_iv', attrs: {}, content: iv }519]520},521{522tag: 'rmr',523attrs: {524jid: key.remoteJid,525from_me: (!!key.fromMe).toString(),526// @ts-ignore527participant: key.participant || undefined528}529}530]531};532return req;533};534exports.encryptMediaRetryRequest = encryptMediaRetryRequest;535const decodeMediaRetryNode = (node) => {536const rmrNode = (0, WABinary_1.getBinaryNodeChild)(node, 'rmr');537const event = {538key: {539id: node.attrs.id,540remoteJid: rmrNode.attrs.jid,541fromMe: rmrNode.attrs.from_me === 'true',542participant: rmrNode.attrs.participant543}544};545const errorNode = (0, WABinary_1.getBinaryNodeChild)(node, 'error');546if (errorNode) {547const errorCode = +errorNode.attrs.code;548event.error = new boom_1.Boom(`Failed to re-upload media (${errorCode})`, { data: errorNode.attrs, statusCode: (0, exports.getStatusCodeForMediaRetry)(errorCode) });549}550else {551const encryptedInfoNode = (0, WABinary_1.getBinaryNodeChild)(node, 'encrypt');552const ciphertext = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_p');553const iv = (0, WABinary_1.getBinaryNodeChildBuffer)(encryptedInfoNode, 'enc_iv');554if (ciphertext && iv) {555event.media = { ciphertext, iv };556}557else {558event.error = new boom_1.Boom('Failed to re-upload media (missing ciphertext)', { statusCode: 404 });559}560}561return event;562};563exports.decodeMediaRetryNode = decodeMediaRetryNode;564const decryptMediaRetryData = ({ ciphertext, iv }, mediaKey, msgId) => {565const retryKey = getMediaRetryKey(mediaKey);566const plaintext = (0, crypto_1.aesDecryptGCM)(ciphertext, retryKey, iv, Buffer.from(msgId));567return WAProto_1.proto.MediaRetryNotification.decode(plaintext);568};569exports.decryptMediaRetryData = decryptMediaRetryData;570const getStatusCodeForMediaRetry = (code) => MEDIA_RETRY_STATUS_MAP[code];571exports.getStatusCodeForMediaRetry = getStatusCodeForMediaRetry;572const MEDIA_RETRY_STATUS_MAP = {573[WAProto_1.proto.MediaRetryNotification.ResultType.SUCCESS]: 200,574[WAProto_1.proto.MediaRetryNotification.ResultType.DECRYPTION_ERROR]: 412,575[WAProto_1.proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,576[WAProto_1.proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418,577};578579580