neroxbailx
Version:
baileys whatsapp-api
1,330 lines • 43.8 kB
JavaScript
"use strict"
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }
}
Object.defineProperty(exports, "__esModule", { value: true })
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) => text.match(Defaults_1.URL_REGEX)?.[0]
const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
const url = extractUrlFromText(text)
if (!!getUrlInfo && url) {
try {
const urlInfo = await getUrlInfo(url)
return urlInfo
}
catch (error) {
logger?.warn({ trace: error.stack }, 'url generation failed')
}
}
}
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?.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 (options.newsletter ? messages_media_1.prepareStream : 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 = (options.newsletter ? fileSha256 : fileEncSha256 !== null && fileEncSha256 ? fileEncSha256 : fileSha256).toString('base64')
const [{ mediaUrl, directPath, handle }] = await Promise.all([
(async () => {
const result = await options.upload(encFilePath, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
return result
})(),
(async () => {
try {
if (requiresThumbnailComputation) {
const { thumbnail, originalImageDimensions } = await messages_media_1.generateThumbnail(originalFilePath, mediaType, options)
uploadData.jpegThumbnail = thumbnail
if (!uploadData.width && originalImageDimensions) {
uploadData.width = originalImageDimensions.width
uploadData.height = originalImageDimensions.height
logger?.debug('set dimensions')
}
logger?.debug('generated thumbnail')
}
if (requiresDurationComputation) {
uploadData.seconds = await messages_media_1.getAudioDuration(originalFilePath)
logger?.debug('computed audio duration')
}
if (requiresWaveformProcessing) {
uploadData.waveform = await messages_media_1.getAudioWaveform(originalFilePath, logger)
logger?.debug('processed waveform')
}
if (requiresAudioBackground) {
uploadData.backgroundArgb = await assertColor(options.backgroundColor)
logger?.debug('computed backgroundColor audio status')
}
}
catch (error) {
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?.debug('removed tmp files')
}
catch (error) {
logger?.warn('failed to remove tmp file')
}
})
const obj = Types_1.WAProto.Message.fromObject({
[`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
url: handle ? undefined : mediaUrl,
directPath,
mediaKey: mediaKey,
fileEncSha256: fileEncSha256,
fileSha256,
fileLength,
mediaKeyTimestamp: handle ? undefined : generics_1.unixTimestampSeconds(),
...uploadData,
media: undefined
})
})
if (uploadData.ptv) {
obj.ptvMessage = obj.videoMessage
delete obj.videoMessage
}
if (cacheableKey) {
logger?.debug({ cacheableKey }, 'set cache')
options.mediaCache.set(cacheableKey, Types_1.WAProto.Message.encode(obj).finish())
}
return obj
}
const prepareDisappearingMessageSettingContent = (expiration) => {
const content = {
ephemeralMessage: {
message: {
protocolMessage: {
type: Types_1.WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
ephemeralExpiration: expiration ? expiration : 0
}
}
}
}
return Types_1.WAProto.Message.fromObject(content)
}
/**
* 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) => {
let content = message.message
if (!content) {
throw new boom_1.Boom('no content in message', { statusCode: 400 })
}
// hacky copy
content = normalizeMessageContent(content)
content = WAProto_1.proto.Message.decode(WAProto_1.proto.Message.encode(content).finish())
let key = Object.keys(content)[0]
let score = content[key].contextInfo?.forwardingScore || 0
if (forceForward) score += forceForward ? forceForward : 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
}
const generateWAMessageContent = async (message, options) => {
let m = {}
if ('text' in message) {
const extContent = { text: message.text }
let urlInfo = message.linkPreview
if (typeof urlInfo === 'undefined') {
urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger)
}
if (urlInfo) {
extContent.canonicalUrl = urlInfo['canonical-url']
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
}
extContent.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m.extendedTextMessage = extContent
}
else if ('contacts' in message) {
const contactLen = message.contacts.contacts.length
let contactMessage
if (!contactLen) {
throw new boom_1.Boom('require atleast 1 contact', { statusCode: 400 })
}
if (contactLen === 1) {
contactMessage = {
contactMessage: Types_1.WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0])
}
}
else {
contactMessage = {
contactsArrayMessage: Types_1.WAProto.Message.ContactsArrayMessage.fromObject(message.contacts)
}
}
const [type] = Object.keys(contactMessage)
contactMessage[type].contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
contactMessage.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m = contactMessage
}
else if ('location' in message) {
let locationMessage
if (message.live) {
locationMessage = {
liveLocationMessage: Types_1.WAProto.Message.LiveLocationMessage.fromObject(message.location)
}
}
else {
locationMessage = {
locationMessage: Types_1.WAProto.Message.LocationMessage.fromObject(message.location)
}
}
const [type] = Object.keys(locationMessage)
locationMessage[type].contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
locationMessage.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m = locationMessage
}
else if ('react' in message) {
if (!message.react.senderTimestampMs) {
message.react.senderTimestampMs = Date.now()
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
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) {
const mess = generateForwardMessageContent(message.forward, message.force)
const [type] = Object.keys(mess)
mess[type].contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
mess.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m = mess
}
else if ('disappearingMessagesInChat' in message) {
const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
(message.disappearingMessagesInChat ? Defaults_1.WA_DEFAULT_EPHEMERAL : 0) :
message.disappearingMessagesInChat
m = prepareDisappearingMessageSettingContent(exp)
}
else if ('groupInvite' in message) {
m.messageContextInfo = {}
m.groupInviteMessage = {}
m.groupInviteMessage.inviteCode = message.groupInvite.code
m.groupInviteMessage.inviteExpiration = message.groupInvite.expiration
m.groupInviteMessage.caption = message.groupInvite.caption
m.groupInviteMessage.groupJid = message.groupInvite.jid
m.groupInviteMessage.groupName = message.groupInvite.name
m.groupInviteMessage.contextInfo = message.contextInfo
m.messageContextInfo.messageSecret = crypto_1.randomBytes(32)
if (options.getProfilePicUrl) {
const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid)
const { thumbnail } = await messages_media_1.generateThumbnail(pfpUrl, 'image')
m.groupInviteMessage.jpegThumbnail = thumbnail
}
m.groupInviteMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('adminInvite' in message) {
m.messageContextInfo = {}
m.newsletterAdminInviteMessage = {}
m.newsletterAdminInviteMessage.newsletterJid = message.adminInvite.jid
m.newsletterAdminInviteMessage.newsletterName= message.adminInvite.name
m.newsletterAdminInviteMessage.caption = message.adminInvite.caption
m.newsletterAdminInviteMessage.inviteExpiration = message.adminInvite.expiration
m.newsletterAdminInviteMessage.contextInfo = message.contextInfo
m.messageContextInfo.messageSecret = crypto_1.randomBytes(32)
if (options.getProfilePicUrl) {
const pfpUrl = await options.getProfilePicUrl(message.adminInvite.jid)
const { thumbnail } = await messages_media_1.generateThumbnail(pfpUrl, 'image')
m.newsletterAdminInviteMessage.jpegThumbnail = thumbnail
}
m.newsletterAdminInviteMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('pin' in message) {
m.pinInChatMessage = {}
m.messageContextInfo = {}
m.pinInChatMessage.key = message.pin.key
m.pinInChatMessage.type = message.pin?.type || 1
m.pinInChatMessage.senderTimestampMs = message.pin?.time || Date.now()
m.messageContextInfo.messageAddOnDurationInSecs = message.pin.type === 1 ? message.pin.time || 86400 : 0
}
else if ('keep' in message) {
m.keepInChatMessage = {}
m.keepInChatMessage.key = message.keep.key
m.keepInChatMessage.keepType = message.keep?.type || 1
m.keepInChatMessage.timestampMs = message.keep?.time || Date.now()
}
else if ('call' in message) {
m.messageContextInfo = {}
m.scheduledCallCreationMessage = {}
m.scheduledCallCreationMessage.scheduledTimestampMs = message.call?.time || Date.now()
m.scheduledCallCreationMessage.callType = message.call?.type || 1
m.scheduledCallCreationMessage.title = message.call?.name || 'Call Creation'
m.messageContextInfo.messageSecret = crypto_1.randomBytes(32)
m.scheduledCallCreationMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('paymentInvite' in message) {
m.messageContextInfo = {}
m.paymentInviteMessage = {}
m.paymentInviteMessage.expiryTimestamp = message.paymentInvite?.expiry || 0
m.paymentInviteMessage.serviceType = message.paymentInvite?.type || 2
m.messageContextInfo.messageSecret = crypto_1.randomBytes(32)
m.paymentInviteMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('buttonReply' in message) {
switch (message.type) {
case 'list':
m.listResponseMessage = {
title: message.buttonReply.title,
description: message.buttonReply.description,
singleSelectReply: {
selectedRowId: message.buttonReply.rowId
},
lisType: WAProto_1.proto.Message.ListResponseMessage.ListType.SINGLE_SELECT
}
break
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
case 'interactive':
m.interactiveResponseMessage = {
body: {
text: message.buttonReply.displayText,
format: WAProto_1.proto.Message.InteractiveResponseMessage.Body.Format.EXTENSIONS_1
},
nativeFlowResponseMessage: {
name: message.buttonReply.nativeFlows.name,
paramsJson: message.buttonReply.nativeFlows.paramsJson,
version: message.buttonReply.nativeFlows.version
}
}
break
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('ptv' in message && message.ptv) {
const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options)
m.ptvMessage = videoMessage
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('order' in message) {
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m.orderMessage = Types_1.WAProto.Message.OrderMessage.fromObject(message.order)
m.orderMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('event' in message) {
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m.eventMessage = Types_1.WAProto.Message.EventMessage.fromObject(message.event)
if (!message.event.startTime) {
m.eventMessage.startTime = generics_1.unixTimestampSeconds()
}
m.eventMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
}
else if ('product' in message) {
const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options)
m.productMessage = Types_1.WAProto.Message.ProductMessage.fromObject({
...message,
product: {
...message.product,
productImage: imageMessage,
}
})
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('pollResult' in message) {
if (!Array.isArray(message.pollResult.values)) {
throw new boom_1.Boom('Invalid pollResult values', { statusCode: 400 })
}
const pollResultSnapshotMessage = {
name: message.pollResult.name,
pollVotes: message.pollResult.values.map(([optionName, optionVoteCount]) => ({
optionName,
optionVoteCount
}))
}
pollResultSnapshotMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m.pollResultSnapshotMessage = pollResultSnapshotMessage
}
else if ('poll' in message) {
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 = {
messageSecret: message.poll.messageSecret || crypto_1.randomBytes(32),
}
const pollCreationMessage = {
name: message.poll.name,
selectableOptionsCount: message.poll?.selectableCount || 0,
options: message.poll.values.map(optionName => ({ optionName })),
}
pollCreationMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
if(message.poll?.toAnnouncementGroup) {
m.pollCreationMessageV2 = pollCreationMessage
} else {
if(message.poll.selectableCount > 0) {
m.pollCreationMessageV3 = pollCreationMessage
} else {
m.pollCreationMessage = pollCreationMessage
}
}
}
else if ('payment' in message) {
const requestPaymentMessage = {
amount: {
currencyCode: message.payment?.currency || 'IDR',
offset: message.payment?.offset || 0,
value: message.payment?.amount || 999999999
},
expiryTimestamp: message.payment?.expiry || 0,
amount1000: message.payment?.amount || 999999999 * 1000,
currencyCodeIso4217: message.payment?.currency || 'IDR',
requestFrom: message.payment?.from || '0@s.whatsapp.net',
noteMessage: {
extendedTextMessage: {
text: message.payment?.note || 'Notes'
}
},
background: {
placeholderArgb: message.payment?.image?.placeholderArgb || 4278190080,
textArgb: message.payment?.image?.textArgb || 4294967295,
subtextArgb: message.payment?.image?.subtextArgb || 4294967295,
type: 1
}
}
requestPaymentMessage.noteMessage.extendedTextMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m.requestPaymentMessage = requestPaymentMessage
}
else if ('sharePhoneNumber' in message) {
m.protocolMessage = {
type: Types_1.WAProto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('requestPhoneNumber' in message) {
m.requestPhoneNumberMessage = {}
}
else {
const mess = await prepareWAMessageMedia(message, options)
const [type] = Object.keys(mess)
mess[type].contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
mess.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m = mess
}
if ('sections' in message && !!message.sections) {
const listMessage = {
title: message.title,
buttonText: message.buttonText,
footerText: message.footer,
description: message.text,
sections: message.sections,
listType: WAProto_1.proto.Message.ListMessage.ListType.SINGLE_SELECT
}
listMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { listMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('productList' in message && !!message.productList) {
const thumbnail = message.thumbnail ? await messages_media_1.generateThumbnail(message.thumbnail, 'image') : null
const listMessage = {
title: message.title,
buttonText: message.buttonText,
footerText: message.footer,
description: message.text,
productListInfo: {
productSections: message.productList,
headerImage: {
productId: message.productList[0].products[0].productId,
jpegThumbnail: thumbnail?.thumbnail || null
},
businessOwnerJid: message.businessOwnerJid
},
listType: WAProto_1.proto.Message.ListMessage.ListType.PRODUCT_LIST
}
listMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { listMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('buttons' in message && !!message.buttons) {
const buttonsMessage = {
buttons: message.buttons.map(b => ({ ...b, type: WAProto_1.proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
}
if ('text' in message) {
buttonsMessage.contentText = message.text
buttonsMessage.headerType = WAProto_1.proto.Message.ButtonsMessage.HeaderType.EMPTY
}
else {
if ('caption' in message) {
buttonsMessage.contentText = message.caption
}
const type = Object.keys(m)[0].replace('Message', '').toUpperCase()
buttonsMessage.headerType = WAProto_1.proto.Message.ButtonsMessage.HeaderType[type]
Object.assign(buttonsMessage, m)
}
if ('footer' in message && !!message.footer) {
buttonsMessage.footerText = message.footer
}
if ('title' in message && !!message.title) {
buttonsMessage.text = message.title
buttonsMessage.headerType = WAProto_1.proto.Message.ButtonsMessage.HeaderType.TEXT
}
buttonsMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { buttonsMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('templateButtons' in message && !!message.templateButtons) {
const hydratedTemplate = {
hydratedButtons: message.templateButtons
}
if ('text' in message) {
hydratedTemplate.hydratedContentText = message.text
}
else {
if ('caption' in message) {
hydratedTemplate.hydratedContentText = message.caption
}
Object.assign(msg, m)
}
if ('footer' in message && !!message.footer) {
hydratedTemplate.hydratedFooterText = message.footer
}
hydratedTemplate.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { templateMessage: { hydratedTemplate }}
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('interactiveButtons' in message && !!message.interactiveButtons) {
const interactiveMessage = {
nativeFlowMessage: {
buttons: message.interactiveButtons
}
}
if ('text' in message) {
interactiveMessage.body = {
text: message.text
},
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: false
}
}
else {
if ('caption' in message) {
interactiveMessage.body = {
text: message.caption
}
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: message.hasMediaAttachment ? message.hasMediaAttachment : false,
...Object.assign(interactiveMessage, m)
}
}
}
if ('footer' in message && !!message.footer) {
interactiveMessage.footer = {
text: message.footer
}
}
interactiveMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { interactiveMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('shop' in message && !!message.shop) {
const interactiveMessage = {
shopStorefrontMessage: {
surface: message.shop.surface,
id: message.shop.id
}
}
if ('text' in message) {
interactiveMessage.body = {
text: message.text
},
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: false
}
}
else {
if ('caption' in message) {
interactiveMessage.body = {
text: message.caption
}
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: message.hasMediaAttachment ? message.hasMediaAttachment : false,
...Object.assign(interactiveMessage, m)
}
}
}
if ('footer' in message && !!message.footer) {
interactiveMessage.footer = {
text: message.footer
}
}
interactiveMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { interactiveMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('collection' in message && !!message.collection) {
const interactiveMessage = {
collectionMessage: {
bizJid: message.collection.bizJid,
id: message.collection.id,
messageVersion: message?.collection?.version
}
}
if ('text' in message) {
interactiveMessage.body = {
text: message.text
},
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: false
}
}
else {
if ('caption' in message) {
interactiveMessage.body = {
text: message.caption
}
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: message.hasMediaAttachment ? message.hasMediaAttachment : false,
...Object.assign(interactiveMessage, m)
}
}
}
if ('footer' in message && !message.footer) {
interactiveMessage.footer = {
text: message.footer
}
}
interactiveMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { interactiveMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
else if ('cards' in message && !!message.cards) {
const slides = await Promise.all(message.cards.map(async (slide) => {
const { image, video, product, title, body, footer, buttons } = slide
let header
if (product) {
const { imageMessage } = await prepareWAMessageMedia({ image: product.productImage, ...options }, options)
header = {
productMessage: {
product: {
...product,
productImage: imageMessage,
},
...slide
}
}
}
else if (image) {
header = await prepareWAMessageMedia({ image: image, ...options }, options)
}
else if (video) {
header = await prepareWAMessageMedia({ video: video, ...options }, options)
}
const msg = {
header: {
title,
hasMediaAttachment: true,
...header
},
body: {
text: body
},
footer: {
text: footer
},
nativeFlowMessage: {
buttons,
}
}
return msg
}))
const interactiveMessage = {
carouselMessage: {
cards: slides
}
}
if ('text' in message) {
interactiveMessage.body = {
text: message.text
},
interactiveMessage.header = {
title: message.title,
subtitle: message.subtitle,
hasMediaAttachment: false
}
}
if ('footer' in message && !!message.footer) {
interactiveMessage.footer = {
text: message.footer
}
}
interactiveMessage.contextInfo = {
...(message.contextInfo || {}),
...(message.mentions ? { mentionedJid: message.mentions } : {})
}
m = { interactiveMessage }
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
}
if ('ephemeral' in message && !!message.ephemeral) {
m = { ephemeralMessage: { message: m } }
}
if ('viewOnce' in message && !!message.viewOnce) {
m = { viewOnceMessage: { message: m } }
}
if ('viewOnceV2' in message && !!message.viewOnceV2) {
m = { viewOnceMessageV2: { message: m } }
}
if ('viewOnceV2Ext' in message && !!message.viewOnceV2Ext) {
m = { viewOnceMessageV2Extension: { message: m } }
}
if ('edit' in message) {
m.messageContextInfo = {
messageSecret: crypto_1.randomBytes(32)
}
m = {
protocolMessage: {
key: message.edit,
editedMessage: m,
timestampMs: Date.now(),
type: Types_1.WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
}
}
}
return Types_1.WAProto.Message.fromObject(m)
}
const generateWAMessageFromContent = (jid, message, options) => {
if (!options.timestamp) {
options.timestamp = new Date()
}
const innerMessage = normalizeMessageContent(message)
const key = getContentType(innerMessage)
const timestamp = generics_1.unixTimestampSeconds(options.timestamp)
const { quoted, userJid } = options
if (quoted && !WABinary_1.isJidNewsletter(jid)) {
const participant = quoted.key.fromMe
? userJid
: quoted.participant || quoted.key.participant || quoted.key.remoteJid
let quotedMsg = normalizeMessageContent(quoted.message)
const msgType = getContentType(quotedMsg)
quotedMsg = WAProto_1.proto.Message.fromObject({ [msgType]: quotedMsg[msgType] })
const quotedContent = quotedMsg[msgType]
if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
delete quotedContent.contextInfo
}
let requestPayment
if (key === 'requestPaymentMessage') {
if (innerMessage?.requestPaymentMessage && innerMessage?.requestPaymentMessage?.noteMessage?.extendedTextMessage) {
requestPayment = innerMessage?.requestPaymentMessage?.noteMessage?.extendedTextMessage
} else if (innerMessage?.requestPaymentMessage && innerMessage?.requestPaymentMessage?.noteMessage?.stickerMessage) {
requestPayment = innerMessage.requestPaymentMessage?.noteMessage?.stickerMessage
}
}
const contextInfo = (key === 'requestPaymentMessage' ? requestPayment?.contextInfo : innerMessage[key].contextInfo) || {}
contextInfo.participant = WABinary_1.jidNormalizedUser(participant)
contextInfo.stanzaId = quoted.key.id
contextInfo.quotedMessage = quotedMsg
if (jid !== quoted.key.remoteJid) {
contextInfo.remoteJid = quoted.key.remoteJid
}
if (key === 'requestPaymentMessage' && requestPayment) {
requestPayment.contextInfo = contextInfo
} else {
innerMessage[key].contextInfo = contextInfo
}
}
if (!!options?.ephemeralExpiration &&
key !== 'protocolMessage' &&
key !== 'ephemeralMessage' &&
!WABinary_1.isJidNewsletter(jid)) {
innerMessage[key].contextInfo = {
...(innerMessage[key].contextInfo || {}),
expiration: options.ephemeralExpiration || Defaults_1.WA_DEFAULT_EPHEMERAL
}
}
message = Types_1.WAProto.Message.fromObject(message)
const messageJSON = {
key: {
remoteJid: jid,
fromMe: true,
id: options?.messageId || generics_1.generateMessageID()
},
message: message,
messageTimestamp: timestamp,
messageStubParameters: [],
participant: WABinary_1.isJidGroup(jid) || WABinary_1.isJidStatusBroadcast(jid) ? userJid : undefined,
status: Types_1.WAMessageStatus.PENDING
}
return Types_1.WAProto.WebMessageInfo.fromObject(messageJSON)
}
const generateWAMessage = async (jid, content, options) => {
options.logger = options?.logger?.child({ msgId: options.messageId })
return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { newsletter: WABinary_1.isJidNewsletter(jid), ...options }), options)
}
const getContentType = (content) => {
if (content) {
const keys = Object.keys(content)
const key = keys.find(k => (k === 'conversation' || k.endsWith('Message') || k.endsWith('V2') || k.endsWith('V3') || k.endsWith('V4') || k.endsWith('V5')) && k !== 'senderKeyDistributionMessage' && k !== 'messageContextInfo')
return key
}
}
/**
* 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
}
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.editedMessage)
|| (message === null || message === void 0 ? void 0 : message.statusAddYours)
|| (message === null || message === void 0 ? void 0 : message.botTaskMessage)
|| (message === null || message === void 0 ? void 0 : message.eventCoverImage)
|| (message === null || message === void 0 ? void 0 : message.questionMessage)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessage)
|| (message === null || message === void 0 ? void 0 : message.botInvokeMessage)
|| (message === null || message === void 0 ? void 0 : message.ephemeralMessage)
|| (message === null || message === void 0 ? void 0 : message.lottieStickerMessage)
|| (message === null || message === void 0 ? void 0 : message.groupStatusMessage)
|| (message === null || message === void 0 ? void 0 : message.limitSharingMessage)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2)
|| (message === null || message === void 0 ? void 0 : message.botForwardedMessage)
|| (message === null || message === void 0 ? void 0 : message.statusMentionMessage)
|| (message === null || message === void 0 ? void 0 : message.groupStatusMessageV2)
|| (message === null || message === void 0 ? void 0 : message.pollCreationMessageV4)
|| (message === null || message === void 0 ? void 0 : message.pollCreationMessageV5)
|| (message === null || message === void 0 ? void 0 : message.associatedChildMessage)
|| (message === null || message === void 0 ? void 0 : message.groupMentionedMessage)
|| (message === null || message === void 0 ? void 0 : message.groupStatusMentionMessage)
|| (message === null || message === void 0 ? void 0 : message.viewOnceMessageV2Extension)
|| (message === null || message === void 0 ? void 0 : message.documentWithCaptionMessage)
|| (message === null || message === void 0 ? void 0 : message.pollCreationOptionImageMessage))
}
}
/**
* Extract the true message content from a message
* Eg. extracts the inner message from a disappearing message/view once message
*/
const extractMessageContent = (content) => {
let _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p
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 = normalizeMessageContent(content)
if (content === null || content === void 0 ? void 0 : content.buttonsMessage) {
return extractFromTemplateMessage(content.buttonsMessage)
}
if (content === null || content === void 0 ? void 0 : content.listMessage) {
return extractFromTemplateMessage(content.listMessage)
}
if ((_a = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _a === void 0 ? void 0 : _a.interactiveMessageTemplate) {
return extractFromTemplateMessage((_b = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _b === void 0 ? void 0 : _b.interactiveMessageTemplate)
}
if ((_c = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _c === void 0 ? void 0 : _c.hydratedFourRowTemplate) {
return extractFromTemplateMessage((_d = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _d === void 0 ? void 0 : _d.hydratedFourRowTemplate)
}
if ((_e = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _e === void 0 ? void 0 : _e.hydratedTemplate) {
return extractFromTemplateMessage((_f = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _f === void 0 ? void 0 : _f.hydratedTemplate)
}
if ((_g = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _g === void 0 ? void 0 : _g.fourRowTemplate) {
return extractFromTemplateMessage((_h = content === null || content === void 0 ? void 0 : content.templateMessage) === null || _h === void 0 ? void 0 : _h.fourRowTemplate)
}
if ((_i = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _i === void 0 ? void 0 : _i.shopStorefrontMessage) {
return extractFromTemplateMessage((_j = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _j === void 0 ? void 0 : _j.shopStorefrontMessage)
}
if ((_k = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _k === void 0 ? void 0 : _k.collectionMessage) {
return extractFromTemplateMessage((_l = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _l === void 0 ? void 0 : _l.collectionMessage)
}
if ((_m = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _m === void 0 ? void 0 : _m.nativeFlowMessage) {
return extractFromTemplateMessage((_n = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _n === void 0 ? void 0 : _n.nativeFlowMessage)
}
if ((_o = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _o === void 0 ? void 0 : _o.carouselMessage) {
return extractFromTemplateMessage((_p = content === null || content === void 0 ? void 0 : content.interactiveMessage) === null || _p === void 0 ? void 0 : _p.carouselMessage)
}
return content
}
/**
* 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'
/** 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)
}
}
/** Update the message with a new reaction */
const updateMessageWithReaction = (msg, reaction) => {
const authorID = generics_1.getKeyAuthor(reaction.key)
const reactions = (msg.reactions || [])
.filter(r => generics_1.getKeyAuthor(r.key) !== authorID)
if (reaction.text) {
reactions.push(reaction)
}
msg.reactions = reactions
}
/** Update the message with a new poll update */
const updateMessageWithPollUpdate = (msg, update) => {
const authorID = generics_1.getKeyAuthor(update.pollUpdateMessageKey)
const votes = (msg.pollUpdates || [])
.filter(r => generics_1.getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
if (update.vote?.selectedOptions?.length) {
votes.push(update)
}
msg.pollUpdates = votes
}
/**
* 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) {
message = normalizeMessageContent(message)
const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
const voteHashMap = opts.reduce((acc, opt) => {
const hash = 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(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)
}
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) => {
if (ctx && axios_1.default.isAxiosError(error) && // check if the message requires a reupload
REUPLOAD_REQUIRED_STATUS.includes(error.response?.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 = extractMessageContent(message.message)
if (!mContent) {
throw new boom_1.Boom('No message present', { statusCode: 400, data: message })
}
const contentType = getContentType(mContent)
let mediaType = 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 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
}
}
/** Checks whether the given message is a media message if it is returns the inner content */
const assertMediaContent = (content) => {
content = extractMessageContent(content)
const mediaContent = content?.documentMessage
|| content?.imageMessage
|| content?.videoMessage
|| content?.audioMessage
|| content?.stickerMessage
if (!mediaContent) {
throw new boom_1.Boom('given message is not a media message', { statusCode: 400, data: content })
}
return mediaContent
}
/**
* this is an experimental patch to make buttons work
* Don't know how it works, but it does for now
*/
const patchMessageForMdIfRequired = (message) => {
if (message?.buttonsMessage ||
message?.templateMessage ||
message?.listMessage ||
message?.interactiveMessage?.nativeFlowMesaage
) {
message = {
viewOnceMessageV2Extension: {
message: {
messageContextInfo: {
deviceListMetadataVersion: 2,
deviceListMetadata: {}
},
...message
}
}
}
}
return message
}
module.exports = {
extractUrlFromText,
generateLinkPreviewIfRequired,
prepareWAMessageMedia,
prepareDisappearingMessageSettingContent,
generateForwardMessageContent,
generateWAMessageContent,
generateWAMessageFromContent,
generateWAMessage,
getContentType,
normalizeMessageContent,
extractMessageContent,
getDevice,
updateMessageWithReceipt,
updateMessageWithReaction,
updateMessageWithPollUpdate,
getAggregateVotesInPollMessage,
aggregateMessageKeysNotFromMe,
downloadMediaMessage,
assertMediaContent,
patchMessageForMdIfRequired
}