violetics 7.0.0-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 -144
- 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 -442
- 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 +878 -552
- 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 +463 -289
- 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 -145
- 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 -35
- 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 +32 -34
- 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 +75 -108
- 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 -111
- 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,65 +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
|
-
ws.connect();
|
|
34
|
-
const ev = (0, Utils_1.makeEventBuffer)(logger);
|
|
35
41
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
|
36
|
-
const ephemeralKeyPair =
|
|
42
|
+
const ephemeralKeyPair = Curve.generateKeyPair();
|
|
37
43
|
/** WA noise protocol wrapper */
|
|
38
|
-
const noise =
|
|
44
|
+
const noise = makeNoiseHandler({
|
|
39
45
|
keyPair: ephemeralKeyPair,
|
|
40
|
-
NOISE_HEADER:
|
|
46
|
+
NOISE_HEADER: NOISE_WA_HEADER,
|
|
41
47
|
logger,
|
|
42
|
-
routingInfo:
|
|
48
|
+
routingInfo: authState?.creds?.routingInfo
|
|
43
49
|
});
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const signalRepository = makeSignalRepository({ creds, keys });
|
|
48
|
-
let lastDateRecv;
|
|
49
|
-
let epoch = 1;
|
|
50
|
-
let keepAliveReq;
|
|
51
|
-
let qrTimer;
|
|
52
|
-
let closed = false;
|
|
53
|
-
const uqTagId = (0, Utils_1.generateMdTagPrefix)();
|
|
54
|
-
const generateMessageTag = () => `${uqTagId}${epoch++}`;
|
|
55
|
-
const sendPromise = (0, util_1.promisify)(ws.send);
|
|
50
|
+
const ws = new WebSocketClient(url, config);
|
|
51
|
+
ws.connect();
|
|
52
|
+
const sendPromise = promisify(ws.send);
|
|
56
53
|
/** send a raw buffer */
|
|
57
54
|
const sendRawMessage = async (data) => {
|
|
58
55
|
if (!ws.isOpen) {
|
|
59
|
-
throw new
|
|
56
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
60
57
|
}
|
|
61
58
|
const bytes = noise.encodeFrame(data);
|
|
62
|
-
await
|
|
59
|
+
await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
|
|
63
60
|
try {
|
|
64
61
|
await sendPromise.call(ws, bytes);
|
|
65
62
|
resolve();
|
|
@@ -72,50 +69,11 @@ const makeSocket = (config) => {
|
|
|
72
69
|
/** send a binary node */
|
|
73
70
|
const sendNode = (frame) => {
|
|
74
71
|
if (logger.level === 'trace') {
|
|
75
|
-
logger.trace({ xml:
|
|
72
|
+
logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' });
|
|
76
73
|
}
|
|
77
|
-
const buff =
|
|
74
|
+
const buff = encodeBinaryNode(frame);
|
|
78
75
|
return sendRawMessage(buff);
|
|
79
76
|
};
|
|
80
|
-
/** log & process any unexpected errors */
|
|
81
|
-
const onUnexpectedError = (err, msg) => {
|
|
82
|
-
logger.error({ err }, `unexpected error in '${msg}'`);
|
|
83
|
-
const message = (err && ((err.stack || err.message) || String(err))).toLowerCase();
|
|
84
|
-
if (message.includes('bad mac') || (message.includes('mac') && message.includes('invalid'))) {
|
|
85
|
-
try {
|
|
86
|
-
uploadPreKeys()
|
|
87
|
-
.catch(e => logger.warn({ e }, 'failed to re-upload prekeys after bad mac'));
|
|
88
|
-
}
|
|
89
|
-
catch (_e) {
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
/** await the next incoming message */
|
|
94
|
-
const awaitNextMessage = async (sendMsg) => {
|
|
95
|
-
if (!ws.isOpen) {
|
|
96
|
-
throw new boom_1.Boom('Connection Closed', {
|
|
97
|
-
statusCode: Types_1.DisconnectReason.connectionClosed
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
let onOpen;
|
|
101
|
-
let onClose;
|
|
102
|
-
const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {
|
|
103
|
-
onOpen = resolve;
|
|
104
|
-
onClose = mapWebSocketError(reject);
|
|
105
|
-
ws.on('frame', onOpen);
|
|
106
|
-
ws.on('close', onClose);
|
|
107
|
-
ws.on('error', onClose);
|
|
108
|
-
})
|
|
109
|
-
.finally(() => {
|
|
110
|
-
ws.off('frame', onOpen);
|
|
111
|
-
ws.off('close', onClose);
|
|
112
|
-
ws.off('error', onClose);
|
|
113
|
-
});
|
|
114
|
-
if (sendMsg) {
|
|
115
|
-
sendRawMessage(sendMsg).catch(onClose);
|
|
116
|
-
}
|
|
117
|
-
return result;
|
|
118
|
-
};
|
|
119
77
|
/**
|
|
120
78
|
* Wait for a message with a certain tag to be received
|
|
121
79
|
* @param msgId the message tag to await
|
|
@@ -125,21 +83,38 @@ const makeSocket = (config) => {
|
|
|
125
83
|
let onRecv;
|
|
126
84
|
let onErr;
|
|
127
85
|
try {
|
|
128
|
-
const result = await
|
|
129
|
-
onRecv =
|
|
86
|
+
const result = await promiseTimeout(timeoutMs, (resolve, reject) => {
|
|
87
|
+
onRecv = data => {
|
|
88
|
+
resolve(data);
|
|
89
|
+
};
|
|
130
90
|
onErr = err => {
|
|
131
|
-
reject(err ||
|
|
91
|
+
reject(err ||
|
|
92
|
+
new Boom('Connection Closed', {
|
|
93
|
+
statusCode: DisconnectReason.connectionClosed
|
|
94
|
+
}));
|
|
132
95
|
};
|
|
133
96
|
ws.on(`TAG:${msgId}`, onRecv);
|
|
134
97
|
ws.on('close', onErr);
|
|
135
98
|
ws.on('error', onErr);
|
|
99
|
+
return () => reject(new Boom('Query Cancelled'));
|
|
136
100
|
});
|
|
137
101
|
return result;
|
|
138
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
|
+
}
|
|
139
111
|
finally {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
112
|
+
if (onRecv)
|
|
113
|
+
ws.off(`TAG:${msgId}`, onRecv);
|
|
114
|
+
if (onErr) {
|
|
115
|
+
ws.off('close', onErr);
|
|
116
|
+
ws.off('error', onErr);
|
|
117
|
+
}
|
|
143
118
|
}
|
|
144
119
|
};
|
|
145
120
|
/** send a query, and wait for its response. auto-generates message ID if not provided */
|
|
@@ -148,12 +123,179 @@ const makeSocket = (config) => {
|
|
|
148
123
|
node.attrs.id = generateMessageTag();
|
|
149
124
|
}
|
|
150
125
|
const msgId = node.attrs.id;
|
|
151
|
-
const
|
|
152
|
-
waitForMessage(msgId, timeoutMs)
|
|
126
|
+
const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
|
|
127
|
+
const result = waitForMessage(msgId, timeoutMs).catch(reject);
|
|
153
128
|
sendNode(node)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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);
|
|
157
299
|
}
|
|
158
300
|
return result;
|
|
159
301
|
};
|
|
@@ -162,30 +304,30 @@ const makeSocket = (config) => {
|
|
|
162
304
|
let helloMsg = {
|
|
163
305
|
clientHello: { ephemeral: ephemeralKeyPair.public }
|
|
164
306
|
};
|
|
165
|
-
helloMsg =
|
|
307
|
+
helloMsg = proto.HandshakeMessage.fromObject(helloMsg);
|
|
166
308
|
logger.info({ browser, helloMsg }, 'connected to WA');
|
|
167
|
-
const init =
|
|
309
|
+
const init = proto.HandshakeMessage.encode(helloMsg).finish();
|
|
168
310
|
const result = await awaitNextMessage(init);
|
|
169
|
-
const handshake =
|
|
311
|
+
const handshake = proto.HandshakeMessage.decode(result);
|
|
170
312
|
logger.trace({ handshake }, 'handshake recv from WA');
|
|
171
|
-
const keyEnc =
|
|
313
|
+
const keyEnc = noise.processHandshake(handshake, creds.noiseKey);
|
|
172
314
|
let node;
|
|
173
315
|
if (!creds.me) {
|
|
174
|
-
node =
|
|
316
|
+
node = generateRegistrationNode(creds, config);
|
|
175
317
|
logger.info({ node }, 'not logged in, attempting registration...');
|
|
176
318
|
}
|
|
177
319
|
else {
|
|
178
|
-
node =
|
|
320
|
+
node = generateLoginNode(creds.me.id, config);
|
|
179
321
|
logger.info({ node }, 'logging in...');
|
|
180
322
|
}
|
|
181
|
-
const payloadEnc = noise.encrypt(
|
|
182
|
-
await sendRawMessage(
|
|
323
|
+
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish());
|
|
324
|
+
await sendRawMessage(proto.HandshakeMessage.encode({
|
|
183
325
|
clientFinish: {
|
|
184
326
|
static: keyEnc,
|
|
185
|
-
payload: payloadEnc
|
|
186
|
-
}
|
|
327
|
+
payload: payloadEnc
|
|
328
|
+
}
|
|
187
329
|
}).finish());
|
|
188
|
-
noise.finishInit();
|
|
330
|
+
await noise.finishInit();
|
|
189
331
|
startKeepAliveRequest();
|
|
190
332
|
};
|
|
191
333
|
const getAvailablePreKeysOnServer = async () => {
|
|
@@ -195,42 +337,50 @@ const makeSocket = (config) => {
|
|
|
195
337
|
id: generateMessageTag(),
|
|
196
338
|
xmlns: 'encrypt',
|
|
197
339
|
type: 'get',
|
|
198
|
-
to:
|
|
340
|
+
to: S_WHATSAPP_NET
|
|
199
341
|
},
|
|
200
|
-
content: [
|
|
201
|
-
{ tag: 'count', attrs: {} }
|
|
202
|
-
]
|
|
342
|
+
content: [{ tag: 'count', attrs: {} }]
|
|
203
343
|
});
|
|
204
|
-
const countChild =
|
|
344
|
+
const countChild = getBinaryNodeChild(result, 'count');
|
|
205
345
|
return +countChild.attrs.value;
|
|
206
346
|
};
|
|
347
|
+
// Pre-key upload state management
|
|
207
348
|
let uploadPreKeysPromise = null;
|
|
208
349
|
let lastUploadTime = 0;
|
|
209
|
-
|
|
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)
|
|
210
353
|
if (retryCount === 0) {
|
|
211
354
|
const timeSinceLastUpload = Date.now() - lastUploadTime;
|
|
212
|
-
if (timeSinceLastUpload <
|
|
355
|
+
if (timeSinceLastUpload < MIN_UPLOAD_INTERVAL) {
|
|
213
356
|
logger.debug(`Skipping upload, only ${timeSinceLastUpload}ms since last upload`);
|
|
214
357
|
return;
|
|
215
358
|
}
|
|
216
359
|
}
|
|
360
|
+
// Prevent multiple concurrent uploads
|
|
217
361
|
if (uploadPreKeysPromise) {
|
|
218
362
|
logger.debug('Pre-key upload already in progress, waiting for completion');
|
|
219
363
|
await uploadPreKeysPromise;
|
|
220
364
|
}
|
|
221
365
|
const uploadLogic = async () => {
|
|
222
366
|
logger.info({ count, retryCount }, 'uploading pre-keys');
|
|
367
|
+
// Generate and save pre-keys atomically (prevents ID collisions on retry)
|
|
223
368
|
const node = await keys.transaction(async () => {
|
|
224
|
-
|
|
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
|
|
225
372
|
ev.emit('creds.update', update);
|
|
226
|
-
return node;
|
|
227
|
-
});
|
|
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)
|
|
228
376
|
try {
|
|
229
377
|
await query(node);
|
|
230
|
-
logger.info({ count }, 'uploaded pre-keys');
|
|
378
|
+
logger.info({ count }, 'uploaded pre-keys successfully');
|
|
231
379
|
lastUploadTime = Date.now();
|
|
232
|
-
}
|
|
380
|
+
}
|
|
381
|
+
catch (uploadError) {
|
|
233
382
|
logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys to server');
|
|
383
|
+
// Exponential backoff retry (max 3 retries)
|
|
234
384
|
if (retryCount < 3) {
|
|
235
385
|
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000);
|
|
236
386
|
logger.info(`Retrying pre-key upload in ${backoffDelay}ms`);
|
|
@@ -240,33 +390,61 @@ const makeSocket = (config) => {
|
|
|
240
390
|
throw uploadError;
|
|
241
391
|
}
|
|
242
392
|
};
|
|
393
|
+
// Add timeout protection
|
|
243
394
|
uploadPreKeysPromise = Promise.race([
|
|
244
395
|
uploadLogic(),
|
|
245
|
-
new Promise((_, reject) =>
|
|
246
|
-
setTimeout(() => reject(new boom_1.Boom('Pre-key upload timeout', { statusCode: 408 })), Defaults_1.UPLOAD_TIMEOUT)
|
|
247
|
-
)
|
|
396
|
+
new Promise((_, reject) => setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT))
|
|
248
397
|
]);
|
|
249
398
|
try {
|
|
250
399
|
await uploadPreKeysPromise;
|
|
251
|
-
}
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
252
402
|
uploadPreKeysPromise = null;
|
|
253
403
|
}
|
|
254
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
|
+
};
|
|
255
414
|
const uploadPreKeysToServerIfRequired = async () => {
|
|
256
415
|
try {
|
|
416
|
+
let count = 0;
|
|
257
417
|
const preKeyCount = await getAvailablePreKeysOnServer();
|
|
258
|
-
|
|
418
|
+
if (preKeyCount === 0)
|
|
419
|
+
count = INITIAL_PREKEY_COUNT;
|
|
420
|
+
else
|
|
421
|
+
count = MIN_PREKEY_COUNT;
|
|
422
|
+
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists();
|
|
259
423
|
logger.info(`${preKeyCount} pre-keys found on server`);
|
|
260
|
-
|
|
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(', ')}`);
|
|
261
435
|
await uploadPreKeys(count);
|
|
262
436
|
}
|
|
263
|
-
|
|
437
|
+
else {
|
|
438
|
+
logger.info(`PreKey validation passed - Server: ${preKeyCount}, Current prekey ${currentPreKeyId} exists`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
264
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
|
|
265
444
|
}
|
|
266
445
|
};
|
|
267
|
-
const onMessageReceived = (data) => {
|
|
268
|
-
noise.decodeFrame(data, frame => {
|
|
269
|
-
var _a;
|
|
446
|
+
const onMessageReceived = async (data) => {
|
|
447
|
+
await noise.decodeFrame(data, frame => {
|
|
270
448
|
// reset ping timeout
|
|
271
449
|
lastDateRecv = new Date();
|
|
272
450
|
let anyTriggered = false;
|
|
@@ -275,21 +453,21 @@ const makeSocket = (config) => {
|
|
|
275
453
|
if (!(frame instanceof Uint8Array)) {
|
|
276
454
|
const msgId = frame.attrs.id;
|
|
277
455
|
if (logger.level === 'trace') {
|
|
278
|
-
logger.trace({ xml:
|
|
456
|
+
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
279
457
|
}
|
|
280
458
|
/* Check if this is a response to a message we sent */
|
|
281
|
-
anyTriggered = ws.emit(`${
|
|
459
|
+
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
282
460
|
/* Check if this is a response to a message we are expecting */
|
|
283
461
|
const l0 = frame.tag;
|
|
284
462
|
const l1 = frame.attrs || {};
|
|
285
|
-
const l2 = Array.isArray(frame.content) ?
|
|
463
|
+
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '';
|
|
286
464
|
for (const key of Object.keys(l1)) {
|
|
287
|
-
anyTriggered = ws.emit(`${
|
|
288
|
-
anyTriggered = ws.emit(`${
|
|
289
|
-
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;
|
|
290
468
|
}
|
|
291
|
-
anyTriggered = ws.emit(`${
|
|
292
|
-
anyTriggered = ws.emit(`${
|
|
469
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;
|
|
470
|
+
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;
|
|
293
471
|
if (!anyTriggered && logger.level === 'debug') {
|
|
294
472
|
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
|
|
295
473
|
}
|
|
@@ -298,39 +476,28 @@ const makeSocket = (config) => {
|
|
|
298
476
|
};
|
|
299
477
|
const end = async (error) => {
|
|
300
478
|
if (closed) {
|
|
301
|
-
logger.trace({ trace: error
|
|
479
|
+
logger.trace({ trace: error?.stack }, 'connection already closed');
|
|
302
480
|
return;
|
|
303
481
|
}
|
|
304
482
|
closed = true;
|
|
305
|
-
logger.info({ trace: error
|
|
483
|
+
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
306
484
|
clearInterval(keepAliveReq);
|
|
307
485
|
clearTimeout(qrTimer);
|
|
308
486
|
ws.removeAllListeners('close');
|
|
309
|
-
ws.removeAllListeners('error');
|
|
310
487
|
ws.removeAllListeners('open');
|
|
311
488
|
ws.removeAllListeners('message');
|
|
312
489
|
if (!ws.isClosed && !ws.isClosing) {
|
|
313
490
|
try {
|
|
314
491
|
await ws.close();
|
|
315
492
|
}
|
|
316
|
-
catch
|
|
317
|
-
}
|
|
318
|
-
// Determine what the consumer should do next based on the disconnect reason
|
|
319
|
-
const statusCode = error?.output?.statusCode;
|
|
320
|
-
// loggedOut (401) and badSession (500) require deleting the session folder and re-scanning QR
|
|
321
|
-
const shouldDeleteSession = statusCode === Types_1.DisconnectReason.loggedOut ||
|
|
322
|
-
statusCode === Types_1.DisconnectReason.badSession;
|
|
323
|
-
// For all other closures (connectionClosed, connectionLost, restartRequired, etc.) just reconnect
|
|
324
|
-
const shouldReconnect = !shouldDeleteSession &&
|
|
325
|
-
statusCode !== Types_1.DisconnectReason.timedOut;
|
|
493
|
+
catch { }
|
|
494
|
+
}
|
|
326
495
|
ev.emit('connection.update', {
|
|
327
496
|
connection: 'close',
|
|
328
497
|
lastDisconnect: {
|
|
329
498
|
error,
|
|
330
499
|
date: new Date()
|
|
331
|
-
}
|
|
332
|
-
shouldDeleteSession,
|
|
333
|
-
shouldReconnect
|
|
500
|
+
}
|
|
334
501
|
});
|
|
335
502
|
ev.removeAllListeners('connection.update');
|
|
336
503
|
};
|
|
@@ -339,7 +506,7 @@ const makeSocket = (config) => {
|
|
|
339
506
|
return;
|
|
340
507
|
}
|
|
341
508
|
if (ws.isClosed || ws.isClosing) {
|
|
342
|
-
throw new
|
|
509
|
+
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed });
|
|
343
510
|
}
|
|
344
511
|
let onOpen;
|
|
345
512
|
let onClose;
|
|
@@ -349,8 +516,7 @@ const makeSocket = (config) => {
|
|
|
349
516
|
ws.on('open', onOpen);
|
|
350
517
|
ws.on('close', onClose);
|
|
351
518
|
ws.on('error', onClose);
|
|
352
|
-
})
|
|
353
|
-
.finally(() => {
|
|
519
|
+
}).finally(() => {
|
|
354
520
|
ws.off('open', onOpen);
|
|
355
521
|
ws.off('close', onClose);
|
|
356
522
|
ws.off('error', onClose);
|
|
@@ -365,8 +531,8 @@ const makeSocket = (config) => {
|
|
|
365
531
|
check if it's been a suspicious amount of time since the server responded with our last seen
|
|
366
532
|
it could be that the network is down
|
|
367
533
|
*/
|
|
368
|
-
if (diff > keepAliveIntervalMs +
|
|
369
|
-
void end(new
|
|
534
|
+
if (diff > keepAliveIntervalMs + 5000) {
|
|
535
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }));
|
|
370
536
|
}
|
|
371
537
|
else if (ws.isOpen) {
|
|
372
538
|
// if its all good, send a keep alive request
|
|
@@ -374,13 +540,12 @@ const makeSocket = (config) => {
|
|
|
374
540
|
tag: 'iq',
|
|
375
541
|
attrs: {
|
|
376
542
|
id: generateMessageTag(),
|
|
377
|
-
to:
|
|
543
|
+
to: S_WHATSAPP_NET,
|
|
378
544
|
type: 'get',
|
|
379
|
-
xmlns: 'w:p'
|
|
545
|
+
xmlns: 'w:p'
|
|
380
546
|
},
|
|
381
547
|
content: [{ tag: 'ping', attrs: {} }]
|
|
382
|
-
})
|
|
383
|
-
.catch(err => {
|
|
548
|
+
}).catch(err => {
|
|
384
549
|
logger.error({ trace: err.stack }, 'error in sending keep alive');
|
|
385
550
|
});
|
|
386
551
|
}
|
|
@@ -389,26 +554,23 @@ const makeSocket = (config) => {
|
|
|
389
554
|
}
|
|
390
555
|
}, keepAliveIntervalMs));
|
|
391
556
|
/** i have no idea why this exists. pls enlighten me */
|
|
392
|
-
const sendPassiveIq = (tag) =>
|
|
557
|
+
const sendPassiveIq = (tag) => query({
|
|
393
558
|
tag: 'iq',
|
|
394
559
|
attrs: {
|
|
395
|
-
to:
|
|
560
|
+
to: S_WHATSAPP_NET,
|
|
396
561
|
xmlns: 'passive',
|
|
397
|
-
type: 'set'
|
|
562
|
+
type: 'set'
|
|
398
563
|
},
|
|
399
|
-
content: [
|
|
400
|
-
|
|
401
|
-
]
|
|
402
|
-
}));
|
|
564
|
+
content: [{ tag, attrs: {} }]
|
|
565
|
+
});
|
|
403
566
|
/** logout & invalidate connection */
|
|
404
567
|
const logout = async (msg) => {
|
|
405
|
-
|
|
406
|
-
const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;
|
|
568
|
+
const jid = authState.creds.me?.id;
|
|
407
569
|
if (jid) {
|
|
408
570
|
await sendNode({
|
|
409
571
|
tag: 'iq',
|
|
410
572
|
attrs: {
|
|
411
|
-
to:
|
|
573
|
+
to: S_WHATSAPP_NET,
|
|
412
574
|
type: 'set',
|
|
413
575
|
id: generateMessageTag(),
|
|
414
576
|
xmlns: 'md'
|
|
@@ -424,27 +586,23 @@ const makeSocket = (config) => {
|
|
|
424
586
|
]
|
|
425
587
|
});
|
|
426
588
|
}
|
|
427
|
-
end(new
|
|
589
|
+
void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }));
|
|
428
590
|
};
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
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;
|
|
437
597
|
authState.creds.me = {
|
|
438
|
-
id:
|
|
598
|
+
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
439
599
|
name: '~'
|
|
440
600
|
};
|
|
441
|
-
|
|
442
601
|
ev.emit('creds.update', authState.creds);
|
|
443
|
-
|
|
444
602
|
await sendNode({
|
|
445
603
|
tag: 'iq',
|
|
446
604
|
attrs: {
|
|
447
|
-
to:
|
|
605
|
+
to: S_WHATSAPP_NET,
|
|
448
606
|
type: 'set',
|
|
449
607
|
id: generateMessageTag(),
|
|
450
608
|
xmlns: 'md'
|
|
@@ -471,7 +629,7 @@ const makeSocket = (config) => {
|
|
|
471
629
|
{
|
|
472
630
|
tag: 'companion_platform_id',
|
|
473
631
|
attrs: {},
|
|
474
|
-
content:
|
|
632
|
+
content: getPlatformId(browser[1])
|
|
475
633
|
},
|
|
476
634
|
{
|
|
477
635
|
tag: 'companion_platform_display',
|
|
@@ -481,68 +639,38 @@ const makeSocket = (config) => {
|
|
|
481
639
|
{
|
|
482
640
|
tag: 'link_code_pairing_nonce',
|
|
483
641
|
attrs: {},
|
|
484
|
-
content:
|
|
642
|
+
content: '0'
|
|
485
643
|
}
|
|
486
644
|
]
|
|
487
645
|
}
|
|
488
646
|
]
|
|
489
647
|
});
|
|
490
|
-
|
|
491
648
|
return authState.creds.pairingCode;
|
|
492
|
-
}
|
|
649
|
+
};
|
|
493
650
|
async function generatePairingKey() {
|
|
494
|
-
const salt =
|
|
495
|
-
const randomIv =
|
|
496
|
-
const key = await
|
|
497
|
-
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);
|
|
498
655
|
return Buffer.concat([salt, randomIv, ciphered]);
|
|
499
656
|
}
|
|
500
657
|
const sendWAMBuffer = (wamBuffer) => {
|
|
501
658
|
return query({
|
|
502
659
|
tag: 'iq',
|
|
503
660
|
attrs: {
|
|
504
|
-
to:
|
|
661
|
+
to: S_WHATSAPP_NET,
|
|
505
662
|
id: generateMessageTag(),
|
|
506
663
|
xmlns: 'w:stats'
|
|
507
664
|
},
|
|
508
665
|
content: [
|
|
509
666
|
{
|
|
510
667
|
tag: 'add',
|
|
511
|
-
attrs: {},
|
|
668
|
+
attrs: { t: Math.round(Date.now() / 1000) + '' },
|
|
512
669
|
content: wamBuffer
|
|
513
670
|
}
|
|
514
671
|
]
|
|
515
672
|
});
|
|
516
673
|
};
|
|
517
|
-
let serverTimeOffsetMs = 0;
|
|
518
|
-
const updateServerTimeOffset = ({ attrs }) => {
|
|
519
|
-
const tValue = attrs && attrs.t;
|
|
520
|
-
if (!tValue) return;
|
|
521
|
-
const parsed = Number(tValue);
|
|
522
|
-
if (Number.isNaN(parsed) || parsed <= 0) return;
|
|
523
|
-
const localMs = Date.now();
|
|
524
|
-
serverTimeOffsetMs = parsed * 1000 - localMs;
|
|
525
|
-
logger.debug({ offset: serverTimeOffsetMs }, 'calculated server time offset');
|
|
526
|
-
};
|
|
527
|
-
const getUnifiedSessionId = () => {
|
|
528
|
-
const offsetMs = 3 * Defaults_1.TimeMs.Day;
|
|
529
|
-
const now = Date.now() + serverTimeOffsetMs;
|
|
530
|
-
const id = (now + offsetMs) % Defaults_1.TimeMs.Week;
|
|
531
|
-
return id.toString();
|
|
532
|
-
};
|
|
533
|
-
const sendUnifiedSession = async () => {
|
|
534
|
-
if (!ws.isOpen) return;
|
|
535
|
-
const node = {
|
|
536
|
-
tag: 'ib',
|
|
537
|
-
attrs: {},
|
|
538
|
-
content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
|
|
539
|
-
};
|
|
540
|
-
try {
|
|
541
|
-
await sendNode(node);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
logger.debug({ error }, 'failed to send unified_session telemetry');
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
674
|
ws.on('message', onMessageReceived);
|
|
547
675
|
ws.on('open', async () => {
|
|
548
676
|
try {
|
|
@@ -550,25 +678,26 @@ const makeSocket = (config) => {
|
|
|
550
678
|
}
|
|
551
679
|
catch (err) {
|
|
552
680
|
logger.error({ err }, 'error in validating connection');
|
|
553
|
-
end(err);
|
|
681
|
+
void end(err);
|
|
554
682
|
}
|
|
555
683
|
});
|
|
556
684
|
ws.on('error', mapWebSocketError(end));
|
|
557
|
-
ws.on('close', () => void end(new
|
|
558
|
-
|
|
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 })));
|
|
559
688
|
// QR gen
|
|
560
689
|
ws.on('CB:iq,type:set,pair-device', async (stanza) => {
|
|
561
690
|
const iq = {
|
|
562
691
|
tag: 'iq',
|
|
563
692
|
attrs: {
|
|
564
|
-
to:
|
|
693
|
+
to: S_WHATSAPP_NET,
|
|
565
694
|
type: 'result',
|
|
566
|
-
id: stanza.attrs.id
|
|
695
|
+
id: stanza.attrs.id
|
|
567
696
|
}
|
|
568
697
|
};
|
|
569
698
|
await sendNode(iq);
|
|
570
|
-
const pairDeviceNode =
|
|
571
|
-
const refNodes =
|
|
699
|
+
const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
|
|
700
|
+
const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
|
|
572
701
|
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
|
|
573
702
|
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
|
|
574
703
|
const advB64 = creds.advSecretKey;
|
|
@@ -579,7 +708,7 @@ const makeSocket = (config) => {
|
|
|
579
708
|
}
|
|
580
709
|
const refNode = refNodes.shift();
|
|
581
710
|
if (!refNode) {
|
|
582
|
-
void end(new
|
|
711
|
+
void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut }));
|
|
583
712
|
return;
|
|
584
713
|
}
|
|
585
714
|
const ref = refNode.content.toString('utf-8');
|
|
@@ -596,7 +725,7 @@ const makeSocket = (config) => {
|
|
|
596
725
|
logger.debug('pair success recv');
|
|
597
726
|
try {
|
|
598
727
|
updateServerTimeOffset(stanza);
|
|
599
|
-
const { reply, creds: updatedCreds } =
|
|
728
|
+
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds);
|
|
600
729
|
logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');
|
|
601
730
|
ev.emit('creds.update', updatedCreds);
|
|
602
731
|
ev.emit('connection.update', { isNewLogin: true, qr: undefined });
|
|
@@ -608,84 +737,97 @@ const makeSocket = (config) => {
|
|
|
608
737
|
void end(error);
|
|
609
738
|
}
|
|
610
739
|
});
|
|
740
|
+
// login complete
|
|
611
741
|
ws.on('CB:success', async (node) => {
|
|
612
742
|
try {
|
|
613
743
|
updateServerTimeOffset(node);
|
|
614
744
|
await uploadPreKeysToServerIfRequired();
|
|
615
745
|
await sendPassiveIq('active');
|
|
616
|
-
|
|
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) {
|
|
617
755
|
logger.warn({ err }, 'failed to send initial passive iq');
|
|
618
756
|
}
|
|
619
757
|
logger.info('opened connection to WA');
|
|
620
|
-
clearTimeout(qrTimer);
|
|
758
|
+
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
621
759
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
622
760
|
ev.emit('connection.update', { connection: 'open' });
|
|
623
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
|
+
}
|
|
624
785
|
});
|
|
625
786
|
ws.on('CB:stream:error', (node) => {
|
|
626
|
-
const [reasonNode] =
|
|
787
|
+
const [reasonNode] = getAllBinaryNodeChildren(node);
|
|
627
788
|
logger.error({ reasonNode, fullErrorNode: node }, 'stream errored out');
|
|
628
|
-
const { reason, statusCode } =
|
|
629
|
-
void end(new
|
|
789
|
+
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
790
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }));
|
|
630
791
|
});
|
|
792
|
+
// stream fail, possible logout
|
|
631
793
|
ws.on('CB:failure', (node) => {
|
|
632
794
|
const reason = +(node.attrs.reason || 500);
|
|
633
|
-
|
|
634
|
-
const description = (Array.isArray(node.content) && node.content[0]?.attrs?.description)
|
|
635
|
-
|| node.attrs.reason
|
|
636
|
-
|| 'unknown';
|
|
637
|
-
logger.warn({ reason, description, attrs: node.attrs }, 'WA connection failure received');
|
|
638
|
-
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 }));
|
|
639
796
|
});
|
|
640
797
|
ws.on('CB:ib,,downgrade_webclient', () => {
|
|
641
|
-
void end(new
|
|
798
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }));
|
|
642
799
|
});
|
|
643
|
-
ws.on('CB:ib,,offline_preview', (node) => {
|
|
800
|
+
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
644
801
|
logger.info('offline preview received', JSON.stringify(node));
|
|
645
|
-
sendNode({
|
|
802
|
+
await sendNode({
|
|
646
803
|
tag: 'ib',
|
|
647
804
|
attrs: {},
|
|
648
805
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
649
806
|
});
|
|
650
807
|
});
|
|
651
808
|
ws.on('CB:ib,,edge_routing', (node) => {
|
|
652
|
-
const edgeRoutingNode =
|
|
653
|
-
const routingInfo =
|
|
654
|
-
if (routingInfo
|
|
655
|
-
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);
|
|
656
813
|
ev.emit('creds.update', authState.creds);
|
|
657
814
|
}
|
|
658
815
|
});
|
|
659
816
|
let didStartBuffer = false;
|
|
660
|
-
let offlineFlushTimer = null;
|
|
661
817
|
process.nextTick(() => {
|
|
662
|
-
|
|
663
|
-
if ((_a = creds.me) === null || _a === void 0 ? void 0 : _a.id) {
|
|
818
|
+
if (creds.me?.id) {
|
|
664
819
|
// start buffering important events
|
|
665
820
|
// if we're logged in
|
|
666
821
|
ev.buffer();
|
|
667
822
|
didStartBuffer = true;
|
|
668
|
-
// Safety net: if server never sends CB:ib,,offline (e.g. slow server, no pending msgs),
|
|
669
|
-
// force-flush the buffer after 10s so messages/commands are never stuck
|
|
670
|
-
offlineFlushTimer = setTimeout(() => {
|
|
671
|
-
if (didStartBuffer && ev.isBuffering()) {
|
|
672
|
-
logger.warn('offline event not received within 10s — force-flushing event buffer to prevent message lag');
|
|
673
|
-
ev.flush(true);
|
|
674
|
-
ev.emit('connection.update', { receivedPendingNotifications: true });
|
|
675
|
-
}
|
|
676
|
-
}, 10000);
|
|
677
823
|
}
|
|
678
824
|
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });
|
|
679
825
|
});
|
|
680
826
|
// called when all offline notifs are handled
|
|
681
827
|
ws.on('CB:ib,,offline', (node) => {
|
|
682
|
-
const child =
|
|
683
|
-
const offlineNotifs = +(
|
|
828
|
+
const child = getBinaryNodeChild(node, 'offline');
|
|
829
|
+
const offlineNotifs = +(child?.attrs.count || 0);
|
|
684
830
|
logger.info(`handled ${offlineNotifs} offline messages/notifications`);
|
|
685
|
-
if (offlineFlushTimer) {
|
|
686
|
-
clearTimeout(offlineFlushTimer);
|
|
687
|
-
offlineFlushTimer = null;
|
|
688
|
-
}
|
|
689
831
|
if (didStartBuffer) {
|
|
690
832
|
ev.flush();
|
|
691
833
|
logger.trace('flushed events for initial buffer');
|
|
@@ -694,39 +836,66 @@ const makeSocket = (config) => {
|
|
|
694
836
|
});
|
|
695
837
|
// update credentials when required
|
|
696
838
|
ev.on('creds.update', update => {
|
|
697
|
-
|
|
698
|
-
const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;
|
|
839
|
+
const name = update.me?.name;
|
|
699
840
|
// if name has just been received
|
|
700
|
-
if (
|
|
841
|
+
if (creds.me?.name !== name) {
|
|
701
842
|
logger.debug({ name }, 'updated pushName');
|
|
702
843
|
sendNode({
|
|
703
844
|
tag: 'presence',
|
|
704
845
|
attrs: { name: name }
|
|
705
|
-
})
|
|
706
|
-
.catch(err => {
|
|
846
|
+
}).catch(err => {
|
|
707
847
|
logger.warn({ trace: err.stack }, 'error in sending presence update on name change');
|
|
708
848
|
});
|
|
709
849
|
}
|
|
710
850
|
Object.assign(creds, update);
|
|
711
851
|
});
|
|
712
|
-
|
|
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
|
+
};
|
|
713
887
|
try {
|
|
714
|
-
await
|
|
715
|
-
} catch (error) {
|
|
716
|
-
logger.warn({ lid, pn, error }, 'Failed to store LID-PN mapping');
|
|
888
|
+
await sendNode(node);
|
|
717
889
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
logger.debug({ error }, 'failed to send unified_session telemetry');
|
|
892
|
+
}
|
|
893
|
+
};
|
|
722
894
|
return {
|
|
723
895
|
type: 'md',
|
|
724
896
|
ws,
|
|
725
897
|
ev,
|
|
726
|
-
authState: {
|
|
727
|
-
creds,
|
|
728
|
-
keys
|
|
729
|
-
},
|
|
898
|
+
authState: { creds, keys },
|
|
730
899
|
signalRepository,
|
|
731
900
|
get user() {
|
|
732
901
|
return authState.creds.me;
|
|
@@ -742,20 +911,25 @@ const makeSocket = (config) => {
|
|
|
742
911
|
onUnexpectedError,
|
|
743
912
|
uploadPreKeys,
|
|
744
913
|
uploadPreKeysToServerIfRequired,
|
|
914
|
+
digestKeyBundle,
|
|
915
|
+
rotateSignedPreKey,
|
|
745
916
|
requestPairingCode,
|
|
746
917
|
updateServerTimeOffset,
|
|
747
918
|
sendUnifiedSession,
|
|
748
|
-
|
|
919
|
+
wamBuffer: publicWAMBuffer,
|
|
920
|
+
/** Waits for the connection to WA to reach a state */
|
|
921
|
+
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
|
749
922
|
sendWAMBuffer,
|
|
923
|
+
executeUSyncQuery,
|
|
924
|
+
onWhatsApp
|
|
750
925
|
};
|
|
751
926
|
};
|
|
752
|
-
exports.makeSocket = makeSocket;
|
|
753
927
|
/**
|
|
754
928
|
* map the websocket error to the right type
|
|
755
929
|
* so it can be retried by the caller
|
|
756
930
|
* */
|
|
757
931
|
function mapWebSocketError(handler) {
|
|
758
932
|
return (error) => {
|
|
759
|
-
handler(new
|
|
933
|
+
handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }));
|
|
760
934
|
};
|
|
761
|
-
}
|
|
935
|
+
}
|