@nexustechpro/baileys 2.0.2 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +924 -1299
- package/lib/Defaults/baileys-version.json +6 -2
- package/lib/Defaults/index.js +172 -172
- package/lib/Signal/libsignal.js +380 -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 +909 -963
- package/lib/Socket/communities.js +430 -430
- package/lib/Socket/groups.js +342 -342
- package/lib/Socket/index.js +22 -22
- package/lib/Socket/messages-recv.js +777 -743
- package/lib/Socket/messages-send.js +295 -305
- package/lib/Socket/mex.js +50 -50
- package/lib/Socket/newsletter.js +148 -148
- package/lib/Socket/nexus-handler.js +75 -261
- package/lib/Socket/socket.js +709 -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 +28 -28
- 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 +12 -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 +694 -764
- package/lib/Utils/crypto.js +109 -135
- package/lib/Utils/decode-wa-message.js +310 -314
- package/lib/Utils/event-buffer.js +547 -547
- package/lib/Utils/generics.js +297 -297
- package/lib/Utils/history.js +91 -83
- package/lib/Utils/index.js +21 -20
- package/lib/Utils/key-store.js +17 -0
- package/lib/Utils/link-preview.js +97 -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 +534 -534
- package/lib/Utils/messages.js +705 -705
- package/lib/Utils/noise-handler.js +255 -255
- package/lib/Utils/pre-key-manager.js +105 -105
- package/lib/Utils/process-message.js +412 -412
- package/lib/Utils/signal.js +160 -158
- package/lib/Utils/use-multi-file-auth-state.js +120 -120
- package/lib/Utils/validate-connection.js +194 -194
- package/lib/WABinary/constants.js +1300 -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 +5 -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 +66 -66
- package/package.json +171 -144
- 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/Socket/socket.js
CHANGED
|
@@ -1,1202 +1,710 @@
|
|
|
1
|
-
import { Boom } from "@hapi/boom"
|
|
2
|
-
import { randomBytes } from "crypto"
|
|
3
|
-
import { URL } from "url"
|
|
4
|
-
import { promisify } from "util"
|
|
5
|
-
import { proto } from "../../WAProto/index.js"
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "../
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
logger.info({ browser, helloMsg },
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
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
|
-
|
|
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
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// Cleanup timers
|
|
712
|
-
clearInterval(keepAliveReq)
|
|
713
|
-
clearInterval(sessionHealthCheck)
|
|
714
|
-
clearTimeout(qrTimer)
|
|
715
|
-
stopPreKeyBackgroundMonitor()
|
|
716
|
-
stopSessionCleanup()
|
|
717
|
-
|
|
718
|
-
// Cleanup listeners
|
|
719
|
-
ws.removeAllListeners("close")
|
|
720
|
-
ws.removeAllListeners("open")
|
|
721
|
-
ws.removeAllListeners("message")
|
|
722
|
-
|
|
723
|
-
// Close WebSocket
|
|
724
|
-
if (!ws.isClosed && !ws.isClosing) {
|
|
725
|
-
try {
|
|
726
|
-
ws.close()
|
|
727
|
-
} catch { }
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Emit connection update
|
|
731
|
-
if (shouldLogError || (error && error.output?.statusCode !== DisconnectReason.connectionClosed)) {
|
|
732
|
-
ev.emit("connection.update", {
|
|
733
|
-
connection: "close",
|
|
734
|
-
lastDisconnect: { error, date: new Date() }
|
|
735
|
-
})
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
ev.removeAllListeners("connection.update")
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const attemptReconnection = async (reason = "unknown") => {
|
|
742
|
-
if (closed) {
|
|
743
|
-
logger.debug("Cannot reconnect - connection already closed")
|
|
744
|
-
return
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (reconnectAttempts >= CONSTANTS.MAX_RECONNECT) {
|
|
748
|
-
logger.error(
|
|
749
|
-
{ attempts: reconnectAttempts, maxAttempts: CONSTANTS.MAX_RECONNECT },
|
|
750
|
-
"Max reconnection attempts reached"
|
|
751
|
-
)
|
|
752
|
-
end(new Boom("Connection Lost", { statusCode: DisconnectReason.connectionLost }))
|
|
753
|
-
return
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
reconnectAttempts++
|
|
757
|
-
|
|
758
|
-
// Longer delays for network issues
|
|
759
|
-
const isNetworkIssue = reason === "websocket-close"
|
|
760
|
-
const baseDelay = isNetworkIssue ? 2000 : 1000
|
|
761
|
-
const backoffDelay = Math.min(baseDelay * Math.pow(2, reconnectAttempts - 1), 30000)
|
|
762
|
-
|
|
763
|
-
logger.info({
|
|
764
|
-
attempt: reconnectAttempts,
|
|
765
|
-
maxAttempts: CONSTANTS.MAX_RECONNECT,
|
|
766
|
-
delay: backoffDelay,
|
|
767
|
-
reason,
|
|
768
|
-
isNetworkIssue
|
|
769
|
-
}, "Attempting WebSocket reconnection")
|
|
770
|
-
|
|
771
|
-
try {
|
|
772
|
-
await new Promise(resolve => setTimeout(resolve, backoffDelay))
|
|
773
|
-
logger.debug("Restarting WebSocket connection")
|
|
774
|
-
await ws.restart()
|
|
775
|
-
logger.info("✅ WebSocket reconnected successfully")
|
|
776
|
-
reconnectAttempts = 0
|
|
777
|
-
} catch (err) {
|
|
778
|
-
logger.error({ err, attempt: reconnectAttempts }, "Reconnection attempt failed")
|
|
779
|
-
|
|
780
|
-
if (reconnectAttempts < CONSTANTS.MAX_RECONNECT) {
|
|
781
|
-
return attemptReconnection(reason)
|
|
782
|
-
} else {
|
|
783
|
-
end(new Boom("Failed to reconnect", { statusCode: DisconnectReason.connectionLost }))
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// ==================== KEEP-ALIVE & HEALTH ====================
|
|
789
|
-
const startKeepAliveRequest = () => {
|
|
790
|
-
keepAliveReq = setInterval(async () => {
|
|
791
|
-
if (!lastDateRecv) lastDateRecv = new Date()
|
|
792
|
-
|
|
793
|
-
if (ws.isOpen) {
|
|
794
|
-
try {
|
|
795
|
-
await query({
|
|
796
|
-
tag: "iq",
|
|
797
|
-
attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: "get", xmlns: "w:p" },
|
|
798
|
-
content: [{ tag: "ping", attrs: {} }]
|
|
799
|
-
})
|
|
800
|
-
consecutiveFailedPings = 0
|
|
801
|
-
logger.trace("Keep-alive ping successful")
|
|
802
|
-
} catch (err) {
|
|
803
|
-
consecutiveFailedPings++
|
|
804
|
-
logger.warn(
|
|
805
|
-
{ consecutiveFailures: consecutiveFailedPings, maxAllowed: CONSTANTS.MAX_FAILED_PINGS },
|
|
806
|
-
"Keep-alive ping failed"
|
|
807
|
-
)
|
|
808
|
-
|
|
809
|
-
if (consecutiveFailedPings >= CONSTANTS.MAX_FAILED_PINGS) {
|
|
810
|
-
logger.error("Multiple consecutive ping failures - connection lost")
|
|
811
|
-
end(new Boom("Connection was lost", { statusCode: DisconnectReason.connectionLost }))
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
} else {
|
|
815
|
-
logger.warn("Keep-alive called when WebSocket not open - triggering reconnection")
|
|
816
|
-
|
|
817
|
-
if (!closed && ws.isClosed) {
|
|
818
|
-
ws.restart().catch(err => {
|
|
819
|
-
logger.error({ err }, "Failed to restart WebSocket from keep-alive")
|
|
820
|
-
end(new Boom("Connection Lost", { statusCode: DisconnectReason.connectionLost }))
|
|
821
|
-
})
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}, keepAliveIntervalMs)
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
const startSessionHealthMonitor = () => {
|
|
828
|
-
sessionHealthCheck = setInterval(() => {
|
|
829
|
-
const timeSinceLastMsg = Date.now() - lastMessageTime
|
|
830
|
-
const healthCheckIntervalMs = keepAliveIntervalMs * CONSTANTS.HEALTH_CHECK_MULTIPLIER
|
|
831
|
-
|
|
832
|
-
if (timeSinceLastMsg > healthCheckIntervalMs) {
|
|
833
|
-
if (ws.isOpen) {
|
|
834
|
-
logger.warn(
|
|
835
|
-
{ timeSinceLastMsg, threshold: healthCheckIntervalMs },
|
|
836
|
-
"Extended inactivity detected"
|
|
837
|
-
)
|
|
838
|
-
} else {
|
|
839
|
-
logger.error(
|
|
840
|
-
{ timeSinceLastMsg },
|
|
841
|
-
"WebSocket closed during extended inactivity - reconnecting"
|
|
842
|
-
)
|
|
843
|
-
attemptReconnection("health-check-failed").catch(err =>
|
|
844
|
-
logger.error({ err }, "Health check reconnection failed")
|
|
845
|
-
)
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
}, keepAliveIntervalMs * CONSTANTS.HEALTH_CHECK_MULTIPLIER)
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// ==================== UTILITY FUNCTIONS ====================
|
|
852
|
-
const sendPassiveIq = (tag) => query({
|
|
853
|
-
tag: "iq",
|
|
854
|
-
attrs: { to: S_WHATSAPP_NET, xmlns: "passive", type: "set" },
|
|
855
|
-
content: [{ tag, attrs: {} }]
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
const logout = async (msg) => {
|
|
859
|
-
const jid = authState.creds.me?.id
|
|
860
|
-
if (jid) {
|
|
861
|
-
await sendNode({
|
|
862
|
-
tag: "iq",
|
|
863
|
-
attrs: { to: S_WHATSAPP_NET, type: "set", id: generateMessageTag(), xmlns: "md" },
|
|
864
|
-
content: [{ tag: "remove-companion-device", attrs: { jid, reason: "user_initiated" } }]
|
|
865
|
-
})
|
|
866
|
-
}
|
|
867
|
-
end(new Boom(msg || "Intentional Logout", { statusCode: DisconnectReason.loggedOut }))
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
871
|
-
const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
|
|
872
|
-
|
|
873
|
-
if (customPairingCode && customPairingCode?.length !== 8) {
|
|
874
|
-
throw new Error("Custom pairing code must be exactly 8 chars")
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
authState.creds.pairingCode = pairingCode
|
|
878
|
-
authState.creds.me = { id: jidEncode(phoneNumber, "s.whatsapp.net"), name: "~" }
|
|
879
|
-
ev.emit("creds.update", authState.creds)
|
|
880
|
-
|
|
881
|
-
await sendNode({
|
|
882
|
-
tag: "iq",
|
|
883
|
-
attrs: { to: S_WHATSAPP_NET, type: "set", id: generateMessageTag(), xmlns: "md" },
|
|
884
|
-
content: [{
|
|
885
|
-
tag: "link_code_companion_reg",
|
|
886
|
-
attrs: {
|
|
887
|
-
jid: authState.creds.me.id,
|
|
888
|
-
stage: "companion_hello",
|
|
889
|
-
should_show_push_notification: "true"
|
|
890
|
-
},
|
|
891
|
-
content: [
|
|
892
|
-
{ tag: "link_code_pairing_wrapped_companion_ephemeral_pub", attrs: {}, content: await generatePairingKey() },
|
|
893
|
-
{ tag: "companion_server_auth_key_pub", attrs: {}, content: authState.creds.noiseKey.public },
|
|
894
|
-
{ tag: "companion_platform_id", attrs: {}, content: getPlatformId(browser[1]) },
|
|
895
|
-
{ tag: "companion_platform_display", attrs: {}, content: `${browser[1]} (${browser[0]})` },
|
|
896
|
-
{ tag: "link_code_pairing_nonce", attrs: {}, content: "0" }
|
|
897
|
-
]
|
|
898
|
-
}]
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
return authState.creds.pairingCode
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
async function generatePairingKey() {
|
|
905
|
-
const salt = randomBytes(32)
|
|
906
|
-
const randomIv = randomBytes(16)
|
|
907
|
-
const key = await derivePairingCodeKey(authState.creds.pairingCode, salt)
|
|
908
|
-
const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv)
|
|
909
|
-
return Buffer.concat([salt, randomIv, ciphered])
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const sendWAMBuffer = (wamBuffer) => query({
|
|
913
|
-
tag: "iq",
|
|
914
|
-
attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: "w:stats" },
|
|
915
|
-
content: [{ tag: "add", attrs: { t: Math.round(Date.now() / 1000) + "" }, content: wamBuffer }]
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
// ==================== WEBSOCKET EVENT HANDLERS ====================
|
|
919
|
-
ws.on("message", onMessageReceived)
|
|
920
|
-
|
|
921
|
-
ws.on("open", async () => {
|
|
922
|
-
try {
|
|
923
|
-
await validateConnection()
|
|
924
|
-
} catch (err) {
|
|
925
|
-
logger.error({ err }, "Error in validating connection")
|
|
926
|
-
end(err)
|
|
927
|
-
}
|
|
928
|
-
})
|
|
929
|
-
|
|
930
|
-
ws.on("error", (err) => {
|
|
931
|
-
const isNetworkTimeout = err?.code === "ETIMEDOUT" || err?.code === "ECONNREFUSED"
|
|
932
|
-
const isNetworkError = err?.code === "ENOTFOUND" || err?.code === "ECONNRESET" || isNetworkTimeout
|
|
933
|
-
|
|
934
|
-
const errorDetails = {
|
|
935
|
-
message: err?.message || "Unknown error",
|
|
936
|
-
code: err?.code,
|
|
937
|
-
isNetworkError,
|
|
938
|
-
stack: err?.stack
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (isNetworkTimeout) {
|
|
942
|
-
logger.warn(errorDetails, "WebSocket connection timeout - network may be unreachable")
|
|
943
|
-
} else if (isNetworkError) {
|
|
944
|
-
logger.warn(errorDetails, "WebSocket network error - will attempt reconnection")
|
|
945
|
-
} else {
|
|
946
|
-
logger.warn(errorDetails, "WebSocket error occurred")
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Trigger reconnection on critical errors
|
|
950
|
-
if (isNetworkError && !closed) {
|
|
951
|
-
attemptReconnection("websocket-error").catch(err => {
|
|
952
|
-
logger.error({ err }, "Reconnection attempt failed after WebSocket error")
|
|
953
|
-
})
|
|
954
|
-
}
|
|
955
|
-
})
|
|
956
|
-
|
|
957
|
-
ws.on("close", (code, reason) => {
|
|
958
|
-
const closeReason = reason?.toString() || "Unknown"
|
|
959
|
-
logger.debug({ code, reason: closeReason }, "WebSocket closed")
|
|
960
|
-
|
|
961
|
-
if (!closed) {
|
|
962
|
-
const delayMs = code === 1000 ? 1000 : 2000
|
|
963
|
-
setTimeout(() => {
|
|
964
|
-
attemptReconnection("websocket-close").catch(err => {
|
|
965
|
-
logger.error({ err, code, reason: closeReason }, "Reconnection failed")
|
|
966
|
-
end(new Boom("Connection Terminated", { statusCode: DisconnectReason.connectionClosed }))
|
|
967
|
-
})
|
|
968
|
-
}, delayMs)
|
|
969
|
-
}
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
ws.on("CB:xmlstreamend", () => {
|
|
973
|
-
logger.info("Stream ended by server")
|
|
974
|
-
if (!closed) {
|
|
975
|
-
end(new Boom("Connection Terminated by Server", { statusCode: DisconnectReason.connectionClosed }))
|
|
976
|
-
}
|
|
977
|
-
})
|
|
978
|
-
|
|
979
|
-
// ==================== PAIRING HANDLERS ====================
|
|
980
|
-
ws.on("CB:iq,type:set,pair-device", async (stanza) => {
|
|
981
|
-
await sendNode({
|
|
982
|
-
tag: "iq",
|
|
983
|
-
attrs: { to: S_WHATSAPP_NET, type: "result", id: stanza.attrs.id }
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
const pairDeviceNode = getBinaryNodeChild(stanza, "pair-device")
|
|
987
|
-
const refNodes = getBinaryNodeChildren(pairDeviceNode, "ref")
|
|
988
|
-
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString("base64")
|
|
989
|
-
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString("base64")
|
|
990
|
-
const advB64 = creds.advSecretKey
|
|
991
|
-
|
|
992
|
-
let qrMs = qrTimeout || 60000
|
|
993
|
-
|
|
994
|
-
const genPairQR = () => {
|
|
995
|
-
if (!ws.isOpen) return
|
|
996
|
-
|
|
997
|
-
const refNode = refNodes.shift()
|
|
998
|
-
if (!refNode) {
|
|
999
|
-
end(new Boom("QR refs attempts ended", { statusCode: DisconnectReason.timedOut }))
|
|
1000
|
-
return
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const ref = refNode.content.toString("utf-8")
|
|
1004
|
-
const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(",")
|
|
1005
|
-
ev.emit("connection.update", { qr })
|
|
1006
|
-
|
|
1007
|
-
qrTimer = setTimeout(genPairQR, qrMs)
|
|
1008
|
-
qrMs = qrTimeout || 20000
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
genPairQR()
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
ws.on("CB:iq,,pair-success", async (stanza) => {
|
|
1015
|
-
logger.debug("Pair success received")
|
|
1016
|
-
|
|
1017
|
-
try {
|
|
1018
|
-
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
|
|
1019
|
-
logger.info(
|
|
1020
|
-
{ me: updatedCreds.me, platform: updatedCreds.platform },
|
|
1021
|
-
"Pairing configured successfully"
|
|
1022
|
-
)
|
|
1023
|
-
|
|
1024
|
-
ev.emit("creds.update", updatedCreds)
|
|
1025
|
-
ev.emit("connection.update", { isNewLogin: true, qr: undefined })
|
|
1026
|
-
triggerPreKeyCheck("device-paired", "high")
|
|
1027
|
-
|
|
1028
|
-
await sendNode(reply)
|
|
1029
|
-
} catch (error) {
|
|
1030
|
-
logger.info({ trace: error.stack }, "Error in pairing")
|
|
1031
|
-
end(error)
|
|
1032
|
-
}
|
|
1033
|
-
})
|
|
1034
|
-
|
|
1035
|
-
// ==================== CONNECTION SUCCESS ====================
|
|
1036
|
-
ws.on("CB:success", async (node) => {
|
|
1037
|
-
try {
|
|
1038
|
-
await uploadPreKeysToServerIfRequired()
|
|
1039
|
-
await sendPassiveIq("active")
|
|
1040
|
-
} catch (err) {
|
|
1041
|
-
logger.warn({ err }, "Failed to send initial passive IQ")
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
logger.info("✅ Opened connection to WhatsApp")
|
|
1045
|
-
clearTimeout(qrTimer)
|
|
1046
|
-
|
|
1047
|
-
triggerPreKeyCheck("connection-established", "high")
|
|
1048
|
-
startPreKeyBackgroundMonitor()
|
|
1049
|
-
startSessionCleanup()
|
|
1050
|
-
|
|
1051
|
-
ev.emit("creds.update", { me: { ...authState.creds.me, lid: node.attrs.lid } })
|
|
1052
|
-
ev.emit("connection.update", { connection: "open" })
|
|
1053
|
-
|
|
1054
|
-
startSessionHealthMonitor()
|
|
1055
|
-
reconnectAttempts = 0
|
|
1056
|
-
|
|
1057
|
-
// Handle LID session creation
|
|
1058
|
-
if (node.attrs.lid && authState.creds.me?.id) {
|
|
1059
|
-
const myLID = node.attrs.lid
|
|
1060
|
-
process.nextTick(async () => {
|
|
1061
|
-
try {
|
|
1062
|
-
const myPN = authState.creds.me.id
|
|
1063
|
-
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
|
|
1064
|
-
|
|
1065
|
-
const { user, device } = jidDecode(myPN)
|
|
1066
|
-
const existingData = await authState.keys.get("device-list", ["_index"])
|
|
1067
|
-
const currentBatch = existingData?.['_index'] || {}
|
|
1068
|
-
currentBatch[user] = [device?.toString() || "0"]
|
|
1069
|
-
|
|
1070
|
-
// Enforce batch size limit
|
|
1071
|
-
const deviceKeys = Object.keys(currentBatch).filter(k => k !== '_index')
|
|
1072
|
-
if (deviceKeys.length > BATCH_SIZE) {
|
|
1073
|
-
deviceKeys.sort()
|
|
1074
|
-
const toRemove = deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE)
|
|
1075
|
-
toRemove.forEach(k => delete currentBatch[k])
|
|
1076
|
-
logger.debug(`Cleaned up ${toRemove.length} old device-list entries (kept ${BATCH_SIZE})`)
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
await authState.keys.set({ "device-list": { "_index": currentBatch } })
|
|
1080
|
-
await signalRepository.migrateSession(myPN, myLID)
|
|
1081
|
-
|
|
1082
|
-
logger.info({ myPN, myLID }, "Own LID session created successfully")
|
|
1083
|
-
} catch (error) {
|
|
1084
|
-
logger.error({ error, lid: myLID }, "Failed to create own LID session")
|
|
1085
|
-
}
|
|
1086
|
-
})
|
|
1087
|
-
}
|
|
1088
|
-
})
|
|
1089
|
-
|
|
1090
|
-
// ==================== ERROR HANDLERS ====================
|
|
1091
|
-
ws.on('CB:stream:error', (node) => {
|
|
1092
|
-
logger.error({ node }, 'Stream errored out')
|
|
1093
|
-
const { reason, statusCode } = getErrorCodeFromStreamError(node)
|
|
1094
|
-
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }))
|
|
1095
|
-
|
|
1096
|
-
if (statusCode === 500 || statusCode === 440) {
|
|
1097
|
-
logger.debug("Triggering background pre-key check after stream error")
|
|
1098
|
-
triggerPreKeyCheck("stream-error-recovery", "normal")
|
|
1099
|
-
}
|
|
1100
|
-
})
|
|
1101
|
-
|
|
1102
|
-
ws.on("CB:failure", (node) => {
|
|
1103
|
-
const reason = +(node.attrs.reason || 500)
|
|
1104
|
-
end(new Boom("Connection Failure", { statusCode: reason, data: node.attrs }))
|
|
1105
|
-
})
|
|
1106
|
-
|
|
1107
|
-
ws.on("CB:ib,,downgrade_webclient", () => {
|
|
1108
|
-
end(new Boom("Multi-device beta not joined", { statusCode: DisconnectReason.multideviceMismatch }))
|
|
1109
|
-
})
|
|
1110
|
-
|
|
1111
|
-
ws.on("CB:ib,,offline_preview", (node) => {
|
|
1112
|
-
logger.info("Offline preview received", JSON.stringify(node))
|
|
1113
|
-
sendNode({
|
|
1114
|
-
tag: "ib",
|
|
1115
|
-
attrs: {},
|
|
1116
|
-
content: [{ tag: "offline_batch", attrs: { count: "100" } }]
|
|
1117
|
-
})
|
|
1118
|
-
})
|
|
1119
|
-
|
|
1120
|
-
ws.on("CB:ib,,edge_routing", (node) => {
|
|
1121
|
-
const edgeRoutingNode = getBinaryNodeChild(node, "edge_routing")
|
|
1122
|
-
const routingInfo = getBinaryNodeChild(edgeRoutingNode, "routing_info")
|
|
1123
|
-
|
|
1124
|
-
if (routingInfo?.content) {
|
|
1125
|
-
authState.creds.routingInfo = Buffer.from(routingInfo?.content)
|
|
1126
|
-
ev.emit("creds.update", authState.creds)
|
|
1127
|
-
}
|
|
1128
|
-
})
|
|
1129
|
-
|
|
1130
|
-
// ==================== OFFLINE NOTIFICATIONS ====================
|
|
1131
|
-
let didStartBuffer = false
|
|
1132
|
-
process.nextTick(() => {
|
|
1133
|
-
if (creds.me?.id) {
|
|
1134
|
-
ev.buffer()
|
|
1135
|
-
didStartBuffer = true
|
|
1136
|
-
}
|
|
1137
|
-
ev.emit("connection.update", {
|
|
1138
|
-
connection: "connecting",
|
|
1139
|
-
receivedPendingNotifications: false,
|
|
1140
|
-
qr: undefined
|
|
1141
|
-
})
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1144
|
-
ws.on("CB:ib,,offline", (node) => {
|
|
1145
|
-
const child = getBinaryNodeChild(node, "offline")
|
|
1146
|
-
const offlineNotifs = +(child?.attrs.count || 0)
|
|
1147
|
-
logger.info(`Handled ${offlineNotifs} offline messages/notifications`)
|
|
1148
|
-
|
|
1149
|
-
if (didStartBuffer) {
|
|
1150
|
-
ev.flush()
|
|
1151
|
-
logger.trace("Flushed events for initial buffer")
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
ev.emit("connection.update", { receivedPendingNotifications: true })
|
|
1155
|
-
})
|
|
1156
|
-
|
|
1157
|
-
// ==================== CREDENTIALS UPDATE ====================
|
|
1158
|
-
ev.on("creds.update", (update) => {
|
|
1159
|
-
const name = update.me?.name
|
|
1160
|
-
if (creds.me?.name !== name) {
|
|
1161
|
-
logger.debug({ name }, "Updated pushName")
|
|
1162
|
-
sendNode({ tag: "presence", attrs: { name } }).catch((err) =>
|
|
1163
|
-
logger.warn({ trace: err.stack }, "Error in sending presence update on name change")
|
|
1164
|
-
)
|
|
1165
|
-
}
|
|
1166
|
-
Object.assign(creds, update)
|
|
1167
|
-
})
|
|
1168
|
-
|
|
1169
|
-
// ==================== RETURN SOCKET API ====================
|
|
1170
|
-
return {
|
|
1171
|
-
type: "md",
|
|
1172
|
-
ws,
|
|
1173
|
-
ev,
|
|
1174
|
-
authState: { creds, keys },
|
|
1175
|
-
signalRepository,
|
|
1176
|
-
get user() {
|
|
1177
|
-
return authState.creds.me
|
|
1178
|
-
},
|
|
1179
|
-
generateMessageTag,
|
|
1180
|
-
query,
|
|
1181
|
-
waitForMessage,
|
|
1182
|
-
waitForSocketOpen,
|
|
1183
|
-
sendRawMessage,
|
|
1184
|
-
sendNode,
|
|
1185
|
-
logout,
|
|
1186
|
-
end,
|
|
1187
|
-
onUnexpectedError,
|
|
1188
|
-
uploadPreKeys,
|
|
1189
|
-
uploadPreKeysToServerIfRequired,
|
|
1190
|
-
requestPairingCode,
|
|
1191
|
-
wamBuffer: publicWAMBuffer,
|
|
1192
|
-
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
|
1193
|
-
sendWAMBuffer,
|
|
1194
|
-
executeUSyncQuery,
|
|
1195
|
-
onWhatsApp,
|
|
1196
|
-
listener: (eventName) => {
|
|
1197
|
-
if (typeof ev.listenerCount === "function") return ev.listenerCount(eventName)
|
|
1198
|
-
if (typeof ev.listener === "function") return ev.listener(eventName)?.length || 0
|
|
1199
|
-
return 0
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1
|
+
import { Boom } from "@hapi/boom"
|
|
2
|
+
import { randomBytes } from "crypto"
|
|
3
|
+
import { URL } from "url"
|
|
4
|
+
import { promisify } from "util"
|
|
5
|
+
import { proto } from "../../WAProto/index.js"
|
|
6
|
+
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, UPLOAD_TIMEOUT, BATCH_SIZE, TimeMs } from "../Defaults/index.js"
|
|
7
|
+
import { DisconnectReason } from "../Types/index.js"
|
|
8
|
+
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler, promiseTimeout, signedKeyPair, xmppSignedPreKey } from "../Utils/index.js"
|
|
9
|
+
import { getPlatformId, migrateIndexKey } from "../Utils/index.js"
|
|
10
|
+
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from "../WABinary/index.js"
|
|
11
|
+
import { BinaryInfo } from "../WAM/BinaryInfo.js"
|
|
12
|
+
import { USyncQuery, USyncUser } from "../WAUSync/index.js"
|
|
13
|
+
import { WebSocketClient } from "./Client/index.js"
|
|
14
|
+
|
|
15
|
+
// ─── Module-scope helpers ──────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Map a raw WebSocket error into a Boom so callers
|
|
19
|
+
* can inspect the statusCode / DisconnectReason.
|
|
20
|
+
*/
|
|
21
|
+
const mapWebSocketError = (handler) => (error) =>
|
|
22
|
+
handler(
|
|
23
|
+
new Boom(`WebSocket Error (${error?.message})`, {
|
|
24
|
+
statusCode: getCodeFromWSError(error),
|
|
25
|
+
data: error
|
|
26
|
+
})
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export const makeSocket = (config) => {
|
|
32
|
+
const {
|
|
33
|
+
waWebSocketUrl,
|
|
34
|
+
connectTimeoutMs,
|
|
35
|
+
logger,
|
|
36
|
+
keepAliveIntervalMs,
|
|
37
|
+
browser,
|
|
38
|
+
auth: authState,
|
|
39
|
+
printQRInTerminal,
|
|
40
|
+
defaultQueryTimeoutMs,
|
|
41
|
+
transactionOpts,
|
|
42
|
+
qrTimeout,
|
|
43
|
+
makeSignalRepository
|
|
44
|
+
} = config
|
|
45
|
+
|
|
46
|
+
if (printQRInTerminal) logger?.warn("printQRInTerminal deprecated")
|
|
47
|
+
|
|
48
|
+
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
|
|
49
|
+
|
|
50
|
+
if (config.mobile || url.protocol === 'tcp:')
|
|
51
|
+
throw new Boom('Mobile API not supported', { statusCode: DisconnectReason.loggedOut })
|
|
52
|
+
|
|
53
|
+
if (url.protocol === 'wss:' && authState?.creds?.routingInfo)
|
|
54
|
+
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
|
|
55
|
+
|
|
56
|
+
const ephemeralKeyPair = Curve.generateKeyPair()
|
|
57
|
+
const noise = makeNoiseHandler({
|
|
58
|
+
keyPair: ephemeralKeyPair,
|
|
59
|
+
NOISE_HEADER: NOISE_WA_HEADER,
|
|
60
|
+
logger,
|
|
61
|
+
routingInfo: authState?.creds?.routingInfo
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const ws = new WebSocketClient(url, config)
|
|
65
|
+
logger.info({ url: url.toString() }, 'Initiating WebSocket connection')
|
|
66
|
+
ws.connect()
|
|
67
|
+
|
|
68
|
+
const ev = makeEventBuffer(logger)
|
|
69
|
+
const { creds } = authState
|
|
70
|
+
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
|
71
|
+
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync)
|
|
72
|
+
const publicWAMBuffer = new BinaryInfo()
|
|
73
|
+
const uqTagId = generateMdTagPrefix()
|
|
74
|
+
const sendPromise = promisify(ws.send)
|
|
75
|
+
|
|
76
|
+
let epoch = 1
|
|
77
|
+
let lastDateRecv
|
|
78
|
+
let lastUploadTime = 0
|
|
79
|
+
let uploadPreKeysPromise = null
|
|
80
|
+
let closed = false
|
|
81
|
+
let keepAliveReq
|
|
82
|
+
let qrTimer
|
|
83
|
+
let serverTimeOffsetMs = 0
|
|
84
|
+
|
|
85
|
+
const generateMessageTag = () => `${uqTagId}${epoch++}`
|
|
86
|
+
|
|
87
|
+
// ─── Transport ──────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const sendRawMessage = async (data) => {
|
|
90
|
+
if (!ws.isOpen)
|
|
91
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
|
92
|
+
const bytes = noise.encodeFrame(data)
|
|
93
|
+
await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
|
|
94
|
+
try { await sendPromise.call(ws, bytes); resolve() }
|
|
95
|
+
catch (error) { reject(error) }
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const sendNode = (frame) => {
|
|
100
|
+
if (logger.level === 'trace') logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
|
|
101
|
+
return sendRawMessage(encodeBinaryNode(frame))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Query / messaging ──────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
|
|
107
|
+
let onRecv, onErr
|
|
108
|
+
try {
|
|
109
|
+
return await promiseTimeout(timeoutMs, (resolve, reject) => {
|
|
110
|
+
onRecv = (data) => resolve(data)
|
|
111
|
+
onErr = (err) =>
|
|
112
|
+
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
|
|
113
|
+
ws.on(`TAG:${msgId}`, onRecv)
|
|
114
|
+
ws.on('close', onErr)
|
|
115
|
+
ws.on('error', onErr)
|
|
116
|
+
return () => reject(new Boom('Query Cancelled'))
|
|
117
|
+
})
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
|
|
120
|
+
logger?.warn?.({ msgId }, 'timed out waiting for message')
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
throw error
|
|
124
|
+
} finally {
|
|
125
|
+
if (onRecv) ws.off(`TAG:${msgId}`, onRecv)
|
|
126
|
+
if (onErr) { ws.off('close', onErr); ws.off('error', onErr) }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const query = async (node, timeoutMs) => {
|
|
131
|
+
if (!node.attrs.id) node.attrs.id = generateMessageTag()
|
|
132
|
+
const msgId = node.attrs.id
|
|
133
|
+
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
134
|
+
const result = waitForMessage(msgId, timeoutMs).catch(reject)
|
|
135
|
+
sendNode(node).then(async () => resolve(await result)).catch(reject)
|
|
136
|
+
})
|
|
137
|
+
if (result && 'tag' in result) assertNodeErrorFree(result)
|
|
138
|
+
return result
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── USync ──────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
const executeUSyncQuery = async (usyncQuery) => {
|
|
144
|
+
if (usyncQuery.protocols.length === 0)
|
|
145
|
+
throw new Boom('USyncQuery must have at least one protocol')
|
|
146
|
+
const userNodes = usyncQuery.users.map((user) => ({
|
|
147
|
+
tag: 'user',
|
|
148
|
+
attrs: { jid: !user.phone ? user.id : undefined },
|
|
149
|
+
content: usyncQuery.protocols.map((a) => a.getUserElement(user)).filter((a) => a !== null)
|
|
150
|
+
}))
|
|
151
|
+
const iq = {
|
|
152
|
+
tag: 'iq',
|
|
153
|
+
attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' },
|
|
154
|
+
content: [{
|
|
155
|
+
tag: 'usync',
|
|
156
|
+
attrs: { context: usyncQuery.context, mode: usyncQuery.mode, sid: generateMessageTag(), last: 'true', index: '0' },
|
|
157
|
+
content: [
|
|
158
|
+
{ tag: 'query', attrs: {}, content: usyncQuery.protocols.map((a) => a.getQueryElement()) },
|
|
159
|
+
{ tag: 'list', attrs: {}, content: userNodes }
|
|
160
|
+
]
|
|
161
|
+
}]
|
|
162
|
+
}
|
|
163
|
+
return usyncQuery.parseUSyncQueryResult(await query(iq))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function pnFromLIDUSync(jids) {
|
|
167
|
+
const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background')
|
|
168
|
+
for (const jid of jids) {
|
|
169
|
+
if (!isLidUser(jid)) usyncQuery.withUser(new USyncUser().withId(jid))
|
|
170
|
+
else logger?.warn('LID user found in LID fetch call')
|
|
171
|
+
}
|
|
172
|
+
if (usyncQuery.users.length === 0) return []
|
|
173
|
+
const results = await executeUSyncQuery(usyncQuery)
|
|
174
|
+
return results ? results.list.filter((a) => !!a.lid).map(({ lid, id }) => ({ pn: id, lid })) : []
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Pre-keys ───────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
const getAvailablePreKeysOnServer = async () => {
|
|
180
|
+
const result = await query({
|
|
181
|
+
tag: 'iq',
|
|
182
|
+
attrs: { id: generateMessageTag(), xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET },
|
|
183
|
+
content: [{ tag: 'count', attrs: {} }]
|
|
184
|
+
})
|
|
185
|
+
return +getBinaryNodeChild(result, 'count').attrs.value
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
|
|
189
|
+
if (retryCount === 0 && Date.now() - lastUploadTime < MIN_UPLOAD_INTERVAL) {
|
|
190
|
+
logger.debug(`Skipping upload, only ${Date.now() - lastUploadTime}ms since last`)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
if (uploadPreKeysPromise) {
|
|
194
|
+
logger.debug('Pre-key upload in progress, waiting...')
|
|
195
|
+
await uploadPreKeysPromise
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
const uploadLogic = async () => {
|
|
199
|
+
logger.info({ count, retryCount }, 'Uploading pre-keys')
|
|
200
|
+
const node = await keys.transaction(async () => {
|
|
201
|
+
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
|
|
202
|
+
ev.emit('creds.update', update)
|
|
203
|
+
return node
|
|
204
|
+
}, creds?.me?.id || 'upload-pre-keys')
|
|
205
|
+
try {
|
|
206
|
+
await query(node)
|
|
207
|
+
logger.info({ count }, '✅ Pre-keys uploaded successfully')
|
|
208
|
+
lastUploadTime = Date.now()
|
|
209
|
+
} catch (uploadError) {
|
|
210
|
+
logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys')
|
|
211
|
+
if (retryCount < 3) {
|
|
212
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000)
|
|
213
|
+
logger.info(`Retrying pre-key upload in ${backoffDelay}ms`)
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay))
|
|
215
|
+
return uploadPreKeys(count, retryCount + 1)
|
|
216
|
+
}
|
|
217
|
+
throw uploadError
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
uploadPreKeysPromise = Promise.race([
|
|
221
|
+
uploadLogic(),
|
|
222
|
+
new Promise((_, reject) =>
|
|
223
|
+
setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT)
|
|
224
|
+
)
|
|
225
|
+
])
|
|
226
|
+
try { await uploadPreKeysPromise } finally { uploadPreKeysPromise = null }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const uploadPreKeysToServerIfRequired = async () => {
|
|
230
|
+
try {
|
|
231
|
+
const preKeyCount = await getAvailablePreKeysOnServer()
|
|
232
|
+
logger.info(`${preKeyCount} pre-keys found on server`)
|
|
233
|
+
if (preKeyCount < MIN_PREKEY_COUNT) {
|
|
234
|
+
const uploadCount = INITIAL_PREKEY_COUNT - preKeyCount
|
|
235
|
+
logger.info(`Server pre-key count low (${preKeyCount}), uploading ${uploadCount}`)
|
|
236
|
+
await uploadPreKeys(uploadCount)
|
|
237
|
+
} else {
|
|
238
|
+
logger.info(`✅ PreKey validation passed - Server: ${preKeyCount} pre-keys`)
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error({ error }, 'Failed to check/upload pre-keys during init')
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Key-bundle digest & signed pre-key rotation ────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Validate our current key-bundle against the server.
|
|
249
|
+
* If the server returns no digest node our keys are out of sync —
|
|
250
|
+
* force a pre-key upload and surface the error so the caller can decide.
|
|
251
|
+
*/
|
|
252
|
+
const digestKeyBundle = async () => {
|
|
253
|
+
const res = await query({
|
|
254
|
+
tag: 'iq',
|
|
255
|
+
attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
|
|
256
|
+
content: [{ tag: 'digest', attrs: {} }]
|
|
257
|
+
})
|
|
258
|
+
const digestNode = getBinaryNodeChild(res, 'digest')
|
|
259
|
+
if (!digestNode) {
|
|
260
|
+
await uploadPreKeys()
|
|
261
|
+
throw new Error('encrypt/get digest returned no digest node')
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Rotate our signed pre-key on the server.
|
|
267
|
+
* Should be called periodically (e.g. every 7 days) to keep sessions healthy.
|
|
268
|
+
*/
|
|
269
|
+
const rotateSignedPreKey = async () => {
|
|
270
|
+
const newId = (creds.signedPreKey.keyId || 0) + 1
|
|
271
|
+
const skey = await signedKeyPair(creds.signedIdentityKey, newId)
|
|
272
|
+
await query({
|
|
273
|
+
tag: 'iq',
|
|
274
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
|
|
275
|
+
content: [{ tag: 'rotate', attrs: {}, content: [xmppSignedPreKey(skey)] }]
|
|
276
|
+
})
|
|
277
|
+
ev.emit('creds.update', { signedPreKey: skey })
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Server time offset ─────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
const updateServerTimeOffset = ({ attrs }) => {
|
|
283
|
+
const tValue = attrs?.t
|
|
284
|
+
if (!tValue) return
|
|
285
|
+
const parsed = Number(tValue)
|
|
286
|
+
if (Number.isNaN(parsed) || parsed <= 0) return
|
|
287
|
+
serverTimeOffsetMs = parsed * 1000 - Date.now()
|
|
288
|
+
logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Unified session telemetry ───────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
const getUnifiedSessionId = () => {
|
|
294
|
+
const offsetMs = 3 * TimeMs.Day
|
|
295
|
+
const now = Date.now() + serverTimeOffsetMs
|
|
296
|
+
return ((now + offsetMs) % TimeMs.Week).toString()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const sendUnifiedSession = async () => {
|
|
300
|
+
if (!ws.isOpen) return
|
|
301
|
+
try {
|
|
302
|
+
await sendNode({
|
|
303
|
+
tag: 'ib',
|
|
304
|
+
attrs: {},
|
|
305
|
+
content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
|
|
306
|
+
})
|
|
307
|
+
} catch (error) {
|
|
308
|
+
logger.debug({ error }, 'failed to send unified_session telemetry')
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── Connection lifecycle ────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
const onUnexpectedError = (err, msg) => {
|
|
315
|
+
logger.error({ err }, `unexpected error in '${msg}'`)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const awaitNextMessage = async (sendMsg) => {
|
|
319
|
+
if (!ws.isOpen)
|
|
320
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
|
321
|
+
let onOpen, onClose
|
|
322
|
+
const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
|
|
323
|
+
onOpen = resolve
|
|
324
|
+
onClose = mapWebSocketError(reject)
|
|
325
|
+
ws.on('frame', onOpen)
|
|
326
|
+
ws.on('close', onClose)
|
|
327
|
+
ws.on('error', onClose)
|
|
328
|
+
}).finally(() => {
|
|
329
|
+
ws.off('frame', onOpen)
|
|
330
|
+
ws.off('close', onClose)
|
|
331
|
+
ws.off('error', onClose)
|
|
332
|
+
})
|
|
333
|
+
if (sendMsg) sendRawMessage(sendMsg).catch(onClose)
|
|
334
|
+
return result
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const validateConnection = async () => {
|
|
338
|
+
let helloMsg = proto.HandshakeMessage.fromObject({ clientHello: { ephemeral: ephemeralKeyPair.public } })
|
|
339
|
+
logger.info({ browser, helloMsg }, 'Connected to WhatsApp')
|
|
340
|
+
const init = proto.HandshakeMessage.encode(helloMsg).finish()
|
|
341
|
+
const result = await awaitNextMessage(init)
|
|
342
|
+
const handshake = proto.HandshakeMessage.decode(result)
|
|
343
|
+
logger.trace({ handshake }, 'Handshake received from WhatsApp')
|
|
344
|
+
const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
|
|
345
|
+
const node = !creds.me ? generateRegistrationNode(creds, config) : generateLoginNode(creds.me.id, config)
|
|
346
|
+
logger.info({ node }, !creds.me ? 'Attempting registration...' : 'Logging in...')
|
|
347
|
+
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
|
|
348
|
+
await sendRawMessage(
|
|
349
|
+
proto.HandshakeMessage.encode({
|
|
350
|
+
clientFinish: { static: keyEnc, payload: payloadEnc }
|
|
351
|
+
}).finish()
|
|
352
|
+
)
|
|
353
|
+
await noise.finishInit()
|
|
354
|
+
startKeepAliveRequest()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const waitForSocketOpen = async () => {
|
|
358
|
+
if (ws.isOpen) return
|
|
359
|
+
if (ws.isClosed || ws.isClosing)
|
|
360
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
|
361
|
+
let onOpen, onClose
|
|
362
|
+
await new Promise((resolve, reject) => {
|
|
363
|
+
onOpen = () => resolve(undefined)
|
|
364
|
+
onClose = mapWebSocketError(reject)
|
|
365
|
+
ws.on('open', onOpen)
|
|
366
|
+
ws.on('close', onClose)
|
|
367
|
+
ws.on('error', onClose)
|
|
368
|
+
}).finally(() => {
|
|
369
|
+
ws.off('open', onOpen)
|
|
370
|
+
ws.off('close', onClose)
|
|
371
|
+
ws.off('error', onClose)
|
|
372
|
+
})
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Keep-alive: ping WA every keepAliveIntervalMs.
|
|
377
|
+
* If the server stops responding (diff > interval + 5s) the connection
|
|
378
|
+
* is considered lost and we call end() — the consumer handles reconnection.
|
|
379
|
+
* No internal reconnect loop — clean separation of concerns.
|
|
380
|
+
*/
|
|
381
|
+
const startKeepAliveRequest = () => {
|
|
382
|
+
keepAliveReq = setInterval(() => {
|
|
383
|
+
if (!lastDateRecv) lastDateRecv = new Date()
|
|
384
|
+
const diff = Date.now() - lastDateRecv.getTime()
|
|
385
|
+
if (diff > keepAliveIntervalMs + 5000) {
|
|
386
|
+
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
|
387
|
+
} else if (ws.isOpen) {
|
|
388
|
+
query({
|
|
389
|
+
tag: 'iq',
|
|
390
|
+
attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:p' },
|
|
391
|
+
content: [{ tag: 'ping', attrs: {} }]
|
|
392
|
+
}).catch((err) => logger.error({ trace: err.stack }, 'error in sending keep alive'))
|
|
393
|
+
} else {
|
|
394
|
+
logger.warn('keep alive called when WS not open')
|
|
395
|
+
}
|
|
396
|
+
}, keepAliveIntervalMs)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const end = (error) => {
|
|
400
|
+
if (closed) { logger.trace({ trace: error?.stack }, 'Connection already closed'); return }
|
|
401
|
+
closed = true
|
|
402
|
+
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
|
403
|
+
clearInterval(keepAliveReq)
|
|
404
|
+
clearTimeout(qrTimer)
|
|
405
|
+
ws.removeAllListeners('close')
|
|
406
|
+
ws.removeAllListeners('open')
|
|
407
|
+
ws.removeAllListeners('message')
|
|
408
|
+
if (!ws.isClosed && !ws.isClosing) { try { ws.close() } catch { } }
|
|
409
|
+
ev.emit('connection.update', { connection: 'close', lastDisconnect: { error, date: new Date() } })
|
|
410
|
+
ev.removeAllListeners('connection.update')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const sendPassiveIq = (tag) =>
|
|
414
|
+
query({
|
|
415
|
+
tag: 'iq',
|
|
416
|
+
attrs: { to: S_WHATSAPP_NET, xmlns: 'passive', type: 'set' },
|
|
417
|
+
content: [{ tag, attrs: {} }]
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
const logout = async (msg) => {
|
|
421
|
+
const jid = authState.creds.me?.id
|
|
422
|
+
if (jid) {
|
|
423
|
+
await sendNode({
|
|
424
|
+
tag: 'iq',
|
|
425
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' },
|
|
426
|
+
content: [{ tag: 'remove-companion-device', attrs: { jid, reason: 'user_initiated' } }]
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── Pairing ─────────────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
435
|
+
await waitForSocketOpen()
|
|
436
|
+
await new Promise(resolve => setTimeout(resolve, 500))
|
|
437
|
+
const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
|
|
438
|
+
if (customPairingCode && customPairingCode?.length !== 8)
|
|
439
|
+
throw new Error('Custom pairing code must be exactly 8 chars')
|
|
440
|
+
authState.creds.pairingCode = pairingCode
|
|
441
|
+
authState.creds.me = { id: jidEncode(phoneNumber, 's.whatsapp.net'), name: '~' }
|
|
442
|
+
ev.emit('creds.update', authState.creds)
|
|
443
|
+
await sendNode({
|
|
444
|
+
tag: 'iq',
|
|
445
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' },
|
|
446
|
+
content: [{
|
|
447
|
+
tag: 'link_code_companion_reg',
|
|
448
|
+
attrs: { jid: authState.creds.me.id, stage: 'companion_hello', should_show_push_notification: 'true' },
|
|
449
|
+
content: [
|
|
450
|
+
{ tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
|
|
451
|
+
{ tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
|
|
452
|
+
{ tag: 'companion_platform_id', attrs: {}, content: getPlatformId(browser[1]) },
|
|
453
|
+
{ tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
|
|
454
|
+
{ tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
|
|
455
|
+
]
|
|
456
|
+
}]
|
|
457
|
+
})
|
|
458
|
+
return authState.creds.pairingCode
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function generatePairingKey() {
|
|
462
|
+
const salt = randomBytes(32)
|
|
463
|
+
const randomIv = randomBytes(16)
|
|
464
|
+
const key = await derivePairingCodeKey(authState.creds.pairingCode, salt)
|
|
465
|
+
const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv)
|
|
466
|
+
return Buffer.concat([salt, randomIv, ciphered])
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ─── Incoming message processing ────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
const onMessageReceived = (data) => {
|
|
472
|
+
noise.decodeFrame(data, (frame) => {
|
|
473
|
+
lastDateRecv = new Date()
|
|
474
|
+
let anyTriggered = ws.emit('frame', frame)
|
|
475
|
+
if (!(frame instanceof Uint8Array)) {
|
|
476
|
+
const msgId = frame.attrs.id
|
|
477
|
+
if (logger.level === 'trace') logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' })
|
|
478
|
+
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered
|
|
479
|
+
const l0 = frame.tag
|
|
480
|
+
const l1 = frame.attrs || {}
|
|
481
|
+
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
|
|
482
|
+
for (const key of Object.keys(l1)) {
|
|
483
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
|
|
484
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
|
|
485
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
|
|
486
|
+
}
|
|
487
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
|
|
488
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
|
|
489
|
+
if (!anyTriggered && logger.level === 'debug')
|
|
490
|
+
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'Unhandled communication received')
|
|
491
|
+
}
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const sendWAMBuffer = (wamBuffer) =>
|
|
496
|
+
query({
|
|
497
|
+
tag: 'iq',
|
|
498
|
+
attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
|
|
499
|
+
content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// ─── WebSocket event bindings ────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
ws.on('message', onMessageReceived)
|
|
505
|
+
|
|
506
|
+
ws.on('open', async () => {
|
|
507
|
+
try { await validateConnection() }
|
|
508
|
+
catch (err) { logger.error({ err }, 'error in validating connection'); end(err) }
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// Let mapWebSocketError convert the raw error then call end()
|
|
512
|
+
ws.on('error', mapWebSocketError(end))
|
|
513
|
+
|
|
514
|
+
// Any close → end(), consumer decides whether to reconnect
|
|
515
|
+
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
|
516
|
+
|
|
517
|
+
ws.on('CB:xmlstreamend', () => {
|
|
518
|
+
logger.info('Stream ended by server')
|
|
519
|
+
if (!closed) end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
// ─── QR pairing ─────────────────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
525
|
+
await sendNode({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'result', id: stanza.attrs.id } })
|
|
526
|
+
const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device')
|
|
527
|
+
const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref')
|
|
528
|
+
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64')
|
|
529
|
+
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64')
|
|
530
|
+
const advB64 = creds.advSecretKey
|
|
531
|
+
let qrMs = qrTimeout || 60000
|
|
532
|
+
const genPairQR = () => {
|
|
533
|
+
if (!ws.isOpen) return
|
|
534
|
+
const refNode = refNodes.shift()
|
|
535
|
+
if (!refNode) { end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
|
|
536
|
+
const ref = refNode.content.toString('utf-8')
|
|
537
|
+
const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',')
|
|
538
|
+
ev.emit('connection.update', { qr })
|
|
539
|
+
qrTimer = setTimeout(genPairQR, qrMs)
|
|
540
|
+
qrMs = qrTimeout || 20000
|
|
541
|
+
}
|
|
542
|
+
genPairQR()
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
ws.on('CB:iq,,pair-success', async (stanza) => {
|
|
546
|
+
logger.debug('Pair success received')
|
|
547
|
+
try {
|
|
548
|
+
updateServerTimeOffset(stanza)
|
|
549
|
+
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
|
|
550
|
+
logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'Pairing configured successfully')
|
|
551
|
+
ev.emit('creds.update', updatedCreds)
|
|
552
|
+
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
|
|
553
|
+
await sendNode(reply)
|
|
554
|
+
void sendUnifiedSession()
|
|
555
|
+
} catch (error) {
|
|
556
|
+
logger.info({ trace: error.stack }, 'Error in pairing')
|
|
557
|
+
end(error)
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
// ─── Login complete ──────────────────────────────────────────────────────────
|
|
562
|
+
|
|
563
|
+
ws.on('CB:success', async (node) => {
|
|
564
|
+
try {
|
|
565
|
+
updateServerTimeOffset(node)
|
|
566
|
+
await uploadPreKeysToServerIfRequired()
|
|
567
|
+
await sendPassiveIq('active')
|
|
568
|
+
try {
|
|
569
|
+
await digestKeyBundle()
|
|
570
|
+
} catch (e) {
|
|
571
|
+
logger.warn({ e }, 'failed to run digest after login')
|
|
572
|
+
}
|
|
573
|
+
} catch (err) {
|
|
574
|
+
logger.warn({ err }, 'Failed to send initial passive IQ')
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
logger.info('✅ Opened connection to WhatsApp')
|
|
578
|
+
clearTimeout(qrTimer)
|
|
579
|
+
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
|
|
580
|
+
ev.emit('connection.update', { connection: 'open' })
|
|
581
|
+
void sendUnifiedSession()
|
|
582
|
+
|
|
583
|
+
if (node.attrs.lid && authState.creds.me?.id) {
|
|
584
|
+
const myLID = node.attrs.lid
|
|
585
|
+
process.nextTick(async () => {
|
|
586
|
+
try {
|
|
587
|
+
const myPN = authState.creds.me.id
|
|
588
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
|
|
589
|
+
const { user, device } = jidDecode(myPN)
|
|
590
|
+
const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
|
|
591
|
+
currentBatch[user] = [device?.toString() || '0']
|
|
592
|
+
const deviceKeys = Object.keys(currentBatch)
|
|
593
|
+
if (deviceKeys.length > BATCH_SIZE) {
|
|
594
|
+
deviceKeys.sort()
|
|
595
|
+
deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach(k => delete currentBatch[k])
|
|
596
|
+
}
|
|
597
|
+
await authState.keys.set({ 'device-list': { 'index': currentBatch } })
|
|
598
|
+
await signalRepository.migrateSession(myPN, myLID)
|
|
599
|
+
logger.info({ myPN, myLID }, 'Own LID session created successfully')
|
|
600
|
+
if (signalRepository.migrateAllPNSessionsToLID) {
|
|
601
|
+
try {
|
|
602
|
+
const migrated = await signalRepository.migrateAllPNSessionsToLID()
|
|
603
|
+
if (migrated > 0) logger.info({ migrated }, 'Batch-migrated PN sessions to LID on connect')
|
|
604
|
+
} catch (migErr) {
|
|
605
|
+
logger.warn({ error: migErr }, 'Failed to batch-migrate PN sessions to LID')
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
logger.error({ error, lid: myLID }, 'Failed to create own LID session')
|
|
610
|
+
}
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
// ─── Stream / connection error handlers ─────────────────────────────────────
|
|
616
|
+
|
|
617
|
+
ws.on('CB:stream:error', (node) => {
|
|
618
|
+
logger.error({ node }, 'Stream errored out')
|
|
619
|
+
const { reason, statusCode } = getErrorCodeFromStreamError(node)
|
|
620
|
+
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }))
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
ws.on('CB:failure', (node) => {
|
|
624
|
+
const reason = +(node.attrs.reason || 500)
|
|
625
|
+
end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
ws.on('CB:ib,,downgrade_webclient', () =>
|
|
629
|
+
end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
ws.on('CB:ib,,offline_preview', (node) => {
|
|
633
|
+
logger.info('Offline preview received', JSON.stringify(node))
|
|
634
|
+
sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
ws.on('CB:ib,,edge_routing', (node) => {
|
|
638
|
+
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
|
|
639
|
+
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
|
|
640
|
+
if (routingInfo?.content) {
|
|
641
|
+
authState.creds.routingInfo = Buffer.from(routingInfo?.content)
|
|
642
|
+
ev.emit('creds.update', authState.creds)
|
|
643
|
+
}
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
// ─── Buffering & offline notifications ──────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
let didStartBuffer = false
|
|
649
|
+
process.nextTick(() => {
|
|
650
|
+
if (creds.me?.id) { ev.buffer(); didStartBuffer = true }
|
|
651
|
+
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
|
|
652
|
+
})
|
|
653
|
+
|
|
654
|
+
ws.on('CB:ib,,offline', (node) => {
|
|
655
|
+
const child = getBinaryNodeChild(node, 'offline')
|
|
656
|
+
const offlineNotifs = +(child?.attrs.count || 0)
|
|
657
|
+
logger.info(`Handled ${offlineNotifs} offline messages/notifications`)
|
|
658
|
+
if (didStartBuffer) { ev.flush(); logger.trace('Flushed events for initial buffer') }
|
|
659
|
+
ev.emit('connection.update', { receivedPendingNotifications: true })
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
// ─── Creds sync ──────────────────────────────────────────────────────────────
|
|
663
|
+
|
|
664
|
+
ev.on('creds.update', (update) => {
|
|
665
|
+
const name = update.me?.name
|
|
666
|
+
if (creds.me?.name !== name) {
|
|
667
|
+
logger.debug({ name }, 'Updated pushName')
|
|
668
|
+
sendNode({ tag: 'presence', attrs: { name } }).catch((err) =>
|
|
669
|
+
logger.warn({ trace: err.stack }, 'Error in sending presence update on name change')
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
Object.assign(creds, update)
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
type: 'md',
|
|
679
|
+
ws,
|
|
680
|
+
ev,
|
|
681
|
+
authState: { creds, keys },
|
|
682
|
+
signalRepository,
|
|
683
|
+
get user() { return authState.creds.me },
|
|
684
|
+
generateMessageTag,
|
|
685
|
+
query,
|
|
686
|
+
waitForMessage,
|
|
687
|
+
waitForSocketOpen,
|
|
688
|
+
sendRawMessage,
|
|
689
|
+
sendNode,
|
|
690
|
+
logout,
|
|
691
|
+
end,
|
|
692
|
+
onUnexpectedError,
|
|
693
|
+
uploadPreKeys,
|
|
694
|
+
uploadPreKeysToServerIfRequired,
|
|
695
|
+
digestKeyBundle,
|
|
696
|
+
rotateSignedPreKey,
|
|
697
|
+
updateServerTimeOffset,
|
|
698
|
+
sendUnifiedSession,
|
|
699
|
+
requestPairingCode,
|
|
700
|
+
wamBuffer: publicWAMBuffer,
|
|
701
|
+
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
|
702
|
+
sendWAMBuffer,
|
|
703
|
+
executeUSyncQuery,
|
|
704
|
+
listener: (eventName) => {
|
|
705
|
+
if (typeof ev.listenerCount === 'function') return ev.listenerCount(eventName)
|
|
706
|
+
if (typeof ev.listener === 'function') return ev.listener(eventName)?.length || 0
|
|
707
|
+
return 0
|
|
708
|
+
}
|
|
709
|
+
}
|
|
1202
710
|
}
|