ishumdz-bail 1.0.0

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 (118) hide show
  1. package/README.md +772 -0
  2. package/WAProto/fix-imports.js +85 -0
  3. package/WAProto/index.d.ts +79257 -0
  4. package/WAProto/index.js +242946 -0
  5. package/engine-requirements.js +10 -0
  6. package/lib/Defaults/index.js +142 -0
  7. package/lib/Games/index.js +497 -0
  8. package/lib/Signal/Group/ciphertext-message.js +12 -0
  9. package/lib/Signal/Group/group-session-builder.js +30 -0
  10. package/lib/Signal/Group/group_cipher.js +93 -0
  11. package/lib/Signal/Group/index.js +12 -0
  12. package/lib/Signal/Group/keyhelper.js +18 -0
  13. package/lib/Signal/Group/queue-job.js +53 -0
  14. package/lib/Signal/Group/sender-chain-key.js +26 -0
  15. package/lib/Signal/Group/sender-key-distribution-message.js +63 -0
  16. package/lib/Signal/Group/sender-key-message.js +66 -0
  17. package/lib/Signal/Group/sender-key-name.js +48 -0
  18. package/lib/Signal/Group/sender-key-record.js +41 -0
  19. package/lib/Signal/Group/sender-key-state.js +84 -0
  20. package/lib/Signal/Group/sender-message-key.js +26 -0
  21. package/lib/Signal/libsignal.js +431 -0
  22. package/lib/Signal/lid-mapping.js +277 -0
  23. package/lib/Socket/Client/index.js +3 -0
  24. package/lib/Socket/Client/types.js +11 -0
  25. package/lib/Socket/Client/websocket.js +84 -0
  26. package/lib/Socket/ban-checker.js +265 -0
  27. package/lib/Socket/business.js +379 -0
  28. package/lib/Socket/chats.js +1193 -0
  29. package/lib/Socket/communities.js +431 -0
  30. package/lib/Socket/groups.js +374 -0
  31. package/lib/Socket/index.js +13 -0
  32. package/lib/Socket/luxu.js +460 -0
  33. package/lib/Socket/messages-recv.js +1916 -0
  34. package/lib/Socket/messages-send.js +1469 -0
  35. package/lib/Socket/mex.js +41 -0
  36. package/lib/Socket/newsletter.js +261 -0
  37. package/lib/Socket/socket.js +988 -0
  38. package/lib/Socket/username.js +234 -0
  39. package/lib/Store/index.js +10 -0
  40. package/lib/Store/keyed-db.js +108 -0
  41. package/lib/Store/make-cache-manager-store.js +85 -0
  42. package/lib/Store/make-in-memory-store.js +198 -0
  43. package/lib/Store/make-ordered-dictionary.js +75 -0
  44. package/lib/Store/object-repository.js +32 -0
  45. package/lib/Types/Auth.js +2 -0
  46. package/lib/Types/Bussines.js +2 -0
  47. package/lib/Types/Call.js +2 -0
  48. package/lib/Types/Chat.js +8 -0
  49. package/lib/Types/Contact.js +2 -0
  50. package/lib/Types/Events.js +2 -0
  51. package/lib/Types/GroupMetadata.js +2 -0
  52. package/lib/Types/Label.js +25 -0
  53. package/lib/Types/LabelAssociation.js +7 -0
  54. package/lib/Types/Message.js +11 -0
  55. package/lib/Types/Mex.js +37 -0
  56. package/lib/Types/Product.js +2 -0
  57. package/lib/Types/Signal.js +2 -0
  58. package/lib/Types/Socket.js +3 -0
  59. package/lib/Types/State.js +56 -0
  60. package/lib/Types/USync.js +2 -0
  61. package/lib/Types/index.js +26 -0
  62. package/lib/Utils/auth-utils.js +302 -0
  63. package/lib/Utils/browser-utils.js +49 -0
  64. package/lib/Utils/business.js +231 -0
  65. package/lib/Utils/chat-utils.js +872 -0
  66. package/lib/Utils/classify-disconnect.js +126 -0
  67. package/lib/Utils/companion-reg-client-utils.js +35 -0
  68. package/lib/Utils/crypto.js +118 -0
  69. package/lib/Utils/decode-wa-message.js +350 -0
  70. package/lib/Utils/event-buffer.js +622 -0
  71. package/lib/Utils/generics.js +403 -0
  72. package/lib/Utils/history.js +134 -0
  73. package/lib/Utils/identity-change-handler.js +50 -0
  74. package/lib/Utils/index.js +23 -0
  75. package/lib/Utils/link-preview.js +85 -0
  76. package/lib/Utils/logger.js +3 -0
  77. package/lib/Utils/lt-hash.js +8 -0
  78. package/lib/Utils/make-mutex.js +33 -0
  79. package/lib/Utils/message-composer.js +273 -0
  80. package/lib/Utils/message-retry-manager.js +265 -0
  81. package/lib/Utils/messages-media.js +793 -0
  82. package/lib/Utils/messages.js +1586 -0
  83. package/lib/Utils/noise-handler.js +201 -0
  84. package/lib/Utils/offline-node-processor.js +40 -0
  85. package/lib/Utils/pre-key-manager.js +106 -0
  86. package/lib/Utils/process-message.js +630 -0
  87. package/lib/Utils/rate-limiter.js +228 -0
  88. package/lib/Utils/reporting-utils.js +258 -0
  89. package/lib/Utils/signal.js +201 -0
  90. package/lib/Utils/stanza-ack.js +38 -0
  91. package/lib/Utils/sync-action-utils.js +49 -0
  92. package/lib/Utils/tc-token-utils.js +163 -0
  93. package/lib/Utils/use-multi-file-auth-state.js +121 -0
  94. package/lib/Utils/validate-connection.js +203 -0
  95. package/lib/WABinary/constants.js +1301 -0
  96. package/lib/WABinary/decode.js +262 -0
  97. package/lib/WABinary/encode.js +220 -0
  98. package/lib/WABinary/generic-utils.js +204 -0
  99. package/lib/WABinary/index.js +6 -0
  100. package/lib/WABinary/jid-utils.js +98 -0
  101. package/lib/WABinary/types.js +2 -0
  102. package/lib/WAM/BinaryInfo.js +10 -0
  103. package/lib/WAM/constants.js +22853 -0
  104. package/lib/WAM/encode.js +150 -0
  105. package/lib/WAM/index.js +4 -0
  106. package/lib/WAUSync/Protocols/USyncContactProtocol.js +52 -0
  107. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +54 -0
  108. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +27 -0
  109. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +38 -0
  110. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
  111. package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +51 -0
  112. package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +29 -0
  113. package/lib/WAUSync/Protocols/index.js +6 -0
  114. package/lib/WAUSync/USyncQuery.js +98 -0
  115. package/lib/WAUSync/USyncUser.js +31 -0
  116. package/lib/WAUSync/index.js +4 -0
  117. package/lib/index.js +37 -0
  118. package/package.json +150 -0
@@ -0,0 +1,1586 @@
1
+ import { Boom } from '@hapi/boom';
2
+ import { randomUUID, randomBytes } from 'crypto';
3
+ import { promises as fs } from 'fs';
4
+ import {} from 'stream';
5
+ import { proto } from '../../WAProto/index.js';
6
+ import { CALL_AUDIO_PREFIX, CALL_VIDEO_PREFIX, MEDIA_KEYS, URL_REGEX, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
7
+ import { WAMessageStatus, WAProto } from '../Types/index.js';
8
+ import { isJidGroup, isJidNewsletter, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary/index.js';
9
+ import { aesEncryptGCM, hmacSign, sha256 } from './crypto.js';
10
+ import { generateMessageIDV2, getKeyAuthor, unixTimestampSeconds } from './generics.js';
11
+ import { downloadContentFromMessage, encryptedStream, generateThumbnail, getAudioDuration, getAudioWaveform, getRawMediaUploadData } from './messages-media.js';
12
+ import { shouldIncludeReportingToken } from './reporting-utils.js';
13
+ const MIMETYPE_MAP = {
14
+ image: 'image/jpeg',
15
+ video: 'video/mp4',
16
+ document: 'application/pdf',
17
+ audio: 'audio/ogg; codecs=opus',
18
+ sticker: 'image/webp',
19
+ 'product-catalog-image': 'image/jpeg'
20
+ };
21
+ const MessageTypeProto = {
22
+ image: WAProto.Message.ImageMessage,
23
+ video: WAProto.Message.VideoMessage,
24
+ audio: WAProto.Message.AudioMessage,
25
+ sticker: WAProto.Message.StickerMessage,
26
+ document: WAProto.Message.DocumentMessage
27
+ };
28
+ const ButtonType = proto.Message.ButtonsMessage.HeaderType;
29
+ const RICH_RESPONSE_CODE_KEYWORDS = new Set([
30
+ 'break',
31
+ 'case',
32
+ 'catch',
33
+ 'continue',
34
+ 'debugger',
35
+ 'default',
36
+ 'delete',
37
+ 'do',
38
+ 'else',
39
+ 'finally',
40
+ 'for',
41
+ 'function',
42
+ 'if',
43
+ 'in',
44
+ 'instanceof',
45
+ 'new',
46
+ 'return',
47
+ 'switch',
48
+ 'this',
49
+ 'throw',
50
+ 'try',
51
+ 'typeof',
52
+ 'var',
53
+ 'void',
54
+ 'while',
55
+ 'with',
56
+ 'true',
57
+ 'false',
58
+ 'null',
59
+ 'undefined',
60
+ 'NaN',
61
+ 'Infinity',
62
+ 'class',
63
+ 'const',
64
+ 'let',
65
+ 'super',
66
+ 'extends',
67
+ 'export',
68
+ 'import',
69
+ 'yield',
70
+ 'static',
71
+ 'constructor',
72
+ 'of',
73
+ 'async',
74
+ 'await',
75
+ 'get',
76
+ 'set',
77
+ 'implements',
78
+ 'interface',
79
+ 'package',
80
+ 'private',
81
+ 'protected',
82
+ 'public',
83
+ 'enum',
84
+ 'throws',
85
+ 'transient'
86
+ ])
87
+ const tokenizeCode = code => {
88
+ const tokens = []
89
+ let i = 0
90
+ const len = code.length
91
+ while (i < len) {
92
+ if (/\s/.test(code[i])) {
93
+ const start = i
94
+ while (i < len && /\s/.test(code[i])) i++
95
+ tokens.push({ content: code.slice(start, i), type: 'DEFAULT' })
96
+ continue
97
+ }
98
+ if (code[i] === '"' || code[i] === "'" || code[i] === '`') {
99
+ const start = i
100
+ const quote = code[i]
101
+ i++
102
+ while (i < len && code[i] !== quote) {
103
+ if (code[i] === '\\') i++
104
+ i++
105
+ }
106
+ i++
107
+ tokens.push({ content: code.slice(start, i), type: 'STR' })
108
+ continue
109
+ }
110
+ if (code[i] === '/' && i + 1 < len && code[i + 1] === '/') {
111
+ const start = i
112
+ while (i < len && code[i] !== '\n') i++
113
+ tokens.push({ content: code.slice(start, i), type: 'COMMENT' })
114
+ continue
115
+ }
116
+ if (code[i] === '/' && i + 1 < len && code[i + 1] === '*') {
117
+ const start = i
118
+ i += 2
119
+ while (i + 1 < len && !(code[i] === '*' && code[i + 1] === '/')) i++
120
+ i += 2
121
+ tokens.push({ content: code.slice(start, i), type: 'COMMENT' })
122
+ continue
123
+ }
124
+ if (/[0-9]/.test(code[i])) {
125
+ const start = i
126
+ while (i < len && /[0-9.]/.test(code[i])) i++
127
+ tokens.push({ content: code.slice(start, i), type: 'NUMBER' })
128
+ continue
129
+ }
130
+ if (/[a-zA-Z_$]/.test(code[i])) {
131
+ const start = i
132
+ while (i < len && /[a-zA-Z0-9_$]/.test(code[i])) i++
133
+ const word = code.slice(start, i)
134
+ if (RICH_RESPONSE_CODE_KEYWORDS.has(word)) {
135
+ tokens.push({ content: word, type: 'KEYWORD' })
136
+ } else {
137
+ let j = i
138
+ while (j < len && /\s/.test(code[j])) j++
139
+ tokens.push({ content: word, type: j < len && code[j] === '(' ? 'METHOD' : 'DEFAULT' })
140
+ }
141
+ continue
142
+ }
143
+ tokens.push({ content: code[i], type: 'DEFAULT' })
144
+ i++
145
+ }
146
+ const merged = []
147
+ for (const t of tokens) {
148
+ if (merged.length && merged[merged.length - 1].type === 'DEFAULT' && t.type === 'DEFAULT') {
149
+ merged[merged.length - 1].content += t.content
150
+ } else {
151
+ merged.push(t)
152
+ }
153
+ }
154
+ return merged
155
+ }
156
+ /**
157
+ * Uses a regex to test whether the string contains a URL, and returns the URL if it does.
158
+ * @param text eg. hello https://google.com
159
+ * @returns the URL, eg. https://google.com
160
+ */
161
+ export const extractUrlFromText = (text) => text.match(URL_REGEX)?.[0];
162
+ export const generateLinkPreviewIfRequired = async (text, getUrlInfo, logger) => {
163
+ const url = extractUrlFromText(text);
164
+ if (!!getUrlInfo && url) {
165
+ try {
166
+ const urlInfo = await getUrlInfo(url);
167
+ return urlInfo;
168
+ }
169
+ catch (error) {
170
+ // ignore if fails
171
+ logger?.warn({ trace: error.stack }, 'url generation failed');
172
+ }
173
+ }
174
+ };
175
+ const assertColor = async (color) => {
176
+ let assertedColor;
177
+ if (typeof color === 'number') {
178
+ assertedColor = color > 0 ? color : 0xffffffff + Number(color) + 1;
179
+ }
180
+ else {
181
+ let hex = color.trim().replace('#', '');
182
+ if (hex.length <= 6) {
183
+ hex = 'FF' + hex.padStart(6, '0');
184
+ }
185
+ assertedColor = parseInt(hex, 16);
186
+ return assertedColor;
187
+ }
188
+ };
189
+ export const prepareWAMessageMedia = async (message, options) => {
190
+ const logger = options.logger;
191
+ let mediaType;
192
+ for (const key of MEDIA_KEYS) {
193
+ if (key in message) {
194
+ mediaType = key;
195
+ }
196
+ }
197
+ if (!mediaType) {
198
+ throw new Boom('Invalid media type', { statusCode: 400 });
199
+ }
200
+ const uploadData = {
201
+ ...message,
202
+ media: message[mediaType]
203
+ };
204
+ delete uploadData[mediaType];
205
+ // check if cacheable + generate cache key
206
+ const cacheableKey = typeof uploadData.media === 'object' &&
207
+ 'url' in uploadData.media &&
208
+ !!uploadData.media.url &&
209
+ !!options.mediaCache &&
210
+ mediaType + ':' + uploadData.media.url.toString();
211
+ if (mediaType === 'document' && !uploadData.fileName) {
212
+ uploadData.fileName = 'file';
213
+ }
214
+ if (!uploadData.mimetype) {
215
+ uploadData.mimetype = MIMETYPE_MAP[mediaType];
216
+ }
217
+ if (cacheableKey) {
218
+ const mediaBuff = await options.mediaCache.get(cacheableKey);
219
+ if (mediaBuff) {
220
+ logger?.debug({ cacheableKey }, 'got media cache hit');
221
+ const obj = proto.Message.decode(mediaBuff);
222
+ const key = `${mediaType}Message`;
223
+ Object.assign(obj[key], { ...uploadData, media: undefined });
224
+ return obj;
225
+ }
226
+ }
227
+ const isNewsletter = !!options.jid && isJidNewsletter(options.jid);
228
+ if (isNewsletter) {
229
+ logger?.info({ key: cacheableKey }, 'Preparing raw media for newsletter');
230
+ const { filePath, fileSha256, fileLength } = await getRawMediaUploadData(uploadData.media, options.mediaTypeOverride || mediaType, logger);
231
+ const fileSha256B64 = fileSha256.toString('base64');
232
+ const { mediaUrl, directPath } = await options.upload(filePath, {
233
+ fileEncSha256B64: fileSha256B64,
234
+ mediaType: mediaType,
235
+ timeoutMs: options.mediaUploadTimeoutMs,
236
+ newsletter: true
237
+ });
238
+ await fs.unlink(filePath);
239
+ const obj = WAProto.Message.fromObject({
240
+ // todo: add more support here
241
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
242
+ url: mediaUrl,
243
+ directPath,
244
+ fileSha256,
245
+ fileLength,
246
+ ...uploadData,
247
+ media: undefined
248
+ })
249
+ });
250
+ if (uploadData.ptv) {
251
+ obj.ptvMessage = obj.videoMessage;
252
+ delete obj.videoMessage;
253
+ }
254
+ if (obj.stickerMessage) {
255
+ obj.stickerMessage.stickerSentTs = Date.now();
256
+ }
257
+ if (cacheableKey) {
258
+ logger?.debug({ cacheableKey }, 'set cache');
259
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
260
+ }
261
+ return obj;
262
+ }
263
+ const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
264
+ const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
265
+ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined';
266
+ const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
267
+ const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
268
+ const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
269
+ logger,
270
+ saveOriginalFileIfRequired: requiresOriginalForSomeProcessing,
271
+ opts: options.options
272
+ });
273
+ const fileEncSha256B64 = fileEncSha256.toString('base64');
274
+ const [{ mediaUrl, directPath }] = await Promise.all([
275
+ (async () => {
276
+ const result = await options.upload(encFilePath, {
277
+ fileEncSha256B64,
278
+ mediaType,
279
+ timeoutMs: options.mediaUploadTimeoutMs
280
+ });
281
+ logger?.debug({ mediaType, cacheableKey }, 'uploaded media');
282
+ return result;
283
+ })(),
284
+ (async () => {
285
+ try {
286
+ if (requiresThumbnailComputation) {
287
+ const { thumbnail, originalImageDimensions } = await generateThumbnail(originalFilePath, mediaType, options);
288
+ uploadData.jpegThumbnail = thumbnail;
289
+ if (!uploadData.width && originalImageDimensions) {
290
+ uploadData.width = originalImageDimensions.width;
291
+ uploadData.height = originalImageDimensions.height;
292
+ logger?.debug('set dimensions');
293
+ }
294
+ logger?.debug('generated thumbnail');
295
+ }
296
+ if (requiresDurationComputation) {
297
+ uploadData.seconds = await getAudioDuration(originalFilePath);
298
+ logger?.debug('computed audio duration');
299
+ }
300
+ if (requiresWaveformProcessing) {
301
+ uploadData.waveform = await getAudioWaveform(originalFilePath, logger);
302
+ logger?.debug('processed waveform');
303
+ }
304
+ if (requiresAudioBackground) {
305
+ uploadData.backgroundArgb = await assertColor(options.backgroundColor);
306
+ logger?.debug('computed backgroundColor audio status');
307
+ }
308
+ }
309
+ catch (error) {
310
+ logger?.warn({ trace: error.stack }, 'failed to obtain extra info');
311
+ }
312
+ })()
313
+ ]).finally(async () => {
314
+ try {
315
+ await fs.unlink(encFilePath);
316
+ if (originalFilePath) {
317
+ await fs.unlink(originalFilePath);
318
+ }
319
+ logger?.debug('removed tmp files');
320
+ }
321
+ catch (error) {
322
+ logger?.warn('failed to remove tmp file');
323
+ }
324
+ });
325
+ const obj = WAProto.Message.fromObject({
326
+ [`${mediaType}Message`]: MessageTypeProto[mediaType].fromObject({
327
+ url: mediaUrl,
328
+ directPath,
329
+ mediaKey,
330
+ fileEncSha256,
331
+ fileSha256,
332
+ fileLength,
333
+ mediaKeyTimestamp: unixTimestampSeconds(),
334
+ ...uploadData,
335
+ media: undefined
336
+ })
337
+ });
338
+ if (uploadData.ptv) {
339
+ obj.ptvMessage = obj.videoMessage;
340
+ delete obj.videoMessage;
341
+ }
342
+ if (cacheableKey) {
343
+ logger?.debug({ cacheableKey }, 'set cache');
344
+ await options.mediaCache.set(cacheableKey, WAProto.Message.encode(obj).finish());
345
+ }
346
+ return obj;
347
+ };
348
+ export const prepareDisappearingMessageSettingContent = (ephemeralExpiration) => {
349
+ ephemeralExpiration = ephemeralExpiration || 0;
350
+ const content = {
351
+ ephemeralMessage: {
352
+ message: {
353
+ protocolMessage: {
354
+ type: WAProto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING,
355
+ ephemeralExpiration
356
+ }
357
+ }
358
+ }
359
+ };
360
+ return WAProto.Message.fromObject(content);
361
+ };
362
+ /**
363
+ * Generate forwarded message content like WA does
364
+ * @param message the message to forward
365
+ * @param options.forceForward will show the message as forwarded even if it is from you
366
+ */
367
+ /**
368
+ * Prepares an album message: a "container" message (albumMessage) plus one
369
+ * generated media message per item, each linked back to the container via
370
+ * messageAssociation so WA clients render them as a single album/grid.
371
+ *
372
+ * Ported from levvleys @levvicode 2.0.16 (Utils/messages.js) -> edgar-bail.
373
+ *
374
+ * @param jid the chat to send the album to
375
+ * @param albums array of media items, each with an `image` or `video` key
376
+ * plus whatever other options prepareWAMessageMedia accepts
377
+ * @param options must include `conn` (the active socket) and `userJid`
378
+ */
379
+ export const prepareAlbumMessageContent = async (jid, albums, options) => {
380
+ if (!Array.isArray(albums)) {
381
+ throw new Boom('albums must be an array containing media objects.');
382
+ }
383
+ if (albums.length === 0) {
384
+ throw new Boom('albums cannot be empty. At least one media item is required.');
385
+ }
386
+ const validCount = albums.filter((m) => 'image' in m || 'video' in m).length;
387
+ if (validCount === 0) {
388
+ throw new Boom('albums contains no valid media. Use \'image\' or \'video\' keys.');
389
+ }
390
+ let mediaHandle;
391
+ let mediaMsg;
392
+ const message = [];
393
+ const albumMsg = generateWAMessageFromContent(jid, {
394
+ albumMessage: {
395
+ expectedImageCount: albums.filter((item) => 'image' in item).length,
396
+ expectedVideoCount: albums.filter((item) => 'video' in item).length
397
+ }
398
+ }, options);
399
+ await options.conn.relayMessage(jid, albumMsg.message, {
400
+ messageId: albumMsg.key.id
401
+ });
402
+ for (const media of albums) {
403
+ let content = {};
404
+ if ('image' in media) {
405
+ content = { image: media.image };
406
+ }
407
+ else if ('video' in media) {
408
+ content = { video: media.video };
409
+ }
410
+ else {
411
+ continue;
412
+ }
413
+ mediaMsg = await generateWAMessage(jid, { ...content, ...media }, {
414
+ userJid: options.userJid,
415
+ upload: async (encFilePath, opts) => {
416
+ const up = await options.conn.waUploadToServer(encFilePath, {
417
+ ...opts,
418
+ newsletter: isJidNewsletter(jid)
419
+ });
420
+ mediaHandle = up.handle;
421
+ return up;
422
+ },
423
+ ...options
424
+ });
425
+ if (mediaMsg) {
426
+ mediaMsg.message.messageContextInfo = {
427
+ messageSecret: randomBytes(32),
428
+ messageAssociation: {
429
+ associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM,
430
+ parentMessageKey: albumMsg.key
431
+ }
432
+ };
433
+ }
434
+ message.push(mediaMsg);
435
+ }
436
+ return message;
437
+ };
438
+
439
+ export const generateForwardMessageContent = (message, forceForward) => {
440
+ let content = message.message;
441
+ if (!content) {
442
+ throw new Boom('no content in message', { statusCode: 400 });
443
+ }
444
+ // hacky copy
445
+ content = normalizeMessageContent(content);
446
+ content = proto.Message.decode(proto.Message.encode(content).finish());
447
+ let key = Object.keys(content)[0];
448
+ let score = content?.[key]?.contextInfo?.forwardingScore || 0;
449
+ score += message.key.fromMe && !forceForward ? 0 : 1;
450
+ if (key === 'conversation') {
451
+ content.extendedTextMessage = { text: content[key] };
452
+ delete content.conversation;
453
+ key = 'extendedTextMessage';
454
+ }
455
+ const key_ = content?.[key];
456
+ if (score > 0) {
457
+ key_.contextInfo = { forwardingScore: score, isForwarded: true };
458
+ }
459
+ else {
460
+ key_.contextInfo = {};
461
+ }
462
+ return content;
463
+ };
464
+ export const hasNonNullishProperty = (message, key) => {
465
+ return (typeof message === 'object' &&
466
+ message !== null &&
467
+ key in message &&
468
+ message[key] !== null &&
469
+ message[key] !== undefined);
470
+ };
471
+ function hasOptionalProperty(obj, key) {
472
+ return typeof obj === 'object' && obj !== null && key in obj && obj[key] !== null;
473
+ }
474
+ const applyContextInfoAndMentions = (interactiveMessage, message) => {
475
+ if ('contextInfo' in message && !!message.contextInfo) {
476
+ interactiveMessage.contextInfo = message.contextInfo
477
+ }
478
+ if ('mentions' in message && !!message.mentions) {
479
+ interactiveMessage.contextInfo = {
480
+ ...(interactiveMessage.contextInfo || {}),
481
+ mentionedJid: message.mentions
482
+ }
483
+ }
484
+ }
485
+ export const generateWAMessageContent = async (message, options) => {
486
+ var _a, _b;
487
+ let m = {};
488
+ if ('interactiveButtons' in message && !!message.interactiveButtons) {
489
+ const nativeFlow = proto.Message.InteractiveMessage.NativeFlowMessage.fromObject({
490
+ buttons: message.interactiveButtons,
491
+ messageParamsJson: message.messageParams ?? "{}"
492
+ });
493
+ let interactiveMessage = { nativeFlowMessage: nativeFlow };
494
+ if ('text' in message) {
495
+ interactiveMessage.body = { text: message.text };
496
+ } else if ('caption' in message) {
497
+ interactiveMessage.body = { text: message.caption };
498
+ }
499
+ if ('title' in message && !!message.title) {
500
+ interactiveMessage.header = {
501
+ title: message.title,
502
+ subtitle: message.subtitle || "",
503
+ hasMediaAttachment: false
504
+ };
505
+ let media = null;
506
+ if ('image' in message && !!message.image) {
507
+ media = await prepareWAMessageMedia({ image: message.image }, options);
508
+ interactiveMessage.header.imageMessage = media.imageMessage;
509
+ if (message.image.caption) {
510
+ interactiveMessage.header.imageMessage.caption = message.image.caption;
511
+ }
512
+ } else if ('video' in message && !!message.video) {
513
+ media = await prepareWAMessageMedia({ video: message.video }, options);
514
+ interactiveMessage.header.videoMessage = media.videoMessage;
515
+ if (message.video.caption) {
516
+ interactiveMessage.header.videoMessage.caption = message.video.caption;
517
+ }
518
+ } else if ('gif' in message && !!message.gif) {
519
+ media = await prepareWAMessageMedia({ video: message.gif }, options);
520
+ interactiveMessage.header.videoMessage = media.videoMessage;
521
+ interactiveMessage.header.videoMessage.gifPlayback = true;
522
+ if (message.gif.caption) {
523
+ interactiveMessage.header.videoMessage.caption = message.gif.caption;
524
+ }
525
+ } else if ('document' in message && !!message.document) {
526
+ media = await prepareWAMessageMedia({ document: message.document }, options);
527
+ interactiveMessage.header.documentMessage = media.documentMessage;
528
+ let docuR = interactiveMessage.header.documentMessage;
529
+ docuR.fileName = message.document.fileName || "Document";
530
+ docuR.mimetype = message.document.mimetype || docuR.mimetype;
531
+ if (message.document.caption) docuR.caption = message.document.caption;
532
+ if (message.document.jpegThumbnail) docuR.jpegThumbnail = message.document.jpegThumbnail;
533
+ } else if ('location' in message && !!message.location) {
534
+ let mLoc = message.location;
535
+ interactiveMessage.header.locationMessage = {
536
+ degreesLongitude: mLoc.longitude || 0,
537
+ degreesLatitude: mLoc.latitude || 0,
538
+ name: mLoc.name || null,
539
+ address: mLoc.address || null,
540
+ url: mLoc.url || null
541
+ };
542
+ media = true;
543
+ } else if ('thumbnail' in message && !!message.thumbnail) {
544
+ interactiveMessage.header.jpegThumbnail = message.thumbnail;
545
+ }
546
+ if (media || ('thumbnail' in message && !!message.thumbnail)) {
547
+ interactiveMessage.header.hasMediaAttachment = true;
548
+ }
549
+ }
550
+ if ('footer' in message && !!message.footer) {
551
+ interactiveMessage.footer = { text: message.footer };
552
+ }
553
+ applyContextInfoAndMentions(interactiveMessage, message);
554
+ m = { interactiveMessage };
555
+ }
556
+ else if ('buttons' in message && !!message.buttons) {
557
+ const buttonsMessage = {
558
+ buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
559
+ };
560
+ if ('text' in message) {
561
+ buttonsMessage.contentText = message.text;
562
+ buttonsMessage.headerType = ButtonType.EMPTY;
563
+ }
564
+ else {
565
+ if ('caption' in message) {
566
+ buttonsMessage.contentText = message.caption;
567
+ }
568
+ const contentType = getContentType(m);
569
+ const type = contentType.replace('Message', '').toUpperCase();
570
+ buttonsMessage.headerType = ButtonType[type];
571
+ Object.assign(buttonsMessage, m);
572
+ }
573
+ if ('footer' in message && !!message.footer) {
574
+ buttonsMessage.footerText = message.footer;
575
+ }
576
+ m = { buttonsMessage };
577
+ }
578
+ else if (hasNonNullishProperty(message, 'sections') || hasNonNullishProperty(message, 'productList')) {
579
+ const isProductList = hasNonNullishProperty(message, 'productList');
580
+ const listMessage = {
581
+ buttonText: message.buttonText,
582
+ title: message.title,
583
+ footerText: message.footer,
584
+ description: message.text ?? message.description,
585
+ listType: isProductList
586
+ ? WAProto.Message.ListMessage.ListType.PRODUCT_LIST
587
+ : (message.listType ?? WAProto.Message.ListMessage.ListType.SINGLE_SELECT)
588
+ };
589
+ if (isProductList) {
590
+ // productList: { sections: [{ title, products: [{ productId }] }], headerImage: { productId, jpegThumbnail }, businessOwnerJid }
591
+ listMessage.productListInfo = {
592
+ productSections: message.productList.sections,
593
+ headerImage: message.productList.headerImage,
594
+ businessOwnerJid: message.productList.businessOwnerJid
595
+ };
596
+ } else {
597
+ listMessage.sections = message.sections;
598
+ }
599
+ if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
600
+ listMessage.contextInfo = message.contextInfo;
601
+ }
602
+ m = { listMessage: WAProto.Message.ListMessage.create(listMessage) };
603
+ }
604
+ else if ('richResponse' in message) {
605
+ const {
606
+ text,
607
+ code,
608
+ language = 'javascript',
609
+ botJid = '867051314767696@bot',
610
+ table,
611
+ latex,
612
+ map,
613
+ html,
614
+ imageUrl,
615
+ imageUrls,
616
+ responseId,
617
+ messageSecret: richSecret
618
+ } = message.richResponse
619
+ const sections = []
620
+ if (text) {
621
+ sections.push({
622
+ view_model: {
623
+ primitive: { text, __typename: 'GenAIMarkdownTextUXPrimitive' },
624
+ __typename: 'GenAISingleLayoutViewModel'
625
+ }
626
+ })
627
+ }
628
+ if (code) {
629
+ sections.push({
630
+ view_model: {
631
+ primitive: {
632
+ language,
633
+ code_blocks: tokenizeCode(String(code)),
634
+ __typename: 'GenAICodeUXPrimitive'
635
+ },
636
+ __typename: 'GenAISingleLayoutViewModel'
637
+ }
638
+ })
639
+ }
640
+ if (table && Array.isArray(table.rows)) {
641
+ sections.push({
642
+ view_model: {
643
+ primitive: {
644
+ rows: table.rows.map(row => ({
645
+ cells: Array.isArray(row) ? row.map(c => ({ text: String(c) })) : row.cells
646
+ })),
647
+ __typename: 'GenAITableUXPrimitive'
648
+ },
649
+ __typename: 'GenAISingleLayoutViewModel'
650
+ }
651
+ })
652
+ }
653
+ if (latex) {
654
+ const expressions = Array.isArray(latex)
655
+ ? latex.map(e => (typeof e === 'string' ? { expression: e } : e))
656
+ : [{ expression: String(latex) }]
657
+ sections.push({
658
+ view_model: {
659
+ primitive: { expressions, __typename: 'GenAILatexUXPrimitive' },
660
+ __typename: 'GenAISingleLayoutViewModel'
661
+ }
662
+ })
663
+ }
664
+ if (html) {
665
+ const htmlPayload = typeof html === 'string' ? html : String(html)
666
+ sections.push({
667
+ view_model: {
668
+ primitive: {
669
+ payload: htmlPayload,
670
+ trusted_sources: [],
671
+ __typename: 'GenAIaeacdsnwHtmlPrimitive'
672
+ },
673
+ __typename: 'GenAISingleLayoutViewModel'
674
+ }
675
+ })
676
+ }
677
+ if (map) {
678
+ sections.push({
679
+ view_model: {
680
+ primitive: {
681
+ latitude: map.latitude,
682
+ longitude: map.longitude,
683
+ zoom: map.zoom,
684
+ title: map.title,
685
+ annotations: map.annotations || [],
686
+ __typename: 'GenAIMapUXPrimitive'
687
+ },
688
+ __typename: 'GenAISingleLayoutViewModel'
689
+ }
690
+ })
691
+ }
692
+ if (imageUrl) {
693
+ sections.push({
694
+ view_model: {
695
+ primitive: { url: imageUrl, __typename: 'GenAIInlineImageUXPrimitive' },
696
+ __typename: 'GenAISingleLayoutViewModel'
697
+ }
698
+ })
699
+ }
700
+ if (imageUrls && Array.isArray(imageUrls) && imageUrls.length > 0) {
701
+ sections.push({
702
+ view_model: {
703
+ primitive: {
704
+ urls: imageUrls.map(u => (typeof u === 'string' ? { url: u } : u)),
705
+ __typename: 'GenAIGridImageUXPrimitive'
706
+ },
707
+ __typename: 'GenAISingleLayoutViewModel'
708
+ }
709
+ })
710
+ }
711
+ if (!sections.length && !text) {
712
+ sections.push({
713
+ view_model: {
714
+ primitive: { text: '', __typename: 'GenAIMarkdownTextUXPrimitive' },
715
+ __typename: 'GenAISingleLayoutViewModel'
716
+ }
717
+ })
718
+ }
719
+ const unifiedData = {
720
+ response_id: responseId || randomUUID(),
721
+ sections
722
+ }
723
+ return proto.Message.fromObject({
724
+ messageContextInfo: {
725
+ deviceListMetadata: {},
726
+ deviceListMetadataVersion: 2,
727
+ messageSecret: richSecret || randomBytes(32)
728
+ },
729
+ botForwardedMessage: {
730
+ message: {
731
+ richResponseMessage: {
732
+ submessages: [],
733
+ messageType: 1,
734
+ unifiedResponse: { data: Buffer.from(JSON.stringify(unifiedData)) },
735
+ contextInfo: {
736
+ forwardingScore: 2,
737
+ isForwarded: true,
738
+ forwardedAiBotMessageInfo: { botJid },
739
+ botMessageSharingInfo: {
740
+ botEntryPointOrigin: 1,
741
+ forwardScore: 2
742
+ }
743
+ }
744
+ }
745
+ }
746
+ }
747
+ })
748
+ }
749
+ else if ('botInvoke' in message && !!message.botInvoke) {
750
+ m = {
751
+ botInvokeMessage: {
752
+ message: message.botInvoke
753
+ }
754
+ }
755
+ }
756
+ else if (hasNonNullishProperty(message, 'template')) {
757
+ const tpl = message.template
758
+ const hydratedButtons = (tpl.buttons || []).map(b => {
759
+ if ('urlButton' in b || 'callButton' in b || 'quickReplyButton' in b) {
760
+ return b
761
+ }
762
+ if (b.url) {
763
+ return { urlButton: { displayText: b.displayText, url: b.url } }
764
+ }
765
+ if (b.callButtonPhoneNumber || b.phoneNumber) {
766
+ return { callButton: { displayText: b.displayText, phoneNumber: b.phoneNumber ?? b.callButtonPhoneNumber } }
767
+ }
768
+ return { quickReplyButton: { displayText: b.displayText, id: b.id ?? b.quickReplyButtonId } }
769
+ })
770
+ const hydratedFourRowTemplate = {
771
+ hydratedContentText: tpl.text,
772
+ hydratedFooterText: tpl.footer,
773
+ hydratedButtons,
774
+ templateId: tpl.templateId
775
+ }
776
+ if (tpl.title) {
777
+ hydratedFourRowTemplate.hydratedTitleText = tpl.title
778
+ }
779
+ if (tpl.image) {
780
+ const { imageMessage } = await prepareWAMessageMedia({ image: tpl.image }, options)
781
+ hydratedFourRowTemplate.imageMessage = imageMessage
782
+ } else if (tpl.video) {
783
+ const { videoMessage } = await prepareWAMessageMedia({ video: tpl.video }, options)
784
+ hydratedFourRowTemplate.videoMessage = videoMessage
785
+ } else if (tpl.document) {
786
+ const { documentMessage } = await prepareWAMessageMedia({ document: tpl.document }, options)
787
+ hydratedFourRowTemplate.documentMessage = documentMessage
788
+ } else if (tpl.location) {
789
+ hydratedFourRowTemplate.locationMessage = WAProto.Message.LocationMessage.create(tpl.location)
790
+ }
791
+ m = {
792
+ templateMessage: {
793
+ hydratedTemplate: hydratedFourRowTemplate,
794
+ contextInfo: tpl.contextInfo
795
+ }
796
+ }
797
+ }
798
+ else if (hasNonNullishProperty(message, 'text')) {
799
+ const extContent = { text: message.text };
800
+ let urlInfo = message.linkPreview;
801
+ if (typeof urlInfo === 'undefined') {
802
+ urlInfo = await generateLinkPreviewIfRequired(message.text, options.getUrlInfo, options.logger);
803
+ }
804
+ if (urlInfo) {
805
+ extContent.matchedText = urlInfo['matched-text'];
806
+ extContent.jpegThumbnail = urlInfo.jpegThumbnail;
807
+ extContent.description = urlInfo.description;
808
+ extContent.title = urlInfo.title;
809
+ extContent.previewType = 0;
810
+ const img = urlInfo.highQualityThumbnail;
811
+ if (img) {
812
+ extContent.thumbnailDirectPath = img.directPath;
813
+ extContent.mediaKey = img.mediaKey;
814
+ extContent.mediaKeyTimestamp = img.mediaKeyTimestamp;
815
+ extContent.thumbnailWidth = img.width;
816
+ extContent.thumbnailHeight = img.height;
817
+ extContent.thumbnailSha256 = img.fileSha256;
818
+ extContent.thumbnailEncSha256 = img.fileEncSha256;
819
+ }
820
+ }
821
+ if (options.backgroundColor) {
822
+ extContent.backgroundArgb = await assertColor(options.backgroundColor);
823
+ }
824
+ if (options.font) {
825
+ extContent.font = options.font;
826
+ }
827
+ m.extendedTextMessage = extContent;
828
+ }
829
+ else if (hasNonNullishProperty(message, 'contacts')) {
830
+ const contactLen = message.contacts.contacts.length;
831
+ if (!contactLen) {
832
+ throw new Boom('require atleast 1 contact', { statusCode: 400 });
833
+ }
834
+ if (contactLen === 1) {
835
+ m.contactMessage = WAProto.Message.ContactMessage.create(message.contacts.contacts[0]);
836
+ }
837
+ else {
838
+ m.contactsArrayMessage = WAProto.Message.ContactsArrayMessage.create(message.contacts);
839
+ }
840
+ }
841
+ else if (hasNonNullishProperty(message, 'location')) {
842
+ m.locationMessage = WAProto.Message.LocationMessage.create(message.location);
843
+ }
844
+ else if (hasNonNullishProperty(message, 'liveLocation')) {
845
+ m.liveLocationMessage = WAProto.Message.LiveLocationMessage.create(message.liveLocation);
846
+ }
847
+ else if (hasNonNullishProperty(message, 'react')) {
848
+ if (!message.react.senderTimestampMs) {
849
+ message.react.senderTimestampMs = Date.now();
850
+ }
851
+ m.reactionMessage = WAProto.Message.ReactionMessage.create(message.react);
852
+ }
853
+ else if (hasNonNullishProperty(message, 'delete')) {
854
+ m.protocolMessage = {
855
+ key: message.delete,
856
+ type: WAProto.Message.ProtocolMessage.Type.REVOKE
857
+ };
858
+ }
859
+ else if (hasNonNullishProperty(message, 'forward')) {
860
+ m = generateForwardMessageContent(message.forward, message.force);
861
+ }
862
+ else if (hasNonNullishProperty(message, 'disappearingMessagesInChat')) {
863
+ const exp = typeof message.disappearingMessagesInChat === 'boolean'
864
+ ? message.disappearingMessagesInChat
865
+ ? WA_DEFAULT_EPHEMERAL
866
+ : 0
867
+ : message.disappearingMessagesInChat;
868
+ m = prepareDisappearingMessageSettingContent(exp);
869
+ }
870
+ else if (hasNonNullishProperty(message, 'groupInvite')) {
871
+ m.groupInviteMessage = {};
872
+ m.groupInviteMessage.inviteCode = message.groupInvite.inviteCode;
873
+ m.groupInviteMessage.inviteExpiration = message.groupInvite.inviteExpiration;
874
+ m.groupInviteMessage.caption = message.groupInvite.text;
875
+ m.groupInviteMessage.groupJid = message.groupInvite.jid;
876
+ m.groupInviteMessage.groupName = message.groupInvite.subject;
877
+ //TODO: use built-in interface and get disappearing mode info etc.
878
+ //TODO: cache / use store!?
879
+ if (options.getProfilePicUrl) {
880
+ const pfpUrl = await options.getProfilePicUrl(message.groupInvite.jid, 'preview');
881
+ if (pfpUrl) {
882
+ const resp = await fetch(pfpUrl, { method: 'GET', dispatcher: options?.options?.dispatcher });
883
+ if (resp.ok) {
884
+ const buf = Buffer.from(await resp.arrayBuffer());
885
+ m.groupInviteMessage.jpegThumbnail = buf;
886
+ }
887
+ }
888
+ }
889
+ }
890
+ else if (hasNonNullishProperty(message, 'pin')) {
891
+ m.pinInChatMessage = {};
892
+ m.messageContextInfo = {};
893
+ m.pinInChatMessage.key = message.pin;
894
+ m.pinInChatMessage.type = message.type;
895
+ m.pinInChatMessage.senderTimestampMs = Date.now();
896
+ m.messageContextInfo.messageAddOnDurationInSecs = message.type === 1 ? message.time || 86400 : 0;
897
+ }
898
+ else if (hasNonNullishProperty(message, 'buttonReply')) {
899
+ switch (message.type) {
900
+ case 'template':
901
+ m.templateButtonReplyMessage = {
902
+ selectedDisplayText: message.buttonReply.displayText,
903
+ selectedId: message.buttonReply.id,
904
+ selectedIndex: message.buttonReply.index
905
+ };
906
+ break;
907
+ case 'plain':
908
+ m.buttonsResponseMessage = {
909
+ selectedButtonId: message.buttonReply.id,
910
+ selectedDisplayText: message.buttonReply.displayText,
911
+ type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
912
+ };
913
+ break;
914
+ }
915
+ }
916
+ else if (hasOptionalProperty(message, 'ptv') && message.ptv) {
917
+ const { videoMessage } = await prepareWAMessageMedia({ video: message.ptv }, options);
918
+ m.ptvMessage = videoMessage;
919
+ }
920
+ else if (hasNonNullishProperty(message, 'product')) {
921
+ const { imageMessage } = await prepareWAMessageMedia({ image: message.product.productImage }, options);
922
+ m.productMessage = WAProto.Message.ProductMessage.create({
923
+ ...message,
924
+ product: {
925
+ ...message.product,
926
+ productImage: imageMessage
927
+ }
928
+ });
929
+ }
930
+ else if (hasNonNullishProperty(message, 'listReply')) {
931
+ m.listResponseMessage = { ...message.listReply };
932
+ }
933
+ else if (hasNonNullishProperty(message, 'event')) {
934
+ m.eventMessage = {};
935
+ const startTime = Math.floor(message.event.startDate.getTime() / 1000);
936
+ if (message.event.call && options.getCallLink) {
937
+ const token = await options.getCallLink(message.event.call, { startTime });
938
+ m.eventMessage.joinLink = (message.event.call === 'audio' ? CALL_AUDIO_PREFIX : CALL_VIDEO_PREFIX) + token;
939
+ }
940
+ m.messageContextInfo = {
941
+ // encKey
942
+ messageSecret: message.event.messageSecret || randomBytes(32)
943
+ };
944
+ m.eventMessage.name = message.event.name;
945
+ m.eventMessage.description = message.event.description;
946
+ m.eventMessage.startTime = startTime;
947
+ m.eventMessage.endTime = message.event.endDate ? message.event.endDate.getTime() / 1000 : undefined;
948
+ m.eventMessage.isCanceled = message.event.isCancelled ?? false;
949
+ m.eventMessage.extraGuestsAllowed = message.event.extraGuestsAllowed;
950
+ m.eventMessage.isScheduleCall = message.event.isScheduleCall ?? false;
951
+ m.eventMessage.location = message.event.location;
952
+ }
953
+ else if (hasNonNullishProperty(message, 'poll')) {
954
+ (_a = message.poll).selectableCount || (_a.selectableCount = 0);
955
+ (_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
956
+ if (!Array.isArray(message.poll.values)) {
957
+ throw new Boom('Invalid poll values', { statusCode: 400 });
958
+ }
959
+ if (message.poll.selectableCount < 0 || message.poll.selectableCount > message.poll.values.length) {
960
+ throw new Boom(`poll.selectableCount in poll should be >= 0 and <= ${message.poll.values.length}`, {
961
+ statusCode: 400
962
+ });
963
+ }
964
+ m.messageContextInfo = {
965
+ // encKey
966
+ messageSecret: message.poll.messageSecret || randomBytes(32)
967
+ };
968
+ const pollCreationMessage = {
969
+ name: message.poll.name,
970
+ selectableOptionsCount: message.poll.selectableCount,
971
+ options: message.poll.values.map(optionName => ({ optionName }))
972
+ };
973
+ if (message.poll.toAnnouncementGroup) {
974
+ // poll v2 is for community announcement groups (single select and multiple)
975
+ m.pollCreationMessageV2 = pollCreationMessage;
976
+ }
977
+ else {
978
+ if (message.poll.selectableCount === 1) {
979
+ //poll v3 is for single select polls
980
+ m.pollCreationMessageV3 = pollCreationMessage;
981
+ }
982
+ else {
983
+ // poll for multiple choice polls
984
+ m.pollCreationMessage = pollCreationMessage;
985
+ }
986
+ }
987
+ }
988
+ else if (hasNonNullishProperty(message, 'pollUpdate')) {
989
+ const { pollMessage: pollMsg, votes } = message.pollUpdate;
990
+ if (!pollMsg?.key?.id) {
991
+ throw new Boom('pollUpdate.pollMessage (with .key.id) is required to vote', { statusCode: 400 });
992
+ }
993
+ if (!Array.isArray(votes)) {
994
+ throw new Boom('pollUpdate.votes must be an array of selected option names', { statusCode: 400 });
995
+ }
996
+ const pollContent = normalizeMessageContent(pollMsg.message);
997
+ const pollEncKey = pollContent?.messageContextInfo?.messageSecret;
998
+ if (!pollEncKey) {
999
+ throw new Boom('Poll encryption key (messageContextInfo.messageSecret) not found on pollMessage', { statusCode: 400 });
1000
+ }
1001
+ const pollMsgId = pollMsg.key.id;
1002
+ const pollCreatorJid = jidNormalizedUser(getKeyAuthor(pollMsg.key, options.userJid));
1003
+ const voterJid = jidNormalizedUser(options.userJid);
1004
+ const pollVoteMessage = {
1005
+ selectedOptions: votes.map(v => sha256(Buffer.from(v))),
1006
+ senderTimestampMs: Date.now()
1007
+ };
1008
+ const encoded = WAProto.Message.PollVoteMessage.encode(pollVoteMessage).finish();
1009
+ const sign = Buffer.concat([
1010
+ Buffer.from(pollMsgId),
1011
+ Buffer.from(pollCreatorJid),
1012
+ Buffer.from(voterJid),
1013
+ Buffer.from('Poll Vote'),
1014
+ new Uint8Array([1])
1015
+ ]);
1016
+ const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256');
1017
+ const encKey = hmacSign(sign, key0, 'sha256');
1018
+ const encIv = randomBytes(12);
1019
+ const aad = Buffer.from(`${pollMsgId}\u0000${voterJid}`);
1020
+ const encPayload = aesEncryptGCM(encoded, encKey, encIv, aad);
1021
+ m.pollUpdateMessage = {
1022
+ pollCreationMessageKey: pollMsg.key,
1023
+ vote: { encPayload, encIv },
1024
+ senderTimestampMs: Date.now()
1025
+ };
1026
+ }
1027
+ else if (hasNonNullishProperty(message, 'album')) {
1028
+ m.albumMessage = {
1029
+ expectedImageCount: message.album.expectedImageCount,
1030
+ expectedVideoCount: message.album.expectedVideoCount
1031
+ };
1032
+ }
1033
+ else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
1034
+ m.protocolMessage = {
1035
+ type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
1036
+ };
1037
+ }
1038
+ else if (hasNonNullishProperty(message, 'requestPhoneNumber')) {
1039
+ m.requestPhoneNumberMessage = {};
1040
+ }
1041
+ else if (hasNonNullishProperty(message, 'declinePaymentRequest')) {
1042
+ m.declinePaymentRequestMessage = WAProto.Message.DeclinePaymentRequestMessage.create({
1043
+ key: message.declinePaymentRequest
1044
+ });
1045
+ }
1046
+ else if (hasNonNullishProperty(message, 'cancelPaymentRequest')) {
1047
+ m.cancelPaymentRequestMessage = WAProto.Message.CancelPaymentRequestMessage.create({
1048
+ key: message.cancelPaymentRequest
1049
+ });
1050
+ }
1051
+ else if (hasNonNullishProperty(message, 'sendPayment')) {
1052
+ const sp = message.sendPayment;
1053
+ if (!sp.requestMessageKey) {
1054
+ throw new Boom('sendPayment.requestMessageKey is required', { statusCode: 400 });
1055
+ }
1056
+ let noteMessage;
1057
+ if (sp.note) {
1058
+ noteMessage = typeof sp.note === 'string'
1059
+ ? { extendedTextMessage: { text: sp.note } }
1060
+ : sp.note;
1061
+ }
1062
+ m.sendPaymentMessage = WAProto.Message.SendPaymentMessage.create({
1063
+ requestMessageKey: sp.requestMessageKey,
1064
+ noteMessage,
1065
+ background: sp.background,
1066
+ transactionData: sp.transactionData
1067
+ });
1068
+ }
1069
+ else if (hasNonNullishProperty(message, 'interactiveResponse')) {
1070
+ const ir = message.interactiveResponse;
1071
+ m.interactiveResponseMessage = WAProto.Message.InteractiveResponseMessage.create({
1072
+ body: ir.text ? { text: ir.text } : undefined,
1073
+ nativeFlowResponseMessage: {
1074
+ name: ir.name,
1075
+ paramsJson: typeof ir.params === 'string' ? ir.params : JSON.stringify(ir.params ?? {}),
1076
+ version: ir.version ?? 3
1077
+ }
1078
+ });
1079
+ }
1080
+ else if (hasNonNullishProperty(message, 'limitSharing')) {
1081
+ m.protocolMessage = {
1082
+ type: proto.Message.ProtocolMessage.Type.LIMIT_SHARING,
1083
+ limitSharing: {
1084
+ sharingLimited: message.limitSharing === true,
1085
+ trigger: 1,
1086
+ limitSharingSettingTimestamp: Date.now(),
1087
+ initiatedByMe: true
1088
+ }
1089
+ };
1090
+ }
1091
+ else if (hasNonNullishProperty(message, 'botFeedback')) {
1092
+ // Meta AI bot family — same caveat as richResponseMessage: this proto shape is
1093
+ // only known to render/validate when sent from a verified bot account.
1094
+ const bf = message.botFeedback;
1095
+ m.protocolMessage = {
1096
+ type: proto.Message.ProtocolMessage.Type.BOT_FEEDBACK_MESSAGE,
1097
+ botFeedbackMessage: {
1098
+ messageKey: bf.messageKey,
1099
+ kind: bf.kind, // e.g. proto.BotFeedbackMessage.BotFeedbackKind.BOT_FEEDBACK_POSITIVE
1100
+ text: bf.text
1101
+ }
1102
+ };
1103
+ }
1104
+ else if (hasNonNullishProperty(message, 'botOnboarding')) {
1105
+ m.protocolMessage = {
1106
+ type: proto.Message.ProtocolMessage.Type.BOT_MEMU_ONBOARDING_MESSAGE
1107
+ };
1108
+ }
1109
+ else if (hasNonNullishProperty(message, 'stopGeneration')) {
1110
+ m.protocolMessage = {
1111
+ type: proto.Message.ProtocolMessage.Type.STOP_GENERATION_MESSAGE,
1112
+ key: message.stopGeneration
1113
+ };
1114
+ }
1115
+ else if (hasNonNullishProperty(message, 'requestWelcome')) {
1116
+ const rw = message.requestWelcome || {};
1117
+ m.protocolMessage = {
1118
+ type: proto.Message.ProtocolMessage.Type.REQUEST_WELCOME_MESSAGE,
1119
+ requestWelcomeMessageMetadata: {
1120
+ welcomeTrigger: rw.welcomeTrigger ?? proto.Message.ProtocolMessage.RequestWelcomeMessageMetadata.WelcomeTrigger.CHAT_OPEN,
1121
+ localChatState: rw.localChatState,
1122
+ botAgentMetadata: rw.botAgentMetadata
1123
+ }
1124
+ };
1125
+ }
1126
+ else if (hasNonNullishProperty(message, 'aiQueryFanout')) {
1127
+ const aq = message.aiQueryFanout;
1128
+ m.protocolMessage = {
1129
+ type: proto.Message.ProtocolMessage.Type.AI_QUERY_FANOUT,
1130
+ aiQueryFanout: {
1131
+ messageKey: aq.messageKey,
1132
+ message: aq.message,
1133
+ timestamp: aq.timestamp ?? Date.now()
1134
+ }
1135
+ };
1136
+ }
1137
+ else if (hasNonNullishProperty(message, 'aiMediaCollection')) {
1138
+ const amc = message.aiMediaCollection;
1139
+ m.protocolMessage = {
1140
+ type: proto.Message.ProtocolMessage.Type.AI_MEDIA_COLLECTION_MESSAGE,
1141
+ aiMediaCollectionMessage: {
1142
+ collectionId: amc.collectionId,
1143
+ expectedMediaCount: amc.expectedMediaCount,
1144
+ hasGlobalCaption: amc.hasGlobalCaption
1145
+ }
1146
+ };
1147
+ }
1148
+ else if (hasNonNullishProperty(message, 'reminder')) {
1149
+ m.protocolMessage = {
1150
+ type: proto.Message.ProtocolMessage.Type.REMINDER_MESSAGE,
1151
+ key: message.reminder
1152
+ };
1153
+ }
1154
+ else if (hasNonNullishProperty(message, 'comment')) {
1155
+ // comment.message: AnyMessageContent-ish already-built proto message content to attach as the comment
1156
+ // comment.targetMessageKey: the MessageKey of the status/newsletter post being commented on
1157
+ m.commentMessage = {
1158
+ message: message.comment.message,
1159
+ targetMessageKey: message.comment.targetMessageKey
1160
+ };
1161
+ }
1162
+ else if (hasNonNullishProperty(message, 'scheduledCall')) {
1163
+ const sc = message.scheduledCall;
1164
+ m.scheduledCallCreationMessage = {
1165
+ scheduledTimestampMs: sc.scheduledTimestampMs,
1166
+ callType: sc.callType ?? WAProto.Message.ScheduledCallCreationMessage.CallType.VOICE,
1167
+ title: sc.title
1168
+ };
1169
+ }
1170
+ else if (hasNonNullishProperty(message, 'invoice')) {
1171
+ m.invoiceMessage = WAProto.Message.InvoiceMessage.create(message.invoice);
1172
+ }
1173
+ else if (hasNonNullishProperty(message, 'keepInChat')) {
1174
+ const kc = message.keepInChat;
1175
+ m.keepInChatMessage = {
1176
+ key: kc.key,
1177
+ keepType: kc.keepType ?? proto.KeepType.KEEP_FOR_ALL,
1178
+ timestampMs: kc.timestampMs ?? Date.now()
1179
+ };
1180
+ }
1181
+ else {
1182
+ m = await prepareWAMessageMedia(message, options);
1183
+ }
1184
+ if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
1185
+ m = { viewOnceMessage: { message: m } };
1186
+ }
1187
+ if ((hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
1188
+ (hasOptionalProperty(message, 'mentionAll') && message.mentionAll)) {
1189
+ const messageType = Object.keys(m)[0];
1190
+ const key = m[messageType];
1191
+ if (key && 'contextInfo' in key) {
1192
+ key.contextInfo = key.contextInfo || {};
1193
+ if (message.mentions?.length) {
1194
+ key.contextInfo.mentionedJid = message.mentions;
1195
+ }
1196
+ if (message.mentionAll) {
1197
+ key.contextInfo.nonJidMentions = 1;
1198
+ }
1199
+ }
1200
+ else if (key) {
1201
+ key.contextInfo = {
1202
+ mentionedJid: message.mentions,
1203
+ nonJidMentions: message.mentionAll ? 1 : 0
1204
+ };
1205
+ }
1206
+ }
1207
+ if (hasOptionalProperty(message, 'edit')) {
1208
+ m = {
1209
+ protocolMessage: {
1210
+ key: message.edit,
1211
+ editedMessage: m,
1212
+ timestampMs: Date.now(),
1213
+ type: WAProto.Message.ProtocolMessage.Type.MESSAGE_EDIT
1214
+ }
1215
+ };
1216
+ }
1217
+ if (hasOptionalProperty(message, 'contextInfo') && !!message.contextInfo) {
1218
+ const messageType = Object.keys(m)[0];
1219
+ const key = m[messageType];
1220
+ if ('contextInfo' in key && !!key.contextInfo) {
1221
+ key.contextInfo = { ...key.contextInfo, ...message.contextInfo };
1222
+ }
1223
+ else if (key) {
1224
+ key.contextInfo = message.contextInfo;
1225
+ }
1226
+ }
1227
+ if (hasOptionalProperty(message, 'albumParentKey') && !!message.albumParentKey) {
1228
+ m.messageContextInfo = {
1229
+ ...m.messageContextInfo,
1230
+ messageAssociation: {
1231
+ associationType: WAProto.MessageAssociation.AssociationType.MEDIA_ALBUM,
1232
+ parentMessageKey: message.albumParentKey
1233
+ }
1234
+ };
1235
+ }
1236
+ if (shouldIncludeReportingToken(m)) {
1237
+ m.messageContextInfo = m.messageContextInfo || {};
1238
+ if (!m.messageContextInfo.messageSecret) {
1239
+ m.messageContextInfo.messageSecret = randomBytes(32);
1240
+ }
1241
+ }
1242
+ return WAProto.Message.create(m);
1243
+ };
1244
+ export const generateWAMessageFromContent = (jid, message, options) => {
1245
+ // set timestamp to now
1246
+ // if not specified
1247
+ if (!options.timestamp) {
1248
+ options.timestamp = new Date();
1249
+ }
1250
+ const innerMessage = normalizeMessageContent(message);
1251
+ const key = getContentType(innerMessage);
1252
+ const timestamp = unixTimestampSeconds(options.timestamp);
1253
+ const { quoted, userJid } = options;
1254
+ if (quoted && !isJidNewsletter(jid)) {
1255
+ const participant = quoted.key.fromMe
1256
+ ? userJid // TODO: Add support for LIDs
1257
+ : quoted.participant || quoted.key.participant || quoted.key.remoteJid;
1258
+ let quotedMsg = normalizeMessageContent(quoted.message);
1259
+ const msgType = getContentType(quotedMsg);
1260
+ // strip any redundant properties
1261
+ quotedMsg = proto.Message.create({ [msgType]: quotedMsg[msgType] });
1262
+ const quotedContent = quotedMsg[msgType];
1263
+ if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
1264
+ delete quotedContent.contextInfo;
1265
+ }
1266
+ const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
1267
+ contextInfo.participant = jidNormalizedUser(participant);
1268
+ contextInfo.stanzaId = quoted.key.id;
1269
+ contextInfo.quotedMessage = quotedMsg;
1270
+ // if a participant is quoted, then it must be a group
1271
+ // hence, remoteJid of group must also be entered
1272
+ if (jid !== quoted.key.remoteJid) {
1273
+ contextInfo.remoteJid = quoted.key.remoteJid;
1274
+ }
1275
+ if (contextInfo && innerMessage[key]) {
1276
+ /* @ts-ignore */
1277
+ innerMessage[key].contextInfo = contextInfo;
1278
+ }
1279
+ }
1280
+ if (
1281
+ // if we want to send a disappearing message
1282
+ !!options?.ephemeralExpiration &&
1283
+ // and it's not a protocol message -- delete, toggle disappear message
1284
+ key !== 'protocolMessage' &&
1285
+ // already not converted to disappearing message
1286
+ key !== 'ephemeralMessage' &&
1287
+ // newsletters don't support ephemeral messages
1288
+ !isJidNewsletter(jid)) {
1289
+ /* @ts-ignore */
1290
+ innerMessage[key].contextInfo = {
1291
+ ...(innerMessage[key].contextInfo || {}),
1292
+ expiration: options.ephemeralExpiration || WA_DEFAULT_EPHEMERAL
1293
+ //ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts?.toString()
1294
+ };
1295
+ }
1296
+ message = WAProto.Message.create(message);
1297
+ const messageJSON = {
1298
+ key: {
1299
+ remoteJid: jid,
1300
+ fromMe: true,
1301
+ id: options?.messageId || generateMessageIDV2()
1302
+ },
1303
+ message: message,
1304
+ messageTimestamp: timestamp,
1305
+ messageStubParameters: [],
1306
+ participant: isJidGroup(jid) || isJidStatusBroadcast(jid) ? userJid : undefined, // TODO: Add support for LIDs
1307
+ status: WAMessageStatus.PENDING
1308
+ };
1309
+ return WAProto.WebMessageInfo.fromObject(messageJSON);
1310
+ };
1311
+ export const generateWAMessage = async (jid, content, options) => {
1312
+ // ensure msg ID is with every log
1313
+ options.logger = options?.logger?.child({ msgId: options.messageId });
1314
+ // Pass jid in the options to generateWAMessageContent
1315
+ return generateWAMessageFromContent(jid, await generateWAMessageContent(content, { ...options, jid }), options);
1316
+ };
1317
+ /** Get the key to access the true type of content */
1318
+ export const getContentType = (content) => {
1319
+ if (content) {
1320
+ const keys = Object.keys(content);
1321
+ const key = keys.find(k => (k === 'conversation' || k.includes('Message')) && k !== 'senderKeyDistributionMessage');
1322
+ return key;
1323
+ }
1324
+ };
1325
+ /**
1326
+ * Normalizes ephemeral, view once messages to regular message content
1327
+ * Eg. image messages in ephemeral messages, in view once messages etc.
1328
+ * @param content
1329
+ * @returns
1330
+ */
1331
+ export const normalizeMessageContent = (content) => {
1332
+ if (!content) {
1333
+ return undefined;
1334
+ }
1335
+ // set max iterations to prevent an infinite loop
1336
+ for (let i = 0; i < 5; i++) {
1337
+ const inner = getFutureProofMessage(content);
1338
+ if (!inner) {
1339
+ break;
1340
+ }
1341
+ content = inner.message;
1342
+ }
1343
+ return content;
1344
+ function getFutureProofMessage(message) {
1345
+ return (message?.ephemeralMessage ||
1346
+ message?.viewOnceMessage ||
1347
+ message?.documentWithCaptionMessage ||
1348
+ message?.viewOnceMessageV2 ||
1349
+ message?.viewOnceMessageV2Extension ||
1350
+ message?.editedMessage ||
1351
+ message?.associatedChildMessage ||
1352
+ message?.groupStatusMessage ||
1353
+ message?.groupStatusMessageV2);
1354
+ }
1355
+ };
1356
+ /**
1357
+ * Extract the true message content from a message
1358
+ * Eg. extracts the inner message from a disappearing message/view once message
1359
+ */
1360
+ export const extractMessageContent = (content) => {
1361
+ const extractFromTemplateMessage = (msg) => {
1362
+ if (msg.imageMessage) {
1363
+ return { imageMessage: msg.imageMessage };
1364
+ }
1365
+ else if (msg.documentMessage) {
1366
+ return { documentMessage: msg.documentMessage };
1367
+ }
1368
+ else if (msg.videoMessage) {
1369
+ return { videoMessage: msg.videoMessage };
1370
+ }
1371
+ else if (msg.locationMessage) {
1372
+ return { locationMessage: msg.locationMessage };
1373
+ }
1374
+ else {
1375
+ return {
1376
+ conversation: 'contentText' in msg ? msg.contentText : 'hydratedContentText' in msg ? msg.hydratedContentText : ''
1377
+ };
1378
+ }
1379
+ };
1380
+ content = normalizeMessageContent(content);
1381
+ if (content?.buttonsMessage) {
1382
+ return extractFromTemplateMessage(content.buttonsMessage);
1383
+ }
1384
+ if (content?.templateMessage?.hydratedFourRowTemplate) {
1385
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedFourRowTemplate);
1386
+ }
1387
+ if (content?.templateMessage?.hydratedTemplate) {
1388
+ return extractFromTemplateMessage(content?.templateMessage?.hydratedTemplate);
1389
+ }
1390
+ if (content?.templateMessage?.fourRowTemplate) {
1391
+ return extractFromTemplateMessage(content?.templateMessage?.fourRowTemplate);
1392
+ }
1393
+ return content;
1394
+ };
1395
+ /**
1396
+ * Returns the device predicted by message ID
1397
+ */
1398
+ export const getDevice = (id) => /^3A.{18}$/.test(id)
1399
+ ? 'ios'
1400
+ : /^3E.{20}$/.test(id)
1401
+ ? 'web'
1402
+ : /^(.{21}|.{32})$/.test(id)
1403
+ ? 'android'
1404
+ : /^(3F|.{18}$)/.test(id)
1405
+ ? 'desktop'
1406
+ : 'wa bot';
1407
+ /** Upserts a receipt in the message */
1408
+ export const updateMessageWithReceipt = (msg, receipt) => {
1409
+ msg.userReceipt = msg.userReceipt || [];
1410
+ const recp = msg.userReceipt.find(m => m.userJid === receipt.userJid);
1411
+ if (recp) {
1412
+ Object.assign(recp, receipt);
1413
+ }
1414
+ else {
1415
+ msg.userReceipt.push(receipt);
1416
+ }
1417
+ };
1418
+ /** Update the message with a new reaction */
1419
+ export const updateMessageWithReaction = (msg, reaction) => {
1420
+ const authorID = getKeyAuthor(reaction.key);
1421
+ const reactions = (msg.reactions || []).filter(r => getKeyAuthor(r.key) !== authorID);
1422
+ reaction.text = reaction.text || '';
1423
+ reactions.push(reaction);
1424
+ msg.reactions = reactions;
1425
+ };
1426
+ /** Update the message with a new poll update */
1427
+ export const updateMessageWithPollUpdate = (msg, update) => {
1428
+ const authorID = getKeyAuthor(update.pollUpdateMessageKey);
1429
+ const reactions = (msg.pollUpdates || []).filter(r => getKeyAuthor(r.pollUpdateMessageKey) !== authorID);
1430
+ if (update.vote?.selectedOptions?.length) {
1431
+ reactions.push(update);
1432
+ }
1433
+ msg.pollUpdates = reactions;
1434
+ };
1435
+ /** Update the message with a new event response */
1436
+ export const updateMessageWithEventResponse = (msg, update) => {
1437
+ const authorID = getKeyAuthor(update.eventResponseMessageKey);
1438
+ const responses = (msg.eventResponses || []).filter(r => getKeyAuthor(r.eventResponseMessageKey) !== authorID);
1439
+ responses.push(update);
1440
+ msg.eventResponses = responses;
1441
+ };
1442
+ /**
1443
+ * Aggregates all poll updates in a poll.
1444
+ * @param msg the poll creation message
1445
+ * @param meId your jid
1446
+ * @returns A list of options & their voters
1447
+ */
1448
+ export function getAggregateVotesInPollMessage({ message, pollUpdates }, meId) {
1449
+ const opts = message?.pollCreationMessage?.options ||
1450
+ message?.pollCreationMessageV2?.options ||
1451
+ message?.pollCreationMessageV3?.options ||
1452
+ [];
1453
+ const voteHashMap = opts.reduce((acc, opt) => {
1454
+ const hash = sha256(Buffer.from(opt.optionName || '')).toString();
1455
+ acc[hash] = {
1456
+ name: opt.optionName || '',
1457
+ voters: []
1458
+ };
1459
+ return acc;
1460
+ }, {});
1461
+ for (const update of pollUpdates || []) {
1462
+ const { vote } = update;
1463
+ if (!vote) {
1464
+ continue;
1465
+ }
1466
+ for (const option of vote.selectedOptions || []) {
1467
+ const hash = option.toString();
1468
+ let data = voteHashMap[hash];
1469
+ if (!data) {
1470
+ voteHashMap[hash] = {
1471
+ name: 'Unknown',
1472
+ voters: []
1473
+ };
1474
+ data = voteHashMap[hash];
1475
+ }
1476
+ voteHashMap[hash].voters.push(getKeyAuthor(update.pollUpdateMessageKey, meId));
1477
+ }
1478
+ }
1479
+ return Object.values(voteHashMap);
1480
+ }
1481
+ /**
1482
+ * Aggregates all event responses in an event message.
1483
+ * @param msg the event creation message
1484
+ * @param meId your jid
1485
+ * @returns A list of response types & their responders
1486
+ */
1487
+ export function getAggregateResponsesInEventMessage({ eventResponses }, meId) {
1488
+ const responseTypes = ['GOING', 'NOT_GOING', 'MAYBE'];
1489
+ const responseMap = {};
1490
+ for (const type of responseTypes) {
1491
+ responseMap[type] = {
1492
+ response: type,
1493
+ responders: []
1494
+ };
1495
+ }
1496
+ for (const update of eventResponses || []) {
1497
+ const responseType = update.eventResponse || 'UNKNOWN';
1498
+ if (responseType !== 'UNKNOWN' && responseMap[responseType]) {
1499
+ responseMap[responseType].responders.push(getKeyAuthor(update.eventResponseMessageKey, meId));
1500
+ }
1501
+ }
1502
+ return Object.values(responseMap);
1503
+ }
1504
+ /** Given a list of message keys, aggregates them by chat & sender. Useful for sending read receipts in bulk */
1505
+ export const aggregateMessageKeysNotFromMe = (keys) => {
1506
+ const keyMap = {};
1507
+ for (const { remoteJid, id, participant, fromMe } of keys) {
1508
+ if (!fromMe) {
1509
+ const uqKey = `${remoteJid}:${participant || ''}`;
1510
+ if (!keyMap[uqKey]) {
1511
+ keyMap[uqKey] = {
1512
+ jid: remoteJid,
1513
+ participant: participant,
1514
+ messageIds: []
1515
+ };
1516
+ }
1517
+ keyMap[uqKey].messageIds.push(id);
1518
+ }
1519
+ }
1520
+ return Object.values(keyMap);
1521
+ };
1522
+ const REUPLOAD_REQUIRED_STATUS = [410, 404];
1523
+ /**
1524
+ * Downloads the given message. Throws an error if it's not a media message
1525
+ */
1526
+ export const downloadMediaMessage = async (message, type, options, ctx) => {
1527
+ const result = await downloadMsg().catch(async (error) => {
1528
+ if (ctx &&
1529
+ typeof error?.status === 'number' && // treat errors with status as HTTP failures requiring reupload
1530
+ REUPLOAD_REQUIRED_STATUS.includes(error.status)) {
1531
+ ctx.logger.info({ key: message.key }, 'sending reupload media request...');
1532
+ // request reupload
1533
+ message = await ctx.reuploadRequest(message);
1534
+ const result = await downloadMsg();
1535
+ return result;
1536
+ }
1537
+ throw error;
1538
+ });
1539
+ return result;
1540
+ async function downloadMsg() {
1541
+ const mContent = extractMessageContent(message.message);
1542
+ if (!mContent) {
1543
+ throw new Boom('No message present', { statusCode: 400, data: message });
1544
+ }
1545
+ const contentType = getContentType(mContent);
1546
+ let mediaType = contentType?.replace('Message', '');
1547
+ const media = mContent[contentType];
1548
+ if (!media || typeof media !== 'object' || (!('url' in media) && !('thumbnailDirectPath' in media))) {
1549
+ throw new Boom(`"${contentType}" message is not a media message`);
1550
+ }
1551
+ let download;
1552
+ if ('thumbnailDirectPath' in media && !('url' in media)) {
1553
+ download = {
1554
+ directPath: media.thumbnailDirectPath,
1555
+ mediaKey: media.mediaKey
1556
+ };
1557
+ mediaType = 'thumbnail-link';
1558
+ }
1559
+ else {
1560
+ download = media;
1561
+ }
1562
+ const stream = await downloadContentFromMessage(download, mediaType, options);
1563
+ if (type === 'buffer') {
1564
+ const bufferArray = [];
1565
+ for await (const chunk of stream) {
1566
+ bufferArray.push(chunk);
1567
+ }
1568
+ return Buffer.concat(bufferArray);
1569
+ }
1570
+ return stream;
1571
+ }
1572
+ };
1573
+ /** Checks whether the given message is a media message; if it is returns the inner content */
1574
+ export const assertMediaContent = (content) => {
1575
+ content = extractMessageContent(content);
1576
+ const mediaContent = content?.documentMessage ||
1577
+ content?.imageMessage ||
1578
+ content?.videoMessage ||
1579
+ content?.audioMessage ||
1580
+ content?.stickerMessage;
1581
+ if (!mediaContent) {
1582
+ throw new Boom('given message is not a media message', { statusCode: 400, data: content });
1583
+ }
1584
+ return mediaContent;
1585
+ };
1586
+ //# sourceMappingURL=messages.js.map