@kanaraa/baileys 3.1.8

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1426 -0
  3. package/WAProto/GenerateStatics.sh +3 -0
  4. package/WAProto/WAProto.proto +5479 -0
  5. package/WAProto/fix-imports.js +85 -0
  6. package/WAProto/index.js +97691 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Defaults/index.js +128 -0
  9. package/lib/Signal/Group/ciphertext-message.js +11 -0
  10. package/lib/Signal/Group/group-session-builder.js +29 -0
  11. package/lib/Signal/Group/group_cipher.js +81 -0
  12. package/lib/Signal/Group/index.js +11 -0
  13. package/lib/Signal/Group/keyhelper.js +17 -0
  14. package/lib/Signal/Group/sender-chain-key.js +25 -0
  15. package/lib/Signal/Group/sender-key-distribution-message.js +62 -0
  16. package/lib/Signal/Group/sender-key-message.js +65 -0
  17. package/lib/Signal/Group/sender-key-name.js +47 -0
  18. package/lib/Signal/Group/sender-key-record.js +40 -0
  19. package/lib/Signal/Group/sender-key-state.js +83 -0
  20. package/lib/Signal/Group/sender-message-key.js +25 -0
  21. package/lib/Signal/libsignal.js +430 -0
  22. package/lib/Signal/lid-mapping.js +276 -0
  23. package/lib/Socket/Client/index.js +2 -0
  24. package/lib/Socket/Client/types.js +10 -0
  25. package/lib/Socket/Client/websocket.js +53 -0
  26. package/lib/Socket/business.js +378 -0
  27. package/lib/Socket/chats.js +1192 -0
  28. package/lib/Socket/communities.js +430 -0
  29. package/lib/Socket/groups.js +346 -0
  30. package/lib/Socket/index.js +11 -0
  31. package/lib/Socket/messages-recv.js +1785 -0
  32. package/lib/Socket/messages-send.js +1174 -0
  33. package/lib/Socket/mex.js +41 -0
  34. package/lib/Socket/newsletter.js +180 -0
  35. package/lib/Socket/socket.js +966 -0
  36. package/lib/Types/Auth.js +1 -0
  37. package/lib/Types/Bussines.js +1 -0
  38. package/lib/Types/Call.js +1 -0
  39. package/lib/Types/Chat.js +7 -0
  40. package/lib/Types/Contact.js +1 -0
  41. package/lib/Types/Events.js +1 -0
  42. package/lib/Types/GroupMetadata.js +1 -0
  43. package/lib/Types/Label.js +24 -0
  44. package/lib/Types/LabelAssociation.js +6 -0
  45. package/lib/Types/Message.js +10 -0
  46. package/lib/Types/Mex.js +36 -0
  47. package/lib/Types/Product.js +1 -0
  48. package/lib/Types/Signal.js +1 -0
  49. package/lib/Types/Socket.js +2 -0
  50. package/lib/Types/State.js +55 -0
  51. package/lib/Types/USync.js +1 -0
  52. package/lib/Types/index.js +25 -0
  53. package/lib/Utils/auth-utils.js +301 -0
  54. package/lib/Utils/browser-utils.js +27 -0
  55. package/lib/Utils/business.js +230 -0
  56. package/lib/Utils/chat-utils.js +871 -0
  57. package/lib/Utils/companion-reg-client-utils.js +34 -0
  58. package/lib/Utils/crypto.js +117 -0
  59. package/lib/Utils/decode-wa-message.js +316 -0
  60. package/lib/Utils/event-buffer.js +621 -0
  61. package/lib/Utils/generics.js +392 -0
  62. package/lib/Utils/history.js +133 -0
  63. package/lib/Utils/identity-change-handler.js +49 -0
  64. package/lib/Utils/index.js +22 -0
  65. package/lib/Utils/interactive-send.js +136 -0
  66. package/lib/Utils/link-preview.js +84 -0
  67. package/lib/Utils/logger.js +2 -0
  68. package/lib/Utils/lt-hash.js +7 -0
  69. package/lib/Utils/make-mutex.js +32 -0
  70. package/lib/Utils/message-retry-manager.js +264 -0
  71. package/lib/Utils/messages-media.js +787 -0
  72. package/lib/Utils/messages.js +2306 -0
  73. package/lib/Utils/noise-handler.js +200 -0
  74. package/lib/Utils/offline-node-processor.js +39 -0
  75. package/lib/Utils/pre-key-manager.js +105 -0
  76. package/lib/Utils/process-message.js +629 -0
  77. package/lib/Utils/reporting-utils.js +257 -0
  78. package/lib/Utils/signal.js +200 -0
  79. package/lib/Utils/stanza-ack.js +37 -0
  80. package/lib/Utils/sync-action-utils.js +48 -0
  81. package/lib/Utils/tc-token-utils.js +162 -0
  82. package/lib/Utils/use-multi-file-auth-state.js +120 -0
  83. package/lib/Utils/validate-connection.js +202 -0
  84. package/lib/WABinary/constants.js +1300 -0
  85. package/lib/WABinary/decode.js +261 -0
  86. package/lib/WABinary/encode.js +219 -0
  87. package/lib/WABinary/generic-utils.js +112 -0
  88. package/lib/WABinary/index.js +5 -0
  89. package/lib/WABinary/jid-utils.js +95 -0
  90. package/lib/WABinary/types.js +1 -0
  91. package/lib/WAM/BinaryInfo.js +9 -0
  92. package/lib/WAM/constants.js +22852 -0
  93. package/lib/WAM/encode.js +149 -0
  94. package/lib/WAM/index.js +3 -0
  95. package/lib/WAUSync/Protocols/USyncContactProtocol.js +51 -0
  96. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +53 -0
  97. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -0
  98. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -0
  99. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +24 -0
  100. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -0
  101. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -0
  102. package/lib/WAUSync/Protocols/index.js +5 -0
  103. package/lib/WAUSync/USyncQuery.js +97 -0
  104. package/lib/WAUSync/USyncUser.js +30 -0
  105. package/lib/WAUSync/index.js +3 -0
  106. package/lib/index.js +10 -0
  107. package/package.json +75 -0
@@ -0,0 +1,2306 @@
1
+ 'use strict';
2
+
3
+ import { Boom } from "@hapi/boom";
4
+ import { randomBytes } from "crypto";
5
+ import { promises as fs } from "fs";
6
+ import { PassThrough, Readable } from "stream";
7
+ import sharp from "sharp";
8
+ import ffmpeg from "fluent-ffmpeg";
9
+ import { proto } from "../../WAProto/index.js";
10
+ import {
11
+ CALL_AUDIO_PREFIX,
12
+ CALL_VIDEO_PREFIX,
13
+ MEDIA_KEYS,
14
+ URL_REGEX,
15
+ WA_DEFAULT_EPHEMERAL,
16
+ } from "../Defaults/index.js";
17
+ import { WAMessageStatus, WAProto } from "../Types/index.js";
18
+ import {
19
+ isJidGroup,
20
+ isJidNewsletter,
21
+ isJidStatusBroadcast,
22
+ jidNormalizedUser,
23
+ } from "../WABinary/index.js";
24
+ import { sha256 } from "./crypto.js";
25
+ import {
26
+ generateMessageID,
27
+ generateMessageIDV2,
28
+ getKeyAuthor,
29
+ unixTimestampSeconds,
30
+ } from "./generics.js";
31
+ import {
32
+ downloadContentFromMessage,
33
+ encryptedStream,
34
+ generateThumbnail,
35
+ getAudioDuration,
36
+ getAudioWaveform,
37
+ getRawMediaUploadData,
38
+ getStream,
39
+ toBuffer,
40
+ } from "./messages-media.js";
41
+
42
+ export const Toolkit = {
43
+ async resize(input, width, height, fit = 'cover') {
44
+ const buf = Buffer.isBuffer(input) ? input : await Toolkit.fetchBuffer(input);
45
+ return sharp(buf).resize(width, height, { fit, position: 'center', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer();
46
+ },
47
+
48
+ async fetchBuffer(url, options = {}, { silent = true } = {}) {
49
+ try {
50
+ const res = await fetch(url, options);
51
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
52
+ return Buffer.from(await res.arrayBuffer());
53
+ } catch (err) {
54
+ if (silent) return Buffer.alloc(0);
55
+ throw err;
56
+ }
57
+ },
58
+
59
+ getMp4DurationSync(buf) {
60
+ try {
61
+ if (!Buffer.isBuffer(buf) || buf.length < 8) return 0;
62
+ let offset = 0;
63
+ while (offset < buf.length - 8) {
64
+ const size = buf.readUInt32BE(offset);
65
+ if (size < 8 || offset + size > buf.length) break;
66
+ const type = buf.toString('ascii', offset + 4, offset + 8);
67
+ if (type === 'moov') {
68
+ let moovOffset = offset + 8;
69
+ const moovEnd = offset + size;
70
+ while (moovOffset < moovEnd - 8) {
71
+ const childSize = buf.readUInt32BE(moovOffset);
72
+ if (childSize < 8 || moovOffset + childSize > moovEnd) break;
73
+ const childType = buf.toString('ascii', moovOffset + 4, moovOffset + 8);
74
+ if (childType === 'mvhd') {
75
+ const version = buf.readUInt8(moovOffset + 8);
76
+ if (version === 0) {
77
+ const timescale = buf.readUInt32BE(moovOffset + 20);
78
+ const duration = buf.readUInt32BE(moovOffset + 24);
79
+ return timescale ? duration / timescale : 0;
80
+ }
81
+ if (version === 1) {
82
+ const timescale = buf.readUInt32BE(moovOffset + 32);
83
+ const duration = Number(buf.readBigUInt64BE(moovOffset + 36));
84
+ return timescale ? duration / timescale : 0;
85
+ }
86
+ }
87
+ moovOffset += childSize;
88
+ }
89
+ }
90
+ offset += size;
91
+ }
92
+ return 0;
93
+ } catch { return 0; }
94
+ },
95
+
96
+ async getMp4Duration(input) {
97
+ try {
98
+ const buf = Buffer.isBuffer(input) ? input : await Toolkit.fetchBuffer(input);
99
+ if (!buf || !buf.length) return 0;
100
+ return Toolkit.getMp4DurationSync(buf);
101
+ } catch { return 0; }
102
+ },
103
+
104
+ getMp4Preview(videoBuffer, { time, result = 'buffer', resize = true, width = 300, height = 150, silent = true } = {}) {
105
+ return new Promise((resolve, reject) => {
106
+ const fail = (err) => {
107
+ if (silent) return resolve(result === 'base64' ? '' : Buffer.alloc(0));
108
+ return reject(err);
109
+ };
110
+ try {
111
+ if (!Buffer.isBuffer(videoBuffer) || !videoBuffer.length) return fail(new Error('Invalid buffer'));
112
+ const inputStream = new Readable({ read() {} });
113
+ inputStream.push(videoBuffer);
114
+ inputStream.push(null);
115
+ const outputStream = new PassThrough();
116
+ const chunks = [];
117
+ outputStream.on('data', (chunk) => chunks.push(chunk));
118
+ outputStream.on('end', async () => {
119
+ try {
120
+ let output = Buffer.concat(chunks);
121
+ if (!output.length) return fail(new Error('Empty output'));
122
+ if (resize) output = await Toolkit.resize(output, width, height);
123
+ return resolve(result === 'base64' ? output.toString('base64') : output);
124
+ } catch (err) { return fail(err); }
125
+ });
126
+ outputStream.on('error', fail);
127
+ time ??= Math.min(Toolkit.getMp4DurationSync(videoBuffer) * 0.2, 10);
128
+ ffmpeg(inputStream)
129
+ .outputOptions([`-ss ${time}`, '-vframes 1', '-vcodec png', '-f image2pipe'])
130
+ .on('error', (err) => fail(new Error(`ffmpeg error: ${err.message}`)))
131
+ .pipe(outputStream, { end: true });
132
+ } catch (err) { return fail(err); }
133
+ });
134
+ },
135
+
136
+ async resolveMedia(input, as = 'url') {
137
+ if (!input) return null;
138
+ if (as === 'buffer') {
139
+ if (Buffer.isBuffer(input)) return input;
140
+ if (typeof input === 'string' && input.startsWith('data:')) return Buffer.from(input.split(',')[1], 'base64');
141
+ if (typeof input === 'string') return Toolkit.fetchBuffer(input);
142
+ }
143
+ if (as === 'base64') {
144
+ const buf = await Toolkit.resolveMedia(input, 'buffer');
145
+ return buf ? buf.toString('base64') : null;
146
+ }
147
+ if (typeof input === 'string' && !input.startsWith('data:')) return input;
148
+ if (Buffer.isBuffer(input)) return `data:application/octet-stream;base64,${input.toString('base64')}`;
149
+ return input;
150
+ },
151
+
152
+ async waitAllPromises(obj) {
153
+ if (obj instanceof Promise) return Toolkit.waitAllPromises(await obj);
154
+ if (Array.isArray(obj)) return Promise.all(obj.map(Toolkit.waitAllPromises));
155
+ if (obj !== null && typeof obj === 'object') {
156
+ const entries = await Promise.all(Object.entries(obj).map(async ([k, v]) => [k, await Toolkit.waitAllPromises(v)]));
157
+ return Object.fromEntries(entries);
158
+ }
159
+ return obj;
160
+ },
161
+
162
+ extractIE(text, opts) {
163
+ return extractIE(text, opts);
164
+ }
165
+ };
166
+
167
+ function extractIE(text, { extract = true, hyperlink = true, citation = true, latex = true } = {}) {
168
+ if (!extract) {
169
+ return { text, ie: [], inline_entities: [] };
170
+ }
171
+
172
+ let ie = [], result = '', last = 0,
173
+ citation_index = 1, hyperlink_index = 0, latex_index = 0,
174
+ stack = [];
175
+
176
+ for (let i = 0; i < text.length; i++) {
177
+ if (text[i] === '[' && text[i - 1] !== '\\') {
178
+ stack.push(i);
179
+ } else if (text[i] === ']' && (text[i + 1] === '(' || text[i + 1] === '<')) {
180
+ let start = stack.pop();
181
+ if (start == null) continue;
182
+ let open = text[i + 1],
183
+ close = open === '(' ? ')' : '>',
184
+ type = open === '(' ? 'link' : 'latex',
185
+ end = i + 2, depth = 1;
186
+ while (end < text.length && depth) {
187
+ if (text[end] === open && text[end - 1] !== '\\') depth++;
188
+ else if (text[end] === close && text[end - 1] !== '\\') depth--;
189
+ end++;
190
+ }
191
+ if (depth) continue;
192
+ let raw = text.slice(start + 1, i).trim(),
193
+ url = text.slice(i + 2, end - 1).trim(),
194
+ key, tag, data;
195
+
196
+ if (type === 'latex') {
197
+ if (!latex) continue;
198
+ let [txt = '', width = null, height = null, font_height = null, padding = null] = raw.split('|');
199
+ key = `NIXEL_LATEX_${latex_index++}`;
200
+ tag = `{{${key}}}${txt || 'image'}{{/${key}}}`;
201
+ data = { type: 'latex', ie: { key, text: txt, url, width, height, font_height, padding } };
202
+ } else if (raw) {
203
+ if (!hyperlink) continue;
204
+ const isTrusted = !url.startsWith('!');
205
+ const resolvedUrl = isTrusted ? url : url.slice(1);
206
+ key = `NIXEL_HYPERLINK_${hyperlink_index++}`;
207
+ tag = `{{${key}}}${resolvedUrl}{{/${key}}}`;
208
+ data = { type: 'hyperlink', ie: { key, text: raw, url: resolvedUrl, is_trusted: isTrusted } };
209
+ } else {
210
+ if (!citation) continue;
211
+ key = `NIXEL_CITATION_${citation_index - 1}`;
212
+ tag = `{{${key}}}${url}{{/${key}}}`;
213
+ data = { type: 'citation', ie: { reference_id: citation_index++, key, text: '', url } };
214
+ }
215
+ result += text.slice(last, start) + tag;
216
+ last = end;
217
+ ie.push(data);
218
+ i = end - 1;
219
+ }
220
+ }
221
+ result += text.slice(last);
222
+
223
+ const inline_entities = ie.map(({ type, ie: e }) => {
224
+ if (type === 'hyperlink') return {
225
+ key: e.key,
226
+ metadata: { display_name: e.text, is_trusted: e.is_trusted !== false, url: e.url, __typename: 'GenAIInlineLinkItem' }
227
+ };
228
+ if (type === 'citation') return {
229
+ key: e.key,
230
+ metadata: {
231
+ reference_id: e.reference_id,
232
+ reference_url: e.url,
233
+ reference_title: e.url,
234
+ reference_display_name: e.url,
235
+ sources: [],
236
+ __typename: 'GenAISearchCitationItem'
237
+ }
238
+ };
239
+ if (type === 'latex') return {
240
+ key: e.key,
241
+ metadata: {
242
+ latex_expression: e.text,
243
+ latex_image: { url: e.url, width: Number(e.width) || 100, height: Number(e.height) || 100 },
244
+ font_height: Number(e.font_height) || 83.333333333333,
245
+ padding: Number(e.padding) || 15,
246
+ __typename: 'GenAILatexItem'
247
+ }
248
+ };
249
+ });
250
+
251
+ return { text: result, ie, inline_entities };
252
+ }
253
+
254
+ export const waitAllPromises = (obj) => Toolkit.waitAllPromises(obj);
255
+
256
+ async function processLocationThumbnail(input, options = {}) {
257
+ const { maxWidth = 300, maxHeight = 300, quality = 80 } = options;
258
+ let buffer;
259
+ if (typeof input === 'string' && (input.startsWith('http://') || input.startsWith('https://'))) {
260
+ const stream = await getStream(input);
261
+ buffer = await toBuffer(stream);
262
+ } else if (typeof input === 'string') {
263
+ buffer = await fs.readFile(input);
264
+ } else if (Buffer.isBuffer(input)) {
265
+ buffer = input;
266
+ } else {
267
+ throw new Error('thumbnail must be URL, path or Buffer');
268
+ }
269
+ return sharp(buffer)
270
+ .resize(maxWidth, maxHeight, { fit: 'inside', withoutEnlargement: true })
271
+ .jpeg({ quality })
272
+ .toBuffer();
273
+ }
274
+
275
+ const MIMETYPE_MAP = {
276
+ image: "image/jpeg",
277
+ video: "video/mp4",
278
+ document: "application/pdf",
279
+ audio: "audio/ogg; codecs=opus",
280
+ sticker: "image/webp",
281
+ "product-catalog-image": "image/jpeg",
282
+ };
283
+
284
+ const MessageTypeProto = {
285
+ image: WAProto.Message.ImageMessage,
286
+ video: WAProto.Message.VideoMessage,
287
+ audio: WAProto.Message.AudioMessage,
288
+ sticker: WAProto.Message.StickerMessage,
289
+ document: WAProto.Message.DocumentMessage,
290
+ };
291
+
292
+ export const hasNonNullishProperty = (message, key) =>
293
+ typeof message === 'object' &&
294
+ message !== null &&
295
+ key in message &&
296
+ message[key] !== null &&
297
+ message[key] !== undefined;
298
+
299
+ function hasOptionalProperty(obj, key) {
300
+ return typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null;
301
+ }
302
+
303
+ export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
304
+
305
+ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
306
+ const url = extractUrlFromText(text);
307
+ if (!!getUrlInfo && url) {
308
+ try {
309
+ const urlInfo = await getUrlInfo(url);
310
+ return urlInfo;
311
+ } catch (error) {
312
+ logger?.warn({ trace: error.stack }, "url generation failed");
313
+ }
314
+ }
315
+ };
316
+
317
+ const assertColor = async (color) => {
318
+ let assertedColor;
319
+ if (typeof color === "number") {
320
+ assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
321
+ } else {
322
+ let hex = color.trim().replace("#", "");
323
+ if (hex.length <= 6) {
324
+ hex = "FF" + hex.padStart(6, "0");
325
+ }
326
+ assertedColor = parseInt(hex, 16);
327
+ return assertedColor;
328
+ }
329
+ };
330
+
331
+ export const prepareWAMessageMedia = async (message, options) => {
332
+ const logger = options.logger;
333
+ let mediaType;
334
+ for (const key of MEDIA_KEYS) {
335
+ if (key in message) {
336
+ mediaType = key;
337
+ }
338
+ }
339
+ if (!mediaType) {
340
+ throw new Boom("Invalid media type", { statusCode: 400 });
341
+ }
342
+ const uploadData = { ...message, media: message[mediaType] };
343
+ delete uploadData[mediaType];
344
+ const cacheableKey =
345
+ typeof uploadData.media === "object" &&
346
+ "url" in uploadData.media &&
347
+ !!uploadData.media.url &&
348
+ !!options.mediaCache &&
349
+ mediaType + ":" + uploadData.media.url.toString();
350
+ if (mediaType === "document" && !uploadData.fileName) {
351
+ uploadData.fileName = "file";
352
+ }
353
+ if (!uploadData.mimetype) {
354
+ uploadData.mimetype = MIMETYPE_MAP[mediaType];
355
+ }
356
+ if (cacheableKey) {
357
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
358
+ if (mediaBuff) {
359
+ logger?.debug({ cacheableKey }, "got media cache hit");
360
+ const obj = proto.Message.decode(mediaBuff);
361
+ const key = `${mediaType}Message`;
362
+ Object.assign(obj[key], { ...uploadData, media: undefined });
363
+ return obj;
364
+ }
365
+ }
366
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
367
+ if (isNewsletter) {
368
+ logger?.info({ key: cacheableKey }, "Preparing raw media for newsletter");
369
+ const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(
370
+ uploadData.media,
371
+ options.mediaTypeOverride || mediaType,
372
+ logger,
373
+ );
374
+ const fileSha256B64 = fileSha256.toString("base64");
375
+ const { mediaUrl, directPath } = await options.upload(filePath, {
376
+ fileEncSha256B64: fileSha256B64,
377
+ mediaType: mediaType,
378
+ timeoutMs: options.mediaUploadTimeoutMs,
379
+ });
380
+ await fs.unlink(filePath);
381
+ const obj = WAProto.Message.fromObject({
382
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
383
+ url: mediaUrl,
384
+ directPath,
385
+ fileSha256,
386
+ fileLength,
387
+ ...uploadData,
388
+ media: undefined,
389
+ }),
390
+ });
391
+ if (uploadData.ptv) {
392
+ obj.ptvMessage = obj.videoMessage;
393
+ delete obj.videoMessage;
394
+ }
395
+ if (obj.stickerMessage) {
396
+ obj.stickerMessage.stickerSentTs = Date.now();
397
+ }
398
+ if (cacheableKey) {
399
+ logger?.debug({ cacheableKey }, "set cache");
400
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
401
+ }
402
+ return obj;
403
+ }
404
+ const requiresDurationComputation = mediaType === "audio" && typeof uploadData.seconds === "undefined";
405
+ const requiresThumbnailComputation =
406
+ (mediaType === "image" || mediaType === "video") &&
407
+ typeof uploadData["jpegThumbnail"] === "undefined";
408
+ const requiresWaveformProcessing = mediaType === "audio" && uploadData.ptt === true;
409
+ const requiresAudioBackground = options.backgroundColor && mediaType === "audio" && uploadData.ptt === true;
410
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
411
+ const {
412
+ mediaKey,
413
+ encFilePath,
414
+ originalFilePath,
415
+ fileEncSha256,
416
+ fileSha256,
417
+ fileLength,
418
+ } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
419
+ logger,
420
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
421
+ opts: options.options,
422
+ });
423
+ const fileEncSha256B64 = fileEncSha256.toString("base64");
424
+ const [{ mediaUrl, directPath }] = await Promise.all([
425
+ (async () => {
426
+ const result = await options.upload(encFilePath, {
427
+ fileEncSha256B64,
428
+ mediaType,
429
+ timeoutMs: options.mediaUploadTimeoutMs,
430
+ });
431
+ logger?.debug({ mediaType, cacheableKey }, "uploaded media");
432
+ return result;
433
+ })(),
434
+ (async () => {
435
+ try {
436
+ if (requiresThumbnailComputation) {
437
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
438
+ uploadData.jpegThumbnail = thumbnail;
439
+ if (!uploadData.width && originalImageDimensions) {
440
+ uploadData.width = originalImageDimensions.width;
441
+ uploadData.height = originalImageDimensions.height;
442
+ logger?.debug("set dimensions");
443
+ }
444
+ logger?.debug("generated thumbnail");
445
+ }
446
+ if (requiresDurationComputation) {
447
+ uploadData.seconds = await getAudioDuration(originalFilePath);
448
+ logger?.debug("computed audio duration");
449
+ }
450
+ if (requiresWaveformProcessing) {
451
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
452
+ logger?.debug("processed waveform");
453
+ }
454
+ if (requiresAudioBackground) {
455
+ uploadData.backgroundArgb = await assertColor(options.backgroundColor);
456
+ logger?.debug("computed backgroundColor audio status");
457
+ }
458
+ } catch (error) {
459
+ logger?.warn({ trace: error.stack }, "failed to obtain extra info");
460
+ }
461
+ })(),
462
+ ]).finally(async () => {
463
+ try {
464
+ await fs.unlink(encFilePath);
465
+ if (originalFilePath) {
466
+ await fs.unlink(originalFilePath);
467
+ }
468
+ logger?.debug("removed tmp files");
469
+ } catch (error) {
470
+ logger?.warn("failed to remove tmp file");
471
+ }
472
+ });
473
+ const obj = WAProto.Message.fromObject({
474
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
475
+ url: mediaUrl,
476
+ directPath,
477
+ mediaKey,
478
+ fileEncSha256,
479
+ fileSha256,
480
+ fileLength,
481
+ mediaKeyTimestamp: unixTimestampSeconds(),
482
+ ...uploadData,
483
+ media: undefined,
484
+ }),
485
+ });
486
+ if (uploadData.ptv) {
487
+ obj.ptvMessage = obj.videoMessage;
488
+ delete obj.videoMessage;
489
+ }
490
+ if (cacheableKey) {
491
+ logger?.debug({ cacheableKey }, "set cache");
492
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
493
+ }
494
+ return obj;
495
+ };
496
+
497
+ export const prepareAlbumMessageContent = async (jid, albums, options) => {
498
+ if (!Array.isArray(albums)) {
499
+ throw new Error("albums must be an array containing media objects.");
500
+ }
501
+ if (albums.length === 0) {
502
+ throw new Error("albums cannot be empty. At least one media item is required.");
503
+ }
504
+ const validCount = albums.filter((m) => "image" in m || "video" in m).length;
505
+ if (validCount === 0) {
506
+ throw new Error("albums contains no valid media. Use 'image' or 'video' keys.");
507
+ }
508
+ let mediaHandle;
509
+ let mediaMsg;
510
+ const message = [];
511
+ const albumMsg = generateWAMessageFromContent(
512
+ jid,
513
+ {
514
+ albumMessage: {
515
+ expectedImageCount: albums.filter((item) => "image" in item).length,
516
+ expectedVideoCount: albums.filter((item) => "video" in item).length,
517
+ },
518
+ },
519
+ options
520
+ );
521
+ await options.conn.relayMessage(jid, albumMsg.message, { messageId: albumMsg.key.id });
522
+ for (const media of albums) {
523
+ let content = {};
524
+ if ("image" in media) {
525
+ content = { image: media.image };
526
+ } else if ("video" in media) {
527
+ content = { video: media.video };
528
+ } else {
529
+ continue;
530
+ }
531
+ mediaMsg = await generateWAMessage(
532
+ jid,
533
+ { ...content, ...media },
534
+ {
535
+ userJid: options.userJid,
536
+ upload: async (encFilePath, opts) => {
537
+ const up = await options.conn.waUploadToServer(encFilePath, {
538
+ ...opts,
539
+ newsletter: isJidNewsletter(jid),
540
+ });
541
+ mediaHandle = up.handle;
542
+ return up;
543
+ },
544
+ ...options,
545
+ }
546
+ );
547
+ if (mediaMsg) {
548
+ mediaMsg.message.messageContextInfo = {
549
+ messageSecret: randomBytes(32),
550
+ messageAssociation: {
551
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM,
552
+ parentMessageKey: albumMsg.key,
553
+ },
554
+ };
555
+ }
556
+ message.push(mediaMsg);
557
+ }
558
+ return message;
559
+ };
560
+
561
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
562
+ ephemeralExpiration = ephemeralExpiration || 0;
563
+ const content = {
564
+ ephemeralMessage: {
565
+ message: {
566
+ protocolMessage: {
567
+ type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
568
+ ephemeralExpiration,
569
+ },
570
+ },
571
+ },
572
+ };
573
+ return WAProto.Message.fromObject(content);
574
+ };
575
+
576
+ export const generateForwardMessageContent = (message, forceForward) => {
577
+ let content = message.message;
578
+ if (!content) {
579
+ throw new Boom("no content in message", { statusCode: 400 });
580
+ }
581
+ content = normalizeMessageContent(content);
582
+ content = proto.Message.decode(proto.Message.encode(content).finish());
583
+ let key = Object.keys(content)[0];
584
+ let score = content?.[key]?.contextInfo?.forwardingScore || 0;
585
+ score += message.key.fromMe && !forceForward ? 0 : 1;
586
+ if (key === "conversation") {
587
+ content.extendedTextMessage = { text: content[key] };
588
+ delete content.conversation;
589
+ key = "extendedTextMessage";
590
+ }
591
+ const key_ = content?.[key];
592
+ if (score > 0) {
593
+ key_.contextInfo = { forwardingScore: score, isForwarded: true };
594
+ } else {
595
+ key_.contextInfo = {};
596
+ }
597
+ return content;
598
+ };
599
+
600
+ export const generateWAMessageContent = async (message, options) => {
601
+ let m = {};
602
+
603
+ if (hasNonNullishProperty(message, "text")) {
604
+ const extContent = { text: message.text };
605
+ let urlInfo = message.linkPreview;
606
+ if (typeof urlInfo === "undefined") {
607
+ urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
608
+ }
609
+ if (urlInfo) {
610
+ extContent.canonicalUrl = urlInfo["canonical-url"];
611
+ extContent.matchedText = urlInfo["matched-text"];
612
+ extContent.jpegThumbnail = urlInfo.jpegThumbnail;
613
+ extContent.description = urlInfo.description;
614
+ extContent.title = urlInfo.title;
615
+ extContent.previewType = 0;
616
+ const img = urlInfo.highQualityThumbnail;
617
+ if (img) {
618
+ extContent.thumbnailDirectPath = img.directPath;
619
+ extContent.mediaKey = img.mediaKey;
620
+ extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
621
+ extContent.thumbnailWidth = img.width;
622
+ extContent.thumbnailHeight = img.height;
623
+ extContent.thumbnailSha256 = img.fileSha256;
624
+ extContent.thumbnailEncSha256 = img.fileEncSha256;
625
+ }
626
+ }
627
+ if (options.backgroundColor) {
628
+ extContent.backgroundArgb = await assertColor(options.backgroundColor);
629
+ }
630
+ if (options.textColor) {
631
+ extContent.textArgb = await assertColor(options.textColor);
632
+ }
633
+ if (options.font) {
634
+ extContent.font = options.font;
635
+ }
636
+ m.extendedTextMessage = extContent;
637
+
638
+ } else if (hasNonNullishProperty(message, "contacts")) {
639
+ const contactLen = message.contacts.contacts.length;
640
+ if (!contactLen) {
641
+ throw new Boom("require atleast 1 contact", { statusCode: 400 });
642
+ }
643
+ if (contactLen === 1) {
644
+ m = { contactMessage: WAProto.Message.ContactMessage.fromObject(message.contacts.contacts[0]) };
645
+ } else {
646
+ m = { contactsArrayMessage: WAProto.Message.ContactsArrayMessage.fromObject(message.contacts) };
647
+ }
648
+
649
+ } else if (hasNonNullishProperty(message, "location")) {
650
+ if (message.location?.jpegThumbnail && !Buffer.isBuffer(message.location.jpegThumbnail)) {
651
+ message.location.jpegThumbnail = await processLocationThumbnail(message.location.jpegThumbnail);
652
+ }
653
+ if (message.live) {
654
+ m = { liveLocationMessage: WAProto.Message.LiveLocationMessage.fromObject(message.location) };
655
+ } else {
656
+ m = { locationMessage: WAProto.Message.LocationMessage.fromObject(message.location) };
657
+ }
658
+
659
+ } else if (hasNonNullishProperty(message, "buttonsMessage")) {
660
+ const raw = { ...message.buttonsMessage };
661
+ if (raw.locationMessage) {
662
+ if (raw.locationMessage.thumbnail && !raw.locationMessage.jpegThumbnail) {
663
+ let tb = raw.locationMessage.thumbnail;
664
+ if (typeof tb === 'string') tb = await Toolkit.fetchBuffer(tb);
665
+ if (Buffer.isBuffer(tb) && tb.length) raw.locationMessage.jpegThumbnail = tb;
666
+ delete raw.locationMessage.thumbnail;
667
+ }
668
+ if (raw.locationMessage.jpegThumbnail) {
669
+ raw.locationMessage.jpegThumbnail = await processLocationThumbnail(raw.locationMessage.jpegThumbnail);
670
+ }
671
+ }
672
+ if (raw.body && !raw.contentText) raw.contentText = raw.body;
673
+ delete raw.body;
674
+ if (raw.footer && !raw.footerText) raw.footerText = raw.footer;
675
+ delete raw.footer;
676
+ if (raw.thumbnail) {
677
+ let tb = raw.thumbnail;
678
+ if (typeof tb === 'string') tb = await Toolkit.fetchBuffer(tb);
679
+ if (Buffer.isBuffer(tb)) raw.jpegThumbnail = tb;
680
+ delete raw.thumbnail;
681
+ }
682
+ if (Array.isArray(raw.buttons)) {
683
+ raw.buttons = raw.buttons.map(btn => {
684
+ const mapped = { ...btn };
685
+ if (mapped.text && !mapped.buttonText) {
686
+ mapped.buttonText = { displayText: mapped.text };
687
+ delete mapped.text;
688
+ }
689
+ if (mapped.id && !mapped.buttonId) {
690
+ mapped.buttonId = mapped.id;
691
+ delete mapped.id;
692
+ }
693
+ if (!mapped.type) mapped.type = 1;
694
+ return mapped;
695
+ });
696
+ }
697
+ if (!raw.headerType) {
698
+ if (raw.jpegThumbnail) raw.headerType = 4;
699
+ else if (raw.videoMessage) raw.headerType = 5;
700
+ else if (raw.documentMessage) raw.headerType = 6;
701
+ else if (raw.locationMessage) raw.headerType = 3;
702
+ else raw.headerType = 1;
703
+ }
704
+ m = { buttonsMessage: raw };
705
+
706
+ } else if (hasNonNullishProperty(message, "react")) {
707
+ if (!message.react.senderTimestampMs) {
708
+ message.react.senderTimestampMs = Date.now();
709
+ }
710
+ m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(message.react);
711
+
712
+ } else if (hasNonNullishProperty(message, "delete")) {
713
+ m.protocolMessage = {
714
+ key: message.delete,
715
+ type: WAProto.Message.ProtocolMessage.Type.REVOKE,
716
+ };
717
+
718
+ } else if (hasNonNullishProperty(message, "sharePhoneNumber")) {
719
+ m.protocolMessage = {
720
+ type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER,
721
+ };
722
+
723
+ } else if (hasNonNullishProperty(message, "requestPhoneNumber")) {
724
+ m.requestPhoneNumberMessage = {};
725
+
726
+ } else if (hasNonNullishProperty(message, "forward")) {
727
+ m = generateForwardMessageContent(message.forward, message.force);
728
+
729
+ } else if (hasNonNullishProperty(message, "disappearingMessagesInChat")) {
730
+ const exp =
731
+ typeof message.disappearingMessagesInChat === "boolean"
732
+ ? message.disappearingMessagesInChat
733
+ ? WA_DEFAULT_EPHEMERAL
734
+ : 0
735
+ : message.disappearingMessagesInChat;
736
+ m = prepareDisappearingMessageSettingContent(exp);
737
+
738
+ } else if (hasNonNullishProperty(message, "groupInvite")) {
739
+ m.groupInviteMessage = {};
740
+ m.groupInviteMessage.inviteCode = message.groupInvite.code ?? message.groupInvite.inviteCode;
741
+ m.groupInviteMessage.inviteExpiration = message.groupInvite.expiration ?? message.groupInvite.inviteExpiration;
742
+ m.groupInviteMessage.caption = message.groupInvite.caption ?? message.groupInvite.text;
743
+ m.groupInviteMessage.groupJid = message.groupInvite.jid;
744
+ m.groupInviteMessage.groupName = message.groupInvite.name ?? message.groupInvite.subject;
745
+ m.groupInviteMessage.contextInfo = message.contextInfo;
746
+ if (options.getProfilePicUrl) {
747
+ const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid);
748
+ const { thumbnail } = await generateThumbnail(pfpUrl, "image");
749
+ m.groupInviteMessage.jpegThumbnail = thumbnail;
750
+ }
751
+
752
+ } else if (hasNonNullishProperty(message, "adminInvite")) {
753
+ m.newsletterAdminInviteMessage = {};
754
+ m.newsletterAdminInviteMessage.newsletterJid = message.adminInvite.jid;
755
+ m.newsletterAdminInviteMessage.newsletterName = message.adminInvite.name;
756
+ m.newsletterAdminInviteMessage.caption = message.adminInvite.caption;
757
+ m.newsletterAdminInviteMessage.inviteExpiration = message.adminInvite.expiration;
758
+ m.newsletterAdminInviteMessage.contextInfo = message.contextInfo;
759
+ if (options.getProfilePicUrl) {
760
+ const pfpUrl = await options.getProfilePicUrl(message.adminInvite.jid);
761
+ const { thumbnail } = await generateThumbnail(pfpUrl, "image");
762
+ m.newsletterAdminInviteMessage.jpegThumbnail = thumbnail;
763
+ }
764
+
765
+ } else if (hasNonNullishProperty(message, "keep")) {
766
+ m.keepInChatMessage = {};
767
+ m.keepInChatMessage.key = message.keep.key;
768
+ m.keepInChatMessage.keepType = message.keep?.type || 1;
769
+ m.keepInChatMessage.timestampMs = message.keep?.time || Date.now();
770
+
771
+ } else if (hasNonNullishProperty(message, "call")) {
772
+ m.scheduledCallCreationMessage = {};
773
+ m.scheduledCallCreationMessage.scheduledTimestampMs = message.call?.time || Date.now();
774
+ m.scheduledCallCreationMessage.callType = message.call?.type || 1;
775
+ m.scheduledCallCreationMessage.title = message.call?.name || "Call Creation";
776
+
777
+ } else if (hasNonNullishProperty(message, "paymentInvite")) {
778
+ m.messageContextInfo = {};
779
+ m.paymentInviteMessage = {};
780
+ m.paymentInviteMessage.expiryTimestamp = message.paymentInvite?.expiry || 0;
781
+ m.paymentInviteMessage.serviceType = message.paymentInvite?.type || 2;
782
+
783
+ } else if (hasNonNullishProperty(message, "payment")) {
784
+ m.requestPaymentMessage = {
785
+ amount: {
786
+ currencyCode: message.payment?.currency || "IDR",
787
+ offset: message.payment?.offset || 0,
788
+ value: message.payment?.amount || 999999999,
789
+ },
790
+ expiryTimestamp: message.payment?.expiry || 0,
791
+ amount1000: (message.payment?.amount || 999999999) * 1e3,
792
+ currencyCodeIso4217: message.payment?.currency || "IDR",
793
+ requestFrom: message.payment?.from || "0@s.whatsapp.net",
794
+ noteMessage: {
795
+ extendedTextMessage: { text: message.payment?.note || "Notes" },
796
+ },
797
+ background: {
798
+ placeholderArgb: message.payment?.image?.placeholderArgb || 4278190080,
799
+ textArgb: message.payment?.image?.textArgb || 4294967295,
800
+ subtextArgb: message.payment?.image?.subtextArgb || 4294967295,
801
+ type: 1,
802
+ },
803
+ };
804
+
805
+ } else if (hasNonNullishProperty(message, "orderStatus")) {
806
+ const os = message.orderStatus;
807
+ if (!os.image) throw new Error("image is required for orderStatus");
808
+ let imageInput = os.image;
809
+ if (typeof imageInput === 'string' && !imageInput.startsWith('http://') && !imageInput.startsWith('https://')) {
810
+ imageInput = await fs.readFile(imageInput);
811
+ }
812
+ const media = await prepareWAMessageMedia({ image: imageInput }, { upload: options.upload, ...options });
813
+ m = {
814
+ viewOnceMessage: {
815
+ message: {
816
+ messageContextInfo: {
817
+ deviceListMetadata: {},
818
+ deviceListMetadataVersion: 2,
819
+ },
820
+ interactiveMessage: proto.Message.InteractiveMessage.create({
821
+ header: { title: os.title || "Order Status", hasMediaAttachment: true, ...media },
822
+ body: { text: os.text || "Silakan cek status pesanan Anda." },
823
+ footer: { text: os.footer || "have a reason" },
824
+ nativeFlowMessage: {
825
+ buttons: [
826
+ {
827
+ name: "order_status",
828
+ buttonParamsJson: JSON.stringify({
829
+ reference_id: os.referenceId || "LV-001",
830
+ order: {
831
+ status: os.status || "PROCESSING",
832
+ subtotal: { value: os.subtotalValue || 0, offset: os.subtotalOffset || 100 },
833
+ tax: { value: os.taxValue || 0, offset: os.taxOffset || 100 },
834
+ currency: os.currency || "IDR",
835
+ },
836
+ }),
837
+ },
838
+ ],
839
+ },
840
+ }),
841
+ },
842
+ },
843
+ };
844
+
845
+ } else if (hasNonNullishProperty(message, "splitPayment")) {
846
+ const sp = message.splitPayment;
847
+ m = {
848
+ interactiveMessage: {
849
+ body: { text: sp.text || "" },
850
+ nativeFlowMessage: {
851
+ buttons: [
852
+ {
853
+ name: "split_payment",
854
+ buttonParamsJson: JSON.stringify({
855
+ reference_id: sp.referenceId || "LV-001",
856
+ currency: sp.currency || "IDR",
857
+ total_amount: {
858
+ value: sp.value || 0,
859
+ offset: sp.offset || 100,
860
+ },
861
+ order_type: sp.orderType || "ORDER",
862
+ }),
863
+ },
864
+ ],
865
+ },
866
+ },
867
+ };
868
+
869
+ } else if (hasNonNullishProperty(message, "pin")) {
870
+ m.pinInChatMessage = {};
871
+ m.messageContextInfo = {};
872
+ m.pinInChatMessage.key = message.pin;
873
+ m.pinInChatMessage.type = message.type;
874
+ m.pinInChatMessage.senderTimestampMs = Date.now();
875
+ m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
876
+
877
+ } else if (hasNonNullishProperty(message, "limitSharing")) {
878
+ m.protocolMessage = {
879
+ type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
880
+ limitSharing: {
881
+ sharingLimited: message.limitSharing === true,
882
+ trigger: 1,
883
+ limitSharingSettingTimestamp: Date.now(),
884
+ initiatedByMe: true,
885
+ },
886
+ };
887
+
888
+ } else if (hasNonNullishProperty(message, "ptv") && message.ptv) {
889
+ const { videoMessage } = await prepareWAMessageMedia({ video: message.video }, options);
890
+ m.ptvMessage = videoMessage;
891
+
892
+ } else if (hasNonNullishProperty(message, "order")) {
893
+ m.orderMessage = WAProto.Message.OrderMessage.fromObject(message.order);
894
+
895
+ } else if (hasNonNullishProperty(message, "product")) {
896
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
897
+ m.productMessage = WAProto.Message.ProductMessage.fromObject({
898
+ ...message,
899
+ product: { ...message.product, productImage: imageMessage },
900
+ });
901
+
902
+ } else if (hasNonNullishProperty(message, "album")) {
903
+ const imageMessages = message.album.filter((item) => "image" in item);
904
+ const videoMessages = message.album.filter((item) => "video" in item);
905
+ m.albumMessage = WAProto.Message.AlbumMessage.fromObject({
906
+ expectedImageCount: imageMessages.length,
907
+ expectedVideoCount: videoMessages.length,
908
+ });
909
+
910
+ } else if (hasNonNullishProperty(message, "listReply")) {
911
+ m.listResponseMessage = { ...message.listReply };
912
+
913
+ } else if (hasNonNullishProperty(message, "event")) {
914
+ m.eventMessage = {};
915
+ const startTime = Math.floor(message.event.startDate?.getTime?.() / 1000 ?? unixTimestampSeconds() + 86400);
916
+ if (message.event.call && options.getCallLink) {
917
+ const token = await options.getCallLink(message.event.call, { startTime });
918
+ m.eventMessage.joinLink =
919
+ (message.event.call === "audio" ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
920
+ }
921
+ m.messageContextInfo = {
922
+ messageSecret: message.event.messageSecret || randomBytes(32),
923
+ };
924
+ m.eventMessage.name = message.event.name;
925
+ m.eventMessage.description = message.event.description;
926
+ m.eventMessage.startTime = startTime;
927
+ m.eventMessage.endTime = message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined;
928
+ m.eventMessage.isCanceled = message.event.isCancelled ?? false;
929
+ m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
930
+ m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
931
+ m.eventMessage.location = message.event.location;
932
+
933
+ } else if (hasNonNullishProperty(message, "pollResult")) {
934
+ if (!Array.isArray(message.pollResult.values)) {
935
+ throw new Boom("Invalid pollResult values", { statusCode: 400 });
936
+ }
937
+ m.pollResultSnapshotMessage = {
938
+ name: message.pollResult.name,
939
+ pollVotes: message.pollResult.values.map(([optionName, optionVoteCount]) => ({
940
+ optionName,
941
+ optionVoteCount,
942
+ })),
943
+ };
944
+
945
+ } else if (hasNonNullishProperty(message, "poll")) {
946
+ message.poll.selectableCount ??= 0;
947
+ message.poll.toAnnouncementGroup ??= false;
948
+ if (!Array.isArray(message.poll.values)) {
949
+ throw new Boom("Invalid poll values", { statusCode: 400 });
950
+ }
951
+ if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
952
+ throw new Boom(
953
+ `poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`,
954
+ { statusCode: 400 }
955
+ );
956
+ }
957
+ m.messageContextInfo = {
958
+ messageSecret: message.poll.messageSecret || randomBytes(32),
959
+ };
960
+ const pollCreationMessage = {
961
+ name: message.poll.name,
962
+ selectableOptionsCount: message.poll.selectableCount,
963
+ options: message.poll.values.map((optionName) => ({ optionName })),
964
+ };
965
+ if (message.poll.toAnnouncementGroup) {
966
+ m.pollCreationMessageV2 = pollCreationMessage;
967
+ } else {
968
+ if (message.poll.selectableCount === 1) {
969
+ m.pollCreationMessageV3 = pollCreationMessage;
970
+ } else {
971
+ m.pollCreationMessage = pollCreationMessage;
972
+ }
973
+ }
974
+
975
+ } else if (hasNonNullishProperty(message, "nativeFlowMessage")) {
976
+ const nf = message.nativeFlowMessage;
977
+ const raw = nf.buttons ?? [];
978
+ const buttons = raw.map(btn => {
979
+ if (btn.name && btn.buttonParamsJson !== undefined) return btn;
980
+ const type = btn.type ?? 'reply';
981
+ const text = btn.text ?? btn.display_text ?? '';
982
+ const id = btn.id ?? '';
983
+ if (type === 'reply') return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
984
+ if (type === 'url') return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: text, url: btn.url ?? '', webview_interaction: btn.webview ?? false }) };
985
+ if (type === 'call') return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
986
+ if (type === 'copy') return { name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: text, copy_code: btn.copy_code ?? btn.code ?? '' }) };
987
+ if (type === 'location') return { name: 'send_location', buttonParamsJson: JSON.stringify({}) };
988
+ if (type === 'address') return { name: 'address_message', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
989
+ if (type === 'reminder') return { name: 'cta_reminder', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
990
+ if (type === 'cancel_reminder') return { name: 'cta_cancel_reminder', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
991
+ return { name: type, buttonParamsJson: JSON.stringify(btn.params ?? {}) };
992
+ });
993
+ let header = { title: nf.title ?? '', subtitle: nf.subtitle ?? '', hasMediaAttachment: false };
994
+ if (nf.image || nf.video || nf.document) {
995
+ const mediaKey = nf.image ? 'image' : nf.video ? 'video' : 'document';
996
+ const mediaVal = nf[mediaKey];
997
+ try {
998
+ const uploaded = await prepareWAMessageMedia({ [mediaKey]: typeof mediaVal === 'string' ? { url: mediaVal } : mediaVal }, options);
999
+ Object.assign(header, uploaded);
1000
+ header.hasMediaAttachment = true;
1001
+ } catch (_) {
1002
+ header[mediaKey] = typeof mediaVal === 'string' ? { url: mediaVal } : mediaVal;
1003
+ header.hasMediaAttachment = true;
1004
+ }
1005
+ }
1006
+ m = {
1007
+ interactiveMessage: {
1008
+ header,
1009
+ body: { text: nf.body ?? '' },
1010
+ footer: { text: nf.footer ?? '' },
1011
+ contextInfo: nf.contextInfo ?? {},
1012
+ nativeFlowMessage: { messageParamsJson: JSON.stringify(nf.params ?? {}), buttons },
1013
+ }
1014
+ };
1015
+
1016
+ } else if (hasNonNullishProperty(message, "buttonReply")) {
1017
+ switch (message.type) {
1018
+ case "list":
1019
+ m.listResponseMessage = {
1020
+ title: message.buttonReply.title,
1021
+ description: message.buttonReply.description,
1022
+ singleSelectReply: { selectedRowId: message.buttonReply.rowId },
1023
+ lisType: proto.Message.ListResponseMessage.ListType.SINGLE_SELECT,
1024
+ };
1025
+ break;
1026
+ case "template":
1027
+ m.templateButtonReplyMessage = {
1028
+ selectedDisplayText: message.buttonReply.displayText,
1029
+ selectedId: message.buttonReply.id,
1030
+ selectedIndex: message.buttonReply.index,
1031
+ };
1032
+ break;
1033
+ case "plain":
1034
+ m.buttonsResponseMessage = {
1035
+ selectedButtonId: message.buttonReply.id,
1036
+ selectedDisplayText: message.buttonReply.displayText,
1037
+ type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT,
1038
+ };
1039
+ break;
1040
+ case "interactive":
1041
+ m.interactiveResponseMessage = {
1042
+ body: {
1043
+ text: message.buttonReply.displayText,
1044
+ format: proto.Message.InteractiveResponseMessage.Body.Format.EXTENSIONS_1,
1045
+ },
1046
+ nativeFlowResponseMessage: {
1047
+ name: message.buttonReply.nativeFlows.name,
1048
+ paramsJson: message.buttonReply.nativeFlows.paramsJson,
1049
+ version: message.buttonReply.nativeFlows.version,
1050
+ },
1051
+ };
1052
+ break;
1053
+ }
1054
+
1055
+ } else if (hasNonNullishProperty(message, "sections")) {
1056
+ m.listMessage = {
1057
+ title: message.title,
1058
+ buttonText: message.buttonText,
1059
+ footerText: message.footer,
1060
+ description: message.text,
1061
+ sections: message.sections,
1062
+ listType: proto.Message.ListMessage.ListType.SINGLE_SELECT,
1063
+ };
1064
+
1065
+ } else if (hasNonNullishProperty(message, "productList")) {
1066
+ const thumbnail = message.thumbnail ? await generateThumbnail(message.thumbnail, "image") : null;
1067
+ m.listMessage = {
1068
+ title: message.title,
1069
+ buttonText: message.buttonText,
1070
+ footerText: message.footer,
1071
+ description: message.text,
1072
+ productListInfo: {
1073
+ productSections: message.productList,
1074
+ headerImage: {
1075
+ productId: message.productList[0].products[0].productId,
1076
+ jpegThumbnail: thumbnail?.thumbnail || null,
1077
+ },
1078
+ businessOwnerJid: message.businessOwnerJid,
1079
+ },
1080
+ listType: proto.Message.ListMessage.ListType.PRODUCT_LIST,
1081
+ };
1082
+
1083
+ } else if (hasNonNullishProperty(message, "buttons")) {
1084
+ const buttonsMessage = {
1085
+ buttons: message.buttons.map((b) => ({
1086
+ ...b,
1087
+ type: proto.Message.ButtonsMessage.Button.Type.RESPONSE,
1088
+ })),
1089
+ };
1090
+ if (hasNonNullishProperty(message, "text")) {
1091
+ buttonsMessage.contentText = message.text;
1092
+ buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.EMPTY;
1093
+ } else {
1094
+ if (hasNonNullishProperty(message, "caption")) {
1095
+ buttonsMessage.contentText = message.caption;
1096
+ }
1097
+ const type = Object.keys(m)[0].replace("Message", "").toUpperCase();
1098
+ buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType[type];
1099
+ Object.assign(buttonsMessage, m);
1100
+ }
1101
+ if (hasNonNullishProperty(message, "title")) {
1102
+ buttonsMessage.text = message.title;
1103
+ buttonsMessage.headerType = proto.Message.ButtonsMessage.HeaderType.TEXT;
1104
+ }
1105
+ if (hasNonNullishProperty(message, "footer")) {
1106
+ buttonsMessage.footerText = message.footer;
1107
+ }
1108
+ m = { buttonsMessage };
1109
+
1110
+ } else if (hasNonNullishProperty(message, "templateButtons")) {
1111
+ const hydratedTemplate = { hydratedButtons: message.templateButtons };
1112
+ if (hasNonNullishProperty(message, "text")) {
1113
+ hydratedTemplate.hydratedContentText = message.text;
1114
+ } else {
1115
+ if (hasNonNullishProperty(message, "caption")) {
1116
+ hydratedTemplate.hydratedContentText = message.caption;
1117
+ }
1118
+ }
1119
+ if (hasNonNullishProperty(message, "footer")) {
1120
+ hydratedTemplate.hydratedFooterText = message.footer;
1121
+ }
1122
+ m = { templateMessage: { hydratedTemplate } };
1123
+
1124
+ } else if (hasNonNullishProperty(message, "interactiveButtons")) {
1125
+ const interactiveMessage = {
1126
+ nativeFlowMessage: { buttons: message.interactiveButtons },
1127
+ };
1128
+ if (hasNonNullishProperty(message, "text")) {
1129
+ interactiveMessage.body = { text: message.text };
1130
+ }
1131
+ if (hasNonNullishProperty(message, "title")) {
1132
+ interactiveMessage.header = {
1133
+ title: message.title,
1134
+ subtitle: null,
1135
+ hasMediaAttachment: false,
1136
+ };
1137
+ if (hasNonNullishProperty(message, "subtitle")) {
1138
+ interactiveMessage.header.subtitle = message.subtitle;
1139
+ }
1140
+ } else if (hasNonNullishProperty(message, "caption")) {
1141
+ interactiveMessage.body = { text: message.caption };
1142
+ interactiveMessage.header = {
1143
+ title: null,
1144
+ subtitle: null,
1145
+ hasMediaAttachment: false,
1146
+ ...Object.assign(interactiveMessage, m),
1147
+ };
1148
+ if (hasNonNullishProperty(message, "title")) interactiveMessage.header.title = message.title;
1149
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1150
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) interactiveMessage.header.hasMediaAttachment = Boolean(message.hasMediaAttachment);
1151
+ }
1152
+ if (hasNonNullishProperty(message, "footer")) {
1153
+ interactiveMessage.footer = { text: message.footer };
1154
+ }
1155
+ m = { interactiveMessage };
1156
+
1157
+ } else if (hasNonNullishProperty(message, "shop")) {
1158
+ const interactiveMessage = {
1159
+ shopStorefrontMessage: { surface: message.shop.surface, id: message.shop.id },
1160
+ };
1161
+ if (hasNonNullishProperty(message, "text")) interactiveMessage.body = { text: message.text };
1162
+ if (hasNonNullishProperty(message, "title")) {
1163
+ interactiveMessage.header = { title: message.title, subtitle: null, hasMediaAttachment: false };
1164
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1165
+ } else if (hasNonNullishProperty(message, "caption")) {
1166
+ interactiveMessage.body = { text: message.caption };
1167
+ interactiveMessage.header = { title: null, subtitle: null, hasMediaAttachment: false, ...Object.assign(interactiveMessage, m) };
1168
+ if (hasNonNullishProperty(message, "title")) interactiveMessage.header.title = message.title;
1169
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1170
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) interactiveMessage.header.hasMediaAttachment = Boolean(message.hasMediaAttachment);
1171
+ }
1172
+ if (hasNonNullishProperty(message, "footer")) interactiveMessage.footer = { text: message.footer };
1173
+ m = { interactiveMessage };
1174
+
1175
+ } else if (hasNonNullishProperty(message, "collection")) {
1176
+ const interactiveMessage = {
1177
+ collectionMessage: {
1178
+ bizJid: message.collection.bizJid,
1179
+ id: message.collection.id,
1180
+ messageVersion: message?.collection?.version,
1181
+ },
1182
+ };
1183
+ if (hasNonNullishProperty(message, "text")) interactiveMessage.body = { text: message.text };
1184
+ if (hasNonNullishProperty(message, "title")) {
1185
+ interactiveMessage.header = { title: message.title, subtitle: null, hasMediaAttachment: false };
1186
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1187
+ } else if (hasNonNullishProperty(message, "caption")) {
1188
+ interactiveMessage.body = { text: message.caption };
1189
+ interactiveMessage.header = { title: null, subtitle: null, hasMediaAttachment: false, ...Object.assign(interactiveMessage, m) };
1190
+ if (hasNonNullishProperty(message, "title")) interactiveMessage.header.title = message.title;
1191
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1192
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) interactiveMessage.header.hasMediaAttachment = Boolean(message.hasMediaAttachment);
1193
+ }
1194
+ if (hasNonNullishProperty(message, "footer")) interactiveMessage.footer = { text: message.footer };
1195
+ m = { interactiveMessage };
1196
+
1197
+ } else if (hasNonNullishProperty(message, "cards")) {
1198
+ const slides = await Promise.all(
1199
+ message.cards.map(async (slide) => {
1200
+ const { image, video, product, title, body, footer, buttons } = slide;
1201
+ let header;
1202
+ if (product) {
1203
+ const { imageMessage } = await prepareWAMessageMedia({ image: product.productImage, ...options }, options);
1204
+ header = { productMessage: { product: { ...product, productImage: imageMessage }, ...slide } };
1205
+ } else if (image) {
1206
+ header = await prepareWAMessageMedia({ image, ...options }, options);
1207
+ } else if (video) {
1208
+ header = await prepareWAMessageMedia({ video, ...options }, options);
1209
+ }
1210
+ return {
1211
+ header: { title, hasMediaAttachment: true, ...header },
1212
+ body: { text: body },
1213
+ footer: { text: footer },
1214
+ nativeFlowMessage: { buttons },
1215
+ };
1216
+ })
1217
+ );
1218
+ const interactiveMessage = { carouselMessage: { cards: slides } };
1219
+ if (hasNonNullishProperty(message, "text")) interactiveMessage.body = { text: message.text };
1220
+ if (hasNonNullishProperty(message, "title")) {
1221
+ interactiveMessage.header = { title: message.title, subtitle: null, hasMediaAttachment: false };
1222
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1223
+ } else if (hasNonNullishProperty(message, "caption")) {
1224
+ interactiveMessage.body = { text: message.caption };
1225
+ interactiveMessage.header = { title: null, subtitle: null, hasMediaAttachment: false, ...Object.assign(interactiveMessage, m) };
1226
+ if (hasNonNullishProperty(message, "title")) interactiveMessage.header.title = message.title;
1227
+ if (hasNonNullishProperty(message, "subtitle")) interactiveMessage.header.subtitle = message.subtitle;
1228
+ if (hasNonNullishProperty(message, "hasMediaAttachment")) interactiveMessage.header.hasMediaAttachment = Boolean(message.hasMediaAttachment);
1229
+ }
1230
+ if (hasNonNullishProperty(message, "footer")) interactiveMessage.footer = { text: message.footer };
1231
+ m = { interactiveMessage };
1232
+
1233
+ } else if (hasNonNullishProperty(message, "carouselMessage")) {
1234
+ const cm = message.carouselMessage;
1235
+ const cards = await Promise.all((cm.cards ?? []).map(async card => {
1236
+ let header = { title: card.title ?? '', subtitle: card.subtitle ?? '', hasMediaAttachment: false };
1237
+ if (card.image || card.video) {
1238
+ const mediaKey = card.image ? 'image' : 'video';
1239
+ const mediaVal = card[mediaKey];
1240
+ try {
1241
+ const uploaded = await prepareWAMessageMedia({ [mediaKey]: typeof mediaVal === 'string' ? { url: mediaVal } : mediaVal }, options);
1242
+ Object.assign(header, uploaded);
1243
+ header.hasMediaAttachment = true;
1244
+ } catch (_) {
1245
+ header[mediaKey] = typeof mediaVal === 'string' ? { url: mediaVal } : mediaVal;
1246
+ header.hasMediaAttachment = true;
1247
+ }
1248
+ }
1249
+ const buttons = (card.buttons ?? []).map(btn => {
1250
+ if (btn.name && btn.buttonParamsJson !== undefined) return btn;
1251
+ const type = btn.type ?? 'reply';
1252
+ const text = btn.text ?? btn.display_text ?? '';
1253
+ const id = btn.id ?? '';
1254
+ if (type === 'reply') return { name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
1255
+ if (type === 'url') return { name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: text, url: btn.url ?? '', webview_interaction: btn.webview ?? false }) };
1256
+ if (type === 'call') return { name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: text, id }) };
1257
+ if (type === 'copy') return { name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: text, copy_code: btn.copy_code ?? btn.code ?? '' }) };
1258
+ return { name: type, buttonParamsJson: JSON.stringify(btn.params ?? {}) };
1259
+ });
1260
+ return {
1261
+ body: { text: card.body ?? '' },
1262
+ footer: { text: card.footer ?? '' },
1263
+ header,
1264
+ nativeFlowMessage: { messageParamsJson: JSON.stringify(card.params ?? {}), buttons },
1265
+ };
1266
+ }));
1267
+ m = {
1268
+ interactiveMessage: {
1269
+ header: { hasMediaAttachment: false },
1270
+ body: { text: cm.body ?? '' },
1271
+ footer: { text: cm.footer ?? '' },
1272
+ contextInfo: cm.contextInfo ?? {},
1273
+ carouselMessage: { cards },
1274
+ }
1275
+ };
1276
+
1277
+ } else if (hasNonNullishProperty(message, "richMessage")) {
1278
+ const { randomUUID } = await import('crypto');
1279
+ const rich = message.richMessage;
1280
+ const submessages = [];
1281
+ const sections = [];
1282
+ const richResponseSources = [];
1283
+
1284
+ const tokenizer = (code, lang = 'javascript') => {
1285
+ const normalizedLang = lang.toLowerCase();
1286
+
1287
+ if (normalizedLang === 'plaintext' || normalizedLang === 'txt' || normalizedLang === 'text') {
1288
+ const raw = [{ codeContent: code, highlightType: 0 }];
1289
+ return {
1290
+ codeBlock: raw,
1291
+ unified_codeBlock: raw.map(t => ({ content: t.codeContent, type: 'DEFAULT' }))
1292
+ };
1293
+ }
1294
+
1295
+ const keywordsMap = {
1296
+ javascript: new Set(['break','case','catch','continue','debugger','delete','do','else','finally','for','function','if','in','instanceof','new','return','switch','this','throw','try','typeof','var','void','while','with','true','false','null','undefined','class','const','let','super','extends','export','import','yield','static','constructor','async','await','get','set']),
1297
+ typescript: new Set(['break','case','catch','continue','debugger','delete','do','else','finally','for','function','if','in','instanceof','new','return','switch','this','throw','try','typeof','var','void','while','with','true','false','null','undefined','class','const','let','super','extends','export','import','yield','static','constructor','async','await','get','set','type','interface','enum','namespace','declare','abstract','implements','as','from','of','readonly','keyof','infer','never','unknown','any']),
1298
+ python: new Set(['False','None','True','and','as','assert','async','await','break','class','continue','def','del','elif','else','except','finally','for','from','global','if','import','in','is','lambda','nonlocal','not','or','pass','raise','return','try','while','with','yield']),
1299
+ java: new Set(['abstract','assert','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','null','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','true','false']),
1300
+ go: new Set(['break','case','chan','const','continue','default','defer','else','fallthrough','for','func','go','goto','if','import','interface','map','package','range','return','select','struct','switch','type','var','true','false','nil','iota']),
1301
+ c: new Set(['auto','break','case','char','const','continue','default','do','double','else','enum','extern','float','for','goto','if','inline','int','long','register','restrict','return','short','signed','sizeof','static','struct','switch','typedef','union','unsigned','void','volatile','while','NULL','true','false']),
1302
+ php: new Set(['abstract','and','array','as','break','callable','case','catch','class','clone','const','continue','declare','default','die','do','echo','else','elseif','empty','enddeclare','endfor','endforeach','endif','endswitch','endwhile','eval','exit','extends','final','finally','fn','for','foreach','function','global','goto','if','implements','include','include_once','instanceof','insteadof','interface','isset','list','match','namespace','new','null','or','print','private','protected','public','readonly','require','require_once','return','static','switch','throw','trait','true','try','unset','use','var','while','xor','yield','false']),
1303
+ rust: new Set(['as','async','await','break','const','continue','crate','dyn','else','enum','extern','false','fn','for','if','impl','in','let','loop','match','mod','move','mut','pub','ref','return','self','Self','static','struct','super','trait','true','type','union','unsafe','use','where','while']),
1304
+ html: new Set(['DOCTYPE','html','head','body','div','span','p','a','ul','ol','li','table','tr','td','th','thead','tbody','tfoot','form','input','button','select','option','textarea','label','script','style','link','meta','title','h1','h2','h3','h4','h5','h6','img','video','audio','canvas','svg','nav','header','footer','section','article','aside','main']),
1305
+ bash: new Set(['if','then','else','elif','fi','for','in','do','done','while','until','case','esac','function','select','time','coproc','break','continue','return','exit','export','local','declare','readonly','unset','set','shift','source','echo','printf','read','test','true','false']),
1306
+ markdown: new Set([]),
1307
+ };
1308
+
1309
+ const TYPE_MAP = { 0: 'DEFAULT', 1: 'KEYWORD', 2: 'METHOD', 3: 'STR', 4: 'NUMBER', 5: 'COMMENT' };
1310
+ const keywords = keywordsMap[normalizedLang] || new Set();
1311
+ const tokens = [];
1312
+ let i = 0;
1313
+
1314
+ const push = (content, type) => {
1315
+ if (!content) return;
1316
+ const last = tokens[tokens.length - 1];
1317
+ if (last && last.highlightType === type) last.codeContent += content;
1318
+ else tokens.push({ codeContent: content, highlightType: type });
1319
+ };
1320
+
1321
+ const isIdentStart = (c) => /[a-zA-Z_$]/.test(c);
1322
+ const isIdentPart = (c) => {
1323
+ if (normalizedLang === 'css') return /[a-zA-Z0-9_$-]/.test(c);
1324
+ if (normalizedLang === 'html') return /[a-zA-Z0-9_$:-]/.test(c);
1325
+ return /[a-zA-Z0-9_$]/.test(c);
1326
+ };
1327
+
1328
+ while (i < code.length) {
1329
+ const c = code[i];
1330
+ if (/\s/.test(c)) {
1331
+ let s = i;
1332
+ while (i < code.length && /\s/.test(code[i])) i++;
1333
+ push(code.slice(s, i), 0);
1334
+ continue;
1335
+ }
1336
+ if (c === '/' && code[i + 1] === '/') {
1337
+ let s = i; i += 2;
1338
+ while (i < code.length && code[i] !== '\n') i++;
1339
+ push(code.slice(s, i), 5);
1340
+ continue;
1341
+ }
1342
+ if (c === '#' && (normalizedLang === 'python' || normalizedLang === 'bash')) {
1343
+ let s = i++;
1344
+ while (i < code.length && code[i] !== '\n') i++;
1345
+ push(code.slice(s, i), 5);
1346
+ continue;
1347
+ }
1348
+ if (c === '/' && code[i + 1] === '*') {
1349
+ let s = i; i += 2;
1350
+ while (i < code.length - 1 && !(code[i] === '*' && code[i + 1] === '/')) i++;
1351
+ i += 2;
1352
+ push(code.slice(s, i), 5);
1353
+ continue;
1354
+ }
1355
+ if (c === '"' || c === "'" || c === '`') {
1356
+ let s = i; const q = c; i++;
1357
+ while (i < code.length) {
1358
+ if (code[i] === '\\' && i + 1 < code.length) i += 2;
1359
+ else if (code[i] === q) { i++; break; }
1360
+ else i++;
1361
+ }
1362
+ push(code.slice(s, i), 3);
1363
+ continue;
1364
+ }
1365
+ if (/[0-9]/.test(c)) {
1366
+ let s = i;
1367
+ while (i < code.length && /[0-9.xXa-fA-F_]/.test(code[i])) i++;
1368
+ push(code.slice(s, i), 4);
1369
+ continue;
1370
+ }
1371
+ if (isIdentStart(c)) {
1372
+ let s = i;
1373
+ while (i < code.length && isIdentPart(code[i])) i++;
1374
+ const word = code.slice(s, i);
1375
+ let type = 0;
1376
+ if (keywords.has(word)) {
1377
+ type = 1;
1378
+ } else {
1379
+ if (normalizedLang === 'html' && s > 0 && (code[s - 1] === '<' || (code[s - 1] === '/' && code[s - 2] === '<'))) {
1380
+ type = 1;
1381
+ } else {
1382
+ let j = i;
1383
+ while (j < code.length && /\s/.test(code[j])) j++;
1384
+ if (normalizedLang === 'css' && code[j] === ':') {
1385
+ type = 1;
1386
+ } else if (code[j] === '(') {
1387
+ type = 2;
1388
+ }
1389
+ }
1390
+ }
1391
+ push(word, type);
1392
+ continue;
1393
+ }
1394
+ push(c, 0); i++;
1395
+ }
1396
+
1397
+ return {
1398
+ codeBlock: tokens,
1399
+ unified_codeBlock: tokens.map(t => ({ content: t.codeContent, type: TYPE_MAP[t.highlightType] }))
1400
+ };
1401
+ };
1402
+
1403
+ const toTableMetadata = (arr, { hyperlink = false, citation = false, latex = false } = {}) => {
1404
+ if (!Array.isArray(arr) || !arr.every(row => Array.isArray(row) && row.every(cell => typeof cell === 'string'))) {
1405
+ throw new TypeError('Table must be a nested array of strings');
1406
+ }
1407
+ const [header, ...rows] = arr;
1408
+ const maxLen = Math.max(header.length, ...rows.map(r => r.length));
1409
+ const normalize = (r) => [...r, ...Array(maxLen - r.length).fill('')];
1410
+ const shouldExtractIE = hyperlink || citation || latex;
1411
+ let hasEntities = false;
1412
+ const processRow = (cells, isHeader) => {
1413
+ const processed = normalize(cells).map(cell => {
1414
+ if (!shouldExtractIE) return { text: cell };
1415
+ const { text, inline_entities } = extractIE(cell, { hyperlink, citation, latex });
1416
+ if (inline_entities.length) hasEntities = true;
1417
+ return { text, inline_entities };
1418
+ });
1419
+ return { is_header: isHeader, cells: processed.map(c => c.text), _meta: processed };
1420
+ };
1421
+ const unified_rows = [processRow(header, true), ...rows.map(r => processRow(r, false))];
1422
+ const result = {
1423
+ title: '',
1424
+ rows: unified_rows.map(r => ({ items: r.cells, ...(r.is_header ? { isHeading: true } : {}) })),
1425
+ unified_rows: unified_rows.map(r => ({ is_header: r.is_header, cells: r.cells }))
1426
+ };
1427
+ if (hasEntities) {
1428
+ result.markdown_cells = unified_rows.map(r => r._meta.map(c => ({ text: c.text, inline_entities: c.inline_entities || [] })));
1429
+ }
1430
+ return result;
1431
+ };
1432
+
1433
+ const newLayout = (name, data, extra = {}) => ({
1434
+ view_model: {
1435
+ [Array.isArray(data) ? 'primitives' : 'primitive']: data,
1436
+ __typename: `GenAI${name}LayoutViewModel`,
1437
+ ...extra
1438
+ }
1439
+ });
1440
+
1441
+ if (rich.product || rich.products) {
1442
+ const raw = rich.product || rich.products;
1443
+ const items = Array.isArray(raw) ? raw : [raw];
1444
+ submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_PRODUCT ]' });
1445
+ const mapped = await Promise.all(items.map(async item => {
1446
+ const imageUrl = await Toolkit.resolveMedia(
1447
+ typeof item.image === 'string' ? item.image : item.image?.url ?? item.image_url ?? '', 'url'
1448
+ );
1449
+ const iconUrl = await Toolkit.resolveMedia(item.icon_url ?? item.icon ?? imageUrl ?? '', 'url');
1450
+ return {
1451
+ title: item.title ?? '',
1452
+ brand: item.brand ?? '',
1453
+ price: item.price ?? '',
1454
+ sale_price: item.sale_price ?? '',
1455
+ product_url: item.product_url ?? item.url ?? '',
1456
+ image: { url: imageUrl },
1457
+ additional_images: [{ url: iconUrl }],
1458
+ __typename: 'GenAIProductItemCardPrimitive'
1459
+ };
1460
+ }));
1461
+ sections.push(newLayout(items.length > 1 ? 'HScroll' : 'Single', items.length > 1 ? mapped : mapped[0]));
1462
+ }
1463
+
1464
+ if (rich.image || rich.images) {
1465
+ const raw = rich.image || rich.images;
1466
+ const inputs = Array.isArray(raw) ? raw : [raw];
1467
+ const urls = await Promise.all(inputs.map(item => Toolkit.resolveMedia(item, 'url')));
1468
+ submessages.push({
1469
+ messageType: 1,
1470
+ gridImageMetadata: {
1471
+ gridImageUrl: { imagePreviewUrl: urls[0] },
1472
+ imageUrls: urls.map(url => ({ imagePreviewUrl: url, imageHighResUrl: url, sourceUrl: url }))
1473
+ }
1474
+ });
1475
+ urls.forEach(url => sections.push(newLayout('Single', {
1476
+ media: { url, mime_type: 'image/png' },
1477
+ imagine_type: 'IMAGE',
1478
+ status: { status: 'READY' },
1479
+ __typename: 'GenAIImaginePrimitive'
1480
+ })));
1481
+ }
1482
+
1483
+ if (rich.video || rich.videos) {
1484
+ const raw = rich.video || rich.videos;
1485
+ const isObjectVideo = (v) => v && typeof v === 'object' && !Array.isArray(v) && (v.url || v.videoUrl);
1486
+ const rawEntries = Array.isArray(raw) ? raw : [raw];
1487
+ submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_VIDEO - NIXEL ]' });
1488
+ rawEntries.forEach(item => {
1489
+ const isObj = isObjectVideo(item);
1490
+ const rawUrl = isObj ? (item.url ?? item.videoUrl ?? '') : (typeof item === 'string' ? item.split('|')[0] : '');
1491
+ const explicitDuration = isObj && item.duration != null ? item.duration : (typeof item === 'string' && item.includes('|') ? Number(item.split('|')[1]) || null : null);
1492
+ const autoFill = item?.autoFill !== false;
1493
+ const bufPromise = autoFill ? Toolkit.fetchBuffer(rawUrl) : Promise.resolve(Buffer.alloc(0));
1494
+ const file_length = isObj && item.file_length != null ? item.file_length : autoFill ? bufPromise.then(b => b?.length ?? 0) : 0;
1495
+ const duration = explicitDuration != null ? explicitDuration : autoFill ? bufPromise.then(b => Toolkit.getMp4DurationSync(b)) : 0;
1496
+ const thumbnail = isObj && item.thumbnail
1497
+ ? bufPromise.then(() => {
1498
+ const tb = item.thumbnail;
1499
+ const tbBuf = Buffer.isBuffer(tb) ? tb : null;
1500
+ if (tbBuf) return sharp(tbBuf).resize(300, 300, { fit: 'cover' }).png().toBuffer().then(b => b.toString('base64'));
1501
+ return Toolkit.fetchBuffer(tb).then(b => sharp(b).resize(300, 300, { fit: 'cover' }).png().toBuffer()).then(b => b.toString('base64'));
1502
+ })
1503
+ : autoFill
1504
+ ? bufPromise.then(b => Toolkit.getMp4Preview(b, { time: 0, result: 'base64', resize: true, width: 300, height: 300 }))
1505
+ : null;
1506
+ sections.push(newLayout('Single', {
1507
+ media: {
1508
+ url: rawUrl,
1509
+ mime_type: isObj ? (item.mime_type ?? 'video/mp4') : 'video/mp4',
1510
+ file_length,
1511
+ duration,
1512
+ },
1513
+ imagine_type: 'ANIMATE',
1514
+ status: { status: 'READY' },
1515
+ thumbnail: { raw_media: thumbnail },
1516
+ __typename: 'GenAIImaginePrimitive',
1517
+ }));
1518
+ });
1519
+ }
1520
+
1521
+ if (rich.tip !== undefined) {
1522
+ const tipText = String(rich.tip);
1523
+ submessages.push({ messageType: 2, messageText: tipText });
1524
+ sections.push(newLayout('Single', { text: tipText, __typename: 'GenAIMetadataTextPrimitive' }));
1525
+ }
1526
+
1527
+ if (rich.code) {
1528
+ const language = rich.code.language ?? rich.code.lang ?? 'javascript';
1529
+ const code = rich.code.code ?? rich.code.content ?? '';
1530
+ const tokenized = tokenizer(code, language);
1531
+ submessages.push({ messageType: 5, codeMetadata: { codeLanguage: language, codeBlocks: tokenized.codeBlock } });
1532
+ sections.push(newLayout('Single', { language, code_blocks: tokenized.unified_codeBlock, __typename: 'GenAICodeUXPrimitive' }));
1533
+ }
1534
+
1535
+ if (rich.table) {
1536
+ const tableOpts = typeof rich.table === 'object' && !Array.isArray(rich.table) ? rich.table : { data: rich.table };
1537
+ const data = tableOpts.data ?? rich.table;
1538
+ const meta = toTableMetadata(data, {
1539
+ hyperlink: tableOpts.hyperlink ?? false,
1540
+ citation: tableOpts.citation ?? false,
1541
+ latex: tableOpts.latex ?? false,
1542
+ });
1543
+ submessages.push({ messageType: 4, tableMetadata: { title: meta.title, rows: meta.rows } });
1544
+ const primitive = { rows: meta.unified_rows, __typename: 'GenATableUXPrimitive' };
1545
+ if (meta.markdown_cells) primitive.markdown_cells = meta.markdown_cells;
1546
+ sections.push(newLayout('Single', primitive));
1547
+ }
1548
+
1549
+ if (rich.post || rich.posts) {
1550
+ const raw = rich.post || rich.posts;
1551
+ const items = Array.isArray(raw) ? raw : [raw];
1552
+ submessages.push({ messageType: 2, messageText: '[ CANNOT_LOAD_POST - NIXEL ]' });
1553
+ const mapped = await Promise.all(items.map(async p => ({
1554
+ title: p.title ?? '',
1555
+ subtitle: p.subtitle ?? '',
1556
+ username: p.username ?? '',
1557
+ profile_picture_url: await Toolkit.resolveMedia(p.profile_picture_url ?? p.profile_url ?? '', 'url'),
1558
+ is_verified: !!(p.is_verified || p.verified),
1559
+ thumbnail_url: await Toolkit.resolveMedia(p.thumbnail_url ?? p.thumbnail ?? '', 'url'),
1560
+ post_caption: p.post_caption ?? p.caption ?? '',
1561
+ source_app: p.source_app ?? p.source ?? '',
1562
+ post_url: p.post_url ?? p.url ?? '',
1563
+ post_deeplink: p.post_deeplink ?? p.deeplink ?? p.post_url ?? p.url ?? '',
1564
+ footer_label: p.footer_label ?? p.footer ?? '',
1565
+ footer_icon: await Toolkit.resolveMedia(p.footer_icon ?? p.icon ?? p.profile_picture_url ?? p.profile_url ?? '', 'url'),
1566
+ likes_count: p.likes_count ?? p.like ?? 0,
1567
+ comments_count: p.comments_count ?? p.comment ?? 0,
1568
+ shares_count: p.shares_count ?? p.share ?? 0,
1569
+ orientation: p.orientation ?? 'LANDSCAPE',
1570
+ post_type: p.post_type ?? 'PHOTO',
1571
+ is_carousel: !!(p.is_carousel),
1572
+ __typename: 'GenAIPostPrimitive',
1573
+ })));
1574
+ sections.push(newLayout(items.length > 1 ? 'HScroll' : 'Single', items.length > 1 ? mapped : mapped[0]));
1575
+ }
1576
+
1577
+ if (rich.reels || rich.reel) {
1578
+ const raw = rich.reels ?? rich.reel;
1579
+ const items = Array.isArray(raw) ? raw : [raw];
1580
+ submessages.push({
1581
+ messageType: 9,
1582
+ contentItemsMetadata: {
1583
+ contentType: 1,
1584
+ itemsMetadata: items.map(item => ({
1585
+ reelItem: {
1586
+ title: item.username ?? item.title ?? '',
1587
+ profileIconUrl: item._resolvedAvatar ?? item.profileIconUrl ?? '',
1588
+ thumbnailUrl: item._resolvedThumb ?? item.thumbnailUrl ?? '',
1589
+ videoUrl: item._resolvedVideo ?? item.videoUrl ?? '',
1590
+ }
1591
+ }))
1592
+ }
1593
+ });
1594
+ items.forEach((item, idx) => richResponseSources.push({
1595
+ provider: 'UNKNOWN',
1596
+ thumbnailCDNURL: item.thumbnailUrl ?? '',
1597
+ sourceProviderURL: item.videoUrl ?? '',
1598
+ sourceQuery: '',
1599
+ faviconCDNURL: item.profileIconUrl ?? '',
1600
+ citationNumber: idx + 1,
1601
+ sourceTitle: item.username ?? item.title ?? '',
1602
+ }));
1603
+ sections.push(newLayout('HScroll', items.map(item => ({
1604
+ reels_url: item.videoUrl ?? '',
1605
+ thumbnail_url: item.thumbnailUrl ?? '',
1606
+ creator: item.username ?? item.title ?? '',
1607
+ avatar_url: item.profileIconUrl ?? '',
1608
+ reels_title: item.reels_title ?? item.title ?? '',
1609
+ likes_count: item.likes_count ?? item.like ?? 0,
1610
+ shares_count: item.shares_count ?? item.share ?? 0,
1611
+ view_count: item.view_count ?? item.view ?? 0,
1612
+ reel_source: item.reel_source ?? item.source ?? 'IG',
1613
+ is_verified: !!(item.is_verified || item.verified),
1614
+ __typename: 'GenAIReelPrimitive',
1615
+ }))));
1616
+ }
1617
+
1618
+ if (rich.sources || rich.source) {
1619
+ const raw = rich.sources ?? rich.source;
1620
+ const sourceArr = Array.isArray(raw) ? raw : [raw];
1621
+ const resolved = await Promise.all(sourceArr.map(async s => {
1622
+ if (typeof s === 'object' && !Array.isArray(s)) {
1623
+ return {
1624
+ ...s,
1625
+ favicon: s.favicon ? { ...s.favicon, url: await Toolkit.resolveMedia(s.favicon.url, 'url') } : s.favicon
1626
+ };
1627
+ }
1628
+ return {
1629
+ source_type: 'THIRD_PARTY',
1630
+ source_display_name: s[2] ?? '',
1631
+ source_subtitle: 'AI',
1632
+ source_url: s[1] ?? '',
1633
+ favicon: { url: await Toolkit.resolveMedia(s[0] ?? '', 'url'), mime_type: 'image/jpeg', width: 16, height: 16 }
1634
+ };
1635
+ }));
1636
+ sections.push(newLayout('Single', { sources: resolved, __typename: 'GenAISearchResultPrimitive' }));
1637
+ }
1638
+
1639
+ if (rich.suggestions || rich.suggest) {
1640
+ const raw = rich.suggestions ?? rich.suggest;
1641
+ const opts = typeof raw === 'object' && !Array.isArray(raw) && raw !== null ? raw : { items: raw };
1642
+ const items = Array.isArray(opts.items ?? raw) ? (opts.items ?? raw) : [opts.items ?? raw];
1643
+ const suggest = items.map(text => ({
1644
+ prompt_text: String(text),
1645
+ prompt_type: 'SUGGESTED_PROMPT',
1646
+ __typename: 'GenAIFollowUpSuggestionPillPrimitive'
1647
+ }));
1648
+ let layout;
1649
+ if (opts.layout) {
1650
+ layout = opts.layout;
1651
+ } else if (suggest.length === 1) {
1652
+ layout = 'Single';
1653
+ } else if (opts.scroll === false) {
1654
+ layout = 'ActionRow';
1655
+ } else {
1656
+ layout = 'HScroll';
1657
+ }
1658
+ sections.push(newLayout(layout, suggest.length === 1 ? suggest[0] : suggest));
1659
+ }
1660
+
1661
+ if (rich.footer) {
1662
+ sections.push(newLayout('Single', { text: rich.footer, __typename: 'GenAIMetadataTextPrimitive' }));
1663
+ }
1664
+
1665
+ if (rich.text) {
1666
+ const input = typeof rich.text === 'string' ? rich.text : String(rich.text);
1667
+ const { text: extractedText, inline_entities } = extractIE(input);
1668
+ submessages.push({ messageType: 2, messageText: extractedText });
1669
+ sections.push(newLayout('Single', {
1670
+ text: extractedText,
1671
+ ...(inline_entities.length && { inline_entities }),
1672
+ __typename: 'GenAIMarkdownTextUXPrimitive'
1673
+ }));
1674
+ }
1675
+
1676
+ const [resolvedSections, resolvedSubmessages] = await Promise.all([
1677
+ Toolkit.waitAllPromises(sections),
1678
+ Toolkit.waitAllPromises(submessages)
1679
+ ]);
1680
+
1681
+ m = {
1682
+ messageContextInfo: {
1683
+ deviceListMetadata: {},
1684
+ deviceListMetadataVersion: 2,
1685
+ botMetadata: {
1686
+ messageDisclaimerText: rich.title ?? '',
1687
+ richResponseSourcesMetadata: { sources: richResponseSources }
1688
+ }
1689
+ },
1690
+ botForwardedMessage: {
1691
+ message: {
1692
+ richResponseMessage: {
1693
+ messageType: 1,
1694
+ submessages: resolvedSubmessages,
1695
+ unifiedResponse: {
1696
+ data: Buffer.from(JSON.stringify({ response_id: randomUUID(), sections: resolvedSections })).toString('base64')
1697
+ },
1698
+ contextInfo: {
1699
+ forwardingScore: 1,
1700
+ isForwarded: true,
1701
+ forwardedAiBotMessageInfo: { botJid: '0@bot' },
1702
+ forwardOrigin: 4,
1703
+ ...(rich.contextInfo ?? {})
1704
+ }
1705
+ }
1706
+ }
1707
+ }
1708
+ };
1709
+
1710
+ } else if (hasNonNullishProperty(message, "stickerPackMessage")) {
1711
+ const sp = message.stickerPackMessage;
1712
+
1713
+ const encryptStickerPack = async (plainBuffer) => {
1714
+ const { createHmac, createCipheriv, randomBytes: rb, hkdf: hkdfCb } = await import('crypto');
1715
+ const { promisify } = await import('util');
1716
+ const hkdf = promisify(hkdfCb);
1717
+ const mediaKey = rb(32);
1718
+ const expanded = await hkdf('sha256', mediaKey, Buffer.alloc(0), Buffer.from('WhatsApp Sticker Pack Keys'), 112);
1719
+ const keyExpanded = Buffer.from(expanded);
1720
+ const iv = keyExpanded.slice(0, 16);
1721
+ const key = keyExpanded.slice(16, 48);
1722
+ const macKey = keyExpanded.slice(48, 80);
1723
+ const padLen = 16 - (plainBuffer.length % 16);
1724
+ const padded = Buffer.concat([plainBuffer, Buffer.alloc(padLen, padLen)]);
1725
+ const cipher = createCipheriv('aes-256-cbc', key, iv);
1726
+ cipher.setAutoPadding(false);
1727
+ const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
1728
+ const mac = createHmac('sha256', macKey).update(Buffer.concat([iv, encrypted])).digest().slice(0, 10);
1729
+ const encFull = Buffer.concat([encrypted, mac]);
1730
+ const { createHash } = await import('crypto');
1731
+ const fileSha256 = createHash('sha256').update(plainBuffer).digest();
1732
+ const fileEncSha256 = createHash('sha256').update(encFull).digest();
1733
+ return { encFull, mediaKey, fileSha256, fileEncSha256 };
1734
+ };
1735
+
1736
+ const toAnimatedWebp = async (buffer) => {
1737
+ const { join } = await import('path');
1738
+ const { tmpdir } = await import('os');
1739
+ const { writeFile: wf, readFile: rf, unlink: ul } = await import('fs/promises');
1740
+ try {
1741
+ const result = await sharp(buffer, { animated: true })
1742
+ .resize(512, 512, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
1743
+ .webp({ quality: 60, loop: 0 })
1744
+ .toBuffer();
1745
+ if (result && result.length > 0) return result;
1746
+ } catch {}
1747
+ const ext = (buffer[4] === 0x66 && buffer[5] === 0x74 && buffer[6] === 0x79 && buffer[7] === 0x70) ? 'mp4' : 'mp4';
1748
+ const ts = Date.now();
1749
+ const tmpIn = join(tmpdir(), `stc_in_${ts}.${ext}`);
1750
+ const tmpOut = join(tmpdir(), `stc_out_${ts}.webp`);
1751
+ await wf(tmpIn, buffer);
1752
+ try {
1753
+ await new Promise((resolve, reject) => {
1754
+ ffmpeg(tmpIn)
1755
+ .outputOptions([
1756
+ '-vcodec', 'libwebp',
1757
+ '-vf', 'fps=15,scale=512:512:force_original_aspect_ratio=decrease,pad=512:512:(ow-iw)/2:(oh-ih)/2:color=black@0,format=rgba',
1758
+ '-loop', '0',
1759
+ '-compression_level', '6',
1760
+ '-quality', '60',
1761
+ '-preset', 'icon',
1762
+ '-an',
1763
+ '-t', '5',
1764
+ ])
1765
+ .toFormat('webp')
1766
+ .on('error', reject)
1767
+ .on('end', resolve)
1768
+ .save(tmpOut);
1769
+ });
1770
+ return await rf(tmpOut);
1771
+ } finally {
1772
+ await ul(tmpIn).catch(() => {});
1773
+ await ul(tmpOut).catch(() => {});
1774
+ }
1775
+ };
1776
+
1777
+ const toStaticWebp = async (buffer) => {
1778
+ return sharp(buffer)
1779
+ .resize(512, 512, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
1780
+ .webp({ quality: 100 })
1781
+ .toBuffer();
1782
+ };
1783
+
1784
+ if (sp.mediaKey && sp.directPath) {
1785
+ m = { stickerPackMessage: sp };
1786
+ } else {
1787
+ const { default: AdmZip } = await import('adm-zip');
1788
+ const { join } = await import('path');
1789
+ const { tmpdir } = await import('os');
1790
+ const { writeFile: wf, unlink: ul } = await import('fs/promises');
1791
+ const { randomUUID, createHash } = await import('crypto');
1792
+
1793
+ const packId = sp.stickerPackId ?? randomUUID();
1794
+ const zip = new AdmZip();
1795
+ const stickers = [];
1796
+ const rawStickers = sp.stickers ?? [];
1797
+
1798
+ for (const stiker of rawStickers) {
1799
+ let buffer;
1800
+ if (Buffer.isBuffer(stiker)) {
1801
+ buffer = stiker;
1802
+ } else if (stiker.buffer) {
1803
+ buffer = stiker.buffer;
1804
+ } else if (stiker.imageUrl || stiker.url) {
1805
+ const res = await fetch(stiker.imageUrl ?? stiker.url);
1806
+ buffer = Buffer.from(await res.arrayBuffer());
1807
+ } else continue;
1808
+
1809
+ const isAnimated = stiker.isAnimated ?? false;
1810
+ let webp;
1811
+ try {
1812
+ webp = isAnimated ? await toAnimatedWebp(buffer) : await toStaticWebp(buffer);
1813
+ } catch {
1814
+ webp = await toStaticWebp(buffer);
1815
+ }
1816
+
1817
+ const fileName = `sticker_${createHash('md5').update(webp).digest('hex').slice(0, 12)}.webp`;
1818
+ zip.addFile(fileName, webp);
1819
+ stickers.push({
1820
+ fileName: String(fileName),
1821
+ isAnimated: Boolean(isAnimated),
1822
+ isLottie: false,
1823
+ mimetype: 'image/webp',
1824
+ emojis: stiker.emojis ?? ['✨'],
1825
+ });
1826
+ }
1827
+
1828
+ let trayBuffer;
1829
+ if (sp.trayIcon) {
1830
+ trayBuffer = Buffer.isBuffer(sp.trayIcon)
1831
+ ? sp.trayIcon
1832
+ : Buffer.from(await (await fetch(sp.trayIcon)).arrayBuffer());
1833
+ } else if (stickers.length > 0) {
1834
+ const firstEntry = zip.getEntries()[0];
1835
+ trayBuffer = firstEntry ? firstEntry.getData() : Buffer.alloc(0);
1836
+ }
1837
+ if (trayBuffer && trayBuffer.length) {
1838
+ trayBuffer = await sharp(trayBuffer).resize(96, 96, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } }).png().toBuffer();
1839
+ }
1840
+
1841
+ const trayName = `${packId}.png`;
1842
+ if (trayBuffer?.length) zip.addFile(trayName, trayBuffer);
1843
+
1844
+ const zipBuffer = zip.toBuffer();
1845
+ const { encFull, mediaKey, fileSha256, fileEncSha256 } = await encryptStickerPack(zipBuffer);
1846
+
1847
+ const tmpPath = join(tmpdir(), `stickerpack_${Date.now()}.zip`);
1848
+ await wf(tmpPath, encFull);
1849
+
1850
+ const upload = await options.upload(tmpPath, {
1851
+ mediaType: 'sticker',
1852
+ fileEncSha256B64: fileEncSha256.toString('base64'),
1853
+ }).finally(() => ul(tmpPath).catch(() => {}));
1854
+
1855
+ m = {
1856
+ stickerPackMessage: {
1857
+ stickerPackId: String(packId),
1858
+ name: String(sp.name ?? 'Sticker Pack'),
1859
+ publisher: String(sp.publisher ?? sp.author ?? 'Unknown'),
1860
+ stickers,
1861
+ mediaKey,
1862
+ directPath: String(upload?.directPath ?? ''),
1863
+ fileSha256,
1864
+ fileEncSha256,
1865
+ fileLength: zipBuffer.length,
1866
+ mediaKeyTimestamp: unixTimestampSeconds(),
1867
+ trayIconFileName: String(trayName),
1868
+ thumbnailDirectPath: String(upload?.directPath ?? ''),
1869
+ thumbnailSha256: fileSha256,
1870
+ thumbnailEncSha256: fileEncSha256,
1871
+ thumbnailHeight: 96,
1872
+ thumbnailWidth: 96,
1873
+ stickerPackSize: zipBuffer.length,
1874
+ stickerPackOrigin: sp.stickerPackOrigin ?? 1,
1875
+ }
1876
+ };
1877
+ }
1878
+
1879
+ } else {
1880
+ m = await prepareWAMessageMedia(message, options);
1881
+ }
1882
+
1883
+ if (hasOptionalProperty(message, "ephemeral")) {
1884
+ m = { ephemeralMessage: { message: m } };
1885
+ }
1886
+
1887
+ if ("viewOnce" in message && !!message.viewOnce) {
1888
+ m = { viewOnceMessage: { message: m } };
1889
+ }
1890
+
1891
+ if (hasOptionalProperty(message, "mentions") && message.mentions?.length) {
1892
+ const messageType = Object.keys(m)[0];
1893
+ const key = m[messageType];
1894
+ if ("contextInfo" in key && !!key.contextInfo) {
1895
+ key.contextInfo.mentionedJid = message.mentions;
1896
+ } else if (key) {
1897
+ key.contextInfo = { mentionedJid: message.mentions };
1898
+ }
1899
+ }
1900
+
1901
+ if (hasOptionalProperty(message, "edit")) {
1902
+ m = {
1903
+ protocolMessage: {
1904
+ key: message.edit,
1905
+ editedMessage: m,
1906
+ timestampMs: Date.now(),
1907
+ type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT,
1908
+ },
1909
+ };
1910
+ }
1911
+
1912
+ if (hasOptionalProperty(message, "contextInfo") && !!message.contextInfo) {
1913
+ const messageType = Object.keys(m)[0];
1914
+ const key = m[messageType];
1915
+ if ("contextInfo" in key && !!key.contextInfo) {
1916
+ key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
1917
+ } else if (key) {
1918
+ key.contextInfo = message.contextInfo;
1919
+ }
1920
+ }
1921
+
1922
+ return WAProto.Message.create(m);
1923
+ };
1924
+
1925
+ export const generateWAMessageFromContent = (jid, message, options) => {
1926
+ if (!options.timestamp) {
1927
+ options.timestamp = new Date();
1928
+ }
1929
+ const innerMessage = normalizeMessageContent(message);
1930
+ const key = getContentType(innerMessage);
1931
+ const timestamp = unixTimestampSeconds(options.timestamp);
1932
+ const threadId = [];
1933
+ const { quoted, userJid } = options;
1934
+
1935
+ if (quoted && !isJidNewsletter(jid)) {
1936
+ const participant = quoted.key.fromMe
1937
+ ? userJid
1938
+ : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
1939
+ let quotedMsg = normalizeMessageContent(quoted.message);
1940
+ const msgType = getContentType(quotedMsg);
1941
+ quotedMsg = proto.Message.fromObject({ [msgType]: quotedMsg[msgType] });
1942
+ const quotedContent = quotedMsg[msgType];
1943
+ if (typeof quotedContent === "object" && quotedContent && "contextInfo" in quotedContent) {
1944
+ delete quotedContent.contextInfo;
1945
+ }
1946
+ let requestPayment;
1947
+ if (key === "requestPaymentMessage") {
1948
+ if (innerMessage?.requestPaymentMessage?.noteMessage?.extendedTextMessage) {
1949
+ requestPayment = innerMessage.requestPaymentMessage.noteMessage.extendedTextMessage;
1950
+ } else if (innerMessage?.requestPaymentMessage?.noteMessage?.stickerMessage) {
1951
+ requestPayment = innerMessage.requestPaymentMessage.noteMessage.stickerMessage;
1952
+ }
1953
+ }
1954
+ const contextInfo =
1955
+ (key === "requestPaymentMessage"
1956
+ ? requestPayment?.contextInfo
1957
+ : innerMessage[key]?.contextInfo) || {};
1958
+ contextInfo.participant = jidNormalizedUser(participant);
1959
+ contextInfo.stanzaId = quoted.key.id;
1960
+ contextInfo.quotedMessage = quotedMsg;
1961
+ if (jid !== quoted.key.remoteJid) {
1962
+ contextInfo.remoteJid = quoted.key.remoteJid;
1963
+ }
1964
+ if (contextInfo.quotedMessage) {
1965
+ contextInfo.quotedType = proto.ContextInfo.QuotedType.EXPLICIT;
1966
+ }
1967
+ if (contextInfo.quotedMessage && isJidGroup(jid)) {
1968
+ threadId.push({
1969
+ threadType: proto.ThreadID.ThreadType.VIEW_REPLIES,
1970
+ threadKey: {
1971
+ remoteJid: quoted?.key?.remoteJid,
1972
+ fromMe: quoted?.key?.fromMe,
1973
+ id: generateMessageID(),
1974
+ ...(quoted?.key?.fromMe ? {} : { participant: quoted?.key?.participant }),
1975
+ },
1976
+ });
1977
+ }
1978
+ if (key === "requestPaymentMessage" && requestPayment) {
1979
+ requestPayment.contextInfo = contextInfo;
1980
+ } else if (key === 'botForwardedMessage') {
1981
+ const richCtx = innerMessage[key]?.message?.richResponseMessage?.contextInfo ?? {};
1982
+ innerMessage[key].message.richResponseMessage.contextInfo = {
1983
+ ...richCtx,
1984
+ stanzaId: contextInfo.stanzaId,
1985
+ participant: contextInfo.participant,
1986
+ quotedMessage: contextInfo.quotedMessage,
1987
+ ...(contextInfo.remoteJid ? { remoteJid: contextInfo.remoteJid } : {}),
1988
+ };
1989
+ } else {
1990
+ innerMessage[key].contextInfo = contextInfo;
1991
+ }
1992
+ }
1993
+
1994
+ if (key !== "protocolMessage" && key !== "ephemeralMessage" && !isJidNewsletter(jid)) {
1995
+ message.messageContextInfo = {
1996
+ threadId: threadId.length > 0 ? threadId : [],
1997
+ messageSecret: randomBytes(32),
1998
+ ...message.messageContextInfo,
1999
+ };
2000
+ innerMessage[key].contextInfo = {
2001
+ ...(innerMessage[key].contextInfo || {}),
2002
+ expiration: options.ephemeralExpiration ? options.ephemeralExpiration : 0,
2003
+ };
2004
+ }
2005
+
2006
+ message = WAProto.Message.fromObject(message);
2007
+ const messageJSON = {
2008
+ key: {
2009
+ remoteJid: jid,
2010
+ fromMe: true,
2011
+ id: options?.messageId || generateMessageIDV2(),
2012
+ },
2013
+ message: message,
2014
+ messageTimestamp: timestamp,
2015
+ messageStubParameters: [],
2016
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined,
2017
+ status: WAMessageStatus.PENDING,
2018
+ };
2019
+ return WAProto.WebMessageInfo.fromObject(messageJSON);
2020
+ };
2021
+
2022
+ export const generateWAMessage = async (jid, content, options) => {
2023
+ options.logger = options?.logger?.child({ msgId: options.messageId });
2024
+ return generateWAMessageFromContent(
2025
+ jid,
2026
+ await generateWAMessageContent(content, {
2027
+ newsletter: isJidNewsletter(jid),
2028
+ ...options,
2029
+ jid,
2030
+ }),
2031
+ options,
2032
+ );
2033
+ };
2034
+
2035
+ export const getContentType = (content) => {
2036
+ if (content) {
2037
+ const keys = Object.keys(content);
2038
+ const key = keys.find(
2039
+ (k) =>
2040
+ (k === "conversation" ||
2041
+ k.endsWith("Message") ||
2042
+ k.endsWith("V2") ||
2043
+ k.endsWith("V3") ||
2044
+ k.endsWith("V4")) &&
2045
+ k !== "senderKeyDistributionMessage" &&
2046
+ k !== "messageContextInfo"
2047
+ );
2048
+ return key;
2049
+ }
2050
+ };
2051
+
2052
+ export const normalizeMessageContent = (content) => {
2053
+ if (!content) {
2054
+ return undefined;
2055
+ }
2056
+ for (let i = 0; i < 5; i++) {
2057
+ const inner = getFutureProofMessage(content);
2058
+ if (!inner) {
2059
+ break;
2060
+ }
2061
+ content = inner.message;
2062
+ }
2063
+ return content;
2064
+
2065
+ function getFutureProofMessage(message) {
2066
+ return (
2067
+ message?.editedMessage ||
2068
+ message?.statusAddYours ||
2069
+ message?.botTaskMessage ||
2070
+ message?.eventCoverImage ||
2071
+ message?.questionMessage ||
2072
+ message?.viewOnceMessage ||
2073
+ message?.botInvokeMessage ||
2074
+ message?.ephemeralMessage ||
2075
+ message?.limitSharingMessage ||
2076
+ message?.viewOnceMessageV2 ||
2077
+ message?.lottieStickerMessage ||
2078
+ message?.questionReplyMessage ||
2079
+ message?.botForwardedMessage ||
2080
+ message?.statusMentionMessage ||
2081
+ message?.pollCreationMessageV4 ||
2082
+ message?.associatedChildMessage ||
2083
+ message?.groupMentionedMessage ||
2084
+ message?.groupStatusMentionMessage ||
2085
+ message?.viewOnceMessageV2Extension ||
2086
+ message?.documentWithCaptionMessage ||
2087
+ message?.pollCreationOptionImageMessage
2088
+ );
2089
+ }
2090
+ };
2091
+
2092
+ export const extractMessageContent = (content) => {
2093
+ const extractFromTemplateMessage = (msg) => {
2094
+ const header = typeof msg.header === "object" && msg.header !== null;
2095
+ if (header ? msg.header?.imageMessage : msg.imageMessage) {
2096
+ return { imageMessage: header ? msg.header.imageMessage : msg.imageMessage };
2097
+ } else if (header ? msg.header?.documentMessage : msg.documentMessage) {
2098
+ return { documentMessage: header ? msg.header.documentMessage : msg.documentMessage };
2099
+ } else if (header ? msg.header?.videoMessage : msg.videoMessage) {
2100
+ return { videoMessage: header ? msg.header.videoMessage : msg.videoMessage };
2101
+ } else if (header ? msg.header?.locationMessage : msg.locationMessage) {
2102
+ return { locationMessage: header ? msg.header.locationMessage : msg.locationMessage };
2103
+ } else if (header ? msg.header?.productMessage : msg.productMessage) {
2104
+ return { productMessage: header ? msg.header.productMessage : msg.productMessage };
2105
+ } else {
2106
+ return {
2107
+ conversation:
2108
+ "contentText" in msg
2109
+ ? msg.contentText
2110
+ : "hydratedContentText" in msg
2111
+ ? msg.hydratedContentText
2112
+ : "body" in msg
2113
+ ? msg.body.text
2114
+ : "",
2115
+ };
2116
+ }
2117
+ };
2118
+ content = normalizeMessageContent(content);
2119
+ if (content?.buttonsMessage) {
2120
+ return extractFromTemplateMessage(content.buttonsMessage);
2121
+ }
2122
+ if (content?.interactiveMessage) {
2123
+ return extractFromTemplateMessage(content.interactiveMessage);
2124
+ }
2125
+ if (content?.templateMessage?.interactiveMessageTemplate) {
2126
+ return extractFromTemplateMessage(content?.templateMessage?.interactiveMessageTemplate);
2127
+ }
2128
+ if (content?.templateMessage?.hydratedFourRowTemplate) {
2129
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
2130
+ }
2131
+ if (content?.templateMessage?.hydratedTemplate) {
2132
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
2133
+ }
2134
+ if (content?.templateMessage?.fourRowTemplate) {
2135
+ return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
2136
+ }
2137
+ return content;
2138
+ };
2139
+
2140
+ export const getDevice = (id) =>
2141
+ /^3A.{18}$/.test(id)
2142
+ ? "ios"
2143
+ : /^3E.{20}$/.test(id)
2144
+ ? "web"
2145
+ : /^(.{21}|.{32})$/.test(id)
2146
+ ? "android"
2147
+ : /^(3F|.{18}$)/.test(id)
2148
+ ? "desktop"
2149
+ : "baileys";
2150
+
2151
+ export const updateMessageWithReceipt = (msg, receipt) => {
2152
+ msg.userReceipt = msg.userReceipt || [];
2153
+ const recp = msg.userReceipt.find((m) => m.userJid === receipt.userJid);
2154
+ if (recp) {
2155
+ Object.assign(recp, receipt);
2156
+ } else {
2157
+ msg.userReceipt.push(receipt);
2158
+ }
2159
+ };
2160
+
2161
+ export const updateMessageWithReaction = (msg, reaction) => {
2162
+ const authorID = getKeyAuthor(reaction.key);
2163
+ const reactions = (msg.reactions || []).filter((r) => getKeyAuthor(r.key) !== authorID);
2164
+ reaction.text = reaction.text || "";
2165
+ reactions.push(reaction);
2166
+ msg.reactions = reactions;
2167
+ };
2168
+
2169
+ export const updateMessageWithPollUpdate = (msg, update) => {
2170
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey);
2171
+ const votes = (msg.pollUpdates || []).filter(
2172
+ (r) => getKeyAuthor(r.pollUpdateMessageKey) !== authorID
2173
+ );
2174
+ if (update.vote?.selectedOptions?.length) {
2175
+ votes.push(update);
2176
+ }
2177
+ msg.pollUpdates = votes;
2178
+ };
2179
+
2180
+ export const updateMessageWithEventResponse = (msg, update) => {
2181
+ const authorID = getKeyAuthor(update.eventResponseMessageKey);
2182
+ const responses = (msg.eventResponses || []).filter(
2183
+ (r) => getKeyAuthor(r.eventResponseMessageKey) !== authorID
2184
+ );
2185
+ responses.push(update);
2186
+ msg.eventResponses = responses;
2187
+ };
2188
+
2189
+ export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
2190
+ message = normalizeMessageContent(message);
2191
+ const opts =
2192
+ message?.pollCreationMessage?.options ||
2193
+ message?.pollCreationMessageV2?.options ||
2194
+ message?.pollCreationMessageV3?.options ||
2195
+ [];
2196
+ const voteHashMap = opts.reduce((acc, opt) => {
2197
+ const hash = sha256(Buffer.from(opt.optionName || "")).toString();
2198
+ acc[hash] = { name: opt.optionName || "", voters: [] };
2199
+ return acc;
2200
+ }, {});
2201
+ for (const update of pollUpdates || []) {
2202
+ const { vote } = update;
2203
+ if (!vote) continue;
2204
+ for (const option of vote.selectedOptions || []) {
2205
+ const hash = option.toString();
2206
+ let data = voteHashMap[hash];
2207
+ if (!data) {
2208
+ voteHashMap[hash] = { name: "Unknown", voters: [] };
2209
+ data = voteHashMap[hash];
2210
+ }
2211
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
2212
+ }
2213
+ }
2214
+ return Object.values(voteHashMap);
2215
+ }
2216
+
2217
+ export function getAggregateResponsesInEventMessage({ eventResponses }, meId) {
2218
+ const responseTypes = ["GOING", "NOT_GOING", "MAYBE"];
2219
+ const responseMap = {};
2220
+ for (const type of responseTypes) {
2221
+ responseMap[type] = { response: type, responders: [] };
2222
+ }
2223
+ for (const update of eventResponses || []) {
2224
+ const { response } = update.response || 0;
2225
+ const responseType = proto.Message.EventResponseMessage.EventResponseType[response];
2226
+ if (responseType !== "UNKNOWN" && responseMap[responseType]) {
2227
+ responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId));
2228
+ }
2229
+ }
2230
+ return Object.values(responseMap);
2231
+ }
2232
+
2233
+ export const aggregateMessageKeysNotFromMe = (keys) => {
2234
+ const keyMap = {};
2235
+ for (const { remoteJid, id, participant, fromMe } of keys) {
2236
+ if (!fromMe) {
2237
+ const uqKey = `${remoteJid}:${participant || ""}`;
2238
+ if (!keyMap[uqKey]) {
2239
+ keyMap[uqKey] = { jid: remoteJid, participant: participant, messageIds: [] };
2240
+ }
2241
+ keyMap[uqKey].messageIds.push(id);
2242
+ }
2243
+ }
2244
+ return Object.values(keyMap);
2245
+ };
2246
+
2247
+ const REUPLOAD_REQUIRED_STATUS = [410, 404];
2248
+
2249
+ export const downloadMediaMessage = async (message, type, options, ctx) => {
2250
+ const result = await downloadMsg().catch(async (error) => {
2251
+ if (ctx && typeof error?.status === "number" && REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
2252
+ ctx.logger.info({ key: message.key }, "sending reupload media request...");
2253
+ message = await ctx.reuploadRequest(message);
2254
+ const result = await downloadMsg();
2255
+ return result;
2256
+ }
2257
+ throw error;
2258
+ });
2259
+ return result;
2260
+
2261
+ async function downloadMsg() {
2262
+ const mContent = extractMessageContent(message.message);
2263
+ if (!mContent) {
2264
+ throw new Boom("No message present", { statusCode: 400, data: message });
2265
+ }
2266
+ const contentType = getContentType(mContent);
2267
+ let mediaType = contentType?.replace("Message", "");
2268
+ const media =
2269
+ contentType === "productMessage"
2270
+ ? mContent[contentType]?.product?.productImage
2271
+ : mContent[contentType];
2272
+ if (!media || typeof media !== "object" || (!("url" in media) && !("thumbnailDirectPath" in media))) {
2273
+ throw new Boom(`"${contentType}" message is not a media message`);
2274
+ }
2275
+ let download;
2276
+ if ("thumbnailDirectPath" in media && !("url" in media)) {
2277
+ download = { directPath: media.thumbnailDirectPath, mediaKey: media.mediaKey };
2278
+ mediaType = "thumbnail-link";
2279
+ } else {
2280
+ download = media;
2281
+ }
2282
+ const stream = await downloadContentFromMessage(download, mediaType, options);
2283
+ if (type === "buffer") {
2284
+ const bufferArray = [];
2285
+ for await (const chunk of stream) {
2286
+ bufferArray.push(chunk);
2287
+ }
2288
+ return Buffer.concat(bufferArray);
2289
+ }
2290
+ return stream;
2291
+ }
2292
+ };
2293
+
2294
+ export const assertMediaContent = (content) => {
2295
+ content = extractMessageContent(content);
2296
+ const mediaContent =
2297
+ content?.documentMessage ||
2298
+ content?.imageMessage ||
2299
+ content?.videoMessage ||
2300
+ content?.audioMessage ||
2301
+ content?.stickerMessage;
2302
+ if (!mediaContent) {
2303
+ throw new Boom("given message is not a media message", { statusCode: 400, data: content });
2304
+ }
2305
+ return mediaContent;
2306
+ };