@whiskeysockets/baileys
Version:
A WebSockets library for interacting with WhatsApp Web
758 lines (757 loc) • 34.1 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertMediaContent = exports.downloadMediaMessage = exports.aggregateMessageKeysNotFromMe = exports.updateMessageWithPollUpdate = 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;
exports.getAggregateVotesInPollMessage = getAggregateVotesInPollMessage;
const boom_1 = require("@hapi/boom");
const axios_1 = __importDefault(require("axios"));
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const WAProto_1 = require("../../WAProto");
const Defaults_1 = require("../Defaults");
const Types_1 = require("../Types");
const WABinary_1 = require("../WABinary");
const crypto_2 = require("./crypto");
const generics_1 = require("./generics");
const messages_media_1 = require("./messages-media");
const MIMETYPE_MAP = {
image: 'image/jpeg',
video: 'video/mp4',
document: 'application/pdf',
audio: 'audio/ogg; codecs=opus',
sticker: 'image/webp',
'product-catalog-image': 'image/jpeg',
};
const MessageTypeProto = {
'image': Types_1.WAProto.Message.ImageMessage,
'video': Types_1.WAProto.Message.VideoMessage,
'audio': Types_1.WAProto.Message.AudioMessage,
'sticker': Types_1.WAProto.Message.StickerMessage,
'document': Types_1.WAProto.Message.DocumentMessage,
};
/**
* Uses a regex to test whether the string contains a URL, and returns the URL if it does.
* @param text eg. hello https://google.com
* @returns the URL, eg. https://google.com
*/
const extractUrlFromText = (text) => { var _a; return (_a = text.match(Defaults_1.URL_REGEX)) === null || _a === void 0 ? void 0 : _a[0]; };
exports.extractUrlFromText = extractUrlFromText;
const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
const url = (0, exports.extractUrlFromText)(text);
if (!!getUrlInfo && url) {
try {
const urlInfo = await getUrlInfo(url);
return urlInfo;
}
catch (error) { // ignore if fails
logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'url generation failed');
}
}
};
exports.generateLinkPreviewIfRequired = generateLinkPreviewIfRequired;
const assertColor = async (color) => {
let assertedColor;
if (typeof color === 'number') {
assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
}
else {
let hex = color.trim().replace('#', '');
if (hex.length <= 6) {
hex = 'FF' + hex.padStart(6, '0');
}
assertedColor = parseInt(hex, 16);
return assertedColor;
}
};
const prepareWAMessageMedia = async (message, options) => {
const logger = options.logger;
let mediaType;
for (const key of Defaults_1.MEDIA_KEYS) {
if (key in message) {
mediaType = key;
}
}
if (!mediaType) {
throw new boom_1.Boom('Invalid media type', { statusCode: 400 });
}
const uploadData = {
...message,
media: message[mediaType]
};
delete uploadData[mediaType];
// check if cacheable + generate cache key
const cacheableKey = typeof uploadData.media === 'object' &&
('url' in uploadData.media) &&
!!uploadData.media.url &&
!!options.mediaCache && (
// generate the key
mediaType + ':' + uploadData.media.url.toString());
if (mediaType === 'document' && !uploadData.fileName) {
uploadData.fileName = 'file';
}
if (!uploadData.mimetype) {
uploadData.mimetype = MIMETYPE_MAP[mediaType];
}
// check for cache hit
if (cacheableKey) {
const mediaBuff = options.mediaCache.get(cacheableKey);
if (mediaBuff) {
logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'got media cache hit');
const obj = Types_1.WAProto.Message.decode(mediaBuff);
const key = `${mediaType}Message`;
Object.assign(obj[key], { ...uploadData, media: undefined });
return obj;
}
}
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') &&
(typeof uploadData['jpegThumbnail'] === 'undefined');
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await (0, messages_media_1.encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
logger,
saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
opts: options.options
});
// url safe Base64 encode the SHA256 hash of the body
const fileEncSha256B64 = fileEncSha256.toString('base64');
const [{ mediaUrl, directPath }] = await Promise.all([
(async () => {
const result = await options.upload(encFilePath, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs });
logger === null || logger === void 0 ? void 0 : logger.debug({ mediaType, cacheableKey }, 'uploaded media');
return result;
})(),
(async () => {
try {
if (requiresThumbnailComputation) {
const { thumbnail, originalImageDimensions } = await (0, messages_media_1.generateThumbnail)(originalFilePath, mediaType, options);
uploadData.jpegThumbnail = thumbnail;
if (!uploadData.width && originalImageDimensions) {
uploadData.width = originalImageDimensions.width;
uploadData.height = originalImageDimensions.height;
logger === null || logger === void 0 ? void 0 : logger.debug('set dimensions');
}
logger === null || logger === void 0 ? void 0 : logger.debug('generated thumbnail');
}
if (requiresDurationComputation) {
uploadData.seconds = await (0, messages_media_1.getAudioDuration)(originalFilePath);
logger === null || logger === void 0 ? void 0 : logger.debug('computed audio duration');
}
if (requiresWaveformProcessing) {
uploadData.waveform = await (0, messages_media_1.getAudioWaveform)(originalFilePath, logger);
logger === null || logger === void 0 ? void 0 : logger.debug('processed waveform');
}
if (requiresAudioBackground) {
uploadData.backgroundArgb = await assertColor(options.backgroundColor);
logger === null || logger === void 0 ? void 0 : logger.debug('computed backgroundColor audio status');
}
}
catch (error) {
logger === null || logger === void 0 ? void 0 : logger.warn({ trace: error.stack }, 'failed to obtain extra info');
}
})(),
])
.finally(async () => {
try {
await fs_1.promises.unlink(encFilePath);
if (originalFilePath) {
await fs_1.promises.unlink(originalFilePath);
}
logger === null || logger === void 0 ? void 0 : logger.debug('removed tmp files');
}
catch (error) {
logger === null || logger === void 0 ? void 0 : logger.warn('failed to remove tmp file');
}
});
const obj = Types_1.WAProto.Message.fromObject({
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
url: mediaUrl,
directPath,
mediaKey,
fileEncSha256,
fileSha256,
fileLength,
mediaKeyTimestamp: (0, generics_1.unixTimestampSeconds)(),
...uploadData,
media: undefined
})
});
if (uploadData.ptv) {
obj.ptvMessage = obj.videoMessage;
delete obj.videoMessage;
}
if (cacheableKey) {
logger === null || logger === void 0 ? void 0 : logger.debug({ cacheableKey }, 'set cache');
options.mediaCache.set(cacheableKey, Types_1.WAProto.Message.encode(obj).finish());
}
return obj;
};
exports.prepareWAMessageMedia = prepareWAMessageMedia;
const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
ephemeralExpiration = ephemeralExpiration || 0;
const content = {
ephemeralMessage: {
message: {
protocolMessage: {
type: Types_1.WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
ephemeralExpiration
}
}
}
};
return Types_1.WAProto.Message.fromObject(content);
};
exports.prepareDisappearingMessageSettingContent = prepareDisappearingMessageSettingContent;
/**
* Generate forwarded message content like WA does
* @param message the message to forward
* @param options.forceForward will show the message as forwarded even if it is from you
*/
const generateForwardMessageContent = (message, forceForward) => {
var _a;
let content = message.message;
if (!content) {
throw new boom_1.Boom('no content in message', { statusCode: 400 });
}
// hacky copy
content = (0, exports.normalizeMessageContent)(content);
content = WAProto_1.proto.Message.decode(WAProto_1.proto.Message.encode(content).finish());
let key = Object.keys(content)[0];
let score = ((_a = content[key].contextInfo) === null || _a === void 0 ? void 0 : _a.forwardingScore) || 0;
score += message.key.fromMe && !forceForward ? 0 : 1;
if (key === 'conversation') {
content.extendedTextMessage = { text: content[key] };
delete content.conversation;
key = 'extendedTextMessage';
}
if (score > 0) {
content[key].contextInfo = { forwardingScore: score, isForwarded: true };
}
else {
content[key].contextInfo = {};
}
return content;
};
exports.generateForwardMessageContent = generateForwardMessageContent;
const generateWAMessageContent = async (message, options) => {
var _a;
var _b, _c;
let m = {};
if ('text' in message) {
const extContent = { text: message.text };
let urlInfo = message.linkPreview;
if (typeof urlInfo === 'undefined') {
urlInfo = await (0, exports.generateLinkPreviewIfRequired)(message.text, options.getUrlInfo, options.logger);
}
if (urlInfo) {
extContent.matchedText = urlInfo['matched-text'];
extContent.jpegThumbnail = urlInfo.jpegThumbnail;
extContent.description = urlInfo.description;
extContent.title = urlInfo.title;
extContent.previewType = 0;
const img = urlInfo.highQualityThumbnail;
if (img) {
extContent.thumbnailDirectPath = img.directPath;
extContent.mediaKey = img.mediaKey;
extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
extContent.thumbnailWidth = img.width;
extContent.thumbnailHeight = img.height;
extContent.thumbnailSha256 = img.fileSha256;
extContent.thumbnailEncSha256 = img.fileEncSha256;
}
}
if (options.backgroundColor) {
extContent.backgroundArgb = await assertColor(options.backgroundColor);
}
if (options.font) {
extContent.font = options.font;
}
m.extendedTextMessage = extContent;
}
else if ('contacts' in message) {
const contactLen = message.contacts.contacts.length;
if (!contactLen) {
throw new boom_1.Boom('require atleast 1 contact', { statusCode: 400 });
}
if (contactLen === 1) {
m.contactMessage = Types_1.WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0]);
}
else {
m.contactsArrayMessage = Types_1.WAProto.Message.ContactsArrayMessage.fromObject(message.contacts);
}
}
else if ('location' in message) {
m.locationMessage = Types_1.WAProto.Message.LocationMessage.fromObject(message.location);
}
else if ('react' in message) {
if (!message.react.senderTimestampMs) {
message.react.senderTimestampMs = Date.now();
}
m.reactionMessage = Types_1.WAProto.Message.ReactionMessage.fromObject(message.react);
}
else if ('delete' in message) {
m.protocolMessage = {
key: message.delete,
type: Types_1.WAProto.Message.ProtocolMessage.Type.REVOKE
};
}
else if ('forward' in message) {
m = (0, exports.generateForwardMessageContent)(message.forward, message.force);
}
else if ('disappearingMessagesInChat' in message) {
const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
(message.disappearingMessagesInChat ? Defaults_1.WA_DEFAULT_EPHEMERAL : 0) :
message.disappearingMessagesInChat;
m = (0, exports.prepareDisappearingMessageSettingContent)(exp);
}
else if ('groupInvite' in message) {
m.groupInviteMessage = {};
m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
m.groupInviteMessage.caption = message.groupInvite.text;
m.groupInviteMessage.groupJid = message.groupInvite.jid;
m.groupInviteMessage.groupName = message.groupInvite.subject;
//TODO: use built-in interface and get disappearing mode info etc.
//TODO: cache / use store!?
if (options.getProfilePicUrl) {
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
if (pfpUrl) {
const resp = await axios_1.default.get(pfpUrl, { responseType: 'arraybuffer' });
if (resp.status === 200) {
m.groupInviteMessage.jpegThumbnail = resp.data;
}
}
}
}
else if ('pin' in message) {
m.pinInChatMessage = {};
m.messageContextInfo = {};
m.pinInChatMessage.key = message.pin;
m.pinInChatMessage.type = message.type;
m.pinInChatMessage.senderTimestampMs = Date.now();
m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
}
else if ('buttonReply' in message) {
switch (message.type) {
case 'template':
m.templateButtonReplyMessage = {
selectedDisplayText: message.buttonReply.displayText,
selectedId: message.buttonReply.id,
selectedIndex: message.buttonReply.index,
};
break;
case 'plain':
m.buttonsResponseMessage = {
selectedButtonId: message.buttonReply.id,
selectedDisplayText: message.buttonReply.displayText,
type: WAProto_1.proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,
};
break;
}
}
else if ('ptv' in message && message.ptv) {
const { videoMessage } = await (0, exports.prepareWAMessageMedia)({ video: message.video }, options);
m.ptvMessage = videoMessage;
}
else if ('product' in message) {
const { imageMessage } = await (0, exports.prepareWAMessageMedia)({ image: message.product.productImage }, options);
m.productMessage = Types_1.WAProto.Message.ProductMessage.fromObject({
...message,
product: {
...message.product,
productImage: imageMessage,
}
});
}
else if ('listReply' in message) {
m.listResponseMessage = { ...message.listReply };
}
else if ('poll' in message) {
(_b = message.poll).selectableCount || (_b.selectableCount = 0);
(_c = message.poll).toAnnouncementGroup || (_c.toAnnouncementGroup = false);
if (!Array.isArray(message.poll.values)) {
throw new boom_1.Boom('Invalid poll values', { statusCode: 400 });
}
if (message.poll.selectableCount < 0
|| message.poll.selectableCount > message.poll.values.length) {
throw new boom_1.Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
}
m.messageContextInfo = {
// encKey
messageSecret: message.poll.messageSecret || (0, crypto_1.randomBytes)(32),
};
const pollCreationMessage = {
name: message.poll.name,
selectableOptionsCount: message.poll.selectableCount,
options: message.poll.values.map(optionName => ({ optionName })),
};
if (message.poll.toAnnouncementGroup) {
// poll v2 is for community announcement groups (single select and multiple)
m.pollCreationMessageV2 = pollCreationMessage;
}
else {
if (message.poll.selectableCount > 0) {
//poll v3 is for single select polls
m.pollCreationMessageV3 = pollCreationMessage;
}
else {
// poll v3 for multiple choice polls
m.pollCreationMessage = pollCreationMessage;
}
}
}
else if ('sharePhoneNumber' in message) {
m.protocolMessage = {
type: WAProto_1.proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
};
}
else if ('requestPhoneNumber' in message) {
m.requestPhoneNumberMessage = {};
}
else {
m = await (0, exports.prepareWAMessageMedia)(message, options);
}
if ('viewOnce' in message && !!message.viewOnce) {
m = { viewOnceMessage: { message: m } };
}
if ('mentions' in message && ((_a = message.mentions) === null || _a === void 0 ? void 0 : _a.length)) {
const [messageType] = Object.keys(m);
m[messageType].contextInfo = m[messageType] || {};
m[messageType].contextInfo.mentionedJid = message.mentions;
}
if ('edit' in message) {
m = {
protocolMessage: {
key: message.edit,
editedMessage: m,
timestampMs: Date.now(),
type: Types_1.WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
}
};
}
if ('contextInfo' in message && !!message.contextInfo) {
const [messageType] = Object.keys(m);
m[messageType] = m[messageType] || {};
m[messageType].contextInfo = message.contextInfo;
}
return Types_1.WAProto.Message.fromObject(m);
};
exports.generateWAMessageContent = generateWAMessageContent;
const generateWAMessageFromContent = (jid, message, options) => {
// set timestamp to now
// if not specified
if (!options.timestamp) {
options.timestamp = new Date();
}
const innerMessage = (0, exports.normalizeMessageContent)(message);
const key = (0, exports.getContentType)(innerMessage);
const timestamp = (0, generics_1.unixTimestampSeconds)(options.timestamp);
const { quoted, userJid } = options;
if (quoted) {
const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid);
let quotedMsg = (0, exports.normalizeMessageContent)(quoted.message);
const msgType = (0, exports.getContentType)(quotedMsg);
// strip any redundant properties
quotedMsg = WAProto_1.proto.Message.fromObject({ [msgType]: quotedMsg[msgType] });
const quotedContent = quotedMsg[msgType];
if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
delete quotedContent.contextInfo;
}
const contextInfo = innerMessage[key].contextInfo || {};
contextInfo.participant = (0, WABinary_1.jidNormalizedUser)(participant);
contextInfo.stanzaId = quoted.key.id;
contextInfo.quotedMessage = quotedMsg;
// if a participant is quoted, then it must be a group
// hence, remoteJid of group must also be entered
if (jid !== quoted.key.remoteJid) {
contextInfo.remoteJid = quoted.key.remoteJid;
}
innerMessage[key].contextInfo = contextInfo;
}
if (
// if we want to send a disappearing message
!!(options === null || options === void 0 ? void 0 : options.ephemeralExpiration) &&
// and it's not a protocol message -- delete, toggle disappear message
key !== 'protocolMessage' &&
// already not converted to disappearing message
key !== 'ephemeralMessage') {
innerMessage[key].contextInfo = {
...(innerMessage[key].contextInfo || {}),
expiration: options.ephemeralExpiration || Defaults_1.WA_DEFAULT_EPHEMERAL,
//ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
};
}
message = Types_1.WAProto.Message.fromObject(message);
const messageJSON = {
key: {
remoteJid: jid,
fromMe: true,
id: (options === null || options === void 0 ? void 0 : options.messageId) || (0, generics_1.generateMessageIDV2)(),
},
message: message,
messageTimestamp: timestamp,
messageStubParameters: [],
participant: (0, WABinary_1.isJidGroup)(jid) || (0, WABinary_1.isJidStatusBroadcast)(jid) ? userJid : undefined,
status: Types_1.WAMessageStatus.PENDING
};
return Types_1.WAProto.WebMessageInfo.fromObject(messageJSON);
};
exports.generateWAMessageFromContent = generateWAMessageFromContent;
const generateWAMessage = async (jid, content, options) => {
var _a;
// ensure msg ID is with every log
options.logger = (_a = options === null || options === void 0 ? void 0 : options.logger) === null || _a === void 0 ? void 0 : _a.child({ msgId: options.messageId });
return (0, exports.generateWAMessageFromContent)(jid, await (0, exports.generateWAMessageContent)(content, options), options);
};
exports.generateWAMessage = generateWAMessage;
/** Get the key to access the true type of content */
const getContentType = (content) => {
if (content) {
const keys = Object.keys(content);
const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
return key;
}
};
exports.getContentType = getContentType;
/**
* Normalizes ephemeral, view once messages to regular message content
* Eg. image messages in ephemeral messages, in view once messages etc.
* @param content
* @returns
*/
const normalizeMessageContent = (content) => {
if (!content) {
return undefined;
}
// set max iterations to prevent an infinite loop
for (let i = 0; i < 5; i++) {
const inner = getFutureProofMessage(content);
if (!inner) {
break;
}
content = inner.message;
}
return content;
function getFutureProofMessage(message) {
return ((message === null || message === void 0 ? void 0 : message.ephemeralMessage)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessage)
|| (message === null || message === void 0 ? void 0 : message.documentWithCaptionMessage)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2Extension)
|| (message === null || message === void 0 ? void 0 : message.editedMessage));
}
};
exports.normalizeMessageContent = normalizeMessageContent;
/**
* Extract the true message content from a message
* Eg. extracts the inner message from a disappearing message/view once message
*/
const extractMessageContent = (content) => {
var _a, _b, _c, _d, _e, _f;
const extractFromTemplateMessage = (msg) => {
if (msg.imageMessage) {
return { imageMessage: msg.imageMessage };
}
else if (msg.documentMessage) {
return { documentMessage: msg.documentMessage };
}
else if (msg.videoMessage) {
return { videoMessage: msg.videoMessage };
}
else if (msg.locationMessage) {
return { locationMessage: msg.locationMessage };
}
else {
return {
conversation: 'contentText' in msg
? msg.contentText
: ('hydratedContentText' in msg ? msg.hydratedContentText : '')
};
}
};
content = (0, exports.normalizeMessageContent)(content);
if (content === null || content === void 0 ? void 0 : content.buttonsMessage) {
return extractFromTemplateMessage(content.buttonsMessage);
}
if ((_a = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _a === void 0 ? void 0 : _a.hydratedFourRowTemplate) {
return extractFromTemplateMessage((_b = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _b === void 0 ? void 0 : _b.hydratedFourRowTemplate);
}
if ((_c = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _c === void 0 ? void 0 : _c.hydratedTemplate) {
return extractFromTemplateMessage((_d = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _d === void 0 ? void 0 : _d.hydratedTemplate);
}
if ((_e = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _e === void 0 ? void 0 : _e.fourRowTemplate) {
return extractFromTemplateMessage((_f = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _f === void 0 ? void 0 : _f.fourRowTemplate);
}
return content;
};
exports.extractMessageContent = extractMessageContent;
/**
* Returns the device predicted by message ID
*/
const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' :
/^3E.{20}$/.test(id) ? 'web' :
/^(.{21}|.{32})$/.test(id) ? 'android' :
/^(3F|.{18}$)/.test(id) ? 'desktop' :
'unknown';
exports.getDevice = getDevice;
/** Upserts a receipt in the message */
const updateMessageWithReceipt = (msg, receipt) => {
msg.userReceipt = msg.userReceipt || [];
const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
if (recp) {
Object.assign(recp, receipt);
}
else {
msg.userReceipt.push(receipt);
}
};
exports.updateMessageWithReceipt = updateMessageWithReceipt;
/** Update the message with a new reaction */
const updateMessageWithReaction = (msg, reaction) => {
const authorID = (0, generics_1.getKeyAuthor)(reaction.key);
const reactions = (msg.reactions || [])
.filter(r => (0, generics_1.getKeyAuthor)(r.key) !== authorID);
if (reaction.text) {
reactions.push(reaction);
}
msg.reactions = reactions;
};
exports.updateMessageWithReaction = updateMessageWithReaction;
/** Update the message with a new poll update */
const updateMessageWithPollUpdate = (msg, update) => {
var _a, _b;
const authorID = (0, generics_1.getKeyAuthor)(update.pollUpdateMessageKey);
const reactions = (msg.pollUpdates || [])
.filter(r => (0, generics_1.getKeyAuthor)(r.pollUpdateMessageKey) !== authorID);
if ((_b = (_a = update.vote) === null || _a === void 0 ? void 0 : _a.selectedOptions) === null || _b === void 0 ? void 0 : _b.length) {
reactions.push(update);
}
msg.pollUpdates = reactions;
};
exports.updateMessageWithPollUpdate = updateMessageWithPollUpdate;
/**
* Aggregates all poll updates in a poll.
* @param msg the poll creation message
* @param meId your jid
* @returns A list of options & their voters
*/
function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
var _a, _b, _c;
const opts = ((_a = message === null || message === void 0 ? void 0 : message.pollCreationMessage) === null || _a === void 0 ? void 0 : _a.options) || ((_b = message === null || message === void 0 ? void 0 : message.pollCreationMessageV2) === null || _b === void 0 ? void 0 : _b.options) || ((_c = message === null || message === void 0 ? void 0 : message.pollCreationMessageV3) === null || _c === void 0 ? void 0 : _c.options) || [];
const voteHashMap = opts.reduce((acc, opt) => {
const hash = (0, crypto_2.sha256)(Buffer.from(opt.optionName || '')).toString();
acc[hash] = {
name: opt.optionName || '',
voters: []
};
return acc;
}, {});
for (const update of pollUpdates || []) {
const { vote } = update;
if (!vote) {
continue;
}
for (const option of vote.selectedOptions || []) {
const hash = option.toString();
let data = voteHashMap[hash];
if (!data) {
voteHashMap[hash] = {
name: 'Unknown',
voters: []
};
data = voteHashMap[hash];
}
voteHashMap[hash].voters.push((0, generics_1.getKeyAuthor)(update.pollUpdateMessageKey, meId));
}
}
return Object.values(voteHashMap);
}
/** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
const aggregateMessageKeysNotFromMe = (keys) => {
const keyMap = {};
for (const { remoteJid, id, participant, fromMe } of keys) {
if (!fromMe) {
const uqKey = `${remoteJid}:${participant || ''}`;
if (!keyMap[uqKey]) {
keyMap[uqKey] = {
jid: remoteJid,
participant: participant,
messageIds: []
};
}
keyMap[uqKey].messageIds.push(id);
}
}
return Object.values(keyMap);
};
exports.aggregateMessageKeysNotFromMe = aggregateMessageKeysNotFromMe;
const REUPLOAD_REQUIRED_STATUS = [410, 404];
/**
* Downloads the given message. Throws an error if it's not a media message
*/
const downloadMediaMessage = async (message, type, options, ctx) => {
const result = await downloadMsg()
.catch(async (error) => {
var _a;
if (ctx && axios_1.default.isAxiosError(error) && // check if the message requires a reupload
REUPLOAD_REQUIRED_STATUS.includes((_a = error.response) === null || _a === void 0 ? void 0 : _a.status)) {
ctx.logger.info({ key: message.key }, 'sending reupload media request...');
// request reupload
message = await ctx.reuploadRequest(message);
const result = await downloadMsg();
return result;
}
throw error;
});
return result;
async function downloadMsg() {
const mContent = (0, exports.extractMessageContent)(message.message);
if (!mContent) {
throw new boom_1.Boom('No message present', { statusCode: 400, data: message });
}
const contentType = (0, exports.getContentType)(mContent);
let mediaType = contentType === null || contentType === void 0 ? void 0 : contentType.replace('Message', '');
const media = mContent[contentType];
if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
throw new boom_1.Boom(`"${contentType}" message is not a media message`);
}
let download;
if ('thumbnailDirectPath' in media && !('url' in media)) {
download = {
directPath: media.thumbnailDirectPath,
mediaKey: media.mediaKey
};
mediaType = 'thumbnail-link';
}
else {
download = media;
}
const stream = await (0, messages_media_1.downloadContentFromMessage)(download, mediaType, options);
if (type === 'buffer') {
const bufferArray = [];
for await (const chunk of stream) {
bufferArray.push(chunk);
}
return Buffer.concat(bufferArray);
}
return stream;
}
};
exports.downloadMediaMessage = downloadMediaMessage;
/** Checks whether the given message is a media message; if it is returns the inner content */
const assertMediaContent = (content) => {
content = (0, exports.extractMessageContent)(content);
const mediaContent = (content === null || content === void 0 ? void 0 : content.documentMessage)
|| (content === null || content === void 0 ? void 0 : content.imageMessage)
|| (content === null || content === void 0 ? void 0 : content.videoMessage)
|| (content === null || content === void 0 ? void 0 : content.audioMessage)
|| (content === null || content === void 0 ? void 0 : content.stickerMessage);
if (!mediaContent) {
throw new boom_1.Boom('given message is not a media message', { statusCode: 400, data: content });
}
return mediaContent;
};
exports.assertMediaContent = assertMediaContent;