@violetix/baileys 0.5.6 → 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 -346
- 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,34 +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
|
-
// A copy, and only when it carries an id: `creds.me` is replaced wholesale by some updates (a
|
|
396
|
-
// `CB:success` or a pushName change can arrive with a lid-only object), and holding the same
|
|
397
|
-
// reference would follow that replacement into the fallback.
|
|
398
|
-
let lastMe = creds.me?.id ? { ...creds.me } : undefined;
|
|
399
|
-
let userMissingLogged = false;
|
|
400
363
|
// add transaction capability
|
|
401
364
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
|
|
402
365
|
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
|
|
403
366
|
let lastDateRecv;
|
|
404
|
-
/** queries awaiting a reply; keep-alive skips its ping while these are flowing */
|
|
405
|
-
let pendingQueries = 0;
|
|
406
367
|
let epoch = 1;
|
|
407
368
|
let keepAliveReq;
|
|
408
369
|
let qrTimer;
|
|
409
370
|
let closed = false;
|
|
410
|
-
/** transparent transport reconnect state; see scheduleReconnect */
|
|
411
|
-
let reconnecting = false;
|
|
412
|
-
let reconnectAttempts = 0;
|
|
413
|
-
let reconnectTimer;
|
|
414
|
-
let reconnectWaitResolve;
|
|
415
371
|
/** set once WhatsApp sends pair-device; requestPairingCode queues until then */
|
|
416
372
|
let pairingReady = false;
|
|
417
373
|
let pairingInProgress = false;
|
|
@@ -420,8 +376,6 @@ export const makeSocket = (config) => {
|
|
|
420
376
|
let pendingPairingResolve;
|
|
421
377
|
let pendingPairingReject;
|
|
422
378
|
const socketEndHandlers = [];
|
|
423
|
-
/** per-connection caches (media host auth, ...) that must be dropped when the transport reopens */
|
|
424
|
-
const socketReconnectHandlers = [];
|
|
425
379
|
const timers = new Set();
|
|
426
380
|
const safeSetTimeout = (fn, ms) => { const id = setTimeout(fn, ms); timers.add(id); return id; };
|
|
427
381
|
const safeSetInterval = (fn, ms) => { const id = setInterval(fn, ms); timers.add(id); return id; };
|
|
@@ -468,19 +422,13 @@ export const makeSocket = (config) => {
|
|
|
468
422
|
const handshake = proto.HandshakeMessage.decode(result);
|
|
469
423
|
logger.trace({ handshake }, 'handshake recv from WA');
|
|
470
424
|
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
|
|
471
|
-
// `version` may be a resolver (e.g. () => fetchLatestWaWebVersion().then(r => r.version));
|
|
472
|
-
// a version the server already rejected as too old is replaced by the fetched one
|
|
473
|
-
const resolvedVersion = typeof config.version === 'function'
|
|
474
|
-
? await config.version()
|
|
475
|
-
: latestVersionOverride ?? config.version;
|
|
476
|
-
const connectConfig = resolvedVersion === config.version ? config : { ...config, version: resolvedVersion };
|
|
477
425
|
let node;
|
|
478
426
|
if (!creds.me) {
|
|
479
|
-
node = generateRegistrationNode(creds,
|
|
427
|
+
node = generateRegistrationNode(creds, config);
|
|
480
428
|
logger.info({ node }, 'not logged in, attempting registration...');
|
|
481
429
|
}
|
|
482
430
|
else {
|
|
483
|
-
node = generateLoginNode(creds.me.id,
|
|
431
|
+
node = generateLoginNode(creds.me.id, config);
|
|
484
432
|
logger.info({ node }, 'logging in...');
|
|
485
433
|
}
|
|
486
434
|
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
|
|
@@ -596,11 +544,6 @@ export const makeSocket = (config) => {
|
|
|
596
544
|
// Don't throw - allow connection to continue even if pre-key check fails
|
|
597
545
|
}
|
|
598
546
|
};
|
|
599
|
-
const ignoreKeyRegistry = makeIgnoreKeyRegistry({
|
|
600
|
-
getMeId: () => authState.creds.me?.id,
|
|
601
|
-
getMeLid: () => authState.creds.me?.lid,
|
|
602
|
-
logger
|
|
603
|
-
});
|
|
604
547
|
const onMessageReceived = async (data) => {
|
|
605
548
|
await noise.decodeFrame(data, frame => {
|
|
606
549
|
lastDateRecv = new Date();
|
|
@@ -611,16 +554,6 @@ export const makeSocket = (config) => {
|
|
|
611
554
|
if (logger.level === 'trace') {
|
|
612
555
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
613
556
|
}
|
|
614
|
-
// Filters run before every other handler, so a dropped stanza never reaches the
|
|
615
|
-
// decryption or persistence paths. The ack still goes out, otherwise the server
|
|
616
|
-
// keeps re-delivering what the host just asked to ignore.
|
|
617
|
-
if (ignoreKeyRegistry.shouldDrop(frame)) {
|
|
618
|
-
logger.debug({ tag: frame.tag, id: msgId, from: frame.attrs.from }, 'stanza dropped by ignoreKey');
|
|
619
|
-
if (ignoredStanzaNeedsAck(frame.tag)) {
|
|
620
|
-
sendNode(buildAckStanza(frame, undefined, authState.creds.me?.id)).catch(err => logger.error({ err, id: msgId }, 'failed to ack ignored stanza'));
|
|
621
|
-
}
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
557
|
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
625
558
|
const l0 = frame.tag;
|
|
626
559
|
const l1 = frame.attrs || {};
|
|
@@ -645,24 +578,6 @@ export const makeSocket = (config) => {
|
|
|
645
578
|
return;
|
|
646
579
|
}
|
|
647
580
|
closed = true;
|
|
648
|
-
// Name the disconnect reason in the message itself. The code was always on `error.output`,
|
|
649
|
-
// but a host does not always receive the Boom: a wrapper that re-emits this close as its own
|
|
650
|
-
// `error` event forwards `message` and nothing else, and the messages alone cannot be told
|
|
651
|
-
// apart — `CB:failure` says the constant 'Connection Failure' for a 401 device-removal and
|
|
652
|
-
// for a transient 503 alike. A host that cannot distinguish them keeps reconnecting a
|
|
653
|
-
// session that is really gone, which is how an unattended fleet turns one logout into a
|
|
654
|
-
// re-auth loop and gets the number banned.
|
|
655
|
-
//
|
|
656
|
-
// Done here rather than at each `end()` call site because there are a dozen of them and a
|
|
657
|
-
// new one would silently miss it. The existing text is kept as a prefix, so message matching
|
|
658
|
-
// that already works keeps working; the suffix is additive.
|
|
659
|
-
const statusCode = error?.output?.statusCode;
|
|
660
|
-
if (statusCode !== undefined && typeof error?.message === 'string' && !error.message.includes(`[${statusCode} `)) {
|
|
661
|
-
try {
|
|
662
|
-
error.message = `${error.message} [${statusCode} ${DisconnectReason[statusCode] || 'unknown'}]`;
|
|
663
|
-
}
|
|
664
|
-
catch { }
|
|
665
|
-
}
|
|
666
581
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
667
582
|
// Signal-key writes are debounced (addTransactionCapability). Nothing else in the library
|
|
668
583
|
// called flush, so every ratchet advance from the last window was dropped on close and the
|
|
@@ -681,9 +596,6 @@ export const makeSocket = (config) => {
|
|
|
681
596
|
keepAliveReq?.cancel();
|
|
682
597
|
clearSafeTimer(qrTimer);
|
|
683
598
|
clearAllTimers();
|
|
684
|
-
// release a pending reconnect wait so scheduleReconnect cannot stay suspended
|
|
685
|
-
reconnectWaitResolve?.();
|
|
686
|
-
reconnectWaitResolve = undefined;
|
|
687
599
|
// a queued requestPairingCode would hang forever otherwise
|
|
688
600
|
pairingReady = false;
|
|
689
601
|
pairingInProgress = false;
|
|
@@ -727,90 +639,6 @@ export const makeSocket = (config) => {
|
|
|
727
639
|
logger.error({ err }, 'error while closing connection');
|
|
728
640
|
}
|
|
729
641
|
};
|
|
730
|
-
/** rebuild the state that belongs to one transport connection, not to the socket object */
|
|
731
|
-
const resetTransport = () => {
|
|
732
|
-
keepAliveReq?.cancel();
|
|
733
|
-
keepAliveReq = undefined;
|
|
734
|
-
clearSafeTimer(qrTimer);
|
|
735
|
-
qrTimer = undefined;
|
|
736
|
-
lastDateRecv = undefined;
|
|
737
|
-
ephemeralKeyPair = Curve.generateKeyPair();
|
|
738
|
-
noise = makeNoiseHandler({
|
|
739
|
-
keyPair: ephemeralKeyPair,
|
|
740
|
-
NOISE_HEADER: NOISE_WA_HEADER,
|
|
741
|
-
logger,
|
|
742
|
-
routingInfo: authState?.creds?.routingInfo
|
|
743
|
-
});
|
|
744
|
-
// the media host token is issued per connection; a cached one fails every upload after this
|
|
745
|
-
for (const handler of socketReconnectHandlers) {
|
|
746
|
-
try {
|
|
747
|
-
handler();
|
|
748
|
-
}
|
|
749
|
-
catch (err) {
|
|
750
|
-
logger.error({ err }, 'error in socket reconnect handler');
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
};
|
|
754
|
-
/**
|
|
755
|
-
* Transparent reconnect. The WebSocket client object is kept and only the raw socket and the
|
|
756
|
-
* noise session under it are replaced, so every stanza listener registered by the layers above
|
|
757
|
-
* (messages-recv, chats, the host) keeps working and the host never sees a `close` for a drop
|
|
758
|
-
* that this socket can repair itself.
|
|
759
|
-
*/
|
|
760
|
-
const scheduleReconnect = async (error) => {
|
|
761
|
-
if (reconnecting || closed) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
reconnecting = true;
|
|
765
|
-
const statusCode = error?.output?.statusCode;
|
|
766
|
-
const maxAttempts = config.maxReconnectAttempts ?? 10;
|
|
767
|
-
if (!config.autoReconnect || DEAD_DISCONNECT_REASONS.has(statusCode) || reconnectAttempts >= maxAttempts) {
|
|
768
|
-
reconnecting = false;
|
|
769
|
-
return end(error);
|
|
770
|
-
}
|
|
771
|
-
reconnectAttempts += 1;
|
|
772
|
-
// 515 means the server wants the connection back now; backing off only delays it
|
|
773
|
-
const immediate = statusCode === DisconnectReason.restartRequired;
|
|
774
|
-
const base = config.reconnectDelayMs ?? 2000;
|
|
775
|
-
const ceiling = config.maxReconnectDelayMs ?? 60000;
|
|
776
|
-
const backoff = Math.min(ceiling, base * 2 ** (reconnectAttempts - 1));
|
|
777
|
-
// jitter keeps a fleet from reconnecting in lockstep after a server-side blip
|
|
778
|
-
const delayMs = immediate ? 0 : Math.round(backoff * (0.75 + Math.random() * 0.5));
|
|
779
|
-
logger.info({ statusCode, attempt: reconnectAttempts, delayMs }, 'connection lost, reconnecting');
|
|
780
|
-
// NOT 'close': a host reads close as "build a new socket", and a second socket for the same
|
|
781
|
-
// account is what earns a 440 connectionReplaced
|
|
782
|
-
ev.emit('connection.update', {
|
|
783
|
-
connection: 'connecting',
|
|
784
|
-
lastDisconnect: { error, date: new Date() }
|
|
785
|
-
});
|
|
786
|
-
await new Promise(resolve => {
|
|
787
|
-
reconnectWaitResolve = resolve;
|
|
788
|
-
reconnectTimer = safeSetTimeout(() => {
|
|
789
|
-
reconnectWaitResolve = undefined;
|
|
790
|
-
resolve();
|
|
791
|
-
}, delayMs);
|
|
792
|
-
});
|
|
793
|
-
reconnectTimer = undefined;
|
|
794
|
-
reconnecting = false;
|
|
795
|
-
if (closed) {
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
try {
|
|
799
|
-
resetTransport();
|
|
800
|
-
ws.reconnect();
|
|
801
|
-
}
|
|
802
|
-
catch (err) {
|
|
803
|
-
logger.error({ err }, 'failed to reopen the transport');
|
|
804
|
-
return end(err);
|
|
805
|
-
}
|
|
806
|
-
};
|
|
807
|
-
/** single entry point for every transport-level close */
|
|
808
|
-
const handleDisconnect = (error) => {
|
|
809
|
-
if (closed) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
return scheduleReconnect(error);
|
|
813
|
-
};
|
|
814
642
|
const waitForSocketOpen = async () => {
|
|
815
643
|
if (ws.isOpen) {
|
|
816
644
|
return;
|
|
@@ -832,89 +660,48 @@ export const makeSocket = (config) => {
|
|
|
832
660
|
ws.off('error', onClose);
|
|
833
661
|
});
|
|
834
662
|
};
|
|
835
|
-
/**
|
|
836
|
-
* Keep-alive, modelled on zapo-js (src/transport/keepalive/WaKeepAlive.ts):
|
|
837
|
-
* - self-rescheduling setTimeout with +-10% jitter (min 250ms), so a fleet of sockets in one
|
|
838
|
-
* process drifts apart instead of pinging in the same tick
|
|
839
|
-
* - at most one ping in flight; a slow pong never stacks more pings on a busy loop
|
|
840
|
-
* - no ping while other queries are pending and data arrived recently: the link is
|
|
841
|
-
* demonstrably alive and the ping would only add queue pressure
|
|
842
|
-
* - "lost" is still decided by inbound silence (interval + grace), and skipped when the
|
|
843
|
-
* timer itself fired late (blocked event loop: pongs may be unread in the socket buffer)
|
|
844
|
-
*/
|
|
845
663
|
const startKeepAliveRequest = () => {
|
|
846
|
-
let timer;
|
|
847
|
-
let stopped = false;
|
|
848
|
-
let pingInFlight = false;
|
|
849
|
-
let lastCheckAt;
|
|
850
|
-
let expectedDelayMs = 0;
|
|
851
|
-
const nextDelay = () => {
|
|
852
|
-
const jitter = Math.max(250, Math.floor(keepAliveIntervalMs * 0.1));
|
|
853
|
-
return Math.max(1000, keepAliveIntervalMs + Math.floor((Math.random() * 2 - 1) * jitter));
|
|
854
|
-
};
|
|
855
|
-
const schedule = (delayMs) => {
|
|
856
|
-
if (stopped) {
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
expectedDelayMs = delayMs;
|
|
860
|
-
timer = safeSetTimeout(() => {
|
|
861
|
-
timer = undefined;
|
|
862
|
-
cb();
|
|
863
|
-
schedule(nextDelay());
|
|
864
|
-
}, delayMs);
|
|
865
|
-
};
|
|
866
664
|
const cb = () => {
|
|
867
665
|
if (!lastDateRecv) {
|
|
868
666
|
lastDateRecv = new Date();
|
|
869
667
|
}
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
lastCheckAt = now;
|
|
874
|
-
const loopStalled = lagMs > keepAliveGracePeriodMs;
|
|
875
|
-
if (loopStalled) {
|
|
876
|
-
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 }));
|
|
877
671
|
}
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
|
|
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
|
+
});
|
|
881
685
|
}
|
|
882
|
-
|
|
686
|
+
else {
|
|
883
687
|
logger.warn('keep alive called when WS not open');
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
if (pingInFlight) {
|
|
887
|
-
logger.trace('keep alive skipped: ping already in flight');
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
if (pendingQueries > 0 && sinceRecvMs < keepAliveGracePeriodMs) {
|
|
891
|
-
logger.trace({ pendingQueries, sinceRecvMs }, 'keep alive skipped: queries pending with recent inbound');
|
|
892
|
-
return;
|
|
893
688
|
}
|
|
894
|
-
pingInFlight = true;
|
|
895
|
-
query({
|
|
896
|
-
tag: 'iq',
|
|
897
|
-
attrs: {
|
|
898
|
-
id: generateMessageTag(),
|
|
899
|
-
to: S_WHATSAPP_NET,
|
|
900
|
-
type: 'get',
|
|
901
|
-
xmlns: 'w:p'
|
|
902
|
-
},
|
|
903
|
-
content: [{ tag: 'ping', attrs: {} }]
|
|
904
|
-
}, keepAliveIntervalMs + keepAliveGracePeriodMs)
|
|
905
|
-
.catch(err => {
|
|
906
|
-
// the verdict stays with inbound silence above; a failed ping alone is not "lost"
|
|
907
|
-
logger.warn({ trace: err.stack }, 'error in sending keep alive');
|
|
908
|
-
})
|
|
909
|
-
.finally(() => { pingInFlight = false; });
|
|
910
689
|
};
|
|
911
|
-
//
|
|
912
|
-
|
|
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)));
|
|
913
700
|
keepAliveReq = {
|
|
914
701
|
cb,
|
|
915
702
|
cancel: () => {
|
|
916
|
-
|
|
917
|
-
clearSafeTimer(
|
|
703
|
+
clearSafeTimer(starter);
|
|
704
|
+
clearSafeTimer(pinger);
|
|
918
705
|
}
|
|
919
706
|
};
|
|
920
707
|
};
|
|
@@ -1027,14 +814,14 @@ export const makeSocket = (config) => {
|
|
|
1027
814
|
if (customPairingCode && customPairingCode.length !== 8) {
|
|
1028
815
|
throw new Error('Custom pairing code must be exactly 8 chars');
|
|
1029
816
|
}
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
//
|
|
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.
|
|
1033
820
|
const wantedCode = customPairingCode ? normalizePairingCode(customPairingCode) : undefined;
|
|
1034
821
|
if (wantedCode) {
|
|
1035
822
|
const invalid = invalidPairingCodeChars(wantedCode);
|
|
1036
823
|
if (invalid.length) {
|
|
1037
|
-
|
|
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');
|
|
1038
825
|
}
|
|
1039
826
|
}
|
|
1040
827
|
if (pairingInProgress) {
|
|
@@ -1106,13 +893,13 @@ export const makeSocket = (config) => {
|
|
|
1106
893
|
}
|
|
1107
894
|
catch (err) {
|
|
1108
895
|
logger.error({ err }, 'error in validating connection');
|
|
1109
|
-
void
|
|
896
|
+
void end(err);
|
|
1110
897
|
}
|
|
1111
898
|
});
|
|
1112
|
-
ws.on('error', mapWebSocketError(
|
|
1113
|
-
ws.on('close', () => void
|
|
899
|
+
ws.on('error', mapWebSocketError(end));
|
|
900
|
+
ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
1114
901
|
// the server terminated the connection
|
|
1115
|
-
ws.on('CB:xmlstreamend', () => void
|
|
902
|
+
ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
1116
903
|
// QR gen
|
|
1117
904
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
1118
905
|
const iq = {
|
|
@@ -1177,9 +964,6 @@ export const makeSocket = (config) => {
|
|
|
1177
964
|
});
|
|
1178
965
|
// login complete
|
|
1179
966
|
ws.on('CB:success', async (node) => {
|
|
1180
|
-
// a completed handshake clears the reconnect budget: the next drop starts from the base delay
|
|
1181
|
-
reconnectAttempts = 0;
|
|
1182
|
-
reconnecting = false;
|
|
1183
967
|
try {
|
|
1184
968
|
updateServerTimeOffset(node);
|
|
1185
969
|
await uploadPreKeysToServerIfRequired();
|
|
@@ -1198,11 +982,6 @@ export const makeSocket = (config) => {
|
|
|
1198
982
|
logger.info('opened connection to WA');
|
|
1199
983
|
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
1200
984
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
1201
|
-
if (!authState.creds.me?.id) {
|
|
1202
|
-
// `me` absent: the spread above produced a lid-only identity, which hosts then read as
|
|
1203
|
-
// `sock.user.id === undefined`. Say so once per socket instead of letting it look healthy.
|
|
1204
|
-
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');
|
|
1205
|
-
}
|
|
1206
985
|
ev.emit('connection.update', { connection: 'open' });
|
|
1207
986
|
void sendUnifiedSession();
|
|
1208
987
|
if (node.attrs.lid && authState.creds.me?.id) {
|
|
@@ -1233,62 +1012,23 @@ export const makeSocket = (config) => {
|
|
|
1233
1012
|
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
1234
1013
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
1235
1014
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
1236
|
-
void
|
|
1015
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
1237
1016
|
});
|
|
1238
1017
|
// stream fail, possible logout
|
|
1239
|
-
ws.on('CB:failure',
|
|
1018
|
+
ws.on('CB:failure', (node) => {
|
|
1240
1019
|
const reason = +(node.attrs.reason || 500);
|
|
1241
|
-
|
|
1242
|
-
// client_too_old: fetch the live WA Web build so the caller's reconnect uses it
|
|
1243
|
-
const latest = await fetchLatestWaWebVersion({});
|
|
1244
|
-
if (latest.isLatest) {
|
|
1245
|
-
latestVersionOverride = latest.version;
|
|
1246
|
-
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');
|
|
1247
|
-
}
|
|
1248
|
-
else {
|
|
1249
|
-
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`');
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
void handleDisconnect(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
1020
|
+
void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
1253
1021
|
});
|
|
1254
1022
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
1255
|
-
void
|
|
1023
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
1256
1024
|
});
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
// time a queued stanza (attrs.offline) arrives, debounced so a burst yields one request.
|
|
1261
|
-
// ponytail: no stanza timeout; the terminal 'offline' bulletin ends the loop.
|
|
1262
|
-
let offlineResuming = false;
|
|
1263
|
-
let offlineBatchInFlight = false;
|
|
1264
|
-
let offlineBatchTimer;
|
|
1265
|
-
const requestOfflineBatch = () => {
|
|
1266
|
-
if (!offlineResuming || offlineBatchInFlight) {
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
offlineBatchInFlight = true;
|
|
1270
|
-
sendNode({
|
|
1025
|
+
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
1026
|
+
logger.info('offline preview received', JSON.stringify(node));
|
|
1027
|
+
await sendNode({
|
|
1271
1028
|
tag: 'ib',
|
|
1272
1029
|
attrs: {},
|
|
1273
1030
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
1274
|
-
}).catch(err => {
|
|
1275
|
-
offlineBatchInFlight = false;
|
|
1276
|
-
logger.debug({ err }, 'offline batch request failed');
|
|
1277
1031
|
});
|
|
1278
|
-
};
|
|
1279
|
-
ws.on('CB:ib,,offline_preview', (node) => {
|
|
1280
|
-
logger.info('offline preview received', JSON.stringify(node));
|
|
1281
|
-
offlineResuming = true;
|
|
1282
|
-
offlineBatchInFlight = false;
|
|
1283
|
-
requestOfflineBatch();
|
|
1284
|
-
});
|
|
1285
|
-
ws.on('frame', (frame) => {
|
|
1286
|
-
if (!offlineResuming || frame instanceof Uint8Array || frame.attrs?.offline === undefined) {
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
offlineBatchInFlight = false;
|
|
1290
|
-
clearTimeout(offlineBatchTimer);
|
|
1291
|
-
offlineBatchTimer = setTimeout(requestOfflineBatch, 100);
|
|
1292
1032
|
});
|
|
1293
1033
|
ws.on('CB:ib,,edge_routing', (node) => {
|
|
1294
1034
|
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
|
|
@@ -1310,8 +1050,6 @@ export const makeSocket = (config) => {
|
|
|
1310
1050
|
});
|
|
1311
1051
|
// called when all offline notifs are handled
|
|
1312
1052
|
ws.on('CB:ib,,offline', (node) => {
|
|
1313
|
-
offlineResuming = false;
|
|
1314
|
-
clearTimeout(offlineBatchTimer);
|
|
1315
1053
|
const child = getBinaryNodeChild(node, 'offline');
|
|
1316
1054
|
const offlineNotifs = +(child?.attrs.count || 0);
|
|
1317
1055
|
logger.info(`handled ${offlineNotifs} offline messages/notifications`);
|
|
@@ -1335,9 +1073,6 @@ export const makeSocket = (config) => {
|
|
|
1335
1073
|
});
|
|
1336
1074
|
}
|
|
1337
1075
|
Object.assign(creds, update);
|
|
1338
|
-
if (creds.me?.id) {
|
|
1339
|
-
lastMe = { ...creds.me };
|
|
1340
|
-
}
|
|
1341
1076
|
});
|
|
1342
1077
|
const updateServerTimeOffset = ({ attrs }) => {
|
|
1343
1078
|
const tValue = attrs?.t;
|
|
@@ -1384,9 +1119,6 @@ export const makeSocket = (config) => {
|
|
|
1384
1119
|
const registerSocketEndHandler = (handler) => {
|
|
1385
1120
|
socketEndHandlers.push(handler);
|
|
1386
1121
|
};
|
|
1387
|
-
const registerSocketReconnectHandler = (handler) => {
|
|
1388
|
-
socketReconnectHandlers.push(handler);
|
|
1389
|
-
};
|
|
1390
1122
|
/**
|
|
1391
1123
|
* Fetches your account's standing when it comes to restrictions.
|
|
1392
1124
|
* @returns Returns the state of the restrictions.
|
|
@@ -1417,33 +1149,9 @@ export const makeSocket = (config) => {
|
|
|
1417
1149
|
authState: { creds, keys },
|
|
1418
1150
|
signalRepository,
|
|
1419
1151
|
get user() {
|
|
1420
|
-
|
|
1421
|
-
// read `.id` unguarded, which is the crash this fallback exists to prevent.
|
|
1422
|
-
const me = authState.creds.me?.id ? authState.creds.me : lastMe;
|
|
1423
|
-
if (!me && !userMissingLogged) {
|
|
1424
|
-
userMissingLogged = true;
|
|
1425
|
-
logger.error(
|
|
1426
|
-
{
|
|
1427
|
-
credsKeys: Object.keys(authState.creds),
|
|
1428
|
-
registered: authState.creds.registered,
|
|
1429
|
-
hasAccount: !!authState.creds.account,
|
|
1430
|
-
sameAuthObject: authState === config.auth
|
|
1431
|
-
},
|
|
1432
|
-
'sock.user is undefined: creds.me never arrived on this socket (host reads sock.user.id unguarded)'
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
return me;
|
|
1436
|
-
},
|
|
1437
|
-
/**
|
|
1438
|
-
* True while the transport is repairing itself. A host that reads this does not build a
|
|
1439
|
-
* second socket for the same account, which is what a `ws.isOpen === false` check would
|
|
1440
|
-
* make it do during the backoff window and the handshake that follows.
|
|
1441
|
-
*/
|
|
1442
|
-
get isReconnecting() {
|
|
1443
|
-
return reconnecting || (reconnectAttempts > 0 && !closed && !ws.isOpen);
|
|
1152
|
+
return authState.creds.me;
|
|
1444
1153
|
},
|
|
1445
1154
|
generateMessageTag,
|
|
1446
|
-
ignoreKey: ignoreKeyRegistry.register,
|
|
1447
1155
|
query,
|
|
1448
1156
|
waitForMessage,
|
|
1449
1157
|
waitForSocketOpen,
|
|
@@ -1452,7 +1160,6 @@ export const makeSocket = (config) => {
|
|
|
1452
1160
|
logout,
|
|
1453
1161
|
end,
|
|
1454
1162
|
registerSocketEndHandler,
|
|
1455
|
-
registerSocketReconnectHandler,
|
|
1456
1163
|
onUnexpectedError,
|
|
1457
1164
|
uploadPreKeys,
|
|
1458
1165
|
uploadPreKeysToServerIfRequired,
|