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