Path: blob/master/node_modules/@adiwajshing/baileys/lib/Socket/socket.js
2593 views
"use strict";1var __importDefault = (this && this.__importDefault) || function (mod) {2return (mod && mod.__esModule) ? mod : { "default": mod };3};4Object.defineProperty(exports, "__esModule", { value: true });5exports.makeSocket = void 0;6const boom_1 = require("@hapi/boom");7const crypto_1 = require("crypto");8const events_1 = __importDefault(require("events"));9const util_1 = require("util");10const ws_1 = __importDefault(require("ws"));11const WAProto_1 = require("../../WAProto");12const Defaults_1 = require("../Defaults");13const Types_1 = require("../Types");14const Utils_1 = require("../Utils");15const WABinary_1 = require("../WABinary");16/**17* Connects to WA servers and performs:18* - simple queries (no retry mechanism, wait for connection establishment)19* - listen to messages and emit events20* - query phone connection21*/22const makeSocket = ({ waWebSocketUrl, connectTimeoutMs, logger, agent, keepAliveIntervalMs, version, browser, auth: initialAuthState, printQRInTerminal, defaultQueryTimeoutMs }) => {23const ws = new ws_1.default(waWebSocketUrl, undefined, {24origin: Defaults_1.DEFAULT_ORIGIN,25timeout: connectTimeoutMs,26agent,27headers: {28'Accept-Encoding': 'gzip, deflate, br',29'Accept-Language': 'en-US,en;q=0.9',30'Cache-Control': 'no-cache',31'Host': 'web.whatsapp.com',32'Pragma': 'no-cache',33'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits'34}35});36ws.setMaxListeners(0);37const ev = new events_1.default();38/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */39const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();40/** WA noise protocol wrapper */41const noise = Utils_1.makeNoiseHandler(ephemeralKeyPair);42let authState = initialAuthState;43if (!authState) {44authState = Utils_1.useSingleFileAuthState('./auth-info-multi.json').state;45logger.warn(`46Baileys just created a single file state for your credentials.47This will not be supported soon.48Please pass the credentials in the config itself49`);50}51const { creds } = authState;52let lastDateRecv;53let epoch = 0;54let keepAliveReq;55let qrTimer;56const uqTagId = `${crypto_1.randomBytes(1).toString('hex')[0]}.${crypto_1.randomBytes(1).toString('hex')[0]}-`;57const generateMessageTag = () => `${uqTagId}${epoch++}`;58const sendPromise = util_1.promisify(ws.send);59/** send a raw buffer */60const sendRawMessage = async (data) => {61if (ws.readyState !== ws.OPEN) {62throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });63}64const bytes = noise.encodeFrame(data);65await sendPromise.call(ws, bytes);66};67/** send a binary node */68const sendNode = (node) => {69const buff = WABinary_1.encodeBinaryNode(node);70return sendRawMessage(buff);71};72/** await the next incoming message */73const awaitNextMessage = async (sendMsg) => {74if (ws.readyState !== ws.OPEN) {75throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });76}77let onOpen;78let onClose;79const result = new Promise((resolve, reject) => {80onOpen = (data) => resolve(data);81onClose = reject;82ws.on('frame', onOpen);83ws.on('close', onClose);84ws.on('error', onClose);85})86.finally(() => {87ws.off('frame', onOpen);88ws.off('close', onClose);89ws.off('error', onClose);90});91if (sendMsg) {92sendRawMessage(sendMsg).catch(onClose);93}94return result;95};96/**97* Wait for a message with a certain tag to be received98* @param tag the message tag to await99* @param json query that was sent100* @param timeoutMs timeout after which the promise will reject101*/102const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {103let onRecv;104let onErr;105try {106const result = await Utils_1.promiseTimeout(timeoutMs, (resolve, reject) => {107onRecv = resolve;108onErr = err => {109reject(err || new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }));110};111ws.on(`TAG:${msgId}`, onRecv);112ws.on('close', onErr); // if the socket closes, you'll never receive the message113ws.off('error', onErr);114});115return result;116}117finally {118ws.off(`TAG:${msgId}`, onRecv);119ws.off('close', onErr); // if the socket closes, you'll never receive the message120ws.off('error', onErr);121}122};123/** send a query, and wait for its response. auto-generates message ID if not provided */124const query = async (node, timeoutMs) => {125if (!node.attrs.id) {126node.attrs.id = generateMessageTag();127}128const msgId = node.attrs.id;129const wait = waitForMessage(msgId, timeoutMs);130await sendNode(node);131const result = await wait;132if ('tag' in result) {133WABinary_1.assertNodeErrorFree(result);134}135return result;136};137/** connection handshake */138const validateConnection = async () => {139logger.info('connected to WA Web');140const init = WAProto_1.proto.HandshakeMessage.encode({141clientHello: { ephemeral: ephemeralKeyPair.public }142}).finish();143const result = await awaitNextMessage(init);144const handshake = WAProto_1.proto.HandshakeMessage.decode(result);145logger.debug('handshake recv from WA Web');146const keyEnc = noise.processHandshake(handshake, creds.noiseKey);147logger.info('handshake complete');148let node;149if (!creds.me) {150logger.info('not logged in, attempting registration...');151node = Utils_1.generateRegistrationNode(creds, { version, browser });152}153else {154logger.info('logging in...');155node = Utils_1.generateLoginNode(creds.me.id, { version, browser });156}157const payloadEnc = noise.encrypt(node);158await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({159clientFinish: {160static: new Uint8Array(keyEnc),161payload: new Uint8Array(payloadEnc),162},163}).finish());164noise.finishInit();165startKeepAliveRequest();166};167/** get some pre-keys and do something with them */168const assertingPreKeys = async (range, execute) => {169const { newPreKeys, lastPreKeyId, preKeysRange } = Utils_1.generateOrGetPreKeys(authState.creds, range);170const update = {171nextPreKeyId: Math.max(lastPreKeyId + 1, creds.nextPreKeyId),172firstUnuploadedPreKeyId: Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId + 1)173};174if (!creds.serverHasPreKeys) {175update.serverHasPreKeys = true;176}177await authState.keys.set({ 'pre-key': newPreKeys });178const preKeys = await Utils_1.getPreKeys(authState.keys, preKeysRange[0], preKeysRange[0] + preKeysRange[1]);179await execute(preKeys);180ev.emit('creds.update', update);181};182/** generates and uploads a set of pre-keys */183const uploadPreKeys = async () => {184await assertingPreKeys(30, async (preKeys) => {185const node = {186tag: 'iq',187attrs: {188id: generateMessageTag(),189xmlns: 'encrypt',190type: 'set',191to: WABinary_1.S_WHATSAPP_NET,192},193content: [194{ tag: 'registration', attrs: {}, content: Utils_1.encodeBigEndian(creds.registrationId) },195{ tag: 'type', attrs: {}, content: Defaults_1.KEY_BUNDLE_TYPE },196{ tag: 'identity', attrs: {}, content: creds.signedIdentityKey.public },197{ tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => Utils_1.xmppPreKey(preKeys[+k], +k)) },198Utils_1.xmppSignedPreKey(creds.signedPreKey)199]200};201await sendNode(node);202logger.info('uploaded pre-keys');203});204};205const onMessageRecieved = (data) => {206noise.decodeFrame(data, frame => {207var _a;208ws.emit('frame', frame);209// if it's a binary node210if (!(frame instanceof Uint8Array)) {211const msgId = frame.attrs.id;212if (logger.level === 'trace') {213logger.trace({ msgId, fromMe: false, frame }, 'communication');214}215let anyTriggered = false;216/* Check if this is a response to a message we sent */217anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame);218/* Check if this is a response to a message we are expecting */219const l0 = frame.tag;220const l1 = frame.attrs || {};221const l2 = Array.isArray(frame.content) ? (_a = frame.content[0]) === null || _a === void 0 ? void 0 : _a.tag : '';222Object.keys(l1).forEach(key => {223anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;224anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;225anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;226});227anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;228anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;229anyTriggered = ws.emit('frame', frame) || anyTriggered;230if (!anyTriggered && logger.level === 'debug') {231logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');232}233}234});235};236const end = (error) => {237logger.info({ error }, 'connection closed');238clearInterval(keepAliveReq);239clearInterval(qrTimer);240ws.removeAllListeners('close');241ws.removeAllListeners('error');242ws.removeAllListeners('open');243ws.removeAllListeners('message');244if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {245try {246ws.close();247}248catch (_a) { }249}250ev.emit('connection.update', {251connection: 'close',252lastDisconnect: {253error,254date: new Date()255}256});257ev.removeAllListeners('connection.update');258};259const waitForSocketOpen = async () => {260if (ws.readyState === ws.OPEN) {261return;262}263if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {264throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });265}266let onOpen;267let onClose;268await new Promise((resolve, reject) => {269onOpen = () => resolve(undefined);270onClose = reject;271ws.on('open', onOpen);272ws.on('close', onClose);273ws.on('error', onClose);274})275.finally(() => {276ws.off('open', onOpen);277ws.off('close', onClose);278ws.off('error', onClose);279});280};281const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {282if (!lastDateRecv) {283lastDateRecv = new Date();284}285const diff = Date.now() - lastDateRecv.getTime();286/*287check if it's been a suspicious amount of time since the server responded with our last seen288it could be that the network is down289*/290if (diff > keepAliveIntervalMs + 5000) {291end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost }));292}293else if (ws.readyState === ws.OPEN) {294// if its all good, send a keep alive request295query({296tag: 'iq',297attrs: {298id: generateMessageTag(),299to: WABinary_1.S_WHATSAPP_NET,300type: 'get',301xmlns: 'w:p',302},303content: [{ tag: 'ping', attrs: {} }]304}, keepAliveIntervalMs)305.then(() => {306lastDateRecv = new Date();307logger.trace('recv keep alive');308})309.catch(err => end(err));310}311else {312logger.warn('keep alive called when WS not open');313}314}, keepAliveIntervalMs));315/** i have no idea why this exists. pls enlighten me */316const sendPassiveIq = (tag) => (sendNode({317tag: 'iq',318attrs: {319to: WABinary_1.S_WHATSAPP_NET,320xmlns: 'passive',321type: 'set',322id: generateMessageTag(),323},324content: [325{ tag, attrs: {} }326]327}));328/** logout & invalidate connection */329const logout = async () => {330var _a;331const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;332if (jid) {333await sendNode({334tag: 'iq',335attrs: {336to: WABinary_1.S_WHATSAPP_NET,337type: 'set',338id: generateMessageTag(),339xmlns: 'md'340},341content: [342{343tag: 'remove-companion-device',344attrs: {345jid: jid,346reason: 'user_initiated'347}348}349]350});351}352end(new boom_1.Boom('Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut }));353};354ws.on('message', onMessageRecieved);355ws.on('open', validateConnection);356ws.on('error', end);357ws.on('close', () => end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed })));358// the server terminated the connection359ws.on('CB:xmlstreamend', () => {360end(new boom_1.Boom('Connection Terminated by Server', { statusCode: Types_1.DisconnectReason.connectionClosed }));361});362// QR gen363ws.on('CB:iq,type:set,pair-device', async (stanza) => {364const iq = {365tag: 'iq',366attrs: {367to: WABinary_1.S_WHATSAPP_NET,368type: 'result',369id: stanza.attrs.id,370}371};372await sendNode(iq);373const refs = stanza.content[0].content.map(n => n.content);374const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');375const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');376const advB64 = creds.advSecretKey;377let qrMs = 60000; // time to let a QR live378const genPairQR = () => {379const ref = refs.shift();380if (!ref) {381end(new boom_1.Boom('QR refs attempts ended', { statusCode: Types_1.DisconnectReason.timedOut }));382return;383}384const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',');385ev.emit('connection.update', { qr });386qrTimer = setTimeout(genPairQR, qrMs);387qrMs = 20000; // shorter subsequent qrs388};389genPairQR();390});391// device paired for the first time392// if device pairs successfully, the server asks to restart the connection393ws.on('CB:iq,,pair-success', async (stanza) => {394var _a;395logger.debug('pair success recv');396try {397const { reply, creds: updatedCreds } = Utils_1.configureSuccessfulPairing(stanza, creds);398logger.debug('pairing configured successfully');399const waiting = awaitNextMessage();400await sendNode(reply);401const value = (await waiting);402if (value.tag === 'stream:error') {403if (((_a = value.attrs) === null || _a === void 0 ? void 0 : _a.code) !== '515') {404throw new boom_1.Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) });405}406}407logger.info({ jid: updatedCreds.me.id }, 'registered connection, restart server');408ev.emit('creds.update', updatedCreds);409ev.emit('connection.update', { isNewLogin: true, qr: undefined });410end(new boom_1.Boom('Restart Required', { statusCode: Types_1.DisconnectReason.restartRequired }));411}412catch (error) {413logger.info({ trace: error.stack }, 'error in pairing');414end(error);415}416});417// login complete418ws.on('CB:success', async () => {419if (!creds.serverHasPreKeys) {420await uploadPreKeys();421}422await sendPassiveIq('active');423logger.info('opened connection to WA');424clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try425ev.emit('connection.update', { connection: 'open' });426});427ws.on('CB:ib,,offline', (node) => {428const child = WABinary_1.getBinaryNodeChild(node, 'offline');429const offlineCount = +child.attrs.count;430logger.info(`got ${offlineCount} offline messages/notifications`);431ev.emit('connection.update', { receivedPendingNotifications: true });432});433ws.on('CB:stream:error', (node) => {434logger.error({ error: node }, 'stream errored out');435const statusCode = +(node.attrs.code || Types_1.DisconnectReason.restartRequired);436end(new boom_1.Boom('Stream Errored', { statusCode, data: node }));437});438// stream fail, possible logout439ws.on('CB:failure', (node) => {440const reason = +(node.attrs.reason || 500);441end(new boom_1.Boom('Connection Failure', { statusCode: reason, data: node.attrs }));442});443ws.on('CB:ib,,downgrade_webclient', () => {444end(new boom_1.Boom('Multi-device beta not joined', { statusCode: Types_1.DisconnectReason.multideviceMismatch }));445});446process.nextTick(() => {447ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });448});449// update credentials when required450ev.on('creds.update', update => Object.assign(creds, update));451if (printQRInTerminal) {452Utils_1.printQRIfNecessaryListener(ev, logger);453}454return {455type: 'md',456ws,457ev,458authState: {459creds,460// add capability461keys: Utils_1.addTransactionCapability(authState.keys, logger)462},463get user() {464return authState.creds.me;465},466assertingPreKeys,467generateMessageTag,468query,469waitForMessage,470waitForSocketOpen,471sendRawMessage,472sendNode,473logout,474end,475/** Waits for the connection to WA to reach a state */476waitForConnectionUpdate: Utils_1.bindWaitForConnectionUpdate(ev)477};478};479exports.makeSocket = makeSocket;480481482