From 51672150e4c6982fb5e39b42db3b4e0881c5faaa Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Fri, 23 Oct 2020 14:21:15 +0530 Subject: [PATCH] More reliable debounced timeouts --- src/Tests/Tests.Connect.ts | 13 ++++++--- src/WAConnection/0.Base.ts | 23 +++++++++++----- src/WAConnection/1.Validation.ts | 47 ++++++++++++++++++++------------ src/WAConnection/3.Connect.ts | 28 +++++++------------ src/WAConnection/Constants.ts | 1 + 5 files changed, 65 insertions(+), 47 deletions(-) diff --git a/src/Tests/Tests.Connect.ts b/src/Tests/Tests.Connect.ts index e8bc4c9..0dd99ed 100644 --- a/src/Tests/Tests.Connect.ts +++ b/src/Tests/Tests.Connect.ts @@ -116,16 +116,20 @@ describe('Test Connect', () => { describe ('Reconnects', () => { const verifyConnectionOpen = async (conn: WAConnection) => { assert.ok (conn.user.jid) + let failed = false // check that the connection stays open - conn.on ('close', ({reason}) => ( - reason !== DisconnectReason.intentional && assert.fail ('should not have closed again') - )) + conn.on ('close', ({reason}) => { + if(reason !== DisconnectReason.intentional) failed = true + }) await delay (60*1000) const status = await conn.getStatus () assert.ok (status) + assert.ok (!conn['debounceTimeout']) // this should be null conn.close () + + if (failed) assert.fail ('should not have closed again') } it('should dispose correctly on bad_session', async () => { const conn = new WAConnection() @@ -185,6 +189,7 @@ describe ('Reconnects', () => { */ it('should disrupt connect loop', async () => { const conn = new WAConnection() + conn.autoReconnect = ReconnectMode.onAllErrors conn.loadAuthInfo ('./auth_info.json') @@ -194,7 +199,7 @@ describe ('Reconnects', () => { while (!conn['conn']) { await delay(100) } - conn['conn'].terminate () + conn['conn'].close () while (conn['conn']) { await delay(100) diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 9b1605f..a744f42 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -32,7 +32,7 @@ const logger = pino({ prettyPrint: { levelFirst: true, ignore: 'hostname', trans export class WAConnection extends EventEmitter { /** The version of WhatsApp Web we're telling the servers we are */ - version: [number, number, number] = [2, 2041, 6] + version: [number, number, number] = [2, 2043, 8] /** The Browser we're telling the WhatsApp Web servers we are */ browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome') /** Metadata like WhatsApp id, name set on WhatsApp etc. */ @@ -46,8 +46,8 @@ export class WAConnection extends EventEmitter { maxIdleTimeMs: 15_000, waitOnlyForLastMessage: false, waitForChats: true, - maxRetries: 5, - connectCooldownMs: 3000, + maxRetries: 10, + connectCooldownMs: 4000, phoneResponseTime: 10_000, alwaysUseTakeover: true } @@ -93,11 +93,12 @@ export class WAConnection extends EventEmitter { protected mediaConn: MediaConnInfo protected debounceTimeout: NodeJS.Timeout + protected onDebounceTimeout = () => {} constructor () { super () this.registerCallback (['Cmd', 'type:disconnect'], json => ( - this.unexpectedDisconnect(json[1].kind || 'unknown') + this.state === 'open' && this.unexpectedDisconnect(json[1].kind || 'unknown') )) } /** @@ -246,7 +247,7 @@ export class WAConnection extends EventEmitter { * @param tag the tag to attach to the message */ async query(q: WAQuery) { - let {json, binaryTags, tag, timeoutMs, expect200, waitForOpen, longTag, requiresPhoneConnection} = q + let {json, binaryTags, tag, timeoutMs, expect200, waitForOpen, longTag, requiresPhoneConnection, startDebouncedTimeout} = q requiresPhoneConnection = requiresPhoneConnection !== false waitForOpen = waitForOpen !== false if (waitForOpen) await this.waitForConnection() @@ -273,6 +274,7 @@ export class WAConnection extends EventEmitter { {query: json, message, status: response.status} ) } + if (startDebouncedTimeout) this.stopDebouncedTimeout () return response } /** interval is started when a query takes too long to respond */ @@ -320,6 +322,14 @@ export class WAConnection extends EventEmitter { await this.send(buff) // send it off return tag } + protected startDebouncedTimeout () { + this.stopDebouncedTimeout () + this.debounceTimeout = setTimeout (() => this.onDebounceTimeout(), this.connectOptions.maxIdleTimeMs) + } + protected stopDebouncedTimeout () { + this.debounceTimeout && clearTimeout (this.debounceTimeout) + this.debounceTimeout = null + } /** * Send a plain JSON message to the WhatsApp servers * @param json the message to send @@ -390,10 +400,9 @@ export class WAConnection extends EventEmitter { this.keepAliveReq && clearInterval(this.keepAliveReq) this.clearPhoneCheckInterval () - try { this.conn?.close() - this.conn?.terminate() + //this.conn?.terminate() } catch { } diff --git a/src/WAConnection/1.Validation.ts b/src/WAConnection/1.Validation.ts index d585b01..0c6f300 100644 --- a/src/WAConnection/1.Validation.ts +++ b/src/WAConnection/1.Validation.ts @@ -6,7 +6,7 @@ import { WAMetric, WAFlag, BaileysError, Presence, WAUser } from './Constants' export class WAConnection extends Base { /** Authenticate the connection */ - protected async authenticate (startDebouncedTimeout: () => void, stopDebouncedTimeout: () => void, reconnect?: string) { + protected async authenticate (reconnect?: string) { // if no auth info is present, that is, a new session has to be established // generate a client ID if (!this.authInfo?.clientID) { @@ -16,7 +16,7 @@ export class WAConnection extends Base { const canLogin = this.authInfo?.encKey && this.authInfo?.macKey this.referenceDate = new Date () // refresh reference date - startDebouncedTimeout () + this.startDebouncedTimeout () const initQueries = [ (async () => { @@ -25,12 +25,13 @@ export class WAConnection extends Base { expect200: true, waitForOpen: false, longTag: true, - requiresPhoneConnection: false + requiresPhoneConnection: false, + startDebouncedTimeout: true }) if (!canLogin) { - stopDebouncedTimeout () // stop the debounced timeout for QR gen + this.stopDebouncedTimeout () // stop the debounced timeout for QR gen const result = await this.generateKeysForAuth (ref) - startDebouncedTimeout () // restart debounced timeout + this.startDebouncedTimeout () // restart debounced timeout return result } })() @@ -49,7 +50,15 @@ export class WAConnection extends Base { if (reconnect) json.push(...['reconnect', reconnect.replace('@s.whatsapp.net', '@c.us')]) else json.push ('takeover') - let response = await this.query({ json, tag: 's1', waitForOpen: false, expect200: true, longTag: true, requiresPhoneConnection: false }) // wait for response with tag "s1" + let response = await this.query({ + json, + tag: 's1', + waitForOpen: false, + expect200: true, + longTag: true, + requiresPhoneConnection: false, + startDebouncedTimeout: true + }) // wait for response with tag "s1" // if its a challenge request (we get it when logging in) if (response[1]?.challenge) { await this.respondToChallenge(response[1].challenge) @@ -66,9 +75,11 @@ export class WAConnection extends Base { this.logger.info('validated connection successfully') this.emit ('connection-validated', this.user) - const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false, requiresPhoneConnection: false }) - this.user.imgUrl = response?.eurl || '' - + if (this.loadProfilePicturesForChatsAutomatically) { + const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false, requiresPhoneConnection: false, startDebouncedTimeout: true }) + this.user.imgUrl = response?.eurl || '' + } + this.sendPostConnectQueries () this.logger.debug('sent init queries') @@ -181,7 +192,7 @@ export class WAConnection extends Base { const json = ['admin', 'challenge', signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID this.logger.info('resolving login challenge') - return this.query({json, expect200: true, waitForOpen: false}) + return this.query({json, expect200: true, waitForOpen: false, startDebouncedTimeout: true}) } /** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */ protected async generateKeysForAuth(ref: string) { @@ -194,21 +205,21 @@ export class WAConnection extends Base { } const regenQR = () => { - this.qrTimeout = setTimeout (() => { + this.qrTimeout = setTimeout (async () => { if (this.state === 'open') return this.logger.debug ('regenerated QR') - - this.generateNewQRCodeRef () - .then (newRef => ref = newRef) - .then (emitQR) - .then (regenQR) - .catch (error => { + try { + const newRef = await this.generateNewQRCodeRef () + ref = newRef + emitQR () + regenQR () + } catch (error) { this.logger.error ({ error }, `error in QR gen`) if (error.status === 429) { // too many QR requests this.endConnection () } - }) + } }, this.connectOptions.regenerateQRIntervalMs) } diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 911f138..db5bd35 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -66,16 +66,6 @@ export class WAConnection extends Base { let cancel: () => void const task = new Promise((resolve, reject) => { cancel = () => reject (CancelledError()) - - const checkIdleTime = () => { - this.debounceTimeout && clearTimeout (this.debounceTimeout) - this.debounceTimeout = setTimeout (() => rejectSafe (TimedOutError()), this.connectOptions.maxIdleTimeMs) - } - const startDebouncedTimeout = () => this.connectOptions.maxIdleTimeMs && this.conn.addEventListener ('message', checkIdleTime) - const stopDebouncedTimeout = () => { - clearTimeout (this.debounceTimeout) - this.conn.removeEventListener ('message', checkIdleTime) - } // determine whether reconnect should be used or not const shouldUseReconnect = (this.lastDisconnectReason === DisconnectReason.close || this.lastDisconnectReason === DisconnectReason.lost) && @@ -110,7 +100,8 @@ export class WAConnection extends Base { } } try { - await this.authenticate (startDebouncedTimeout, stopDebouncedTimeout, reconnectID) + this.onDebounceTimeout = () => rejectSafe(TimedOutError()) + await this.authenticate (reconnectID) this.startKeepAliveRequest() @@ -118,7 +109,9 @@ export class WAConnection extends Base { .removeAllListeners ('error') .removeAllListeners ('close') const result = waitForChats && (await waitForChats) - this.conn.removeEventListener ('message', checkIdleTime) + + this.stopDebouncedTimeout () + resolve (result) } catch (error) { reject (error) @@ -177,10 +170,8 @@ export class WAConnection extends Base { const deregisterCallbacks = () => { // wait for actual messages to load, "last" is the most recent message, "before" contains prior messages this.deregisterCallback(['action', 'add:last']) - if (!waitOnlyForLast) { - this.deregisterCallback(['action', 'add:before']) - this.deregisterCallback(['action', 'add:unread']) - } + this.deregisterCallback(['action', 'add:before']) + this.deregisterCallback(['action', 'add:unread']) this.deregisterCallback(['response', 'type:chat']) this.deregisterCallback(['response', 'type:contacts']) @@ -189,13 +180,13 @@ export class WAConnection extends Base { // wait for messages to load const chatUpdate = json => { + this.startDebouncedTimeout () // restart debounced timeout receivedMessages = true const isLast = json[1].last || waitOnlyForLast const messages = json[2] as WANode[] if (messages) { - messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => { const jid = message.key.remoteJid const chat = chats.get(jid) @@ -207,7 +198,6 @@ export class WAConnection extends Base { message['epoch'] = prevEpoch-1 chat.messages.insert (message) } - }) } // if received contacts before messages @@ -222,6 +212,7 @@ export class WAConnection extends Base { // get chats this.registerCallback(['response', 'type:chat'], json => { if (json[1].duplicate || !json[2]) return + this.startDebouncedTimeout () // restart debounced timeout json[2] .forEach(([item, chat]: [any, WAChat]) => { @@ -248,6 +239,7 @@ export class WAConnection extends Base { // get contacts this.registerCallback(['response', 'type:contacts'], json => { if (json[1].duplicate || !json[2]) return + this.startDebouncedTimeout () // restart debounced timeout receivedContacts = true diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 126aaf2..fad874e 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -57,6 +57,7 @@ export interface WAQuery { waitForOpen?: boolean longTag?: boolean requiresPhoneConnection?: boolean + startDebouncedTimeout?: boolean } export enum ReconnectMode { /** does not reconnect */