mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Pending Requests
This commit is contained in:
@@ -32,6 +32,12 @@ export default class WAConnectionBase {
|
|||||||
lastSeen: Date = null
|
lastSeen: Date = null
|
||||||
/** Log messages that are not handled, so you can debug & see what custom stuff you can implement */
|
/** Log messages that are not handled, so you can debug & see what custom stuff you can implement */
|
||||||
logLevel: MessageLogLevel = MessageLogLevel.none
|
logLevel: MessageLogLevel = MessageLogLevel.none
|
||||||
|
/** Should requests be queued when the connection breaks in between; if false, then an error will be thrown */
|
||||||
|
pendingRequestTimeoutMs: number = null
|
||||||
|
/** What to do when you need the phone to authenticate the connection (generate QR code by default) */
|
||||||
|
onReadyForPhoneAuthentication = generateQRCode
|
||||||
|
|
||||||
|
protected unexpectedDisconnectCallback: (err: string) => any
|
||||||
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */
|
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */
|
||||||
protected authInfo: AuthenticationCredentials = {
|
protected authInfo: AuthenticationCredentials = {
|
||||||
clientID: null,
|
clientID: null,
|
||||||
@@ -49,14 +55,22 @@ export default class WAConnectionBase {
|
|||||||
protected callbacks = {}
|
protected callbacks = {}
|
||||||
protected encoder = new Encoder()
|
protected encoder = new Encoder()
|
||||||
protected decoder = new Decoder()
|
protected decoder = new Decoder()
|
||||||
/** What to do when you need the phone to authenticate the connection (generate QR code by default) */
|
protected pendingRequests: (() => void)[] = []
|
||||||
onReadyForPhoneAuthentication = generateQRCode
|
protected reconnectLoop: () => Promise<void>
|
||||||
unexpectedDisconnect = (err: string) => this.close()
|
|
||||||
|
constructor () {
|
||||||
|
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind))
|
||||||
|
}
|
||||||
|
async unexpectedDisconnect (error: string) {
|
||||||
|
this.close()
|
||||||
|
if ((error === 'lost' || error === 'closed') && this.autoReconnect) {
|
||||||
|
await this.reconnectLoop ()
|
||||||
|
}
|
||||||
|
if (this.unexpectedDisconnectCallback) this.unexpectedDisconnectCallback (error)
|
||||||
|
}
|
||||||
/** Set the callback for unexpected disconnects including take over events, log out events etc. */
|
/** Set the callback for unexpected disconnects including take over events, log out events etc. */
|
||||||
setOnUnexpectedDisconnect(callback: (error: string) => void) {
|
setOnUnexpectedDisconnect(callback: (error: string) => void) {
|
||||||
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind))
|
this.unexpectedDisconnectCallback = callback
|
||||||
this.unexpectedDisconnect = err => { this.close(); callback(err) }
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* base 64 encode the authentication credentials and return them
|
* base 64 encode the authentication credentials and return them
|
||||||
@@ -98,9 +112,8 @@ export default class WAConnectionBase {
|
|||||||
* @param authInfo the authentication credentials or path to browser credentials JSON
|
* @param authInfo the authentication credentials or path to browser credentials JSON
|
||||||
*/
|
*/
|
||||||
loadAuthInfoFromBrowser(authInfo: AuthenticationCredentialsBrowser | string) {
|
loadAuthInfoFromBrowser(authInfo: AuthenticationCredentialsBrowser | string) {
|
||||||
if (!authInfo) {
|
if (!authInfo) throw new Error('given authInfo is null')
|
||||||
throw new Error('given authInfo is null')
|
|
||||||
}
|
|
||||||
if (typeof authInfo === 'string') {
|
if (typeof authInfo === 'string') {
|
||||||
this.log(`loading authentication credentials from ${authInfo}`)
|
this.log(`loading authentication credentials from ${authInfo}`)
|
||||||
const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists
|
const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists
|
||||||
@@ -203,27 +216,25 @@ export default class WAConnectionBase {
|
|||||||
/**
|
/**
|
||||||
* Query something from the WhatsApp servers
|
* Query something from the WhatsApp servers
|
||||||
* @param json the query itself
|
* @param json the query itself
|
||||||
* @param [binaryTags] the tags to attach if the query is supposed to be sent encoded in binary
|
* @param binaryTags the tags to attach if the query is supposed to be sent encoded in binary
|
||||||
* @param [timeoutMs] timeout after which the query will be failed (set to null to disable a timeout)
|
* @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
|
* @param tag the tag to attach to the message
|
||||||
* recieved JSON
|
* recieved JSON
|
||||||
*/
|
*/
|
||||||
async query(json: any[] | WANode, binaryTags: WATag = null, timeoutMs: number = null, tag: string = null) {
|
async query(json: any[] | WANode, binaryTags: WATag = null, timeoutMs: number = null, tag: string = null) {
|
||||||
if (binaryTags) {
|
if (binaryTags) tag = await this.sendBinary(json as WANode, binaryTags, tag)
|
||||||
tag = this.sendBinary(json as WANode, binaryTags, tag)
|
else tag = await this.sendJSON(json, tag)
|
||||||
} else {
|
|
||||||
tag = this.sendJSON(json, tag)
|
|
||||||
}
|
|
||||||
return this.waitForMessage(tag, json, timeoutMs)
|
return this.waitForMessage(tag, json, timeoutMs)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Send a binary encoded message
|
* Send a binary encoded message
|
||||||
* @param json the message to encode & send
|
* @param json the message to encode & send
|
||||||
* @param {[number, number]} tags the binary tags to tell WhatsApp what the message is all about
|
* @param tags the binary tags to tell WhatsApp what the message is all about
|
||||||
* @param {string} [tag] the tag to attach to the message
|
* @param tag the tag to attach to the message
|
||||||
* @return {string} the message tag
|
* @return the message tag
|
||||||
*/
|
*/
|
||||||
private sendBinary(json: WANode, tags: [number, number], tag: string) {
|
private async sendBinary(json: WANode, tags: WATag, tag: string) {
|
||||||
const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format
|
const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format
|
||||||
|
|
||||||
let buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey
|
let buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey
|
||||||
@@ -235,38 +246,42 @@ export default class WAConnectionBase {
|
|||||||
sign, // the HMAC sign of the message
|
sign, // the HMAC sign of the message
|
||||||
buff, // the actual encrypted buffer
|
buff, // the actual encrypted buffer
|
||||||
])
|
])
|
||||||
this.send(buff) // send it off
|
await this.send(buff) // send it off
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Send a plain JSON message to the WhatsApp servers
|
* Send a plain JSON message to the WhatsApp servers
|
||||||
* @private
|
|
||||||
* @param json the message to send
|
* @param json the message to send
|
||||||
* @param [tag] the tag to attach to the message
|
* @param tag the tag to attach to the message
|
||||||
* @return the message tag
|
* @return the message tag
|
||||||
*/
|
*/
|
||||||
private sendJSON(json: any[] | WANode, tag: string = null) {
|
private async sendJSON(json: any[] | WANode, tag: string = null) {
|
||||||
tag = tag || Utils.generateMessageTag(this.msgCount)
|
tag = tag || Utils.generateMessageTag(this.msgCount)
|
||||||
this.send(tag + ',' + JSON.stringify(json))
|
await this.send(tag + ',' + JSON.stringify(json))
|
||||||
return tag
|
return tag
|
||||||
}
|
}
|
||||||
/** Send some message to the WhatsApp servers */
|
/** Send some message to the WhatsApp servers */
|
||||||
protected send(m) {
|
protected async send(m) {
|
||||||
if (!this.conn) {
|
if (!this.conn) {
|
||||||
throw new Error('cannot send message, disconnected from WhatsApp')
|
const timeout = this.pendingRequestTimeoutMs
|
||||||
|
try {
|
||||||
|
const task = new Promise (resolve => this.pendingRequests.push(resolve))
|
||||||
|
await Utils.promiseTimeout (timeout, task)
|
||||||
|
} catch {
|
||||||
|
throw new Error('cannot send message, disconnected from WhatsApp')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages
|
this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages
|
||||||
this.conn.send(m)
|
return this.conn.send(m)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
|
* Disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
|
||||||
* @see close() if you just want to close the connection
|
* @see close() if you just want to close the connection
|
||||||
*/
|
*/
|
||||||
async logout() {
|
async logout() {
|
||||||
if (!this.conn) {
|
if (!this.conn) throw new Error("You're not even connected, you can't log out")
|
||||||
throw new Error("You're not even connected, you can't log out")
|
|
||||||
}
|
await new Promise(resolve => {
|
||||||
await new Promise((resolve) => {
|
|
||||||
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => {
|
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => {
|
||||||
this.authInfo = null
|
this.authInfo = null
|
||||||
resolve()
|
resolve()
|
||||||
@@ -274,6 +289,7 @@ export default class WAConnectionBase {
|
|||||||
})
|
})
|
||||||
this.close()
|
this.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Close the connection to WhatsApp Web */
|
/** Close the connection to WhatsApp Web */
|
||||||
close() {
|
close() {
|
||||||
this.msgCount = 0
|
this.msgCount = 0
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
|||||||
*/
|
*/
|
||||||
async connectSlim(authInfo: AuthenticationCredentialsBase64 | string = null, timeoutMs: number = null) {
|
async connectSlim(authInfo: AuthenticationCredentialsBase64 | string = null, timeoutMs: number = null) {
|
||||||
// if we're already connected, throw an error
|
// if we're already connected, throw an error
|
||||||
if (this.conn) {
|
if (this.conn) throw new Error('already connected or connecting')
|
||||||
throw new Error('already connected or connecting')
|
|
||||||
}
|
|
||||||
// set authentication credentials if required
|
// set authentication credentials if required
|
||||||
try {
|
try {
|
||||||
this.loadAuthInfoFromBase64(authInfo)
|
this.loadAuthInfoFromBase64(authInfo)
|
||||||
@@ -34,7 +32,7 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
|||||||
|
|
||||||
this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' })
|
this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' })
|
||||||
|
|
||||||
let promise: Promise<UserMetaData> = new Promise((resolve, reject) => {
|
const promise: Promise<UserMetaData> = new Promise((resolve, reject) => {
|
||||||
this.conn.on('open', () => {
|
this.conn.on('open', () => {
|
||||||
this.log('connected to WhatsApp Web, authenticating...')
|
this.log('connected to WhatsApp Web, authenticating...')
|
||||||
// start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
|
// start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
|
||||||
@@ -53,11 +51,12 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
|||||||
// if there was an error in the WebSocket
|
// if there was an error in the WebSocket
|
||||||
this.conn.on('error', error => { this.close(); reject(error) })
|
this.conn.on('error', error => { this.close(); reject(error) })
|
||||||
})
|
})
|
||||||
promise = Utils.promiseTimeout(timeoutMs, promise)
|
const user = await Utils.promiseTimeout(timeoutMs, promise).catch(err => {this.close(); throw err})
|
||||||
return promise.catch(err => {
|
|
||||||
this.close()
|
this.pendingRequests.forEach (send => send()) // send off all pending request
|
||||||
throw err
|
this.pendingRequests = []
|
||||||
})
|
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sets up callbacks to receive chats, contacts & unread messages.
|
* Sets up callbacks to receive chats, contacts & unread messages.
|
||||||
@@ -207,23 +206,16 @@ export default class WAConnectionConnector extends WAConnectionValidator {
|
|||||||
const diff = (new Date().getTime() - this.lastSeen.getTime()) / 1000
|
const diff = (new Date().getTime() - this.lastSeen.getTime()) / 1000
|
||||||
/*
|
/*
|
||||||
check if it's been a suspicious amount of time since the server responded with our last seen
|
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, or the phone got unpaired from our connection
|
it could be that the network is down
|
||||||
*/
|
*/
|
||||||
if (diff > refreshInterval + 5) {
|
if (diff > refreshInterval + 5) this.unexpectedDisconnect ('lost')
|
||||||
this.close()
|
else this.send ('?,,') // if its all good, send a keep alive request
|
||||||
|
|
||||||
if (this.autoReconnect) {
|
|
||||||
// attempt reconnecting if the user wants us to
|
|
||||||
this.log('disconnected unexpectedly, reconnecting...')
|
|
||||||
const reconnectLoop = () => this.connect(null, 25 * 1000).catch(reconnectLoop)
|
|
||||||
reconnectLoop() // keep trying to connect
|
|
||||||
} else {
|
|
||||||
this.unexpectedDisconnect('lost connection unexpectedly')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if its all good, send a keep alive request
|
|
||||||
this.send('?,,')
|
|
||||||
}
|
|
||||||
}, refreshInterval * 1000)
|
}, refreshInterval * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reconnectLoop = async () => {
|
||||||
|
// attempt reconnecting if the user wants us to
|
||||||
|
this.log('network is down, reconnecting...')
|
||||||
|
return this.connectSlim(null, 25*1000).catch(this.reconnectLoop)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,33 @@ describe('Test Connect', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await conn.logout()
|
await conn.logout()
|
||||||
|
|
||||||
await assert.rejects(async () => conn.connectSlim(auth), 'reconnect should have failed')
|
await assert.rejects(async () => conn.connectSlim(auth), 'reconnect should have failed')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
describe ('Pending Requests', async () => {
|
||||||
|
it('should queue requests when closed', async () => {
|
||||||
|
const conn = new WAConnection ()
|
||||||
|
conn.pendingRequestTimeoutMs = null
|
||||||
|
|
||||||
|
await conn.connectSlim ()
|
||||||
|
|
||||||
|
await createTimeout (2000)
|
||||||
|
|
||||||
|
conn.close ()
|
||||||
|
|
||||||
|
const task: Promise<any> = new Promise ((resolve, reject) => {
|
||||||
|
conn.query(['query', 'Status', conn.userMetaData.id])
|
||||||
|
.then (json => resolve(json))
|
||||||
|
.catch (error => reject ('should not have failed, got error: ' + error))
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTimeout (2000)
|
||||||
|
|
||||||
|
await conn.connectSlim ()
|
||||||
|
const json = await task
|
||||||
|
|
||||||
|
assert.ok (json.status)
|
||||||
|
|
||||||
|
conn.close ()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user