@nexustechpro/baileys 2.0.1 → 2.0.5

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.
Files changed (102) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +924 -1299
  3. package/lib/Defaults/baileys-version.json +6 -2
  4. package/lib/Defaults/index.js +172 -172
  5. package/lib/Signal/libsignal.js +380 -292
  6. package/lib/Signal/lid-mapping.js +264 -171
  7. package/lib/Socket/Client/index.js +2 -2
  8. package/lib/Socket/Client/types.js +10 -10
  9. package/lib/Socket/Client/websocket.js +45 -310
  10. package/lib/Socket/business.js +375 -375
  11. package/lib/Socket/chats.js +909 -963
  12. package/lib/Socket/communities.js +430 -430
  13. package/lib/Socket/groups.js +342 -342
  14. package/lib/Socket/index.js +22 -22
  15. package/lib/Socket/messages-recv.js +777 -743
  16. package/lib/Socket/messages-send.js +667 -393
  17. package/lib/Socket/mex.js +50 -50
  18. package/lib/Socket/newsletter.js +148 -148
  19. package/lib/Socket/nexus-handler.js +75 -261
  20. package/lib/Socket/socket.js +709 -1201
  21. package/lib/Store/index.js +5 -5
  22. package/lib/Store/make-cache-manager-store.js +81 -81
  23. package/lib/Store/make-in-memory-store.js +416 -416
  24. package/lib/Store/make-ordered-dictionary.js +81 -81
  25. package/lib/Store/object-repository.js +30 -30
  26. package/lib/Types/Auth.js +1 -1
  27. package/lib/Types/Bussines.js +1 -1
  28. package/lib/Types/Call.js +1 -1
  29. package/lib/Types/Chat.js +7 -7
  30. package/lib/Types/Contact.js +1 -1
  31. package/lib/Types/Events.js +1 -1
  32. package/lib/Types/GroupMetadata.js +1 -1
  33. package/lib/Types/Label.js +24 -24
  34. package/lib/Types/LabelAssociation.js +6 -6
  35. package/lib/Types/Message.js +10 -10
  36. package/lib/Types/Newsletter.js +28 -28
  37. package/lib/Types/Product.js +1 -1
  38. package/lib/Types/Signal.js +1 -1
  39. package/lib/Types/Socket.js +2 -2
  40. package/lib/Types/State.js +12 -12
  41. package/lib/Types/USync.js +1 -1
  42. package/lib/Types/index.js +25 -25
  43. package/lib/Utils/auth-utils.js +264 -256
  44. package/lib/Utils/baileys-event-stream.js +55 -55
  45. package/lib/Utils/browser-utils.js +27 -27
  46. package/lib/Utils/business.js +228 -230
  47. package/lib/Utils/chat-utils.js +694 -764
  48. package/lib/Utils/crypto.js +109 -135
  49. package/lib/Utils/decode-wa-message.js +310 -314
  50. package/lib/Utils/event-buffer.js +547 -547
  51. package/lib/Utils/generics.js +297 -297
  52. package/lib/Utils/history.js +91 -83
  53. package/lib/Utils/index.js +21 -20
  54. package/lib/Utils/key-store.js +17 -0
  55. package/lib/Utils/link-preview.js +97 -88
  56. package/lib/Utils/logger.js +2 -2
  57. package/lib/Utils/lt-hash.js +47 -47
  58. package/lib/Utils/make-mutex.js +39 -39
  59. package/lib/Utils/message-retry-manager.js +148 -148
  60. package/lib/Utils/messages-media.js +534 -532
  61. package/lib/Utils/messages.js +705 -705
  62. package/lib/Utils/noise-handler.js +255 -255
  63. package/lib/Utils/pre-key-manager.js +105 -105
  64. package/lib/Utils/process-message.js +412 -412
  65. package/lib/Utils/signal.js +160 -158
  66. package/lib/Utils/use-multi-file-auth-state.js +120 -120
  67. package/lib/Utils/validate-connection.js +194 -194
  68. package/lib/WABinary/constants.js +1300 -1300
  69. package/lib/WABinary/decode.js +237 -237
  70. package/lib/WABinary/encode.js +232 -232
  71. package/lib/WABinary/generic-utils.js +252 -211
  72. package/lib/WABinary/index.js +5 -5
  73. package/lib/WABinary/jid-utils.js +279 -95
  74. package/lib/WABinary/types.js +1 -1
  75. package/lib/WAM/BinaryInfo.js +9 -9
  76. package/lib/WAM/constants.js +22852 -22852
  77. package/lib/WAM/encode.js +149 -149
  78. package/lib/WAM/index.js +3 -3
  79. package/lib/WAUSync/Protocols/USyncContactProtocol.js +28 -28
  80. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -53
  81. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -26
  82. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -37
  83. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -50
  84. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -28
  85. package/lib/WAUSync/Protocols/index.js +4 -4
  86. package/lib/WAUSync/USyncQuery.js +93 -93
  87. package/lib/WAUSync/USyncUser.js +22 -22
  88. package/lib/WAUSync/index.js +3 -3
  89. package/lib/index.js +66 -66
  90. package/package.json +171 -144
  91. package/lib/Signal/Group/ciphertext-message.js +0 -12
  92. package/lib/Signal/Group/group-session-builder.js +0 -30
  93. package/lib/Signal/Group/group_cipher.js +0 -100
  94. package/lib/Signal/Group/index.js +0 -12
  95. package/lib/Signal/Group/keyhelper.js +0 -18
  96. package/lib/Signal/Group/sender-chain-key.js +0 -26
  97. package/lib/Signal/Group/sender-key-distribution-message.js +0 -63
  98. package/lib/Signal/Group/sender-key-message.js +0 -66
  99. package/lib/Signal/Group/sender-key-name.js +0 -48
  100. package/lib/Signal/Group/sender-key-record.js +0 -41
  101. package/lib/Signal/Group/sender-key-state.js +0 -84
  102. package/lib/Signal/Group/sender-message-key.js +0 -26
@@ -1,706 +1,706 @@
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];
21
-
22
- 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
- };
28
-
29
- 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
- };
34
-
35
- 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
- };
40
-
41
- export const normalizeMessageContent = (content) => {
42
- if (!content) return;
43
- 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;
47
- }
48
- return content;
49
- };
50
-
51
- 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
- };
56
-
57
- // ===== MEDIA PREPARATION =====
58
- export const prepareWAMessageMedia = async (message, options) => {
59
- let mediaType = MEDIA_KEYS.find(key => key in message)
60
- if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
61
-
62
- const uploadData = { ...message, media: message[mediaType] }
63
- 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
- if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
68
- if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
69
-
70
- if (cacheableKey) {
71
- const cached = await options.mediaCache?.get(cacheableKey)
72
- if (cached) {
73
- const obj = proto.Message.decode(cached)
74
- Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
75
- return obj
76
- }
77
- }
78
-
79
- const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
80
- const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
81
- 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, {
85
- logger: options.logger,
86
- saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
87
- opts: options.options,
88
- isPtt: uploadData.ptt,
89
- forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
90
- convertVideo: mediaType === 'video'
91
- })
92
-
93
- // ✅ FIX: Extract the correct values based on encryption method
94
- const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
95
-
96
- 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
-
103
- const [{ mediaUrl, directPath, handle }] = await Promise.all([
104
- (async () => {
105
- const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
106
- options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
107
- return result
108
- })(),
109
- (async () => {
110
- try {
111
- if (requiresThumbnailComputation) {
112
- const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
113
- uploadData.jpegThumbnail = thumbnail
114
- if (!uploadData.width && originalImageDimensions) {
115
- uploadData.width = originalImageDimensions.width
116
- uploadData.height = originalImageDimensions.height
117
- }
118
- }
119
- if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
120
- if (requiresWaveformProcessing) {
121
- try {
122
- uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
123
- } catch (err) {
124
- options.logger?.warn('Failed to generate waveform, using fallback')
125
- 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
- }
127
- }
128
- if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
129
- } catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
130
- })()
131
- ]).finally(async () => {
132
- 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
- }
145
- })
146
-
147
- const obj = WAProto.Message.fromObject({
148
- [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
149
- url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
150
- mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
151
- })
152
- })
153
-
154
- if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
155
- if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
156
- return obj
157
- }
158
-
159
- export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
160
- ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
161
- });
162
-
163
- 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
-
168
- if (key === 'conversation') {
169
- content.extendedTextMessage = { text: content[key] };
170
- delete content.conversation;
171
- key = 'extendedTextMessage';
172
- }
173
-
174
- content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
175
- return content;
176
- };
177
-
178
- // ===== MESSAGE HANDLERS =====
179
- 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
-
183
- if (urlInfo) {
184
- Object.assign(extContent, {
185
- matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
186
- 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
- }
195
- }
196
-
197
- if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
198
- if (options.font) extContent.font = options.font;
199
- return { extendedTextMessage: extContent };
200
- };
201
-
202
- const handleSpecialMessages = async (message, options) => {
203
- 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) };
207
- }
208
- if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
209
- if ('react' in message) {
210
- if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now();
211
- return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) };
212
- }
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);
215
- if ('disappearingMessagesInChat' in message) {
216
- const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
217
- return prepareDisappearingMessageSettingContent(exp);
218
- }
219
- return null;
220
- };
221
-
222
- const handleGroupInvite = async (message, options) => {
223
- const m = {
224
- groupInviteMessage: {
225
- inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
226
- caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
227
- }
228
- };
229
-
230
- if (options.getProfilePicUrl) {
231
- const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
232
- 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());
235
- }
236
- }
237
- return m;
238
- };
239
-
240
- const handleEventMessage = (message, options) => {
241
- const startTime = Math.floor(message.event.startDate.getTime() / 1000);
242
- const m = {
243
- eventMessage: {
244
- name: message.event.name, description: message.event.description, startTime,
245
- endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
246
- isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
247
- isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
248
- },
249
- messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
250
- };
251
-
252
- 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
- });
256
- }
257
- return m;
258
- };
259
-
260
- 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 });
265
- 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
- };
275
-
276
- 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
- };
280
-
281
- 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
-
289
- const m = {
290
- 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
296
- })
297
- };
298
-
299
- if (message.requestPayment.currencyCodeIso4217 === 'BRL' && message.requestPayment.pixKey) {
300
- if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
301
- m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } };
302
- m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${message.requestPayment.pixKey}`;
303
- }
304
-
305
- return m;
306
- };
307
-
308
- // ===== MAIN GENERATOR =====
309
- 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
- );
318
-
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
- }
330
-
331
- let m = {};
332
-
333
- // ===== HANDLE TEXT =====
334
- 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);
336
- }
337
-
338
- // ===== HANDLE SPECIAL MESSAGES =====
339
- 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 } };
375
- }
376
- else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
377
- }
378
-
379
- // ===== SMART BUTTON HANDLING =====
380
- 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
-
383
- if (hasNativeFlow) {
384
- // Convert to interactiveMessage
385
- const interactive = {
386
- body: { text: message.text || message.caption || message.contentText || '' },
387
- footer: { text: message.footer || message.footerText || '' },
388
- nativeFlowMessage: {
389
- 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 || '' }) };
393
- })
394
- }
395
- };
396
-
397
- if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
398
- if (Object.keys(m).length > 0) {
399
- interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
400
- Object.assign(interactive.header, m);
401
- }
402
-
403
- m = { interactiveMessage: interactive };
404
- } 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 };
412
- }
413
- }
414
-
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 } };
422
- }
423
-
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 } };
427
- }
428
-
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 };
437
- }
438
-
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);
460
- }
461
-
462
- if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
463
-
464
- m = { interactiveMessage };
465
- }
466
-
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]) {
472
- m[finalKey].contextInfo = {
473
- ...(m[finalKey].contextInfo || {}),
474
- ...(message.contextInfo || {}),
475
- mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
476
- };
477
- }
478
-
479
- // ViewOnce wrapper
480
- if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
481
-
482
- // Edit wrapper
483
- if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
484
-
485
- return WAProto.Message.create(m);
486
- };
487
-
488
-
489
- 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
-
496
- 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;
505
- }
506
-
507
- if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
508
- innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
509
- }
510
-
511
- return WAProto.WebMessageInfo.fromObject({
512
- key: { remoteJid: jid, fromMe: true, id: options?.messageId || generateMessageIDV2() },
513
- message: WAProto.Message.create(message),
514
- messageTimestamp: timestamp,
515
- messageStubParameters: [],
516
- participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
517
- status: WAMessageStatus.PENDING
518
- });
519
- };
520
-
521
- 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';
528
-
529
- 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
- };
535
-
536
- 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
- };
542
-
543
- 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
- };
548
-
549
- export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
550
- const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || [];
551
- 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
-
557
- 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));
564
- }
565
- }
566
- return Object.values(voteHashMap);
567
- }
568
-
569
- export const aggregateMessageKeysNotFromMe = (keys) => {
570
- const keyMap = {};
571
- for (const { remoteJid, id, participant, fromMe } of keys) {
572
- if (!fromMe) {
573
- const uqKey = `${remoteJid}:${participant || ''}`;
574
- keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] };
575
- keyMap[uqKey].messageIds.push(id);
576
- }
577
- }
578
- return Object.values(keyMap);
579
- };
580
-
581
- const REUPLOAD_STATUS = [410, 404];
582
-
583
- export const downloadMediaMessage = async (message, type, options, ctx) => {
584
- 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];
596
- 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);
600
- if (type === 'buffer') {
601
- const chunks = [];
602
- for await (const chunk of stream) chunks.push(chunk);
603
- return Buffer.concat(chunks);
604
- }
605
- return stream;
606
- };
607
- 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();
611
- }
612
- throw error;
613
- });
614
- };
615
-
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 });
619
-
620
- const lib = await getImageProcessingLibrary();
621
- const packId_ = packId || generateMessageIDV2();
622
- const validStickers = [];
623
-
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;
636
- }
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
- }
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
- }
697
-
698
- return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
699
- }
700
-
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;
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];
21
+
22
+ 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
+ };
28
+
29
+ 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
+ };
34
+
35
+ 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
+ };
40
+
41
+ export const normalizeMessageContent = (content) => {
42
+ if (!content) return;
43
+ 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;
47
+ }
48
+ return content;
49
+ };
50
+
51
+ 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
+ };
56
+
57
+ // ===== MEDIA PREPARATION =====
58
+ export const prepareWAMessageMedia = async (message, options) => {
59
+ let mediaType = MEDIA_KEYS.find(key => key in message)
60
+ if (!mediaType) throw new Boom('Invalid media type', { statusCode: 400 })
61
+
62
+ const uploadData = { ...message, media: message[mediaType] }
63
+ 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
+ if (mediaType === 'document' && !uploadData.fileName) uploadData.fileName = 'file'
68
+ if (!uploadData.mimetype) uploadData.mimetype = MIMETYPE_MAP[mediaType]
69
+
70
+ if (cacheableKey) {
71
+ const cached = await options.mediaCache?.get(cacheableKey)
72
+ if (cached) {
73
+ const obj = proto.Message.decode(cached)
74
+ Object.assign(obj[`${mediaType}Message`], { ...uploadData, media: undefined })
75
+ return obj
76
+ }
77
+ }
78
+
79
+ const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
80
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData.jpegThumbnail === 'undefined'
81
+ 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, {
85
+ logger: options.logger,
86
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
87
+ opts: options.options,
88
+ isPtt: uploadData.ptt,
89
+ forceOpus: mediaType === 'audio' && uploadData.mimetype && uploadData.mimetype.includes('opus'),
90
+ convertVideo: mediaType === 'video'
91
+ })
92
+
93
+ // ✅ FIX: Extract the correct values based on encryption method
94
+ const { mediaKey, encWriteStream, bodyPath, fileEncSha256, fileSha256, fileLength, didSaveToTmpPath, opusConverted, encFilePath } = encryptionResult
95
+
96
+ 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
+
103
+ const [{ mediaUrl, directPath, handle }] = await Promise.all([
104
+ (async () => {
105
+ const result = await options.upload(uploadStream, { fileEncSha256B64, mediaType, timeoutMs: options.mediaUploadTimeoutMs })
106
+ options.logger?.debug({ mediaType, cacheableKey }, 'uploaded media')
107
+ return result
108
+ })(),
109
+ (async () => {
110
+ try {
111
+ if (requiresThumbnailComputation) {
112
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(bodyPath, mediaType, options)
113
+ uploadData.jpegThumbnail = thumbnail
114
+ if (!uploadData.width && originalImageDimensions) {
115
+ uploadData.width = originalImageDimensions.width
116
+ uploadData.height = originalImageDimensions.height
117
+ }
118
+ }
119
+ if (requiresDurationComputation) uploadData.seconds = await getAudioDuration(bodyPath)
120
+ if (requiresWaveformProcessing) {
121
+ try {
122
+ uploadData.waveform = await getAudioWaveform(bodyPath, options.logger)
123
+ } catch (err) {
124
+ options.logger?.warn('Failed to generate waveform, using fallback')
125
+ 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
+ }
127
+ }
128
+ if (options.backgroundColor && mediaType === 'audio') uploadData.backgroundArgb = assertColor(options.backgroundColor)
129
+ } catch (e) { options.logger?.warn({ trace: e.stack }, 'failed to obtain extra info') }
130
+ })()
131
+ ]).finally(async () => {
132
+ 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
+ }
145
+ })
146
+
147
+ const obj = WAProto.Message.fromObject({
148
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
149
+ url: handle ? undefined : mediaUrl, directPath, mediaKey, fileEncSha256, fileSha256, fileLength,
150
+ mediaKeyTimestamp: handle ? undefined : unixTimestampSeconds(), ...uploadData, media: undefined
151
+ })
152
+ })
153
+
154
+ if (uploadData.ptv) { obj.ptvMessage = obj.videoMessage; delete obj.videoMessage }
155
+ if (cacheableKey) await options.mediaCache?.set(cacheableKey, WAProto.Message.encode(obj).finish())
156
+ return obj
157
+ }
158
+
159
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => WAProto.Message.fromObject({
160
+ ephemeralMessage: { message: { protocolMessage: { type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: ephemeralExpiration || 0 } } }
161
+ });
162
+
163
+ 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
+
168
+ if (key === 'conversation') {
169
+ content.extendedTextMessage = { text: content[key] };
170
+ delete content.conversation;
171
+ key = 'extendedTextMessage';
172
+ }
173
+
174
+ content[key].contextInfo = score > 0 ? { forwardingScore: score, isForwarded: true } : {};
175
+ return content;
176
+ };
177
+
178
+ // ===== MESSAGE HANDLERS =====
179
+ 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
+
183
+ if (urlInfo) {
184
+ Object.assign(extContent, {
185
+ matchedText: urlInfo['matched-text'], jpegThumbnail: urlInfo.jpegThumbnail,
186
+ 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
+ }
195
+ }
196
+
197
+ if (options.backgroundColor) extContent.backgroundArgb = assertColor(options.backgroundColor);
198
+ if (options.font) extContent.font = options.font;
199
+ return { extendedTextMessage: extContent };
200
+ };
201
+
202
+ const handleSpecialMessages = async (message, options) => {
203
+ 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) };
207
+ }
208
+ if ('location' in message) return { locationMessage: WAProto.Message.LocationMessage.create(message.location) };
209
+ if ('react' in message) {
210
+ if (!message.react.senderTimestampMs) message.react.senderTimestampMs = Date.now();
211
+ return { reactionMessage: WAProto.Message.ReactionMessage.create(message.react) };
212
+ }
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);
215
+ if ('disappearingMessagesInChat' in message) {
216
+ const exp = typeof message.disappearingMessagesInChat === 'boolean' ? (message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) : message.disappearingMessagesInChat;
217
+ return prepareDisappearingMessageSettingContent(exp);
218
+ }
219
+ return null;
220
+ };
221
+
222
+ const handleGroupInvite = async (message, options) => {
223
+ const m = {
224
+ groupInviteMessage: {
225
+ inviteCode: message.groupInvite.inviteCode, inviteExpiration: message.groupInvite.inviteExpiration,
226
+ caption: message.groupInvite.text, groupJid: message.groupInvite.jid, groupName: message.groupInvite.subject
227
+ }
228
+ };
229
+
230
+ if (options.getProfilePicUrl) {
231
+ const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
232
+ 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());
235
+ }
236
+ }
237
+ return m;
238
+ };
239
+
240
+ const handleEventMessage = (message, options) => {
241
+ const startTime = Math.floor(message.event.startDate.getTime() / 1000);
242
+ const m = {
243
+ eventMessage: {
244
+ name: message.event.name, description: message.event.description, startTime,
245
+ endTime: message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined,
246
+ isCanceled: message.event.isCancelled ?? false, extraGuestsAllowed: message.event.extraGuestsAllowed,
247
+ isScheduleCall: message.event.isScheduleCall ?? false, location: message.event.location
248
+ },
249
+ messageContextInfo: { messageSecret: message.event.messageSecret || randomBytes(32) }
250
+ };
251
+
252
+ 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
+ });
256
+ }
257
+ return m;
258
+ };
259
+
260
+ 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 });
265
+ 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
+ };
275
+
276
+ 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
+ };
280
+
281
+ 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
+
289
+ const m = {
290
+ 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
296
+ })
297
+ };
298
+
299
+ if (message.requestPayment.currencyCodeIso4217 === 'BRL' && message.requestPayment.pixKey) {
300
+ if (!m.requestPaymentMessage.noteMessage.extendedTextMessage)
301
+ m.requestPaymentMessage.noteMessage = { extendedTextMessage: { text: '' } };
302
+ m.requestPaymentMessage.noteMessage.extendedTextMessage.text += `\nPix Key: ${message.requestPayment.pixKey}`;
303
+ }
304
+
305
+ return m;
306
+ };
307
+
308
+ // ===== MAIN GENERATOR =====
309
+ 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
+ );
318
+
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
+ }
330
+
331
+ let m = {};
332
+
333
+ // ===== HANDLE TEXT =====
334
+ 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);
336
+ }
337
+
338
+ // ===== HANDLE SPECIAL MESSAGES =====
339
+ 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 } };
375
+ }
376
+ else if (MEDIA_KEYS.some(k => k in message)) m = await prepareWAMessageMedia(message, options);
377
+ }
378
+
379
+ // ===== SMART BUTTON HANDLING =====
380
+ 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
+
383
+ if (hasNativeFlow) {
384
+ // Convert to interactiveMessage
385
+ const interactive = {
386
+ body: { text: message.text || message.caption || message.contentText || '' },
387
+ footer: { text: message.footer || message.footerText || '' },
388
+ nativeFlowMessage: {
389
+ 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 || '' }) };
393
+ })
394
+ }
395
+ };
396
+
397
+ if (message.title) interactive.header = { title: message.title, subtitle: message.subtitle, hasMediaAttachment: message.hasMediaAttachment || false };
398
+ if (Object.keys(m).length > 0) {
399
+ interactive.header = interactive.header || { title: message.title || '', hasMediaAttachment: true };
400
+ Object.assign(interactive.header, m);
401
+ }
402
+
403
+ m = { interactiveMessage: interactive };
404
+ } 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 };
412
+ }
413
+ }
414
+
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 } };
422
+ }
423
+
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 } };
427
+ }
428
+
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 };
437
+ }
438
+
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);
460
+ }
461
+
462
+ if ('footer' in message && !!message.footer) interactiveMessage.footer = { text: message.footer };
463
+
464
+ m = { interactiveMessage };
465
+ }
466
+
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]) {
472
+ m[finalKey].contextInfo = {
473
+ ...(m[finalKey].contextInfo || {}),
474
+ ...(message.contextInfo || {}),
475
+ mentionedJid: message.mentions || message.contextInfo?.mentionedJid || []
476
+ };
477
+ }
478
+
479
+ // ViewOnce wrapper
480
+ if (message.viewOnce === true) m = { viewOnceMessage: { message: m } };
481
+
482
+ // Edit wrapper
483
+ if (message.edit) m = { protocolMessage: { key: message.edit, editedMessage: m, timestampMs: Date.now(), type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT } };
484
+
485
+ return WAProto.Message.create(m);
486
+ };
487
+
488
+
489
+ 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
+
496
+ 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;
505
+ }
506
+
507
+ if (options?.ephemeralExpiration && key !== 'protocolMessage' && key !== 'ephemeralMessage' && !isJidNewsletter(jid)) {
508
+ innerMessage[key].contextInfo = { ...(innerMessage[key].contextInfo || {}), expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL };
509
+ }
510
+
511
+ 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,
515
+ messageStubParameters: [],
516
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
517
+ status: WAMessageStatus.PENDING
518
+ });
519
+ };
520
+
521
+ 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';
528
+
529
+ 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
+ };
535
+
536
+ 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
+ };
542
+
543
+ 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
+ };
548
+
549
+ export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
550
+ const opts = message?.pollCreationMessage?.options || message?.pollCreationMessageV2?.options || message?.pollCreationMessageV3?.options || [];
551
+ 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
+
557
+ 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));
564
+ }
565
+ }
566
+ return Object.values(voteHashMap);
567
+ }
568
+
569
+ export const aggregateMessageKeysNotFromMe = (keys) => {
570
+ const keyMap = {};
571
+ for (const { remoteJid, id, participant, fromMe } of keys) {
572
+ if (!fromMe) {
573
+ const uqKey = `${remoteJid}:${participant || ''}`;
574
+ keyMap[uqKey] ||= { jid: remoteJid, participant, messageIds: [] };
575
+ keyMap[uqKey].messageIds.push(id);
576
+ }
577
+ }
578
+ return Object.values(keyMap);
579
+ };
580
+
581
+ const REUPLOAD_STATUS = [410, 404];
582
+
583
+ export const downloadMediaMessage = async (message, type, options, ctx) => {
584
+ 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];
596
+ 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);
600
+ if (type === 'buffer') {
601
+ const chunks = [];
602
+ for await (const chunk of stream) chunks.push(chunk);
603
+ return Buffer.concat(chunks);
604
+ }
605
+ return stream;
606
+ };
607
+ 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();
611
+ }
612
+ throw error;
613
+ });
614
+ };
615
+
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 });
619
+
620
+ const lib = await getImageProcessingLibrary();
621
+ const packId_ = packId || generateMessageIDV2();
622
+ const validStickers = [];
623
+
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;
636
+ }
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
+ }
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
+ }
697
+
698
+ return { stickerPackMessage: await processBatch(validStickers, 0), isBatched: false };
699
+ }
700
+
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
706
  };