violetics 7.0.1-alpha → 7.0.2-alpha
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 +3 -2
- package/README.md +1001 -232
- package/WAProto/index.js +75379 -142631
- package/engine-requirements.js +11 -8
- package/lib/Defaults/index.js +132 -146
- package/lib/Signal/Group/ciphertext-message.js +2 -6
- package/lib/Signal/Group/group-session-builder.js +7 -42
- package/lib/Signal/Group/group_cipher.js +37 -52
- package/lib/Signal/Group/index.js +11 -57
- package/lib/Signal/Group/keyhelper.js +7 -45
- package/lib/Signal/Group/sender-chain-key.js +7 -16
- package/lib/Signal/Group/sender-key-distribution-message.js +8 -12
- package/lib/Signal/Group/sender-key-message.js +9 -13
- package/lib/Signal/Group/sender-key-name.js +2 -6
- package/lib/Signal/Group/sender-key-record.js +9 -22
- package/lib/Signal/Group/sender-key-state.js +27 -43
- package/lib/Signal/Group/sender-message-key.js +4 -8
- package/lib/Signal/libsignal.js +319 -94
- package/lib/Signal/lid-mapping.js +224 -139
- package/lib/Socket/Client/index.js +2 -19
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +53 -0
- package/lib/Socket/business.js +162 -44
- package/lib/Socket/chats.js +477 -418
- package/lib/Socket/communities.js +430 -0
- package/lib/Socket/groups.js +110 -99
- package/lib/Socket/index.js +10 -10
- package/lib/Socket/messages-recv.js +884 -561
- package/lib/Socket/messages-send.js +859 -428
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.js +195 -390
- package/lib/Socket/socket.js +465 -315
- package/lib/Store/index.js +3 -10
- package/lib/Store/make-in-memory-store.js +73 -79
- package/lib/Store/make-ordered-dictionary.js +4 -7
- package/lib/Store/object-repository.js +2 -6
- package/lib/Types/Auth.js +1 -2
- package/lib/Types/Bussines.js +1 -0
- package/lib/Types/Call.js +1 -2
- package/lib/Types/Chat.js +7 -4
- package/lib/Types/Contact.js +1 -2
- package/lib/Types/Events.js +1 -2
- package/lib/Types/GroupMetadata.js +1 -2
- package/lib/Types/Label.js +2 -5
- package/lib/Types/LabelAssociation.js +2 -5
- package/lib/Types/Message.js +17 -9
- package/lib/Types/Newsletter.js +33 -38
- package/lib/Types/Product.js +1 -2
- package/lib/Types/Signal.js +1 -2
- package/lib/Types/Socket.js +2 -2
- package/lib/Types/State.js +12 -2
- package/lib/Types/USync.js +1 -2
- package/lib/Types/index.js +14 -31
- package/lib/Utils/auth-utils.js +228 -152
- package/lib/Utils/browser-utils.js +28 -0
- package/lib/Utils/business.js +66 -70
- package/lib/Utils/chat-utils.js +331 -249
- package/lib/Utils/crypto.js +57 -91
- package/lib/Utils/decode-wa-message.js +168 -84
- package/lib/Utils/event-buffer.js +138 -80
- package/lib/Utils/generics.js +180 -297
- package/lib/Utils/history.js +83 -49
- package/lib/Utils/identity-change-handler.js +48 -0
- package/lib/Utils/index.js +19 -33
- package/lib/Utils/link-preview.js +14 -23
- package/lib/Utils/logger.js +2 -7
- package/lib/Utils/lt-hash.js +2 -46
- package/lib/Utils/make-mutex.js +24 -47
- package/lib/Utils/message-retry-manager.js +224 -0
- package/lib/Utils/messages-media.js +501 -496
- package/lib/Utils/messages.js +1428 -362
- package/lib/Utils/noise-handler.js +145 -100
- package/lib/Utils/pre-key-manager.js +105 -0
- package/lib/Utils/process-message.js +356 -150
- package/lib/Utils/reporting-utils.js +257 -0
- package/lib/Utils/signal.js +78 -73
- package/lib/Utils/sync-action-utils.js +47 -0
- package/lib/Utils/tc-token-utils.js +17 -0
- package/lib/Utils/use-multi-file-auth-state.js +35 -45
- package/lib/Utils/validate-connection.js +91 -107
- package/lib/WABinary/constants.js +1300 -1304
- package/lib/WABinary/decode.js +26 -48
- package/lib/WABinary/encode.js +109 -155
- package/lib/WABinary/generic-utils.js +161 -149
- package/lib/WABinary/index.js +5 -21
- package/lib/WABinary/jid-utils.js +73 -40
- package/lib/WABinary/types.js +1 -2
- package/lib/WAM/BinaryInfo.js +2 -6
- package/lib/WAM/constants.js +19070 -11568
- package/lib/WAM/encode.js +17 -23
- package/lib/WAM/index.js +3 -19
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +8 -12
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +11 -15
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +9 -13
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +9 -14
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +20 -23
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +13 -9
- package/lib/WAUSync/Protocols/index.js +4 -20
- package/lib/WAUSync/USyncQuery.js +40 -36
- package/lib/WAUSync/USyncUser.js +2 -6
- package/lib/WAUSync/index.js +3 -19
- package/lib/index.js +11 -44
- package/package.json +74 -107
- package/lib/Defaults/baileys-version.json +0 -3
- package/lib/Defaults/phonenumber-mcc.json +0 -223
- package/lib/Signal/Group/queue-job.js +0 -57
- package/lib/Socket/Client/abstract-socket-client.js +0 -13
- package/lib/Socket/Client/mobile-socket-client.js +0 -65
- package/lib/Socket/Client/web-socket-client.js +0 -118
- package/lib/Socket/groupStatus.js +0 -637
- package/lib/Socket/registration.js +0 -166
- package/lib/Socket/usync.js +0 -70
- package/lib/Store/make-cache-manager-store.js +0 -83
- package/lib/Utils/baileys-event-stream.js +0 -63
package/lib/Socket/socket.js
CHANGED
|
@@ -1,75 +1,62 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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, PROCESSABLE_HISTORY_TYPES, TimeMs, UPLOAD_TIMEOUT } 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 } from '../Utils/browser-utils.js';
|
|
10
|
+
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, 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
14
|
/**
|
|
15
15
|
* Connects to WA servers and performs:
|
|
16
16
|
* - simple queries (no retry mechanism, wait for connection establishment)
|
|
17
17
|
* - listen to messages and emit events
|
|
18
18
|
* - query phone connection
|
|
19
|
-
*/
|
|
20
|
-
const makeSocket = (config) => {
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
19
|
+
*/
|
|
20
|
+
export const makeSocket = (config) => {
|
|
21
|
+
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config;
|
|
22
|
+
const publicWAMBuffer = new BinaryInfo();
|
|
23
|
+
let serverTimeOffsetMs = 0;
|
|
24
|
+
const uqTagId = generateMdTagPrefix();
|
|
25
|
+
const generateMessageTag = () => `${uqTagId}${epoch++}`;
|
|
26
|
+
if (printQRInTerminal) {
|
|
27
|
+
logger.warn({}, '⚠️ The printQRInTerminal option has been deprecated. You will no longer receive QR codes in the terminal automatically. Please listen to the connection.update event yourself and handle the QR your way. You can remove this message by removing this opttion. This message will be removed in a future version.');
|
|
28
|
+
}
|
|
29
|
+
const syncDisabled = PROCESSABLE_HISTORY_TYPES.map(syncType => config.shouldSyncHistoryMessage({ syncType })).filter(x => x === false)
|
|
30
|
+
.length === PROCESSABLE_HISTORY_TYPES.length;
|
|
31
|
+
if (syncDisabled) {
|
|
32
|
+
logger.warn('⚠️ DANGER: DISABLING ALL SYNC BY shouldSyncHistoryMsg PREVENTS BAILEYS FROM ACCESSING INITIAL LID MAPPINGS, LEADING TO INSTABILIY AND SESSION ERRORS');
|
|
33
|
+
}
|
|
34
|
+
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl;
|
|
24
35
|
if (config.mobile || url.protocol === 'tcp:') {
|
|
25
|
-
throw new
|
|
26
|
-
statusCode: Types_1.DisconnectReason.loggedOut
|
|
27
|
-
});
|
|
36
|
+
throw new Boom('Mobile API is not supported anymore', { statusCode: DisconnectReason.loggedOut });
|
|
28
37
|
}
|
|
29
|
-
if (url.protocol === 'wss' &&
|
|
38
|
+
if (url.protocol === 'wss' && authState?.creds?.routingInfo) {
|
|
30
39
|
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
|
|
31
40
|
}
|
|
32
|
-
const ws = new Client_1.WebSocketClient(url, config);
|
|
33
|
-
// If connectJitterMs is set, stagger the initial connection by a random delay.
|
|
34
|
-
// This prevents thundering-herd when many bots start simultaneously.
|
|
35
|
-
// Uses setTimeout (not await) because makeSocket is a sync arrow function.
|
|
36
|
-
if (connectJitterMs && connectJitterMs > 0) {
|
|
37
|
-
const jitter = Math.floor(Math.random() * connectJitterMs);
|
|
38
|
-
logger.debug({ jitter }, 'delaying connect by jitter ms');
|
|
39
|
-
setTimeout(() => ws.connect(), jitter);
|
|
40
|
-
} else {
|
|
41
|
-
ws.connect();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const ev = (0, Utils_1.makeEventBuffer)(logger);
|
|
45
41
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
|
46
|
-
const ephemeralKeyPair =
|
|
42
|
+
const ephemeralKeyPair = Curve.generateKeyPair();
|
|
47
43
|
/** WA noise protocol wrapper */
|
|
48
|
-
const noise =
|
|
44
|
+
const noise = makeNoiseHandler({
|
|
49
45
|
keyPair: ephemeralKeyPair,
|
|
50
|
-
NOISE_HEADER:
|
|
46
|
+
NOISE_HEADER: NOISE_WA_HEADER,
|
|
51
47
|
logger,
|
|
52
|
-
routingInfo:
|
|
48
|
+
routingInfo: authState?.creds?.routingInfo
|
|
53
49
|
});
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const signalRepository = makeSignalRepository({ creds, keys });
|
|
58
|
-
let lastDateRecv;
|
|
59
|
-
let epoch = 1;
|
|
60
|
-
let keepAliveReq;
|
|
61
|
-
let qrTimer;
|
|
62
|
-
let closed = false;
|
|
63
|
-
const uqTagId = (0, Utils_1.generateMdTagPrefix)();
|
|
64
|
-
const generateMessageTag = () => `${uqTagId}${epoch++}`;
|
|
65
|
-
const sendPromise = (0, util_1.promisify)(ws.send);
|
|
50
|
+
const ws = new WebSocketClient(url, config);
|
|
51
|
+
ws.connect();
|
|
52
|
+
const sendPromise = promisify(ws.send);
|
|
66
53
|
/** send a raw buffer */
|
|
67
54
|
const sendRawMessage = async (data) => {
|
|
68
55
|
if (!ws.isOpen) {
|
|
69
|
-
throw new
|
|
56
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
70
57
|
}
|
|
71
58
|
const bytes = noise.encodeFrame(data);
|
|
72
|
-
await
|
|
59
|
+
await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
|
|
73
60
|
try {
|
|
74
61
|
await sendPromise.call(ws, bytes);
|
|
75
62
|
resolve();
|
|
@@ -82,50 +69,11 @@ const makeSocket = (config) => {
|
|
|
82
69
|
/** send a binary node */
|
|
83
70
|
const sendNode = (frame) => {
|
|
84
71
|
if (logger.level === 'trace') {
|
|
85
|
-
logger.trace({ xml:
|
|
72
|
+
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' });
|
|
86
73
|
}
|
|
87
|
-
const buff =
|
|
74
|
+
const buff = encodeBinaryNode(frame);
|
|
88
75
|
return sendRawMessage(buff);
|
|
89
76
|
};
|
|
90
|
-
/** log & process any unexpected errors */
|
|
91
|
-
const onUnexpectedError = (err, msg) => {
|
|
92
|
-
logger.error({ err }, `unexpected error in '${msg}'`);
|
|
93
|
-
const message = (err && ((err.stack || err.message) || String(err))).toLowerCase();
|
|
94
|
-
if (message.includes('bad mac') || (message.includes('mac') && message.includes('invalid'))) {
|
|
95
|
-
try {
|
|
96
|
-
uploadPreKeys()
|
|
97
|
-
.catch(e => logger.warn({ e }, 'failed to re-upload prekeys after bad mac'));
|
|
98
|
-
}
|
|
99
|
-
catch (_e) {
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
/** await the next incoming message */
|
|
104
|
-
const awaitNextMessage = async (sendMsg) => {
|
|
105
|
-
if (!ws.isOpen) {
|
|
106
|
-
throw new boom_1.Boom('Connection Closed', {
|
|
107
|
-
statusCode: Types_1.DisconnectReason.connectionClosed
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
let onOpen;
|
|
111
|
-
let onClose;
|
|
112
|
-
const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {
|
|
113
|
-
onOpen = resolve;
|
|
114
|
-
onClose = mapWebSocketError(reject);
|
|
115
|
-
ws.on('frame', onOpen);
|
|
116
|
-
ws.on('close', onClose);
|
|
117
|
-
ws.on('error', onClose);
|
|
118
|
-
})
|
|
119
|
-
.finally(() => {
|
|
120
|
-
ws.off('frame', onOpen);
|
|
121
|
-
ws.off('close', onClose);
|
|
122
|
-
ws.off('error', onClose);
|
|
123
|
-
});
|
|
124
|
-
if (sendMsg) {
|
|
125
|
-
sendRawMessage(sendMsg).catch(onClose);
|
|
126
|
-
}
|
|
127
|
-
return result;
|
|
128
|
-
};
|
|
129
77
|
/**
|
|
130
78
|
* Wait for a message with a certain tag to be received
|
|
131
79
|
* @param msgId the message tag to await
|
|
@@ -135,21 +83,38 @@ const makeSocket = (config) => {
|
|
|
135
83
|
let onRecv;
|
|
136
84
|
let onErr;
|
|
137
85
|
try {
|
|
138
|
-
const result = await
|
|
139
|
-
onRecv =
|
|
86
|
+
const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
|
|
87
|
+
onRecv = data => {
|
|
88
|
+
resolve(data);
|
|
89
|
+
};
|
|
140
90
|
onErr = err => {
|
|
141
|
-
reject(err ||
|
|
91
|
+
reject(err ||
|
|
92
|
+
new Boom('Connection Closed', {
|
|
93
|
+
statusCode: DisconnectReason.connectionClosed
|
|
94
|
+
}));
|
|
142
95
|
};
|
|
143
96
|
ws.on(`TAG:${msgId}`, onRecv);
|
|
144
97
|
ws.on('close', onErr);
|
|
145
98
|
ws.on('error', onErr);
|
|
99
|
+
return () => reject(new Boom('Query Cancelled'));
|
|
146
100
|
});
|
|
147
101
|
return result;
|
|
148
102
|
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Catch timeout and return undefined instead of throwing
|
|
105
|
+
if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
|
|
106
|
+
logger?.warn?.({ msgId }, 'timed out waiting for message');
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
149
111
|
finally {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
112
|
+
if (onRecv)
|
|
113
|
+
ws.off(`TAG:${msgId}`, onRecv);
|
|
114
|
+
if (onErr) {
|
|
115
|
+
ws.off('close', onErr);
|
|
116
|
+
ws.off('error', onErr);
|
|
117
|
+
}
|
|
153
118
|
}
|
|
154
119
|
};
|
|
155
120
|
/** send a query, and wait for its response. auto-generates message ID if not provided */
|
|
@@ -158,12 +123,179 @@ const makeSocket = (config) => {
|
|
|
158
123
|
node.attrs.id = generateMessageTag();
|
|
159
124
|
}
|
|
160
125
|
const msgId = node.attrs.id;
|
|
161
|
-
const
|
|
162
|
-
waitForMessage(msgId, timeoutMs)
|
|
126
|
+
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
127
|
+
const result = waitForMessage(msgId, timeoutMs).catch(reject);
|
|
163
128
|
sendNode(node)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
129
|
+
.then(async () => resolve(await result))
|
|
130
|
+
.catch(reject);
|
|
131
|
+
});
|
|
132
|
+
if (result && 'tag' in result) {
|
|
133
|
+
assertNodeErrorFree(result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
// Validate current key-bundle on server; on failure, trigger pre-key upload and rethrow
|
|
138
|
+
const digestKeyBundle = async () => {
|
|
139
|
+
const res = await query({
|
|
140
|
+
tag: 'iq',
|
|
141
|
+
attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
|
|
142
|
+
content: [{ tag: 'digest', attrs: {} }]
|
|
143
|
+
});
|
|
144
|
+
const digestNode = getBinaryNodeChild(res, 'digest');
|
|
145
|
+
if (!digestNode) {
|
|
146
|
+
await uploadPreKeys();
|
|
147
|
+
throw new Error('encrypt/get digest returned no digest node');
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
// Rotate our signed pre-key on server; on failure, run digest as fallback and rethrow
|
|
151
|
+
const rotateSignedPreKey = async () => {
|
|
152
|
+
const newId = (creds.signedPreKey.keyId || 0) + 1;
|
|
153
|
+
const skey = await signedKeyPair(creds.signedIdentityKey, newId);
|
|
154
|
+
await query({
|
|
155
|
+
tag: 'iq',
|
|
156
|
+
attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
tag: 'rotate',
|
|
160
|
+
attrs: {},
|
|
161
|
+
content: [xmppSignedPreKey(skey)]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
// Persist new signed pre-key in creds
|
|
166
|
+
ev.emit('creds.update', { signedPreKey: skey });
|
|
167
|
+
};
|
|
168
|
+
const executeUSyncQuery = async (usyncQuery) => {
|
|
169
|
+
if (usyncQuery.protocols.length === 0) {
|
|
170
|
+
throw new Boom('USyncQuery must have at least one protocol');
|
|
171
|
+
}
|
|
172
|
+
// todo: validate users, throw WARNING on no valid users
|
|
173
|
+
// variable below has only validated users
|
|
174
|
+
const validUsers = usyncQuery.users;
|
|
175
|
+
const userNodes = validUsers.map(user => {
|
|
176
|
+
return {
|
|
177
|
+
tag: 'user',
|
|
178
|
+
attrs: {
|
|
179
|
+
jid: !user.phone ? user.id : undefined
|
|
180
|
+
},
|
|
181
|
+
content: usyncQuery.protocols.map(a => a.getUserElement(user)).filter(a => a !== null)
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
const listNode = {
|
|
185
|
+
tag: 'list',
|
|
186
|
+
attrs: {},
|
|
187
|
+
content: userNodes
|
|
188
|
+
};
|
|
189
|
+
const queryNode = {
|
|
190
|
+
tag: 'query',
|
|
191
|
+
attrs: {},
|
|
192
|
+
content: usyncQuery.protocols.map(a => a.getQueryElement())
|
|
193
|
+
};
|
|
194
|
+
const iq = {
|
|
195
|
+
tag: 'iq',
|
|
196
|
+
attrs: {
|
|
197
|
+
to: S_WHATSAPP_NET,
|
|
198
|
+
type: 'get',
|
|
199
|
+
xmlns: 'usync'
|
|
200
|
+
},
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
tag: 'usync',
|
|
204
|
+
attrs: {
|
|
205
|
+
context: usyncQuery.context,
|
|
206
|
+
mode: usyncQuery.mode,
|
|
207
|
+
sid: generateMessageTag(),
|
|
208
|
+
last: 'true',
|
|
209
|
+
index: '0'
|
|
210
|
+
},
|
|
211
|
+
content: [queryNode, listNode]
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
const result = await query(iq);
|
|
216
|
+
return usyncQuery.parseUSyncQueryResult(result);
|
|
217
|
+
};
|
|
218
|
+
const onWhatsApp = async (...phoneNumber) => {
|
|
219
|
+
let usyncQuery = new USyncQuery();
|
|
220
|
+
let contactEnabled = false;
|
|
221
|
+
for (const jid of phoneNumber) {
|
|
222
|
+
if (isLidUser(jid)) {
|
|
223
|
+
logger?.warn('LIDs are not supported with onWhatsApp');
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
if (!contactEnabled) {
|
|
228
|
+
contactEnabled = true;
|
|
229
|
+
usyncQuery = usyncQuery.withContactProtocol();
|
|
230
|
+
}
|
|
231
|
+
const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`;
|
|
232
|
+
usyncQuery.withUser(new USyncUser().withPhone(phone));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (usyncQuery.users.length === 0) {
|
|
236
|
+
return []; // return early without forcing an empty query
|
|
237
|
+
}
|
|
238
|
+
const results = await executeUSyncQuery(usyncQuery);
|
|
239
|
+
if (results) {
|
|
240
|
+
return results.list.filter(a => !!a.contact).map(({ contact, id }) => ({ jid: id, exists: contact }));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const pnFromLIDUSync = async (jids) => {
|
|
244
|
+
const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background');
|
|
245
|
+
for (const jid of jids) {
|
|
246
|
+
if (isLidUser(jid)) {
|
|
247
|
+
logger?.warn('LID user found in LID fetch call');
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
usyncQuery.withUser(new USyncUser().withId(jid));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (usyncQuery.users.length === 0) {
|
|
255
|
+
return []; // return early without forcing an empty query
|
|
256
|
+
}
|
|
257
|
+
const results = await executeUSyncQuery(usyncQuery);
|
|
258
|
+
if (results) {
|
|
259
|
+
return results.list.filter(a => !!a.lid).map(({ lid, id }) => ({ pn: id, lid: lid }));
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
262
|
+
};
|
|
263
|
+
const ev = makeEventBuffer(logger);
|
|
264
|
+
const { creds } = authState;
|
|
265
|
+
// add transaction capability
|
|
266
|
+
const keys = addTransactionCapability(authState.keys, logger, transactionOpts);
|
|
267
|
+
const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync);
|
|
268
|
+
let lastDateRecv;
|
|
269
|
+
let epoch = 1;
|
|
270
|
+
let keepAliveReq;
|
|
271
|
+
let qrTimer;
|
|
272
|
+
let closed = false;
|
|
273
|
+
/** log & process any unexpected errors */
|
|
274
|
+
const onUnexpectedError = (err, msg) => {
|
|
275
|
+
logger.error({ err }, `unexpected error in '${msg}'`);
|
|
276
|
+
};
|
|
277
|
+
/** await the next incoming message */
|
|
278
|
+
const awaitNextMessage = async (sendMsg) => {
|
|
279
|
+
if (!ws.isOpen) {
|
|
280
|
+
throw new Boom('Connection Closed', {
|
|
281
|
+
statusCode: DisconnectReason.connectionClosed
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
let onOpen;
|
|
285
|
+
let onClose;
|
|
286
|
+
const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
|
|
287
|
+
onOpen = resolve;
|
|
288
|
+
onClose = mapWebSocketError(reject);
|
|
289
|
+
ws.on('frame', onOpen);
|
|
290
|
+
ws.on('close', onClose);
|
|
291
|
+
ws.on('error', onClose);
|
|
292
|
+
}).finally(() => {
|
|
293
|
+
ws.off('frame', onOpen);
|
|
294
|
+
ws.off('close', onClose);
|
|
295
|
+
ws.off('error', onClose);
|
|
296
|
+
});
|
|
297
|
+
if (sendMsg) {
|
|
298
|
+
sendRawMessage(sendMsg).catch(onClose);
|
|
167
299
|
}
|
|
168
300
|
return result;
|
|
169
301
|
};
|
|
@@ -172,30 +304,30 @@ const makeSocket = (config) => {
|
|
|
172
304
|
let helloMsg = {
|
|
173
305
|
clientHello: { ephemeral: ephemeralKeyPair.public }
|
|
174
306
|
};
|
|
175
|
-
helloMsg =
|
|
307
|
+
helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
|
|
176
308
|
logger.info({ browser, helloMsg }, 'connected to WA');
|
|
177
|
-
const init =
|
|
309
|
+
const init = proto.HandshakeMessage.encode(helloMsg).finish();
|
|
178
310
|
const result = await awaitNextMessage(init);
|
|
179
|
-
const handshake =
|
|
311
|
+
const handshake = proto.HandshakeMessage.decode(result);
|
|
180
312
|
logger.trace({ handshake }, 'handshake recv from WA');
|
|
181
|
-
const keyEnc =
|
|
313
|
+
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
|
|
182
314
|
let node;
|
|
183
315
|
if (!creds.me) {
|
|
184
|
-
node =
|
|
316
|
+
node = generateRegistrationNode(creds, config);
|
|
185
317
|
logger.info({ node }, 'not logged in, attempting registration...');
|
|
186
318
|
}
|
|
187
319
|
else {
|
|
188
|
-
node =
|
|
320
|
+
node = generateLoginNode(creds.me.id, config);
|
|
189
321
|
logger.info({ node }, 'logging in...');
|
|
190
322
|
}
|
|
191
|
-
const payloadEnc = noise.encrypt(
|
|
192
|
-
await sendRawMessage(
|
|
323
|
+
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
|
|
324
|
+
await sendRawMessage(proto.HandshakeMessage.encode({
|
|
193
325
|
clientFinish: {
|
|
194
326
|
static: keyEnc,
|
|
195
|
-
payload: payloadEnc
|
|
196
|
-
}
|
|
327
|
+
payload: payloadEnc
|
|
328
|
+
}
|
|
197
329
|
}).finish());
|
|
198
|
-
noise.finishInit();
|
|
330
|
+
await noise.finishInit();
|
|
199
331
|
startKeepAliveRequest();
|
|
200
332
|
};
|
|
201
333
|
const getAvailablePreKeysOnServer = async () => {
|
|
@@ -205,49 +337,50 @@ const makeSocket = (config) => {
|
|
|
205
337
|
id: generateMessageTag(),
|
|
206
338
|
xmlns: 'encrypt',
|
|
207
339
|
type: 'get',
|
|
208
|
-
to:
|
|
340
|
+
to: S_WHATSAPP_NET
|
|
209
341
|
},
|
|
210
|
-
content: [
|
|
211
|
-
{ tag: 'count', attrs: {} }
|
|
212
|
-
]
|
|
342
|
+
content: [{ tag: 'count', attrs: {} }]
|
|
213
343
|
});
|
|
214
|
-
const countChild =
|
|
344
|
+
const countChild = getBinaryNodeChild(result, 'count');
|
|
215
345
|
return +countChild.attrs.value;
|
|
216
346
|
};
|
|
347
|
+
// Pre-key upload state management
|
|
217
348
|
let uploadPreKeysPromise = null;
|
|
218
349
|
let lastUploadTime = 0;
|
|
219
|
-
|
|
350
|
+
/** generates and uploads a set of pre-keys to the server */
|
|
351
|
+
const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
|
|
352
|
+
// Check minimum interval (except for retries)
|
|
220
353
|
if (retryCount === 0) {
|
|
221
354
|
const timeSinceLastUpload = Date.now() - lastUploadTime;
|
|
222
|
-
if (timeSinceLastUpload <
|
|
355
|
+
if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
|
|
223
356
|
logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
|
|
224
357
|
return;
|
|
225
358
|
}
|
|
226
359
|
}
|
|
360
|
+
// Prevent multiple concurrent uploads
|
|
227
361
|
if (uploadPreKeysPromise) {
|
|
228
362
|
logger.debug('Pre-key upload already in progress, waiting for completion');
|
|
229
363
|
await uploadPreKeysPromise;
|
|
230
364
|
}
|
|
231
|
-
// Spread prekey uploads across a random window to avoid thundering-herd
|
|
232
|
-
// when many bots start simultaneously and all need to upload prekeys.
|
|
233
|
-
const uploadJitter = Math.floor(Math.random() * Defaults_1.PREKEY_UPLOAD_JITTER_MS);
|
|
234
|
-
if (uploadJitter > 0) {
|
|
235
|
-
await new Promise(resolve => setTimeout(resolve, uploadJitter));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
365
|
const uploadLogic = async () => {
|
|
239
366
|
logger.info({ count, retryCount }, 'uploading pre-keys');
|
|
367
|
+
// Generate and save pre-keys atomically (prevents ID collisions on retry)
|
|
240
368
|
const node = await keys.transaction(async () => {
|
|
241
|
-
|
|
369
|
+
logger.debug({ requestedCount: count }, 'generating pre-keys with requested count');
|
|
370
|
+
const { update, node } = await getNextPreKeysNode({ creds, keys }, count);
|
|
371
|
+
// Update credentials immediately to prevent duplicate IDs on retry
|
|
242
372
|
ev.emit('creds.update', update);
|
|
243
|
-
return node;
|
|
244
|
-
});
|
|
373
|
+
return node; // Only return node since update is already used
|
|
374
|
+
}, creds?.me?.id || 'upload-pre-keys');
|
|
375
|
+
// Upload to server (outside transaction, can fail without affecting local keys)
|
|
245
376
|
try {
|
|
246
377
|
await query(node);
|
|
247
|
-
logger.info({ count }, 'uploaded pre-keys');
|
|
378
|
+
logger.info({ count }, 'uploaded pre-keys successfully');
|
|
248
379
|
lastUploadTime = Date.now();
|
|
249
|
-
}
|
|
380
|
+
}
|
|
381
|
+
catch (uploadError) {
|
|
250
382
|
logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
|
|
383
|
+
// Exponential backoff retry (max 3 retries)
|
|
251
384
|
if (retryCount < 3) {
|
|
252
385
|
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
|
|
253
386
|
logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
|
|
@@ -257,33 +390,61 @@ const makeSocket = (config) => {
|
|
|
257
390
|
throw uploadError;
|
|
258
391
|
}
|
|
259
392
|
};
|
|
393
|
+
// Add timeout protection
|
|
260
394
|
uploadPreKeysPromise = Promise.race([
|
|
261
395
|
uploadLogic(),
|
|
262
|
-
new Promise((_, reject) =>
|
|
263
|
-
setTimeout(() => reject(new boom_1.Boom('Pre-key upload timeout', { statusCode: 408 })), Defaults_1.UPLOAD_TIMEOUT)
|
|
264
|
-
)
|
|
396
|
+
new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
|
|
265
397
|
]);
|
|
266
398
|
try {
|
|
267
399
|
await uploadPreKeysPromise;
|
|
268
|
-
}
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
269
402
|
uploadPreKeysPromise = null;
|
|
270
403
|
}
|
|
271
404
|
};
|
|
405
|
+
const verifyCurrentPreKeyExists = async () => {
|
|
406
|
+
const currentPreKeyId = creds.nextPreKeyId - 1;
|
|
407
|
+
if (currentPreKeyId <= 0) {
|
|
408
|
+
return { exists: false, currentPreKeyId: 0 };
|
|
409
|
+
}
|
|
410
|
+
const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()]);
|
|
411
|
+
const exists = !!preKeys[currentPreKeyId.toString()];
|
|
412
|
+
return { exists, currentPreKeyId };
|
|
413
|
+
};
|
|
272
414
|
const uploadPreKeysToServerIfRequired = async () => {
|
|
273
415
|
try {
|
|
416
|
+
let count = 0;
|
|
274
417
|
const preKeyCount = await getAvailablePreKeysOnServer();
|
|
275
|
-
|
|
418
|
+
if (preKeyCount === 0)
|
|
419
|
+
count = INITIAL_PREKEY_COUNT;
|
|
420
|
+
else
|
|
421
|
+
count = MIN_PREKEY_COUNT;
|
|
422
|
+
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists();
|
|
276
423
|
logger.info(`${preKeyCount} pre-keys found on server`);
|
|
277
|
-
|
|
424
|
+
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`);
|
|
425
|
+
const lowServerCount = preKeyCount <= count;
|
|
426
|
+
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0;
|
|
427
|
+
const shouldUpload = lowServerCount || missingCurrentPreKey;
|
|
428
|
+
if (shouldUpload) {
|
|
429
|
+
const reasons = [];
|
|
430
|
+
if (lowServerCount)
|
|
431
|
+
reasons.push(`server count low (${preKeyCount})`);
|
|
432
|
+
if (missingCurrentPreKey)
|
|
433
|
+
reasons.push(`current prekey ${currentPreKeyId} missing from storage`);
|
|
434
|
+
logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`);
|
|
278
435
|
await uploadPreKeys(count);
|
|
279
436
|
}
|
|
280
|
-
|
|
437
|
+
else {
|
|
438
|
+
logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
281
442
|
logger.error({ error }, 'Failed to check/upload pre-keys during initialization');
|
|
443
|
+
// Don't throw - allow connection to continue even if pre-key check fails
|
|
282
444
|
}
|
|
283
445
|
};
|
|
284
|
-
const onMessageReceived = (data) => {
|
|
285
|
-
noise.decodeFrame(data, frame => {
|
|
286
|
-
var _a;
|
|
446
|
+
const onMessageReceived = async (data) => {
|
|
447
|
+
await noise.decodeFrame(data, frame => {
|
|
287
448
|
// reset ping timeout
|
|
288
449
|
lastDateRecv = new Date();
|
|
289
450
|
let anyTriggered = false;
|
|
@@ -292,21 +453,21 @@ const makeSocket = (config) => {
|
|
|
292
453
|
if (!(frame instanceof Uint8Array)) {
|
|
293
454
|
const msgId = frame.attrs.id;
|
|
294
455
|
if (logger.level === 'trace') {
|
|
295
|
-
logger.trace({ xml:
|
|
456
|
+
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
296
457
|
}
|
|
297
458
|
/* Check if this is a response to a message we sent */
|
|
298
|
-
anyTriggered = ws.emit(`${
|
|
459
|
+
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
299
460
|
/* Check if this is a response to a message we are expecting */
|
|
300
461
|
const l0 = frame.tag;
|
|
301
462
|
const l1 = frame.attrs || {};
|
|
302
|
-
const l2 = Array.isArray(frame.content) ?
|
|
463
|
+
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '';
|
|
303
464
|
for (const key of Object.keys(l1)) {
|
|
304
|
-
anyTriggered = ws.emit(`${
|
|
305
|
-
anyTriggered = ws.emit(`${
|
|
306
|
-
anyTriggered = ws.emit(`${
|
|
465
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;
|
|
466
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;
|
|
467
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;
|
|
307
468
|
}
|
|
308
|
-
anyTriggered = ws.emit(`${
|
|
309
|
-
anyTriggered = ws.emit(`${
|
|
469
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
|
|
470
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
|
|
310
471
|
if (!anyTriggered && logger.level === 'debug') {
|
|
311
472
|
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
|
|
312
473
|
}
|
|
@@ -315,39 +476,28 @@ const makeSocket = (config) => {
|
|
|
315
476
|
};
|
|
316
477
|
const end = async (error) => {
|
|
317
478
|
if (closed) {
|
|
318
|
-
logger.trace({ trace: error
|
|
479
|
+
logger.trace({ trace: error?.stack }, 'connection already closed');
|
|
319
480
|
return;
|
|
320
481
|
}
|
|
321
482
|
closed = true;
|
|
322
|
-
logger.info({ trace: error
|
|
483
|
+
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
323
484
|
clearInterval(keepAliveReq);
|
|
324
485
|
clearTimeout(qrTimer);
|
|
325
486
|
ws.removeAllListeners('close');
|
|
326
|
-
ws.removeAllListeners('error');
|
|
327
487
|
ws.removeAllListeners('open');
|
|
328
488
|
ws.removeAllListeners('message');
|
|
329
489
|
if (!ws.isClosed && !ws.isClosing) {
|
|
330
490
|
try {
|
|
331
491
|
await ws.close();
|
|
332
492
|
}
|
|
333
|
-
catch
|
|
334
|
-
}
|
|
335
|
-
// Determine what the consumer should do next based on the disconnect reason
|
|
336
|
-
const statusCode = error?.output?.statusCode;
|
|
337
|
-
// loggedOut (401) and badSession (500) require deleting the session folder and re-scanning QR
|
|
338
|
-
const shouldDeleteSession = statusCode === Types_1.DisconnectReason.loggedOut ||
|
|
339
|
-
statusCode === Types_1.DisconnectReason.badSession;
|
|
340
|
-
// For all other closures (connectionClosed, connectionLost, restartRequired, etc.) just reconnect
|
|
341
|
-
const shouldReconnect = !shouldDeleteSession &&
|
|
342
|
-
statusCode !== Types_1.DisconnectReason.timedOut;
|
|
493
|
+
catch { }
|
|
494
|
+
}
|
|
343
495
|
ev.emit('connection.update', {
|
|
344
496
|
connection: 'close',
|
|
345
497
|
lastDisconnect: {
|
|
346
498
|
error,
|
|
347
499
|
date: new Date()
|
|
348
|
-
}
|
|
349
|
-
shouldDeleteSession,
|
|
350
|
-
shouldReconnect
|
|
500
|
+
}
|
|
351
501
|
});
|
|
352
502
|
ev.removeAllListeners('connection.update');
|
|
353
503
|
};
|
|
@@ -356,7 +506,7 @@ const makeSocket = (config) => {
|
|
|
356
506
|
return;
|
|
357
507
|
}
|
|
358
508
|
if (ws.isClosed || ws.isClosing) {
|
|
359
|
-
throw new
|
|
509
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
360
510
|
}
|
|
361
511
|
let onOpen;
|
|
362
512
|
let onClose;
|
|
@@ -366,20 +516,13 @@ const makeSocket = (config) => {
|
|
|
366
516
|
ws.on('open', onOpen);
|
|
367
517
|
ws.on('close', onClose);
|
|
368
518
|
ws.on('error', onClose);
|
|
369
|
-
})
|
|
370
|
-
.finally(() => {
|
|
519
|
+
}).finally(() => {
|
|
371
520
|
ws.off('open', onOpen);
|
|
372
521
|
ws.off('close', onClose);
|
|
373
522
|
ws.off('error', onClose);
|
|
374
523
|
});
|
|
375
524
|
};
|
|
376
|
-
const startKeepAliveRequest = () => {
|
|
377
|
-
// Add a random initial offset (±20% of keepAliveIntervalMs) so bots started
|
|
378
|
-
// at the same time do NOT send keep-alive pings simultaneously, which would
|
|
379
|
-
// create a thundering-herd of IQ requests to WA servers.
|
|
380
|
-
const jitter = Math.floor(Math.random() * keepAliveIntervalMs * 0.4) - Math.floor(keepAliveIntervalMs * 0.2);
|
|
381
|
-
const effectiveInterval = Math.max(keepAliveIntervalMs + jitter, 5000);
|
|
382
|
-
return (keepAliveReq = setInterval(() => {
|
|
525
|
+
const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {
|
|
383
526
|
if (!lastDateRecv) {
|
|
384
527
|
lastDateRecv = new Date();
|
|
385
528
|
}
|
|
@@ -388,8 +531,8 @@ const makeSocket = (config) => {
|
|
|
388
531
|
check if it's been a suspicious amount of time since the server responded with our last seen
|
|
389
532
|
it could be that the network is down
|
|
390
533
|
*/
|
|
391
|
-
if (diff > keepAliveIntervalMs +
|
|
392
|
-
void end(new
|
|
534
|
+
if (diff > keepAliveIntervalMs + 5000) {
|
|
535
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
393
536
|
}
|
|
394
537
|
else if (ws.isOpen) {
|
|
395
538
|
// if its all good, send a keep alive request
|
|
@@ -397,42 +540,37 @@ const makeSocket = (config) => {
|
|
|
397
540
|
tag: 'iq',
|
|
398
541
|
attrs: {
|
|
399
542
|
id: generateMessageTag(),
|
|
400
|
-
to:
|
|
543
|
+
to: S_WHATSAPP_NET,
|
|
401
544
|
type: 'get',
|
|
402
|
-
xmlns: 'w:p'
|
|
545
|
+
xmlns: 'w:p'
|
|
403
546
|
},
|
|
404
547
|
content: [{ tag: 'ping', attrs: {} }]
|
|
405
|
-
})
|
|
406
|
-
.catch(err => {
|
|
548
|
+
}).catch(err => {
|
|
407
549
|
logger.error({ trace: err.stack }, 'error in sending keep alive');
|
|
408
550
|
});
|
|
409
551
|
}
|
|
410
552
|
else {
|
|
411
553
|
logger.warn('keep alive called when WS not open');
|
|
412
554
|
}
|
|
413
|
-
},
|
|
414
|
-
};
|
|
555
|
+
}, keepAliveIntervalMs));
|
|
415
556
|
/** i have no idea why this exists. pls enlighten me */
|
|
416
|
-
const sendPassiveIq = (tag) =>
|
|
557
|
+
const sendPassiveIq = (tag) => query({
|
|
417
558
|
tag: 'iq',
|
|
418
559
|
attrs: {
|
|
419
|
-
to:
|
|
560
|
+
to: S_WHATSAPP_NET,
|
|
420
561
|
xmlns: 'passive',
|
|
421
|
-
type: 'set'
|
|
562
|
+
type: 'set'
|
|
422
563
|
},
|
|
423
|
-
content: [
|
|
424
|
-
|
|
425
|
-
]
|
|
426
|
-
}));
|
|
564
|
+
content: [{ tag, attrs: {} }]
|
|
565
|
+
});
|
|
427
566
|
/** logout & invalidate connection */
|
|
428
567
|
const logout = async (msg) => {
|
|
429
|
-
|
|
430
|
-
const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;
|
|
568
|
+
const jid = authState.creds.me?.id;
|
|
431
569
|
if (jid) {
|
|
432
570
|
await sendNode({
|
|
433
571
|
tag: 'iq',
|
|
434
572
|
attrs: {
|
|
435
|
-
to:
|
|
573
|
+
to: S_WHATSAPP_NET,
|
|
436
574
|
type: 'set',
|
|
437
575
|
id: generateMessageTag(),
|
|
438
576
|
xmlns: 'md'
|
|
@@ -448,27 +586,23 @@ const makeSocket = (config) => {
|
|
|
448
586
|
]
|
|
449
587
|
});
|
|
450
588
|
}
|
|
451
|
-
end(new
|
|
589
|
+
void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
452
590
|
};
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
591
|
+
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
592
|
+
const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5));
|
|
593
|
+
if (customPairingCode && customPairingCode?.length !== 8) {
|
|
594
|
+
throw new Error('Custom pairing code must be exactly 8 chars');
|
|
595
|
+
}
|
|
596
|
+
authState.creds.pairingCode = pairingCode;
|
|
461
597
|
authState.creds.me = {
|
|
462
|
-
id:
|
|
598
|
+
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
463
599
|
name: '~'
|
|
464
600
|
};
|
|
465
|
-
|
|
466
601
|
ev.emit('creds.update', authState.creds);
|
|
467
|
-
|
|
468
602
|
await sendNode({
|
|
469
603
|
tag: 'iq',
|
|
470
604
|
attrs: {
|
|
471
|
-
to:
|
|
605
|
+
to: S_WHATSAPP_NET,
|
|
472
606
|
type: 'set',
|
|
473
607
|
id: generateMessageTag(),
|
|
474
608
|
xmlns: 'md'
|
|
@@ -495,7 +629,7 @@ const makeSocket = (config) => {
|
|
|
495
629
|
{
|
|
496
630
|
tag: 'companion_platform_id',
|
|
497
631
|
attrs: {},
|
|
498
|
-
content:
|
|
632
|
+
content: getPlatformId(browser[1])
|
|
499
633
|
},
|
|
500
634
|
{
|
|
501
635
|
tag: 'companion_platform_display',
|
|
@@ -505,68 +639,38 @@ const makeSocket = (config) => {
|
|
|
505
639
|
{
|
|
506
640
|
tag: 'link_code_pairing_nonce',
|
|
507
641
|
attrs: {},
|
|
508
|
-
content:
|
|
642
|
+
content: '0'
|
|
509
643
|
}
|
|
510
644
|
]
|
|
511
645
|
}
|
|
512
646
|
]
|
|
513
647
|
});
|
|
514
|
-
|
|
515
648
|
return authState.creds.pairingCode;
|
|
516
|
-
}
|
|
649
|
+
};
|
|
517
650
|
async function generatePairingKey() {
|
|
518
|
-
const salt =
|
|
519
|
-
const randomIv =
|
|
520
|
-
const key = await
|
|
521
|
-
const ciphered =
|
|
651
|
+
const salt = randomBytes(32);
|
|
652
|
+
const randomIv = randomBytes(16);
|
|
653
|
+
const key = await derivePairingCodeKey(authState.creds.pairingCode, salt);
|
|
654
|
+
const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv);
|
|
522
655
|
return Buffer.concat([salt, randomIv, ciphered]);
|
|
523
656
|
}
|
|
524
657
|
const sendWAMBuffer = (wamBuffer) => {
|
|
525
658
|
return query({
|
|
526
659
|
tag: 'iq',
|
|
527
660
|
attrs: {
|
|
528
|
-
to:
|
|
661
|
+
to: S_WHATSAPP_NET,
|
|
529
662
|
id: generateMessageTag(),
|
|
530
663
|
xmlns: 'w:stats'
|
|
531
664
|
},
|
|
532
665
|
content: [
|
|
533
666
|
{
|
|
534
667
|
tag: 'add',
|
|
535
|
-
attrs: {},
|
|
668
|
+
attrs: { t: Math.round(Date.now() / 1000) + '' },
|
|
536
669
|
content: wamBuffer
|
|
537
670
|
}
|
|
538
671
|
]
|
|
539
672
|
});
|
|
540
673
|
};
|
|
541
|
-
let serverTimeOffsetMs = 0;
|
|
542
|
-
const updateServerTimeOffset = ({ attrs }) => {
|
|
543
|
-
const tValue = attrs && attrs.t;
|
|
544
|
-
if (!tValue) return;
|
|
545
|
-
const parsed = Number(tValue);
|
|
546
|
-
if (Number.isNaN(parsed) || parsed <= 0) return;
|
|
547
|
-
const localMs = Date.now();
|
|
548
|
-
serverTimeOffsetMs = parsed * 1000 - localMs;
|
|
549
|
-
logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
|
|
550
|
-
};
|
|
551
|
-
const getUnifiedSessionId = () => {
|
|
552
|
-
const offsetMs = 3 * Defaults_1.TimeMs.Day;
|
|
553
|
-
const now = Date.now() + serverTimeOffsetMs;
|
|
554
|
-
const id = (now + offsetMs) % Defaults_1.TimeMs.Week;
|
|
555
|
-
return id.toString();
|
|
556
|
-
};
|
|
557
|
-
const sendUnifiedSession = async () => {
|
|
558
|
-
if (!ws.isOpen) return;
|
|
559
|
-
const node = {
|
|
560
|
-
tag: 'ib',
|
|
561
|
-
attrs: {},
|
|
562
|
-
content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
|
|
563
|
-
};
|
|
564
|
-
try {
|
|
565
|
-
await sendNode(node);
|
|
566
|
-
} catch (error) {
|
|
567
|
-
logger.debug({ error }, 'failed to send unified_session telemetry');
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
674
|
ws.on('message', onMessageReceived);
|
|
571
675
|
ws.on('open', async () => {
|
|
572
676
|
try {
|
|
@@ -574,25 +678,26 @@ const makeSocket = (config) => {
|
|
|
574
678
|
}
|
|
575
679
|
catch (err) {
|
|
576
680
|
logger.error({ err }, 'error in validating connection');
|
|
577
|
-
end(err);
|
|
681
|
+
void end(err);
|
|
578
682
|
}
|
|
579
683
|
});
|
|
580
684
|
ws.on('error', mapWebSocketError(end));
|
|
581
|
-
ws.on('close', () => void end(new
|
|
582
|
-
|
|
685
|
+
ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
686
|
+
// the server terminated the connection
|
|
687
|
+
ws.on('CB:xmlstreamend', () => void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
583
688
|
// QR gen
|
|
584
689
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
585
690
|
const iq = {
|
|
586
691
|
tag: 'iq',
|
|
587
692
|
attrs: {
|
|
588
|
-
to:
|
|
693
|
+
to: S_WHATSAPP_NET,
|
|
589
694
|
type: 'result',
|
|
590
|
-
id: stanza.attrs.id
|
|
695
|
+
id: stanza.attrs.id
|
|
591
696
|
}
|
|
592
697
|
};
|
|
593
698
|
await sendNode(iq);
|
|
594
|
-
const pairDeviceNode =
|
|
595
|
-
const refNodes =
|
|
699
|
+
const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
|
|
700
|
+
const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
|
|
596
701
|
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
|
|
597
702
|
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
|
|
598
703
|
const advB64 = creds.advSecretKey;
|
|
@@ -603,7 +708,7 @@ const makeSocket = (config) => {
|
|
|
603
708
|
}
|
|
604
709
|
const refNode = refNodes.shift();
|
|
605
710
|
if (!refNode) {
|
|
606
|
-
void end(new
|
|
711
|
+
void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
|
|
607
712
|
return;
|
|
608
713
|
}
|
|
609
714
|
const ref = refNode.content.toString('utf-8');
|
|
@@ -620,7 +725,7 @@ const makeSocket = (config) => {
|
|
|
620
725
|
logger.debug('pair success recv');
|
|
621
726
|
try {
|
|
622
727
|
updateServerTimeOffset(stanza);
|
|
623
|
-
const { reply, creds: updatedCreds } =
|
|
728
|
+
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
|
|
624
729
|
logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
|
|
625
730
|
ev.emit('creds.update', updatedCreds);
|
|
626
731
|
ev.emit('connection.update', { isNewLogin: true, qr: undefined });
|
|
@@ -632,84 +737,97 @@ const makeSocket = (config) => {
|
|
|
632
737
|
void end(error);
|
|
633
738
|
}
|
|
634
739
|
});
|
|
740
|
+
// login complete
|
|
635
741
|
ws.on('CB:success', async (node) => {
|
|
636
742
|
try {
|
|
637
743
|
updateServerTimeOffset(node);
|
|
638
744
|
await uploadPreKeysToServerIfRequired();
|
|
639
745
|
await sendPassiveIq('active');
|
|
640
|
-
|
|
746
|
+
// After successful login, validate our key-bundle against server
|
|
747
|
+
try {
|
|
748
|
+
await digestKeyBundle();
|
|
749
|
+
}
|
|
750
|
+
catch (e) {
|
|
751
|
+
logger.warn({ e }, 'failed to run digest after login');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
641
755
|
logger.warn({ err }, 'failed to send initial passive iq');
|
|
642
756
|
}
|
|
643
757
|
logger.info('opened connection to WA');
|
|
644
|
-
clearTimeout(qrTimer);
|
|
758
|
+
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
645
759
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
646
760
|
ev.emit('connection.update', { connection: 'open' });
|
|
647
761
|
void sendUnifiedSession();
|
|
762
|
+
if (node.attrs.lid && authState.creds.me?.id) {
|
|
763
|
+
const myLID = node.attrs.lid;
|
|
764
|
+
process.nextTick(async () => {
|
|
765
|
+
try {
|
|
766
|
+
const myPN = authState.creds.me.id;
|
|
767
|
+
// Store our own LID-PN mapping
|
|
768
|
+
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }]);
|
|
769
|
+
// Create device list for our own user (needed for bulk migration)
|
|
770
|
+
const { user, device } = jidDecode(myPN);
|
|
771
|
+
await authState.keys.set({
|
|
772
|
+
'device-list': {
|
|
773
|
+
[user]: [device?.toString() || '0']
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
// migrate our own session
|
|
777
|
+
await signalRepository.migrateSession(myPN, myLID);
|
|
778
|
+
logger.info({ myPN, myLID }, 'Own LID session created successfully');
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
logger.error({ error, lid: myLID }, 'Failed to create own LID session');
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
}
|
|
648
785
|
});
|
|
649
786
|
ws.on('CB:stream:error', (node) => {
|
|
650
|
-
const [reasonNode] =
|
|
787
|
+
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
651
788
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
652
|
-
const { reason, statusCode } =
|
|
653
|
-
void end(new
|
|
789
|
+
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
790
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
654
791
|
});
|
|
792
|
+
// stream fail, possible logout
|
|
655
793
|
ws.on('CB:failure', (node) => {
|
|
656
794
|
const reason = +(node.attrs.reason || 500);
|
|
657
|
-
|
|
658
|
-
const description = (Array.isArray(node.content) && node.content[0]?.attrs?.description)
|
|
659
|
-
|| node.attrs.reason
|
|
660
|
-
|| 'unknown';
|
|
661
|
-
logger.warn({ reason, description, attrs: node.attrs }, 'WA connection failure received');
|
|
662
|
-
void end(new boom_1.Boom(`Connection Failure (${description})`, { statusCode: reason, data: node.attrs }));
|
|
795
|
+
void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }));
|
|
663
796
|
});
|
|
664
797
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
665
|
-
void end(new
|
|
798
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
666
799
|
});
|
|
667
|
-
ws.on('CB:ib,,offline_preview', (node) => {
|
|
800
|
+
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
668
801
|
logger.info('offline preview received', JSON.stringify(node));
|
|
669
|
-
sendNode({
|
|
802
|
+
await sendNode({
|
|
670
803
|
tag: 'ib',
|
|
671
804
|
attrs: {},
|
|
672
805
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
673
806
|
});
|
|
674
807
|
});
|
|
675
808
|
ws.on('CB:ib,,edge_routing', (node) => {
|
|
676
|
-
const edgeRoutingNode =
|
|
677
|
-
const routingInfo =
|
|
678
|
-
if (routingInfo
|
|
679
|
-
authState.creds.routingInfo = Buffer.from(routingInfo
|
|
809
|
+
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing');
|
|
810
|
+
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info');
|
|
811
|
+
if (routingInfo?.content) {
|
|
812
|
+
authState.creds.routingInfo = Buffer.from(routingInfo?.content);
|
|
680
813
|
ev.emit('creds.update', authState.creds);
|
|
681
814
|
}
|
|
682
815
|
});
|
|
683
816
|
let didStartBuffer = false;
|
|
684
|
-
let offlineFlushTimer = null;
|
|
685
817
|
process.nextTick(() => {
|
|
686
|
-
|
|
687
|
-
if ((_a = creds.me) === null || _a === void 0 ? void 0 : _a.id) {
|
|
818
|
+
if (creds.me?.id) {
|
|
688
819
|
// start buffering important events
|
|
689
820
|
// if we're logged in
|
|
690
821
|
ev.buffer();
|
|
691
822
|
didStartBuffer = true;
|
|
692
|
-
// Safety net: if server never sends CB:ib,,offline (e.g. slow server, no pending msgs),
|
|
693
|
-
// force-flush the buffer after 10s so messages/commands are never stuck
|
|
694
|
-
offlineFlushTimer = setTimeout(() => {
|
|
695
|
-
if (didStartBuffer && ev.isBuffering()) {
|
|
696
|
-
logger.warn('offline event not received within 10s — force-flushing event buffer to prevent message lag');
|
|
697
|
-
ev.flush(true);
|
|
698
|
-
ev.emit('connection.update', { receivedPendingNotifications: true });
|
|
699
|
-
}
|
|
700
|
-
}, 10000);
|
|
701
823
|
}
|
|
702
824
|
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
|
|
703
825
|
});
|
|
704
826
|
// called when all offline notifs are handled
|
|
705
827
|
ws.on('CB:ib,,offline', (node) => {
|
|
706
|
-
const child =
|
|
707
|
-
const offlineNotifs = +(
|
|
828
|
+
const child = getBinaryNodeChild(node, 'offline');
|
|
829
|
+
const offlineNotifs = +(child?.attrs.count || 0);
|
|
708
830
|
logger.info(`handled ${offlineNotifs} offline messages/notifications`);
|
|
709
|
-
if (offlineFlushTimer) {
|
|
710
|
-
clearTimeout(offlineFlushTimer);
|
|
711
|
-
offlineFlushTimer = null;
|
|
712
|
-
}
|
|
713
831
|
if (didStartBuffer) {
|
|
714
832
|
ev.flush();
|
|
715
833
|
logger.trace('flushed events for initial buffer');
|
|
@@ -718,39 +836,66 @@ const makeSocket = (config) => {
|
|
|
718
836
|
});
|
|
719
837
|
// update credentials when required
|
|
720
838
|
ev.on('creds.update', update => {
|
|
721
|
-
|
|
722
|
-
const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;
|
|
839
|
+
const name = update.me?.name;
|
|
723
840
|
// if name has just been received
|
|
724
|
-
if (
|
|
841
|
+
if (creds.me?.name !== name) {
|
|
725
842
|
logger.debug({ name }, 'updated pushName');
|
|
726
843
|
sendNode({
|
|
727
844
|
tag: 'presence',
|
|
728
845
|
attrs: { name: name }
|
|
729
|
-
})
|
|
730
|
-
.catch(err => {
|
|
846
|
+
}).catch(err => {
|
|
731
847
|
logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
|
|
732
848
|
});
|
|
733
849
|
}
|
|
734
850
|
Object.assign(creds, update);
|
|
735
851
|
});
|
|
736
|
-
|
|
852
|
+
const updateServerTimeOffset = ({ attrs }) => {
|
|
853
|
+
const tValue = attrs?.t;
|
|
854
|
+
if (!tValue) {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
const parsed = Number(tValue);
|
|
858
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const localMs = Date.now();
|
|
862
|
+
serverTimeOffsetMs = parsed * 1000 - localMs;
|
|
863
|
+
logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
|
|
864
|
+
};
|
|
865
|
+
const getUnifiedSessionId = () => {
|
|
866
|
+
const offsetMs = 3 * TimeMs.Day;
|
|
867
|
+
const now = Date.now() + serverTimeOffsetMs;
|
|
868
|
+
const id = (now + offsetMs) % TimeMs.Week;
|
|
869
|
+
return id.toString();
|
|
870
|
+
};
|
|
871
|
+
const sendUnifiedSession = async () => {
|
|
872
|
+
if (!ws.isOpen) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const node = {
|
|
876
|
+
tag: 'ib',
|
|
877
|
+
attrs: {},
|
|
878
|
+
content: [
|
|
879
|
+
{
|
|
880
|
+
tag: 'unified_session',
|
|
881
|
+
attrs: {
|
|
882
|
+
id: getUnifiedSessionId()
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
]
|
|
886
|
+
};
|
|
737
887
|
try {
|
|
738
|
-
await
|
|
739
|
-
} catch (error) {
|
|
740
|
-
logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
|
|
888
|
+
await sendNode(node);
|
|
741
889
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
logger.debug({ error }, 'failed to send unified_session telemetry');
|
|
892
|
+
}
|
|
893
|
+
};
|
|
746
894
|
return {
|
|
747
895
|
type: 'md',
|
|
748
896
|
ws,
|
|
749
897
|
ev,
|
|
750
|
-
authState: {
|
|
751
|
-
creds,
|
|
752
|
-
keys
|
|
753
|
-
},
|
|
898
|
+
authState: { creds, keys },
|
|
754
899
|
signalRepository,
|
|
755
900
|
get user() {
|
|
756
901
|
return authState.creds.me;
|
|
@@ -766,20 +911,25 @@ const makeSocket = (config) => {
|
|
|
766
911
|
onUnexpectedError,
|
|
767
912
|
uploadPreKeys,
|
|
768
913
|
uploadPreKeysToServerIfRequired,
|
|
914
|
+
digestKeyBundle,
|
|
915
|
+
rotateSignedPreKey,
|
|
769
916
|
requestPairingCode,
|
|
770
917
|
updateServerTimeOffset,
|
|
771
918
|
sendUnifiedSession,
|
|
772
|
-
|
|
919
|
+
wamBuffer: publicWAMBuffer,
|
|
920
|
+
/** Waits for the connection to WA to reach a state */
|
|
921
|
+
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
|
773
922
|
sendWAMBuffer,
|
|
923
|
+
executeUSyncQuery,
|
|
924
|
+
onWhatsApp
|
|
774
925
|
};
|
|
775
926
|
};
|
|
776
|
-
exports.makeSocket = makeSocket;
|
|
777
927
|
/**
|
|
778
928
|
* map the websocket error to the right type
|
|
779
929
|
* so it can be retried by the caller
|
|
780
930
|
* */
|
|
781
931
|
function mapWebSocketError(handler) {
|
|
782
932
|
return (error) => {
|
|
783
|
-
handler(new
|
|
933
|
+
handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }));
|
|
784
934
|
};
|
|
785
|
-
}
|
|
935
|
+
}
|