@nexustechpro/baileys 2.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/WAProto/index.js +22 -18
- package/lib/Defaults/baileys-version.json +1 -1
- package/lib/Defaults/index.js +9 -8
- package/lib/Signal/libsignal.js +551 -342
- package/lib/Socket/chats.js +83 -65
- package/lib/Socket/index.js +2 -3
- package/lib/Socket/messages-recv.js +227 -41
- package/lib/Socket/messages-send.js +97 -117
- package/lib/Socket/newsletter.js +87 -36
- package/lib/Socket/nexus-handler.js +326 -89
- package/lib/Socket/registration.js +50 -33
- package/lib/Socket/socket.js +245 -69
- package/lib/Store/make-in-memory-store.js +29 -20
- package/lib/Types/Newsletter.js +37 -29
- package/lib/Types/State.js +43 -0
- package/lib/Utils/auth-utils.js +2 -2
- package/lib/Utils/chat-utils.js +48 -16
- package/lib/Utils/companion-reg-client-utils.js +34 -0
- package/lib/Utils/decode-wa-message.js +39 -22
- package/lib/Utils/generics.js +5 -7
- package/lib/Utils/index.js +4 -0
- package/lib/Utils/key-store.js +1 -1
- package/lib/Utils/link-preview.js +134 -61
- package/lib/Utils/messages-media.js +496 -381
- package/lib/Utils/messages.js +699 -706
- package/lib/Utils/process-message.js +53 -35
- package/lib/Utils/reporting-utils.js +155 -0
- package/lib/Utils/signal.js +134 -104
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/Utils/use-multi-file-auth-state.js +174 -91
- package/lib/WABinary/constants.js +6 -0
- package/lib/WABinary/index.js +1 -0
- package/lib/index.js +2 -3
- package/package.json +13 -9
package/lib/Socket/socket.js
CHANGED
|
@@ -1,16 +1,60 @@
|
|
|
1
|
-
import { Boom } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import { URL } from
|
|
4
|
-
import { promisify } from
|
|
5
|
-
import { proto } from
|
|
6
|
-
import {
|
|
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 {
|
|
7
|
+
DEF_CALLBACK_PREFIX,
|
|
8
|
+
DEF_TAG_PREFIX,
|
|
9
|
+
INITIAL_PREKEY_COUNT,
|
|
10
|
+
MIN_PREKEY_COUNT,
|
|
11
|
+
MIN_UPLOAD_INTERVAL,
|
|
12
|
+
NOISE_WA_HEADER,
|
|
13
|
+
UPLOAD_TIMEOUT,
|
|
14
|
+
BATCH_SIZE,
|
|
15
|
+
TimeMs
|
|
16
|
+
} from '../Defaults/index.js'
|
|
17
|
+
import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js'
|
|
18
|
+
import { DisconnectReason, XWAPaths } from '../Types/index.js'
|
|
19
|
+
import {
|
|
20
|
+
addTransactionCapability,
|
|
21
|
+
aesEncryptCTR,
|
|
22
|
+
bindWaitForConnectionUpdate,
|
|
23
|
+
buildPairingQRData,
|
|
24
|
+
bytesToCrockford,
|
|
25
|
+
configureSuccessfulPairing,
|
|
26
|
+
Curve,
|
|
27
|
+
derivePairingCodeKey,
|
|
28
|
+
generateLoginNode,
|
|
29
|
+
generateMdTagPrefix,
|
|
30
|
+
generateRegistrationNode,
|
|
31
|
+
getCodeFromWSError,
|
|
32
|
+
getCompanionPlatformId,
|
|
33
|
+
getErrorCodeFromStreamError,
|
|
34
|
+
getNextPreKeysNode,
|
|
35
|
+
makeEventBuffer,
|
|
36
|
+
makeNoiseHandler,
|
|
37
|
+
promiseTimeout,
|
|
38
|
+
signedKeyPair,
|
|
39
|
+
xmppSignedPreKey
|
|
40
|
+
} from '../Utils/index.js'
|
|
41
|
+
import { getPlatformId, migrateIndexKey } from '../Utils/index.js'
|
|
42
|
+
import {
|
|
43
|
+
assertNodeErrorFree,
|
|
44
|
+
binaryNodeToString,
|
|
45
|
+
encodeBinaryNode,
|
|
46
|
+
getAllBinaryNodeChildren,
|
|
47
|
+
getBinaryNodeChild,
|
|
48
|
+
getBinaryNodeChildren,
|
|
49
|
+
isLidUser,
|
|
50
|
+
jidDecode,
|
|
51
|
+
jidEncode,
|
|
52
|
+
S_WHATSAPP_NET
|
|
53
|
+
} from '../WABinary/index.js'
|
|
54
|
+
import { BinaryInfo } from '../WAM/BinaryInfo.js'
|
|
55
|
+
import { USyncQuery, USyncUser } from '../WAUSync/index.js'
|
|
56
|
+
import { WebSocketClient } from './Client/index.js'
|
|
57
|
+
import { executeWMexQuery } from './mex.js'
|
|
14
58
|
|
|
15
59
|
// ─── Module-scope helpers ──────────────────────────────────────────────────────
|
|
16
60
|
|
|
@@ -43,7 +87,7 @@ export const makeSocket = (config) => {
|
|
|
43
87
|
makeSignalRepository
|
|
44
88
|
} = config
|
|
45
89
|
|
|
46
|
-
|
|
90
|
+
|
|
47
91
|
|
|
48
92
|
const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
|
|
49
93
|
|
|
@@ -65,6 +109,14 @@ export const makeSocket = (config) => {
|
|
|
65
109
|
logger.info({ url: url.toString() }, 'Initiating WebSocket connection')
|
|
66
110
|
ws.connect()
|
|
67
111
|
|
|
112
|
+
if (printQRInTerminal) {
|
|
113
|
+
import('qrcode-terminal').then(({ default: qrcode }) => {
|
|
114
|
+
ev.on('connection.update', ({ qr }) => {
|
|
115
|
+
if (qr) qrcode.generate(qr, { small: true })
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
68
120
|
const ev = makeEventBuffer(logger)
|
|
69
121
|
const { creds } = authState
|
|
70
122
|
const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
|
|
@@ -73,6 +125,7 @@ export const makeSocket = (config) => {
|
|
|
73
125
|
const uqTagId = generateMdTagPrefix()
|
|
74
126
|
const sendPromise = promisify(ws.send)
|
|
75
127
|
|
|
128
|
+
// State
|
|
76
129
|
let epoch = 1
|
|
77
130
|
let lastDateRecv
|
|
78
131
|
let lastUploadTime = 0
|
|
@@ -81,6 +134,10 @@ export const makeSocket = (config) => {
|
|
|
81
134
|
let keepAliveReq
|
|
82
135
|
let qrTimer
|
|
83
136
|
let serverTimeOffsetMs = 0
|
|
137
|
+
let didStartBuffer = false
|
|
138
|
+
|
|
139
|
+
/** Socket end handlers — registered via registerSocketEndHandler() */
|
|
140
|
+
const socketEndHandlers = []
|
|
84
141
|
|
|
85
142
|
const generateMessageTag = () => `${uqTagId}${epoch++}`
|
|
86
143
|
|
|
@@ -153,7 +210,13 @@ export const makeSocket = (config) => {
|
|
|
153
210
|
attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' },
|
|
154
211
|
content: [{
|
|
155
212
|
tag: 'usync',
|
|
156
|
-
attrs: {
|
|
213
|
+
attrs: {
|
|
214
|
+
context: usyncQuery.context,
|
|
215
|
+
mode: usyncQuery.mode,
|
|
216
|
+
sid: generateMessageTag(),
|
|
217
|
+
last: 'true',
|
|
218
|
+
index: '0'
|
|
219
|
+
},
|
|
157
220
|
content: [
|
|
158
221
|
{ tag: 'query', attrs: {}, content: usyncQuery.protocols.map((a) => a.getQueryElement()) },
|
|
159
222
|
{ tag: 'list', attrs: {}, content: userNodes }
|
|
@@ -166,14 +229,39 @@ export const makeSocket = (config) => {
|
|
|
166
229
|
async function pnFromLIDUSync(jids) {
|
|
167
230
|
const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background')
|
|
168
231
|
for (const jid of jids) {
|
|
169
|
-
if (
|
|
170
|
-
|
|
232
|
+
if (isLidUser(jid)) { logger?.warn('LID user found in LID fetch call'); continue }
|
|
233
|
+
usyncQuery.withUser(new USyncUser().withId(jid))
|
|
171
234
|
}
|
|
172
235
|
if (usyncQuery.users.length === 0) return []
|
|
173
236
|
const results = await executeUSyncQuery(usyncQuery)
|
|
174
237
|
return results ? results.list.filter((a) => !!a.lid).map(({ lid, id }) => ({ pn: id, lid })) : []
|
|
175
238
|
}
|
|
176
239
|
|
|
240
|
+
const onWhatsApp = async (...phoneNumbers) => {
|
|
241
|
+
let usyncQuery = new USyncQuery()
|
|
242
|
+
let contactEnabled = false
|
|
243
|
+
for (let jid of phoneNumbers) {
|
|
244
|
+
if (isLidUser(jid)) {
|
|
245
|
+
const pn = await signalRepository.lidMapping.getPNForLID(jid)
|
|
246
|
+
if (pn) {
|
|
247
|
+
jid = pn
|
|
248
|
+
} else {
|
|
249
|
+
if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
|
|
250
|
+
usyncQuery.withUser(new USyncUser().withId(jid))
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
|
|
255
|
+
const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`
|
|
256
|
+
usyncQuery.withUser(new USyncUser().withPhone(phone))
|
|
257
|
+
}
|
|
258
|
+
if (usyncQuery.users.length === 0) return []
|
|
259
|
+
const results = await executeUSyncQuery(usyncQuery)
|
|
260
|
+
return results
|
|
261
|
+
? results.list.filter((a) => !!a.contact && a.id && a.id !== 'undefined').map(({ contact, id }) => ({ jid: id, exists: contact }))
|
|
262
|
+
: []
|
|
263
|
+
}
|
|
264
|
+
|
|
177
265
|
// ─── Pre-keys ───────────────────────────────────────────────────────────────
|
|
178
266
|
|
|
179
267
|
const getAvailablePreKeysOnServer = async () => {
|
|
@@ -185,11 +273,24 @@ export const makeSocket = (config) => {
|
|
|
185
273
|
return +getBinaryNodeChild(result, 'count').attrs.value
|
|
186
274
|
}
|
|
187
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Verify that our current pre-key actually exists in local key storage.
|
|
278
|
+
* Catches the case where the server still has keys but our local store is missing them.
|
|
279
|
+
*/
|
|
280
|
+
const verifyCurrentPreKeyExists = async () => {
|
|
281
|
+
const currentPreKeyId = creds.nextPreKeyId - 1
|
|
282
|
+
if (currentPreKeyId <= 0) return { exists: false, currentPreKeyId: 0 }
|
|
283
|
+
const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()])
|
|
284
|
+
return { exists: !!preKeys[currentPreKeyId.toString()], currentPreKeyId }
|
|
285
|
+
}
|
|
286
|
+
|
|
188
287
|
const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
|
|
288
|
+
// Rate-limit guard — only on the first attempt, not retries
|
|
189
289
|
if (retryCount === 0 && Date.now() - lastUploadTime < MIN_UPLOAD_INTERVAL) {
|
|
190
|
-
logger.debug(`Skipping upload
|
|
290
|
+
logger.debug(`Skipping upload — only ${Date.now() - lastUploadTime}ms since last upload`)
|
|
191
291
|
return
|
|
192
292
|
}
|
|
293
|
+
// Dedup: if an upload is already in-flight, wait for it
|
|
193
294
|
if (uploadPreKeysPromise) {
|
|
194
295
|
logger.debug('Pre-key upload in progress, waiting...')
|
|
195
296
|
await uploadPreKeysPromise
|
|
@@ -197,6 +298,7 @@ export const makeSocket = (config) => {
|
|
|
197
298
|
}
|
|
198
299
|
const uploadLogic = async () => {
|
|
199
300
|
logger.info({ count, retryCount }, 'Uploading pre-keys')
|
|
301
|
+
// Generate keys inside a transaction to prevent ID collisions on retry
|
|
200
302
|
const node = await keys.transaction(async () => {
|
|
201
303
|
const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
|
|
202
304
|
ev.emit('creds.update', update)
|
|
@@ -230,15 +332,23 @@ export const makeSocket = (config) => {
|
|
|
230
332
|
try {
|
|
231
333
|
const preKeyCount = await getAvailablePreKeysOnServer()
|
|
232
334
|
logger.info(`${preKeyCount} pre-keys found on server`)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
335
|
+
const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
|
|
336
|
+
logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
|
|
337
|
+
const lowServerCount = preKeyCount <= MIN_PREKEY_COUNT
|
|
338
|
+
const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
|
|
339
|
+
if (lowServerCount || missingCurrentPreKey) {
|
|
340
|
+
const reasons = []
|
|
341
|
+
if (lowServerCount) reasons.push(`server count low (${preKeyCount})`)
|
|
342
|
+
if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing from storage`)
|
|
343
|
+
logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`)
|
|
344
|
+
const uploadCount = preKeyCount === 0 ? INITIAL_PREKEY_COUNT : MIN_PREKEY_COUNT
|
|
236
345
|
await uploadPreKeys(uploadCount)
|
|
237
346
|
} else {
|
|
238
|
-
logger.info(`✅ PreKey validation passed
|
|
347
|
+
logger.info(`✅ PreKey validation passed — Server: ${preKeyCount}, prekey ${currentPreKeyId} exists`)
|
|
239
348
|
}
|
|
240
349
|
} catch (error) {
|
|
241
350
|
logger.error({ error }, 'Failed to check/upload pre-keys during init')
|
|
351
|
+
// Non-fatal — allow connection to continue
|
|
242
352
|
}
|
|
243
353
|
}
|
|
244
354
|
|
|
@@ -285,7 +395,7 @@ export const makeSocket = (config) => {
|
|
|
285
395
|
const parsed = Number(tValue)
|
|
286
396
|
if (Number.isNaN(parsed) || parsed <= 0) return
|
|
287
397
|
serverTimeOffsetMs = parsed * 1000 - Date.now()
|
|
288
|
-
logger.debug({ offset: serverTimeOffsetMs }, '
|
|
398
|
+
logger.debug({ offset: serverTimeOffsetMs }, 'Calculated server time offset')
|
|
289
399
|
}
|
|
290
400
|
|
|
291
401
|
// ─── Unified session telemetry ───────────────────────────────────────────────
|
|
@@ -305,14 +415,65 @@ export const makeSocket = (config) => {
|
|
|
305
415
|
content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
|
|
306
416
|
})
|
|
307
417
|
} catch (error) {
|
|
308
|
-
logger.debug({ error }, '
|
|
418
|
+
logger.debug({ error }, 'Failed to send unified_session telemetry')
|
|
309
419
|
}
|
|
310
420
|
}
|
|
311
421
|
|
|
422
|
+
// ─── WAM buffer ─────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
const sendWAMBuffer = (wamBuffer) =>
|
|
425
|
+
query({
|
|
426
|
+
tag: 'iq',
|
|
427
|
+
attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
|
|
428
|
+
content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
// ─── WMex queries ────────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Fetches account restriction / reachout timelock status.
|
|
435
|
+
*/
|
|
436
|
+
const fetchAccountReachoutTimelock = async () => {
|
|
437
|
+
const queryResult = await executeWMexQuery(
|
|
438
|
+
{},
|
|
439
|
+
QueryIds.REACHOUT_TIMELOCK,
|
|
440
|
+
XWAPaths.xwa2_fetch_account_reachout_timelock,
|
|
441
|
+
query,
|
|
442
|
+
generateMessageTag
|
|
443
|
+
)
|
|
444
|
+
const result = {
|
|
445
|
+
isActive: !!queryResult?.is_active,
|
|
446
|
+
timeEnforcementEnds:
|
|
447
|
+
queryResult?.time_enforcement_ends && queryResult.time_enforcement_ends !== '0'
|
|
448
|
+
? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
|
|
449
|
+
: undefined,
|
|
450
|
+
enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
|
|
451
|
+
}
|
|
452
|
+
ev.emit('connection.update', { reachoutTimeLock: result })
|
|
453
|
+
return result
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Fetches new-chat message cap quota and usage.
|
|
458
|
+
*/
|
|
459
|
+
const fetchNewChatMessageCap = async () =>
|
|
460
|
+
executeWMexQuery(
|
|
461
|
+
{ input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } },
|
|
462
|
+
QueryIds.MESSAGE_CAPPING_INFO,
|
|
463
|
+
XWAPaths.xwa2_message_capping_info,
|
|
464
|
+
query,
|
|
465
|
+
generateMessageTag
|
|
466
|
+
)
|
|
467
|
+
|
|
312
468
|
// ─── Connection lifecycle ────────────────────────────────────────────────────
|
|
313
469
|
|
|
314
470
|
const onUnexpectedError = (err, msg) => {
|
|
315
|
-
|
|
471
|
+
const isClosed = err?.message?.includes('Connection Closed') || err?.output?.statusCode === 428
|
|
472
|
+
if (isClosed) {
|
|
473
|
+
logger.debug({ msg: err?.message }, `Connection closed during '${msg}'`)
|
|
474
|
+
} else {
|
|
475
|
+
logger.error({ err }, `unexpected error in '${msg}'`)
|
|
476
|
+
}
|
|
316
477
|
}
|
|
317
478
|
|
|
318
479
|
const awaitNextMessage = async (sendMsg) => {
|
|
@@ -335,7 +496,7 @@ export const makeSocket = (config) => {
|
|
|
335
496
|
}
|
|
336
497
|
|
|
337
498
|
const validateConnection = async () => {
|
|
338
|
-
|
|
499
|
+
const helloMsg = proto.HandshakeMessage.fromObject({ clientHello: { ephemeral: ephemeralKeyPair.public } })
|
|
339
500
|
logger.info({ browser, helloMsg }, 'Connected to WhatsApp')
|
|
340
501
|
const init = proto.HandshakeMessage.encode(helloMsg).finish()
|
|
341
502
|
const result = await awaitNextMessage(init)
|
|
@@ -346,9 +507,7 @@ export const makeSocket = (config) => {
|
|
|
346
507
|
logger.info({ node }, !creds.me ? 'Attempting registration...' : 'Logging in...')
|
|
347
508
|
const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
|
|
348
509
|
await sendRawMessage(
|
|
349
|
-
proto.HandshakeMessage.encode({
|
|
350
|
-
clientFinish: { static: keyEnc, payload: payloadEnc }
|
|
351
|
-
}).finish()
|
|
510
|
+
proto.HandshakeMessage.encode({ clientFinish: { static: keyEnc, payload: payloadEnc } }).finish()
|
|
352
511
|
)
|
|
353
512
|
await noise.finishInit()
|
|
354
513
|
startKeepAliveRequest()
|
|
@@ -376,27 +535,32 @@ export const makeSocket = (config) => {
|
|
|
376
535
|
* Keep-alive: ping WA every keepAliveIntervalMs.
|
|
377
536
|
* If the server stops responding (diff > interval + 5s) the connection
|
|
378
537
|
* is considered lost and we call end() — the consumer handles reconnection.
|
|
379
|
-
* No internal reconnect loop — clean separation of concerns.
|
|
380
538
|
*/
|
|
381
539
|
const startKeepAliveRequest = () => {
|
|
382
540
|
keepAliveReq = setInterval(() => {
|
|
383
541
|
if (!lastDateRecv) lastDateRecv = new Date()
|
|
384
542
|
const diff = Date.now() - lastDateRecv.getTime()
|
|
385
543
|
if (diff > keepAliveIntervalMs + 5000) {
|
|
386
|
-
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
|
544
|
+
void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
|
387
545
|
} else if (ws.isOpen) {
|
|
388
546
|
query({
|
|
389
547
|
tag: 'iq',
|
|
390
548
|
attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:p' },
|
|
391
549
|
content: [{ tag: 'ping', attrs: {} }]
|
|
392
|
-
}).catch((err) => logger.error({ trace: err.stack }, '
|
|
550
|
+
}).catch((err) => logger.error({ trace: err.stack }, 'Error in sending keep alive'))
|
|
393
551
|
} else {
|
|
394
|
-
logger.warn('
|
|
552
|
+
logger.warn('Keep alive called when WS not open')
|
|
395
553
|
}
|
|
396
554
|
}, keepAliveIntervalMs)
|
|
397
555
|
}
|
|
398
556
|
|
|
399
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Tear down the socket.
|
|
559
|
+
* Awaits ws.close() so cleanup is deterministic before emitting connection.update.
|
|
560
|
+
* Runs all registered socketEndHandlers in order.
|
|
561
|
+
* Calls signalRepository.close() and ev.destroy() to prevent leaks.
|
|
562
|
+
*/
|
|
563
|
+
const end = async (error) => {
|
|
400
564
|
if (closed) { logger.trace({ trace: error?.stack }, 'Connection already closed'); return }
|
|
401
565
|
closed = true
|
|
402
566
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
|
|
@@ -405,9 +569,18 @@ export const makeSocket = (config) => {
|
|
|
405
569
|
ws.removeAllListeners('close')
|
|
406
570
|
ws.removeAllListeners('open')
|
|
407
571
|
ws.removeAllListeners('message')
|
|
408
|
-
|
|
572
|
+
signalRepository.close?.()
|
|
573
|
+
if (!ws.isClosed && !ws.isClosing) {
|
|
574
|
+
try { await ws.close() } catch { }
|
|
575
|
+
}
|
|
576
|
+
for (const handler of socketEndHandlers) {
|
|
577
|
+
try { await handler(error) }
|
|
578
|
+
catch (err) { logger.error({ err }, 'error in socket end handler') }
|
|
579
|
+
}
|
|
409
580
|
ev.emit('connection.update', { connection: 'close', lastDisconnect: { error, date: new Date() } })
|
|
410
581
|
ev.removeAllListeners('connection.update')
|
|
582
|
+
ev.removeAllListeners()
|
|
583
|
+
ev.destroy?.()
|
|
411
584
|
}
|
|
412
585
|
|
|
413
586
|
const sendPassiveIq = (tag) =>
|
|
@@ -426,14 +599,21 @@ export const makeSocket = (config) => {
|
|
|
426
599
|
content: [{ tag: 'remove-companion-device', attrs: { jid, reason: 'user_initiated' } }]
|
|
427
600
|
})
|
|
428
601
|
}
|
|
429
|
-
end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
|
602
|
+
void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
|
430
603
|
}
|
|
431
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Register a cleanup handler that will be awaited during end().
|
|
607
|
+
* Use for releasing external resources tied to this socket's lifetime.
|
|
608
|
+
*/
|
|
609
|
+
const registerSocketEndHandler = (handler) => socketEndHandlers.push(handler)
|
|
610
|
+
|
|
432
611
|
// ─── Pairing ─────────────────────────────────────────────────────────────────
|
|
433
612
|
|
|
434
613
|
const requestPairingCode = async (phoneNumber, customPairingCode) => {
|
|
435
614
|
await waitForSocketOpen()
|
|
436
|
-
|
|
615
|
+
// Brief stabilisation delay — ensures the WS open event has fully propagated
|
|
616
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
437
617
|
const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
|
|
438
618
|
if (customPairingCode && customPairingCode?.length !== 8)
|
|
439
619
|
throw new Error('Custom pairing code must be exactly 8 chars')
|
|
@@ -449,7 +629,7 @@ export const makeSocket = (config) => {
|
|
|
449
629
|
content: [
|
|
450
630
|
{ tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
|
|
451
631
|
{ tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
|
|
452
|
-
{ tag: 'companion_platform_id', attrs: {}, content:
|
|
632
|
+
{ tag: 'companion_platform_id', attrs: {}, content: getCompanionPlatformId(browser) },
|
|
453
633
|
{ tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` },
|
|
454
634
|
{ tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
|
|
455
635
|
]
|
|
@@ -492,31 +672,22 @@ export const makeSocket = (config) => {
|
|
|
492
672
|
})
|
|
493
673
|
}
|
|
494
674
|
|
|
495
|
-
const sendWAMBuffer = (wamBuffer) =>
|
|
496
|
-
query({
|
|
497
|
-
tag: 'iq',
|
|
498
|
-
attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
|
|
499
|
-
content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
|
|
500
|
-
})
|
|
501
|
-
|
|
502
675
|
// ─── WebSocket event bindings ────────────────────────────────────────────────
|
|
503
676
|
|
|
504
677
|
ws.on('message', onMessageReceived)
|
|
505
678
|
|
|
506
679
|
ws.on('open', async () => {
|
|
507
680
|
try { await validateConnection() }
|
|
508
|
-
catch (err) { logger.error({ err }, 'error in validating connection'); end(err) }
|
|
681
|
+
catch (err) { logger.error({ err }, 'error in validating connection'); void end(err) }
|
|
509
682
|
})
|
|
510
683
|
|
|
511
|
-
// Let mapWebSocketError convert the raw error then call end()
|
|
512
684
|
ws.on('error', mapWebSocketError(end))
|
|
513
685
|
|
|
514
|
-
|
|
515
|
-
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
|
686
|
+
ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
|
516
687
|
|
|
517
688
|
ws.on('CB:xmlstreamend', () => {
|
|
518
689
|
logger.info('Stream ended by server')
|
|
519
|
-
if (!closed) end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
|
|
690
|
+
if (!closed) void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
|
|
520
691
|
})
|
|
521
692
|
|
|
522
693
|
// ─── QR pairing ─────────────────────────────────────────────────────────────
|
|
@@ -532,9 +703,10 @@ export const makeSocket = (config) => {
|
|
|
532
703
|
const genPairQR = () => {
|
|
533
704
|
if (!ws.isOpen) return
|
|
534
705
|
const refNode = refNodes.shift()
|
|
535
|
-
if (!refNode) { end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
|
|
706
|
+
if (!refNode) { void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
|
|
536
707
|
const ref = refNode.content.toString('utf-8')
|
|
537
|
-
|
|
708
|
+
// Use buildPairingQRData so the browser tuple is included in the QR payload
|
|
709
|
+
const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser)
|
|
538
710
|
ev.emit('connection.update', { qr })
|
|
539
711
|
qrTimer = setTimeout(genPairQR, qrMs)
|
|
540
712
|
qrMs = qrTimeout || 20000
|
|
@@ -554,7 +726,7 @@ export const makeSocket = (config) => {
|
|
|
554
726
|
void sendUnifiedSession()
|
|
555
727
|
} catch (error) {
|
|
556
728
|
logger.info({ trace: error.stack }, 'Error in pairing')
|
|
557
|
-
end(error)
|
|
729
|
+
void end(error)
|
|
558
730
|
}
|
|
559
731
|
})
|
|
560
732
|
|
|
@@ -565,15 +737,11 @@ export const makeSocket = (config) => {
|
|
|
565
737
|
updateServerTimeOffset(node)
|
|
566
738
|
await uploadPreKeysToServerIfRequired()
|
|
567
739
|
await sendPassiveIq('active')
|
|
568
|
-
try {
|
|
569
|
-
|
|
570
|
-
} catch (e) {
|
|
571
|
-
logger.warn({ e }, 'failed to run digest after login')
|
|
572
|
-
}
|
|
740
|
+
try { await digestKeyBundle() }
|
|
741
|
+
catch (e) { logger.warn({ e }, 'failed to run digest after login') }
|
|
573
742
|
} catch (err) {
|
|
574
743
|
logger.warn({ err }, 'Failed to send initial passive IQ')
|
|
575
744
|
}
|
|
576
|
-
|
|
577
745
|
logger.info('✅ Opened connection to WhatsApp')
|
|
578
746
|
clearTimeout(qrTimer)
|
|
579
747
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
|
|
@@ -585,18 +753,22 @@ export const makeSocket = (config) => {
|
|
|
585
753
|
process.nextTick(async () => {
|
|
586
754
|
try {
|
|
587
755
|
const myPN = authState.creds.me.id
|
|
756
|
+
// Store own LID-PN mapping
|
|
588
757
|
await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
|
|
758
|
+
// Build device-list using index-based batching to avoid unbounded key growth
|
|
589
759
|
const { user, device } = jidDecode(myPN)
|
|
590
760
|
const currentBatch = await migrateIndexKey(authState.keys, 'device-list')
|
|
591
761
|
currentBatch[user] = [device?.toString() || '0']
|
|
592
762
|
const deviceKeys = Object.keys(currentBatch)
|
|
593
763
|
if (deviceKeys.length > BATCH_SIZE) {
|
|
594
764
|
deviceKeys.sort()
|
|
595
|
-
deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach(k => delete currentBatch[k])
|
|
765
|
+
deviceKeys.slice(0, deviceKeys.length - BATCH_SIZE).forEach((k) => delete currentBatch[k])
|
|
596
766
|
}
|
|
597
|
-
await authState.keys.set({ 'device-list': {
|
|
767
|
+
await authState.keys.set({ 'device-list': { index: currentBatch } })
|
|
768
|
+
// Migrate own session from PN → LID
|
|
598
769
|
await signalRepository.migrateSession(myPN, myLID)
|
|
599
770
|
logger.info({ myPN, myLID }, 'Own LID session created successfully')
|
|
771
|
+
// Batch-migrate any remaining PN sessions to LID
|
|
600
772
|
if (signalRepository.migrateAllPNSessionsToLID) {
|
|
601
773
|
try {
|
|
602
774
|
const migrated = await signalRepository.migrateAllPNSessionsToLID()
|
|
@@ -615,37 +787,37 @@ export const makeSocket = (config) => {
|
|
|
615
787
|
// ─── Stream / connection error handlers ─────────────────────────────────────
|
|
616
788
|
|
|
617
789
|
ws.on('CB:stream:error', (node) => {
|
|
618
|
-
|
|
790
|
+
const [reasonNode] = getAllBinaryNodeChildren(node)
|
|
791
|
+
logger.error({ reasonNode, fullErrorNode: node }, 'Stream errored out')
|
|
619
792
|
const { reason, statusCode } = getErrorCodeFromStreamError(node)
|
|
620
|
-
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }))
|
|
793
|
+
void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }))
|
|
621
794
|
})
|
|
622
795
|
|
|
623
796
|
ws.on('CB:failure', (node) => {
|
|
624
797
|
const reason = +(node.attrs.reason || 500)
|
|
625
|
-
end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
|
|
798
|
+
void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
|
|
626
799
|
})
|
|
627
800
|
|
|
628
801
|
ws.on('CB:ib,,downgrade_webclient', () =>
|
|
629
|
-
end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
|
|
802
|
+
void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
|
|
630
803
|
)
|
|
631
804
|
|
|
632
|
-
ws.on('CB:ib,,offline_preview', (node) => {
|
|
805
|
+
ws.on('CB:ib,,offline_preview', async (node) => {
|
|
633
806
|
logger.info('Offline preview received', JSON.stringify(node))
|
|
634
|
-
sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
|
|
807
|
+
await sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
|
|
635
808
|
})
|
|
636
809
|
|
|
637
810
|
ws.on('CB:ib,,edge_routing', (node) => {
|
|
638
811
|
const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
|
|
639
812
|
const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
|
|
640
813
|
if (routingInfo?.content) {
|
|
641
|
-
authState.creds.routingInfo = Buffer.from(routingInfo
|
|
814
|
+
authState.creds.routingInfo = Buffer.from(routingInfo.content)
|
|
642
815
|
ev.emit('creds.update', authState.creds)
|
|
643
816
|
}
|
|
644
817
|
})
|
|
645
818
|
|
|
646
819
|
// ─── Buffering & offline notifications ──────────────────────────────────────
|
|
647
820
|
|
|
648
|
-
let didStartBuffer = false
|
|
649
821
|
process.nextTick(() => {
|
|
650
822
|
if (creds.me?.id) { ev.buffer(); didStartBuffer = true }
|
|
651
823
|
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
|
|
@@ -689,6 +861,7 @@ export const makeSocket = (config) => {
|
|
|
689
861
|
sendNode,
|
|
690
862
|
logout,
|
|
691
863
|
end,
|
|
864
|
+
registerSocketEndHandler,
|
|
692
865
|
onUnexpectedError,
|
|
693
866
|
uploadPreKeys,
|
|
694
867
|
uploadPreKeysToServerIfRequired,
|
|
@@ -701,6 +874,9 @@ export const makeSocket = (config) => {
|
|
|
701
874
|
waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
|
|
702
875
|
sendWAMBuffer,
|
|
703
876
|
executeUSyncQuery,
|
|
877
|
+
onWhatsApp,
|
|
878
|
+
fetchAccountReachoutTimelock,
|
|
879
|
+
fetchNewChatMessageCap,
|
|
704
880
|
listener: (eventName) => {
|
|
705
881
|
if (typeof ev.listenerCount === 'function') return ev.listenerCount(eventName)
|
|
706
882
|
if (typeof ev.listener === 'function') return ev.listener(eventName)?.length || 0
|