@zervonex/baileys 3.0.1 → 3.0.3
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/README.md +3204 -3203
- package/WAProto/fetcher.js +1 -1
- package/WAProto/index.js +87 -87
- package/lib/Builders/AIRich.js +642 -589
- package/lib/Builders/builders.js +5 -4
- package/lib/Defaults/index.js +2 -1
- package/lib/Socket/index.js +14 -14
- package/lib/Socket/messages-recv.js +980 -903
- package/lib/Socket/messages-send.js +707 -692
- package/lib/Socket/newsletter.js +781 -751
- package/lib/Socket/socket.js +0 -1
- package/lib/Socket/{nexus-handler.js → zervonex-handler.js} +54 -54
- package/lib/Utils/chat-utils.js +1 -0
- package/lib/Utils/index.js +38 -39
- package/lib/Utils/messages.js +30 -20
- package/lib/Utils/spam-report-utils.js +134 -141
- package/lib/Utils/tc-token-utils.js +125 -73
- package/lib/index.js +51 -52
- package/package.json +11 -7
- package/lib/Utils/key-store.js +0 -3
|
@@ -1,904 +1,981 @@
|
|
|
1
|
-
import NodeCache from '@cacheable/node-cache'
|
|
2
|
-
import { Boom } from '@hapi/boom'
|
|
3
|
-
import { randomBytes } from 'crypto'
|
|
4
|
-
import { proto } from '../../WAProto/index.js'
|
|
5
|
-
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js'
|
|
6
|
-
import { WAMessageStatus, WAMessageStubType } from '../Types/index.js'
|
|
7
|
-
import {
|
|
8
|
-
aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode,
|
|
9
|
-
decodeMessageNode, makeMutex, decryptMessageNode, delay, derivePairingCodeKey,
|
|
10
|
-
encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext,
|
|
11
|
-
extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg,
|
|
12
|
-
getNextPreKeys, getStatusFromReceiptType, extractMessageText,
|
|
13
|
-
handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS,
|
|
14
|
-
NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds,
|
|
15
|
-
xmppPreKey, xmppSignedPreKey, ACCOUNT_RESTRICTED_TEXT, buildMergedTcTokenIndexWrite,
|
|
16
|
-
isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid,
|
|
17
|
-
storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY, decryptMsmsgBotMessage,
|
|
18
|
-
decodeDecryptedMsmsgMessage, extractReportingInfoFromStanza, getContentType,
|
|
19
|
-
} from '../Utils/index.js'
|
|
20
|
-
import {
|
|
21
|
-
areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild,
|
|
22
|
-
getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString,
|
|
23
|
-
getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast,
|
|
24
|
-
isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET, jidEncode,
|
|
25
|
-
} from '../WABinary/index.js'
|
|
26
|
-
import { extractGroupMetadata } from './groups.js'
|
|
27
|
-
import { makeMessagesSocket } from './messages-send.js'
|
|
28
|
-
|
|
29
|
-
// ─── Pure stanza builders ─────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export const buildGroupCallStanza = ({ id, to, callId, callCreator, offerContent }) => ({
|
|
32
|
-
tag: 'call',
|
|
33
|
-
attrs: { id, to },
|
|
34
|
-
content: [{ tag: 'offer', attrs: { 'call-id': callId, 'call-creator': callCreator, type: 'group', 'group-jid': to }, content: offerContent }]
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
export const buildGroupCallCancelStanza = ({ to, callId, callCreator }) => ({
|
|
38
|
-
tag: 'call',
|
|
39
|
-
attrs: { from: callCreator, to },
|
|
40
|
-
content: [{ tag: 'terminate', attrs: { 'call-id': callId, 'call-creator': callCreator, reason: 'user-terminated', count: '0' }, content: undefined }]
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
logger.
|
|
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
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
case '
|
|
447
|
-
case '
|
|
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
|
-
const
|
|
600
|
-
|
|
601
|
-
const
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
if (
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1
|
+
import NodeCache from '@cacheable/node-cache'
|
|
2
|
+
import { Boom } from '@hapi/boom'
|
|
3
|
+
import { randomBytes } from 'crypto'
|
|
4
|
+
import { proto } from '../../WAProto/index.js'
|
|
5
|
+
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_AGE_SECONDS, STATUS_EXPIRY_SECONDS } from '../Defaults/index.js'
|
|
6
|
+
import { ReachoutTimelockEnforcementType, WAMessageStatus, WAMessageStubType } from '../Types/index.js'
|
|
7
|
+
import {
|
|
8
|
+
aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode,
|
|
9
|
+
decodeMessageNode, makeMutex, decryptMessageNode, delay, derivePairingCodeKey,
|
|
10
|
+
encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext,
|
|
11
|
+
extractE2ESessionFromRetryReceipt, getCallStatusFromNode, getHistoryMsg,
|
|
12
|
+
getNextPreKeys, getStatusFromReceiptType, extractMessageText,
|
|
13
|
+
handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS,
|
|
14
|
+
NO_MESSAGE_FOUND_ERROR_TEXT, SERVER_ERROR_CODES, toNumber, unixTimestampSeconds,
|
|
15
|
+
xmppPreKey, xmppSignedPreKey, ACCOUNT_RESTRICTED_TEXT, buildMergedTcTokenIndexWrite,
|
|
16
|
+
isTcTokenExpired, readTcTokenIndex, resolveTcTokenJid, resolveIssuanceJid,
|
|
17
|
+
storeTcTokensFromIqResult, TC_TOKEN_INDEX_KEY, decryptMsmsgBotMessage,
|
|
18
|
+
decodeDecryptedMsmsgMessage, extractReportingInfoFromStanza, getContentType,
|
|
19
|
+
} from '../Utils/index.js'
|
|
20
|
+
import {
|
|
21
|
+
areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild,
|
|
22
|
+
getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString,
|
|
23
|
+
getBinaryNodeChildUInt, isJidGroup, isJidNewsletter, isJidStatusBroadcast,
|
|
24
|
+
isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET, jidEncode,
|
|
25
|
+
} from '../WABinary/index.js'
|
|
26
|
+
import { extractGroupMetadata } from './groups.js'
|
|
27
|
+
import { makeMessagesSocket } from './messages-send.js'
|
|
28
|
+
|
|
29
|
+
// ─── Pure stanza builders ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const buildGroupCallStanza = ({ id, to, callId, callCreator, offerContent }) => ({
|
|
32
|
+
tag: 'call',
|
|
33
|
+
attrs: { id, to },
|
|
34
|
+
content: [{ tag: 'offer', attrs: { 'call-id': callId, 'call-creator': callCreator, type: 'group', 'group-jid': to }, content: offerContent }]
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const buildGroupCallCancelStanza = ({ to, callId, callCreator }) => ({
|
|
38
|
+
tag: 'call',
|
|
39
|
+
attrs: { from: callCreator, to },
|
|
40
|
+
content: [{ tag: 'terminate', attrs: { 'call-id': callId, 'call-creator': callCreator, reason: 'user-terminated', count: '0' }, content: undefined }]
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const ENFORCEMENT_TYPE_VALUES = new Set(Object.values(ReachoutTimelockEnforcementType))
|
|
44
|
+
const isValidEnforcementType = (value) => typeof value === 'string' && ENFORCEMENT_TYPE_VALUES.has(value)
|
|
45
|
+
|
|
46
|
+
// ─── Socket ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export const makeMessagesRecvSocket = (config) => {
|
|
49
|
+
const { logger, retryRequestDelayMs, maxMsgRetryCount, getMessage, shouldIgnoreJid, enableAutoSessionRecreation } = config
|
|
50
|
+
const sock = makeMessagesSocket(config)
|
|
51
|
+
const { ev, authState, ws, processingMutex, signalRepository, query, upsertMessage, resyncAppState, onUnexpectedError, assertSessions, sendNode, relayMessage, sendReceipt, uploadPreKeys, sendPeerDataOperationMessage, messageRetryManager, registerSocketEndHandler, issuePrivacyTokens, fetchAccountReachoutTimelock, placeholderResendCache: sockPlaceholderResendCache } = sock
|
|
52
|
+
|
|
53
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping)
|
|
54
|
+
const { messageMutex, notificationMutex, receiptMutex: _receiptMutex, placeholderResendCache } = sock
|
|
55
|
+
const retryMutex = makeMutex()
|
|
56
|
+
const msgRetryCache = config.msgRetryCounterCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.MSG_RETRY, useClones: false })
|
|
57
|
+
const callOfferCache = config.callOfferCache || new NodeCache({ stdTTL: DEFAULT_CACHE_TTLS.CALL_OFFER, useClones: false })
|
|
58
|
+
const identityAssertDebounce = new NodeCache({ stdTTL: 5, useClones: false })
|
|
59
|
+
const inFlightPreKeyLow = new Set()
|
|
60
|
+
const inFlight463Recoveries = new Set()
|
|
61
|
+
|
|
62
|
+
let sendActiveReceipts = false
|
|
63
|
+
|
|
64
|
+
// ── PDO helpers ───────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
const fetchMessageHistory = async (count, oldestMsgKey, oldestMsgTimestamp) => {
|
|
67
|
+
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
68
|
+
return sendPeerDataOperationMessage({ historySyncOnDemandRequest: { chatJid: oldestMsgKey.remoteJid, oldestMsgFromMe: oldestMsgKey.fromMe, oldestMsgId: oldestMsgKey.id, oldestMsgTimestampMs: oldestMsgTimestamp, onDemandMsgCount: count }, peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.HISTORY_SYNC_ON_DEMAND })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const requestPlaceholderResend = async (messageKey, msgData) => {
|
|
72
|
+
if (!authState.creds.me?.id) throw new Boom('Not authenticated')
|
|
73
|
+
if (await placeholderResendCache.get(messageKey?.id)) { logger.debug({ messageKey }, 'already requested resend'); return }
|
|
74
|
+
await placeholderResendCache.set(messageKey?.id, msgData || true)
|
|
75
|
+
await delay(2000)
|
|
76
|
+
if (!(await placeholderResendCache.get(messageKey?.id))) { logger.debug({ messageKey }, 'message received while resend requested'); return 'RESOLVED' }
|
|
77
|
+
setTimeout(async () => { if (await placeholderResendCache.get(messageKey?.id)) { logger.debug({ messageKey }, 'PDO message without response after 8 seconds. Phone possibly offline'); await placeholderResendCache.del(messageKey?.id) } }, 8000)
|
|
78
|
+
return sendPeerDataOperationMessage({ placeholderMessageResendRequest: [{ messageKey }], peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.PLACEHOLDER_MESSAGE_RESEND })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const requestContactRefresh = async (jids) => {
|
|
82
|
+
if (!jids?.length) return
|
|
83
|
+
return sendPeerDataOperationMessage({ contactRefreshRequest: { jids: Array.isArray(jids) ? jids : [jids] }, peerDataOperationRequestType: proto.Message.PeerDataOperationRequestType.CONTACT_REFRESH_REQUEST })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Newsletter notifications ──────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const handleReachoutTimelockNotification = (data) => {
|
|
89
|
+
const payload = data.xwa2_notify_account_reachout_timelock
|
|
90
|
+
if (!payload) { logger.warn('reachout timelock notification missing payload'); return }
|
|
91
|
+
if (!payload.is_active) { ev.emit('connection.update', { reachoutTimeLock: { isActive: false, enforcementType: ReachoutTimelockEnforcementType.DEFAULT } }); return }
|
|
92
|
+
const timeEnforcementEnds = payload.time_enforcement_ends ? new Date(parseInt(payload.time_enforcement_ends, 10) * 1000) : new Date(Date.now() + 60000)
|
|
93
|
+
const enforcementType = isValidEnforcementType(payload.enforcement_type) ? payload.enforcement_type : ReachoutTimelockEnforcementType.DEFAULT
|
|
94
|
+
logger.info({ enforcementType, timeEnforcementEnds }, 'reachout timelock restriction set')
|
|
95
|
+
ev.emit('connection.update', { reachoutTimeLock: { isActive: true, timeEnforcementEnds, enforcementType } })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handleMessageCappingNotification = (data) => {
|
|
99
|
+
const payload = data.xwa2_notify_new_chat_messages_capping_info_update
|
|
100
|
+
if (!payload) { logger.warn('message capping notification missing payload'); return }
|
|
101
|
+
logger.info({ payload }, 'received message capping update')
|
|
102
|
+
ev.emit('message-capping.update', payload)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const handleLegacyMexNewsletterNotification = async (node) => {
|
|
106
|
+
const mexNode = getBinaryNodeChild(node, 'mex')
|
|
107
|
+
const updateNode = mexNode?.content ? null : getBinaryNodeChild(node, 'update') || getAllBinaryNodeChildren(node)[0]
|
|
108
|
+
const payloadNode = mexNode?.content ? mexNode : updateNode
|
|
109
|
+
if (!payloadNode?.content) { logger.warn({ node: binaryNodeToString(node) }, 'invalid mex newsletter notification'); return }
|
|
110
|
+
let data
|
|
111
|
+
try {
|
|
112
|
+
const payloadContent = payloadNode.content
|
|
113
|
+
if (Array.isArray(payloadContent)) { logger.warn({ payloadNode }, 'invalid mex newsletter notification payload format'); return }
|
|
114
|
+
const contentBuf = typeof payloadContent === 'string' ? Buffer.from(payloadContent, 'binary') : Buffer.from(payloadContent)
|
|
115
|
+
data = JSON.parse(contentBuf.toString())
|
|
116
|
+
} catch (error) { logger.error({ err: error, node: binaryNodeToString(node) }, 'failed to parse mex newsletter notification'); return }
|
|
117
|
+
const operation = data?.operation ?? payloadNode?.attrs?.op_name
|
|
118
|
+
let updates = data?.updates
|
|
119
|
+
if (!updates) { const linkedProfiles = data?.data?.xwa2_notify_linked_profiles; if (linkedProfiles) updates = [linkedProfiles] }
|
|
120
|
+
if (!updates || !operation) { logger.warn({ data }, 'invalid mex newsletter notification content'); return }
|
|
121
|
+
logger.info({ operation, updates }, 'got mex newsletter notification')
|
|
122
|
+
switch (operation) {
|
|
123
|
+
case 'NotificationNewsletterUpdate':
|
|
124
|
+
for (const update of updates) if (update.jid && update.settings && Object.keys(update.settings).length > 0) ev.emit('newsletter-settings.update', { id: update.jid, update: update.settings })
|
|
125
|
+
break
|
|
126
|
+
case 'NotificationNewsletterAdminPromote':
|
|
127
|
+
for (const update of updates) if (update.jid && update.user) ev.emit('newsletter-participants.update', { id: update.jid, author: node.attrs.from, user: update.user, new_role: 'ADMIN', action: 'promote' })
|
|
128
|
+
break
|
|
129
|
+
case 'NotificationLinkedProfilesUpdates':
|
|
130
|
+
for (const update of updates) {
|
|
131
|
+
const lid = update?.jid
|
|
132
|
+
const addedProfiles = Array.isArray(update?.added_profiles) ? update.added_profiles : []
|
|
133
|
+
const mappings = []
|
|
134
|
+
for (const profile of addedProfiles) {
|
|
135
|
+
const pn = typeof profile === 'string' ? profile : (profile?.pn ?? profile?.jid ?? null)
|
|
136
|
+
if (lid && pn) { const mapping = { lid, pn }; ev.emit('lid-mapping.update', mapping); mappings.push(mapping) }
|
|
137
|
+
}
|
|
138
|
+
await signalRepository.lidMapping.storeLIDPNMappings(mappings)
|
|
139
|
+
}
|
|
140
|
+
break
|
|
141
|
+
default:
|
|
142
|
+
logger.info({ operation, data }, 'unhandled mex newsletter notification')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const handleMexNotification = async (node) => {
|
|
147
|
+
const updateNode = getBinaryNodeChild(node, 'update')
|
|
148
|
+
if (!updateNode) { await handleLegacyMexNewsletterNotification(node); return }
|
|
149
|
+
const opName = updateNode.attrs?.op_name
|
|
150
|
+
if (!opName) { logger.warn({ node: binaryNodeToString(node) }, 'mex notification missing op_name, fallback to legacy'); await handleLegacyMexNewsletterNotification(node); return }
|
|
151
|
+
let mexResponse
|
|
152
|
+
try { mexResponse = JSON.parse(updateNode.content.toString()) } catch (error) { logger.error({ err: error, opName }, 'failed to parse mex notification JSON'); return }
|
|
153
|
+
if (mexResponse.errors?.length) { logger.warn({ errors: mexResponse.errors, opName }, 'mex notification has GQL errors'); return }
|
|
154
|
+
const data = mexResponse.data
|
|
155
|
+
if (!data) { logger.warn({ opName }, 'mex notification has null data'); return }
|
|
156
|
+
logger.debug({ opName }, 'processing mex notification')
|
|
157
|
+
switch (opName) {
|
|
158
|
+
case 'NotificationUserReachoutTimelockUpdate': handleReachoutTimelockNotification(data); break
|
|
159
|
+
case 'MessageCappingInfoNotification': handleMessageCappingNotification(data); break
|
|
160
|
+
case 'NotificationNewsletterJoin':
|
|
161
|
+
ev.emit('newsletter.join', data?.xwa2_notify_newsletter_on_join); break
|
|
162
|
+
case 'NotificationNewsletterUserSettingChange':
|
|
163
|
+
ev.emit('newsletter.viewer-settings.update', data?.xwa2_notify_newsletter_on_user_setting_change); break
|
|
164
|
+
default: {
|
|
165
|
+
const xwa2Key = Object.keys(data).find(k => k.startsWith('xwa2_notify_'))
|
|
166
|
+
if (xwa2Key) {
|
|
167
|
+
ev.emit('newsletter.update', { type: opName, data: data[xwa2Key] })
|
|
168
|
+
logger.debug({ opName, xwa2Key }, 'emitted newsletter update from xwa2 key')
|
|
169
|
+
} else {
|
|
170
|
+
await handleLegacyMexNewsletterNotification(node)
|
|
171
|
+
}
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const handleNewsletterNotification = async (node) => {
|
|
178
|
+
const from = node.attrs.from
|
|
179
|
+
const children = getAllBinaryNodeChildren(node)
|
|
180
|
+
const author = node.attrs.participant
|
|
181
|
+
for (const child of children) {
|
|
182
|
+
logger.debug({ from, child }, 'got newsletter notification')
|
|
183
|
+
switch (child.tag) {
|
|
184
|
+
case 'reaction':
|
|
185
|
+
ev.emit('newsletter.reaction', { id: from, server_id: child.attrs.message_id, reaction: { code: getBinaryNodeChildString(child, 'reaction'), count: 1 } })
|
|
186
|
+
break
|
|
187
|
+
case 'view':
|
|
188
|
+
ev.emit('newsletter.view', { id: from, server_id: child.attrs.message_id, count: parseInt(child.content?.toString() || '0', 10) })
|
|
189
|
+
break
|
|
190
|
+
case 'participant':
|
|
191
|
+
ev.emit('newsletter-participants.update', { id: from, author, user: child.attrs.jid, action: child.attrs.action, new_role: child.attrs.role })
|
|
192
|
+
break
|
|
193
|
+
case 'update':
|
|
194
|
+
const settingsNode = getBinaryNodeChild(child, 'settings')
|
|
195
|
+
if (settingsNode) {
|
|
196
|
+
const update = {}
|
|
197
|
+
const nameNode = getBinaryNodeChild(settingsNode, 'name')
|
|
198
|
+
if (nameNode?.content) update.name = nameNode.content.toString()
|
|
199
|
+
const descNode = getBinaryNodeChild(settingsNode, 'description')
|
|
200
|
+
if (descNode?.content) update.description = descNode.content.toString()
|
|
201
|
+
ev.emit('newsletter-settings.update', { id: from, update })
|
|
202
|
+
}
|
|
203
|
+
break
|
|
204
|
+
case 'message':
|
|
205
|
+
const plaintextNode = getBinaryNodeChild(child, 'plaintext')
|
|
206
|
+
if (plaintextNode?.content) {
|
|
207
|
+
try {
|
|
208
|
+
const buf = typeof plaintextNode.content === 'string' ? Buffer.from(plaintextNode.content, 'binary') : Buffer.from(plaintextNode.content)
|
|
209
|
+
const messageProto = proto.Message.decode(buf).toJSON()
|
|
210
|
+
const fullMessage = proto.WebMessageInfo.fromObject({ key: { remoteJid: from, id: child.attrs.message_id || child.attrs.server_id, fromMe: false }, message: messageProto, messageTimestamp: +child.attrs.t }).toJSON()
|
|
211
|
+
await upsertMessage(fullMessage, 'append')
|
|
212
|
+
} catch (error) { logger.error({ error }, 'Failed to decode plaintext newsletter message') }
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
default:
|
|
216
|
+
logger.warn({ node }, 'Unknown newsletter notification')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── ACK / call helpers ────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
const sendMessageAck = async (node, errorCode) => {
|
|
224
|
+
const { tag, attrs, content } = node
|
|
225
|
+
const hasUnavailable = !!getBinaryNodeChild({ tag, attrs, content }, 'unavailable')
|
|
226
|
+
const stanza = { tag: 'ack', attrs: { id: attrs.id, to: attrs.from, class: tag } }
|
|
227
|
+
if (errorCode) stanza.attrs.error = errorCode.toString()
|
|
228
|
+
if (attrs.participant) stanza.attrs.participant = attrs.participant
|
|
229
|
+
if (attrs.recipient) stanza.attrs.recipient = attrs.recipient
|
|
230
|
+
if (attrs.type || hasUnavailable || errorCode) stanza.attrs.type = attrs.type
|
|
231
|
+
if (tag === 'message' && authState.creds.me?.id) stanza.attrs.from = authState.creds.me.id
|
|
232
|
+
logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack')
|
|
233
|
+
try { await sendNode(stanza) } catch (error) {
|
|
234
|
+
if (error?.output?.statusCode === 428 || error?.message?.includes('Connection')) logger.warn({ id: attrs.id, error: error?.message }, 'Failed to send ACK (connection closed) - message already received')
|
|
235
|
+
else throw error
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const rejectCall = async (callId, callFrom) => {
|
|
240
|
+
await query({ tag: 'call', attrs: { from: authState.creds.me.id, to: callFrom }, content: [{ tag: 'reject', attrs: { 'call-id': callId, 'call-creator': callFrom, count: '0' }, content: undefined }] })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const offerCall = async (toJid, isVideo = false) => {
|
|
244
|
+
const callId = randomBytes(16).toString('hex').toUpperCase().substring(0, 64)
|
|
245
|
+
const offerContent = []
|
|
246
|
+
offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined })
|
|
247
|
+
offerContent.push({ tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined })
|
|
248
|
+
if (isVideo) offerContent.push({ tag: 'video', attrs: { orientation: '0', screen_width: '1920', screen_height: '1080', device_orientation: '0', enc: 'vp8', dec: 'vp8' } })
|
|
249
|
+
offerContent.push({ tag: 'net', attrs: { medium: '3' }, content: undefined })
|
|
250
|
+
offerContent.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) })
|
|
251
|
+
offerContent.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined })
|
|
252
|
+
const encKey = randomBytes(32)
|
|
253
|
+
const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device))
|
|
254
|
+
await assertSessions(devices, true)
|
|
255
|
+
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, { call: { callKey: encKey } }, {})
|
|
256
|
+
offerContent.push({ tag: 'destination', attrs: {}, content: destinations })
|
|
257
|
+
if (shouldIncludeDeviceIdentity) offerContent.push({ tag: 'device-identity', attrs: {}, content: encodeSignedDeviceIdentity(authState.creds.account, true) })
|
|
258
|
+
await query({ tag: 'call', attrs: { to: toJid }, content: [{ tag: 'offer', attrs: { 'call-id': callId, 'call-creator': authState.creds.me.id }, content: offerContent }] })
|
|
259
|
+
return { callId, toJid, isVideo }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Retry logic ───────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
const sendRetryRequest = async (node, forceIncludeKeys = false) => {
|
|
265
|
+
const { fullMessage } = decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '')
|
|
266
|
+
const { key: msgKey } = fullMessage
|
|
267
|
+
const msgId = msgKey.id
|
|
268
|
+
|
|
269
|
+
if (messageRetryManager) {
|
|
270
|
+
if (messageRetryManager.hasExceededMaxRetries(msgId)) { logger.debug({ msgId }, 'reached retry limit with new retry manager, clearing'); messageRetryManager.markRetryFailed(msgId); return }
|
|
271
|
+
const retryCount = messageRetryManager.incrementRetryCount(msgId)
|
|
272
|
+
await msgRetryCache.set(`${msgId}:${msgKey?.participant}`, retryCount)
|
|
273
|
+
} else {
|
|
274
|
+
const key = `${msgId}:${msgKey?.participant}`
|
|
275
|
+
let retryCount = (await msgRetryCache.get(key)) || 0
|
|
276
|
+
if (retryCount >= maxMsgRetryCount) { logger.debug({ retryCount, msgId }, 'reached retry limit, clearing'); await msgRetryCache.del(key); return }
|
|
277
|
+
await msgRetryCache.set(key, retryCount + 1)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const retryCount = (await msgRetryCache.get(`${msgId}:${msgKey?.participant}`)) || 1
|
|
281
|
+
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
|
282
|
+
const fromJid = node.attrs.from
|
|
283
|
+
let shouldRecreateSession = false, recreateReason = ''
|
|
284
|
+
|
|
285
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1) {
|
|
286
|
+
try {
|
|
287
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid)
|
|
288
|
+
const hasSession = await signalRepository.validateSession(fromJid)
|
|
289
|
+
let lidJid = null
|
|
290
|
+
if (isPnUser(fromJid)) lidJid = await signalRepository.lidMapping.getLIDForPN(fromJid)
|
|
291
|
+
const result = messageRetryManager.shouldRecreateSession(fromJid, retryCount, hasSession.exists)
|
|
292
|
+
shouldRecreateSession = result.recreate; recreateReason = result.reason
|
|
293
|
+
if (shouldRecreateSession) {
|
|
294
|
+
logger.debug({ fromJid, lidJid, retryCount, reason: recreateReason }, 'recreating session for retry')
|
|
295
|
+
await authState.keys.set({ session: { [sessionId]: null } })
|
|
296
|
+
if (lidJid) await authState.keys.set({ session: { [signalRepository.jidToSignalProtocolAddress(lidJid)]: null } })
|
|
297
|
+
forceIncludeKeys = true
|
|
298
|
+
}
|
|
299
|
+
} catch (error) { logger.warn({ error, fromJid }, 'failed to check session recreation') }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (retryCount <= 2) {
|
|
303
|
+
if (messageRetryManager) {
|
|
304
|
+
messageRetryManager.schedulePhoneRequest(msgId, async () => {
|
|
305
|
+
try { const requestId = await requestPlaceholderResend(msgKey); logger.debug(`sendRetryRequest: requested placeholder resend (${requestId}) for message ${msgId} (scheduled)`) }
|
|
306
|
+
catch (error) { logger.warn({ error, msgId }, 'failed to send scheduled phone request') }
|
|
307
|
+
})
|
|
308
|
+
} else {
|
|
309
|
+
const id = await requestPlaceholderResend(msgKey)
|
|
310
|
+
logger.debug(`sendRetryRequest: requested placeholder resend for message ${id}`)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const deviceIdentity = encodeSignedDeviceIdentity(account, true)
|
|
315
|
+
await authState.keys.transaction(async () => {
|
|
316
|
+
const receipt = { tag: 'receipt', attrs: { id: msgId, type: 'retry', to: node.attrs.from }, content: [{ tag: 'retry', attrs: { count: retryCount.toString(), id: node.attrs.id, t: node.attrs.t, v: '1', error: '0' } }, { tag: 'registration', attrs: {}, content: encodeBigEndian(authState.creds.registrationId) }] }
|
|
317
|
+
if (node.attrs.recipient) receipt.attrs.recipient = node.attrs.recipient
|
|
318
|
+
if (node.attrs.participant) receipt.attrs.participant = node.attrs.participant
|
|
319
|
+
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) {
|
|
320
|
+
const { update, preKeys } = await getNextPreKeys(authState, 1)
|
|
321
|
+
const [keyId] = Object.keys(preKeys)
|
|
322
|
+
const key = preKeys[+keyId]
|
|
323
|
+
receipt.content.push({ tag: 'keys', attrs: {}, content: [{ tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) }, { tag: 'identity', attrs: {}, content: identityKey.public }, xmppPreKey(key, +keyId), xmppSignedPreKey(signedPreKey), { tag: 'device-identity', attrs: {}, content: deviceIdentity }] })
|
|
324
|
+
ev.emit('creds.update', update)
|
|
325
|
+
}
|
|
326
|
+
await sendNode(receipt)
|
|
327
|
+
logger.info({ msgAttrs: node.attrs, retryCount }, 'sent retry receipt')
|
|
328
|
+
}, authState?.creds?.me?.id || 'sendRetryRequest')
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ── TC token helpers ──────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
const reissueTcTokenAfterIdentityChange = (from) => {
|
|
334
|
+
void (async () => {
|
|
335
|
+
const normalizedJid = jidNormalizedUser(from)
|
|
336
|
+
const tcJid = await resolveTcTokenJid(normalizedJid, getLIDForPN)
|
|
337
|
+
const tcTokenData = await authState.keys.get('tctoken', [tcJid])
|
|
338
|
+
const senderTs = tcTokenData?.[tcJid]?.senderTimestamp
|
|
339
|
+
if (senderTs === null || senderTs === undefined || isTcTokenExpired(senderTs)) return
|
|
340
|
+
logger.debug({ jid: normalizedJid, senderTimestamp: senderTs }, 'identity changed, re-issuing tctoken')
|
|
341
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
342
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
343
|
+
const issueJid = await resolveIssuanceJid(normalizedJid, issueToLid, getLIDForPN, getPNForLID)
|
|
344
|
+
const result = await issuePrivacyTokens([issueJid], senderTs)
|
|
345
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
346
|
+
})().catch(err => logger.debug({ jid: from, err: err?.message }, 'failed to re-issue tctoken after identity change'))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const handlePrivacyTokenNotification = async (node) => {
|
|
350
|
+
const tokensNode = getBinaryNodeChild(node, 'tokens')
|
|
351
|
+
if (!tokensNode) return
|
|
352
|
+
const from = jidNormalizedUser(node.attrs.from)
|
|
353
|
+
const senderLid = node.attrs.sender_lid && isLidUser(jidNormalizedUser(node.attrs.sender_lid)) ? jidNormalizedUser(node.attrs.sender_lid) : undefined
|
|
354
|
+
const fallbackJid = senderLid ?? (await resolveTcTokenJid(from, getLIDForPN))
|
|
355
|
+
logger.debug({ from, storageJid: fallbackJid }, 'processing privacy token notification')
|
|
356
|
+
await storeTcTokensFromIqResult({ result: node, fallbackJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const tcTokenKnownJids = new Set()
|
|
360
|
+
const tcTokenIndexLoaded = (async () => {
|
|
361
|
+
try { const jids = await readTcTokenIndex(authState.keys); for (const jid of jids) tcTokenKnownJids.add(jid); if (tcTokenKnownJids.size > 0) logger.debug({ count: tcTokenKnownJids.size }, 'loaded tctoken index') }
|
|
362
|
+
catch (err) { logger.warn({ err: err?.message }, 'failed to load tctoken index') }
|
|
363
|
+
})()
|
|
364
|
+
|
|
365
|
+
let tcTokenIndexTimer
|
|
366
|
+
const flushTcTokenIndex = async () => {
|
|
367
|
+
if (tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined }
|
|
368
|
+
const write = await buildMergedTcTokenIndexWrite(authState.keys, tcTokenKnownJids)
|
|
369
|
+
return authState.keys.set({ tctoken: write })
|
|
370
|
+
}
|
|
371
|
+
const scheduleTcTokenIndexSave = () => {
|
|
372
|
+
if (tcTokenIndexTimer) clearTimeout(tcTokenIndexTimer)
|
|
373
|
+
tcTokenIndexTimer = setTimeout(() => { tcTokenIndexTimer = undefined; flushTcTokenIndex().catch(err => logger.warn({ err: err?.message }, 'failed to save tctoken index')) }, 5000)
|
|
374
|
+
}
|
|
375
|
+
const trackTcTokenJid = (jid) => { if (jid && jid !== TC_TOKEN_INDEX_KEY && !tcTokenKnownJids.has(jid)) { tcTokenKnownJids.add(jid); scheduleTcTokenIndexSave() } }
|
|
376
|
+
|
|
377
|
+
// ── Encrypt notification ──────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
const handleEncryptNotification = async (node) => {
|
|
380
|
+
const from = node.attrs.from
|
|
381
|
+
if (from === S_WHATSAPP_NET) {
|
|
382
|
+
const stanzaId = node.attrs.id
|
|
383
|
+
if (stanzaId && inFlightPreKeyLow.has(stanzaId)) return
|
|
384
|
+
const count = +getBinaryNodeChild(node, 'count').attrs.value
|
|
385
|
+
logger.debug({ count, shouldUploadMorePreKeys: count < MIN_PREKEY_COUNT }, 'recv pre-key count')
|
|
386
|
+
if (count < MIN_PREKEY_COUNT) {
|
|
387
|
+
if (stanzaId) inFlightPreKeyLow.add(stanzaId)
|
|
388
|
+
try { await uploadPreKeys() } finally { if (stanzaId) inFlightPreKeyLow.delete(stanzaId) }
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
const result = await handleIdentityChange(node, { meId: authState.creds.me?.id, meLid: authState.creds.me?.lid, validateSession: signalRepository.validateSession, assertSessions, debounceCache: identityAssertDebounce, logger, onBeforeSessionRefresh: reissueTcTokenAfterIdentityChange })
|
|
392
|
+
if (result.action === 'no_identity_node') logger.info({ node }, 'unknown encrypt notification')
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Group notification ────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
const handleGroupNotification = (fullNode, child, msg) => {
|
|
399
|
+
const actingParticipantLid = fullNode.attrs.participant
|
|
400
|
+
const actingParticipantPn = fullNode.attrs.participant_pn
|
|
401
|
+
const actingParticipantUsername = fullNode.attrs.participant_username
|
|
402
|
+
const affectedParticipantLid = getBinaryNodeChild(child, 'participant')?.attrs?.jid || actingParticipantLid
|
|
403
|
+
const affectedParticipantPn = getBinaryNodeChild(child, 'participant')?.attrs?.phone_number || actingParticipantPn
|
|
404
|
+
switch (child?.tag) {
|
|
405
|
+
case 'create':
|
|
406
|
+
const metadata = extractGroupMetadata(child)
|
|
407
|
+
msg.messageStubType = WAMessageStubType.GROUP_CREATE
|
|
408
|
+
msg.messageStubParameters = [metadata.subject]
|
|
409
|
+
msg.key = { participant: metadata.owner, participantAlt: metadata.ownerPn }
|
|
410
|
+
ev.emit('chats.upsert', [{ id: metadata.id, name: metadata.subject, conversationTimestamp: metadata.creation }])
|
|
411
|
+
ev.emit('groups.upsert', [{ ...metadata, author: actingParticipantLid, authorPn: actingParticipantPn, authorUsername: actingParticipantUsername }])
|
|
412
|
+
break
|
|
413
|
+
case 'ephemeral':
|
|
414
|
+
case 'not_ephemeral':
|
|
415
|
+
msg.message = { protocolMessage: { type: proto.Message.ProtocolMessage.Type.EPHEMERAL_SETTING, ephemeralExpiration: +(child.attrs.expiration || 0) } }
|
|
416
|
+
break
|
|
417
|
+
case 'modify':
|
|
418
|
+
const modifiedParticipants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => ({ id: attrs.jid, phoneNumber: isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, lid: isLidUser(attrs.lid) ? attrs.lid : undefined, admin: attrs.type || null }))
|
|
419
|
+
msg.messageStubParameters = modifiedParticipants.map(a => JSON.stringify(a))
|
|
420
|
+
msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_CHANGE_NUMBER
|
|
421
|
+
break
|
|
422
|
+
case 'promote':
|
|
423
|
+
case 'demote':
|
|
424
|
+
case 'remove':
|
|
425
|
+
case 'add':
|
|
426
|
+
case 'leave':
|
|
427
|
+
msg.messageStubType = WAMessageStubType[`GROUP_PARTICIPANT_${child.tag.toUpperCase()}`]
|
|
428
|
+
const participants = getBinaryNodeChildren(child, 'participant').map(({ attrs }) => ({ id: attrs.jid, phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined, admin: attrs.type || null }))
|
|
429
|
+
if (participants.length === 1 && (areJidsSameUser(participants[0].id, actingParticipantLid) || areJidsSameUser(participants[0].id, actingParticipantPn)) && child.tag === 'remove') msg.messageStubType = WAMessageStubType.GROUP_PARTICIPANT_LEAVE
|
|
430
|
+
msg.messageStubParameters = participants.map(a => JSON.stringify(a))
|
|
431
|
+
break
|
|
432
|
+
case 'subject':
|
|
433
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
|
|
434
|
+
msg.messageStubParameters = [child.attrs.subject]
|
|
435
|
+
break
|
|
436
|
+
case 'description':
|
|
437
|
+
const description = getBinaryNodeChild(child, 'body')?.content?.toString()
|
|
438
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_DESCRIPTION
|
|
439
|
+
msg.messageStubParameters = description ? [description] : undefined
|
|
440
|
+
break
|
|
441
|
+
case 'announcement':
|
|
442
|
+
case 'not_announcement':
|
|
443
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
|
|
444
|
+
msg.messageStubParameters = [child.tag === 'announcement' ? 'on' : 'off']
|
|
445
|
+
break
|
|
446
|
+
case 'locked':
|
|
447
|
+
case 'unlocked':
|
|
448
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
|
|
449
|
+
msg.messageStubParameters = [child.tag === 'locked' ? 'on' : 'off']
|
|
450
|
+
break
|
|
451
|
+
case 'invite':
|
|
452
|
+
msg.messageStubType = WAMessageStubType.GROUP_CHANGE_INVITE_LINK
|
|
453
|
+
msg.messageStubParameters = [child.attrs.code]
|
|
454
|
+
break
|
|
455
|
+
case 'member_add_mode':
|
|
456
|
+
const addMode = child.content
|
|
457
|
+
if (addMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBER_ADD_MODE; msg.messageStubParameters = [addMode.toString()] }
|
|
458
|
+
break
|
|
459
|
+
case 'membership_approval_mode':
|
|
460
|
+
const approvalMode = getBinaryNodeChild(child, 'group_join')
|
|
461
|
+
if (approvalMode) { msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_MODE; msg.messageStubParameters = [approvalMode.attrs.state] }
|
|
462
|
+
break
|
|
463
|
+
case 'created_membership_requests':
|
|
464
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
|
|
465
|
+
msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), 'created', child.attrs.request_method]
|
|
466
|
+
break
|
|
467
|
+
case 'revoked_membership_requests':
|
|
468
|
+
const isDenied = areJidsSameUser(affectedParticipantLid, actingParticipantLid)
|
|
469
|
+
msg.messageStubType = WAMessageStubType.GROUP_MEMBERSHIP_JOIN_APPROVAL_REQUEST_NON_ADMIN_ADD
|
|
470
|
+
msg.messageStubParameters = [JSON.stringify({ lid: affectedParticipantLid, pn: affectedParticipantPn }), isDenied ? 'revoked' : 'rejected']
|
|
471
|
+
break
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Devices notification ──────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
const handleDevicesNotification = async (node) => {
|
|
478
|
+
const [child] = getAllBinaryNodeChildren(node)
|
|
479
|
+
const from = jidNormalizedUser(node.attrs.from)
|
|
480
|
+
if (!child) { logger.debug({ from }, 'devices notification missing child, skipping'); return }
|
|
481
|
+
const tag = child.tag
|
|
482
|
+
const deviceHash = child.attrs.device_hash
|
|
483
|
+
const devices = getBinaryNodeChildren(child, 'device')
|
|
484
|
+
if (areJidsSameUser(from, authState.creds.me.id) || areJidsSameUser(from, authState.creds.me.lid)) logger.info({ deviceJids: devices.map(d => d.attrs.jid) }, 'got my own devices')
|
|
485
|
+
if (!devices.length) { logger.debug({ from, tag }, 'no devices in notification, skipping'); return }
|
|
486
|
+
const decoded = []
|
|
487
|
+
for (const d of devices) { const jid = d.attrs.jid; if (!jid) continue; const parts = jidDecode(jid); if (!parts) { logger.debug({ jid }, 'failed to decode device jid, skipping'); continue }; decoded.push({ jid, user: parts.user, server: parts.server, device: parts.device }) }
|
|
488
|
+
if (!decoded.length) return
|
|
489
|
+
await sock.devicesMutex.mutex(async () => {
|
|
490
|
+
const byUser = new Map()
|
|
491
|
+
for (const d of decoded) { const list = byUser.get(d.user) || []; list.push(d); byUser.set(d.user, list) }
|
|
492
|
+
for (const [user, entries] of byUser) {
|
|
493
|
+
if (tag === 'update') { logger.debug({ user }, `${user}'s device list updated, dropping cached devices`); await sock.userDevicesCache?.del(user); continue }
|
|
494
|
+
if (tag === 'remove') await signalRepository.deleteSession(entries.map(e => e.jid))
|
|
495
|
+
const existingCache = (await sock.userDevicesCache?.get(user)) || []
|
|
496
|
+
if (!existingCache.length) { logger.debug({ user, tag }, 'device list not cached, deferring to USync refresh'); continue }
|
|
497
|
+
const affected = new Set(entries.map(e => e.device))
|
|
498
|
+
let updatedDevices
|
|
499
|
+
switch (tag) {
|
|
500
|
+
case 'add': logger.info({ deviceHash, count: entries.length }, 'devices added'); updatedDevices = [...existingCache.filter(d => !affected.has(d.device)), ...entries.map(e => ({ user: e.user, server: e.server, device: e.device }))]; break
|
|
501
|
+
case 'remove': logger.info({ deviceHash, count: entries.length }, 'devices removed'); updatedDevices = existingCache.filter(d => !affected.has(d.device)); break
|
|
502
|
+
default: logger.debug({ tag }, 'unknown device list change tag'); continue
|
|
503
|
+
}
|
|
504
|
+
if (updatedDevices.length === 0) await sock.userDevicesCache?.del(user)
|
|
505
|
+
else await sock.userDevicesCache?.set(user, updatedDevices)
|
|
506
|
+
}
|
|
507
|
+
})
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ── processNotification ───────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
const processNotification = async (node) => {
|
|
513
|
+
const result = {}
|
|
514
|
+
const [child] = getAllBinaryNodeChildren(node)
|
|
515
|
+
const nodeType = node.attrs.type
|
|
516
|
+
const from = jidNormalizedUser(node.attrs.from)
|
|
517
|
+
switch (nodeType) {
|
|
518
|
+
case 'privacy_token': await handlePrivacyTokenNotification(node); break
|
|
519
|
+
case 'newsletter': await handleNewsletterNotification(node); break
|
|
520
|
+
case 'mex': await handleMexNotification(node); break
|
|
521
|
+
case 'w:gp2': handleGroupNotification(node, child, result); break
|
|
522
|
+
case 'mediaretry': ev.emit('messages.media-update', [decodeMediaRetryNode(node)]); break
|
|
523
|
+
case 'encrypt': await handleEncryptNotification(node); break
|
|
524
|
+
case 'devices':
|
|
525
|
+
try { await handleDevicesNotification(node) } catch (error) { logger.error({ error, node }, 'failed to handle devices notification') }
|
|
526
|
+
break
|
|
527
|
+
case 'server_sync':
|
|
528
|
+
const update = getBinaryNodeChild(node, 'collection')
|
|
529
|
+
if (update) await resyncAppState([update.attrs.name], false)
|
|
530
|
+
break
|
|
531
|
+
case 'picture':
|
|
532
|
+
const setPicture = getBinaryNodeChild(node, 'set')
|
|
533
|
+
const delPicture = getBinaryNodeChild(node, 'delete')
|
|
534
|
+
ev.emit('contacts.update', [{ id: jidNormalizedUser(node?.attrs?.from) || (setPicture || delPicture)?.attrs?.hash || '', imgUrl: setPicture ? 'changed' : 'removed' }])
|
|
535
|
+
if (isJidGroup(from)) {
|
|
536
|
+
const pictureNode = setPicture || delPicture
|
|
537
|
+
result.messageStubType = WAMessageStubType.GROUP_CHANGE_ICON
|
|
538
|
+
if (setPicture) result.messageStubParameters = [setPicture.attrs.id]
|
|
539
|
+
result.participant = pictureNode?.attrs.author
|
|
540
|
+
result.key = { ...(result.key || {}), participant: setPicture?.attrs.author }
|
|
541
|
+
}
|
|
542
|
+
break
|
|
543
|
+
case 'account_sync':
|
|
544
|
+
if (child.tag === 'disappearing_mode') {
|
|
545
|
+
const newDuration = +child.attrs.duration
|
|
546
|
+
logger.info({ newDuration }, 'updated account disappearing mode')
|
|
547
|
+
ev.emit('creds.update', { accountSettings: { ...authState.creds.accountSettings, defaultDisappearingMode: { ephemeralExpiration: newDuration, ephemeralSettingTimestamp: +child.attrs.t } } })
|
|
548
|
+
} else if (child.tag === 'blocklist') {
|
|
549
|
+
for (const { attrs } of getBinaryNodeChildren(child, 'item')) ev.emit('blocklist.update', { blocklist: [attrs.jid], type: attrs.action === 'block' ? 'add' : 'remove' })
|
|
550
|
+
}
|
|
551
|
+
break
|
|
552
|
+
case 'link_code_companion_reg':
|
|
553
|
+
const linkCodeNode = getBinaryNodeChild(node, 'link_code_companion_reg')
|
|
554
|
+
const ref = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeNode, 'link_code_pairing_ref'))
|
|
555
|
+
const primaryIdentityPublicKey = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeNode, 'primary_identity_pub'))
|
|
556
|
+
const primaryEphemeralWrapped = toRequiredBuffer(getBinaryNodeChildBuffer(linkCodeNode, 'link_code_pairing_wrapped_primary_ephemeral_pub'))
|
|
557
|
+
const codePairingPublicKey = await decipherLinkPublicKey(primaryEphemeralWrapped)
|
|
558
|
+
const companionSharedKey = Curve.sharedKey(authState.creds.pairingEphemeralKeyPair.private, codePairingPublicKey)
|
|
559
|
+
const random = randomBytes(32)
|
|
560
|
+
const linkCodeSalt = randomBytes(32)
|
|
561
|
+
const linkCodePairingExpanded = hkdf(companionSharedKey, 32, { salt: linkCodeSalt, info: 'link_code_pairing_key_bundle_encryption_key' })
|
|
562
|
+
const encryptPayload = Buffer.concat([Buffer.from(authState.creds.signedIdentityKey.public), primaryIdentityPublicKey, random])
|
|
563
|
+
const encryptIv = randomBytes(12)
|
|
564
|
+
const encrypted = aesEncryptGCM(encryptPayload, linkCodePairingExpanded, encryptIv, Buffer.alloc(0))
|
|
565
|
+
const encryptedPayload = Buffer.concat([linkCodeSalt, encryptIv, encrypted])
|
|
566
|
+
const identitySharedKey = Curve.sharedKey(authState.creds.signedIdentityKey.private, primaryIdentityPublicKey)
|
|
567
|
+
const identityPayload = Buffer.concat([companionSharedKey, identitySharedKey, random])
|
|
568
|
+
authState.creds.advSecretKey = Buffer.from(hkdf(identityPayload, 32, { info: 'adv_secret' })).toString('base64')
|
|
569
|
+
await query({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'set', id: sock.generateMessageTag(), xmlns: 'md' }, content: [{ tag: 'link_code_companion_reg', attrs: { jid: authState.creds.me.id, stage: 'companion_finish' }, content: [{ tag: 'link_code_pairing_wrapped_key_bundle', attrs: {}, content: encryptedPayload }, { tag: 'companion_identity_public', attrs: {}, content: authState.creds.signedIdentityKey.public }, { tag: 'link_code_pairing_ref', attrs: {}, content: ref }] }] })
|
|
570
|
+
authState.creds.registered = true
|
|
571
|
+
ev.emit('creds.update', authState.creds)
|
|
572
|
+
break
|
|
573
|
+
}
|
|
574
|
+
if (Object.keys(result).length) return result
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── Link pairing helpers ──────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
const decipherLinkPublicKey = async (data) => {
|
|
580
|
+
const buffer = toRequiredBuffer(data)
|
|
581
|
+
const salt = buffer.slice(0, 32)
|
|
582
|
+
const secretKey = await derivePairingCodeKey(authState.creds.pairingCode, salt)
|
|
583
|
+
const iv = buffer.slice(32, 48)
|
|
584
|
+
const payload = buffer.slice(48, 80)
|
|
585
|
+
return aesDecryptCTR(payload, secretKey, iv)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const toRequiredBuffer = (data) => {
|
|
589
|
+
if (data === undefined) throw new Boom('Invalid buffer', { statusCode: 400 })
|
|
590
|
+
return data instanceof Buffer ? data : Buffer.from(data)
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── Retry helpers ─────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
const willSendMessageAgain = async (id, participant) => { const retryCount = (await msgRetryCache.get(`${id}:${participant}`)) || 0; return retryCount < maxMsgRetryCount }
|
|
596
|
+
const updateSendMessageAgainCount = async (id, participant) => { const key = `${id}:${participant}`; await msgRetryCache.set(key, ((await msgRetryCache.get(key)) || 0) + 1) }
|
|
597
|
+
|
|
598
|
+
const sendMessagesAgain = async (key, ids, retryNode, receiptNode) => {
|
|
599
|
+
const remoteJid = key.remoteJid
|
|
600
|
+
const participant = key.participant || remoteJid
|
|
601
|
+
const retryCount = +retryNode.attrs.count || 1
|
|
602
|
+
const msgs = []
|
|
603
|
+
|
|
604
|
+
for (const id of ids) {
|
|
605
|
+
let msg
|
|
606
|
+
if (messageRetryManager) { const cached = messageRetryManager.getRecentMessage(remoteJid, id); if (cached) { msg = cached.message; logger.debug({ jid: remoteJid, id }, 'found message in retry cache'); messageRetryManager.markRetrySuccess(id) } }
|
|
607
|
+
if (!msg) { msg = await getMessage({ ...key, id }); if (msg) { logger.debug({ jid: remoteJid, id }, 'found message via getMessage'); if (messageRetryManager) messageRetryManager.markRetrySuccess(id) } }
|
|
608
|
+
msgs.push(msg)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const sendToAll = !jidDecode(participant)?.device
|
|
612
|
+
const sessionId = signalRepository.jidToSignalProtocolAddress(participant)
|
|
613
|
+
let injectedFromBundle = false
|
|
614
|
+
const bundle = extractE2ESessionFromRetryReceipt(receiptNode)
|
|
615
|
+
|
|
616
|
+
if (bundle) {
|
|
617
|
+
try { await signalRepository.injectE2ESession({ jid: participant, session: bundle }); injectedFromBundle = true; logger.debug({ participant, retryCount }, 'injected session from retry receipt key bundle') }
|
|
618
|
+
catch (error) { logger.warn({ error, participant }, 'failed to inject session from retry receipt') }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!injectedFromBundle && typeof signalRepository.getSessionInfo === 'function') {
|
|
622
|
+
const receivedRegId = getBinaryNodeChildUInt(receiptNode, 'registration', 4)
|
|
623
|
+
if (typeof receivedRegId === 'number' && Number.isInteger(receivedRegId)) {
|
|
624
|
+
const info = await signalRepository.getSessionInfo(participant)
|
|
625
|
+
if (info && info.registrationId !== 0 && info.registrationId !== receivedRegId) { logger.info({ participant, stored: info.registrationId, received: receivedRegId }, 'reg id mismatch on retry without bundle, deleting session'); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (ids[0] && messageRetryManager && typeof messageRetryManager.saveBaseKey === 'function') {
|
|
630
|
+
const info = typeof signalRepository.getSessionInfo === 'function' ? await signalRepository.getSessionInfo(participant) : undefined
|
|
631
|
+
if (info) {
|
|
632
|
+
if (retryCount === 2) messageRetryManager.saveBaseKey(sessionId, ids[0], info.baseKey)
|
|
633
|
+
else if (retryCount > 2) {
|
|
634
|
+
if (messageRetryManager.hasSameBaseKey(sessionId, ids[0], info.baseKey)) { logger.warn({ participant, retryCount }, 'base key collision on retry, forcing fresh session'); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
635
|
+
messageRetryManager.deleteBaseKey(sessionId, ids[0])
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
let shouldRecreateSession = false, recreateReason = ''
|
|
641
|
+
if (enableAutoSessionRecreation && messageRetryManager && retryCount > 1 && !injectedFromBundle) {
|
|
642
|
+
try {
|
|
643
|
+
const hasSession = await signalRepository.validateSession(participant)
|
|
644
|
+
const result = messageRetryManager.shouldRecreateSession(participant, retryCount, hasSession.exists)
|
|
645
|
+
shouldRecreateSession = result.recreate; recreateReason = result.reason
|
|
646
|
+
if (shouldRecreateSession) { logger.debug({ participant, retryCount, reason: recreateReason }, 'recreating session for outgoing retry'); await authState.keys.set({ session: { [sessionId]: null } }) }
|
|
647
|
+
} catch (error) { logger.warn({ error, participant }, 'failed to check session recreation for outgoing retry') }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!injectedFromBundle) await assertSessions([participant], true)
|
|
651
|
+
if (isJidGroup(remoteJid)) await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } })
|
|
652
|
+
logger.debug({ participant, sendToAll, shouldRecreateSession, recreateReason, injectedFromBundle }, 'preparing retry recp')
|
|
653
|
+
|
|
654
|
+
for (const [i, msg] of msgs.entries()) {
|
|
655
|
+
if (!ids[i]) continue
|
|
656
|
+
if (msg && (await willSendMessageAgain(ids[i], participant))) {
|
|
657
|
+
await updateSendMessageAgainCount(ids[i], participant)
|
|
658
|
+
const msgRelayOpts = { messageId: ids[i] }
|
|
659
|
+
if (sendToAll) msgRelayOpts.useUserDevicesCache = false
|
|
660
|
+
else msgRelayOpts.participant = { jid: participant, count: +retryNode.attrs.count }
|
|
661
|
+
await relayMessage(key.remoteJid, msg, msgRelayOpts)
|
|
662
|
+
} else logger.debug({ jid: key.remoteJid, id: ids[i] }, 'recv retry request, but message not available')
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── handleReceipt ─────────────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
const handleReceipt = async (node) => {
|
|
669
|
+
const { attrs, content } = node
|
|
670
|
+
const isLid = attrs.from.includes('lid')
|
|
671
|
+
const isNodeFromMe = areJidsSameUser(attrs.participant || attrs.from, isLid ? authState.creds.me?.lid : authState.creds.me?.id)
|
|
672
|
+
const remoteJid = !isNodeFromMe || isJidGroup(attrs.from) ? attrs.from : attrs.recipient
|
|
673
|
+
const fromMe = !attrs.recipient || ((attrs.type === 'retry' || attrs.type === 'sender') && isNodeFromMe)
|
|
674
|
+
const key = { remoteJid, id: '', fromMe, participant: attrs.participant }
|
|
675
|
+
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid }, 'ignoring receipt from jid'); await sendMessageAck(node); return }
|
|
676
|
+
const ids = [attrs.id]
|
|
677
|
+
if (Array.isArray(content)) ids.push(...getBinaryNodeChildren(content[0], 'item').map(i => i.attrs.id))
|
|
678
|
+
try {
|
|
679
|
+
await Promise.all([processingMutex.mutex(async () => {
|
|
680
|
+
const status = getStatusFromReceiptType(attrs.type)
|
|
681
|
+
if (typeof status !== 'undefined' && (status >= proto.WebMessageInfo.Status.SERVER_ACK || !isNodeFromMe)) {
|
|
682
|
+
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
|
683
|
+
if (attrs.participant) {
|
|
684
|
+
const updateKey = status === proto.WebMessageInfo.Status.DELIVERY_ACK ? 'receiptTimestamp' : 'readTimestamp'
|
|
685
|
+
ev.emit('message-receipt.update', ids.map(id => ({ key: { ...key, id }, receipt: { userJid: jidNormalizedUser(attrs.participant), [updateKey]: +attrs.t } })))
|
|
686
|
+
}
|
|
687
|
+
} else ev.emit('messages.update', ids.map(id => ({ key: { ...key, id }, update: { status, messageTimestamp: toNumber(+(attrs.t ?? 0)) } })))
|
|
688
|
+
}
|
|
689
|
+
if (attrs.type === 'retry') {
|
|
690
|
+
key.participant = key.participant || attrs.from
|
|
691
|
+
const retryNode = getBinaryNodeChild(node, 'retry')
|
|
692
|
+
if (ids[0] && key.participant && (await willSendMessageAgain(ids[0], key.participant))) {
|
|
693
|
+
if (key.fromMe) {
|
|
694
|
+
try { await updateSendMessageAgainCount(ids[0], key.participant); logger.debug({ attrs, key }, 'recv retry request'); await sendMessagesAgain(key, ids, retryNode, node) }
|
|
695
|
+
catch (error) { logger.error({ key, ids, trace: error instanceof Error ? error.stack : 'Unknown error' }, 'error in sending message again') }
|
|
696
|
+
} else logger.info({ attrs, key }, 'recv retry for not fromMe message')
|
|
697
|
+
} else {
|
|
698
|
+
logger.info({ attrs, key, participant: key.participant }, 'retry limit exhausted - clearing broken session')
|
|
699
|
+
try {
|
|
700
|
+
await signalRepository.deleteSession([key.participant])
|
|
701
|
+
await msgRetryCache.del(`${ids[0]}:${key.participant}`)
|
|
702
|
+
} catch (err) { logger.error({ err, participant: key.participant }, 'failed to clear session/cache at retry exhaustion') }
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
})])
|
|
706
|
+
} finally { await sendMessageAck(node) }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// ── handleNotification ────────────────────────────────────────────────────
|
|
710
|
+
|
|
711
|
+
const handleNotification = async (node) => {
|
|
712
|
+
const remoteJid = node.attrs.from
|
|
713
|
+
if (shouldIgnoreJid(remoteJid) && remoteJid !== S_WHATSAPP_NET) { logger.debug({ remoteJid, id: node.attrs.id }, 'ignored notification'); await sendMessageAck(node); return }
|
|
714
|
+
try {
|
|
715
|
+
await Promise.all([notificationMutex.mutex(async () => {
|
|
716
|
+
const msg = await processNotification(node)
|
|
717
|
+
if (msg) {
|
|
718
|
+
const fromMe = areJidsSameUser(node.attrs.participant || remoteJid, authState.creds.me.id)
|
|
719
|
+
const { senderAlt: participantAlt, addressingMode } = extractAddressingContext(node)
|
|
720
|
+
msg.key = { remoteJid, fromMe, participant: node.attrs.participant, participantAlt, participantUsername: node.attrs.participant_username, addressingMode, id: node.attrs.id, ...(msg.key || {}) }
|
|
721
|
+
msg.participant ?? (msg.participant = node.attrs.participant)
|
|
722
|
+
msg.messageTimestamp = +node.attrs.t
|
|
723
|
+
await upsertMessage(proto.WebMessageInfo.fromObject(msg), 'append')
|
|
724
|
+
}
|
|
725
|
+
})])
|
|
726
|
+
} finally { await sendMessageAck(node) }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ── handleMessage ─────────────────────────────────────────────────────────
|
|
730
|
+
|
|
731
|
+
const handleMessage = async (node) => {
|
|
732
|
+
if (shouldIgnoreJid(node.attrs.from) && node.attrs.from !== S_WHATSAPP_NET) { logger.debug({ key: node.attrs.key }, 'ignored message'); await sendMessageAck(node, NACK_REASONS.UnhandledError); return }
|
|
733
|
+
|
|
734
|
+
const encNode = getBinaryNodeChild(node, 'enc')
|
|
735
|
+
if (encNode && encNode.attrs.type === 'msmsg') {
|
|
736
|
+
const encIv = getBinaryNodeChildBuffer(encNode, 'enc_iv')
|
|
737
|
+
const encPayload = getBinaryNodeChildBuffer(encNode, 'enc_p') || encNode.content
|
|
738
|
+
if (!encIv || !encPayload) { logger.debug({ id: node.attrs.id, from: node.attrs.from, hasIv: !!encIv, hasPayload: !!encPayload }, 'msmsg missing enc_iv or enc_p, cannot decrypt'); await sendMessageAck(node); return }
|
|
739
|
+
try {
|
|
740
|
+
const messageSecret = authState.creds.myAppStateKeyId ? Buffer.from(authState.creds.myAppStateKeyId, 'base64') : undefined
|
|
741
|
+
if (!messageSecret) { logger.debug({ key: node.attrs.key }, 'no message secret for msmsg, skipping'); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
|
|
742
|
+
const messageKey = { participant: node.attrs.participant || node.attrs.from, meId: authState.creds.me.id, meLid: authState.creds.me.lid, conversationJid: node.attrs.from, senderJid: node.attrs.participant, stanzaId: node.attrs.id, targetId: node.attrs.id }
|
|
743
|
+
const decrypted = await decryptMsmsgBotMessage(messageSecret, messageKey, { encPayload, encIv })
|
|
744
|
+
node.attrs._msmsgDecrypted = decodeDecryptedMsmsgMessage(decrypted)
|
|
745
|
+
logger.debug({ key: node.attrs.key }, 'decrypted msmsg bot message')
|
|
746
|
+
} catch (err) { logger.warn({ err, id: node.attrs.id }, 'failed to decrypt msmsg, falling back to nack'); await sendMessageAck(node, NACK_REASONS.MissingMessageSecret); return }
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
|
|
750
|
+
const alt = msg.key.participantAlt || msg.key.remoteJidAlt
|
|
751
|
+
if (!!alt) {
|
|
752
|
+
const altServer = jidDecode(alt)?.server
|
|
753
|
+
const primaryJid = msg.key.participant || msg.key.remoteJid
|
|
754
|
+
if (altServer === 'lid') { if (!(await signalRepository.lidMapping.getPNForLID(alt))) { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]); await signalRepository.migrateSession(primaryJid, alt) } }
|
|
755
|
+
else { await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]); await signalRepository.migrateSession(alt, primaryJid) }
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
let acked = false
|
|
759
|
+
try {
|
|
760
|
+
await messageMutex.mutex(async () => {
|
|
761
|
+
await decrypt()
|
|
762
|
+
if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message)
|
|
763
|
+
|
|
764
|
+
if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
|
|
765
|
+
if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) { acked = true; return sendMessageAck(node, NACK_REASONS.ParsingError) }
|
|
766
|
+
if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
|
|
767
|
+
const unavailableNode = getBinaryNodeChild(node, 'unavailable')
|
|
768
|
+
const unavailableType = unavailableNode?.attrs?.type
|
|
769
|
+
if (['bot_unavailable_fanout', 'hosted_unavailable_fanout', 'view_once_unavailable_fanout'].includes(unavailableType)) { logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type'); acked = true; return sendMessageAck(node) }
|
|
770
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
|
|
771
|
+
if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message'); acked = true; return sendMessageAck(node) }
|
|
772
|
+
const cleanKey = { remoteJid: msg.key.remoteJid, fromMe: msg.key.fromMe, id: msg.key.id, participant: msg.key.participant }
|
|
773
|
+
const msgData = { key: msg.key, messageTimestamp: msg.messageTimestamp, pushName: msg.pushName, participant: msg.participant, verifiedBizName: msg.verifiedBizName }
|
|
774
|
+
requestPlaceholderResend(cleanKey, msgData).then(requestId => { if (requestId && requestId !== 'RESOLVED') { logger.debug({ msgId: msg.key.id, requestId }, 'requested placeholder resend for unavailable message'); ev.emit('messages.update', [{ key: msg.key, update: { messageStubParameters: [NO_MESSAGE_FOUND_ERROR_TEXT, requestId] } }]) } }).catch(err => logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message'))
|
|
775
|
+
acked = true; await sendMessageAck(node)
|
|
776
|
+
} else {
|
|
777
|
+
if ((msg.messageStubParameters?.[0] || '').includes('InvalidPreKeyId')) { logger.debug({ msgId: msg.key.id, remoteJid: msg.key.remoteJid }, 'skipping retry for InvalidPreKeyId, sender must re-establish session'); acked = true; return sendMessageAck(node, NACK_REASONS.UnhandledError) }
|
|
778
|
+
if (isJidStatusBroadcast(msg.key.remoteJid)) {
|
|
779
|
+
const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp)
|
|
780
|
+
if (messageAge > STATUS_EXPIRY_SECONDS) { logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message'); acked = true; return sendMessageAck(node) }
|
|
781
|
+
}
|
|
782
|
+
retryMutex.mutex(async () => {
|
|
783
|
+
try { if (!ws.isOpen) { logger.debug({ node }, 'Connection closed, skipping retry'); return }; await sendRetryRequest(node, true); if (retryRequestDelayMs) await delay(retryRequestDelayMs) }
|
|
784
|
+
catch (err) { logger.error({ err }, 'Retry mechanism failed'); try { await sendRetryRequest(node, !getBinaryNodeChild(node, 'enc')) } catch (retryErr) { logger.error({ retryErr }, 'Final retry attempt failed') } }
|
|
785
|
+
acked = true; await sendMessageAck(node, NACK_REASONS.UnhandledError)
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
} else {
|
|
789
|
+
if (messageRetryManager && msg.key.id) messageRetryManager.cancelPendingPhoneRequest(msg.key.id)
|
|
790
|
+
const isNewsletter = isJidNewsletter(msg.key.remoteJid)
|
|
791
|
+
if (!isNewsletter) {
|
|
792
|
+
let type = undefined, participant = msg.key.participant
|
|
793
|
+
if (category === 'peer') type = 'peer_msg'
|
|
794
|
+
else if (msg.key.fromMe) { type = 'sender'; if (isLidUser(msg.key.remoteJid) || isLidUser(msg.key.remoteJidAlt)) participant = author }
|
|
795
|
+
else if (!sendActiveReceipts) type = 'inactive'
|
|
796
|
+
acked = true; await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type)
|
|
797
|
+
if (getHistoryMsg(msg.message)) await sendReceipt(jidNormalizedUser(msg.key.remoteJid), undefined, [msg.key.id], 'hist_sync')
|
|
798
|
+
} else { acked = true; await sendMessageAck(node); logger.debug({ key: msg.key }, 'processed newsletter message without receipts') }
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
cleanMessage(msg, authState.creds.me.id, authState.creds.me.lid)
|
|
802
|
+
|
|
803
|
+
if (!msg.key.fromMe) {
|
|
804
|
+
try {
|
|
805
|
+
const info = extractReportingInfoFromStanza(node)
|
|
806
|
+
if (info) {
|
|
807
|
+
const senderJid = msg.key.participant || author || msg.key.remoteJid
|
|
808
|
+
const contentType = msg.message ? getContentType(msg.message) : undefined
|
|
809
|
+
config.reportingInfoStore.add(senderJid, { stanzaId: info.stanzaId, timestamp: info.timestamp || Number(msg.messageTimestamp) || 0, reportingTag: info.reportingTag, reportingToken: info.reportingToken, tokenVersion: info.tokenVersion, validationPolicy: info.validationPolicy, text: extractMessageText(msg), pushName: msg.pushName || node.attrs?.notify || '', messageType: contentType === 'conversation' || contentType === 'extendedTextMessage' ? 'text' : contentType || 'text', fromJid: senderJid })
|
|
810
|
+
}
|
|
811
|
+
} catch (storeErr) { logger.warn({ err: storeErr, key: msg.key }, 'failed to store reporting_tag') }
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify')
|
|
815
|
+
})
|
|
816
|
+
} catch (error) {
|
|
817
|
+
const isClosed = error?.message?.includes('Connection Closed') || error?.output?.statusCode === 428
|
|
818
|
+
if (isClosed) logger.debug({ msg: error?.message }, 'Connection closed while handling message')
|
|
819
|
+
else logger.error({ error, stack: error?.stack, msg: error?.message || String(error), node: binaryNodeToString(node) }, 'error in handling message')
|
|
820
|
+
if (!acked) await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'))
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ── handleCall ────────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
const handleCall = async (node) => {
|
|
827
|
+
const { attrs } = node
|
|
828
|
+
const [infoChild] = getAllBinaryNodeChildren(node)
|
|
829
|
+
if (!infoChild) throw new Boom('Missing call info in call node')
|
|
830
|
+
const status = getCallStatusFromNode(infoChild)
|
|
831
|
+
const callId = infoChild.attrs['call-id']
|
|
832
|
+
const from = infoChild.attrs.from || infoChild.attrs['call-creator']
|
|
833
|
+
const call = { chatId: attrs.from, from, callerPn: infoChild.attrs['caller_pn'], id: callId, date: new Date(+attrs.t * 1000), offline: !!attrs.offline, status }
|
|
834
|
+
if (status === 'relaylatency') { const latencyValue = infoChild.attrs.latency || infoChild.attrs['latency_ms'] || infoChild.attrs['latency-ms']; const latencyMs = latencyValue ? Number(latencyValue) : undefined; if (Number.isFinite(latencyMs)) call.latencyMs = latencyMs }
|
|
835
|
+
if (status === 'offer') { call.isVideo = !!getBinaryNodeChild(infoChild, 'video'); call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid']; call.groupJid = infoChild.attrs['group-jid']; await callOfferCache.set(call.id, call) }
|
|
836
|
+
const existingCall = await callOfferCache.get(call.id)
|
|
837
|
+
if (existingCall) { call.isVideo = existingCall.isVideo; call.isGroup = existingCall.isGroup; call.callerPn = call.callerPn || existingCall.callerPn }
|
|
838
|
+
if (['reject', 'accept', 'timeout', 'terminate'].includes(status)) await callOfferCache.del(call.id)
|
|
839
|
+
ev.emit('call', [call])
|
|
840
|
+
await sendMessageAck(node)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ── handleBadAck ──────────────────────────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
const handleBadAck = async ({ attrs }) => {
|
|
846
|
+
const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id }
|
|
847
|
+
if (!attrs.error) return
|
|
848
|
+
const isReachoutTimelocked = attrs.error === String(NACK_REASONS.SenderReachoutTimelocked)
|
|
849
|
+
if (attrs.error === SERVER_ERROR_CODES.MessageAccountRestriction) {
|
|
850
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'error 463: account restricted or missing tctoken for contact')
|
|
851
|
+
const ackFrom = attrs.from
|
|
852
|
+
if (ackFrom && !inFlight463Recoveries.has(ackFrom)) {
|
|
853
|
+
inFlight463Recoveries.add(ackFrom)
|
|
854
|
+
void (async () => {
|
|
855
|
+
try {
|
|
856
|
+
const getPNForLID = signalRepository.lidMapping.getPNForLID.bind(signalRepository.lidMapping)
|
|
857
|
+
const tcStorageJid = await resolveTcTokenJid(ackFrom, getLIDForPN)
|
|
858
|
+
const issueToLid = sock.serverProps?.lidTrustedTokenIssueToLid ?? false
|
|
859
|
+
const issueJid = await resolveIssuanceJid(ackFrom, issueToLid, getLIDForPN, getPNForLID)
|
|
860
|
+
const result = await issuePrivacyTokens([issueJid], unixTimestampSeconds())
|
|
861
|
+
await storeTcTokensFromIqResult({ result, fallbackJid: tcStorageJid, keys: authState.keys, getLIDForPN, onNewJidStored: trackTcTokenJid })
|
|
862
|
+
logger.debug({ from: ackFrom }, 'completed 463 token recovery issuance')
|
|
863
|
+
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.PENDING } }])
|
|
864
|
+
} catch (err) { logger.debug({ from: ackFrom, err: err?.message }, 'failed 463 token recovery issuance') }
|
|
865
|
+
finally { inFlight463Recoveries.delete(ackFrom) }
|
|
866
|
+
})()
|
|
867
|
+
}
|
|
868
|
+
} else if (attrs.error === SERVER_ERROR_CODES.SmaxInvalid) {
|
|
869
|
+
logger.warn({ msgId: attrs.id, from: attrs.from }, 'smax-invalid (479): stale device session or malformed addressing')
|
|
870
|
+
} else if (isReachoutTimelocked) {
|
|
871
|
+
await fetchAccountReachoutTimelock().catch(err => logger.warn({ err }, 'failed to fetch reachout timelock'))
|
|
872
|
+
logger.warn({ attrs }, 'received error in ack')
|
|
873
|
+
} else {
|
|
874
|
+
logger.warn({ attrs }, 'received error in ack')
|
|
875
|
+
}
|
|
876
|
+
ev.emit('messages.update', [{ key, update: { status: WAMessageStatus.ERROR, messageStubParameters: isReachoutTimelocked ? [attrs.error, ACCOUNT_RESTRICTED_TEXT] : [attrs.error] } }])
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// ── Offline node processor ────────────────────────────────────────────────
|
|
880
|
+
|
|
881
|
+
const processNodeWithBuffer = async (node, identifier, exec) => { ev.buffer(); await exec(node, false).catch(err => onUnexpectedError(err, identifier)); ev.flush() }
|
|
882
|
+
const yieldToEventLoop = () => new Promise(resolve => setImmediate(resolve))
|
|
883
|
+
|
|
884
|
+
const makeOfflineNodeProcessor = () => {
|
|
885
|
+
const nodeProcessorMap = new Map([['message', handleMessage], ['call', handleCall], ['receipt', handleReceipt], ['notification', handleNotification]])
|
|
886
|
+
const nodes = []
|
|
887
|
+
let isProcessing = false
|
|
888
|
+
const enqueue = (type, node) => {
|
|
889
|
+
nodes.push({ type, node })
|
|
890
|
+
if (isProcessing) return
|
|
891
|
+
isProcessing = true;
|
|
892
|
+
(async () => {
|
|
893
|
+
let processedInBatch = 0
|
|
894
|
+
while (nodes.length && ws.isOpen) {
|
|
895
|
+
const { type, node } = nodes.shift()
|
|
896
|
+
const processor = nodeProcessorMap.get(type)
|
|
897
|
+
if (!processor) { onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node'); continue }
|
|
898
|
+
await processor(node).catch(err => onUnexpectedError(err, `processing offline ${type}`))
|
|
899
|
+
if (++processedInBatch >= 10) { processedInBatch = 0; await yieldToEventLoop() }
|
|
900
|
+
}
|
|
901
|
+
isProcessing = false
|
|
902
|
+
})().catch(error => onUnexpectedError(error, 'processing offline nodes'))
|
|
903
|
+
}
|
|
904
|
+
return { enqueue }
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const offlineNodeProcessor = makeOfflineNodeProcessor()
|
|
908
|
+
|
|
909
|
+
const processNode = async (type, node, identifier, exec) => {
|
|
910
|
+
const from = node.attrs.from
|
|
911
|
+
if (from && from !== S_WHATSAPP_NET && shouldIgnoreJid(from)) { await sendMessageAck(node, type === 'message' ? NACK_REASONS.UnhandledError : undefined); return }
|
|
912
|
+
if (node.attrs.offline) offlineNodeProcessor.enqueue(type, node)
|
|
913
|
+
else await processNodeWithBuffer(node, identifier, exec)
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// ── WS event bindings ─────────────────────────────────────────────────────
|
|
917
|
+
|
|
918
|
+
ws.on('CB:message', async (node) => { await processNode('message', node, 'processing message', handleMessage) })
|
|
919
|
+
ws.on('CB:call', async (node) => { await processNode('call', node, 'handling call', handleCall) })
|
|
920
|
+
ws.on('CB:receipt', async (node) => { await processNode('receipt', node, 'handling receipt', handleReceipt) })
|
|
921
|
+
ws.on('CB:notification', async (node) => { await processNode('notification', node, 'handling notification', handleNotification) })
|
|
922
|
+
ws.on('CB:ack,class:message', (node) => { handleBadAck(node).catch(error => onUnexpectedError(error, 'handling bad ack')) })
|
|
923
|
+
|
|
924
|
+
ev.on('call', async ([call]) => {
|
|
925
|
+
if (!call) return
|
|
926
|
+
if (call.status === 'timeout' || (call.status === 'offer' && call.isGroup)) {
|
|
927
|
+
const msg = { key: { remoteJid: call.chatId, id: call.id, fromMe: false }, messageTimestamp: unixTimestampSeconds(call.date) }
|
|
928
|
+
if (call.status === 'timeout') msg.messageStubType = call.isGroup ? (call.isVideo ? WAMessageStubType.CALL_MISSED_GROUP_VIDEO : WAMessageStubType.CALL_MISSED_GROUP_VOICE) : (call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE)
|
|
929
|
+
else msg.message = { call: { callKey: Buffer.from(call.id) } }
|
|
930
|
+
await upsertMessage(proto.WebMessageInfo.fromObject(msg), call.offline ? 'append' : 'notify')
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
// ── TC token pruning ──────────────────────────────────────────────────────
|
|
935
|
+
|
|
936
|
+
let lastTcTokenPruneTs = 0
|
|
937
|
+
const pruneExpiredTcTokens = async () => {
|
|
938
|
+
try {
|
|
939
|
+
await tcTokenIndexLoaded
|
|
940
|
+
const persisted = await readTcTokenIndex(authState.keys)
|
|
941
|
+
const allJids = new Set([...tcTokenKnownJids, ...persisted])
|
|
942
|
+
if (!allJids.size) return
|
|
943
|
+
const jids = [...allJids]
|
|
944
|
+
const allTokens = await authState.keys.get('tctoken', jids)
|
|
945
|
+
const writes = {}, survivors = new Set()
|
|
946
|
+
let mutated = 0
|
|
947
|
+
for (const jid of jids) {
|
|
948
|
+
const entry = allTokens[jid]
|
|
949
|
+
if (!entry) { mutated++; continue }
|
|
950
|
+
const hasPeerToken = !!entry.token?.length
|
|
951
|
+
const peerTokenExpired = hasPeerToken && isTcTokenExpired(entry.timestamp)
|
|
952
|
+
const hasSenderTs = entry.senderTimestamp !== undefined
|
|
953
|
+
const senderTsExpired = hasSenderTs && isTcTokenExpired(entry.senderTimestamp)
|
|
954
|
+
const keepPeer = hasPeerToken && !peerTokenExpired
|
|
955
|
+
const keepSender = hasSenderTs && !senderTsExpired
|
|
956
|
+
if (!keepPeer && !keepSender) { writes[jid] = null; mutated++ }
|
|
957
|
+
else if (peerTokenExpired && keepSender) { writes[jid] = { token: Buffer.alloc(0), senderTimestamp: entry.senderTimestamp }; survivors.add(jid); mutated++ }
|
|
958
|
+
else survivors.add(jid)
|
|
959
|
+
}
|
|
960
|
+
if (mutated === 0) return
|
|
961
|
+
await authState.keys.set({ tctoken: { ...writes, [TC_TOKEN_INDEX_KEY]: { token: Buffer.from(JSON.stringify([...survivors])) } } })
|
|
962
|
+
tcTokenKnownJids.clear(); for (const jid of survivors) tcTokenKnownJids.add(jid)
|
|
963
|
+
logger.debug({ mutated, remaining: survivors.size }, 'pruned expired tctokens')
|
|
964
|
+
} catch (err) { logger.warn({ err: err?.message }, 'failed to prune expired tctokens') }
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
ev.on('connection.update', ({ isOnline, connection }) => {
|
|
968
|
+
if (typeof isOnline !== 'undefined') { sendActiveReceipts = isOnline; logger.trace(`sendActiveReceipts set to "${sendActiveReceipts}"`) }
|
|
969
|
+
if (connection === 'close' && tcTokenIndexTimer) { clearTimeout(tcTokenIndexTimer); tcTokenIndexTimer = undefined; try { void Promise.resolve(flushTcTokenIndex()).catch(() => { }) } catch { } }
|
|
970
|
+
if (isOnline) { const now = Date.now(); if (now - lastTcTokenPruneTs >= 86_400_000) { lastTcTokenPruneTs = now; void pruneExpiredTcTokens() } }
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
registerSocketEndHandler(() => {
|
|
974
|
+
if (!config.msgRetryCounterCache && msgRetryCache.close) msgRetryCache.close()
|
|
975
|
+
if (!config.callOfferCache && callOfferCache.close) callOfferCache.close()
|
|
976
|
+
identityAssertDebounce.close()
|
|
977
|
+
sendActiveReceipts = false
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
return { ...sock, sendMessageAck, sendRetryRequest, rejectCall, offerCall, fetchMessageHistory, requestPlaceholderResend, requestContactRefresh, messageRetryManager, tcTokenIndexLoaded }
|
|
904
981
|
}
|