Path: blob/master/node_modules/@adiwajshing/baileys/lib/LegacySocket/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 http_1 = require("http");8const util_1 = require("util");9const ws_1 = __importDefault(require("ws"));10const Defaults_1 = require("../Defaults");11const Types_1 = require("../Types");12const Utils_1 = require("../Utils");13const WABinary_1 = require("../WABinary");14/**15* Connects to WA servers and performs:16* - simple queries (no retry mechanism, wait for connection establishment)17* - listen to messages and emit events18* - query phone connection19*/20const makeSocket = ({ waWebSocketUrl, connectTimeoutMs, phoneResponseTimeMs, logger, agent, keepAliveIntervalMs, expectResponseTimeout, }) => {21// for generating tags22const referenceDateSeconds = (0, Utils_1.unixTimestampSeconds)(new Date());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);37let lastDateRecv;38let epoch = 0;39let authInfo;40let keepAliveReq;41let phoneCheckInterval;42let phoneCheckListeners = 0;43const phoneConnectionChanged = (value) => {44ws.emit('phone-connection', { value });45};46const sendPromise = (0, util_1.promisify)(ws.send);47/** generate message tag and increment epoch */48const generateMessageTag = (longTag = false) => {49const tag = `${longTag ? referenceDateSeconds : (referenceDateSeconds % 1000)}.--${epoch}`;50epoch += 1; // increment message count, it makes the 'epoch' field when sending binary messages51return tag;52};53const sendRawMessage = (data) => {54if (ws.readyState !== ws.OPEN) {55throw new boom_1.Boom('Connection Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });56}57return sendPromise.call(ws, data);58};59/**60* Send a message to the WA servers61* @returns the tag attached in the message62* */63const sendNode = async ({ json, binaryTag, tag, longTag }) => {64tag = tag || generateMessageTag(longTag);65let data;66if (logger.level === 'trace') {67logger.trace({ tag, fromMe: true, json, binaryTag }, 'communication');68}69if (binaryTag) {70if (Array.isArray(json)) {71throw new boom_1.Boom('Expected BinaryNode with binary code', { statusCode: 400 });72}73if (!authInfo) {74throw new boom_1.Boom('No encryption/mac keys to encrypt node with', { statusCode: 400 });75}76const binary = (0, WABinary_1.encodeBinaryNodeLegacy)(json); // encode the JSON to the WhatsApp binary format77const buff = (0, Utils_1.aesEncrypt)(binary, authInfo.encKey); // encrypt it using AES and our encKey78const sign = (0, Utils_1.hmacSign)(buff, authInfo.macKey); // sign the message using HMAC and our macKey79data = Buffer.concat([80Buffer.from(tag + ','),81Buffer.from(binaryTag),82sign,83buff, // the actual encrypted buffer84]);85}86else {87data = `${tag},${JSON.stringify(json)}`;88}89await sendRawMessage(data);90return tag;91};92const end = (error) => {93logger.info({ error }, 'connection closed');94ws.removeAllListeners('close');95ws.removeAllListeners('error');96ws.removeAllListeners('open');97ws.removeAllListeners('message');98phoneCheckListeners = 0;99clearInterval(keepAliveReq);100clearPhoneCheckInterval();101if (ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {102try {103ws.close();104}105catch (_a) { }106}107ws.emit('ws-close', error);108ws.removeAllListeners('ws-close');109};110const onMessageRecieved = (message) => {111var _a, _b, _c;112if (message[0] === '!' || message[0] === '!'.charCodeAt(0)) {113// when the first character in the message is an '!', the server is sending a pong frame114const timestamp = message.slice(1, message.length).toString();115lastDateRecv = new Date(parseInt(timestamp));116ws.emit('received-pong');117}118else {119let messageTag;120let json;121try {122const dec = (0, Utils_1.decodeWAMessage)(message, authInfo);123messageTag = dec[0];124json = dec[1];125if (!json) {126return;127}128}129catch (error) {130end(error);131return;132}133//if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false })134if (logger.level === 'trace') {135logger.trace({ tag: messageTag, fromMe: false, json }, 'communication');136}137let anyTriggered = false;138/* Check if this is a response to a message we sent */139anyTriggered = ws.emit(`${Defaults_1.DEF_TAG_PREFIX}${messageTag}`, json);140/* Check if this is a response to a message we are expecting */141const l0 = json.tag || json[0] || '';142const l1 = (json === null || json === void 0 ? void 0 : json.attrs) || (json === null || json === void 0 ? void 0 : json[1]) || {};143const l2 = ((_b = (_a = json === null || json === void 0 ? void 0 : json.content) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.tag) || ((_c = json[2]) === null || _c === void 0 ? void 0 : _c[0]) || '';144Object.keys(l1).forEach(key => {145anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, json) || anyTriggered;146anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, json) || anyTriggered;147anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},${key}`, json) || anyTriggered;148});149anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0},,${l2}`, json) || anyTriggered;150anyTriggered = ws.emit(`${Defaults_1.DEF_CALLBACK_PREFIX}${l0}`, json) || anyTriggered;151if (!anyTriggered && logger.level === 'debug') {152logger.debug({ unhandled: true, tag: messageTag, fromMe: false, json }, 'communication recv');153}154}155};156/** Exits a query if the phone connection is active and no response is still found */157const exitQueryIfResponseNotExpected = (tag, cancel) => {158let timeout;159const listener = ([, connected]) => {160if (connected) {161timeout = setTimeout(() => {162logger.info({ tag }, 'cancelling wait for message as a response is no longer expected from the phone');163cancel(new boom_1.Boom('Not expecting a response', { statusCode: 422 }));164}, expectResponseTimeout);165ws.off(Defaults_1.PHONE_CONNECTION_CB, listener);166}167};168ws.on(Defaults_1.PHONE_CONNECTION_CB, listener);169return () => {170ws.off(Defaults_1.PHONE_CONNECTION_CB, listener);171timeout && clearTimeout(timeout);172};173};174/** interval is started when a query takes too long to respond */175const startPhoneCheckInterval = () => {176phoneCheckListeners += 1;177if (!phoneCheckInterval) {178// if its been a long time and we haven't heard back from WA, send a ping179phoneCheckInterval = setInterval(() => {180if (phoneCheckListeners <= 0) {181logger.warn('phone check called without listeners');182return;183}184logger.info('checking phone connection...');185sendAdminTest();186phoneConnectionChanged(false);187}, phoneResponseTimeMs);188}189};190const clearPhoneCheckInterval = () => {191phoneCheckListeners -= 1;192if (phoneCheckListeners <= 0) {193clearInterval(phoneCheckInterval);194phoneCheckInterval = undefined;195phoneCheckListeners = 0;196}197};198/** checks for phone connection */199const sendAdminTest = () => sendNode({ json: ['admin', 'test'] });200/**201* Wait for a message with a certain tag to be received202* @param tag the message tag to await203* @param json query that was sent204* @param timeoutMs timeout after which the promise will reject205*/206const waitForMessage = (tag, requiresPhoneConnection, timeoutMs) => {207if (ws.readyState !== ws.OPEN) {208throw new boom_1.Boom('Connection not open', { statusCode: Types_1.DisconnectReason.connectionClosed });209}210let cancelToken = () => { };211return {212promise: (async () => {213let onRecv;214let onErr;215let cancelPhoneChecker;216try {217const result = await (0, Utils_1.promiseTimeout)(timeoutMs, (resolve, reject) => {218onRecv = resolve;219onErr = err => {220reject(err || new boom_1.Boom('Intentional Close', { statusCode: Types_1.DisconnectReason.connectionClosed }));221};222cancelToken = () => onErr(new boom_1.Boom('Cancelled', { statusCode: 500 }));223if (requiresPhoneConnection) {224startPhoneCheckInterval();225cancelPhoneChecker = exitQueryIfResponseNotExpected(tag, onErr);226}227ws.on(`TAG:${tag}`, onRecv);228ws.on('ws-close', onErr); // if the socket closes, you'll never receive the message229});230return result;231}232finally {233requiresPhoneConnection && clearPhoneCheckInterval();234cancelPhoneChecker && cancelPhoneChecker();235ws.off(`TAG:${tag}`, onRecv);236ws.off('ws-close', onErr); // if the socket closes, you'll never receive the message237}238})(),239cancelToken: () => {240cancelToken();241}242};243};244/**245* Query something from the WhatsApp servers246* @param json the query itself247* @param binaryTags the tags to attach if the query is supposed to be sent encoded in binary248* @param timeoutMs timeout after which the query will be failed (set to null to disable a timeout)249* @param tag the tag to attach to the message250*/251const query = async ({ json, timeoutMs, expect200, tag, longTag, binaryTag, requiresPhoneConnection }) => {252tag = tag || generateMessageTag(longTag);253const { promise, cancelToken } = waitForMessage(tag, !!requiresPhoneConnection, timeoutMs);254try {255await sendNode({ json, tag, binaryTag });256}257catch (error) {258cancelToken();259// swallow error260await promise.catch(() => { });261// throw back the error262throw error;263}264const response = await promise;265const responseStatusCode = +(response.status ? response.status : 200); // default status266// read here: http://getstatuscode.com/599267if (responseStatusCode === 599) { // the connection has gone bad268end(new boom_1.Boom('WA server overloaded', { statusCode: 599, data: { query: json, response } }));269}270if (expect200 && Math.floor(responseStatusCode / 100) !== 2) {271const message = http_1.STATUS_CODES[responseStatusCode] || 'unknown';272throw new boom_1.Boom(`Unexpected status in '${Array.isArray(json) ? json[0] : ((json === null || json === void 0 ? void 0 : json.tag) || 'query')}': ${message}(${responseStatusCode})`, { data: { query: json, response }, statusCode: response.status });273}274return response;275};276const startKeepAliveRequest = () => (keepAliveReq = setInterval(() => {277if (!lastDateRecv) {278lastDateRecv = new Date();279}280const diff = Date.now() - lastDateRecv.getTime();281/*282check if it's been a suspicious amount of time since the server responded with our last seen283it could be that the network is down284*/285if (diff > keepAliveIntervalMs + 5000) {286end(new boom_1.Boom('Connection was lost', { statusCode: Types_1.DisconnectReason.connectionLost }));287}288else if (ws.readyState === ws.OPEN) {289sendRawMessage('?,,'); // if its all good, send a keep alive request290}291else {292logger.warn('keep alive called when WS not open');293}294}, keepAliveIntervalMs));295const waitForSocketOpen = async () => {296if (ws.readyState === ws.OPEN) {297return;298}299if (ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {300throw new boom_1.Boom('Connection Already Closed', { statusCode: Types_1.DisconnectReason.connectionClosed });301}302let onOpen;303let onClose;304await new Promise((resolve, reject) => {305onOpen = () => resolve(undefined);306onClose = reject;307ws.on('open', onOpen);308ws.on('close', onClose);309ws.on('error', onClose);310})311.finally(() => {312ws.off('open', onOpen);313ws.off('close', onClose);314ws.off('error', onClose);315});316};317ws.on('message', onMessageRecieved);318ws.on('open', () => {319startKeepAliveRequest();320logger.info('Opened WS connection to WhatsApp Web');321});322ws.on('error', end);323ws.on('close', () => end(new boom_1.Boom('Connection Terminated', { statusCode: Types_1.DisconnectReason.connectionLost })));324ws.on(Defaults_1.PHONE_CONNECTION_CB, json => {325if (!json[1]) {326end(new boom_1.Boom('Connection terminated by phone', { statusCode: Types_1.DisconnectReason.connectionLost }));327logger.info('Connection terminated by phone, closing...');328}329else {330phoneConnectionChanged(true);331}332});333ws.on('CB:Cmd,type:disconnect', json => {334const { kind } = json[1];335let reason;336switch (kind) {337case 'replaced':338reason = Types_1.DisconnectReason.connectionReplaced;339break;340default:341reason = Types_1.DisconnectReason.connectionLost;342break;343}344end(new boom_1.Boom(`Connection terminated by server: "${kind || 'unknown'}"`, { statusCode: reason }));345});346return {347type: 'legacy',348ws,349sendAdminTest,350updateKeys: (info) => authInfo = info,351waitForSocketOpen,352sendNode,353generateMessageTag,354waitForMessage,355query,356/** Generic function for action, set queries */357setQuery: async (nodes, binaryTag = [Types_1.WAMetric.group, Types_1.WAFlag.ignore], tag) => {358const json = {359tag: 'action',360attrs: { epoch: epoch.toString(), type: 'set' },361content: nodes362};363return query({364json,365binaryTag,366tag,367expect200: true,368requiresPhoneConnection: true369});370},371currentEpoch: () => epoch,372end373};374};375exports.makeSocket = makeSocket;376377378