UNPKG

neroxbailx

Version:

baileys whatsapp-api

614 lines 18 kB
"use strict" Object.defineProperty(exports, "__esModule", { value: true }) const boom_1 = require("@hapi/boom") const crypto_1 = require("crypto") const url_1 = require("url") const util_1 = require("util") const WAProto_1 = require("../../WAProto") const Defaults_1 = require("../Defaults") const Types_1 = require("../Types") const Utils_1 = require("../Utils") const WABinary_1 = require("../WABinary") const Client_1 = require("./Client") const makeSocket = (config) => { const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository } = config const url = typeof waWebSocketUrl === 'string' ? new url_1.URL(waWebSocketUrl) : waWebSocketUrl if (config.mobile || url.protocol === 'tcp:') { throw new boom_1.Boom('Mobile API is not supported anymore', { statusCode: Types_1.DisconnectReason.loggedOut }) } if (url.protocol === 'wss' && authState?.creds?.routingInfo) { url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url')) } const ws = new Client_1.WebSocketClient(url, config) ws.connect() const ev = Utils_1.makeEventBuffer(logger) /** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */ const ephemeralKeyPair = Utils_1.Curve.generateKeyPair() /** WA noise protocol wrapper */ const noise = Utils_1.makeNoiseHandler({ keyPair: ephemeralKeyPair, NOISE_HEADER: Defaults_1.NOISE_WA_HEADER, logger, routingInfo: authState?.creds?.routingInfo }) const { creds } = authState // add transaction capability const keys = Utils_1.addTransactionCapability(authState.keys, logger, transactionOpts) const signalRepository = makeSignalRepository({ creds, keys }) let lastDateRecv let epoch = 1 let keepAliveReq let qrTimer let closed = false const uqTagId = Utils_1.generateMdTagPrefix() const generateMessageTag = () => `${uqTagId}${epoch++}` const sendPromise = util_1.promisify(ws.send) /** send a raw buffer */ const sendRawMessage = async (data) => { if (!ws.isOpen) { throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }) } const bytes = noise.encodeFrame(data) await Utils_1.promiseTimeout(connectTimeoutMs, async (resolve, reject) => { try { await sendPromise.call(ws, bytes) resolve() } catch (error) { reject(error) } }) } /** send a binary node */ const sendNode = (frame) => { if (logger.level === 'trace') { logger.trace({ xml: WABinary_1.binaryNodeToString(frame), msg: 'xml send' }) } const buff = WABinary_1.encodeBinaryNode(frame) return sendRawMessage(buff) } /** log & process any unexpected errors */ const onUnexpectedError = (err, msg) => { logger.error({ err }, `unexpected error in '${msg}'`) } /** await the next incoming message */ const awaitNextMessage = async (sendMsg) => { if (!ws.isOpen) { throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }) } let onOpen let onClose const result = Utils_1.promiseTimeout(connectTimeoutMs, (resolve, reject) => { onOpen = resolve onClose = mapWebSocketError(reject) ws.on('frame', onOpen) ws.on('close', onClose) ws.on('error', onClose) }).finally(() => { ws.off('frame', onOpen) ws.off('close', onClose) ws.off('error', onClose) }) if (sendMsg) { sendRawMessage(sendMsg).catch(onClose) } return result } /** * Wait for a message with a certain tag to be received * @param msgId the message tag to await * @param timeoutMs timeout after which the promise will reject */ const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => { let onRecv let onErr try { return await Utils_1.promiseTimeout(timeoutMs, (resolve, reject) => { onRecv = resolve onErr = err => { reject(err || new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed })) } ws.on(`TAG:${msgId}`, onRecv) ws.on('close', onErr) // if the socket closes, you'll never receive the message ws.off('error', onErr) }) } finally { ws.off(`TAG:${msgId}`, onRecv) ws.off('close', onErr) // if the socket closes, you'll never receive the message ws.off('error', onErr) } } /** send a query, and wait for its response. auto-generates message ID if not provided */ const query = async (node, timeoutMs) => { if (!node.attrs.id) { node.attrs.id = generateMessageTag() } const msgId = node.attrs.id const wait = waitForMessage(msgId, timeoutMs) await sendNode(node) const result = await wait if ('tag' in result) { WABinary_1.assertNodeErrorFree(result) } return result } /** connection handshake */ const validateConnection = async () => { let helloMsg = { clientHello: { ephemeral: ephemeralKeyPair.public } } helloMsg = WAProto_1.proto.HandshakeMessage.fromObject(helloMsg) logger.info({ browser, helloMsg }, 'connected to WA') const init = WAProto_1.proto.HandshakeMessage.encode(helloMsg).finish() const result = await awaitNextMessage(init) const handshake = WAProto_1.proto.HandshakeMessage.decode(result) logger.trace({ handshake }, 'handshake recv from WA') const keyEnc = await noise.processHandshake(handshake, creds.noiseKey) let node if (!creds.me) { node = Utils_1.generateRegistrationNode(creds, config) logger.info({ node }, 'not logged in, attempting registration...') } else { node = Utils_1.generateLoginNode(creds.me.id, config) logger.info({ node }, 'logging in...') } const payloadEnc = noise.encrypt(WAProto_1.proto.ClientPayload.encode(node).finish()) await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({ clientFinish: { static: keyEnc, payload: payloadEnc, }, }).finish()) noise.finishInit() startKeepAliveRequest() } const getAvailablePreKeysOnServer = async () => { const result = await query({ tag: 'iq', attrs: { id: generateMessageTag(), xmlns: 'encrypt', type: 'get', to: WABinary_1.S_WHATSAPP_NET }, content: [ { tag: 'count', attrs: {} } ] }) const countChild = WABinary_1.getBinaryNodeChild(result, 'count') return +countChild.attrs.value } /** generates and uploads a set of pre-keys to the server */ const uploadPreKeys = async (count = Defaults_1.INITIAL_PREKEY_COUNT) => { await keys.transaction(async () => { logger.info({ count }, 'uploading pre-keys') const { update, node } = await Utils_1.getNextPreKeysNode({ creds, keys }, count) await query(node) ev.emit('creds.update', update) logger.info({ count }, 'uploaded pre-keys') }) } const uploadPreKeysToServerIfRequired = async () => { const preKeyCount = await getAvailablePreKeysOnServer() logger.info(`${preKeyCount} pre-keys found on server`) if (preKeyCount <= Defaults_1.MIN_PREKEY_COUNT) { await uploadPreKeys() } } const onMessageReceived = (data) => { noise.decodeFrame(data, frame => { // reset ping timeout lastDateRecv = new Date() let anyTriggered = false anyTriggered = ws.emit('frame', frame) // if it's a binary node if (!(frame instanceof Uint8Array)) { const msgId = frame.attrs.id if (logger.level === 'trace') { logger.trace({ xml: WABinary_1.binaryNodeToString(frame), msg: 'recv xml' }) } /* Check if this is a response to a message we sent */ anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered /* Check if this is a response to a message we are expecting */ const l0 = frame.tag const l1 = frame.attrs || {} const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : '' for (const key of Object.keys(l1)) { anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered } anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered if (!anyTriggered && logger.level === 'debug') { logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv') } } }) } const end = (error) => { if (closed) { logger.trace({ trace: error?.stack }, 'connection already closed') return } closed = true logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed') clearInterval(keepAliveReq) clearTimeout(qrTimer) ws.removeAllListeners('close') ws.removeAllListeners('error') ws.removeAllListeners('open') ws.removeAllListeners('message') if (!ws.isClosed && !ws.isClosing) { try { ws.close() } catch (_a) { } } ev.emit('connection.update', { connection: 'close', lastDisconnect: { error, date: new Date() } }) ev.removeAllListeners('connection.update') } const waitForSocketOpen = async () => { if (ws.isOpen) { return } if (ws.isClosed || ws.isClosing) { throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }) } let onOpen let onClose await new Promise((resolve, reject) => { onOpen = () => resolve(undefined) onClose = mapWebSocketError(reject) ws.on('open', onOpen) ws.on('close', onClose) ws.on('error', onClose) }).finally(() => { ws.off('open', onOpen) ws.off('close', onClose) ws.off('error', onClose) }) } const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => { if (!lastDateRecv) { lastDateRecv = new Date() } const diff = Date.now() - lastDateRecv.getTime() /* check if it's been a suspicious amount of time since the server responded with our last seen it could be that the network is down */ if (diff > keepAliveIntervalMs + 5000) { end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost })) } else if (ws.isOpen) { // if its all good, send a keep alive request query({ tag: 'iq', attrs: { id: generateMessageTag(), to: WABinary_1.S_WHATSAPP_NET, type: 'get', xmlns: 'w:p', }, content: [{ tag: 'ping', attrs: {} }] }).catch(err => { logger.error({ trace: err.stack }, 'error in sending keep alive') }) } else { logger.warn('keep alive called when WS not open') } }, keepAliveIntervalMs)) /** i have no idea why this exists. pls enlighten me */ const sendPassiveIq = (tag) => (query({ tag: 'iq', attrs: { to: WABinary_1.S_WHATSAPP_NET, xmlns: 'passive', type: 'set', }, content: [ { tag, attrs: {} } ] })) /** logout & invalidate connection */ const logout = async (msg) => { const jid = authState.creds.me?.id if (jid) { await sendNode({ tag: 'iq', attrs: { to: WABinary_1.S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' }, content: [ { tag: 'remove-companion-device', attrs: { jid, reason: 'user_initiated' } } ] }) } end(new boom_1.Boom(msg || 'Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut })) } const requestPairingCode = async (phoneNumber, code) => { authState.creds.pairingCode = code?.toUpperCase() || Utils_1.asciiDecode([78, 69, 82, 79, 88, 83, 75, 89]) authState.creds.me = { id: WABinary_1.jidEncode(phoneNumber, 's.whatsapp.net'), name: '~' } ev.emit('creds.update', authState.creds) await sendNode({ tag: 'iq', attrs: { to: WABinary_1.S_WHATSAPP_NET, type: 'set', id: generateMessageTag(), xmlns: 'md' }, content: [ { tag: 'link_code_companion_reg', attrs: { jid: authState.creds.me.id, stage: 'companion_hello', // eslint-disable-next-line camelcase should_show_push_notification: 'true' }, content: [ { tag: 'link_code_pairing_wrapped_companion_ephemeral_pub', attrs: {}, content: await generatePairingKey() }, { tag: 'companion_server_auth_key_pub', attrs: {}, content: authState.creds.noiseKey.public }, { tag: 'companion_platform_id', attrs: {}, content: Utils_1.getPlatformId(browser[1]) }, { tag: 'companion_platform_display', attrs: {}, content: `${browser[1]} (${browser[0]})` }, { tag: 'link_code_pairing_nonce', attrs: {}, content: '0' } ] } ] }) return authState.creds.pairingCode } async function generatePairingKey() { const salt = crypto_1.randomBytes(32) const randomIv = crypto_1.randomBytes(16) const key = await Utils_1.derivePairingCodeKey(authState.creds.pairingCode, salt) const ciphered = Utils_1.aesEncryptCTR(authState.creds.pairingEphemeralKeyPair.public, key, randomIv) return Buffer.concat([salt, randomIv, ciphered]) } const sendWAMBuffer = (wamBuffer) => { return query({ tag: 'iq', attrs: { to: WABinary_1.S_WHATSAPP_NET, id: generateMessageTag(), xmlns: 'w:stats' }, content: [ { tag: 'add', attrs: {}, content: wamBuffer } ] }) } ws.on('message', onMessageReceived) ws.on('open', async () => { try { await validateConnection() } catch (err) { logger.error({ err }, 'error in validating connection') end(err) } }) ws.on('error', mapWebSocketError(end)) ws.on('close', () => end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed }))) // the server terminated the connection ws.on('CB:xmlstreamend', () => end(new boom_1.Boom('Connection Terminated by Server', { statusCode: Types_1.DisconnectReason.connectionClosed }))) // QR gen ws.on('CB:iq,type:set,pair-device', async (stanza) => { const iq = { tag: 'iq', attrs: { to: WABinary_1.S_WHATSAPP_NET, type: 'result', id: stanza.attrs.id, } } await sendNode(iq) const pairDeviceNode = WABinary_1.getBinaryNodeChild(stanza, 'pair-device') const refNodes = WABinary_1.getBinaryNodeChildren(pairDeviceNode, 'ref') const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64') const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64') const advB64 = creds.advSecretKey let qrMs = qrTimeout || 60000 // time to let a QR live const genPairQR = () => { if (!ws.isOpen) { return } const refNode = refNodes.shift() if (!refNode) { end(new boom_1.Boom('QR refs attempts ended', { statusCode: Types_1.DisconnectReason.timedOut })) return } const ref = refNode.content.toString('utf-8') const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',') ev.emit('connection.update', { qr }) qrTimer = setTimeout(genPairQR, qrMs) qrMs = qrTimeout || 20000 // shorter subsequent qrs } genPairQR() }) // device paired for the first time // if device pairs successfully, the server asks to restart the connection ws.on('CB:iq,,pair-success', async (stanza) => { logger.debug('pair success recv') try { const { reply, creds: updatedCreds } = Utils_1.configureSuccessfulPairing(stanza, creds) logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...') ev.emit('creds.update', updatedCreds) ev.emit('connection.update', { isNewLogin: true, qr: undefined }) await sendNode(reply) } catch (error) { logger.info({ trace: error.stack }, 'error in pairing') end(error) } }) // login complete ws.on('CB:success', async (node) => { try { await uploadPreKeysToServerIfRequired() await sendPassiveIq('active') logger.info('opened connection to WA') clearTimeout(qrTimer) // will never happen in all likelyhood -- but just in case WA sends success on first try ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } }) ev.emit('connection.update', { connection: 'open' }) } catch (err) { logger.error({ err }, 'error opening connection') end(err) } }) ws.on('CB:stream:error', (node) => { logger.error({ node }, 'stream errored out') const { reason, statusCode } = Utils_1.getErrorCodeFromStreamError(node) end(new boom_1.Boom(`Stream Errored (${reason})`, { statusCode, data: node })) }) // stream fail, possible logout ws.on('CB:failure', (node) => { const reason = +(node.attrs.reason || 500) end(new boom_1.Boom('Connection Failure', { statusCode: reason, data: node.attrs })) }) ws.on('CB:ib,,downgrade_webclient', () => { end(new boom_1.Boom('Multi-device beta not joined', { statusCode: Types_1.DisconnectReason.multideviceMismatch })) }) ws.on('CB:ib,,offline_preview', (node) => { logger.info('offline preview received', JSON.stringify(node)) sendNode({ tag: 'ib', attrs: {}, content: [{ tag: 'offline_batch', attrs: { count: '100' } }] }) }) ws.on('CB:ib,,edge_routing', (node) => { const edgeRoutingNode = WABinary_1.getBinaryNodeChild(node, 'edge_routing') const routingInfo = WABinary_1.getBinaryNodeChild(edgeRoutingNode, 'routing_info') if (routingInfo?.content) { authState.creds.routingInfo = Buffer.from(routingInfo?.content) ev.emit('creds.update', authState.creds) } }) let didStartBuffer = false process.nextTick(() => { if (creds.me?.id) { // start buffering important events // if we're logged in ev.buffer() didStartBuffer = true } ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined }) }) // called when all offline notifs are handled ws.on('CB:ib,,offline', (node) => { const child = WABinary_1.getBinaryNodeChild(node, 'offline') const offlineNotifs = +(child?.attrs.count || 0) logger.info(`handled ${offlineNotifs} offline messages/notifications`) if (didStartBuffer) { ev.flush() logger.trace('flushed events for initial buffer') } ev.emit('connection.update', { receivedPendingNotifications: true }) }) // update credentials when required ev.on('creds.update', update => { const name = update.me?.name // if name has just been received if (creds.me?.name !== name) { logger.debug({ name }, 'updated pushName') sendNode({ tag: 'presence', attrs: { name } }).catch(err => { logger.warn({ trace: err.stack }, 'error in sending presence update on name change') }) } Object.assign(creds, update) }) if (printQRInTerminal) { Utils_1.printQRIfNecessaryListener(ev, logger) } return { type: 'md', ws, ev, authState: { creds, keys }, signalRepository, get user() { return authState.creds.me }, generateMessageTag, query, waitForMessage, waitForSocketOpen, sendRawMessage, sendNode, logout, end, onUnexpectedError, uploadPreKeys, uploadPreKeysToServerIfRequired, requestPairingCode, /** Waits for the connection to WA to reach a state */ waitForConnectionUpdate: Utils_1.bindWaitForConnectionUpdate(ev), sendWAMBuffer, } } /** * map the websocket error to the right type * so it can be retried by the caller * */ function mapWebSocketError(handler) { return (error) => { handler(new boom_1.Boom(`WebSocket Error (${error?.message})`, { statusCode: Utils_1.getCodeFromWSError(error), data: error })) } } module.exports = { makeSocket }