@nexustechpro/baileys 2.0.5 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,108 +1,141 @@
1
- import { Boom } from '@hapi/boom';
2
- import { randomBytes } from 'crypto';
3
- import { promises as fs } from 'fs';
4
- import { zip } from 'fflate';
5
- import { proto } from '../../WAProto/index.js';
6
- import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
7
- import { WAMessageStatus, WAProto } from '../Types/index.js';
8
- import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
- import { sha256 } from './crypto.js';
10
- import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
- import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js';
12
-
13
- 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' };
14
- const MessageTypeProto = { image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage, audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage, document: WAProto.Message.DocumentMessage };
15
-
16
- // High-level content keys that need processing (not raw WAProto)
17
- const HIGH_LEVEL_KEYS = ['text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location', 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack', 'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin', 'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce', 'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons', 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop'];
18
-
19
- // ===== UTILITIES =====
20
- export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
1
+ import { Boom } from '@hapi/boom'
2
+ import { randomBytes } from 'crypto'
3
+ import { promises as fs } from 'fs'
4
+ import { zip } from 'fflate'
5
+ import { proto } from '../../WAProto/index.js'
6
+ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js'
7
+ import { WAMessageStatus, WAProto } from '../Types/index.js'
8
+ import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js'
9
+ import { sha256 } from './crypto.js'
10
+ import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js'
11
+ import { downloadContentFromMessage, encryptedStream, prepareStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData, getStream, toBuffer, getImageProcessingLibrary } from './messages-media.js'
12
+ import { shouldIncludeReportingToken } from './reporting-utils.js'
13
+
14
+ // ─── CONSTANTS ────────────────────────────────────────────────────────────────
15
+ const MIMETYPE_MAP = {
16
+ image: 'image/jpeg', video: 'video/mp4', document: 'application/pdf',
17
+ audio: 'audio/ogg; codecs=opus', sticker: 'image/webp', 'product-catalog-image': 'image/jpeg'
18
+ }
19
+
20
+ const MessageTypeProto = {
21
+ image: WAProto.Message.ImageMessage, video: WAProto.Message.VideoMessage,
22
+ audio: WAProto.Message.AudioMessage, sticker: WAProto.Message.StickerMessage,
23
+ document: WAProto.Message.DocumentMessage
24
+ }
25
+
26
+ // High-level keys that require processing — NOT raw WAProto passthrough
27
+ const HIGH_LEVEL_KEYS = [
28
+ 'text', 'image', 'video', 'audio', 'document', 'sticker', 'contacts', 'location',
29
+ 'react', 'delete', 'forward', 'disappearingMessagesInChat', 'groupInvite', 'stickerPack',
30
+ 'pin', 'buttonReply', 'ptv', 'product', 'listReply', 'event', 'poll', 'inviteAdmin',
31
+ 'requestPayment', 'sharePhoneNumber', 'requestPhoneNumber', 'limitSharing', 'viewOnce',
32
+ 'mentions', 'edit', 'buttons', 'templateButtons', 'sections', 'interactiveButtons',
33
+ 'album', 'call', 'paymentInvite', 'order', 'keep', 'shop', 'payment'
34
+ ]
35
+
36
+ // ─── UTILITIES ────────────────────────────────────────────────────────────────
37
+ export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0]
21
38
 
22
39
  export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
23
- const url = extractUrlFromText(text);
24
- if (!getUrlInfo || !url) return;
25
- try { return await getUrlInfo(url); }
26
- catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed'); }
27
- };
40
+ const url = extractUrlFromText(text)
41
+ if (!getUrlInfo || !url) return
42
+ try { return await getUrlInfo(url) }
43
+ catch (e) { logger?.warn({ trace: e.stack }, 'url generation failed') }
44
+ }
28
45
 
29
46
  const assertColor = (color) => {
30
- if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1;
31
- let hex = color.trim().replace('#', '');
32
- return parseInt((hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex), 16);
33
- };
47
+ if (typeof color === 'number') return color > 0 ? color : 0xffffffff + Number(color) + 1
48
+ const hex = color.trim().replace('#', '')
49
+ return parseInt(hex.length <= 6 ? 'FF' + hex.padStart(6, '0') : hex, 16)
50
+ }
34
51
 
35
52
  export const getContentType = (content) => {
36
- if (!content) return;
37
- const keys = Object.keys(content);
38
- return keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
39
- };
53
+ if (!content) return
54
+ return Object.keys(content).find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage')
55
+ }
40
56
 
41
57
  export const normalizeMessageContent = (content) => {
42
- if (!content) return;
58
+ if (!content) return
43
59
  for (let i = 0; i < 5; i++) {
44
- const inner = content?.ephemeralMessage || content?.viewOnceMessage || content?.documentWithCaptionMessage || content?.viewOnceMessageV2 || content?.viewOnceMessageV2Extension || content?.editedMessage;
45
- if (!inner) break;
46
- content = inner.message;
60
+ const inner = (
61
+ content?.ephemeralMessage || content?.viewOnceMessage ||
62
+ content?.documentWithCaptionMessage || content?.viewOnceMessageV2 ||
63
+ content?.viewOnceMessageV2Extension || content?.editedMessage ||
64
+ content?.groupMentionedMessage || content?.botInvokeMessage ||
65
+ content?.lottieStickerMessage || content?.eventCoverImage ||
66
+ content?.statusMentionMessage || content?.pollCreationOptionImageMessage ||
67
+ content?.associatedChildMessage || content?.groupStatusMentionMessage ||
68
+ content?.pollCreationMessageV4 || content?.pollCreationMessageV5 ||
69
+ content?.statusAddYours || content?.groupStatusMessage ||
70
+ content?.limitSharingMessage || content?.botTaskMessage ||
71
+ content?.questionMessage || content?.groupStatusMessageV2 ||
72
+ content?.botForwardedMessage
73
+ )
74
+ if (!inner) break
75
+ content = inner.message
47
76
  }
48
- return content;
49
- };
77
+ return content
78
+ }
50
79
 
51
80
  export const extractMessageContent = (content) => {
52
- content = normalizeMessageContent(content);
53
- const extractTemplate = (msg) => msg.imageMessage ? { imageMessage: msg.imageMessage } : msg.documentMessage ? { documentMessage: msg.documentMessage } : msg.videoMessage ? { videoMessage: msg.videoMessage } : msg.locationMessage ? { locationMessage: msg.locationMessage } : { conversation: msg.contentText || msg.hydratedContentText || '' };
54
- return content?.buttonsMessage ? extractTemplate(content.buttonsMessage) : content?.templateMessage?.hydratedFourRowTemplate ? extractTemplate(content.templateMessage.hydratedFourRowTemplate) : content?.templateMessage?.hydratedTemplate ? extractTemplate(content.templateMessage.hydratedTemplate) : content?.templateMessage?.fourRowTemplate ? extractTemplate(content.templateMessage.fourRowTemplate) : content;
55
- };
81
+ content = normalizeMessageContent(content)
82
+ const extractFromButtons = (msg) => {
83
+ const header = typeof msg.header === 'object' && msg.header !== null
84
+ if ((header ? msg.header?.imageMessage : msg.imageMessage)) return { imageMessage: header ? msg.header.imageMessage : msg.imageMessage }
85
+ if ((header ? msg.header?.documentMessage : msg.documentMessage)) return { documentMessage: header ? msg.header.documentMessage : msg.documentMessage }
86
+ if ((header ? msg.header?.videoMessage : msg.videoMessage)) return { videoMessage: header ? msg.header.videoMessage : msg.videoMessage }
87
+ if ((header ? msg.header?.locationMessage : msg.locationMessage)) return { locationMessage: header ? msg.header.locationMessage : msg.locationMessage }
88
+ if ((header ? msg.header?.productMessage : msg.productMessage)) return { productMessage: header ? msg.header.productMessage : msg.productMessage }
89
+ return { conversation: 'contentText' in msg ? msg.contentText : ('hydratedContentText' in msg ? msg.hydratedContentText : 'body' in msg ? msg.body?.text : '') || '' }
90
+ }
91
+ if (content?.buttonsMessage) return extractFromButtons(content.buttonsMessage)
92
+ if (content?.interactiveMessage) return extractFromButtons(content.interactiveMessage)
93
+ if (content?.templateMessage?.interactiveMessageTemplate) return extractFromButtons(content.templateMessage.interactiveMessageTemplate)
94
+ if (content?.templateMessage?.hydratedFourRowTemplate) return extractFromButtons(content.templateMessage.hydratedFourRowTemplate)
95
+ if (content?.templateMessage?.hydratedTemplate) return extractFromButtons(content.templateMessage.hydratedTemplate)
96
+ if (content?.templateMessage?.fourRowTemplate) return extractFromButtons(content.templateMessage.fourRowTemplate)
97
+ return content
98
+ }
56
99
 
57
- // ===== MEDIA PREPARATION =====
100
+ // ─── MEDIA PREPARATION ────────────────────────────────────────────────────────
58
101
  export const prepareWAMessageMedia = async (message, options) => {
59
- let mediaType = MEDIA_KEYS.find(key => key in message)
102
+ const mediaType = MEDIA_KEYS.find(k => k in message)
60
103
  if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
61
-
62
104
  const uploadData = { ...message, media: message[mediaType] }
63
105
  delete uploadData[mediaType]
64
-
65
- const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache ? `${mediaType}:${uploadData.media.url.toString()}` : null
66
-
67
106
  if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
68
107
  if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
69
-
108
+ const cacheableKey = typeof uploadData.media === 'object' && 'url' in uploadData.media && uploadData.media.url && options.mediaCache
109
+ ? `${mediaType}:${uploadData.media.url.toString()}` : null
70
110
  if (cacheableKey) {
71
111
  const cached = await options.mediaCache?.get(cacheableKey)
72
112
  if (cached) {
73
- const obj = proto.Message.decode(cached)
113
+ options.logger?.debug({ cacheableKey }, 'got media cache hit')
114
+ const obj = WAProto.Message.decode(cached)
74
115
  Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
75
116
  return obj
76
117
  }
77
118
  }
78
-
119
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid)
79
120
  const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
80
121
  const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
81
122
  const requiresWaveformProcessing = mediaType === 'audio' && (uploadData.ptt === true || !!options.backgroundColor)
82
- const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
83
-
84
- const encryptionResult = await (options.newsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
123
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation || requiresWaveformProcessing
124
+ const encryptionResult = await (isNewsletter ? prepareStream : encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
85
125
  logger: options.logger,
86
126
  saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
87
127
  opts: options.options,
88
128
  isPtt: uploadData.ptt,
89
- forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
129
+ forceOpus: mediaType === 'audio' && uploadData.mimetype?.includes('opus'),
90
130
  convertVideo: mediaType === 'video'
91
131
  })
92
-
93
- // ✅ FIX: Extract the correct values based on encryption method
94
132
  const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
95
-
96
133
  if (mediaType === 'audio' && opusConverted) uploadData.mimetype = 'audio/ogg; codecs=opus'
97
-
98
- const fileEncSha256B64 = (options.newsletter ? fileSha256 : fileEncSha256 ?? fileSha256).toString('base64')
99
-
100
- // ✅ FIX: Determine what to upload - use encFilePath for encrypted, encWriteStream for newsletter
101
- const uploadStream = options.newsletter ? encWriteStream : (encFilePath || encWriteStream)
102
-
134
+ const fileEncSha256B64 = (isNewsletter ? fileSha256 : (fileEncSha256 ?? fileSha256)).toString('base64')
135
+ const uploadSource = isNewsletter ? encWriteStream : (encFilePath || encWriteStream)
103
136
  const [{ mediaUrl, directPath, handle }] = await Promise.all([
104
137
  (async () => {
105
- const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
138
+ const result = await options.upload(uploadSource, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
106
139
  options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
107
140
  return result
108
141
  })(),
@@ -114,14 +147,20 @@ export const prepareWAMessageMedia = async (message, options) => {
114
147
  if (!uploadData.width && originalImageDimensions) {
115
148
  uploadData.width = originalImageDimensions.width
116
149
  uploadData.height = originalImageDimensions.height
150
+ options.logger?.debug('set dimensions')
117
151
  }
152
+ options.logger?.debug('generated thumbnail')
153
+ }
154
+ if (requiresDurationComputation) {
155
+ uploadData.seconds = await getAudioDuration(bodyPath)
156
+ options.logger?.debug('computed audio duration')
118
157
  }
119
- if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
120
158
  if (requiresWaveformProcessing) {
121
159
  try {
122
160
  uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
123
- } catch (err) {
124
- options.logger?.warn('Failed to generate waveform, using fallback')
161
+ options.logger?.debug('processed waveform')
162
+ } catch {
163
+ options.logger?.warn('failed to generate waveform, using fallback')
125
164
  uploadData.waveform = new Uint8Array([0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 88, 99, 0, 99, 0, 55, 0, 99])
126
165
  }
127
166
  }
@@ -130,94 +169,88 @@ export const prepareWAMessageMedia = async (message, options) => {
130
169
  })()
131
170
  ]).finally(async () => {
132
171
  if (encWriteStream && !Buffer.isBuffer(encWriteStream)) encWriteStream.destroy?.()
133
- // FIX: Clean up encrypted file path
134
- if (encFilePath && typeof encFilePath === 'string') {
135
- try {
136
- await fs.unlink(encFilePath)
137
- } catch { }
138
- }
139
- if (didSaveToTmpPath && bodyPath) {
140
- try {
141
- await fs.access(bodyPath)
142
- await fs.unlink(bodyPath)
143
- } catch { }
144
- }
172
+ if (encFilePath && typeof encFilePath === 'string') try { await fs.unlink(encFilePath) } catch { }
173
+ if (didSaveToTmpPath && bodyPath) try { await fs.access(bodyPath); await fs.unlink(bodyPath) } catch { }
145
174
  })
146
-
147
175
  const obj = WAProto.Message.fromObject({
148
176
  [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
149
- url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
150
- mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
177
+ url: handle ? undefined : mediaUrl,
178
+ directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
179
+ mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(),
180
+ ...uploadData, media: undefined
151
181
  })
152
182
  })
153
-
154
183
  if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
155
- if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
184
+ if (cacheableKey) {
185
+ options.logger?.debug({ cacheableKey }, 'set cache')
186
+ await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
187
+ }
156
188
  return obj
157
189
  }
158
190
 
159
191
  export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
160
192
  ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
161
- });
193
+ })
162
194
 
163
195
  export const generateForwardMessageContent = (message, forceForward) => {
164
- const content = proto.Message.decode(proto.Message.encode(normalizeMessageContent(message.message)).finish());
165
- let key = Object.keys(content)[0];
166
- let score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1);
167
-
196
+ let content = normalizeMessageContent(message.message)
197
+ if (!content) throw new Boom('no content in message', { statusCode: 400 })
198
+ content = proto.Message.decode(proto.Message.encode(content).finish())
199
+ let key = Object.keys(content)[0]
200
+ let score = (content?.[key]?.contextInfo?.forwardingScore || 0) + (message.key.fromMe && !forceForward ? 0 : 1)
168
201
  if (key === 'conversation') {
169
- content.extendedTextMessage = { text: content[key] };
170
- delete content.conversation;
171
- key = 'extendedTextMessage';
202
+ content.extendedTextMessage = { text: content[key] }
203
+ delete content.conversation
204
+ key = 'extendedTextMessage'
172
205
  }
206
+ content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {}
207
+ return content
208
+ }
173
209
 
174
- content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
175
- return content;
176
- };
177
-
178
- // ===== MESSAGE HANDLERS =====
210
+ // ─── SUB-HANDLERS ─────────────────────────────────────────────────────────────
179
211
  const handleTextMessage = async (message, options) => {
180
- const extContent = { text: message.text };
181
- let urlInfo = message.linkPreview || await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
182
-
212
+ const extContent = { text: message.text }
213
+ let urlInfo = message.linkPreview
214
+ if (typeof urlInfo === 'undefined') urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger)
183
215
  if (urlInfo) {
184
216
  Object.assign(extContent, {
185
217
  matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
186
218
  description: urlInfo.description, title: urlInfo.title, previewType: 0
187
- });
188
- if (urlInfo.highQualityThumbnail) {
189
- const img = urlInfo.highQualityThumbnail;
190
- Object.assign(extContent, {
191
- thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
192
- thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
193
- });
194
- }
219
+ })
220
+ const img = urlInfo.highQualityThumbnail
221
+ if (img) Object.assign(extContent, {
222
+ thumbnailDirectPath: img.directPath, mediaKey: img.mediaKey, mediaKeyTimestamp: img.mediaKeyTimestamp,
223
+ thumbnailWidth: img.width, thumbnailHeight: img.height, thumbnailSha256: img.fileSha256, thumbnailEncSha256: img.fileEncSha256
224
+ })
195
225
  }
196
-
197
- if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
198
- if (options.font) extContent.font = options.font;
199
- return { extendedTextMessage: extContent };
200
- };
226
+ if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor)
227
+ if (options.font) extContent.font = options.font
228
+ return { extendedTextMessage: extContent }
229
+ }
201
230
 
202
231
  const handleSpecialMessages = async (message, options) => {
203
232
  if ('contacts' in message) {
204
- const { contacts } = message.contacts;
205
- if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 });
206
- return contacts.length === 1 ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) } : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) };
233
+ const { contacts } = message.contacts
234
+ if (!contacts.length) throw new Boom('require atleast 1 contact', { statusCode: 400 })
235
+ return contacts.length === 1
236
+ ? { contactMessage: WAProto.Message.ContactMessage.create(contacts[0]) }
237
+ : { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.create(message.contacts) }
207
238
  }
208
- if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
239
+ if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) }
209
240
  if ('react' in message) {
210
- if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now();
211
- return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) };
241
+ if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now()
242
+ return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) }
212
243
  }
213
- if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } };
214
- if ('forward' in message) return generateForwardMessageContent(message.forward, message.force);
244
+ if ('delete' in message) return { protocolMessage: { key: message.delete, type: WAProto.Message.ProtocolMessage.Type.REVOKE } }
245
+ if ('forward' in message) return generateForwardMessageContent(message.forward, message.force)
215
246
  if ('disappearingMessagesInChat' in message) {
216
- const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
217
- return prepareDisappearingMessageSettingContent(exp);
247
+ const exp = typeof message.disappearingMessagesInChat === 'boolean'
248
+ ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0)
249
+ : message.disappearingMessagesInChat
250
+ return prepareDisappearingMessageSettingContent(exp)
218
251
  }
219
- return null;
220
- };
252
+ return null
253
+ }
221
254
 
222
255
  const handleGroupInvite = async (message, options) => {
223
256
  const m = {
@@ -225,20 +258,19 @@ const handleGroupInvite = async (message, options) => {
225
258
  inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
226
259
  caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
227
260
  }
228
- };
229
-
261
+ }
230
262
  if (options.getProfilePicUrl) {
231
- const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
263
+ const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview')
232
264
  if (pfpUrl) {
233
- const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
234
- if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer());
265
+ const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher })
266
+ if (resp.ok) m.groupInviteMessage.jpegThumbnail = Buffer.from(await resp.arrayBuffer())
235
267
  }
236
268
  }
237
- return m;
238
- };
269
+ return m
270
+ }
239
271
 
240
- const handleEventMessage = (message, options) => {
241
- const startTime = Math.floor(message.event.startDate.getTime() / 1000);
272
+ const handleEventMessage = async (message, options) => {
273
+ const startTime = Math.floor(message.event.startDate.getTime() / 1000)
242
274
  const m = {
243
275
  eventMessage: {
244
276
  name: message.event.name, description: message.event.description, startTime,
@@ -247,460 +279,543 @@ const handleEventMessage = (message, options) => {
247
279
  isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
248
280
  },
249
281
  messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
250
- };
251
-
282
+ }
252
283
  if (message.event.call && options.getCallLink) {
253
- options.getCallLink(message.event.call, { startTime }).then(token => {
254
- m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
255
- });
284
+ const token = await options.getCallLink(message.event.call, { startTime })
285
+ m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token
256
286
  }
257
- return m;
258
- };
287
+ return m
288
+ }
259
289
 
260
290
  const handlePollMessage = (message) => {
261
- message.poll.selectableCount ||= 0;
262
- message.poll.toAnnouncementGroup ||= false;
263
-
264
- if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 });
291
+ message.poll.selectableCount ||= 0
292
+ message.poll.toAnnouncementGroup ||= false
293
+ if (!Array.isArray(message.poll.values)) throw new Boom('Invalid poll values', { statusCode: 400 })
265
294
  if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length)
266
- throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 });
267
-
268
- const pollMsg = { name: message.poll.name, selectableOptionsCount: message.poll.selectableCount, options: message.poll.values.map(optionName => ({ optionName })) };
269
- const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } };
270
- if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg;
271
- else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg;
272
- else m.pollCreationMessage = pollMsg;
273
- return m;
274
- };
295
+ throw new Boom(`poll.selectableCount should be >= 0 and <= ${message.poll.values.length}`, { statusCode: 400 })
296
+ const pollMsg = {
297
+ name: message.poll.name,
298
+ selectableOptionsCount: message.poll.selectableCount,
299
+ options: message.poll.values.map(optionName => ({ optionName }))
300
+ }
301
+ const m = { messageContextInfo: { messageSecret: message.poll.messageSecret || randomBytes(32) } }
302
+ if (message.poll.toAnnouncementGroup) m.pollCreationMessageV2 = pollMsg
303
+ else if (message.poll.selectableCount === 1) m.pollCreationMessageV3 = pollMsg
304
+ else m.pollCreationMessage = pollMsg
305
+ return m
306
+ }
275
307
 
276
308
  const handleProductMessage = async (message, options) => {
277
- const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
278
- return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) };
279
- };
309
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options)
310
+ return { productMessage: WAProto.Message.ProductMessage.create({ ...message, product: { ...message.product, productImage: imageMessage } }) }
311
+ }
280
312
 
281
313
  const handleRequestPayment = async (message, options) => {
282
- const sticker = message.requestPayment.sticker ? await prepareWAMessageMedia({ sticker: message.requestPayment.sticker }, options) : null;
283
- let notes = message.requestPayment.sticker
284
- ? { stickerMessage: { ...sticker.stickerMessage, contextInfo: message.requestPayment.contextInfo } }
285
- : message.requestPayment.note ? { extendedTextMessage: { text: message.requestPayment.note, contextInfo: message.requestPayment.contextInfo } } : null;
286
-
287
- if (!notes) throw new Boom('Invalid request payment', { statusCode: 400 });
288
-
314
+ const data = message.requestPayment || message.payment
315
+ const sticker = data.sticker ? await prepareWAMessageMedia({ sticker: data.sticker }, options) : null
316
+ let notes
317
+ if (sticker) {
318
+ notes = { stickerMessage: { ...sticker.stickerMessage, contextInfo: data.contextInfo } }
319
+ } else if (data.note) {
320
+ notes = { extendedTextMessage: { text: data.note, contextInfo: data.contextInfo } }
321
+ } else {
322
+ notes = { extendedTextMessage: { text: data.note || 'Notes' } }
323
+ }
289
324
  const m = {
290
325
  requestPaymentMessage: WAProto.Message.RequestPaymentMessage.fromObject({
291
- expiryTimestamp: message.requestPayment.expiryTimestamp || message.requestPayment.expiry,
292
- amount1000: message.requestPayment.amount1000 || message.requestPayment.amount,
293
- currencyCodeIso4217: message.requestPayment.currencyCodeIso4217 || message.requestPayment.currency,
294
- requestFrom: message.requestPayment.requestFrom || message.requestPayment.from,
295
- noteMessage: notes, background: message.requestPayment.background
326
+ expiryTimestamp: data.expiryTimestamp || data.expiry || 0,
327
+ amount1000: data.amount1000 || data.amount || 0,
328
+ currencyCodeIso4217: data.currencyCodeIso4217 || data.currency || 'IDR',
329
+ requestFrom: data.requestFrom || data.from || '0@s.whatsapp.net',
330
+ noteMessage: notes,
331
+ background: data.background ?? { id: 'DEFAULT', placeholderArgb: 0xfff0f0f0 }
296
332
  })
297
- };
298
-
299
- if (message.requestPayment.currencyCodeIso4217 === 'BRL' && message.requestPayment.pixKey) {
333
+ }
334
+ // BRL Pix key support
335
+ if ((data.currencyCodeIso4217 === 'BRL' || data.currency === 'BRL') && data.pixKey) {
300
336
  if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
301
- m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } };
302
- m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${message.requestPayment.pixKey}`;
337
+ m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } }
338
+ m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${data.pixKey}`
303
339
  }
340
+ return m
341
+ }
304
342
 
305
- return m;
306
- };
343
+ const handleButtonReply = (message) => {
344
+ switch (message.type) {
345
+ case 'list': return { listResponseMessage: { title: message.buttonReply.title, description: message.buttonReply.description, singleSelectReply: { selectedRowId: message.buttonReply.rowId }, lisType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT } }
346
+ case 'template': return { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } }
347
+ case 'interactive': return { interactiveResponseMessage: { body: { text: message.buttonReply.displayText, format: proto.Message.InteractiveResponseMessage.Body.Format.EXTENSIONS_1 }, nativeFlowResponseMessage: { name: message.buttonReply.nativeFlows?.name, paramsJson: message.buttonReply.nativeFlows?.paramsJson, version: message.buttonReply.nativeFlows?.version } } }
348
+ default: return { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT } }
349
+ }
350
+ }
307
351
 
308
- // ===== MAIN GENERATOR =====
352
+ // ─── MAIN GENERATOR ───────────────────────────────────────────────────────────
309
353
  export const generateWAMessageContent = async (message, options = {}) => {
310
- const messageKeys = Object.keys(message);
311
-
312
- // ===== SMART DETECTION =====
313
- const isRawProtoMessage = messageKeys.some(key =>
314
- key.endsWith('Message') &&
315
- typeof message[key] === 'object' &&
316
- !HIGH_LEVEL_KEYS.includes(key)
317
- );
354
+ const messageKeys = Object.keys(message)
318
355
 
319
- const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message);
320
-
321
- // Pass through raw protocol messages directly
322
- if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) {
323
- return WAProto.Message.create(message);
324
- }
325
-
326
- // If no high-level keys AND has proto message keys, pass through
327
- if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) {
328
- return WAProto.Message.create(message);
329
- }
356
+ // ─── PROTO PASSTHROUGH ────────────────────────────────────────────────────
357
+ const isRawProtoMessage = messageKeys.some(k => k.endsWith('Message') && typeof message[k] === 'object' && !HIGH_LEVEL_KEYS.includes(k))
358
+ const isWrapperMessage = ['viewOnceMessage', 'ephemeralMessage', 'viewOnceMessageV2', 'documentWithCaptionMessage'].some(k => k in message)
359
+ if ((isRawProtoMessage || isWrapperMessage) && messageKeys.length === 1) return WAProto.Message.create(message)
360
+ if (!messageKeys.some(k => HIGH_LEVEL_KEYS.includes(k)) && isRawProtoMessage) return WAProto.Message.create(message)
330
361
 
331
- let m = {};
362
+ let m = {}
332
363
 
333
- // ===== HANDLE TEXT =====
364
+ // ─── TEXT ─────────────────────────────────────────────────────────────────
334
365
  if ('text' in message && !('buttons' in message) && !('templateButtons' in message) && !('sections' in message) && !('interactiveButtons' in message) && !('shop' in message)) {
335
- m = await handleTextMessage(message, options);
366
+ m = await handleTextMessage(message, options)
336
367
  }
337
368
 
338
- // ===== HANDLE SPECIAL MESSAGES =====
369
+ // ─── SPECIAL / MEDIA ──────────────────────────────────────────────────────
339
370
  else {
340
- const special = await handleSpecialMessages(message, options);
341
- if (special) m = special;
342
- else if ('groupInvite' in message) m = await handleGroupInvite(message, options);
343
- else if ('stickerPack' in message) return await prepareStickerPackMessage(message.stickerPack, options);
344
- else if ('pin' in message) {
345
- const messageKey = typeof message.pin === 'boolean' ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation'); })()) : (message.pin && typeof message.pin === 'object') ? (message.pin.key || message.pin.stanzaId || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant || message.pin.sender } : null)) : message.pin;
346
- const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin && typeof message.pin === 'object' ? message.pin.unpin !== true : true);
347
- const pinTime = message.pin && typeof message.pin === 'object' ? message.pin.time : message.time;
348
- if (!messageKey || !messageKey.id) throw new Boom('Invalid message key for pin operation');
349
- m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } };
350
- }
351
- else if ('keep' in message) m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } };
352
- else if ('call' in message) m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } };
353
- else if ('paymentInvite' in message) m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } };
354
- else if ('buttonReply' in message) m = message.type === 'template' ? { templateButtonReplyMessage: { selectedDisplayText: message.buttonReply.displayText, selectedId: message.buttonReply.id, selectedIndex: message.buttonReply.index } } : { buttonsResponseMessage: { selectedButtonId: message.buttonReply.id, selectedDisplayText: message.buttonReply.displayText, type: 0 } };
355
- else if ('ptv' in message && message.ptv) {
356
- const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
357
- m = { ptvMessage: videoMessage };
358
- }
359
- else if ('product' in message) m = await handleProductMessage(message, options);
360
- else if ('order' in message) m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) };
361
- else if ('listReply' in message) m = { listResponseMessage: { ...message.listReply } };
362
- else if ('event' in message) m = handleEventMessage(message, options);
363
- else if ('poll' in message) m = handlePollMessage(message);
364
- else if ('inviteAdmin' in message) m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } };
365
- else if ('requestPayment' in message) m = await handleRequestPayment(message, options);
366
- else if ('extendedTextMessage' in message) m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) };
367
- else if ('interactiveMessage' in message) m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) };
368
- else if ('sharePhoneNumber' in message) m = { protocolMessage: { type: 4 } };
369
- else if ('requestPhoneNumber' in message) m = { requestPhoneNumberMessage: {} };
370
- else if ('limitSharing' in message) m = { protocolMessage: { type: 3, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } };
371
- else if ('album' in message) {
372
- const imageMessages = message.album.filter(item => 'image' in item);
373
- const videoMessages = message.album.filter(item => 'video' in item);
374
- m = { albumMessage: { expectedImageCount: imageMessages.length, expectedVideoCount: videoMessages.length } };
371
+ const special = await handleSpecialMessages(message, options)
372
+ if (special) {
373
+ m = special
374
+ } else if ('groupInvite' in message) {
375
+ m = await handleGroupInvite(message, options)
376
+ } else if ('stickerPack' in message) {
377
+ return WAProto.Message.create({ stickerPackMessage: (await prepareStickerPackMessage(message.stickerPack, options)).stickerPackMessage })
378
+ } else if ('pin' in message) {
379
+ const messageKey = typeof message.pin === 'boolean'
380
+ ? (options.quoted?.key || (() => { throw new Boom('No quoted message key found for pin operation') })())
381
+ : typeof message.pin === 'object'
382
+ ? (message.pin.key || (message.pin.id ? { remoteJid: options.jid, fromMe: message.pin.fromMe || false, id: message.pin.id, participant: message.pin.participant } : null))
383
+ : message.pin
384
+ const shouldPin = typeof message.pin === 'boolean' ? message.pin : (message.pin?.unpin !== true)
385
+ const pinTime = typeof message.pin === 'object' ? message.pin.time : message.time
386
+ if (!messageKey?.id) throw new Boom('Invalid message key for pin operation')
387
+ m = { pinInChatMessage: { key: messageKey, type: shouldPin ? 1 : 2, senderTimestampMs: Date.now().toString() }, messageContextInfo: { messageAddOnDurationInSecs: shouldPin ? (pinTime || 86400) : 0 } }
388
+ } else if ('keep' in message) {
389
+ m = { keepInChatMessage: { key: message.keep, keepType: message.type, timestampMs: Date.now() } }
390
+ } else if ('call' in message) {
391
+ m = { scheduledCallCreationMessage: { scheduledTimestampMs: message.call.time || Date.now(), callType: message.call.type || 1, title: message.call.title } }
392
+ } else if ('paymentInvite' in message) {
393
+ m = { paymentInviteMessage: { serviceType: message.paymentInvite.type, expiryTimestamp: message.paymentInvite.expiry } }
394
+ } else if ('buttonReply' in message) {
395
+ m = handleButtonReply(message)
396
+ } else if ('ptv' in message && message.ptv) {
397
+ const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options)
398
+ m = { ptvMessage: videoMessage }
399
+ } else if ('product' in message) {
400
+ m = await handleProductMessage(message, options)
401
+ } else if ('order' in message) {
402
+ m = { orderMessage: WAProto.Message.OrderMessage.fromObject({ orderId: message.order.id, thumbnail: message.order.thumbnail, itemCount: message.order.itemCount, status: message.order.status, surface: message.order.surface, orderTitle: message.order.title, message: message.order.text, sellerJid: message.order.seller, token: message.order.token, totalAmount1000: message.order.amount, totalCurrencyCode: message.order.currency }) }
403
+ } else if ('sections' in message && message.sections) {
404
+ m = {
405
+ listMessage: {
406
+ title: message.title, buttonText: message.buttonText, footerText: message.footer,
407
+ description: message.text, sections: message.sections,
408
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
409
+ contextInfo: { ...(message.contextInfo || {}), ...(message.mentions ? { mentionedJid: message.mentions } : {}) }
410
+ }
411
+ }
412
+ } else if ('listReply' in message) {
413
+ m = { listResponseMessage: { ...message.listReply } }
414
+ } else if ('event' in message) {
415
+ m = await handleEventMessage(message, options)
416
+ } else if ('poll' in message) {
417
+ m = handlePollMessage(message)
418
+ } else if ('inviteAdmin' in message) {
419
+ m = { newsletterAdminInviteMessage: { inviteExpiration: message.inviteAdmin.inviteExpiration, caption: message.inviteAdmin.text, newsletterJid: message.inviteAdmin.jid, newsletterName: message.inviteAdmin.subject, jpegThumbnail: message.inviteAdmin.thumbnail } }
420
+ } else if ('requestPayment' in message || 'payment' in message) {
421
+ m = await handleRequestPayment(message, options)
422
+ } else if ('extendedTextMessage' in message) {
423
+ m = { extendedTextMessage: WAProto.Message.ExtendedTextMessage.create(message.extendedTextMessage) }
424
+ } else if ('interactiveMessage' in message) {
425
+ m = { interactiveMessage: WAProto.Message.InteractiveMessage.create(message.interactiveMessage) }
426
+ } else if ('sharePhoneNumber' in message) {
427
+ m = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER } }
428
+ } else if ('requestPhoneNumber' in message) {
429
+ m = { requestPhoneNumberMessage: {} }
430
+ } else if ('limitSharing' in message) {
431
+ m = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING, limitSharing: { sharingLimited: message.limitSharing === true, trigger: 1, limitSharingSettingTimestamp: Date.now(), initiatedByMe: true } } }
432
+ } else if ('album' in message) {
433
+ const imageItems = message.album.filter(i => 'image' in i)
434
+ const videoItems = message.album.filter(i => 'video' in i)
435
+ m = { albumMessage: { expectedImageCount: imageItems.length, expectedVideoCount: videoItems.length } }
436
+ } else if (MEDIA_KEYS.some(k => k in message)) {
437
+ m = await prepareWAMessageMedia(message, options)
375
438
  }
376
- else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
377
439
  }
378
440
 
379
- // ===== SMART BUTTON HANDLING =====
441
+ // ─── SMART BUTTON HANDLING ────────────────────────────────────────────────
380
442
  if ('buttons' in message && Array.isArray(message.buttons) && message.buttons.length > 0) {
381
- const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson);
382
-
443
+ const hasNativeFlow = message.buttons.some(b => b.nativeFlowInfo || b.name || b.buttonParamsJson)
383
444
  if (hasNativeFlow) {
384
- // Convert to interactiveMessage
385
445
  const interactive = {
386
446
  body: { text: message.text || message.caption || message.contentText || '' },
387
447
  footer: { text: message.footer || message.footerText || '' },
388
448
  nativeFlowMessage: {
389
449
  buttons: message.buttons.map(btn => {
390
- if (btn.name && btn.buttonParamsJson) return btn;
391
- if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson };
392
- return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) };
450
+ if (btn.name && btn.buttonParamsJson) return btn
451
+ if (btn.nativeFlowInfo) return { name: btn.nativeFlowInfo.name, buttonParamsJson: btn.nativeFlowInfo.paramsJson }
452
+ return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: btn.buttonText?.displayText || btn.displayText || '', id: btn.buttonId || btn.id || '' }) }
393
453
  })
394
454
  }
395
- };
396
-
397
- if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
455
+ }
456
+ if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment || false }
398
457
  if (Object.keys(m).length > 0) {
399
- interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
400
- Object.assign(interactive.header, m);
458
+ interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true }
459
+ Object.assign(interactive.header, m)
401
460
  }
402
-
403
- m = { interactiveMessage: interactive };
461
+ m = { interactiveMessage: interactive }
404
462
  } else {
405
- // Old-style buttons
406
- const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) };
407
- if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY; }
408
- else { if ('caption' in message) buttonsMessage.contentText = message.caption; const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase(); buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY; Object.assign(buttonsMessage, m); }
409
- if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT; }
410
- if (message.footer) buttonsMessage.footerText = message.footer;
411
- m = { buttonsMessage };
463
+ const buttonsMessage = { buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })) }
464
+ if ('text' in message) { buttonsMessage.contentText = message.text; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY }
465
+ else {
466
+ if ('caption' in message) buttonsMessage.contentText = message.caption
467
+ const type = Object.keys(m)[0]?.replace('Message', '').toUpperCase()
468
+ buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type] || proto.Message.ButtonsMessage.HeaderType.EMPTY
469
+ Object.assign(buttonsMessage, m)
470
+ }
471
+ if (message.title) { buttonsMessage.text = message.title; buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT }
472
+ if (message.footer) buttonsMessage.footerText = message.footer
473
+ m = { buttonsMessage }
412
474
  }
413
475
  }
414
476
 
415
- // ===== TEMPLATE BUTTONS =====
416
- else if ('templateButtons' in message && !!message.templateButtons) {
417
- const msg = { hydratedButtons: message.templateButtons };
418
- if ('text' in message) msg.hydratedContentText = message.text;
419
- else { if ('caption' in message) msg.hydratedContentText = message.caption; Object.assign(msg, m); }
420
- if ('footer' in message && !!message.footer) msg.hydratedFooterText = message.footer;
421
- m = { templateMessage: { fourRowTemplate: msg, hydratedTemplate: msg } };
477
+ // ─── TEMPLATE BUTTONS ─────────────────────────────────────────────────────
478
+ else if ('templateButtons' in message && message.templateButtons) {
479
+ const hydratedTemplate = { hydratedButtons: message.templateButtons }
480
+ if ('text' in message) hydratedTemplate.hydratedContentText = message.text
481
+ else { if ('caption' in message) hydratedTemplate.hydratedContentText = message.caption; Object.assign(hydratedTemplate, m) }
482
+ if (message.footer) hydratedTemplate.hydratedFooterText = message.footer
483
+ m = { templateMessage: { fourRowTemplate: hydratedTemplate, hydratedTemplate } }
422
484
  }
423
485
 
424
- // ===== LIST MESSAGE =====
425
- else if ('sections' in message && !!message.sections) {
426
- m = { listMessage: { sections: message.sections, buttonText: message.buttonText, title: message.title, footerText: message.footer, description: message.text, listType: proto.Message.ListMessage.ListType.SINGLE_SELECT } };
486
+ // ─── INTERACTIVE BUTTONS ──────────────────────────────────────────────────
487
+ else if ('interactiveButtons' in message && message.interactiveButtons) {
488
+ const interactive = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) }
489
+ if ('text' in message) { interactive.body = { text: message.text }; interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: false } }
490
+ else if ('caption' in message) {
491
+ interactive.body = { text: message.caption }
492
+ interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0) }
493
+ if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
494
+ }
495
+ if (message.footer) interactive.footer = { text: message.footer }
496
+ m = { interactiveMessage: interactive, messageContextInfo: { messageSecret: randomBytes(32) } }
427
497
  }
428
498
 
429
- // ===== INTERACTIVE BUTTONS =====
430
- else if ('interactiveButtons' in message && !!message.interactiveButtons) {
431
- const interactiveMessage = { nativeFlowMessage: WAProto.Message.InteractiveMessage.NativeFlowMessage.fromObject({ buttons: message.interactiveButtons }) };
432
- if ('text' in message) interactiveMessage.body = { text: message.text };
433
- else if ('caption' in message) { interactiveMessage.body = { text: message.caption }; interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
434
- if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
435
- if ('title' in message && !!message.title) { interactiveMessage.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message?.media ?? false }; Object.assign(interactiveMessage.header, m); }
436
- m = { interactiveMessage };
499
+ // ─── SHOP MESSAGE ─────────────────────────────────────────────────────────
500
+ else if ('shop' in message && message.shop) {
501
+ const interactive = {
502
+ shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({ surface: message.shop.surface || 1, id: message.shop.id || message.id })
503
+ }
504
+ if ('text' in message) interactive.body = { text: message.text }
505
+ else if ('caption' in message) interactive.body = { text: message.caption }
506
+ if (message.title || Object.keys(m).length > 0) {
507
+ interactive.header = { title: message.title || '', subtitle: message.subtitle || '', hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0) }
508
+ if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
509
+ }
510
+ if (message.footer) interactive.footer = { text: message.footer }
511
+ m = { interactiveMessage: interactive }
437
512
  }
438
513
 
439
- // ===== SHOP MESSAGE (YOUR EXAMPLE) =====
440
- else if ('shop' in message && !!message.shop) {
441
- const interactiveMessage = {
442
- shopStorefrontMessage: WAProto.Message.InteractiveMessage.ShopMessage.fromObject({
443
- surface: message.shop.surface || 1,
444
- id: message.shop.id || message.id
445
- })
446
- };
447
-
448
- // Handle body text
449
- if ('text' in message) interactiveMessage.body = { text: message.text };
450
- else if ('caption' in message) interactiveMessage.body = { text: message.caption };
451
-
452
- // Handle header with media
453
- if (message.title || message.subtitle || Object.keys(m).length > 0) {
454
- interactiveMessage.header = {
455
- title: message.title || '',
456
- subtitle: message.subtitle || '',
457
- hasMediaAttachment: message.hasMediaAttachment ?? (Object.keys(m).length > 0)
458
- };
459
- if (Object.keys(m).length > 0) Object.assign(interactiveMessage.header, m);
514
+ // ─── COLLECTION ───────────────────────────────────────────────────────────
515
+ else if ('collection' in message && message.collection) {
516
+ const interactive = { collectionMessage: { bizJid: message.collection.bizJid, id: message.collection.id, messageVersion: message.collection.version } }
517
+ if ('text' in message) { interactive.body = { text: message.text }; interactive.header = { title: message.title || '', hasMediaAttachment: false } }
518
+ else if ('caption' in message) {
519
+ interactive.body = { text: message.caption }
520
+ interactive.header = { title: message.title || '', hasMediaAttachment: message.hasMediaAttachment ?? false }
521
+ if (Object.keys(m).length > 0) Object.assign(interactive.header, m)
460
522
  }
461
-
462
- if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
463
-
464
- m = { interactiveMessage };
523
+ if (message.footer) interactive.footer = { text: message.footer }
524
+ m = { interactiveMessage: interactive }
465
525
  }
466
526
 
467
- // ===== AUTO-APPLY CONTEXT & WRAPPERS =====
468
- const finalKey = Object.keys(m)[0];
469
-
470
- // Auto-merge contextInfo and mentions
471
- if ((message.contextInfo || message.mentions) && finalKey && m[finalKey]) {
527
+ // ─── AUTO CONTEXT + MENTIONS MERGE ────────────────────────────────────────
528
+ const finalKey = Object.keys(m)[0]
529
+ if ((message.contextInfo || message.mentions?.length) && finalKey && m[finalKey] && typeof m[finalKey] === 'object') {
472
530
  m[finalKey].contextInfo = {
473
531
  ...(m[finalKey].contextInfo || {}),
474
532
  ...(message.contextInfo || {}),
475
- mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
476
- };
533
+ ...(message.mentions?.length ? { mentionedJid: message.mentions } : {})
534
+ }
477
535
  }
478
536
 
479
- // ViewOnce wrapper
480
- if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
537
+ // ─── WRAPPERS ─────────────────────────────────────────────────────────────
538
+ if (('viewOnce' in message && message.viewOnce) || ('viewOnceMessage' in message && message.viewOnceMessage)) {
539
+ m = { viewOnceMessage: { message: m } }
540
+ }
541
+ if ('edit' in message) {
542
+ m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } }
543
+ }
544
+ if ('contextInfo' in message && message.contextInfo) {
545
+ const k = Object.keys(m)[0]
546
+ if (k && m[k]) m[k].contextInfo = { ...(m[k].contextInfo || {}), ...message.contextInfo }
547
+ }
481
548
 
482
- // Edit wrapper
483
- if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
549
+ if (shouldIncludeReportingToken(m)) {
550
+ m.messageContextInfo = m.messageContextInfo || {}
551
+ if (!m.messageContextInfo.messageSecret) {
552
+ m.messageContextInfo.messageSecret = randomBytes(32)
553
+ }
554
+ }
484
555
 
485
- return WAProto.Message.create(m);
486
- };
556
+ return WAProto.Message.create(m)
557
+ }
487
558
 
559
+ // ─── STICKER PACK ─────────────────────────────────────────────────────────────
560
+ export const prepareStickerPackMessage = async (stickerPack, options) => {
561
+ const { stickers, cover, name, publisher, packId, description } = stickerPack
562
+ if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 })
563
+ if (stickers.length > 120) throw new Boom('Sticker pack exceeds maximum of 120 stickers', { statusCode: 400 })
564
+ const lib = await getImageProcessingLibrary()
565
+ const packId_ = packId || generateMessageIDV2()
566
+ const isWebPBuffer = (buf) => buf.length >= 12 && buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50
567
+ const isAnimatedWebP = (buf) => {
568
+ if (!isWebPBuffer(buf)) return false
569
+ let offset = 12
570
+ while (offset < buf.length - 8) {
571
+ const fourCC = buf.toString('ascii', offset, offset + 4)
572
+ const chunkSize = buf.readUInt32LE(offset + 4)
573
+ if (fourCC === 'VP8X' && offset + 8 < buf.length && (buf[offset + 8] & 0x02)) return true
574
+ if (fourCC === 'ANIM' || fourCC === 'ANMF') return true
575
+ offset += 8 + chunkSize + (chunkSize % 2)
576
+ }
577
+ return false
578
+ }
579
+ const toWebp = async (buffer) => {
580
+ if (isWebPBuffer(buffer)) {
581
+ if ('sharp' in lib && lib.sharp) return await lib.sharp.default(buffer).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75, effort: 6 }).toBuffer()
582
+ return buffer
583
+ }
584
+ if ('sharp' in lib && lib.sharp) return await lib.sharp.default(buffer).resize(512, 512, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75, effort: 6 }).toBuffer()
585
+ if ('jimp' in lib && lib.jimp) return await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'))
586
+ throw new Boom('No image processing library available for WebP conversion', { statusCode: 500 })
587
+ }
588
+ const validStickers = []
589
+ await Promise.all(stickers.map(async (s) => {
590
+ try {
591
+ const { stream } = await getStream(s.data || s.sticker)
592
+ const buffer = await toBuffer(stream)
593
+ if (!buffer?.length) return
594
+ const animated = isAnimatedWebP(buffer)
595
+ let webpBuffer = await toWebp(buffer)
596
+ if (webpBuffer.length > 1024 * 1024) {
597
+ if ('sharp' in lib && lib.sharp) webpBuffer = await lib.sharp.default(webpBuffer).webp({ quality: 50 }).toBuffer()
598
+ if (webpBuffer.length > 1024 * 1024) return
599
+ }
600
+ const hash = sha256(webpBuffer).toString('base64').replace(/\//g, '-').replace(/=/g, '')
601
+ validStickers.push({
602
+ fileName: `${hash}.webp`, buffer: webpBuffer, mimetype: 'image/webp',
603
+ isAnimated: s.isAnimated ?? animated, isLottie: s.isLottie || false,
604
+ emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel || ''
605
+ })
606
+ } catch (e) { options.logger?.warn({ err: e }, 'failed processing sticker') }
607
+ }))
608
+ if (!validStickers.length) throw new Boom('No valid stickers could be processed', { statusCode: 400 })
609
+ const { stream: covStream } = await getStream(cover)
610
+ const coverBuffer = await toWebp(await toBuffer(covStream))
611
+ const processBatch = async (batch, batchIdx) => {
612
+ const batchData = {}
613
+ batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 6 }] })
614
+ const trayFile = `${packId_}_${batchIdx}.webp`
615
+ batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 6 }]
616
+ const zipBuf = await new Promise((resolve, reject) => zip(batchData, { level: 6, memLevel: 9 }, (err, data) => err ? reject(err) : resolve(Buffer.from(data))))
617
+ if (zipBuf.length > 10 * 1024 * 1024) throw new Boom(`Sticker pack batch ${batchIdx} too large: ${(zipBuf.length / 1024 / 1024).toFixed(2)}MB`, { statusCode: 400 })
618
+ const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options })
619
+ const uploadRes = await options.upload(upload.encFilePath, { fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 300000 })
620
+ try { await fs.unlink(upload.encFilePath) } catch { }
621
+ let thumbRes = null
622
+ try {
623
+ let thumbBuf
624
+ if ('sharp' in lib && lib.sharp) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252, { fit: 'cover' }).jpeg({ quality: 80 }).toBuffer()
625
+ else if ('jimp' in lib && lib.jimp) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'))
626
+ if (thumbBuf?.length) {
627
+ const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey })
628
+ thumbRes = await options.upload(thumbUpload.encFilePath, { fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs || 60000 })
629
+ try { await fs.unlink(thumbUpload.encFilePath) } catch { }
630
+ thumbRes._buf = thumbBuf
631
+ thumbRes._enc = thumbUpload
632
+ }
633
+ } catch (e) { options.logger?.warn({ err: e }, 'failed generating sticker pack thumbnail') }
634
+ return {
635
+ name: batchIdx > 0 ? `${name} (${batchIdx + 1})` : name, publisher, packDescription: description,
636
+ stickerPackId: batchIdx > 0 ? `${packId_}_${batchIdx}` : packId_,
637
+ stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.THIRD_PARTY,
638
+ stickerPackSize: zipBuf.length,
639
+ stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, isLottie: s.isLottie, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
640
+ fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
641
+ directPath: uploadRes.directPath, fileLength: upload.fileLength,
642
+ mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
643
+ ...(thumbRes && {
644
+ thumbnailDirectPath: thumbRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252,
645
+ thumbnailSha256: thumbRes._enc?.fileSha256, thumbnailEncSha256: thumbRes._enc?.fileEncSha256,
646
+ imageDataHash: thumbRes._buf ? sha256(thumbRes._buf).toString('base64') : undefined
647
+ })
648
+ }
649
+ }
650
+ if (validStickers.length > 60) {
651
+ const batches = []
652
+ for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60))
653
+ const results = await Promise.all(batches.map((b, i) => processBatch(b, i)))
654
+ return { stickerPackMessage: results, isBatched: true, batchCount: batches.length }
655
+ }
656
+ return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false }
657
+ }
488
658
 
659
+ // ─── MESSAGE BUILDERS ─────────────────────────────────────────────────────────
489
660
  export const generateWAMessageFromContent = (jid, message, options) => {
490
- if (!options.timestamp) options.timestamp = new Date();
491
- const innerMessage = normalizeMessageContent(message);
492
- const key = getContentType(innerMessage);
493
- const timestamp = unixTimestampSeconds(options.timestamp);
494
- const { quoted, userJid } = options;
495
-
661
+ if (!options.timestamp) options.timestamp = new Date()
662
+ const innerMessage = normalizeMessageContent(message)
663
+ const key = getContentType(innerMessage)
664
+ const { quoted, userJid } = options
496
665
  if (quoted && !isJidNewsletter(jid)) {
497
- const participant = quoted.key.fromMe ? userJid : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
498
- const quotedMsg = proto.Message.create({ [getContentType(normalizeMessageContent(quoted.message))]: normalizeMessageContent(quoted.message)[getContentType(normalizeMessageContent(quoted.message))] });
499
- const contextInfo = (innerMessage[key]?.contextInfo) || {};
500
- contextInfo.participant = jidNormalizedUser(participant);
501
- contextInfo.stanzaId = quoted.key.id;
502
- contextInfo.quotedMessage = quotedMsg;
503
- if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid;
504
- innerMessage[key].contextInfo = contextInfo;
666
+ const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
667
+ const normalizedQuoted = normalizeMessageContent(quoted.message)
668
+ const quotedType = getContentType(normalizedQuoted)
669
+ const quotedMsg = proto.Message.fromObject({ [quotedType]: normalizedQuoted[quotedType] })
670
+ const quotedContent = quotedMsg[quotedType]
671
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) delete quotedContent.contextInfo
672
+ const contextInfo = (innerMessage[key]?.contextInfo) || {}
673
+ contextInfo.participant = jidNormalizedUser(participant)
674
+ contextInfo.stanzaId = quoted.key.id
675
+ contextInfo.quotedMessage = quotedMsg
676
+ if (jid !== quoted.key.remoteJid) contextInfo.remoteJid = quoted.key.remoteJid
677
+ if (innerMessage[key]) innerMessage[key].contextInfo = contextInfo
505
678
  }
506
-
507
679
  if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
508
- innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
680
+ innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL }
509
681
  }
510
-
511
682
  return WAProto.WebMessageInfo.fromObject({
512
- key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2(), participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined },
513
- message: WAProto.Message.create(message),
514
- messageTimestamp: timestamp,
683
+ key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
684
+ message: WAProto.Message.fromObject(message),
685
+ messageTimestamp: unixTimestampSeconds(options.timestamp),
515
686
  messageStubParameters: [],
516
- participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
687
+ participant: (isJidGroup(jid) || isJidStatusBroadcast(jid)) ? userJid : undefined,
517
688
  status: WAMessageStatus.PENDING
518
- });
519
- };
689
+ })
690
+ }
520
691
 
521
692
  export const generateWAMessage = async (jid, content, options = {}) => {
522
- options.logger = options?.logger?.child({ msgId: options.messageId });
523
- return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
524
- };
525
-
526
- // ===== UTILITIES =====
527
- export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown';
693
+ options.logger = options?.logger?.child({ msgId: options.messageId })
694
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options)
695
+ }
528
696
 
697
+ // ─── RECEIPTS / REACTIONS / POLLS ─────────────────────────────────────────────
529
698
  export const updateMessageWithReceipt = (msg, receipt) => {
530
- msg.userReceipt ||= [];
531
- const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
532
- if (recp) Object.assign(recp, receipt);
533
- else msg.userReceipt.push(receipt);
534
- };
699
+ msg.userReceipt ||= []
700
+ const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid)
701
+ if (recp) Object.assign(recp, receipt)
702
+ else msg.userReceipt.push(receipt)
703
+ }
535
704
 
536
705
  export const updateMessageWithReaction = (msg, reaction) => {
537
- const authorID = getKeyAuthor(reaction.key);
538
- msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
539
- reaction.text ||= '';
540
- msg.reactions.push(reaction);
541
- };
706
+ const authorID = getKeyAuthor(reaction.key)
707
+ msg.reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID)
708
+ reaction.text ||= ''
709
+ msg.reactions.push(reaction)
710
+ }
542
711
 
543
712
  export const updateMessageWithPollUpdate = (msg, update) => {
544
- const authorID = getKeyAuthor(update.pollUpdateMessageKey);
545
- msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
546
- if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update);
547
- };
713
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey)
714
+ msg.pollUpdates = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID)
715
+ if (update.vote?.selectedOptions?.length) msg.pollUpdates.push(update)
716
+ }
717
+
718
+ export const updateMessageWithEventResponse = (msg, update) => {
719
+ const authorID = getKeyAuthor(update.eventResponseMessageKey)
720
+ msg.eventResponses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID)
721
+ msg.eventResponses.push(update)
722
+ }
548
723
 
549
724
  export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
550
- const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || [];
725
+ const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || []
551
726
  const voteHashMap = opts.reduce((acc, opt) => {
552
- const hash = sha256(Buffer.from(opt.optionName || '')).toString();
553
- acc[hash] = { name: opt.optionName || '', voters: [] };
554
- return acc;
555
- }, {});
556
-
727
+ acc[sha256(Buffer.from(opt.optionName || '')).toString()] = { name: opt.optionName || '', voters: [] }
728
+ return acc
729
+ }, {})
557
730
  for (const update of pollUpdates || []) {
558
- const { vote } = update;
559
- if (!vote) continue;
560
- for (const option of vote.selectedOptions || []) {
561
- const hash = option.toString();
562
- voteHashMap[hash] ||= { name: 'Unknown', voters: [] };
563
- voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
731
+ if (!update.vote) continue
732
+ for (const option of update.vote.selectedOptions || []) {
733
+ const hash = option.toString()
734
+ voteHashMap[hash] ||= { name: 'Unknown', voters: [] }
735
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId))
564
736
  }
565
737
  }
566
- return Object.values(voteHashMap);
738
+ return Object.values(voteHashMap)
739
+ }
740
+
741
+ export function getAggregateResponsesInEventMessage({ eventResponses }, meId) {
742
+ const responseMap = { GOING: { response: 'GOING', responders: [] }, NOT_GOING: { response: 'NOT_GOING', responders: [] }, MAYBE: { response: 'MAYBE', responders: [] } }
743
+ for (const update of eventResponses || []) {
744
+ const type = update.eventResponse || 'UNKNOWN'
745
+ if (responseMap[type]) responseMap[type].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId))
746
+ }
747
+ return Object.values(responseMap)
567
748
  }
568
749
 
569
750
  export const aggregateMessageKeysNotFromMe = (keys) => {
570
- const keyMap = {};
751
+ const keyMap = {}
571
752
  for (const { remoteJid, id, participant, fromMe } of keys) {
572
753
  if (!fromMe) {
573
- const uqKey = `${remoteJid}:${participant || ''}`;
574
- keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] };
575
- keyMap[uqKey].messageIds.push(id);
754
+ const uqKey = `${remoteJid}:${participant || ''}`
755
+ keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] }
756
+ keyMap[uqKey].messageIds.push(id)
576
757
  }
577
758
  }
578
- return Object.values(keyMap);
579
- };
759
+ return Object.values(keyMap)
760
+ }
580
761
 
581
- const REUPLOAD_STATUS = [410, 404];
762
+ // ─── DOWNLOAD ─────────────────────────────────────────────────────────────────
763
+ const REUPLOAD_REQUIRED_STATUS = [410, 404]
582
764
 
583
765
  export const downloadMediaMessage = async (message, type, options, ctx) => {
584
766
  const downloadMsg = async () => {
585
- let normalizedMessage = message;
586
- if (!message.message && message.key && message.participant) normalizedMessage = { key: message.key, message: message, messageTimestamp: message.messageTimestamp };
587
- if (!normalizedMessage.message && typeof message === 'object') {
588
- const possibleMessage = message.message || message.quoted?.message || message;
589
- normalizedMessage = { key: message.key || {}, message: possibleMessage, messageTimestamp: message.messageTimestamp };
590
- }
591
- const mContent = extractMessageContent(normalizedMessage.message);
592
- if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message });
593
- const contentType = getContentType(mContent);
594
- let mediaType = contentType?.replace('Message', '');
595
- const media = mContent[contentType];
767
+ let normalized = message
768
+ if (!message.message && message.key) normalized = { key: message.key, message: message.quoted?.message || message, messageTimestamp: message.messageTimestamp }
769
+ const mContent = extractMessageContent(normalized.message)
770
+ if (!mContent) throw new Boom('No message present', { statusCode: 400, data: message })
771
+ const contentType = getContentType(mContent)
772
+ let mediaType = contentType?.replace('Message', '')
773
+ const media = mContent[contentType]
596
774
  if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media)))
597
- throw new Boom(`"${contentType}" is not a media message`);
598
- const download = 'thumbnailDirectPath' in media && !('url' in media) ? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey } : media;
599
- const stream = await downloadContentFromMessage(download, mediaType, options);
775
+ throw new Boom(`"${contentType}" message is not a media message`)
776
+ const download = ('thumbnailDirectPath' in media && !('url' in media))
777
+ ? { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey }
778
+ : media
779
+ if ('thumbnailDirectPath' in media && !('url' in media)) mediaType = 'thumbnail-link'
780
+ const stream = await downloadContentFromMessage(download, mediaType, options)
600
781
  if (type === 'buffer') {
601
- const chunks = [];
602
- for await (const chunk of stream) chunks.push(chunk);
603
- return Buffer.concat(chunks);
782
+ const chunks = []
783
+ for await (const chunk of stream) chunks.push(chunk)
784
+ return Buffer.concat(chunks)
604
785
  }
605
- return stream;
606
- };
786
+ return stream
787
+ }
607
788
  return downloadMsg().catch(async (error) => {
608
- if (ctx && typeof error?.status === 'number' && REUPLOAD_STATUS.includes(error.status)) {
609
- message = await ctx.reuploadRequest(message);
610
- return downloadMsg();
789
+ if (ctx && typeof error?.status === 'number' && REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
790
+ ctx.logger.info({ key: message.key }, 'sending reupload media request...')
791
+ message = await ctx.reuploadRequest(message)
792
+ return downloadMsg()
611
793
  }
612
- throw error;
613
- });
614
- };
794
+ throw error
795
+ })
796
+ }
615
797
 
616
- export async function prepareStickerPackMessage(stickerPack, options) {
617
- const { stickers, name, publisher, packId, description } = stickerPack;
618
- if (!stickers?.length) throw new Boom('Sticker pack requires at least one sticker', { statusCode: 400 });
798
+ export const assertMediaContent = (content) => {
799
+ content = extractMessageContent(content)
800
+ const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage
801
+ if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content })
802
+ return mediaContent
803
+ }
619
804
 
620
- const lib = await getImageProcessingLibrary();
621
- const packId_ = packId || generateMessageIDV2();
622
- const validStickers = [];
805
+ // ─── DEVICE / MD UTILS ────────────────────────────────────────────────────────
806
+ export const getDevice = (id) => /^3A.{18}$/.test(id) ? 'ios' : /^3E.{20}$/.test(id) ? 'web' : /^(.{21}|.{32})$/.test(id) ? 'android' : /^(3F|.{18}$)/.test(id) ? 'desktop' : 'unknown'
623
807
 
624
- for (const s of stickers) {
625
- try {
626
- const { stream } = await getStream(s.data);
627
- let buffer = await toBuffer(stream);
628
- const isWebP = buffer.length >= 12 && buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46;
629
- if (!isWebP) {
630
- if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp().toBuffer();
631
- else if ('jimp' in lib) buffer = await lib.jimp.Jimp.read(buffer).then(img => img.getBuffer('image/webp'));
632
- }
633
- if (buffer.length > 1024 * 1024) {
634
- if ('sharp' in lib) buffer = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer();
635
- if (buffer.length > 1024 * 1024) continue;
808
+ export const patchMessageForMdIfRequired = (message) => {
809
+ if (message?.buttonsMessage || message?.templateMessage || message?.listMessage || message?.interactiveMessage?.nativeFlowMessage) {
810
+ message = {
811
+ viewOnceMessageV2Extension: {
812
+ message: { messageContextInfo: { deviceListMetadataVersion: 2, deviceListMetadata: {} }, ...message }
636
813
  }
637
- validStickers.push({
638
- fileName: `${sha256(buffer).toString('base64').replace(/\//g, '-')}.webp`,
639
- buffer, mimetype: 'image/webp', isAnimated: s.isAnimated || false,
640
- emojis: s.emojis || [], accessibilityLabel: s.accessibilityLabel
641
- });
642
- } catch (e) { options.logger?.warn(`Sticker failed: ${e.message}`); }
643
- }
644
-
645
- if (!validStickers.length) throw new Boom('No valid stickers', { statusCode: 400 });
646
-
647
- const { stream: covStream } = await getStream(stickerPack.cover);
648
- let coverBuffer = await toBuffer(covStream);
649
- const isWebPCover = coverBuffer.length >= 12 && coverBuffer[0] === 0x52 && coverBuffer[1] === 0x49 && coverBuffer[2] === 0x46 && coverBuffer[3] === 0x46;
650
- if (!isWebPCover) {
651
- if ('sharp' in lib) coverBuffer = await lib.sharp.default(coverBuffer).webp().toBuffer();
652
- else if ('jimp' in lib) coverBuffer = await lib.jimp.Jimp.read(coverBuffer).then(img => img.getBuffer('image/webp'));
653
- }
654
-
655
- const processBatch = async (batch, batchIdx) => {
656
- const batchData = {};
657
- batch.forEach(s => { batchData[s.fileName] = [new Uint8Array(s.buffer), { level: 0 }]; });
658
- const trayFile = `${packId_}_batch${batchIdx}.webp`;
659
- batchData[trayFile] = [new Uint8Array(coverBuffer), { level: 0 }];
660
-
661
- const zipBuf = await new Promise((resolve, reject) => { zip(batchData, (err, data) => err ? reject(err) : resolve(Buffer.from(data))); });
662
- const upload = await encryptedStream(zipBuf, 'sticker-pack', { logger: options.logger, opts: options.options });
663
- const uploadRes = await options.upload(upload.encFilePath, {
664
- fileEncSha256B64: upload.fileEncSha256.toString('base64'), mediaType: 'sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
665
- });
666
- await fs.unlink(upload.encFilePath);
667
-
668
- let thumbBuf;
669
- if ('sharp' in lib) thumbBuf = await lib.sharp.default(coverBuffer).resize(252, 252).jpeg().toBuffer();
670
- else if ('jimp' in lib) thumbBuf = await lib.jimp.Jimp.read(coverBuffer).then(img => img.resize({ w: 252, h: 252 }).getBuffer('image/jpeg'));
671
-
672
- let thumbUploadRes;
673
- if (thumbBuf?.length) {
674
- const thumbUpload = await encryptedStream(thumbBuf, 'thumbnail-sticker-pack', { logger: options.logger, opts: options.options, mediaKey: upload.mediaKey });
675
- thumbUploadRes = await options.upload(thumbUpload.encFilePath, {
676
- fileEncSha256B64: thumbUpload.fileEncSha256.toString('base64'), mediaType: 'thumbnail-sticker-pack', timeoutMs: options.mediaUploadTimeoutMs
677
- });
678
- await fs.unlink(thumbUpload.encFilePath);
679
814
  }
680
-
681
- return {
682
- name: `${name} (${batchIdx + 1})`, publisher, packDescription: description, stickerPackId: `${packId_}_${batchIdx}`,
683
- stickerPackOrigin: WAProto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED, stickerPackSize: zipBuf.length,
684
- stickers: batch.map(s => ({ fileName: s.fileName, mimetype: s.mimetype, isAnimated: s.isAnimated, emojis: s.emojis, accessibilityLabel: s.accessibilityLabel })),
685
- fileSha256: upload.fileSha256, fileEncSha256: upload.fileEncSha256, mediaKey: upload.mediaKey,
686
- directPath: uploadRes.directPath, fileLength: upload.fileLength, mediaKeyTimestamp: unixTimestampSeconds(), trayIconFileName: trayFile,
687
- ...(thumbUploadRes && { thumbnailDirectPath: thumbUploadRes.directPath, thumbnailHeight: 252, thumbnailWidth: 252, imageDataHash: thumbBuf ? sha256(thumbBuf).toString('base64') : undefined })
688
- };
689
- };
690
-
691
- if (validStickers.length > 60) {
692
- const batches = [];
693
- for (let i = 0; i < validStickers.length; i += 60) batches.push(validStickers.slice(i, i + 60));
694
- const batchResults = await Promise.all(batches.map((b, i) => processBatch(b, i)));
695
- return { stickerPackMessage: batchResults, isBatched: true, batchCount: batches.length };
696
815
  }
697
-
698
- return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
816
+ return message
699
817
  }
700
818
 
701
- export const assertMediaContent = (content) => {
702
- content = extractMessageContent(content);
703
- const mediaContent = content?.documentMessage || content?.imageMessage || content?.videoMessage || content?.audioMessage || content?.stickerMessage;
704
- if (!mediaContent) throw new Boom('given message is not a media message', { statusCode: 400, data: content });
705
- return mediaContent;
706
- };
819
+ export const hasNonNullishProperty = (message, key) => typeof message === 'object' && message !== null && key in message && message[key] !== null && message[key] !== undefined
820
+
821
+ export const hasOptionalProperty = (obj, key) => typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null