@zervonex/baileys 0.0.0-stage → 3.0.1

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.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3203 -2
  3. package/WAProto/WAProtoCompile.js +291 -0
  4. package/WAProto/fetcher.js +69 -0
  5. package/WAProto/index.js +88 -0
  6. package/WAProto/parser.js +313 -0
  7. package/engine-requirements.js +10 -0
  8. package/lib/Builders/AIRich.js +590 -0
  9. package/lib/Builders/base.js +130 -0
  10. package/lib/Builders/builders.js +448 -0
  11. package/lib/Builders/index.js +3 -0
  12. package/lib/Defaults/index.js +225 -0
  13. package/lib/Defaults/phonenumber-mcc.json +223 -0
  14. package/lib/Signal/index.js +2 -0
  15. package/lib/Signal/libsignal.js +523 -0
  16. package/lib/Signal/lid-mapping.js +283 -0
  17. package/lib/Socket/Client/index.js +2 -0
  18. package/lib/Socket/Client/types.js +10 -0
  19. package/lib/Socket/Client/websocket.js +84 -0
  20. package/lib/Socket/business.js +152 -0
  21. package/lib/Socket/chats.js +855 -0
  22. package/lib/Socket/communities.js +147 -0
  23. package/lib/Socket/groups.js +294 -0
  24. package/lib/Socket/index.js +15 -0
  25. package/lib/Socket/messages-recv.js +904 -0
  26. package/lib/Socket/messages-send.js +693 -0
  27. package/lib/Socket/mex.js +34 -0
  28. package/lib/Socket/newsletter.js +752 -0
  29. package/lib/Socket/nexus-handler.js +55 -0
  30. package/lib/Socket/registration.js +155 -0
  31. package/lib/Socket/socket.js +798 -0
  32. package/lib/Socket/spam-report.js +65 -0
  33. package/lib/Socket/username.js +82 -0
  34. package/lib/Store/index.js +6 -0
  35. package/lib/Store/make-cache-manager-store.js +81 -0
  36. package/lib/Store/make-in-memory-store.js +425 -0
  37. package/lib/Store/make-ordered-dictionary.js +82 -0
  38. package/lib/Store/object-repository.js +31 -0
  39. package/lib/Store/reporting-info-store.js +38 -0
  40. package/lib/Types/Auth.js +2 -0
  41. package/lib/Types/Bussines.js +2 -0
  42. package/lib/Types/Call.js +2 -0
  43. package/lib/Types/Chat.js +8 -0
  44. package/lib/Types/Contact.js +2 -0
  45. package/lib/Types/Events.js +2 -0
  46. package/lib/Types/GroupMetadata.js +2 -0
  47. package/lib/Types/Label.js +25 -0
  48. package/lib/Types/LabelAssociation.js +7 -0
  49. package/lib/Types/Message.js +15 -0
  50. package/lib/Types/Newsletter.js +100 -0
  51. package/lib/Types/Product.js +2 -0
  52. package/lib/Types/Signal.js +2 -0
  53. package/lib/Types/Socket.js +14 -0
  54. package/lib/Types/State.js +26 -0
  55. package/lib/Types/USync.js +2 -0
  56. package/lib/Types/index.js +30 -0
  57. package/lib/Utils/auth-utils.js +142 -0
  58. package/lib/Utils/baileys-event-stream.js +33 -0
  59. package/lib/Utils/bot-capabilities.js +40 -0
  60. package/lib/Utils/bot-planning-replay.js +44 -0
  61. package/lib/Utils/browser-utils.js +26 -0
  62. package/lib/Utils/business.js +75 -0
  63. package/lib/Utils/chat-utils.js +316 -0
  64. package/lib/Utils/companion-reg-client-utils.js +20 -0
  65. package/lib/Utils/consumer-application.js +38 -0
  66. package/lib/Utils/crypto.js +52 -0
  67. package/lib/Utils/decode-wa-message.js +197 -0
  68. package/lib/Utils/event-buffer.js +273 -0
  69. package/lib/Utils/extras.js +186 -0
  70. package/lib/Utils/generics.js +195 -0
  71. package/lib/Utils/history.js +51 -0
  72. package/lib/Utils/identity-change-handler.js +19 -0
  73. package/lib/Utils/index.js +39 -0
  74. package/lib/Utils/key-store.js +3 -0
  75. package/lib/Utils/link-preview.js +51 -0
  76. package/lib/Utils/logger.js +2 -0
  77. package/lib/Utils/lt-hash.js +3 -0
  78. package/lib/Utils/make-mutex.js +16 -0
  79. package/lib/Utils/message-composer.js +59 -0
  80. package/lib/Utils/message-retry-manager.js +48 -0
  81. package/lib/Utils/messages-media.js +520 -0
  82. package/lib/Utils/messages.js +742 -0
  83. package/lib/Utils/meta-ai-msmsg.js +122 -0
  84. package/lib/Utils/meta-compositing.js +75 -0
  85. package/lib/Utils/modern-messages.js +235 -0
  86. package/lib/Utils/native-flow.js +55 -0
  87. package/lib/Utils/noise-handler.js +161 -0
  88. package/lib/Utils/pre-key-manager.js +76 -0
  89. package/lib/Utils/process-message.js +399 -0
  90. package/lib/Utils/reporting-utils.js +171 -0
  91. package/lib/Utils/signal.js +167 -0
  92. package/lib/Utils/spam-report-utils.js +142 -0
  93. package/lib/Utils/sync-action-utils.js +33 -0
  94. package/lib/Utils/tc-token-utils.js +102 -0
  95. package/lib/Utils/use-multi-file-auth-state.js +307 -0
  96. package/lib/Utils/validate-connection.js +181 -0
  97. package/lib/WABinary/constants.js +1321 -0
  98. package/lib/WABinary/decode.js +152 -0
  99. package/lib/WABinary/encode.js +140 -0
  100. package/lib/WABinary/generic-utils.js +179 -0
  101. package/lib/WABinary/index.js +6 -0
  102. package/lib/WABinary/jid-utils.js +235 -0
  103. package/lib/WABinary/types.js +4 -0
  104. package/lib/WAM/BinaryInfo.js +10 -0
  105. package/lib/WAM/constants.js +22853 -0
  106. package/lib/WAM/encode.js +150 -0
  107. package/lib/WAM/index.js +3 -0
  108. package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
  109. package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
  110. package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
  111. package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
  112. package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
  113. package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
  114. package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
  115. package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
  116. package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
  117. package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
  118. package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
  119. package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
  120. package/lib/WAUSync/Protocols/index.js +12 -0
  121. package/lib/WAUSync/USyncQuery.js +100 -0
  122. package/lib/WAUSync/USyncUser.js +20 -0
  123. package/lib/WAUSync/index.js +3 -0
  124. package/lib/index.js +68 -0
  125. package/package.json +216 -5
@@ -0,0 +1,798 @@
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, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_COUNT,
8
+ MIN_UPLOAD_INTERVAL, NOISE_WA_HEADER, UPLOAD_TIMEOUT, TimeMs
9
+ } from '../Defaults/index.js'
10
+ import {
11
+ QueryIds, ReachoutTimelockEnforcementType,
12
+ DisconnectReason, XWAPaths
13
+ } from '../Types/index.js'
14
+ import {
15
+ addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData,
16
+ bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode,
17
+ generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getCompanionPlatformId,
18
+ getErrorCodeFromStreamError, getNextPreKeysNode, makeEventBuffer, makeNoiseHandler,
19
+ promiseTimeout, signedKeyPair, xmppSignedPreKey, getPlatformId, isAndroidBrowser
20
+ } from '../Utils/index.js'
21
+ import {
22
+ assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren,
23
+ getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode,
24
+ S_WHATSAPP_NET
25
+ } from '../WABinary/index.js'
26
+ import { BinaryInfo } from '../WAM/BinaryInfo.js'
27
+ import { USyncQuery, USyncUser } from '../WAUSync/index.js'
28
+ import { WebSocketClient } from './Client/index.js'
29
+ import { executeWMexQuery } from './mex.js'
30
+
31
+ const mapWebSocketError = (handler) => (error) =>
32
+ handler(new Boom(`WebSocket Error (${error?.message})`, { statusCode: getCodeFromWSError(error), data: error }))
33
+
34
+ export const makeSocket = (config) => {
35
+ const {
36
+ waWebSocketUrl,
37
+ connectTimeoutMs,
38
+ logger,
39
+ keepAliveIntervalMs,
40
+ browser,
41
+ auth: authState,
42
+ printQRInTerminal,
43
+ defaultQueryTimeoutMs,
44
+ transactionOpts,
45
+ qrTimeout,
46
+ makeSignalRepository
47
+ } = config
48
+
49
+ const url = typeof waWebSocketUrl === 'string' ? new URL(waWebSocketUrl) : waWebSocketUrl
50
+
51
+ if (config.mobile || url.protocol === 'tcp:')
52
+ throw new Boom('Mobile API not supported', { statusCode: DisconnectReason.loggedOut })
53
+
54
+ if (url.protocol === 'wss:' && authState?.creds?.routingInfo)
55
+ url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'))
56
+
57
+ const ephemeralKeyPair = Curve.generateKeyPair()
58
+ const noise = makeNoiseHandler({
59
+ keyPair: ephemeralKeyPair,
60
+ NOISE_HEADER: NOISE_WA_HEADER,
61
+ logger,
62
+ routingInfo: authState?.creds?.routingInfo
63
+ })
64
+
65
+ const ws = new WebSocketClient(url, config)
66
+ logger.info({ url: url.toString() }, 'Initiating WebSocket connection')
67
+ ws.connect()
68
+
69
+ if (printQRInTerminal) {
70
+ import('qrcode-terminal').then(({ default: qrcode }) => {
71
+ ev.on('connection.update', ({ qr }) => {
72
+ if (qr) qrcode.generate(qr, { small: true })
73
+ })
74
+ })
75
+ }
76
+
77
+ if (browser[1].toLocaleLowerCase().includes('android')) {
78
+ logger.warn('⚠️ Using the Android browser is experimental and may lead to unexpected behavior. Use at your own risk.')
79
+ }
80
+
81
+ const ev = makeEventBuffer(logger)
82
+ const { creds } = authState
83
+ const keys = addTransactionCapability(authState.keys, logger, transactionOpts)
84
+ const signalRepository = makeSignalRepository({ creds, keys }, logger, pnFromLIDUSync)
85
+ const publicWAMBuffer = new BinaryInfo()
86
+ const uqTagId = generateMdTagPrefix()
87
+ const sendPromise = promisify(ws.send)
88
+
89
+ let epoch = 1
90
+ let lastDateRecv
91
+ let lastUploadTime = 0
92
+ let uploadPreKeysPromise = null
93
+ let closed = false
94
+ let keepAliveReq
95
+ let qrTimer
96
+ let serverTimeOffsetMs = 0
97
+ let didStartBuffer = false
98
+
99
+ const socketEndHandlers = []
100
+
101
+ const generateMessageTag = () => `${uqTagId}${epoch++}`
102
+
103
+ // ─── Transport ──────────────────────────────────────────────────────────────
104
+
105
+ const sendRawMessage = async (data) => {
106
+ if (!ws.isOpen)
107
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
108
+ const bytes = noise.encodeFrame(data)
109
+ await promiseTimeout(connectTimeoutMs, async (resolve, reject) => {
110
+ try { await sendPromise.call(ws, bytes); resolve() }
111
+ catch (error) { reject(error) }
112
+ })
113
+ }
114
+
115
+ const sendNode = (frame) => {
116
+ if (logger.level === 'trace') logger.trace({ xml: binaryNodeToString(frame), msg: 'xml send' })
117
+ return sendRawMessage(encodeBinaryNode(frame))
118
+ }
119
+
120
+ // ─── Query ──────────────────────────────────────────────────────────────────
121
+
122
+ const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {
123
+ let onRecv, onErr
124
+ try {
125
+ return await promiseTimeout(timeoutMs, (resolve, reject) => {
126
+ onRecv = (data) => resolve(data)
127
+ onErr = (err) => reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
128
+ ws.on(`TAG:${msgId}`, onRecv)
129
+ ws.on('close', onErr)
130
+ ws.on('error', onErr)
131
+ return () => reject(new Boom('Query Cancelled'))
132
+ })
133
+ } catch (error) {
134
+ if (error instanceof Boom && error.output?.statusCode === DisconnectReason.timedOut) {
135
+ logger?.warn?.({ msgId }, 'timed out waiting for message')
136
+ return undefined
137
+ }
138
+ throw error
139
+ } finally {
140
+ if (onRecv) ws.off(`TAG:${msgId}`, onRecv)
141
+ if (onErr) { ws.off('close', onErr); ws.off('error', onErr) }
142
+ }
143
+ }
144
+
145
+ const query = async (node, timeoutMs) => {
146
+ if (!node.attrs.id) node.attrs.id = generateMessageTag()
147
+ const msgId = node.attrs.id
148
+ const result = await promiseTimeout(timeoutMs, async (resolve, reject) => {
149
+ const result = waitForMessage(msgId, timeoutMs).catch(reject)
150
+ sendNode(node).then(async () => resolve(await result)).catch(reject)
151
+ })
152
+ if (result && 'tag' in result) assertNodeErrorFree(result)
153
+ return result
154
+ }
155
+
156
+ // ─── USync ──────────────────────────────────────────────────────────────────
157
+
158
+ const executeUSyncQuery = async (usyncQuery) => {
159
+ if (usyncQuery.protocols.length === 0)
160
+ throw new Boom('USyncQuery must have at least one protocol')
161
+ const userNodes = usyncQuery.users.map((user) => ({
162
+ tag: 'user',
163
+ attrs: Object.fromEntries(
164
+ Object.entries({ jid: !user.phone && !user.username ? user.id : undefined }).filter(([, v]) => v !== undefined)
165
+ ),
166
+ content: usyncQuery.protocols.map((a) => a.getUserElement(user)).filter((a) => a !== null)
167
+ }))
168
+ const iq = {
169
+ tag: 'iq',
170
+ attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'usync' },
171
+ content: [{
172
+ tag: 'usync',
173
+ attrs: {
174
+ context: usyncQuery.context,
175
+ mode: usyncQuery.mode,
176
+ sid: generateMessageTag(),
177
+ last: 'true',
178
+ index: '0'
179
+ },
180
+ content: [
181
+ { tag: 'query', attrs: {}, content: usyncQuery.protocols.map((a) => a.getQueryElement()) },
182
+ { tag: 'list', attrs: {}, content: userNodes }
183
+ ]
184
+ }]
185
+ }
186
+ return usyncQuery.parseUSyncQueryResult(await query(iq))
187
+ }
188
+
189
+ async function pnFromLIDUSync(jids) {
190
+ const usyncQuery = new USyncQuery().withLIDProtocol().withContext('background')
191
+ for (const jid of jids) {
192
+ if (isLidUser(jid)) { logger?.warn('LID user found in LID fetch call'); continue }
193
+ usyncQuery.withUser(new USyncUser().withId(jid))
194
+ }
195
+ if (usyncQuery.users.length === 0) return []
196
+ const results = await executeUSyncQuery(usyncQuery)
197
+ return results ? results.list.filter((a) => !!a.lid).map(({ lid, id }) => ({ pn: id, lid })) : []
198
+ }
199
+
200
+ const onWhatsApp = async (...phoneNumbers) => {
201
+ let usyncQuery = new USyncQuery()
202
+ let contactEnabled = false
203
+ for (let jid of phoneNumbers) {
204
+ if (isLidUser(jid)) {
205
+ const pn = await signalRepository.lidMapping.getPNForLID(jid)
206
+ if (pn) {
207
+ jid = pn
208
+ } else {
209
+ if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
210
+ usyncQuery.withUser(new USyncUser().withId(jid))
211
+ continue
212
+ }
213
+ }
214
+ if (!contactEnabled) { contactEnabled = true; usyncQuery = usyncQuery.withContactProtocol() }
215
+ const phone = `+${jid.replace('+', '').split('@')[0]?.split(':')[0]}`
216
+ usyncQuery.withUser(new USyncUser().withPhone(phone))
217
+ }
218
+ if (usyncQuery.users.length === 0) return []
219
+ const results = await executeUSyncQuery(usyncQuery)
220
+ return results
221
+ ? results.list.filter((a) => !!a.contact && a.id && a.id !== 'undefined').map(({ contact, id }) => ({ jid: id, exists: contact }))
222
+ : []
223
+ }
224
+
225
+ // ─── Pre-keys ───────────────────────────────────────────────────────────────
226
+
227
+ const getAvailablePreKeysOnServer = async () => {
228
+ const result = await query({
229
+ tag: 'iq',
230
+ attrs: { id: generateMessageTag(), xmlns: 'encrypt', type: 'get', to: S_WHATSAPP_NET },
231
+ content: [{ tag: 'count', attrs: {} }]
232
+ })
233
+ return +getBinaryNodeChild(result, 'count').attrs.value
234
+ }
235
+
236
+ const verifyCurrentPreKeyExists = async () => {
237
+ const currentPreKeyId = creds.nextPreKeyId - 1
238
+ if (currentPreKeyId <= 0) return { exists: false, currentPreKeyId: 0 }
239
+ const preKeys = await keys.get('pre-key', [currentPreKeyId.toString()])
240
+ return { exists: !!preKeys[currentPreKeyId.toString()], currentPreKeyId }
241
+ }
242
+
243
+ const uploadPreKeys = async (count = MIN_PREKEY_COUNT, retryCount = 0) => {
244
+ if (retryCount === 0 && Date.now() - lastUploadTime < MIN_UPLOAD_INTERVAL) {
245
+ logger.debug(`Skipping upload — only ${Date.now() - lastUploadTime}ms since last upload`)
246
+ return
247
+ }
248
+ if (uploadPreKeysPromise) {
249
+ logger.debug('Pre-key upload in progress, waiting...')
250
+ await uploadPreKeysPromise
251
+ return
252
+ }
253
+ const uploadLogic = async () => {
254
+ logger.info({ count, retryCount }, 'Uploading pre-keys')
255
+ const node = await keys.transaction(async () => {
256
+ const { update, node } = await getNextPreKeysNode({ creds, keys }, count)
257
+ ev.emit('creds.update', update)
258
+ return node
259
+ }, creds?.me?.id || 'upload-pre-keys')
260
+ try {
261
+ await query(node)
262
+ logger.info({ count }, '✅ Pre-keys uploaded successfully')
263
+ lastUploadTime = Date.now()
264
+ } catch (uploadError) {
265
+ logger.error({ uploadError: uploadError.toString(), count }, 'Failed to upload pre-keys')
266
+ if (retryCount < 3) {
267
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount), 10000)
268
+ logger.info(`Retrying pre-key upload in ${backoffDelay}ms`)
269
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay))
270
+ return uploadPreKeys(count, retryCount + 1)
271
+ }
272
+ throw uploadError
273
+ }
274
+ }
275
+ uploadPreKeysPromise = Promise.race([
276
+ uploadLogic(),
277
+ new Promise((_, reject) =>
278
+ setTimeout(() => reject(new Boom('Pre-key upload timeout', { statusCode: 408 })), UPLOAD_TIMEOUT)
279
+ )
280
+ ])
281
+ try { await uploadPreKeysPromise } finally { uploadPreKeysPromise = null }
282
+ }
283
+
284
+ const uploadPreKeysToServerIfRequired = async () => {
285
+ try {
286
+ const preKeyCount = await getAvailablePreKeysOnServer()
287
+ logger.info(`${preKeyCount} pre-keys found on server`)
288
+ const { exists: currentPreKeyExists, currentPreKeyId } = await verifyCurrentPreKeyExists()
289
+ logger.info(`Current prekey ID: ${currentPreKeyId}, exists in storage: ${currentPreKeyExists}`)
290
+ const lowServerCount = preKeyCount <= MIN_PREKEY_COUNT
291
+ const missingCurrentPreKey = !currentPreKeyExists && currentPreKeyId > 0
292
+ if (lowServerCount || missingCurrentPreKey) {
293
+ const reasons = []
294
+ if (lowServerCount) reasons.push(`server count low (${preKeyCount})`)
295
+ if (missingCurrentPreKey) reasons.push(`current prekey ${currentPreKeyId} missing from storage`)
296
+ logger.info(`Uploading PreKeys due to: ${reasons.join(', ')}`)
297
+ await uploadPreKeys(preKeyCount === 0 ? INITIAL_PREKEY_COUNT : MIN_PREKEY_COUNT)
298
+ } else {
299
+ logger.info(`✅ PreKey validation passed — Server: ${preKeyCount}, prekey ${currentPreKeyId} exists`)
300
+ }
301
+ } catch (error) {
302
+ logger.error({ error }, 'Failed to check/upload pre-keys during init')
303
+ }
304
+ }
305
+
306
+ // ─── Key-bundle & signed pre-key ────────────────────────────────────────────
307
+
308
+ const digestKeyBundle = async () => {
309
+ const res = await query({
310
+ tag: 'iq',
311
+ attrs: { to: S_WHATSAPP_NET, type: 'get', xmlns: 'encrypt' },
312
+ content: [{ tag: 'digest', attrs: {} }]
313
+ })
314
+ const digestNode = getBinaryNodeChild(res, 'digest')
315
+ if (!digestNode) {
316
+ await uploadPreKeys()
317
+ throw new Error('encrypt/get digest returned no digest node')
318
+ }
319
+ }
320
+
321
+ const rotateSignedPreKey = async () => {
322
+ const newId = (creds.signedPreKey.keyId || 0) + 1
323
+ const skey = await signedKeyPair(creds.signedIdentityKey, newId)
324
+ await query({
325
+ tag: 'iq',
326
+ attrs: { to: S_WHATSAPP_NET, type: 'set', xmlns: 'encrypt' },
327
+ content: [{ tag: 'rotate', attrs: {}, content: [xmppSignedPreKey(skey)] }]
328
+ })
329
+ ev.emit('creds.update', { signedPreKey: skey })
330
+ }
331
+
332
+ // ─── Server time & unified session ──────────────────────────────────────────
333
+
334
+ const updateServerTimeOffset = ({ attrs }) => {
335
+ const tValue = attrs?.t
336
+ if (!tValue) return
337
+ const parsed = Number(tValue)
338
+ if (Number.isNaN(parsed) || parsed <= 0) return
339
+ serverTimeOffsetMs = parsed * 1000 - Date.now()
340
+ logger.debug({ offset: serverTimeOffsetMs }, 'Calculated server time offset')
341
+ }
342
+
343
+ const getUnifiedSessionId = () => {
344
+ const offsetMs = 3 * TimeMs.Day
345
+ const now = Date.now() + serverTimeOffsetMs
346
+ return ((now + offsetMs) % TimeMs.Week).toString()
347
+ }
348
+
349
+ const sendUnifiedSession = async () => {
350
+ if (!ws.isOpen) return
351
+ try {
352
+ await sendNode({
353
+ tag: 'ib',
354
+ attrs: {},
355
+ content: [{ tag: 'unified_session', attrs: { id: getUnifiedSessionId() } }]
356
+ })
357
+ } catch (error) {
358
+ logger.debug({ error }, 'Failed to send unified_session telemetry')
359
+ }
360
+ }
361
+
362
+ // ─── WAM buffer ─────────────────────────────────────────────────────────────
363
+
364
+ const sendWAMBuffer = (wamBuffer) =>
365
+ query({
366
+ tag: 'iq',
367
+ attrs: { to: S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' },
368
+ content: [{ tag: 'add', attrs: { t: Math.round(Date.now() / 1000) + '' }, content: wamBuffer }]
369
+ })
370
+
371
+ // ─── WMex queries ────────────────────────────────────────────────────────────
372
+
373
+ const fetchAccountReachoutTimelock = async () => {
374
+ const queryResult = await executeWMexQuery(
375
+ {},
376
+ QueryIds.REACHOUT_TIMELOCK,
377
+ XWAPaths.xwa2_fetch_account_reachout_timelock,
378
+ query,
379
+ generateMessageTag
380
+ )
381
+ const result = {
382
+ isActive: !!queryResult?.is_active,
383
+ timeEnforcementEnds:
384
+ queryResult?.time_enforcement_ends && queryResult.time_enforcement_ends !== '0'
385
+ ? new Date(parseInt(queryResult.time_enforcement_ends, 10) * 1000)
386
+ : undefined,
387
+ enforcementType: queryResult?.enforcement_type ?? ReachoutTimelockEnforcementType.DEFAULT
388
+ }
389
+ ev.emit('connection.update', { reachoutTimeLock: result })
390
+ return result
391
+ }
392
+
393
+ const fetchNewChatMessageCap = async () =>
394
+ executeWMexQuery(
395
+ { input: { type: 'INDIVIDUAL_NEW_CHAT_MSG' } },
396
+ QueryIds.MESSAGE_CAPPING_INFO,
397
+ XWAPaths.xwa2_message_capping_info,
398
+ query,
399
+ generateMessageTag
400
+ )
401
+
402
+ // ─── Connection lifecycle ────────────────────────────────────────────────────
403
+
404
+ const onUnexpectedError = (err, msg) => {
405
+ const isClosed = err?.message?.includes('Connection Closed') || err?.output?.statusCode === 428
406
+ if (isClosed) {
407
+ logger.debug({ msg: err?.message }, `Connection closed during '${msg}'`)
408
+ } else {
409
+ logger.error({ err }, `unexpected error in '${msg}'`)
410
+ }
411
+ }
412
+
413
+ const awaitNextMessage = async (sendMsg) => {
414
+ if (!ws.isOpen)
415
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
416
+ let onOpen, onClose
417
+ const result = promiseTimeout(connectTimeoutMs, (resolve, reject) => {
418
+ onOpen = resolve
419
+ onClose = mapWebSocketError(reject)
420
+ ws.on('frame', onOpen)
421
+ ws.on('close', onClose)
422
+ ws.on('error', onClose)
423
+ }).finally(() => {
424
+ ws.off('frame', onOpen)
425
+ ws.off('close', onClose)
426
+ ws.off('error', onClose)
427
+ })
428
+ if (sendMsg) sendRawMessage(sendMsg).catch(onClose)
429
+ return result
430
+ }
431
+
432
+ const validateConnection = async () => {
433
+ const helloMsg = proto.HandshakeMessage.fromObject({ clientHello: { ephemeral: ephemeralKeyPair.public } })
434
+ logger.info({ browser, helloMsg }, 'Connected to WhatsApp')
435
+ const init = proto.HandshakeMessage.encode(helloMsg).finish()
436
+ const result = await awaitNextMessage(init)
437
+ const handshake = proto.HandshakeMessage.decode(result)
438
+ logger.trace({ handshake }, 'Handshake received from WhatsApp')
439
+ const keyEnc = await noise.processHandshake(handshake, creds.noiseKey)
440
+ const node = !creds.me ? generateRegistrationNode(creds, config) : generateLoginNode(creds.me.id, config)
441
+ logger.info({ node }, !creds.me ? 'Attempting registration...' : 'Logging in...')
442
+ const payloadEnc = noise.encrypt(proto.ClientPayload.encode(node).finish())
443
+ await sendRawMessage(
444
+ proto.HandshakeMessage.encode({ clientFinish: { static: keyEnc, payload: payloadEnc } }).finish()
445
+ )
446
+ await noise.finishInit()
447
+ startKeepAliveRequest()
448
+ }
449
+
450
+ const waitForSocketOpen = async () => {
451
+ if (ws.isOpen) return
452
+ if (ws.isClosed || ws.isClosing)
453
+ throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
454
+ let onOpen, onClose
455
+ await new Promise((resolve, reject) => {
456
+ onOpen = () => resolve(undefined)
457
+ onClose = mapWebSocketError(reject)
458
+ ws.on('open', onOpen)
459
+ ws.on('close', onClose)
460
+ ws.on('error', onClose)
461
+ }).finally(() => {
462
+ ws.off('open', onOpen)
463
+ ws.off('close', onClose)
464
+ ws.off('error', onClose)
465
+ })
466
+ }
467
+
468
+ const startKeepAliveRequest = () => {
469
+ keepAliveReq = setInterval(() => {
470
+ if (!lastDateRecv) lastDateRecv = new Date()
471
+ const diff = Date.now() - lastDateRecv.getTime()
472
+ if (diff > keepAliveIntervalMs + 5000) {
473
+ void end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
474
+ } else if (ws.isOpen) {
475
+ query({
476
+ tag: 'iq',
477
+ attrs: { id: generateMessageTag(), to: S_WHATSAPP_NET, type: 'get', xmlns: 'w:p' },
478
+ content: [{ tag: 'ping', attrs: {} }]
479
+ }).catch((err) => logger.error({ trace: err.stack }, 'Error in sending keep alive'))
480
+ } else {
481
+ logger.warn('Keep alive called when WS not open')
482
+ }
483
+ }, keepAliveIntervalMs)
484
+ }
485
+
486
+ const end = async (error) => {
487
+ if (closed) { logger.trace({ trace: error?.stack }, 'Connection already closed'); return }
488
+ closed = true
489
+ logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed')
490
+ clearInterval(keepAliveReq)
491
+ clearTimeout(qrTimer)
492
+ ws.removeAllListeners('close')
493
+ ws.removeAllListeners('open')
494
+ ws.removeAllListeners('message')
495
+ signalRepository.close?.()
496
+ if (!ws.isClosed && !ws.isClosing) {
497
+ try { await ws.close() } catch { }
498
+ }
499
+ for (const handler of socketEndHandlers) {
500
+ try { await handler(error) }
501
+ catch (err) { logger.error({ err }, 'error in socket end handler') }
502
+ }
503
+ ev.emit('connection.update', { connection: 'close', lastDisconnect: { error, date: new Date() } })
504
+ ev.removeAllListeners('connection.update')
505
+ ev.removeAllListeners()
506
+ ev.destroy?.()
507
+ }
508
+
509
+ const sendPassiveIq = (tag) =>
510
+ query({
511
+ tag: 'iq',
512
+ attrs: { to: S_WHATSAPP_NET, xmlns: 'passive', type: 'set' },
513
+ content: [{ tag, attrs: {} }]
514
+ })
515
+
516
+ const logout = async (msg) => {
517
+ const jid = authState.creds.me?.id
518
+ if (jid) {
519
+ await sendNode({
520
+ tag: 'iq',
521
+ attrs: { to: S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' },
522
+ content: [{ tag: 'remove-companion-device', attrs: { jid, reason: 'user_initiated' } }]
523
+ })
524
+ }
525
+ void end(new Boom(msg || 'Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
526
+ }
527
+
528
+ const registerSocketEndHandler = (handler) => socketEndHandlers.push(handler)
529
+
530
+ // ─── Pairing ─────────────────────────────────────────────────────────────────
531
+
532
+ const requestPairingCode = async (phoneNumber, customPairingCode) => {
533
+ if (authState.creds.pairingCode && !customPairingCode) return authState.creds.pairingCode
534
+ await waitForSocketOpen()
535
+ await new Promise((resolve) => setTimeout(resolve, 500))
536
+ const pairingCode = customPairingCode ?? bytesToCrockford(randomBytes(5))
537
+ if (customPairingCode && customPairingCode?.length !== 8)
538
+ throw new Error('Custom pairing code must be exactly 8 chars')
539
+ authState.creds.pairingCode = pairingCode
540
+ authState.creds.me = { id: jidEncode(phoneNumber, 's.whatsapp.net'), name: '~' }
541
+ ev.emit('creds.update', authState.creds)
542
+ await sendNode({
543
+ tag: 'iq',
544
+ attrs: { to: S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' },
545
+ content: [{
546
+ tag: 'link_code_companion_reg',
547
+ attrs: { jid: authState.creds.me.id, stage: 'companion_hello', should_show_push_notification: 'true' },
548
+ content: [
549
+ { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() },
550
+ { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public },
551
+ { tag: 'companion_platform_id', attrs: {}, content: isAndroidBrowser(browser) ? getPlatformId('Chrome') : getCompanionPlatformId(browser) },
552
+ { tag: 'companion_platform_display', attrs: {}, content: isAndroidBrowser(browser) ? 'Chrome (Mac OS)' : `${browser[1]} (${browser[0]})` },
553
+ { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' }
554
+ ]
555
+ }]
556
+ })
557
+ return authState.creds.pairingCode
558
+ }
559
+
560
+ async function generatePairingKey() {
561
+ const salt = randomBytes(32)
562
+ const randomIv = randomBytes(16)
563
+ const key = await derivePairingCodeKey(authState.creds.pairingCode, salt)
564
+ const ciphered = aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv)
565
+ return Buffer.concat([salt, randomIv, ciphered])
566
+ }
567
+
568
+ // ─── Incoming message processing ────────────────────────────────────────────
569
+
570
+ const onMessageReceived = (data) => {
571
+ noise.decodeFrame(data, (frame) => {
572
+ lastDateRecv = new Date()
573
+ let anyTriggered = ws.emit('frame', frame)
574
+ if (!(frame instanceof Uint8Array)) {
575
+ const msgId = frame.attrs.id
576
+ if (logger.level === 'trace') logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' })
577
+ anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered
578
+ const l0 = frame.tag
579
+ const l1 = frame.attrs || {}
580
+ const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
581
+ for (const key of Object.keys(l1)) {
582
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
583
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
584
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
585
+ }
586
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
587
+ anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
588
+ if (!anyTriggered && logger.level === 'debug')
589
+ logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'Unhandled communication received')
590
+ }
591
+ })
592
+ }
593
+
594
+ // ─── WebSocket event bindings ────────────────────────────────────────────────
595
+
596
+ ws.on('message', onMessageReceived)
597
+
598
+ ws.on('open', async () => {
599
+ try { await validateConnection() }
600
+ catch (err) { logger.error({ err }, 'error in validating connection'); void end(err) }
601
+ })
602
+
603
+ ws.on('error', mapWebSocketError(end))
604
+
605
+ ws.on('close', () => void end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
606
+
607
+ ws.on('CB:xmlstreamend', () => {
608
+ logger.info('Stream ended by server')
609
+ if (!closed) void end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
610
+ })
611
+
612
+ // ─── QR pairing ─────────────────────────────────────────────────────────────
613
+
614
+ ws.on('CB:iq,type:set,pair-device', async (stanza) => {
615
+ await sendNode({ tag: 'iq', attrs: { to: S_WHATSAPP_NET, type: 'result', id: stanza.attrs.id } })
616
+ const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device')
617
+ const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref')
618
+ const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64')
619
+ const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64')
620
+ const advB64 = creds.advSecretKey
621
+ let qrMs = qrTimeout || 60000
622
+ const genPairQR = () => {
623
+ if (!ws.isOpen) return
624
+ const refNode = refNodes.shift()
625
+ if (!refNode) { void end(new Boom('QR refs attempts ended', { statusCode: DisconnectReason.timedOut })); return }
626
+ const ref = refNode.content.toString('utf-8')
627
+ const qr = buildPairingQRData(ref, noiseKeyB64, identityKeyB64, advB64, browser)
628
+ ev.emit('connection.update', { qr })
629
+ qrTimer = setTimeout(genPairQR, qrMs)
630
+ qrMs = qrTimeout || 20000
631
+ }
632
+ genPairQR()
633
+ })
634
+
635
+ ws.on('CB:iq,,pair-success', async (stanza) => {
636
+ logger.debug('Pair success received')
637
+ try {
638
+ updateServerTimeOffset(stanza)
639
+ const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
640
+ logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'Pairing configured successfully')
641
+ ev.emit('creds.update', updatedCreds)
642
+ ev.emit('connection.update', { isNewLogin: true, qr: undefined })
643
+ await sendNode(reply)
644
+ void sendUnifiedSession()
645
+ } catch (error) {
646
+ logger.info({ trace: error.stack }, 'Error in pairing')
647
+ void end(error)
648
+ }
649
+ })
650
+
651
+ // ─── Login complete ──────────────────────────────────────────────────────────
652
+
653
+ ws.on('CB:success', async (node) => {
654
+ try {
655
+ updateServerTimeOffset(node)
656
+ await uploadPreKeysToServerIfRequired()
657
+ await sendPassiveIq('active')
658
+ try { await digestKeyBundle() }
659
+ catch (e) { logger.warn({ e }, 'failed to run digest after login') }
660
+ } catch (err) {
661
+ logger.warn({ err }, 'Failed to send initial passive IQ')
662
+ }
663
+ logger.info('✅ Opened connection to WhatsApp')
664
+ clearTimeout(qrTimer)
665
+ ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } })
666
+ ev.emit('connection.update', { connection: 'open' })
667
+ void sendUnifiedSession()
668
+ if (node.attrs.lid && authState.creds.me?.id) {
669
+ const myLID = node.attrs.lid
670
+ process.nextTick(async () => {
671
+ try {
672
+ const myPN = authState.creds.me.id
673
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: myLID, pn: myPN }])
674
+ const { user, device } = jidDecode(myPN)
675
+ await authState.keys.set({ 'device-list': { [user]: [device?.toString() || '0'] } })
676
+ await signalRepository.migrateSession(myPN, myLID)
677
+ logger.info({ myPN, myLID }, 'Own LID session created successfully')
678
+ if (signalRepository.lidMapping.getLIDPNMappings) {
679
+ try {
680
+ const allMappings = await signalRepository.lidMapping.getLIDPNMappings()
681
+ const others = (allMappings || []).filter(m => m.pn !== myPN && m.lid && m.pn)
682
+ let migrated = 0
683
+ for (const { pn, lid } of others) {
684
+ try { await signalRepository.migrateSession(pn, lid); migrated++ }
685
+ catch (sessErr) { logger.warn({ error: sessErr, pn, lid }, 'Failed to migrate individual PN session to LID') }
686
+ }
687
+ if (migrated > 0) logger.info({ migrated }, 'Batch-migrated PN sessions to LID on connect')
688
+ } catch (migErr) {
689
+ logger.warn({ error: migErr }, 'Failed to batch-migrate PN sessions to LID')
690
+ }
691
+ }
692
+ } catch (error) {
693
+ logger.error({ error, lid: myLID }, 'Failed to create own LID session')
694
+ }
695
+ })
696
+ }
697
+ })
698
+
699
+ // ─── Stream / connection error handlers ─────────────────────────────────────
700
+
701
+ ws.on('CB:stream:error', (node) => {
702
+ const [reasonNode] = getAllBinaryNodeChildren(node)
703
+ logger.error({ reasonNode, fullErrorNode: node }, 'Stream errored out')
704
+ const { reason, statusCode } = getErrorCodeFromStreamError(node)
705
+ void end(new Boom(`Stream Errored (${reason})`, { statusCode, data: reasonNode || node }))
706
+ })
707
+
708
+ ws.on('CB:failure', (node) => {
709
+ const reason = +(node.attrs.reason || 500)
710
+ void end(new Boom('Connection Failure', { statusCode: reason, data: node.attrs }))
711
+ })
712
+
713
+ ws.on('CB:ib,,downgrade_webclient', () =>
714
+ void end(new Boom('Multi-device beta not joined', { statusCode: DisconnectReason.multideviceMismatch }))
715
+ )
716
+
717
+ ws.on('CB:ib,,offline_preview', async (node) => {
718
+ logger.info('Offline preview received', JSON.stringify(node))
719
+ await sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] })
720
+ })
721
+
722
+ ws.on('CB:ib,,edge_routing', (node) => {
723
+ const edgeRoutingNode = getBinaryNodeChild(node, 'edge_routing')
724
+ const routingInfo = getBinaryNodeChild(edgeRoutingNode, 'routing_info')
725
+ if (routingInfo?.content) {
726
+ authState.creds.routingInfo = Buffer.from(routingInfo.content)
727
+ ev.emit('creds.update', authState.creds)
728
+ }
729
+ })
730
+
731
+ // ─── Buffering & offline notifications ──────────────────────────────────────
732
+
733
+ process.nextTick(() => {
734
+ if (creds.me?.id) { ev.buffer(); didStartBuffer = true }
735
+ ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined })
736
+ })
737
+
738
+ ws.on('CB:ib,,offline', (node) => {
739
+ const child = getBinaryNodeChild(node, 'offline')
740
+ const offlineNotifs = +(child?.attrs.count || 0)
741
+ logger.info(`Handled ${offlineNotifs} offline messages/notifications`)
742
+ if (didStartBuffer) { ev.flush(); logger.trace('Flushed events for initial buffer') }
743
+ ev.emit('connection.update', { receivedPendingNotifications: true })
744
+ })
745
+
746
+ // ─── Creds sync ──────────────────────────────────────────────────────────────
747
+
748
+ ev.on('creds.update', (update) => {
749
+ const name = update.me?.name
750
+ if (creds.me?.name !== name) {
751
+ logger.debug({ name }, 'Updated pushName')
752
+ sendNode({ tag: 'presence', attrs: { name } }).catch((err) =>
753
+ logger.warn({ trace: err.stack }, 'Error in sending presence update on name change')
754
+ )
755
+ }
756
+ Object.assign(creds, update)
757
+ })
758
+
759
+ // ─── Public API ──────────────────────────────────────────────────────────────
760
+
761
+ return {
762
+ type: 'md',
763
+ ws,
764
+ ev,
765
+ authState: { creds, keys },
766
+ signalRepository,
767
+ get user() { return authState.creds.me },
768
+ generateMessageTag,
769
+ query,
770
+ waitForMessage,
771
+ waitForSocketOpen,
772
+ sendRawMessage,
773
+ sendNode,
774
+ logout,
775
+ end,
776
+ registerSocketEndHandler,
777
+ onUnexpectedError,
778
+ uploadPreKeys,
779
+ uploadPreKeysToServerIfRequired,
780
+ digestKeyBundle,
781
+ rotateSignedPreKey,
782
+ updateServerTimeOffset,
783
+ sendUnifiedSession,
784
+ requestPairingCode,
785
+ wamBuffer: publicWAMBuffer,
786
+ waitForConnectionUpdate: bindWaitForConnectionUpdate(ev),
787
+ sendWAMBuffer,
788
+ executeUSyncQuery,
789
+ onWhatsApp,
790
+ fetchAccountReachoutTimelock,
791
+ fetchNewChatMessageCap,
792
+ listener: (eventName) => {
793
+ if (typeof ev.listenerCount === 'function') return ev.listenerCount(eventName)
794
+ if (typeof ev.listener === 'function') return ev.listener(eventName)?.length || 0
795
+ return 0
796
+ }
797
+ }
798
+ }