Path: blob/master/node_modules/@adiwajshing/baileys/lib/Utils/messages.js
1129 views
"use strict";1var __importDefault = (this && this.__importDefault) || function (mod) {2return (mod && mod.__esModule) ? mod : { "default": mod };3};4Object.defineProperty(exports, "__esModule", { value: true });5exports.patchMessageForMdIfRequired = exports.assertMediaContent = exports.downloadMediaMessage = exports.aggregateMessageKeysNotFromMe = exports.updateMessageWithReaction = exports.updateMessageWithReceipt = exports.getDevice = exports.extractMessageContent = exports.normalizeMessageContent = exports.getContentType = exports.generateWAMessage = exports.generateWAMessageFromContent = exports.generateWAMessageContent = exports.generateForwardMessageContent = exports.prepareDisappearingMessageSettingContent = exports.prepareWAMessageMedia = exports.generateLinkPreviewIfRequired = exports.extractUrlFromText = void 0;6const boom_1 = require("@hapi/boom");7const axios_1 = __importDefault(require("axios"));8const fs_1 = require("fs");9const WAProto_1 = require("../../WAProto");10const Defaults_1 = require("../Defaults");11const Types_1 = require("../Types");12const WABinary_1 = require("../WABinary");13const generics_1 = require("./generics");14const messages_media_1 = require("./messages-media");15const MIMETYPE_MAP = {16image: 'image/jpeg',17video: 'video/mp4',18document: 'application/pdf',19audio: 'audio/ogg; codecs=opus',20sticker: 'image/webp',21history: 'application/x-protobuf',22'md-app-state': 'application/x-protobuf',23};24const MessageTypeProto = {25'image': Types_1.WAProto.Message.ImageMessage,26'video': Types_1.WAProto.Message.VideoMessage,27'audio': Types_1.WAProto.Message.AudioMessage,28'sticker': Types_1.WAProto.Message.StickerMessage,29'document': Types_1.WAProto.Message.DocumentMessage,30};31const ButtonType = WAProto_1.proto.Message.ButtonsMessage.HeaderType;32/**33* Uses a regex to test whether the string contains a URL, and returns the URL if it does.34* @param text eg. hello https://google.com35* @returns the URL, eg. https://google.com36*/37const extractUrlFromText = (text) => {38var _a;39return (!Defaults_1.URL_EXCLUDE_REGEX.test(text) ? (_a = text.match(Defaults_1.URL_REGEX)) === null || _a === void 0 ? void 0 : _a[0] : undefined);40};41exports.extractUrlFromText = extractUrlFromText;42const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {43const url = (0, exports.extractUrlFromText)(text);44if (!!getUrlInfo && url) {45try {46const urlInfo = await getUrlInfo(url);47return urlInfo;48}49catch (error) { // ignore if fails50logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'url generation failed');51}52}53};54exports.generateLinkPreviewIfRequired = generateLinkPreviewIfRequired;55const prepareWAMessageMedia = async (message, options) => {56const logger = options.logger;57let mediaType;58for (const key of Defaults_1.MEDIA_KEYS) {59if (key in message) {60mediaType = key;61}62}63if (!mediaType) {64throw new boom_1.Boom('Invalid media type', { statusCode: 400 });65}66const uploadData = {67...message,68media: message[mediaType]69};70delete uploadData[mediaType];71// check if cacheable + generate cache key72const cacheableKey = typeof uploadData.media === 'object' &&73('url' in uploadData.media) &&74!!uploadData.media.url &&75!!options.mediaCache && (76// generate the key77mediaType + ':' + uploadData.media.url.toString());78if (mediaType === 'document' && !uploadData.fileName) {79uploadData.fileName = 'file';80}81if (!uploadData.mimetype) {82uploadData.mimetype = MIMETYPE_MAP[mediaType];83}84// check for cache hit85if (cacheableKey) {86const mediaBuff = options.mediaCache.get(cacheableKey);87if (mediaBuff) {88logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'got media cache hit');89const obj = Types_1.WAProto.Message.decode(mediaBuff);90const key = `${mediaType}Message`;91Object.assign(obj[key], { ...uploadData, media: undefined });92return obj;93}94}95const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';96const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') &&97(typeof uploadData['jpegThumbnail'] === 'undefined');98const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;99const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath } = await (0, messages_media_1.encryptedStream)(uploadData.media, mediaType, requiresOriginalForSomeProcessing);100// url safe Base64 encode the SHA256 hash of the body101const fileEncSha256B64 = encodeURIComponent(fileEncSha256.toString('base64')102.replace(/\+/g, '-')103.replace(/\//g, '_')104.replace(/\=+$/, ''));105const [{ mediaUrl, directPath }] = await Promise.all([106(async () => {107const result = await options.upload(encWriteStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs });108logger === null || logger === void 0 ? void 0 : logger.debug({ mediaType, cacheableKey }, 'uploaded media');109return result;110})(),111(async () => {112try {113if (requiresThumbnailComputation) {114uploadData.jpegThumbnail = await (0, messages_media_1.generateThumbnail)(bodyPath, mediaType, options);115logger === null || logger === void 0 ? void 0 : logger.debug('generated thumbnail');116}117if (requiresDurationComputation) {118uploadData.seconds = await (0, messages_media_1.getAudioDuration)(bodyPath);119logger === null || logger === void 0 ? void 0 : logger.debug('computed audio duration');120}121}122catch (error) {123logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'failed to obtain extra info');124}125})(),126])127.finally(async () => {128encWriteStream.destroy();129// remove tmp files130if (didSaveToTmpPath && bodyPath) {131await fs_1.promises.unlink(bodyPath);132logger === null || logger === void 0 ? void 0 : logger.debug('removed tmp files');133}134});135const obj = Types_1.WAProto.Message.fromObject({136[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({137url: mediaUrl,138directPath,139mediaKey,140fileEncSha256,141fileSha256,142fileLength,143mediaKeyTimestamp: (0, generics_1.unixTimestampSeconds)(),144...uploadData,145media: undefined146})147});148if (cacheableKey) {149logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'set cache');150options.mediaCache.set(cacheableKey, Types_1.WAProto.Message.encode(obj).finish());151}152return obj;153};154exports.prepareWAMessageMedia = prepareWAMessageMedia;155const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {156ephemeralExpiration = ephemeralExpiration || 0;157const content = {158ephemeralMessage: {159message: {160protocolMessage: {161type: Types_1.WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,162ephemeralExpiration163}164}165}166};167return Types_1.WAProto.Message.fromObject(content);168};169exports.prepareDisappearingMessageSettingContent = prepareDisappearingMessageSettingContent;170/**171* Generate forwarded message content like WA does172* @param message the message to forward173* @param options.forceForward will show the message as forwarded even if it is from you174*/175const generateForwardMessageContent = (message, forceForward) => {176var _a;177let content = message.message;178if (!content) {179throw new boom_1.Boom('no content in message', { statusCode: 400 });180}181// hacky copy182content = (0, exports.normalizeMessageContent)(content);183content = WAProto_1.proto.Message.decode(WAProto_1.proto.Message.encode(content).finish());184let key = Object.keys(content)[0];185let score = ((_a = content[key].contextInfo) === null || _a === void 0 ? void 0 : _a.forwardingScore) || 0;186score += message.key.fromMe && !forceForward ? 0 : 1;187if (key === 'conversation') {188content.extendedTextMessage = { text: content[key] };189delete content.conversation;190key = 'extendedTextMessage';191}192if (score > 0) {193content[key].contextInfo = { forwardingScore: score, isForwarded: true };194}195else {196content[key].contextInfo = {};197}198return content;199};200exports.generateForwardMessageContent = generateForwardMessageContent;201const generateWAMessageContent = async (message, options) => {202var _a;203let m = {};204if ('text' in message) {205const extContent = { text: message.text };206let urlInfo = message.linkPreview;207if (typeof urlInfo === 'undefined') {208urlInfo = await (0, exports.generateLinkPreviewIfRequired)(message.text, options.getUrlInfo, options.logger);209}210if (urlInfo) {211extContent.canonicalUrl = urlInfo['canonical-url'];212extContent.matchedText = urlInfo['matched-text'];213extContent.jpegThumbnail = urlInfo.jpegThumbnail;214extContent.description = urlInfo.description;215extContent.title = urlInfo.title;216extContent.previewType = 0;217}218m.extendedTextMessage = extContent;219}220else if ('contacts' in message) {221const contactLen = message.contacts.contacts.length;222if (!contactLen) {223throw new boom_1.Boom('require atleast 1 contact', { statusCode: 400 });224}225if (contactLen === 1) {226m.contactMessage = Types_1.WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0]);227}228else {229m.contactsArrayMessage = Types_1.WAProto.Message.ContactsArrayMessage.fromObject(message.contacts);230}231}232else if ('location' in message) {233m.locationMessage = Types_1.WAProto.Message.LocationMessage.fromObject(message.location);234}235else if ('react' in message) {236if (!message.react.senderTimestampMs) {237message.react.senderTimestampMs = Date.now();238}239m.reactionMessage = Types_1.WAProto.Message.ReactionMessage.fromObject(message.react);240}241else if ('delete' in message) {242m.protocolMessage = {243key: message.delete,244type: Types_1.WAProto.Message.ProtocolMessage.Type.REVOKE245};246}247else if ('forward' in message) {248m = (0, exports.generateForwardMessageContent)(message.forward, message.force);249}250else if ('disappearingMessagesInChat' in message) {251const exp = typeof message.disappearingMessagesInChat === 'boolean' ?252(message.disappearingMessagesInChat ? Defaults_1.WA_DEFAULT_EPHEMERAL : 0) :253message.disappearingMessagesInChat;254m = (0, exports.prepareDisappearingMessageSettingContent)(exp);255}256else if ('buttonReply' in message) {257switch (message.type) {258case 'template':259m.templateButtonReplyMessage = {260selectedDisplayText: message.buttonReply.displayText,261selectedId: message.buttonReply.id,262selectedIndex: message.buttonReply.index,263};264break;265case 'plain':266m.buttonsResponseMessage = {267selectedButtonId: message.buttonReply.id,268selectedDisplayText: message.buttonReply.displayText,269type: WAProto_1.proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,270};271break;272}273}274else if ('product' in message) {275const { imageMessage } = await (0, exports.prepareWAMessageMedia)({ image: message.product.productImage }, options);276m.productMessage = Types_1.WAProto.Message.ProductMessage.fromObject({277...message,278product: {279...message.product,280productImage: imageMessage,281}282});283}284else {285m = await (0, exports.prepareWAMessageMedia)(message, options);286}287if ('buttons' in message && !!message.buttons) {288const buttonsMessage = {289buttons: message.buttons.map(b => ({ ...b, type: WAProto_1.proto.Message.ButtonsMessage.Button.Type.RESPONSE }))290};291if ('text' in message) {292buttonsMessage.contentText = message.text;293buttonsMessage.headerType = ButtonType.EMPTY;294}295else {296if ('caption' in message) {297buttonsMessage.contentText = message.caption;298}299const type = Object.keys(m)[0].replace('Message', '').toUpperCase();300buttonsMessage.headerType = ButtonType[type];301Object.assign(buttonsMessage, m);302}303if ('footer' in message && !!message.footer) {304buttonsMessage.footerText = message.footer;305}306m = { buttonsMessage };307}308else if ('templateButtons' in message && !!message.templateButtons) {309const msg = {310hydratedButtons: message.templateButtons311};312if ('text' in message) {313msg.hydratedContentText = message.text;314}315else {316if ('caption' in message) {317msg.hydratedContentText = message.caption;318}319Object.assign(msg, m);320}321if ('footer' in message && !!message.footer) {322msg.hydratedFooterText = message.footer;323}324m = {325templateMessage: {326fourRowTemplate: msg,327hydratedTemplate: msg328}329};330}331if ('sections' in message && !!message.sections) {332const listMessage = {333sections: message.sections,334buttonText: message.buttonText,335title: message.title,336footerText: message.footer,337description: message.text,338listType: WAProto_1.proto.Message.ListMessage.ListType.SINGLE_SELECT339};340m = { listMessage };341}342if ('viewOnce' in message && !!message.viewOnce) {343m = { viewOnceMessage: { message: m } };344}345if ('mentions' in message && ((_a = message.mentions) === null || _a === void 0 ? void 0 : _a.length)) {346const [messageType] = Object.keys(m);347m[messageType].contextInfo = m[messageType] || {};348m[messageType].contextInfo.mentionedJid = message.mentions;349}350return Types_1.WAProto.Message.fromObject(m);351};352exports.generateWAMessageContent = generateWAMessageContent;353const generateWAMessageFromContent = (jid, message, options) => {354if (!options.timestamp) {355options.timestamp = new Date();356} // set timestamp to now357const key = Object.keys(message)[0];358const timestamp = (0, generics_1.unixTimestampSeconds)(options.timestamp);359const { quoted, userJid } = options;360if (quoted) {361const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid);362let quotedMsg = (0, exports.normalizeMessageContent)(quoted.message);363const msgType = (0, exports.getContentType)(quotedMsg);364// strip any redundant properties365quotedMsg = WAProto_1.proto.Message.fromObject({ [msgType]: quotedMsg[msgType] });366const quotedContent = quotedMsg[msgType];367if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {368delete quotedContent.contextInfo;369}370const contextInfo = message[key].contextInfo || {};371contextInfo.participant = (0, WABinary_1.jidNormalizedUser)(participant);372contextInfo.stanzaId = quoted.key.id;373contextInfo.quotedMessage = quotedMsg;374// if a participant is quoted, then it must be a group375// hence, remoteJid of group must also be entered376if (quoted.key.participant || quoted.participant) {377contextInfo.remoteJid = quoted.key.remoteJid;378}379message[key].contextInfo = contextInfo;380}381if (382// if we want to send a disappearing message383!!(options === null || options === void 0 ? void 0 : options.ephemeralExpiration) &&384// and it's not a protocol message -- delete, toggle disappear message385key !== 'protocolMessage' &&386// already not converted to disappearing message387key !== 'ephemeralMessage') {388message[key].contextInfo = {389...(message[key].contextInfo || {}),390expiration: options.ephemeralExpiration || Defaults_1.WA_DEFAULT_EPHEMERAL,391//ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()392};393message = {394ephemeralMessage: {395message396}397};398}399message = Types_1.WAProto.Message.fromObject(message);400const messageJSON = {401key: {402remoteJid: jid,403fromMe: true,404id: (options === null || options === void 0 ? void 0 : options.messageId) || (0, generics_1.generateMessageID)(),405},406message: message,407messageTimestamp: timestamp,408messageStubParameters: [],409participant: (0, WABinary_1.isJidGroup)(jid) ? userJid : undefined,410status: Types_1.WAMessageStatus.PENDING411};412return Types_1.WAProto.WebMessageInfo.fromObject(messageJSON);413};414exports.generateWAMessageFromContent = generateWAMessageFromContent;415const generateWAMessage = async (jid, content, options) => {416var _a;417// ensure msg ID is with every log418options.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) === null || _a === void 0 ? void 0 : _a.child({ msgId: options.messageId });419return (0, exports.generateWAMessageFromContent)(jid, await (0, exports.generateWAMessageContent)(content, options), options);420};421exports.generateWAMessage = generateWAMessage;422/** Get the key to access the true type of content */423const getContentType = (content) => {424if (content) {425const keys = Object.keys(content);426const key = keys.find(k => (k === 'conversation' || k.endsWith('Message')) && k !== 'senderKeyDistributionMessage');427return key;428}429};430exports.getContentType = getContentType;431/**432* Normalizes ephemeral, view once messages to regular message content433* Eg. image messages in ephemeral messages, in view once messages etc.434* @param content435* @returns436*/437const normalizeMessageContent = (content) => {438var _a, _b, _c, _d, _e;439content = ((_c = (_b = (_a = content === null || content === void 0 ? void 0 : content.ephemeralMessage) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.viewOnceMessage) === null || _c === void 0 ? void 0 : _c.message) ||440((_d = content === null || content === void 0 ? void 0 : content.ephemeralMessage) === null || _d === void 0 ? void 0 : _d.message) ||441((_e = content === null || content === void 0 ? void 0 : content.viewOnceMessage) === null || _e === void 0 ? void 0 : _e.message) ||442content ||443undefined;444return content;445};446exports.normalizeMessageContent = normalizeMessageContent;447/**448* Extract the true message content from a message449* Eg. extracts the inner message from a disappearing message/view once message450*/451const extractMessageContent = (content) => {452var _a, _b, _c, _d, _e, _f;453const extractFromTemplateMessage = (msg) => {454if (msg.imageMessage) {455return { imageMessage: msg.imageMessage };456}457else if (msg.documentMessage) {458return { documentMessage: msg.documentMessage };459}460else if (msg.videoMessage) {461return { videoMessage: msg.videoMessage };462}463else if (msg.locationMessage) {464return { locationMessage: msg.locationMessage };465}466else {467return { conversation: 'contentText' in msg ? msg.contentText : ('hydratedContentText' in msg ? msg.hydratedContentText : '') };468}469};470content = (0, exports.normalizeMessageContent)(content);471if (content === null || content === void 0 ? void 0 : content.buttonsMessage) {472return extractFromTemplateMessage(content.buttonsMessage);473}474if ((_a = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _a === void 0 ? void 0 : _a.hydratedFourRowTemplate) {475return extractFromTemplateMessage((_b = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _b === void 0 ? void 0 : _b.hydratedFourRowTemplate);476}477if ((_c = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _c === void 0 ? void 0 : _c.hydratedTemplate) {478return extractFromTemplateMessage((_d = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _d === void 0 ? void 0 : _d.hydratedTemplate);479}480if ((_e = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _e === void 0 ? void 0 : _e.fourRowTemplate) {481return extractFromTemplateMessage((_f = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _f === void 0 ? void 0 : _f.fourRowTemplate);482}483return content;484};485exports.extractMessageContent = extractMessageContent;486/**487* Returns the device predicted by message ID488*/489const getDevice = (id) => {490const deviceType = id.length > 21 ? 'android' : id.substring(0, 2) === '3A' ? 'ios' : 'web';491return deviceType;492};493exports.getDevice = getDevice;494/** Upserts a receipt in the message */495const updateMessageWithReceipt = (msg, receipt) => {496msg.userReceipt = msg.userReceipt || [];497const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);498if (recp) {499Object.assign(recp, receipt);500}501else {502msg.userReceipt.push(receipt);503}504};505exports.updateMessageWithReceipt = updateMessageWithReceipt;506const getKeyAuthor = (key) => (((key === null || key === void 0 ? void 0 : key.fromMe) ? 'me' : (key === null || key === void 0 ? void 0 : key.participant) || (key === null || key === void 0 ? void 0 : key.remoteJid)) || '');507/** Update the message with a new reaction */508const updateMessageWithReaction = (msg, reaction) => {509const authorID = getKeyAuthor(reaction.key);510const reactions = (msg.reactions || [])511.filter(r => getKeyAuthor(r.key) !== authorID);512if (reaction.text) {513reactions.push(reaction);514}515msg.reactions = reactions;516};517exports.updateMessageWithReaction = updateMessageWithReaction;518/** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */519const aggregateMessageKeysNotFromMe = (keys) => {520const keyMap = {};521for (const { remoteJid, id, participant, fromMe } of keys) {522if (!fromMe) {523const uqKey = `${remoteJid}:${participant || ''}`;524if (!keyMap[uqKey]) {525keyMap[uqKey] = {526jid: remoteJid,527participant: participant,528messageIds: []529};530}531keyMap[uqKey].messageIds.push(id);532}533}534return Object.values(keyMap);535};536exports.aggregateMessageKeysNotFromMe = aggregateMessageKeysNotFromMe;537const REUPLOAD_REQUIRED_STATUS = [410, 404];538/**539* Downloads the given message. Throws an error if it's not a media message540*/541const downloadMediaMessage = async (message, type, options, ctx) => {542var _a;543try {544const result = await downloadMsg();545return result;546}547catch (error) {548if (ctx) {549if (axios_1.default.isAxiosError(error)) {550// check if the message requires a reupload551if (REUPLOAD_REQUIRED_STATUS.includes((_a = error.response) === null || _a === void 0 ? void 0 : _a.status)) {552ctx.logger.info({ key: message.key }, 'sending reupload media request...');553// request reupload554message = await ctx.reuploadRequest(message);555const result = await downloadMsg();556return result;557}558}559}560throw error;561}562async function downloadMsg() {563const mContent = (0, exports.extractMessageContent)(message.message);564if (!mContent) {565throw new boom_1.Boom('No message present', { statusCode: 400, data: message });566}567const contentType = (0, exports.getContentType)(mContent);568const mediaType = contentType === null || contentType === void 0 ? void 0 : contentType.replace('Message', '');569const media = mContent[contentType];570if (!media || typeof media !== 'object' || !('url' in media)) {571throw new boom_1.Boom(`"${contentType}" message is not a media message`);572}573const stream = await (0, messages_media_1.downloadContentFromMessage)(media, mediaType, options);574if (type === 'buffer') {575let buffer = Buffer.from([]);576for await (const chunk of stream) {577buffer = Buffer.concat([buffer, chunk]);578}579return buffer;580}581return stream;582}583};584exports.downloadMediaMessage = downloadMediaMessage;585/** Checks whether the given message is a media message; if it is returns the inner content */586const assertMediaContent = (content) => {587content = (0, exports.extractMessageContent)(content);588const mediaContent = (content === null || content === void 0 ? void 0 : content.documentMessage)589|| (content === null || content === void 0 ? void 0 : content.imageMessage)590|| (content === null || content === void 0 ? void 0 : content.videoMessage)591|| (content === null || content === void 0 ? void 0 : content.audioMessage)592|| (content === null || content === void 0 ? void 0 : content.stickerMessage);593if (!mediaContent) {594throw new boom_1.Boom('given message is not a media message', { statusCode: 400, data: content });595}596return mediaContent;597};598exports.assertMediaContent = assertMediaContent;599const generateContextInfo = () => {600const info = {601deviceListMetadataVersion: 2,602deviceListMetadata: {}603};604return info;605};606/**607* this is an experimental patch to make buttons work608* Don't know how it works, but it does for now609*/610const patchMessageForMdIfRequired = (message) => {611const requiresPatch = !!(message.buttonsMessage612// || message.templateMessage613|| message.listMessage);614if (requiresPatch) {615message = {616viewOnceMessage: {617message: {618messageContextInfo: generateContextInfo(),619...message620}621}622};623}624return message;625};626exports.patchMessageForMdIfRequired = patchMessageForMdIfRequired;627628629