diff --git a/Example/example.ts b/Example/example.ts index a6ecff2..d3e56ab 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -9,7 +9,6 @@ import { ReconnectMode, ProxyAgent, waChatKey, - delay, } from '../src/WAConnection/WAConnection' import * as fs from 'fs' @@ -20,13 +19,6 @@ async function example() { // attempt to reconnect at most 10 times in a row conn.connectOptions.maxRetries = 10 conn.chatOrderingKey = waChatKey(true) // order chats such that pinned chats are on top - - conn.on ('credentials-updated', () => { - // save credentials whenever updated - console.log (`credentials updated`) - const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session - fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file - }) conn.on('chats-received', ({ hasNewChats }) => { console.log(`you have ${conn.chats.length} chats, new chats available: ${hasNewChats}`) }) @@ -41,6 +33,9 @@ async function example() { // uncomment the following line to proxy the connection; some random proxy I got off of: https://proxyscrape.com/free-proxy-list //conn.connectOptions.agent = ProxyAgent ('http://1.0.180.120:8080') await conn.connect() + // credentials are updated on every connect + const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session + fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file console.log('oh hello ' + conn.user.name + ' (' + conn.user.jid + ')') diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 79b43ed..cb01ddd 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -100,16 +100,20 @@ export class WAConnection extends EventEmitter { return null } async unexpectedDisconnect (error: DisconnectReason) { - const willReconnect = + if (this.state === 'open') { + const willReconnect = (this.autoReconnect === ReconnectMode.onAllErrors || (this.autoReconnect === ReconnectMode.onConnectionLost && error !== DisconnectReason.replaced)) && error !== DisconnectReason.invalidSession // do not reconnect if credentials have been invalidated - this.closeInternal(error, willReconnect) - willReconnect && ( - this.connect () - .catch(err => {}) // prevent unhandled exeception - ) + this.closeInternal(error, willReconnect) + willReconnect && ( + this.connect() + .catch(err => {}) // prevent unhandled exeception + ) + } else { + this.endConnection(error) + } } /** * base 64 encode the authentication credentials and return them @@ -313,7 +317,7 @@ export class WAConnection extends EventEmitter { protected startDebouncedTimeout () { this.stopDebouncedTimeout () this.debounceTimeout = setTimeout ( - () => this.emit('ws-close', { reason: DisconnectReason.timedOut }), + () => this.endConnection(DisconnectReason.timedOut), this.connectOptions.maxIdleTimeMs ) } @@ -394,11 +398,11 @@ export class WAConnection extends EventEmitter { this.lastDisconnectReason = reason this.lastDisconnectTime = new Date () - this.endConnection () + this.endConnection(reason) // reconnecting if the timeout is active for the reconnect loop this.emit ('close', { reason, isReconnecting }) } - protected endConnection () { + protected endConnection (reason: DisconnectReason) { this.conn?.removeAllListeners ('close') this.conn?.removeAllListeners ('error') this.conn?.removeAllListeners ('open') @@ -410,7 +414,7 @@ export class WAConnection extends EventEmitter { this.phoneCheckListeners = 0 this.clearPhoneCheckInterval () - this.emit ('ws-close', { reason: DisconnectReason.close }) + this.emit ('ws-close', { reason }) try { this.conn?.close() diff --git a/src/WAConnection/1.Validation.ts b/src/WAConnection/1.Validation.ts index 37bceff..81171c8 100644 --- a/src/WAConnection/1.Validation.ts +++ b/src/WAConnection/1.Validation.ts @@ -4,7 +4,7 @@ import {WAConnection as Base} from './0.Base' import { WAMetric, WAFlag, BaileysError, Presence, WAUser, WAInitResponse } from './Constants' export class WAConnection extends Base { - + /** Authenticate the connection */ protected async authenticate (reconnect?: string) { // if no auth info is present, that is, a new session has to be established @@ -87,33 +87,21 @@ export class WAConnection extends Base { response = await this.waitForMessage('s2', true) } - const newUser = this.validateNewConnection(response[1]) // validate the connection - if (newUser.jid !== this.user?.jid) { + const {user, auth} = this.validateNewConnection(response[1]) // validate the connection + if (user.jid !== this.user?.jid) { isNewUser = true // clear out old data this.chats.clear() this.contacts = {} } - this.user = newUser + this.user = user this.logger.info('validated connection successfully') - this.emit ('connection-validated', this.user) - 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') - return { isNewUser } + return { user, auth, isNewUser } } /** * Send the same queries WA Web sends after connect @@ -149,25 +137,22 @@ export class WAConnection extends Base { private validateNewConnection(json) { // set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone const onValidationSuccess = () => ({ - jid: Utils.whatsappID(json.wid), - name: json.pushname, - phone: json.phone, - imgUrl: null - }) as WAUser + user: { + jid: Utils.whatsappID(json.wid), + name: json.pushname, + phone: json.phone, + imgUrl: null + } as WAUser, + auth: this.authInfo + }) if (!json.secret) { - let credsChanged = false // if we didn't get a secret, we don't need it, we're validated if (json.clientToken && json.clientToken !== this.authInfo.clientToken) { this.authInfo = { ...this.authInfo, clientToken: json.clientToken } - credsChanged = true } if (json.serverToken && json.serverToken !== this.authInfo.serverToken) { this.authInfo = { ...this.authInfo, serverToken: json.serverToken } - credsChanged = true - } - if (credsChanged) { - this.emit ('credentials-updated', this.authInfo) } return onValidationSuccess() } @@ -208,8 +193,6 @@ export class WAConnection extends Base { serverToken: json.serverToken, clientID: this.authInfo.clientID, } - - this.emit ('credentials-updated', this.authInfo) return onValidationSuccess() } /** @@ -238,16 +221,17 @@ export class WAConnection extends Base { this.logger.debug ('regenerating QR') try { - const {ref: newRef, ttl} = await this.requestNewQRCodeRef() + const {ref: newRef, ttl: newTTL} = await this.requestNewQRCodeRef() + ttl = newTTL ref = newRef - - qrLoop (ttl) } catch (error) { this.logger.warn ({ error }, `error in QR gen`) if (error.status === 429) { // too many QR requests - this.emit ('ws-close', { reason: error.message }) + this.endConnection(error.message) + return } } + qrLoop (ttl) }, ttl || 20_000) // default is 20s, on the off-chance ttl is not present } qrLoop (ttl) diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 3998b5d..40a240f 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -23,12 +23,12 @@ export class WAConnection extends Base { let tries = 0 let lastConnect = this.lastDisconnectTime - let updates: any + let result: WAOpenResult while (this.state === 'connecting') { tries += 1 try { const diff = lastConnect ? new Date().getTime()-lastConnect.getTime() : Infinity - updates = await this.connectInternal ( + result = await this.connectInternal ( options, diff > this.connectOptions.connectCooldownMs ? 0 : this.connectOptions.connectCooldownMs ) @@ -49,7 +49,7 @@ export class WAConnection extends Base { if (!willReconnect) throw error } } - const result: WAOpenResult = { user: this.user, newConnection, ...(updates || {}) } + result.newConnection = newConnection this.emit ('open', result) this.logger.info ('opened connection to WhatsApp Web') @@ -100,14 +100,14 @@ export class WAConnection extends Base { .removeAllListeners('error') .removeAllListeners('close') this.stopDebouncedTimeout() - resolve(authResult) + resolve(authResult as WAOpenResult) } catch (error) { reject(error) } }) this.conn.on('error', rejectAll) this.conn.on('close', () => rejectAll(new Error(DisconnectReason.close))) - }) as Promise<{ isNewUser: boolean }> + }) as Promise ) this.on ('ws-close', rejectAllOnWSClose) @@ -120,7 +120,9 @@ export class WAConnection extends Base { const result = await connect () return result } catch (error) { - this.endConnection() + if (this.conn) { + this.endConnection(error.message) + } throw error } finally { this.off ('ws-close', rejectAllOnWSClose) @@ -142,8 +144,7 @@ export class WAConnection extends Base { } catch (error) { this.logger.error ({ error }, `encountered error in decrypting message, closing: ${error}`) - if (this.state === 'open') this.unexpectedDisconnect (DisconnectReason.badSession) - else this.emit ('ws-close', new Error(DisconnectReason.badSession)) + this.unexpectedDisconnect(DisconnectReason.badSession) } if (this.shouldLogMessages) this.messageLog.push ({ tag: messageTag, json: JSON.stringify(json), fromMe: false }) @@ -171,18 +172,6 @@ export class WAConnection extends Base { if (anyTriggered) return - if (this.state === 'open' && json[0] === 'Pong') { - if (!json[1]) { - this.unexpectedDisconnect(DisconnectReason.close) - this.logger.info('Connection terminated by phone, closing...') - return - } - 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)) } @@ -199,8 +188,8 @@ export class WAConnection extends Base { check if it's been a suspicious amount of time since the server responded with our last seen it could be that the network is down */ - 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 + 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 }, KEEP_ALIVE_INTERVAL_MS) } } diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index c20f99a..d764369 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -11,9 +11,18 @@ export class WAConnection extends Base { super () this.setMaxListeners (30) // on disconnects - this.on ('CB:Cmd,type:disconnect', json => ( + this.on('CB:Cmd,type:disconnect', json => ( this.state === 'open' && this.unexpectedDisconnect(json[1].kind || 'unknown') )) + this.on('CB:Pong', json => { + if (!json[1]) { + this.unexpectedDisconnect(DisconnectReason.close) + this.logger.info('Connection terminated by phone, closing...') + } else if (this.phoneConnected !== json[1]) { + this.phoneConnected = json[1] + this.emit ('connection-phone-change', { connected: this.phoneConnected }) + } + }) // chats received this.on('CB:response,type:chat', json => { if (json[1].duplicate || !json[2]) return @@ -600,13 +609,11 @@ export class WAConnection extends Base { /** when the connection is opening */ on (event: 'connecting', listener: () => void): this /** when the connection has been validated */ - on (event: 'connection-validated', listener: (user: WAUser) => void): this + on (event: 'connection-validated', listener: (item: { user: WAUser, auth: AuthenticationCredentials, isNewUser: boolean }) => void): this /** when the connection has closed */ on (event: 'close', listener: (err: {reason?: DisconnectReason | string, isReconnecting: boolean}) => void): this /** when the socket is closed */ on (event: 'ws-close', listener: (err: {reason?: DisconnectReason | string}) => void): this - /** when WA updates the credentials */ - on (event: 'credentials-updated', listener: (auth: AuthenticationCredentials) => 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 dedf465..1721d24 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -405,7 +405,7 @@ export interface WAOpenResult { newConnection: boolean user: WAUser isNewUser: boolean - hasNewChats?: boolean + auth: AuthenticationCredentials } export enum GroupSettingChange { @@ -451,15 +451,14 @@ export type BaileysEvent = 'close' | 'ws-close' | 'qr' | - 'connection-validated' | 'connection-phone-change' | 'contacts-received' | 'chats-received' | + 'initial-data-received' | 'chat-new' | 'chat-update' | 'group-participants-update' | 'group-update' | 'received-pong' | - 'credentials-updated' | 'blocklist-update' | 'contact-update' \ No newline at end of file