Path: blob/master/node_modules/@adiwajshing/baileys/lib/Socket/socket.js
1129 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 util_1 = require("util");8const ws_1 = __importDefault(require("ws"));9const WAProto_1 = require("../../WAProto");10const Defaults_1 = require("../Defaults");11const Types_1 = require("../Types");12const Utils_1 = require("../Utils");13const event_buffer_1 = require("../Utils/event-buffer");14const WABinary_1 = require("../WABinary");15/**16* Connects to WA servers and performs:17* - simple queries (no retry mechanism, wait for connection establishment)18* - listen to messages and emit events19* - query phone connection20*/21const makeSocket = ({ waWebSocketUrl, connectTimeoutMs, logger, agent, keepAliveIntervalMs, version, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, syncFullHistory, transactionOpts, qrTimeout }) => {22const ws = new ws_1.default(waWebSocketUrl, undefined, {23origin: Defaults_1.DEFAULT_ORIGIN,24handshakeTimeout: connectTimeoutMs,25timeout: connectTimeoutMs,26agent27});28ws.setMaxListeners(0);29const ev = (0, event_buffer_1.makeEventBuffer)(logger);30/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */31const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();32/** WA noise protocol wrapper */33const noise = (0, Utils_1.makeNoiseHandler)(ephemeralKeyPair, logger);34const { creds } = authState;35// add transaction capability36const keys = (0, Utils_1.addTransactionCapability)(authState.keys, logger, transactionOpts);37let lastDateRecv;38let epoch = 1;39let keepAliveReq;40let qrTimer;41let closed = false;42const uqTagId = (0, Utils_1.generateMdTagPrefix)();43const generateMessageTag = () => `${uqTagId}${epoch++}`;44const sendPromise = (0, util_1.promisify)(ws.send);45/** send a raw buffer */46const sendRawMessage = async (data) => {47if (ws.readyState !== ws.OPEN) {48throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });49}50const bytes = noise.encodeFrame(data);51await sendPromise.call(ws, bytes);52};53/** send a binary node */54const sendNode = (frame) => {55if (logger.level === 'trace') {56logger.trace({ msgId: frame.attrs.id, fromMe: true, frame }, 'communication');57}58const buff = (0, WABinary_1.encodeBinaryNode)(frame);59return sendRawMessage(buff);60};61/** log & process any unexpected errors */62const onUnexpectedError = (error, msg) => {63logger.error({ trace: error.stack, output: error.output }, `unexpected error in '${msg}'`);64};65/** await the next incoming message */66const awaitNextMessage = async (sendMsg) => {67if (ws.readyState !== ws.OPEN) {68throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });69}70let onOpen;71let onClose;72const result = (0, Utils_1.promiseTimeout)(connectTimeoutMs, (resolve, reject) => {73onOpen = (data) => resolve(data);74onClose = reject;75ws.on('frame', onOpen);76ws.on('close', onClose);77ws.on('error', onClose);78})79.finally(() => {80ws.off('frame', onOpen);81ws.off('close', onClose);82ws.off('error', onClose);83});84if (sendMsg) {85sendRawMessage(sendMsg).catch(onClose);86}87return result;88};89/**90* Wait for a message with a certain tag to be received91* @param tag the message tag to await92* @param json query that was sent93* @param timeoutMs timeout after which the promise will reject94*/95const waitForMessage = async (msgId, timeoutMs = defaultQueryTimeoutMs) => {96let onRecv;97let onErr;98try {99const result = await (0, Utils_1.promiseTimeout)(timeoutMs, (resolve, reject) => {100onRecv = resolve;101onErr = err => {102reject(err || new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed }));103};104ws.on(`TAG:${msgId}`, onRecv);105ws.on('close', onErr); // if the socket closes, you'll never receive the message106ws.off('error', onErr);107});108return result;109}110finally {111ws.off(`TAG:${msgId}`, onRecv);112ws.off('close', onErr); // if the socket closes, you'll never receive the message113ws.off('error', onErr);114}115};116/** send a query, and wait for its response. auto-generates message ID if not provided */117const query = async (node, timeoutMs) => {118if (!node.attrs.id) {119node.attrs.id = generateMessageTag();120}121const msgId = node.attrs.id;122const wait = waitForMessage(msgId, timeoutMs);123await sendNode(node);124const result = await wait;125if ('tag' in result) {126(0, WABinary_1.assertNodeErrorFree)(result);127}128return result;129};130/** connection handshake */131const validateConnection = async () => {132let helloMsg = {133clientHello: { ephemeral: ephemeralKeyPair.public }134};135helloMsg = WAProto_1.proto.HandshakeMessage.fromObject(helloMsg);136logger.info({ browser, helloMsg }, 'connected to WA Web');137const init = WAProto_1.proto.HandshakeMessage.encode(helloMsg).finish();138const result = await awaitNextMessage(init);139const handshake = WAProto_1.proto.HandshakeMessage.decode(result);140logger.trace({ handshake }, 'handshake recv from WA Web');141const keyEnc = noise.processHandshake(handshake, creds.noiseKey);142const config = { version, browser, syncFullHistory };143let node;144if (!creds.me) {145node = (0, Utils_1.generateRegistrationNode)(creds, config);146logger.info({ node }, 'not logged in, attempting registration...');147}148else {149node = (0, Utils_1.generateLoginNode)(creds.me.id, config);150logger.info({ node }, 'logging in...');151}152const payloadEnc = noise.encrypt(WAProto_1.proto.ClientPayload.encode(node).finish());153await sendRawMessage(WAProto_1.proto.HandshakeMessage.encode({154clientFinish: {155static: keyEnc,156payload: payloadEnc,157},158}).finish());159noise.finishInit();160startKeepAliveRequest();161};162const getAvailablePreKeysOnServer = async () => {163const result = await query({164tag: 'iq',165attrs: {166id: generateMessageTag(),167xmlns: 'encrypt',168type: 'get',169to: WABinary_1.S_WHATSAPP_NET170},171content: [172{ tag: 'count', attrs: {} }173]174});175const countChild = (0, WABinary_1.getBinaryNodeChild)(result, 'count');176return +countChild.attrs.value;177};178/** generates and uploads a set of pre-keys to the server */179const uploadPreKeys = async (count = Defaults_1.INITIAL_PREKEY_COUNT) => {180await keys.transaction(async () => {181logger.info({ count }, 'uploading pre-keys');182const { update, node } = await (0, Utils_1.getNextPreKeysNode)({ creds, keys }, count);183await query(node);184ev.emit('creds.update', update);185logger.info({ count }, 'uploaded pre-keys');186});187};188const uploadPreKeysToServerIfRequired = async () => {189const preKeyCount = await getAvailablePreKeysOnServer();190logger.info(`${preKeyCount} pre-keys found on server`);191if (preKeyCount <= Defaults_1.MIN_PREKEY_COUNT) {192await uploadPreKeys();193}194};195const onMessageRecieved = (data) => {196noise.decodeFrame(data, frame => {197var _a;198// reset ping timeout199lastDateRecv = new Date();200let anyTriggered = false;201anyTriggered = ws.emit('frame', frame);202// if it's a binary node203if (!(frame instanceof Uint8Array)) {204const msgId = frame.attrs.id;205if (logger.level === 'trace') {206logger.trace({ msgId, fromMe: false, frame }, 'communication');207}208/* Check if this is a response to a message we sent */209anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;210/* Check if this is a response to a message we are expecting */211const l0 = frame.tag;212const l1 = frame.attrs || {};213const l2 = Array.isArray(frame.content) ? (_a = frame.content[0]) === null || _a === void 0 ? void 0 : _a.tag : '';214Object.keys(l1).forEach(key => {215anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered;216anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered;217anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered;218});219anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered;220anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered;221if (!anyTriggered && logger.level === 'debug') {222logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');223}224}225});226};227const end = (error) => {228if (closed) {229logger.trace({ trace: error === null || error === void 0 ? void 0 : error.stack }, 'connection already closed');230return;231}232closed = true;233logger.info({ trace: error === null || error === void 0 ? void 0 : error.stack }, error ? 'connection errored' : 'connection closed');234clearInterval(keepAliveReq);235clearTimeout(qrTimer);236ws.removeAllListeners('close');237ws.removeAllListeners('error');238ws.removeAllListeners('open');239ws.removeAllListeners('message');240if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {241try {242ws.close();243}244catch (_a) { }245}246ev.emit('connection.update', {247connection: 'close',248lastDisconnect: {249error,250date: new Date()251}252});253ev.removeAllListeners('connection.update');254};255const waitForSocketOpen = async () => {256if (ws.readyState === ws.OPEN) {257return;258}259if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {260throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });261}262let onOpen;263let onClose;264await new Promise((resolve, reject) => {265onOpen = () => resolve(undefined);266onClose = reject;267ws.on('open', onOpen);268ws.on('close', onClose);269ws.on('error', onClose);270})271.finally(() => {272ws.off('open', onOpen);273ws.off('close', onClose);274ws.off('error', onClose);275});276};277const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {278if (!lastDateRecv) {279lastDateRecv = new Date();280}281const diff = Date.now() - lastDateRecv.getTime();282/*283check if it's been a suspicious amount of time since the server responded with our last seen284it could be that the network is down285*/286if (diff > keepAliveIntervalMs + 5000) {287end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost }));288}289else if (ws.readyState === ws.OPEN) {290// if its all good, send a keep alive request291query({292tag: 'iq',293attrs: {294id: generateMessageTag(),295to: WABinary_1.S_WHATSAPP_NET,296type: 'get',297xmlns: 'w:p',298},299content: [{ tag: 'ping', attrs: {} }]300})301.catch(err => {302logger.error({ trace: err.stack }, 'error in sending keep alive');303});304}305else {306logger.warn('keep alive called when WS not open');307}308}, keepAliveIntervalMs));309/** i have no idea why this exists. pls enlighten me */310const sendPassiveIq = (tag) => (query({311tag: 'iq',312attrs: {313to: WABinary_1.S_WHATSAPP_NET,314xmlns: 'passive',315type: 'set',316},317content: [318{ tag, attrs: {} }319]320}));321/** logout & invalidate connection */322const logout = async () => {323var _a;324const jid = (_a = authState.creds.me) === null || _a === void 0 ? void 0 : _a.id;325if (jid) {326await sendNode({327tag: 'iq',328attrs: {329to: WABinary_1.S_WHATSAPP_NET,330type: 'set',331id: generateMessageTag(),332xmlns: 'md'333},334content: [335{336tag: 'remove-companion-device',337attrs: {338jid,339reason: 'user_initiated'340}341}342]343});344}345end(new boom_1.Boom('Intentional Logout', { statusCode: Types_1.DisconnectReason.loggedOut }));346};347ws.on('message', onMessageRecieved);348ws.on('open', validateConnection);349ws.on('error', error => end(new boom_1.Boom(`WebSocket Error (${error.message})`, { statusCode: (0, Utils_1.getCodeFromWSError)(error), data: error })));350ws.on('close', () => end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionClosed })));351// the server terminated the connection352ws.on('CB:xmlstreamend', () => end(new boom_1.Boom('Connection Terminated by Server', { statusCode: Types_1.DisconnectReason.connectionClosed })));353// QR gen354ws.on('CB:iq,type:set,pair-device', async (stanza) => {355const iq = {356tag: 'iq',357attrs: {358to: WABinary_1.S_WHATSAPP_NET,359type: 'result',360id: stanza.attrs.id,361}362};363await sendNode(iq);364const pairDeviceNode = (0, WABinary_1.getBinaryNodeChild)(stanza, 'pair-device');365const refNodes = (0, WABinary_1.getBinaryNodeChildren)(pairDeviceNode, 'ref');366const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');367const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');368const advB64 = creds.advSecretKey;369let qrMs = qrTimeout || 60000; // time to let a QR live370const genPairQR = () => {371if (ws.readyState !== ws.OPEN) {372return;373}374const refNode = refNodes.shift();375if (!refNode) {376end(new boom_1.Boom('QR refs attempts ended', { statusCode: Types_1.DisconnectReason.timedOut }));377return;378}379const ref = refNode.content.toString('utf-8');380const qr = [ref, noiseKeyB64, identityKeyB64, advB64].join(',');381ev.emit('connection.update', { qr });382qrTimer = setTimeout(genPairQR, qrMs);383qrMs = qrTimeout || 20000; // shorter subsequent qrs384};385genPairQR();386});387// device paired for the first time388// if device pairs successfully, the server asks to restart the connection389ws.on('CB:iq,,pair-success', async (stanza) => {390logger.debug('pair success recv');391try {392const { reply, creds: updatedCreds } = (0, Utils_1.configureSuccessfulPairing)(stanza, creds);393logger.info({ me: updatedCreds.me, platform: updatedCreds.platform }, 'pairing configured successfully, expect to restart the connection...');394ev.emit('creds.update', updatedCreds);395ev.emit('connection.update', { isNewLogin: true, qr: undefined });396await sendNode(reply);397}398catch (error) {399logger.info({ trace: error.stack }, 'error in pairing');400end(error);401}402});403// login complete404ws.on('CB:success', async () => {405await uploadPreKeysToServerIfRequired();406await sendPassiveIq('active');407logger.info('opened connection to WA');408clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try409ev.emit('connection.update', { connection: 'open' });410});411ws.on('CB:stream:error', (node) => {412logger.error({ node }, 'stream errored out');413const { reason, statusCode } = (0, Utils_1.getErrorCodeFromStreamError)(node);414end(new boom_1.Boom(`Stream Errored (${reason})`, { statusCode, data: node }));415});416// stream fail, possible logout417ws.on('CB:failure', (node) => {418const reason = +(node.attrs.reason || 500);419end(new boom_1.Boom('Connection Failure', { statusCode: reason, data: node.attrs }));420});421ws.on('CB:ib,,downgrade_webclient', () => {422end(new boom_1.Boom('Multi-device beta not joined', { statusCode: Types_1.DisconnectReason.multideviceMismatch }));423});424process.nextTick(() => {425// start buffering important events426ev.buffer();427ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false, qr: undefined });428});429// update credentials when required430ev.on('creds.update', update => {431var _a, _b;432const name = (_a = update.me) === null || _a === void 0 ? void 0 : _a.name;433// if name has just been received434if (((_b = creds.me) === null || _b === void 0 ? void 0 : _b.name) !== name) {435logger.info({ name }, 'updated pushName');436sendNode({437tag: 'presence',438attrs: { name: name }439})440.catch(err => {441logger.warn({ trace: err.stack }, 'error in sending presence update on name change');442});443}444Object.assign(creds, update);445});446if (printQRInTerminal) {447(0, Utils_1.printQRIfNecessaryListener)(ev, logger);448}449return {450type: 'md',451ws,452ev,453authState: { creds, keys },454get user() {455return authState.creds.me;456},457generateMessageTag,458query,459waitForMessage,460waitForSocketOpen,461sendRawMessage,462sendNode,463logout,464end,465onUnexpectedError,466uploadPreKeys,467/** Waits for the connection to WA to reach a state */468waitForConnectionUpdate: (0, Utils_1.bindWaitForConnectionUpdate)(ev),469};470};471exports.makeSocket = makeSocket;472473474