@violetix/baileys 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1530 -2009
- package/WAProto/index.d.ts +14030 -14586
- package/WAProto/index.js +58426 -88874
- package/lib/Defaults/index.d.ts +2 -7
- package/lib/Defaults/index.js +5 -20
- package/lib/Signal/Group/sender-key-message.js +19 -1
- package/lib/Signal/libsignal.js +82 -29
- package/lib/Signal/xeddsa-fast.js +114 -0
- package/lib/Socket/Client/types.js +6 -8
- package/lib/Socket/Client/websocket.js +3 -38
- package/lib/Socket/business.d.ts +0 -3
- package/lib/Socket/chats.d.ts +0 -3
- package/lib/Socket/chats.js +18 -45
- package/lib/Socket/communities.d.ts +0 -3
- package/lib/Socket/graphql.js +3 -7
- package/lib/Socket/groups.d.ts +0 -3
- package/lib/Socket/groups.js +3 -8
- package/lib/Socket/index.d.ts +15 -17
- package/lib/Socket/messages-recv.d.ts +0 -3
- package/lib/Socket/messages-recv.js +43 -127
- package/lib/Socket/messages-send.d.ts +0 -4
- package/lib/Socket/messages-send.js +63 -191
- package/lib/Socket/newsletter.d.ts +0 -3
- package/lib/Socket/newsletter.js +1 -2
- package/lib/Socket/newsletter.js.map +1 -1
- package/lib/Socket/socket.d.ts +0 -9
- package/lib/Socket/socket.js +53 -341
- package/lib/Store/make-in-memory-store.js +30 -41
- package/lib/Types/Auth.d.ts +2 -60
- package/lib/Types/Bussines.d.ts +2 -9
- package/lib/Types/Call.d.ts +2 -22
- package/lib/Types/Chat.d.ts +1 -2
- package/lib/Types/Contact.d.ts +2 -11
- package/lib/Types/Events.d.ts +2 -41
- package/lib/Types/GroupMetadata.d.ts +2 -36
- package/lib/Types/Message.d.ts +1 -11
- package/lib/Types/Product.d.ts +2 -5
- package/lib/Types/Signal.d.ts +2 -10
- package/lib/Types/Socket.d.ts +2 -33
- package/lib/Types/USync.d.ts +2 -9
- package/lib/Types/index.d.ts +1 -3
- package/lib/Utils/auth-utils.d.ts +2 -3
- package/lib/Utils/auth-utils.js +53 -37
- package/lib/Utils/business.js +11 -25
- package/lib/Utils/chat-utils.js +4 -6
- package/lib/Utils/decode-wa-message.js +2 -2
- package/lib/Utils/event-buffer.js +12 -21
- package/lib/Utils/generics.d.ts +5 -5
- package/lib/Utils/generics.js +7 -25
- package/lib/Utils/history.d.ts +1 -2
- package/lib/Utils/history.js +83 -137
- package/lib/Utils/index.d.ts +0 -4
- package/lib/Utils/index.js +0 -4
- package/lib/Utils/listener-guard.js +0 -2
- package/lib/Utils/message-retry-manager.js +0 -20
- package/lib/Utils/messages-media.d.ts +8 -39
- package/lib/Utils/messages-media.js +89 -218
- package/lib/Utils/messages.d.ts +0 -1
- package/lib/Utils/messages.js +36 -122
- package/lib/Utils/process-message.d.ts +0 -18
- package/lib/Utils/process-message.js +47 -99
- package/lib/Utils/rich-message-utils.js +4 -3
- package/lib/Utils/use-multi-file-auth-state.d.ts +3 -5
- package/lib/Utils/use-multi-file-auth-state.js +6 -27
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +3 -12
- package/lib/WABinary/types.d.ts +2 -6
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +3 -6
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +2 -3
- package/lib/WAUSync/USyncQuery.d.ts +1 -1
- package/lib/WAUSync/USyncQuery.js +4 -7
- package/package.json +107 -123
- package/lib/Utils/a2ui.d.ts +0 -50
- package/lib/Utils/a2ui.js +0 -168
- package/lib/Utils/anchor-guard.d.ts +0 -71
- package/lib/Utils/anchor-guard.js +0 -404
- package/lib/Utils/connection-manager.d.ts +0 -44
- package/lib/Utils/connection-manager.js +0 -258
- package/lib/Utils/ignore-key.d.ts +0 -51
- package/lib/Utils/ignore-key.js +0 -176
package/lib/Socket/socket.js
CHANGED
|
@@ -6,9 +6,7 @@ import { proto } from '../../WAProto/index.js';
|
|
|
6
6
|
import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT, NOISE_WA_HEADER, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } from '../Defaults/index.js';
|
|
7
7
|
import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
|
|
8
8
|
import { DisconnectReason, XWAPaths } from '../Types/index.js';
|
|
9
|
-
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey,
|
|
10
|
-
import { ignoredStanzaNeedsAck, makeIgnoreKeyRegistry } from '../Utils/ignore-key.js';
|
|
11
|
-
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
9
|
+
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPairingCodePlatform, invalidPairingCodeChars, makeEventBuffer, makeNoiseHandler, normalizePairingCode, PAIRING_CODE_ALPHABET, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
|
|
12
10
|
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
13
11
|
import { BinaryInfo } from '../WAM/BinaryInfo.js';
|
|
14
12
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
@@ -21,23 +19,6 @@ import { executeWMexQuery } from './mex.js';
|
|
|
21
19
|
* - listen to messages and emit events
|
|
22
20
|
* - query phone connection
|
|
23
21
|
*/
|
|
24
|
-
/**
|
|
25
|
-
* Version the server accepted after it rejected the configured one with 405 (client_too_old).
|
|
26
|
-
* Process-wide on purpose: every socket built afterwards with a static `version` reuses it,
|
|
27
|
-
* so a stale hardcoded version heals on the next reconnect instead of looping on 405.
|
|
28
|
-
*/
|
|
29
|
-
let latestVersionOverride;
|
|
30
|
-
/**
|
|
31
|
-
* Reasons that end a session for good. Reconnecting any of these is how an unattended host turns
|
|
32
|
-
* one logout into an auth loop and gets the number banned.
|
|
33
|
-
*/
|
|
34
|
-
const DEAD_DISCONNECT_REASONS = new Set([
|
|
35
|
-
DisconnectReason.loggedOut,
|
|
36
|
-
DisconnectReason.forbidden,
|
|
37
|
-
DisconnectReason.multideviceMismatch,
|
|
38
|
-
DisconnectReason.badSession,
|
|
39
|
-
DisconnectReason.connectionReplaced
|
|
40
|
-
]);
|
|
41
22
|
export const makeSocket = (config) => {
|
|
42
23
|
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, keepAliveGracePeriodMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
|
|
43
24
|
const publicWAMBuffer = new BinaryInfo();
|
|
@@ -63,9 +44,9 @@ export const makeSocket = (config) => {
|
|
|
63
44
|
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
|
|
64
45
|
}
|
|
65
46
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
|
66
|
-
|
|
47
|
+
const ephemeralKeyPair = Curve.generateKeyPair();
|
|
67
48
|
/** WA noise protocol wrapper */
|
|
68
|
-
|
|
49
|
+
const noise = makeNoiseHandler({
|
|
69
50
|
keyPair: ephemeralKeyPair,
|
|
70
51
|
NOISE_HEADER: NOISE_WA_HEADER,
|
|
71
52
|
logger,
|
|
@@ -147,13 +128,12 @@ export const makeSocket = (config) => {
|
|
|
147
128
|
node.attrs.id = generateMessageTag();
|
|
148
129
|
}
|
|
149
130
|
const msgId = node.attrs.id;
|
|
150
|
-
pendingQueries++;
|
|
151
131
|
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
152
132
|
const result = waitForMessage(msgId, timeoutMs).catch(reject);
|
|
153
133
|
sendNode(node)
|
|
154
134
|
.then(async () => resolve(await result))
|
|
155
135
|
.catch(reject);
|
|
156
|
-
})
|
|
136
|
+
});
|
|
157
137
|
if (result && 'tag' in result) {
|
|
158
138
|
assertNodeErrorFree(result);
|
|
159
139
|
}
|
|
@@ -271,8 +251,7 @@ export const makeSocket = (config) => {
|
|
|
271
251
|
* where usernameKey is the PIN protecting the username.
|
|
272
252
|
*/
|
|
273
253
|
const onWhatsAppUsername = async (...queries) => {
|
|
274
|
-
|
|
275
|
-
const usyncQuery = new USyncQuery().withContactProtocol('lid').withUsernameProtocol().withBusinessProtocol(null);
|
|
254
|
+
const usyncQuery = new USyncQuery().withContactProtocol().withUsernameProtocol();
|
|
276
255
|
const normalized = [];
|
|
277
256
|
for (const query of queries) {
|
|
278
257
|
const raw = typeof query === 'string' ? query : query?.username;
|
|
@@ -302,10 +281,7 @@ export const makeSocket = (config) => {
|
|
|
302
281
|
return results.list.map((result, index) => ({
|
|
303
282
|
username: typeof result.username === 'string' ? result.username : normalized[index] || '',
|
|
304
283
|
jid: result.id,
|
|
305
|
-
exists: result.contact === true
|
|
306
|
-
keyRequired: result.keyRequired === true,
|
|
307
|
-
isBusiness: !!result.business,
|
|
308
|
-
pnJid: result.business?.pnJid || undefined
|
|
284
|
+
exists: result.contact === true
|
|
309
285
|
}));
|
|
310
286
|
};
|
|
311
287
|
/** fetch the @username of one or more jids, if they have one set */
|
|
@@ -384,31 +360,14 @@ export const makeSocket = (config) => {
|
|
|
384
360
|
};
|
|
385
361
|
const ev = makeEventBuffer(logger);
|
|
386
362
|
const { creds } = authState;
|
|
387
|
-
/**
|
|
388
|
-
* Last identity this socket has been logged in as. `sock.user` is a getter over
|
|
389
|
-
* `authState.creds.me`, and hosts read it as `sock.user.id` with no optional chaining (the
|
|
390
|
-
* @neoxr/wb engine does exactly that in serialize() and in its JID() helper). One stanza that
|
|
391
|
-
* arrives while `creds.me` is momentarily absent therefore throws inside the host's event
|
|
392
|
-
* listener instead of being processed. Keeping the identity we already know turns that into a
|
|
393
|
-
* correct answer: a socket that is receiving stanzas is logged in as this user.
|
|
394
|
-
*/
|
|
395
|
-
let lastMe = creds.me;
|
|
396
|
-
let userMissingLogged = false;
|
|
397
363
|
// add transaction capability
|
|
398
364
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
|
|
399
365
|
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
|
|
400
366
|
let lastDateRecv;
|
|
401
|
-
/** queries awaiting a reply; keep-alive skips its ping while these are flowing */
|
|
402
|
-
let pendingQueries = 0;
|
|
403
367
|
let epoch = 1;
|
|
404
368
|
let keepAliveReq;
|
|
405
369
|
let qrTimer;
|
|
406
370
|
let closed = false;
|
|
407
|
-
/** transparent transport reconnect state; see scheduleReconnect */
|
|
408
|
-
let reconnecting = false;
|
|
409
|
-
let reconnectAttempts = 0;
|
|
410
|
-
let reconnectTimer;
|
|
411
|
-
let reconnectWaitResolve;
|
|
412
371
|
/** set once WhatsApp sends pair-device; requestPairingCode queues until then */
|
|
413
372
|
let pairingReady = false;
|
|
414
373
|
let pairingInProgress = false;
|
|
@@ -417,8 +376,6 @@ export const makeSocket = (config) => {
|
|
|
417
376
|
let pendingPairingResolve;
|
|
418
377
|
let pendingPairingReject;
|
|
419
378
|
const socketEndHandlers = [];
|
|
420
|
-
/** per-connection caches (media host auth, ...) that must be dropped when the transport reopens */
|
|
421
|
-
const socketReconnectHandlers = [];
|
|
422
379
|
const timers = new Set();
|
|
423
380
|
const safeSetTimeout = (fn, ms) => { const id = setTimeout(fn, ms); timers.add(id); return id; };
|
|
424
381
|
const safeSetInterval = (fn, ms) => { const id = setInterval(fn, ms); timers.add(id); return id; };
|
|
@@ -465,19 +422,13 @@ export const makeSocket = (config) => {
|
|
|
465
422
|
const handshake = proto.HandshakeMessage.decode(result);
|
|
466
423
|
logger.trace({ handshake }, 'handshake recv from WA');
|
|
467
424
|
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
|
|
468
|
-
// `version` may be a resolver (e.g. () => fetchLatestWaWebVersion().then(r => r.version));
|
|
469
|
-
// a version the server already rejected as too old is replaced by the fetched one
|
|
470
|
-
const resolvedVersion = typeof config.version === 'function'
|
|
471
|
-
? await config.version()
|
|
472
|
-
: latestVersionOverride ?? config.version;
|
|
473
|
-
const connectConfig = resolvedVersion === config.version ? config : { ...config, version: resolvedVersion };
|
|
474
425
|
let node;
|
|
475
426
|
if (!creds.me) {
|
|
476
|
-
node = generateRegistrationNode(creds,
|
|
427
|
+
node = generateRegistrationNode(creds, config);
|
|
477
428
|
logger.info({ node }, 'not logged in, attempting registration...');
|
|
478
429
|
}
|
|
479
430
|
else {
|
|
480
|
-
node = generateLoginNode(creds.me.id,
|
|
431
|
+
node = generateLoginNode(creds.me.id, config);
|
|
481
432
|
logger.info({ node }, 'logging in...');
|
|
482
433
|
}
|
|
483
434
|
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
|
|
@@ -593,11 +544,6 @@ export const makeSocket = (config) => {
|
|
|
593
544
|
// Don't throw - allow connection to continue even if pre-key check fails
|
|
594
545
|
}
|
|
595
546
|
};
|
|
596
|
-
const ignoreKeyRegistry = makeIgnoreKeyRegistry({
|
|
597
|
-
getMeId: () => authState.creds.me?.id,
|
|
598
|
-
getMeLid: () => authState.creds.me?.lid,
|
|
599
|
-
logger
|
|
600
|
-
});
|
|
601
547
|
const onMessageReceived = async (data) => {
|
|
602
548
|
await noise.decodeFrame(data, frame => {
|
|
603
549
|
lastDateRecv = new Date();
|
|
@@ -608,16 +554,6 @@ export const makeSocket = (config) => {
|
|
|
608
554
|
if (logger.level === 'trace') {
|
|
609
555
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
610
556
|
}
|
|
611
|
-
// Filters run before every other handler, so a dropped stanza never reaches the
|
|
612
|
-
// decryption or persistence paths. The ack still goes out, otherwise the server
|
|
613
|
-
// keeps re-delivering what the host just asked to ignore.
|
|
614
|
-
if (ignoreKeyRegistry.shouldDrop(frame)) {
|
|
615
|
-
logger.debug({ tag: frame.tag, id: msgId, from: frame.attrs.from }, 'stanza dropped by ignoreKey');
|
|
616
|
-
if (ignoredStanzaNeedsAck(frame.tag)) {
|
|
617
|
-
sendNode(buildAckStanza(frame, undefined, authState.creds.me?.id)).catch(err => logger.error({ err, id: msgId }, 'failed to ack ignored stanza'));
|
|
618
|
-
}
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
557
|
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
622
558
|
const l0 = frame.tag;
|
|
623
559
|
const l1 = frame.attrs || {};
|
|
@@ -642,24 +578,6 @@ export const makeSocket = (config) => {
|
|
|
642
578
|
return;
|
|
643
579
|
}
|
|
644
580
|
closed = true;
|
|
645
|
-
// Name the disconnect reason in the message itself. The code was always on `error.output`,
|
|
646
|
-
// but a host does not always receive the Boom: a wrapper that re-emits this close as its own
|
|
647
|
-
// `error` event forwards `message` and nothing else, and the messages alone cannot be told
|
|
648
|
-
// apart — `CB:failure` says the constant 'Connection Failure' for a 401 device-removal and
|
|
649
|
-
// for a transient 503 alike. A host that cannot distinguish them keeps reconnecting a
|
|
650
|
-
// session that is really gone, which is how an unattended fleet turns one logout into a
|
|
651
|
-
// re-auth loop and gets the number banned.
|
|
652
|
-
//
|
|
653
|
-
// Done here rather than at each `end()` call site because there are a dozen of them and a
|
|
654
|
-
// new one would silently miss it. The existing text is kept as a prefix, so message matching
|
|
655
|
-
// that already works keeps working; the suffix is additive.
|
|
656
|
-
const statusCode = error?.output?.statusCode;
|
|
657
|
-
if (statusCode !== undefined && typeof error?.message === 'string' && !error.message.includes(`[${statusCode} `)) {
|
|
658
|
-
try {
|
|
659
|
-
error.message = `${error.message} [${statusCode} ${DisconnectReason[statusCode] || 'unknown'}]`;
|
|
660
|
-
}
|
|
661
|
-
catch { }
|
|
662
|
-
}
|
|
663
581
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
664
582
|
// Signal-key writes are debounced (addTransactionCapability). Nothing else in the library
|
|
665
583
|
// called flush, so every ratchet advance from the last window was dropped on close and the
|
|
@@ -678,9 +596,6 @@ export const makeSocket = (config) => {
|
|
|
678
596
|
keepAliveReq?.cancel();
|
|
679
597
|
clearSafeTimer(qrTimer);
|
|
680
598
|
clearAllTimers();
|
|
681
|
-
// release a pending reconnect wait so scheduleReconnect cannot stay suspended
|
|
682
|
-
reconnectWaitResolve?.();
|
|
683
|
-
reconnectWaitResolve = undefined;
|
|
684
599
|
// a queued requestPairingCode would hang forever otherwise
|
|
685
600
|
pairingReady = false;
|
|
686
601
|
pairingInProgress = false;
|
|
@@ -724,90 +639,6 @@ export const makeSocket = (config) => {
|
|
|
724
639
|
logger.error({ err }, 'error while closing connection');
|
|
725
640
|
}
|
|
726
641
|
};
|
|
727
|
-
/** rebuild the state that belongs to one transport connection, not to the socket object */
|
|
728
|
-
const resetTransport = () => {
|
|
729
|
-
keepAliveReq?.cancel();
|
|
730
|
-
keepAliveReq = undefined;
|
|
731
|
-
clearSafeTimer(qrTimer);
|
|
732
|
-
qrTimer = undefined;
|
|
733
|
-
lastDateRecv = undefined;
|
|
734
|
-
ephemeralKeyPair = Curve.generateKeyPair();
|
|
735
|
-
noise = makeNoiseHandler({
|
|
736
|
-
keyPair: ephemeralKeyPair,
|
|
737
|
-
NOISE_HEADER: NOISE_WA_HEADER,
|
|
738
|
-
logger,
|
|
739
|
-
routingInfo: authState?.creds?.routingInfo
|
|
740
|
-
});
|
|
741
|
-
// the media host token is issued per connection; a cached one fails every upload after this
|
|
742
|
-
for (const handler of socketReconnectHandlers) {
|
|
743
|
-
try {
|
|
744
|
-
handler();
|
|
745
|
-
}
|
|
746
|
-
catch (err) {
|
|
747
|
-
logger.error({ err }, 'error in socket reconnect handler');
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
};
|
|
751
|
-
/**
|
|
752
|
-
* Transparent reconnect. The WebSocket client object is kept and only the raw socket and the
|
|
753
|
-
* noise session under it are replaced, so every stanza listener registered by the layers above
|
|
754
|
-
* (messages-recv, chats, the host) keeps working and the host never sees a `close` for a drop
|
|
755
|
-
* that this socket can repair itself.
|
|
756
|
-
*/
|
|
757
|
-
const scheduleReconnect = async (error) => {
|
|
758
|
-
if (reconnecting || closed) {
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
reconnecting = true;
|
|
762
|
-
const statusCode = error?.output?.statusCode;
|
|
763
|
-
const maxAttempts = config.maxReconnectAttempts ?? 10;
|
|
764
|
-
if (!config.autoReconnect || DEAD_DISCONNECT_REASONS.has(statusCode) || reconnectAttempts >= maxAttempts) {
|
|
765
|
-
reconnecting = false;
|
|
766
|
-
return end(error);
|
|
767
|
-
}
|
|
768
|
-
reconnectAttempts += 1;
|
|
769
|
-
// 515 means the server wants the connection back now; backing off only delays it
|
|
770
|
-
const immediate = statusCode === DisconnectReason.restartRequired;
|
|
771
|
-
const base = config.reconnectDelayMs ?? 2000;
|
|
772
|
-
const ceiling = config.maxReconnectDelayMs ?? 60000;
|
|
773
|
-
const backoff = Math.min(ceiling, base * 2 ** (reconnectAttempts - 1));
|
|
774
|
-
// jitter keeps a fleet from reconnecting in lockstep after a server-side blip
|
|
775
|
-
const delayMs = immediate ? 0 : Math.round(backoff * (0.75 + Math.random() * 0.5));
|
|
776
|
-
logger.info({ statusCode, attempt: reconnectAttempts, delayMs }, 'connection lost, reconnecting');
|
|
777
|
-
// NOT 'close': a host reads close as "build a new socket", and a second socket for the same
|
|
778
|
-
// account is what earns a 440 connectionReplaced
|
|
779
|
-
ev.emit('connection.update', {
|
|
780
|
-
connection: 'connecting',
|
|
781
|
-
lastDisconnect: { error, date: new Date() }
|
|
782
|
-
});
|
|
783
|
-
await new Promise(resolve => {
|
|
784
|
-
reconnectWaitResolve = resolve;
|
|
785
|
-
reconnectTimer = safeSetTimeout(() => {
|
|
786
|
-
reconnectWaitResolve = undefined;
|
|
787
|
-
resolve();
|
|
788
|
-
}, delayMs);
|
|
789
|
-
});
|
|
790
|
-
reconnectTimer = undefined;
|
|
791
|
-
reconnecting = false;
|
|
792
|
-
if (closed) {
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
try {
|
|
796
|
-
resetTransport();
|
|
797
|
-
ws.reconnect();
|
|
798
|
-
}
|
|
799
|
-
catch (err) {
|
|
800
|
-
logger.error({ err }, 'failed to reopen the transport');
|
|
801
|
-
return end(err);
|
|
802
|
-
}
|
|
803
|
-
};
|
|
804
|
-
/** single entry point for every transport-level close */
|
|
805
|
-
const handleDisconnect = (error) => {
|
|
806
|
-
if (closed) {
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
return scheduleReconnect(error);
|
|
810
|
-
};
|
|
811
642
|
const waitForSocketOpen = async () => {
|
|
812
643
|
if (ws.isOpen) {
|
|
813
644
|
return;
|
|
@@ -829,89 +660,48 @@ export const makeSocket = (config) => {
|
|
|
829
660
|
ws.off('error', onClose);
|
|
830
661
|
});
|
|
831
662
|
};
|
|
832
|
-
/**
|
|
833
|
-
* Keep-alive, modelled on zapo-js (src/transport/keepalive/WaKeepAlive.ts):
|
|
834
|
-
* - self-rescheduling setTimeout with +-10% jitter (min 250ms), so a fleet of sockets in one
|
|
835
|
-
* process drifts apart instead of pinging in the same tick
|
|
836
|
-
* - at most one ping in flight; a slow pong never stacks more pings on a busy loop
|
|
837
|
-
* - no ping while other queries are pending and data arrived recently: the link is
|
|
838
|
-
* demonstrably alive and the ping would only add queue pressure
|
|
839
|
-
* - "lost" is still decided by inbound silence (interval + grace), and skipped when the
|
|
840
|
-
* timer itself fired late (blocked event loop: pongs may be unread in the socket buffer)
|
|
841
|
-
*/
|
|
842
663
|
const startKeepAliveRequest = () => {
|
|
843
|
-
let timer;
|
|
844
|
-
let stopped = false;
|
|
845
|
-
let pingInFlight = false;
|
|
846
|
-
let lastCheckAt;
|
|
847
|
-
let expectedDelayMs = 0;
|
|
848
|
-
const nextDelay = () => {
|
|
849
|
-
const jitter = Math.max(250, Math.floor(keepAliveIntervalMs * 0.1));
|
|
850
|
-
return Math.max(1000, keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter));
|
|
851
|
-
};
|
|
852
|
-
const schedule = (delayMs) => {
|
|
853
|
-
if (stopped) {
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
expectedDelayMs = delayMs;
|
|
857
|
-
timer = safeSetTimeout(() => {
|
|
858
|
-
timer = undefined;
|
|
859
|
-
cb();
|
|
860
|
-
schedule(nextDelay());
|
|
861
|
-
}, delayMs);
|
|
862
|
-
};
|
|
863
664
|
const cb = () => {
|
|
864
665
|
if (!lastDateRecv) {
|
|
865
666
|
lastDateRecv = new Date();
|
|
866
667
|
}
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
lastCheckAt = now;
|
|
871
|
-
const loopStalled = lagMs > keepAliveGracePeriodMs;
|
|
872
|
-
if (loopStalled) {
|
|
873
|
-
logger.warn({ lagMs, sinceLastRecvMs: sinceRecvMs }, 'event loop stalled; deferring keep-alive timeout');
|
|
668
|
+
const diff = Date.now() - lastDateRecv.getTime();
|
|
669
|
+
if (diff > keepAliveIntervalMs + keepAliveGracePeriodMs) {
|
|
670
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
874
671
|
}
|
|
875
|
-
if (
|
|
876
|
-
|
|
877
|
-
|
|
672
|
+
else if (ws.isOpen) {
|
|
673
|
+
query({
|
|
674
|
+
tag: 'iq',
|
|
675
|
+
attrs: {
|
|
676
|
+
id: generateMessageTag(),
|
|
677
|
+
to: S_WHATSAPP_NET,
|
|
678
|
+
type: 'get',
|
|
679
|
+
xmlns: 'w:p'
|
|
680
|
+
},
|
|
681
|
+
content: [{ tag: 'ping', attrs: {} }]
|
|
682
|
+
}).catch(err => {
|
|
683
|
+
logger.error({ trace: err.stack }, 'error in sending keep alive');
|
|
684
|
+
});
|
|
878
685
|
}
|
|
879
|
-
|
|
686
|
+
else {
|
|
880
687
|
logger.warn('keep alive called when WS not open');
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
if (pingInFlight) {
|
|
884
|
-
logger.trace('keep alive skipped: ping already in flight');
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
if (pendingQueries > 0 && sinceRecvMs < keepAliveGracePeriodMs) {
|
|
888
|
-
logger.trace({ pendingQueries, sinceRecvMs }, 'keep alive skipped: queries pending with recent inbound');
|
|
889
|
-
return;
|
|
890
688
|
}
|
|
891
|
-
pingInFlight = true;
|
|
892
|
-
query({
|
|
893
|
-
tag: 'iq',
|
|
894
|
-
attrs: {
|
|
895
|
-
id: generateMessageTag(),
|
|
896
|
-
to: S_WHATSAPP_NET,
|
|
897
|
-
type: 'get',
|
|
898
|
-
xmlns: 'w:p'
|
|
899
|
-
},
|
|
900
|
-
content: [{ tag: 'ping', attrs: {} }]
|
|
901
|
-
}, keepAliveIntervalMs + keepAliveGracePeriodMs)
|
|
902
|
-
.catch(err => {
|
|
903
|
-
// the verdict stays with inbound silence above; a failed ping alone is not "lost"
|
|
904
|
-
logger.warn({ trace: err.stack }, 'error in sending keep alive');
|
|
905
|
-
})
|
|
906
|
-
.finally(() => { pingInFlight = false; });
|
|
907
689
|
};
|
|
908
|
-
//
|
|
909
|
-
|
|
690
|
+
// Per socket, so keepAliveIntervalMs from the config is actually the ping interval: the
|
|
691
|
+
// shared process-wide timer ran at a hardcoded 30s while the staleness check below still
|
|
692
|
+
// used the configured value, so any interval other than 30s produced false connectionLost.
|
|
693
|
+
// The random start offset staggers the fleet - on the shared timer every socket in the
|
|
694
|
+
// process pinged in the same tick, which is a self-inflicted burst at 100+ sessions.
|
|
695
|
+
let pinger;
|
|
696
|
+
const starter = safeSetTimeout(() => {
|
|
697
|
+
cb();
|
|
698
|
+
pinger = safeSetInterval(cb, keepAliveIntervalMs);
|
|
699
|
+
}, Math.floor(Math.random() * (keepAliveIntervalMs / 2)));
|
|
910
700
|
keepAliveReq = {
|
|
911
701
|
cb,
|
|
912
702
|
cancel: () => {
|
|
913
|
-
|
|
914
|
-
clearSafeTimer(
|
|
703
|
+
clearSafeTimer(starter);
|
|
704
|
+
clearSafeTimer(pinger);
|
|
915
705
|
}
|
|
916
706
|
};
|
|
917
707
|
};
|
|
@@ -1024,14 +814,14 @@ export const makeSocket = (config) => {
|
|
|
1024
814
|
if (customPairingCode && customPairingCode.length !== 8) {
|
|
1025
815
|
throw new Error('Custom pairing code must be exactly 8 chars');
|
|
1026
816
|
}
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
817
|
+
// Not fatal, because whether WhatsApp normalizes these characters is not observable from
|
|
818
|
+
// here - but it is the one thing that fails silently on the phone rather than on the wire,
|
|
819
|
+
// so it gets named in the log instead of being guessed at during the next outage.
|
|
1030
820
|
const wantedCode = customPairingCode ? normalizePairingCode(customPairingCode) : undefined;
|
|
1031
821
|
if (wantedCode) {
|
|
1032
822
|
const invalid = invalidPairingCodeChars(wantedCode);
|
|
1033
823
|
if (invalid.length) {
|
|
1034
|
-
|
|
824
|
+
logger.warn({ pairingCode: wantedCode, invalid, allowed: PAIRING_CODE_ALPHABET }, 'custom pairing code uses characters absent from WhatsApp\'s own alphabet; if linking fails with "Couldn\'t link device", try a code built only from the allowed characters');
|
|
1035
825
|
}
|
|
1036
826
|
}
|
|
1037
827
|
if (pairingInProgress) {
|
|
@@ -1103,13 +893,13 @@ export const makeSocket = (config) => {
|
|
|
1103
893
|
}
|
|
1104
894
|
catch (err) {
|
|
1105
895
|
logger.error({ err }, 'error in validating connection');
|
|
1106
|
-
void
|
|
896
|
+
void end(err);
|
|
1107
897
|
}
|
|
1108
898
|
});
|
|
1109
|
-
ws.on('error', mapWebSocketError(
|
|
1110
|
-
ws.on('close', () => void
|
|
899
|
+
ws.on('error', mapWebSocketError(end));
|
|
900
|
+
ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
1111
901
|
// the server terminated the connection
|
|
1112
|
-
ws.on('CB:xmlstreamend', () => void
|
|
902
|
+
ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
1113
903
|
// QR gen
|
|
1114
904
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
1115
905
|
const iq = {
|
|
@@ -1174,9 +964,6 @@ export const makeSocket = (config) => {
|
|
|
1174
964
|
});
|
|
1175
965
|
// login complete
|
|
1176
966
|
ws.on('CB:success', async (node) => {
|
|
1177
|
-
// a completed handshake clears the reconnect budget: the next drop starts from the base delay
|
|
1178
|
-
reconnectAttempts = 0;
|
|
1179
|
-
reconnecting = false;
|
|
1180
967
|
try {
|
|
1181
968
|
updateServerTimeOffset(node);
|
|
1182
969
|
await uploadPreKeysToServerIfRequired();
|
|
@@ -1195,11 +982,6 @@ export const makeSocket = (config) => {
|
|
|
1195
982
|
logger.info('opened connection to WA');
|
|
1196
983
|
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
1197
984
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
1198
|
-
if (!authState.creds.me?.id) {
|
|
1199
|
-
// `me` absent: the spread above produced a lid-only identity, which hosts then read as
|
|
1200
|
-
// `sock.user.id === undefined`. Say so once per socket instead of letting it look healthy.
|
|
1201
|
-
logger.error({ credsKeys: Object.keys(authState.creds), registered: authState.creds.registered, hasAccount: !!authState.creds.account }, 'login completed but creds.me.id is missing — host reads sock.user.id unguarded');
|
|
1202
|
-
}
|
|
1203
985
|
ev.emit('connection.update', { connection: 'open' });
|
|
1204
986
|
void sendUnifiedSession();
|
|
1205
987
|
if (node.attrs.lid && authState.creds.me?.id) {
|
|
@@ -1230,62 +1012,23 @@ export const makeSocket = (config) => {
|
|
|
1230
1012
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
1231
1013
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
1232
1014
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
1233
|
-
void
|
|
1015
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
1234
1016
|
});
|
|
1235
1017
|
// stream fail, possible logout
|
|
1236
|
-
ws.on('CB:failure',
|
|
1018
|
+
ws.on('CB:failure', (node) => {
|
|
1237
1019
|
const reason = +(node.attrs.reason || 500);
|
|
1238
|
-
|
|
1239
|
-
// client_too_old: fetch the live WA Web build so the caller's reconnect uses it
|
|
1240
|
-
const latest = await fetchLatestWaWebVersion({});
|
|
1241
|
-
if (latest.isLatest) {
|
|
1242
|
-
latestVersionOverride = latest.version;
|
|
1243
|
-
logger.warn({ rejected: latestVersionOverride === config.version ? undefined : config.version, latest: latest.version }, 'server rejected the client version as too old (405); the next connection uses the latest WA Web version');
|
|
1244
|
-
}
|
|
1245
|
-
else {
|
|
1246
|
-
logger.warn({ err: latest.error?.message }, 'server rejected the client version as too old (405) and the latest version could not be fetched; pass a current `version`');
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
void handleDisconnect(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
1020
|
+
void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
1250
1021
|
});
|
|
1251
1022
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
1252
|
-
void
|
|
1023
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
1253
1024
|
});
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
// time a queued stanza (attrs.offline) arrives, debounced so a burst yields one request.
|
|
1258
|
-
// ponytail: no stanza timeout; the terminal 'offline' bulletin ends the loop.
|
|
1259
|
-
let offlineResuming = false;
|
|
1260
|
-
let offlineBatchInFlight = false;
|
|
1261
|
-
let offlineBatchTimer;
|
|
1262
|
-
const requestOfflineBatch = () => {
|
|
1263
|
-
if (!offlineResuming || offlineBatchInFlight) {
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
offlineBatchInFlight = true;
|
|
1267
|
-
sendNode({
|
|
1025
|
+
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
1026
|
+
logger.info('offline preview received', JSON.stringify(node));
|
|
1027
|
+
await sendNode({
|
|
1268
1028
|
tag: 'ib',
|
|
1269
1029
|
attrs: {},
|
|
1270
1030
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
1271
|
-
}).catch(err => {
|
|
1272
|
-
offlineBatchInFlight = false;
|
|
1273
|
-
logger.debug({ err }, 'offline batch request failed');
|
|
1274
1031
|
});
|
|
1275
|
-
};
|
|
1276
|
-
ws.on('CB:ib,,offline_preview', (node) => {
|
|
1277
|
-
logger.info('offline preview received', JSON.stringify(node));
|
|
1278
|
-
offlineResuming = true;
|
|
1279
|
-
offlineBatchInFlight = false;
|
|
1280
|
-
requestOfflineBatch();
|
|
1281
|
-
});
|
|
1282
|
-
ws.on('frame', (frame) => {
|
|
1283
|
-
if (!offlineResuming || frame instanceof Uint8Array || frame.attrs?.offline === undefined) {
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
offlineBatchInFlight = false;
|
|
1287
|
-
clearTimeout(offlineBatchTimer);
|
|
1288
|
-
offlineBatchTimer = setTimeout(requestOfflineBatch, 100);
|
|
1289
1032
|
});
|
|
1290
1033
|
ws.on('CB:ib,,edge_routing', (node) => {
|
|
1291
1034
|
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
|
|
@@ -1307,8 +1050,6 @@ export const makeSocket = (config) => {
|
|
|
1307
1050
|
});
|
|
1308
1051
|
// called when all offline notifs are handled
|
|
1309
1052
|
ws.on('CB:ib,,offline', (node) => {
|
|
1310
|
-
offlineResuming = false;
|
|
1311
|
-
clearTimeout(offlineBatchTimer);
|
|
1312
1053
|
const child = getBinaryNodeChild(node, 'offline');
|
|
1313
1054
|
const offlineNotifs = +(child?.attrs.count || 0);
|
|
1314
1055
|
logger.info(`handled ${offlineNotifs} offline messages/notifications`);
|
|
@@ -1332,9 +1073,6 @@ export const makeSocket = (config) => {
|
|
|
1332
1073
|
});
|
|
1333
1074
|
}
|
|
1334
1075
|
Object.assign(creds, update);
|
|
1335
|
-
if (creds.me) {
|
|
1336
|
-
lastMe = creds.me;
|
|
1337
|
-
}
|
|
1338
1076
|
});
|
|
1339
1077
|
const updateServerTimeOffset = ({ attrs }) => {
|
|
1340
1078
|
const tValue = attrs?.t;
|
|
@@ -1381,9 +1119,6 @@ export const makeSocket = (config) => {
|
|
|
1381
1119
|
const registerSocketEndHandler = (handler) => {
|
|
1382
1120
|
socketEndHandlers.push(handler);
|
|
1383
1121
|
};
|
|
1384
|
-
const registerSocketReconnectHandler = (handler) => {
|
|
1385
|
-
socketReconnectHandlers.push(handler);
|
|
1386
|
-
};
|
|
1387
1122
|
/**
|
|
1388
1123
|
* Fetches your account's standing when it comes to restrictions.
|
|
1389
1124
|
* @returns Returns the state of the restrictions.
|
|
@@ -1414,31 +1149,9 @@ export const makeSocket = (config) => {
|
|
|
1414
1149
|
authState: { creds, keys },
|
|
1415
1150
|
signalRepository,
|
|
1416
1151
|
get user() {
|
|
1417
|
-
|
|
1418
|
-
if (!me && !userMissingLogged) {
|
|
1419
|
-
userMissingLogged = true;
|
|
1420
|
-
logger.error(
|
|
1421
|
-
{
|
|
1422
|
-
credsKeys: Object.keys(authState.creds),
|
|
1423
|
-
registered: authState.creds.registered,
|
|
1424
|
-
hasAccount: !!authState.creds.account,
|
|
1425
|
-
sameAuthObject: authState === config.auth
|
|
1426
|
-
},
|
|
1427
|
-
'sock.user is undefined: creds.me never arrived on this socket (host reads sock.user.id unguarded)'
|
|
1428
|
-
);
|
|
1429
|
-
}
|
|
1430
|
-
return me;
|
|
1431
|
-
},
|
|
1432
|
-
/**
|
|
1433
|
-
* True while the transport is repairing itself. A host that reads this does not build a
|
|
1434
|
-
* second socket for the same account, which is what a `ws.isOpen === false` check would
|
|
1435
|
-
* make it do during the backoff window and the handshake that follows.
|
|
1436
|
-
*/
|
|
1437
|
-
get isReconnecting() {
|
|
1438
|
-
return reconnecting || (reconnectAttempts > 0 && !closed && !ws.isOpen);
|
|
1152
|
+
return authState.creds.me;
|
|
1439
1153
|
},
|
|
1440
1154
|
generateMessageTag,
|
|
1441
|
-
ignoreKey: ignoreKeyRegistry.register,
|
|
1442
1155
|
query,
|
|
1443
1156
|
waitForMessage,
|
|
1444
1157
|
waitForSocketOpen,
|
|
@@ -1447,7 +1160,6 @@ export const makeSocket = (config) => {
|
|
|
1447
1160
|
logout,
|
|
1448
1161
|
end,
|
|
1449
1162
|
registerSocketEndHandler,
|
|
1450
|
-
registerSocketReconnectHandler,
|
|
1451
1163
|
onUnexpectedError,
|
|
1452
1164
|
uploadPreKeys,
|
|
1453
1165
|
uploadPreKeysToServerIfRequired,
|