From ea36aabb6ca9c3abe47849a91421885f081ba339 Mon Sep 17 00:00:00 2001 From: Adhiraj Date: Sat, 22 Aug 2020 17:46:41 +0530 Subject: [PATCH] better connections --- Example/example.ts | 11 +- README.md | 6 +- package.json | 2 +- src/Tests/Tests.Connect.ts | 19 ++- src/WAConnection/0.Base.ts | 16 +- src/WAConnection/1.Validation.ts | 11 +- src/WAConnection/3.Connect.ts | 251 +++++++++++++++---------------- src/WAConnection/4.Events.ts | 2 +- src/WAConnection/Constants.ts | 18 ++- src/WAConnection/Utils.ts | 49 +++++- 10 files changed, 215 insertions(+), 170 deletions(-) diff --git a/Example/example.ts b/Example/example.ts index d484285..afbd600 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -6,7 +6,7 @@ import { Mimetype, WALocationMessage, MessageLogLevel, - WAMessageType, + WA_MESSAGE_STUB_TYPES, ReconnectMode, } from '../src/WAConnection/WAConnection' import * as fs from 'fs' @@ -18,8 +18,9 @@ async function example() { // loads the auth file credentials if present if (fs.existsSync('./auth_info.json')) conn.loadAuthInfo ('./auth_info.json') - // connect or timeout in 20 seconds - await conn.connect(20 * 1000) + conn.on ('qr', qr => console.log (qr)) + // connect or timeout in 30 seconds + await conn.connect({ timeoutMs: 30 * 1000 }) const unread = await conn.loadAllUnreadMessages () @@ -38,7 +39,7 @@ async function example() { }) // set to false to NOT relay your own sent messages conn.on('message-new', async (m) => { - const messageStubType = WAMessageType[m.messageStubType] || 'MESSAGE' + const messageStubType = WA_MESSAGE_STUB_TYPES[m.messageStubType] || 'MESSAGE' console.log('got notification of type: ' + messageStubType) const messageContent = m.message @@ -117,7 +118,7 @@ async function example() { const batterylevel = parseInt(batteryLevelStr) console.log('battery level: ' + batterylevel) }) - conn.on('closed', ({reason, isReconnecting}) => ( + conn.on('close', ({reason, isReconnecting}) => ( console.log ('oh no got disconnected: ' + reason + ', reconnecting: ' + isReconnecting) )) } diff --git a/README.md b/README.md index 6ea16e1..069e220 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ async function connectToWhatsApp () { const conn = new WAConnection() // 20 second timeout - await conn.connect (20*1000) + await conn.connect ({timeoutMs: 30*1000}) console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")") // every chat object has a list of most recent messages console.log ("you have " + conn.chats.all().length + " chats") @@ -62,7 +62,7 @@ If the connection is successful, you will see a QR code printed on your terminal If you don't want to wait for WhatsApp to send all your chats while connecting, you can use the following function: ``` ts -await conn.connect (20*1000, false) +await conn.connect ({timeoutMs: 30*1000}, false) ``` Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons: @@ -133,7 +133,7 @@ on (event: 'open', listener: () => void): this /** when the connection is opening */ on (event: 'connecting', listener: () => void): this /** when the connection has closed */ -on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this +on (event: 'close', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this /** when a new QR is generated, ready for scanning */ on (event: 'qr', listener: (qr: string) => void): this /** when the connection to the phone changes */ diff --git a/package.json b/package.json index a159fe1..e3b4798 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ ], "scripts": { "prepare": "npm run build", - "test": "mocha --timeout 60000 -r ts-node/register src/Tests/Tests.*.ts", + "test": "mocha --timeout 120000 -r ts-node/register src/Tests/Tests.*.ts", "lint": "eslint '*/*.ts' --quiet --fix", "build": "tsc", "build:docs": "typedoc", diff --git a/src/Tests/Tests.Connect.ts b/src/Tests/Tests.Connect.ts index 432ee47..9bb3810 100644 --- a/src/Tests/Tests.Connect.ts +++ b/src/Tests/Tests.Connect.ts @@ -1,8 +1,7 @@ import * as assert from 'assert' import {WAConnection} from '../WAConnection/WAConnection' -import { AuthenticationCredentialsBase64, BaileysError, MessageLogLevel, ReconnectMode } from '../WAConnection/Constants' -import { delay, promiseTimeout } from '../WAConnection/Utils' -import { close } from 'fs' +import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode } from '../WAConnection/Constants' +import { delay } from '../WAConnection/Utils' describe('QR Generation', () => { it('should generate QR', async () => { @@ -13,7 +12,7 @@ describe('QR Generation', () => { conn.removeAllListeners ('qr') conn.on ('qr', qr => calledQR += 1) - await conn.connect(15000) + await conn.connect({ timeoutMs: 15000 }) .then (() => assert.fail('should not have succeeded')) .catch (error => { assert.equal (error.message, 'timed out') @@ -42,7 +41,7 @@ describe('Test Connect', () => { const conn = new WAConnection() await conn .loadAuthInfo (auth) - .connect (20*1000) + .connect ({timeoutMs: 20*1000}) .then (conn => { assert.ok(conn.user) assert.ok(conn.user.id) @@ -96,7 +95,7 @@ describe ('Reconnects', () => { conn.close () } }) - it ('should reconnect connection', async () => { + it ('should reconnect on broken connection', async () => { const conn = new WAConnection () conn.autoReconnect = ReconnectMode.onConnectionLost @@ -108,7 +107,7 @@ describe ('Reconnects', () => { const task = new Promise (resolve => { let closes = 0 - conn.on ('closed', ({reason, isReconnecting}) => { + conn.on ('close', ({reason, isReconnecting}) => { console.log (`closed: ${reason}`) assert.ok (reason) assert.ok (isReconnecting) @@ -116,14 +115,14 @@ describe ('Reconnects', () => { // let it fail reconnect a few times if (closes > 4) { - conn.removeAllListeners ('closed') + conn.removeAllListeners ('close') conn.removeAllListeners ('connecting') resolve () } }) conn.on ('connecting', () => { // close again - delay (500).then (closeConn) + delay (3500).then (closeConn) }) }) @@ -143,7 +142,7 @@ describe ('Reconnects', () => { await delay (2000) } finally { conn.removeAllListeners ('connecting') - conn.removeAllListeners ('closed') + conn.removeAllListeners ('close') conn.removeAllListeners ('open') conn.close () } diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 9cfca7f..0de74b9 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -35,7 +35,7 @@ export class WAConnection extends EventEmitter { /** Should requests be queued when the connection breaks in between; if false, then an error will be thrown */ pendingRequestTimeoutMs: number = null /** The connection state */ - state: WAConnectionState = 'closed' + state: WAConnectionState = 'close' /** New QR generation interval, set to null if you don't want to regenerate */ regenerateQRIntervalMs = 30*1000 @@ -73,7 +73,10 @@ export class WAConnection extends EventEmitter { this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind)) } async unexpectedDisconnect (error?: DisconnectReason) { - const willReconnect = this.autoReconnect === ReconnectMode.onAllErrors || (this.autoReconnect === ReconnectMode.onConnectionLost && (error !== 'replaced')) + const willReconnect = + (this.autoReconnect === ReconnectMode.onAllErrors || + (this.autoReconnect === ReconnectMode.onConnectionLost && (error !== 'replaced'))) && + error !== 'invalid_session' this.log (`got disconnected, reason ${error || 'unknown'}${willReconnect ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info) this.closeInternal(error, willReconnect) @@ -280,16 +283,15 @@ export class WAConnection extends EventEmitter { this.closeInternal ('intentional') this.cancelReconnect && this.cancelReconnect () - this.cancelledReconnect = true - this.pendingRequests.forEach (({reject}) => reject(new Error('closed'))) + this.pendingRequests.forEach (({reject}) => reject(new Error('close'))) this.pendingRequests = [] } protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) { this.qrTimeout && clearTimeout (this.qrTimeout) this.phoneCheck && clearTimeout (this.phoneCheck) - this.state = 'closed' + this.state = 'close' this.msgCount = 0 this.conn?.removeAllListeners ('close') this.conn?.close() @@ -299,13 +301,13 @@ export class WAConnection extends EventEmitter { Object.keys(this.callbacks).forEach(key => { if (!key.includes('function:')) { this.log (`cancelling message wait: ${key}`, MessageLogLevel.info) - this.callbacks[key].errCallback(new Error('closed')) + this.callbacks[key].errCallback(new Error('close')) delete this.callbacks[key] } }) if (this.keepAliveReq) clearInterval(this.keepAliveReq) // reconnecting if the timeout is active for the reconnect loop - this.emit ('closed', { reason, isReconnecting: this.cancelReconnect || isReconnecting }) + this.emit ('close', { reason, isReconnecting: this.cancelReconnect || isReconnecting}) } protected async reconnectLoop () { diff --git a/src/WAConnection/1.Validation.ts b/src/WAConnection/1.Validation.ts index be01923..c3c66e9 100644 --- a/src/WAConnection/1.Validation.ts +++ b/src/WAConnection/1.Validation.ts @@ -39,9 +39,9 @@ export class WAConnection extends Base { case 401: // if the phone was unpaired throw new BaileysError ('unpaired from phone', json) case 429: // request to login was denied, don't know why it happens - throw new BaileysError ('request denied, try reconnecting', json) + throw new BaileysError ('request denied', json) default: - throw new BaileysError ('unexpected status', json) + throw new BaileysError ('unexpected status ' + json.status, json) } } // if its a challenge request (we get it when logging in) @@ -160,6 +160,7 @@ export class WAConnection extends Base { const qr = [ref, publicKey, this.authInfo.clientID].join(',') this.emit ('qr', qr) } + const regenQR = () => { this.qrTimeout = setTimeout (() => { if (this.state === 'open') return @@ -173,9 +174,9 @@ export class WAConnection extends Base { .catch (err => this.log (`error in QR gen: ${err}`, MessageLogLevel.info)) }, this.regenerateQRIntervalMs) } - if (this.regenerateQRIntervalMs) { - regenQR () - } + + emitQR () + if (this.regenerateQRIntervalMs) regenQR () const json = await this.waitForMessage('s1', []) this.qrTimeout && clearTimeout (this.qrTimeout) diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 13ec76a..0d4ca02 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -1,50 +1,45 @@ -import WS from 'ws' import * as Utils from './Utils' -import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError } from './Constants' +import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions } from './Constants' import {WAConnection as Base} from './1.Validation' import Decoder from '../Binary/Decoder' export class WAConnection extends Base { /** * Connect to WhatsAppWeb - * @param timeoutMs timeout after which the connect will fail, set to null for an infinite timeout - * @param waitForChats should the chats be waited for + * @param options the connect options */ - async connect(timeoutMs: number = null, waitForChats: boolean = true) { + async connect(options: WAConnectOptions = {}) { // if we're already connected, throw an error - if (this.state !== 'closed') throw new Error('cannot connect when state=' + this.state) + if (this.state !== 'close') throw new Error('cannot connect when state=' + this.state) this.state = 'connecting' this.emit ('connecting') - this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' }) - - const promise: Promise = Utils.promiseTimeout(timeoutMs, (resolve, reject) => { - this.conn.on('open', () => { - this.log('connected to WhatsApp Web server, authenticating...', MessageLogLevel.info) - // start sending keep alive requests (keeps the WebSocket alive & updates our last seen) - this.authenticate() - .then(() => { - this.startKeepAliveRequest() - - this.conn.removeAllListeners ('error') - this.conn.removeAllListeners ('close') - this.conn.on ('close', () => this.unexpectedDisconnect ('closed')) - - this.state = 'open' - resolve() - }) - .catch(reject) + const { ws, cancel } = Utils.openWebSocketConnection (5000, typeof options?.retryOnNetworkErrors === 'undefined' ? true : options?.retryOnNetworkErrors) + const promise: Promise = Utils.promiseTimeout(options?.timeoutMs, (resolve, reject) => { + ws + .then (conn => this.conn = conn) + .then (() => this.conn.on('message', data => this.onMessageRecieved(data as any))) + .then (() => this.log('connected to WhatsApp Web server, authenticating...', MessageLogLevel.info)) + .then (() => this.authenticate()) + .then (() => { + this.startKeepAliveRequest() + this.conn.removeAllListeners ('error') + this.conn.removeAllListeners ('close') + this.conn.on ('close', () => this.unexpectedDisconnect ('close')) + }) + .then (resolve) + .catch (err => { + cancel () + reject (err) }) - this.conn.on('message', m => this.onMessageRecieved(m)) - // if there was an error in the WebSocket - this.conn.on('error', reject) - this.conn.on('close', () => reject(new Error('closed'))) }) try { await promise - waitForChats && await this.receiveChatsAndContacts(timeoutMs, true) + + const waitForChats = typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats + waitForChats && await this.receiveChatsAndContacts(options?.timeoutMs, true) this.phoneConnected = true this.state = 'open' @@ -55,9 +50,12 @@ export class WAConnection extends Base { this.releasePendingRequests () this.log ('opened connection to WhatsApp Web', MessageLogLevel.info) + return this } catch (error) { - this.closeInternal (error.message) + const loggedOut = error instanceof BaileysError && error.status >= 400 + if (loggedOut && this.cancelReconnect) this.cancelReconnect () + this.closeInternal (loggedOut ? 'invalid_session' : error.message) throw error } } @@ -77,101 +75,93 @@ export class WAConnection extends Base { let receivedContacts = false let receivedMessages = false - let convoResolve: () => void - const waitForConvos = () => - Utils.promiseTimeout(timeoutMs, resolve => { - convoResolve = () => { - // de-register the callbacks, so that they don't get called again - this.deregisterCallback(['action', 'add:last']) - if (!stopAfterMostRecentMessage) { - this.deregisterCallback(['action', 'add:before']) - this.deregisterCallback(['action', 'add:unread']) - } - resolve() - } - const chatUpdate = json => { - receivedMessages = true - const isLast = json[1].last || stopAfterMostRecentMessage - const messages = json[2] as WANode[] + let resolveTask: () => void + const deregisterCallbacks = () => { + // wait for actual messages to load, "last" is the most recent message, "before" contains prior messages + this.deregisterCallback(['action', 'add:last']) + if (!stopAfterMostRecentMessage) { + this.deregisterCallback(['action', 'add:before']) + this.deregisterCallback(['action', 'add:unread']) + } + this.deregisterCallback(['response', 'type:chat']) + this.deregisterCallback(['response', 'type:contacts']) + } + const checkForResolution = () => { + if (receivedContacts && receivedMessages) resolveTask () + } + + // wait for messages to load + const chatUpdate = json => { + receivedMessages = true + const isLast = json[1].last || stopAfterMostRecentMessage + const messages = json[2] as WANode[] - if (messages) { - messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => { - const jid = message.key.remoteJid - const chat = this.chats.get(jid) - chat?.messages.unshift (message) - }) - } - // if received contacts before messages - if (isLast && receivedContacts) convoResolve () - } - - // wait for actual messages to load, "last" is the most recent message, "before" contains prior messages - this.registerCallback(['action', 'add:last'], chatUpdate) - - if (!stopAfterMostRecentMessage) { - this.registerCallback(['action', 'add:before'], chatUpdate) - this.registerCallback(['action', 'add:unread'], chatUpdate) - } - }) - const waitForChats = async () => ( - Utils.promiseTimeout (timeoutMs, resolve => { - this.registerCallback(['response', 'type:chat'], json => { - if (json[1].duplicate || !json[2]) return - - json[2] - .forEach(([item, chat]: [any, WAChat]) => { - if (!chat) { - this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info) - return - } - chat.jid = Utils.whatsappID (chat.jid) - chat.t = +chat.t - chat.count = +chat.count - chat.messages = [] - - const oldChat = this.chats.get(chat.jid) - oldChat && this.chats.delete (oldChat) - - this.chats.insert (chat) // chats data (log json to see what it looks like) - }) - - this.deregisterCallback(['response', 'type:chat']) - - this.log ('received chats list', MessageLogLevel.info) - - if (this.chats.all().length > 0) waitForConvos().then (resolve) - else resolve () - }) - }) - ) - const waitForContacts = async () => ( - new Promise (resolve => { - this.registerCallback(['response', 'type:contacts'], json => { - if (json[1].duplicate) return - - receivedContacts = true - - json[2].forEach(([type, contact]: ['user', WAContact]) => { - if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info) - - contact.jid = Utils.whatsappID (contact.jid) - this.contacts[contact.jid] = contact - }) - // if you receive contacts after messages - // should probably resolve the promise - if (receivedMessages) convoResolve() - resolve () - - this.deregisterCallback(['response', 'type:contacts']) - - this.log ('received contacts list', MessageLogLevel.info) + if (messages) { + messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => { + const jid = message.key.remoteJid + const chat = this.chats.get(jid) + chat?.messages.unshift (message) }) - }) - ) - // wait for the chats & contacts to load - await Promise.all( [waitForChats(), waitForContacts()] ) + } + // if received contacts before messages + if (isLast && receivedContacts) checkForResolution () + } + // wait for actual messages to load, "last" is the most recent message, "before" contains prior messages + this.registerCallback(['action', 'add:last'], chatUpdate) + if (!stopAfterMostRecentMessage) { + this.registerCallback(['action', 'add:before'], chatUpdate) + this.registerCallback(['action', 'add:unread'], chatUpdate) + } + + this.registerCallback(['response', 'type:chat'], json => { + if (json[1].duplicate || !json[2]) return + + json[2] + .forEach(([item, chat]: [any, WAChat]) => { + if (!chat) { + this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info) + return + } + chat.jid = Utils.whatsappID (chat.jid) + chat.t = +chat.t + chat.count = +chat.count + chat.messages = [] + + const oldChat = this.chats.get(chat.jid) + oldChat && this.chats.delete (oldChat) + + this.chats.insert (chat) // chats data (log json to see what it looks like) + }) + + this.log ('received chats list', MessageLogLevel.info) + }) + // get contacts + this.registerCallback(['response', 'type:contacts'], json => { + if (json[1].duplicate) return + + receivedContacts = true + + json[2].forEach(([type, contact]: ['user', WAContact]) => { + if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info) + + contact.jid = Utils.whatsappID (contact.jid) + this.contacts[contact.jid] = contact + }) + this.log ('received contacts list', MessageLogLevel.info) + checkForResolution () + }) + // wait for the chats & contacts to load + await Utils.promiseTimeout (timeoutMs, (resolve, reject) => { + resolveTask = resolve + const rejectTask = (reason) => { + reject (new Error(reason)) + this.off ('close', rejectTask) + } + this.on ('close', rejectTask) + }).finally (deregisterCallbacks) + this.chats.all ().forEach (chat => { const respectiveContact = this.contacts[chat.jid] chat.title = respectiveContact?.name || respectiveContact?.notify @@ -181,16 +171,15 @@ export class WAConnection extends Base { this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request this.pendingRequests = [] } - private onMessageRecieved(message) { + private onMessageRecieved(message: string | Buffer) { if (message[0] === '!') { // when the first character in the message is an '!', the server is updating the last seen - const timestamp = message.slice(1, message.length) + const timestamp = message.slice(1, message.length).toString ('utf-8') this.lastSeen = new Date(parseInt(timestamp)) } else { const decrypted = Utils.decryptWA (message, this.authInfo.macKey, this.authInfo.encKey, new Decoder()) - if (!decrypted) { - return - } + if (!decrypted) return + const [messageTag, json] = decrypted if (this.logLevel === MessageLogLevel.all) { @@ -261,12 +250,16 @@ export class WAConnection extends Base { this.cancelledReconnect = false try { while (true) { - const {delay, cancel} = Utils.delayCancellable (5000) - this.cancelReconnect = cancel + const {delay, cancel} = Utils.delayCancellable (2500) + this.cancelReconnect = () => { + this.cancelledReconnect = true + this.cancelReconnect = null + cancel () + } await delay try { - await this.connect () + await this.connect ({ timeoutMs: 30000, retryOnNetworkErrors: true }) this.cancelReconnect = null break } catch (error) { diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index fa10465..4befbab 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -286,7 +286,7 @@ export class WAConnection extends Base { /** when the connection is opening */ on (event: 'connecting', listener: () => void): this /** when the connection has closed */ - on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this + on (event: 'close', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this /** when a new QR is generated, ready for scanning */ on (event: 'qr', listener: (qr: string) => void): this /** when the connection to the phone changes */ diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index b40fd2c..42dd2df 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -21,7 +21,7 @@ export interface WALocationMessage { address?: string } /** Reverse stub type dictionary */ -export const WAMessageType = function () { +export const WA_MESSAGE_STUB_TYPES = function () { const types = WA_MESSAGE_STUB_TYPE const dict: Record = {} Object.keys(types).forEach(element => dict[ types[element] ] = element) @@ -50,14 +50,22 @@ export interface WAQuery { export enum ReconnectMode { /** does not reconnect */ off = 0, - /** reconnects only when the connection is 'lost' or 'closed' */ + /** reconnects only when the connection is 'lost' or 'close' */ onConnectionLost = 1, /** reconnects on all disconnects, including take overs */ onAllErrors = 2 } +export type WAConnectOptions = { + /** timeout after which the connect will fail, set to null for an infinite timeout */ + timeoutMs?: number + /** should the chats be waited for */ + waitForChats?: boolean + /** retry on network errors while connecting */ + retryOnNetworkErrors?: boolean +} -export type WAConnectionState = 'open' | 'connecting' | 'closed' -export type DisconnectReason = 'closed' | 'lost' | 'replaced' | 'intentional' +export type WAConnectionState = 'open' | 'connecting' | 'close' +export type DisconnectReason = 'close' | 'lost' | 'replaced' | 'intentional' | 'invalid_session' export enum MessageLogLevel { none=0, info=1, @@ -305,7 +313,7 @@ export interface WASendMessageResponse { export type BaileysEvent = 'open' | 'connecting' | - 'closed' | + 'close' | 'qr' | 'connection-phone-change' | 'user-presence-update' | diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index 2b69ffd..3065096 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -5,9 +5,10 @@ import {promises as fs} from 'fs' import fetch from 'node-fetch' import { exec } from 'child_process' import {platform, release} from 'os' +import WS from 'ws' import Decoder from '../Binary/Decoder' -import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageType, WAMessage, WAMessageContent, BaileysError, WAMessageProto } from './Constants' +import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto } from './Constants' const platformMap = { 'aix': 'AIX', @@ -18,7 +19,7 @@ const platformMap = { export const Browsers = { ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string], macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string], - baileys: browser => ['Baileys', browser, '2.0'] as [string, string, string], + baileys: browser => ['Baileys', browser, '3.0'] as [string, string, string], /** The appropriate browser based on your OS & release */ appropriate: browser => [ platformMap [platform()] || 'Ubuntu', browser, release() ] as [string, string, string] } @@ -106,6 +107,42 @@ export async function promiseTimeout(ms: number, promise: (resolve: (v?: T)=> cancel () } } + +export const openWebSocketConnection = (timeoutMs: number, retryOnNetworkError: boolean) => { + const newWS = async () => { + const conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com', timeout: timeoutMs }) + await new Promise ((resolve, reject) => { + conn.on('open', () => { + conn.removeAllListeners ('error') + conn.removeAllListeners ('close') + conn.removeAllListeners ('open') + + resolve () + }) + // if there was an error in the WebSocket + conn.on('error', reject) + conn.on('close', () => reject(new Error('close'))) + }) + return conn + } + let cancelled = false + const connect = async () => { + while (!cancelled) { + try { + const ws = await newWS() + if (!cancelled) return ws + break + } catch (error) { + if (!retryOnNetworkError) throw error + await delay (1000) + } + } + throw new Error ('cancelled') + } + const cancel = () => cancelled = true + return { ws: connect(), cancel } +} + // whatsapp requires a message tag for every message, we just use the timestamp as one export function generateMessageTag(epoch?: number) { let tag = unixTimestampSeconds().toString() @@ -138,7 +175,6 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf let json let tags = null if (typeof data === 'string') { - // if the first character is a "[", then the data must just be plain JSON array or object json = JSON.parse(data) // parse the JSON } else { if (!macKey || !encKey) { @@ -159,7 +195,12 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey if (!checksum.equals(computedChecksum)) { - throw new Error (`Checksums don't match:\nog: ${checksum.toString('hex')}\ncomputed: ${computedChecksum.toString('hex')}`) + throw new Error (` + Checksums don't match: + og: ${checksum.toString('hex')} + computed: ${computedChecksum.toString('hex')} + message: ${message.slice(0, 80).toString()} + `) } // the checksum the server sent, must match the one we computed for the message to be valid const decrypted = aesDecrypt(data, encKey) // decrypt using AES