diff --git a/src/Tests/Common.ts b/src/Tests/Common.ts index 619eba0..52bb0f7 100644 --- a/src/Tests/Common.ts +++ b/src/Tests/Common.ts @@ -1,4 +1,4 @@ -import { WAConnection, MessageLogLevel, MessageOptions, MessageType, unixTimestampSeconds, toNumber, GET_MESSAGE_ID, waMessageKey } from '../WAConnection/WAConnection' +import { WAConnection, MessageOptions, MessageType, unixTimestampSeconds, toNumber, GET_MESSAGE_ID, waMessageKey } from '../WAConnection/WAConnection' import * as assert from 'assert' import {promises as fs} from 'fs' @@ -21,7 +21,7 @@ export const WAConnectionTest = (name: string, func: (conn: WAConnection) => voi describe(name, () => { const conn = new WAConnection() conn.connectOptions.maxIdleTimeMs = 30_000 - conn.logLevel = MessageLogLevel.unhandled + conn.logger.level = 'debug' before(async () => { const file = './auth_info.json' diff --git a/src/Tests/Tests.Connect.ts b/src/Tests/Tests.Connect.ts index 1608959..e8bc4c9 100644 --- a/src/Tests/Tests.Connect.ts +++ b/src/Tests/Tests.Connect.ts @@ -2,7 +2,7 @@ import * as assert from 'assert' import {WAConnection} from '../WAConnection/WAConnection' import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode, DisconnectReason } from '../WAConnection/Constants' import { delay } from '../WAConnection/Utils' -import { assertChatDBIntegrity } from './Common' +import { assertChatDBIntegrity, testJid } from './Common' describe('QR Generation', () => { it('should generate QR', async () => { @@ -73,22 +73,41 @@ describe('Test Connect', () => { }) it ('should disconnect & reconnect phone', async () => { const conn = new WAConnection () + conn.logger.level = 'debug' await conn.loadAuthInfo('./auth_info.json').connect () assert.equal (conn.phoneConnected, true) try { const waitForEvent = expect => new Promise (resolve => { conn.on ('connection-phone-change', ({connected}) => { - assert.equal (connected, expect) - conn.removeAllListeners ('connection-phone-change') - resolve () + if (connected === expect) { + conn.removeAllListeners ('connection-phone-change') + resolve () + } }) }) console.log ('disconnect your phone from the internet') + await delay (10_000) + console.log ('phone should be disconnected now, testing...') + + const messagesPromise = Promise.all ( + [ + conn.loadMessages (testJid, 50), + conn.getStatus (testJid), + conn.getProfilePicture (testJid).catch (() => '') + ] + ) + await waitForEvent (false) + console.log ('reconnect your phone to the internet') await waitForEvent (true) + + console.log ('reconnected successfully') + + const final = await messagesPromise + assert.ok (final) } finally { conn.close () } diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 0468544..6d2c1d3 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -48,7 +48,7 @@ export class WAConnection extends EventEmitter { waitForChats: true, maxRetries: 5, connectCooldownMs: 3000, - phoneResponseTime: 7500, + phoneResponseTime: 7_500, alwaysUseTakeover: false } /** When to auto-reconnect */ @@ -81,6 +81,7 @@ export class WAConnection extends EventEmitter { protected encoder = new Encoder() protected decoder = new Decoder() protected pendingRequests: {resolve: () => void, reject: (error) => void}[] = [] + protected phoneCheckInterval = undefined protected referenceDate = new Date () // used for generating tags protected lastSeen: Date = null // last keep alive received @@ -216,13 +217,17 @@ export class WAConnection extends EventEmitter { * @param json query that was sent * @param timeoutMs timeout after which the promise will reject */ - async waitForMessage(tag: string, json?: Object, timeoutMs?: number) { + async waitForMessage(tag: string, json: Object, requiresPhoneConnection: boolean, timeoutMs?: number) { + if (!this.phoneCheckInterval && requiresPhoneConnection) { + this.startPhoneCheckInterval () + } try { const result = await Utils.promiseTimeout(timeoutMs, (resolve, reject) => (this.callbacks[tag] = { queryJSON: json, callback: resolve, errCallback: reject }), ) return result as any } finally { + requiresPhoneConnection && this.clearPhoneCheckInterval () delete this.callbacks[tag] } } @@ -239,32 +244,56 @@ export class WAConnection extends EventEmitter { * @param timeoutMs timeout after which the query will be failed (set to null to disable a timeout) * @param tag the tag to attach to the message */ - async query({json, binaryTags, tag, timeoutMs, expect200, waitForOpen, longTag}: WAQuery) { + async query(q: WAQuery) { + let {json, binaryTags, tag, timeoutMs, expect200, waitForOpen, longTag, requiresPhoneConnection} = q + requiresPhoneConnection = requiresPhoneConnection !== false waitForOpen = waitForOpen !== false if (waitForOpen) await this.waitForConnection() tag = tag || this.generateMessageTag (longTag) - const promise = this.waitForMessage(tag, json, timeoutMs) + const promise = this.waitForMessage(tag, json, requiresPhoneConnection, timeoutMs) - if (binaryTags) tag = await this.sendBinary(json as WANode, binaryTags, tag, longTag) - else tag = await this.sendJSON(json, tag, longTag) + if (binaryTags) tag = await this.sendBinary(json as WANode, binaryTags, tag) + else tag = await this.sendJSON(json, tag) const response = await promise + if (expect200 && response.status && Math.floor(+response.status / 100) !== 2) { // read here: http://getstatuscode.com/599 if (response.status === 599) { this.unexpectedDisconnect (DisconnectReason.badSession) - const response = await this.query ({json, binaryTags, tag, timeoutMs, expect200, waitForOpen}) + const response = await this.query (q) return response } + const message = STATUS_CODES[response.status] || 'unknown' - throw new BaileysError( + throw new BaileysError ( `Unexpected status in '${json[0] || 'generic query'}': ${STATUS_CODES[response.status]}(${response.status})`, {query: json, message, status: response.status} ) } return response } + /** interval is started when a query takes too long to respond */ + protected startPhoneCheckInterval () { + // if its been a long time and we haven't heard back from WA, send a ping + this.phoneCheckInterval = setInterval (() => { + if (!this.conn) return // if disconnected, then don't do anything + + this.logger.debug ('checking phone connection...') + this.sendAdminTest () + + this.phoneConnected = false + this.emit ('connection-phone-change', { connected: false }) + }, this.connectOptions.phoneResponseTime) + } + protected clearPhoneCheckInterval () { + this.phoneCheckInterval && clearInterval (this.phoneCheckInterval) + this.phoneCheckInterval = undefined + } + protected async sendAdminTest () { + return this.sendJSON (['admin', 'test']) + } /** * Send a binary encoded message * @param json the message to encode & send @@ -335,9 +364,6 @@ export class WAConnection extends EventEmitter { protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) { this.logger.info (`closed connection, reason ${reason}${isReconnecting ? ', reconnecting in a few seconds...' : ''}`) - this.qrTimeout && clearTimeout (this.qrTimeout) - this.debounceTimeout && clearTimeout (this.debounceTimeout) - this.state = 'close' this.phoneConnected = false this.lastDisconnectReason = reason @@ -358,7 +384,12 @@ export class WAConnection extends EventEmitter { this.conn?.removeAllListeners ('open') this.conn?.removeAllListeners ('message') + this.qrTimeout && clearTimeout (this.qrTimeout) + this.debounceTimeout && clearTimeout (this.debounceTimeout) this.keepAliveReq && clearInterval(this.keepAliveReq) + this.clearPhoneCheckInterval () + + try { this.conn?.close() this.conn?.terminate() diff --git a/src/WAConnection/1.Validation.ts b/src/WAConnection/1.Validation.ts index b227b48..4017345 100644 --- a/src/WAConnection/1.Validation.ts +++ b/src/WAConnection/1.Validation.ts @@ -24,7 +24,8 @@ export class WAConnection extends Base { json: ['admin', 'init', this.version, this.browserDescription, this.authInfo?.clientID, true], expect200: true, waitForOpen: false, - longTag: true + longTag: true, + requiresPhoneConnection: false }) if (!canLogin) { stopDebouncedTimeout () // stop the debounced timeout for QR gen @@ -48,11 +49,11 @@ 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 }) // wait for response with tag "s1" + let response = await this.query({ json, tag: 's1', waitForOpen: false, expect200: true, longTag: true, requiresPhoneConnection: false }) // 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) - response = await this.waitForMessage('s2', []) + response = await this.waitForMessage('s2', [], true) } return response })() @@ -64,7 +65,7 @@ export class WAConnection extends Base { this.logger.info('validated connection successfully') - const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false }) + const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false, requiresPhoneConnection: false }) this.user.imgUrl = response?.eurl || '' this.sendPostConnectQueries () @@ -93,7 +94,8 @@ export class WAConnection extends Base { expect200: true, waitForOpen: false, longTag: true, - timeoutMs: this.connectOptions.maxIdleTimeMs + timeoutMs: this.connectOptions.maxIdleTimeMs, + requiresPhoneConnection: false }) return response.ref as string } @@ -212,7 +214,7 @@ export class WAConnection extends Base { emitQR () if (this.connectOptions.regenerateQRIntervalMs) regenQR () - const json = await this.waitForMessage('s1', []) + const json = await this.waitForMessage('s1', [], false) this.qrTimeout && clearTimeout (this.qrTimeout) this.qrTimeout = null diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 950b249..3d5daa0 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -37,7 +37,7 @@ export class WAConnection extends Base { const willReconnect = !loggedOut && (tries <= (options?.maxRetries || 5)) && this.state === 'connecting' const reason = loggedOut ? DisconnectReason.invalidSession : error.message - this.logger.warn (`connect attempt ${tries} failed${ willReconnect ? ', retrying...' : ''}`, error) + this.logger.warn ({ error }, `connect attempt ${tries} failed${ willReconnect ? ', retrying...' : ''}`) if ((this.state as string) !== 'close' && !willReconnect) { this.closeInternal (reason) @@ -317,10 +317,6 @@ export class WAConnection extends Base { if (this.logger.level === 'trace') { this.logger.trace(messageTag + ', ' + JSON.stringify(json)) } - if (!this.phoneConnected && this.state === 'open') { - this.phoneConnected = true - this.emit ('connection-phone-change', { connected: true }) - } /* Check if this is a response to a message we sent */ @@ -365,11 +361,18 @@ export class WAConnection extends Base { return } } + if (this.state === 'open' && json[0] === 'Pong') { + if (this.phoneConnected !== json[1]) { + this.phoneConnected = json[1] + this.emit ('connection-phone-change', { connected: this.phoneConnected }) + return + } + } if (this.logger.level === 'debug') { - this.logger.debug({ unhandled: true }, messageTag + ', ' + JSON.stringify(json)) + this.logger.debug({ unhandled: true }, messageTag + ',' + JSON.stringify(json)) } } catch (error) { - this.logger.error (`encountered error in decrypting message, closing`, error) + this.logger.error ({ error }, `encountered error in decrypting message, closing`) if (this.state === 'open') this.unexpectedDisconnect (DisconnectReason.badSession) else this.endConnection () @@ -389,29 +392,6 @@ export class WAConnection extends Base { */ if (diff > KEEP_ALIVE_INTERVAL_MS+5000) this.unexpectedDisconnect (DisconnectReason.lost) else if (this.conn) this.send ('?,,') // if its all good, send a keep alive request - - // poll phone connection as well, - // 5000 ms for timeout - this.checkPhoneConnection (this.connectOptions.phoneResponseTime || 7500) - .then (connected => { - this.phoneConnected !== connected && this.emit ('connection-phone-change', {connected}) - this.phoneConnected = connected - }) - }, KEEP_ALIVE_INTERVAL_MS) } - /** - * Check if your phone is connected - * @param timeoutMs max time for the phone to respond - */ - async checkPhoneConnection(timeoutMs = 5000) { - if (this.state !== 'open') return false - - try { - const response = await this.query({json: ['admin', 'test'], timeoutMs, waitForOpen: false}) - return response[1] as boolean - } catch (error) { - return false - } - } } diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index e5cc5ae..d3c6113 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -1,7 +1,7 @@ import * as QR from 'qrcode-terminal' import { WAConnection as Base } from './3.Connect' import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WANode, WAOpenResult, Presence, AuthenticationCredentials } from './Constants' -import { whatsappID, unixTimestampSeconds, isGroupID, toNumber, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey } from './Utils' +import { whatsappID, unixTimestampSeconds, isGroupID, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey } from './Utils' import KeyedDB from '@adiwajshing/keyed-db' import { Mutex } from './Mutex' @@ -175,7 +175,7 @@ export class WAConnection extends Base { /** Get the URL to download the profile picture of a person/group */ @Mutex (jid => jid) async getProfilePicture(jid: string | null) { - const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.jid], expect200: true }) + const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.jid], expect200: true, requiresPhoneConnection: false }) return response.eurl as string } protected forwardStatusUpdate (update: WAMessageStatusUpdate) { diff --git a/src/WAConnection/5.User.ts b/src/WAConnection/5.User.ts index 2f723ec..853ca24 100644 --- a/src/WAConnection/5.User.ts +++ b/src/WAConnection/5.User.ts @@ -13,7 +13,7 @@ import { Mutex } from './Mutex' export class WAConnection extends Base { /** Query whether a given number is registered on WhatsApp */ - isOnWhatsApp = (jid: string) => this.query({json: ['query', 'exist', jid]}).then((m) => m.status === 200) + isOnWhatsApp = (jid: string) => this.query({json: ['query', 'exist', jid], requiresPhoneConnection: false}).then((m) => m.status === 200) /** * Tell someone about your presence -- online, typing, offline etc. * @param jid the ID of the person/group who you are updating @@ -35,7 +35,7 @@ export class WAConnection extends Base { requestPresenceUpdate = async (jid: string) => this.query({ json: ['action', 'presence', 'subscribe', jid] }) /** Query the status of the person (see groupMetadata() for groups) */ async getStatus (jid?: string) { - const status: { status: string } = await this.query({ json: ['query', 'Status', jid || this.user.jid] }) + const status: { status: string } = await this.query({ json: ['query', 'Status', jid || this.user.jid], requiresPhoneConnection: false }) return status } async setStatus (status: string) { @@ -60,7 +60,7 @@ export class WAConnection extends Base { /** Get the stories of your contacts */ async getStories() { const json = ['query', { epoch: this.msgCount.toString(), type: 'status' }, null] - const response = await this.query({json, binaryTags: [30, WAFlag.ignore], expect200: true}) as WANode + const response = await this.query({json, binaryTags: [30, WAFlag.ignore], expect200: true }) as WANode if (Array.isArray(response[2])) { return response[2].map (row => ( { @@ -78,7 +78,7 @@ export class WAConnection extends Base { return this.query({ json, binaryTags: [5, WAFlag.ignore], expect200: true }) // this has to be an encrypted query } /** Query broadcast list info */ - async getBroadcastListInfo(jid: string) { return this.query({json: ['query', 'contact', jid], expect200: true}) as Promise } + async getBroadcastListInfo(jid: string) { return this.query({json: ['query', 'contact', jid], expect200: true }) as Promise } /** Delete the chat of a given ID */ async deleteChat (jid: string) { const response = await this.setQuery ([ ['chat', {type: 'delete', jid: jid}, null] ], [12, WAFlag.ignore]) as {status: number} diff --git a/src/WAConnection/6.MessagesSend.ts b/src/WAConnection/6.MessagesSend.ts index 770674b..8f5a571 100644 --- a/src/WAConnection/6.MessagesSend.ts +++ b/src/WAConnection/6.MessagesSend.ts @@ -266,7 +266,7 @@ export class WAConnection extends Base { /** Query a string to check if it has a url, if it does, return required extended text message */ async generateLinkPreview (text: string) { const query = ['query', {type: 'url', url: text, epoch: this.msgCount.toString()}, null] - const response = await this.query ({json: query, binaryTags: [26, WAFlag.ignore], expect200: true}) + const response = await this.query ({json: query, binaryTags: [26, WAFlag.ignore], expect200: true, requiresPhoneConnection: false}) if (response[1]) response[1].jpegThumbnail = response[2] const data = response[1] as WAUrlInfo @@ -289,7 +289,7 @@ export class WAConnection extends Base { return this.mediaConn } protected async getNewMediaConn () { - const {media_conn} = await this.query({json: ['query', 'mediaConn']}) + const {media_conn} = await this.query({json: ['query', 'mediaConn'], requiresPhoneConnection: false}) return media_conn as MediaConnInfo } } diff --git a/src/WAConnection/8.Groups.ts b/src/WAConnection/8.Groups.ts index a56ac9c..c0a5b31 100644 --- a/src/WAConnection/8.Groups.ts +++ b/src/WAConnection/8.Groups.ts @@ -145,7 +145,7 @@ export class WAConnection extends Base { /** Get the invite link of the given group */ async groupInviteCode(jid: string) { const json = ['query', 'inviteCode', jid] - const response = await this.query({json, expect200: true}) + const response = await this.query({json, expect200: true, requiresPhoneConnection: false}) return response.code as string } } \ No newline at end of file diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 68b2a90..4945a35 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -56,6 +56,7 @@ export interface WAQuery { expect200?: boolean waitForOpen?: boolean longTag?: boolean + requiresPhoneConnection?: boolean } export enum ReconnectMode { /** does not reconnect */