mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
More accurate phone connection detection
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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 ()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<WABroadcastListInfo> }
|
||||
async getBroadcastListInfo(jid: string) { return this.query({json: ['query', 'contact', jid], expect200: true }) as Promise<WABroadcastListInfo> }
|
||||
/** 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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export interface WAQuery {
|
||||
expect200?: boolean
|
||||
waitForOpen?: boolean
|
||||
longTag?: boolean
|
||||
requiresPhoneConnection?: boolean
|
||||
}
|
||||
export enum ReconnectMode {
|
||||
/** does not reconnect */
|
||||
|
||||
Reference in New Issue
Block a user