@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.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- 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
|
+
}
|