mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
More reliable connect with automatic retries + default connect options
This commit is contained in:
@@ -19,18 +19,16 @@ async function example() {
|
|||||||
// loads the auth file credentials if present
|
// loads the auth file credentials if present
|
||||||
fs.existsSync('./auth_info.json') && conn.loadAuthInfo ('./auth_info.json')
|
fs.existsSync('./auth_info.json') && conn.loadAuthInfo ('./auth_info.json')
|
||||||
|
|
||||||
/* Called when contacts are received,
|
|
||||||
* do note, that this method may be called before the connection is done completely because WA is funny sometimes
|
|
||||||
* */
|
|
||||||
conn.on ('contacts-received', contacts => console.log(`received ${Object.keys(contacts).length} contacts`))
|
|
||||||
|
|
||||||
// connect or timeout in 60 seconds
|
// connect or timeout in 60 seconds
|
||||||
await conn.connect({ timeoutMs: 60 * 1000, retryOnNetworkErrors: true })
|
conn.connectOptions.timeoutMs = 60*1000
|
||||||
|
// attempt to reconnect at most 10 times
|
||||||
|
conn.connectOptions.maxRetries = 10
|
||||||
|
await conn.connect()
|
||||||
|
|
||||||
const unread = await conn.loadAllUnreadMessages ()
|
const unread = await conn.loadAllUnreadMessages ()
|
||||||
|
|
||||||
console.log('oh hello ' + conn.user.name + ' (' + conn.user.jid + ')')
|
console.log('oh hello ' + conn.user.name + ' (' + conn.user.jid + ')')
|
||||||
console.log('you have ' + conn.chats.all().length + ' chats')
|
console.log('you have ' + conn.chats.all().length + ' chats & ' + Object.keys(conn.contacts).length + ' contacts')
|
||||||
console.log ('you have ' + unread.length + ' unread messages')
|
console.log ('you have ' + unread.length + ' unread messages')
|
||||||
|
|
||||||
const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session
|
const authInfo = conn.base64EncodedAuthInfo() // get all the auth info we need to restore this session
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -43,8 +43,7 @@ import { WAConnection } from '@adiwajshing/baileys'
|
|||||||
async function connectToWhatsApp () {
|
async function connectToWhatsApp () {
|
||||||
const conn = new WAConnection()
|
const conn = new WAConnection()
|
||||||
|
|
||||||
// 20 second timeout
|
await conn.connect ()
|
||||||
await conn.connect ({timeoutMs: 30*1000})
|
|
||||||
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")")
|
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")")
|
||||||
// every chat object has a list of most recent messages
|
// every chat object has a list of most recent messages
|
||||||
console.log ("you have " + conn.chats.all().length + " chats")
|
console.log ("you have " + conn.chats.all().length + " chats")
|
||||||
@@ -60,9 +59,9 @@ connectToWhatsApp ()
|
|||||||
|
|
||||||
If the connection is successful, you will see a QR code printed on your terminal screen, scan it with WhatsApp on your phone and you'll be logged in!
|
If the connection is successful, you will see a QR code printed on your terminal screen, scan it with WhatsApp on your phone and you'll be logged in!
|
||||||
|
|
||||||
If you don't want to wait for WhatsApp to send all your chats while connecting, you can use the following function:
|
If you don't want to wait for WhatsApp to send all your chats while connecting, you can set the following property to false:
|
||||||
``` ts
|
``` ts
|
||||||
await conn.connect ({timeoutMs: 30*1000}, false)
|
conn.connectOptions.waitForChats = false
|
||||||
```
|
```
|
||||||
|
|
||||||
Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons:
|
Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons:
|
||||||
@@ -103,9 +102,9 @@ await conn.connect() // works the same
|
|||||||
```
|
```
|
||||||
See the browser credentials type in the docs.
|
See the browser credentials type in the docs.
|
||||||
|
|
||||||
## QR Overriding
|
## QR Callback
|
||||||
|
|
||||||
If you want to do some custom processing with the QR code used to authenticate, you can override the following method:
|
If you want to do some custom processing with the QR code used to authenticate, you can register for the following event:
|
||||||
``` ts
|
``` ts
|
||||||
conn.on('qr', qr => {
|
conn.on('qr', qr => {
|
||||||
// Now, use the 'qr' string to display in QR UI or send somewhere
|
// Now, use the 'qr' string to display in QR UI or send somewhere
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe('QR Generation', () => {
|
|||||||
conn.removeAllListeners ('qr')
|
conn.removeAllListeners ('qr')
|
||||||
conn.on ('qr', qr => calledQR += 1)
|
conn.on ('qr', qr => calledQR += 1)
|
||||||
|
|
||||||
await conn.connect({ timeoutMs: 15000 })
|
await conn.connect()
|
||||||
.then (() => assert.fail('should not have succeeded'))
|
.then (() => assert.fail('should not have succeeded'))
|
||||||
.catch (error => {
|
.catch (error => {
|
||||||
assert.equal (error.message, 'timed out')
|
assert.equal (error.message, 'timed out')
|
||||||
@@ -49,11 +49,14 @@ describe('Test Connect', () => {
|
|||||||
conn.loadAuthInfo ('./auth_info.json')
|
conn.loadAuthInfo ('./auth_info.json')
|
||||||
|
|
||||||
let timeout = 0.1
|
let timeout = 0.1
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
setTimeout (() => conn.close(), timeout*1000)
|
let tmout = setTimeout (() => conn.close(), timeout*1000)
|
||||||
try {
|
try {
|
||||||
await conn.connect ()
|
await conn.connect ()
|
||||||
|
|
||||||
|
clearTimeout (tmout)
|
||||||
|
conn.close ()
|
||||||
|
|
||||||
break
|
break
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
@@ -64,9 +67,10 @@ describe('Test Connect', () => {
|
|||||||
})
|
})
|
||||||
it('should reconnect', async () => {
|
it('should reconnect', async () => {
|
||||||
const conn = new WAConnection()
|
const conn = new WAConnection()
|
||||||
|
conn.connectOptions.timeoutMs = 20*1000
|
||||||
await conn
|
await conn
|
||||||
.loadAuthInfo (auth)
|
.loadAuthInfo (auth)
|
||||||
.connect ({timeoutMs: 20*1000})
|
.connect ()
|
||||||
.then (conn => {
|
.then (conn => {
|
||||||
assert.ok(conn.user)
|
assert.ok(conn.user)
|
||||||
assert.ok(conn.user.jid)
|
assert.ok(conn.user.jid)
|
||||||
@@ -135,7 +139,7 @@ describe ('Reconnects', () => {
|
|||||||
closes += 1
|
closes += 1
|
||||||
|
|
||||||
// let it fail reconnect a few times
|
// let it fail reconnect a few times
|
||||||
if (closes > 3) {
|
if (closes >= 1) {
|
||||||
conn.removeAllListeners ('close')
|
conn.removeAllListeners ('close')
|
||||||
conn.removeAllListeners ('connecting')
|
conn.removeAllListeners ('connecting')
|
||||||
resolve ()
|
resolve ()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
WAChat,
|
WAChat,
|
||||||
WAQuery,
|
WAQuery,
|
||||||
ReconnectMode,
|
ReconnectMode,
|
||||||
|
WAConnectOptions,
|
||||||
} from './Constants'
|
} from './Constants'
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import KeyedDB from '@adiwajshing/keyed-db'
|
import KeyedDB from '@adiwajshing/keyed-db'
|
||||||
@@ -38,7 +39,12 @@ export class WAConnection extends EventEmitter {
|
|||||||
state: WAConnectionState = 'close'
|
state: WAConnectionState = 'close'
|
||||||
/** New QR generation interval, set to null if you don't want to regenerate */
|
/** New QR generation interval, set to null if you don't want to regenerate */
|
||||||
regenerateQRIntervalMs = 30*1000
|
regenerateQRIntervalMs = 30*1000
|
||||||
|
connectOptions: WAConnectOptions = {
|
||||||
|
timeoutMs: 60*1000,
|
||||||
|
waitForChats: true,
|
||||||
|
maxRetries: 5
|
||||||
|
}
|
||||||
|
/** When to auto-reconnect */
|
||||||
autoReconnect = ReconnectMode.onConnectionLost
|
autoReconnect = ReconnectMode.onConnectionLost
|
||||||
/** Whether the phone is connected */
|
/** Whether the phone is connected */
|
||||||
phoneConnected: boolean = false
|
phoneConnected: boolean = false
|
||||||
@@ -46,6 +52,7 @@ export class WAConnection extends EventEmitter {
|
|||||||
maxCachedMessages = 25
|
maxCachedMessages = 25
|
||||||
|
|
||||||
chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
|
chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
|
||||||
|
contacts: { [k: string]: WAContact } = {}
|
||||||
|
|
||||||
/** 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 = null
|
protected authInfo: AuthenticationCredentials = null
|
||||||
@@ -62,24 +69,30 @@ export class WAConnection extends EventEmitter {
|
|||||||
protected referenceDate = new Date () // used for generating tags
|
protected referenceDate = new Date () // used for generating tags
|
||||||
protected lastSeen: Date = null // last keep alive received
|
protected lastSeen: Date = null // last keep alive received
|
||||||
protected qrTimeout: NodeJS.Timeout
|
protected qrTimeout: NodeJS.Timeout
|
||||||
protected phoneCheck: NodeJS.Timeout
|
|
||||||
|
|
||||||
protected lastDisconnectReason: DisconnectReason
|
protected lastDisconnectReason: DisconnectReason
|
||||||
protected cancelledReconnect = false
|
|
||||||
protected cancelReconnect: () => void
|
|
||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
super ()
|
super ()
|
||||||
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind || 'unknown'))
|
this.registerCallback (['Cmd', 'type:disconnect'], json => (
|
||||||
|
this.unexpectedDisconnect(json[1].kind || 'unknown')
|
||||||
|
))
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Connect to WhatsAppWeb
|
||||||
|
* @param options the connect options
|
||||||
|
*/
|
||||||
|
async connect() {
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
async unexpectedDisconnect (error: DisconnectReason) {
|
async unexpectedDisconnect (error: DisconnectReason) {
|
||||||
const willReconnect =
|
const willReconnect =
|
||||||
(this.autoReconnect === ReconnectMode.onAllErrors ||
|
(this.autoReconnect === ReconnectMode.onAllErrors ||
|
||||||
(this.autoReconnect === ReconnectMode.onConnectionLost && (error !== DisconnectReason.replaced))) &&
|
(this.autoReconnect === ReconnectMode.onConnectionLost && error !== DisconnectReason.replaced)) &&
|
||||||
error !== DisconnectReason.invalidSession // do not reconnect if credentials have been invalidated
|
error !== DisconnectReason.invalidSession // do not reconnect if credentials have been invalidated
|
||||||
|
|
||||||
this.closeInternal(error, willReconnect)
|
this.closeInternal(error, willReconnect)
|
||||||
willReconnect && !this.cancelReconnect && this.reconnectLoop ()
|
willReconnect && this.connect ()
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* base 64 encode the authentication credentials and return them
|
* base 64 encode the authentication credentials and return them
|
||||||
@@ -213,6 +226,7 @@ export class WAConnection extends EventEmitter {
|
|||||||
|
|
||||||
const response = await this.waitForMessage(tag, json, timeoutMs)
|
const response = await this.waitForMessage(tag, json, timeoutMs)
|
||||||
if (expect200 && response.status && Math.floor(+response.status / 100) !== 2) {
|
if (expect200 && response.status && Math.floor(+response.status / 100) !== 2) {
|
||||||
|
// read here: http://getstatuscode.com/599
|
||||||
if (response.status === 599) {
|
if (response.status === 599) {
|
||||||
this.unexpectedDisconnect (DisconnectReason.badSession)
|
this.unexpectedDisconnect (DisconnectReason.badSession)
|
||||||
const response = await this.query ({json, binaryTags, tag, timeoutMs, expect200, waitForOpen})
|
const response = await this.query ({json, binaryTags, tag, timeoutMs, expect200, waitForOpen})
|
||||||
@@ -286,13 +300,11 @@ export class WAConnection extends EventEmitter {
|
|||||||
/** Close the connection to WhatsApp Web */
|
/** Close the connection to WhatsApp Web */
|
||||||
close () {
|
close () {
|
||||||
this.closeInternal (DisconnectReason.intentional)
|
this.closeInternal (DisconnectReason.intentional)
|
||||||
this.cancelReconnect && this.cancelReconnect ()
|
|
||||||
}
|
}
|
||||||
protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) {
|
protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) {
|
||||||
this.log (`closed connection, reason ${reason}${isReconnecting ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info)
|
this.log (`closed connection, reason ${reason}${isReconnecting ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info)
|
||||||
|
|
||||||
this.qrTimeout && clearTimeout (this.qrTimeout)
|
this.qrTimeout && clearTimeout (this.qrTimeout)
|
||||||
this.phoneCheck && clearTimeout (this.phoneCheck)
|
|
||||||
this.keepAliveReq && clearInterval(this.keepAliveReq)
|
this.keepAliveReq && clearInterval(this.keepAliveReq)
|
||||||
|
|
||||||
this.state = 'close'
|
this.state = 'close'
|
||||||
@@ -308,6 +320,11 @@ export class WAConnection extends EventEmitter {
|
|||||||
this.pendingRequests = []
|
this.pendingRequests = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.removePendingCallbacks ()
|
||||||
|
// reconnecting if the timeout is active for the reconnect loop
|
||||||
|
this.emit ('close', { reason, isReconnecting })
|
||||||
|
}
|
||||||
|
protected removePendingCallbacks () {
|
||||||
Object.keys(this.callbacks).forEach(key => {
|
Object.keys(this.callbacks).forEach(key => {
|
||||||
if (!key.includes('function:')) {
|
if (!key.includes('function:')) {
|
||||||
this.log (`cancelling message wait: ${key}`, MessageLogLevel.info)
|
this.log (`cancelling message wait: ${key}`, MessageLogLevel.info)
|
||||||
@@ -315,12 +332,6 @@ export class WAConnection extends EventEmitter {
|
|||||||
delete this.callbacks[key]
|
delete this.callbacks[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// reconnecting if the timeout is active for the reconnect loop
|
|
||||||
this.emit ('close', { reason, isReconnecting: this.cancelReconnect || isReconnecting})
|
|
||||||
}
|
|
||||||
protected async reconnectLoop () {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
generateMessageTag () {
|
generateMessageTag () {
|
||||||
return `${Utils.unixTimestampSeconds(this.referenceDate)}.--${this.msgCount}`
|
return `${Utils.unixTimestampSeconds(this.referenceDate)}.--${this.msgCount}`
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ export class WAConnection extends Base {
|
|||||||
this.log('validated connection successfully', MessageLogLevel.info)
|
this.log('validated connection successfully', MessageLogLevel.info)
|
||||||
|
|
||||||
this.sendPostConnectQueries ()
|
this.sendPostConnectQueries ()
|
||||||
this.lastSeen = new Date() // set last seen to right now
|
|
||||||
})
|
})
|
||||||
// load profile picture
|
// load profile picture
|
||||||
.then (() => this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false }))
|
.then (() => this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false }))
|
||||||
|
|||||||
@@ -1,83 +1,143 @@
|
|||||||
import * as Utils from './Utils'
|
import * as Utils from './Utils'
|
||||||
import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason } from './Constants'
|
import { WAMessage, WAChat, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError } from './Constants'
|
||||||
import {WAConnection as Base} from './1.Validation'
|
import {WAConnection as Base} from './1.Validation'
|
||||||
import Decoder from '../Binary/Decoder'
|
import Decoder from '../Binary/Decoder'
|
||||||
|
|
||||||
export class WAConnection extends Base {
|
export class WAConnection extends Base {
|
||||||
/**
|
/** Connect to WhatsApp Web */
|
||||||
* Connect to WhatsAppWeb
|
async connect() {
|
||||||
* @param options the connect options
|
|
||||||
*/
|
|
||||||
async connect(options: WAConnectOptions = {}) {
|
|
||||||
// if we're already connected, throw an error
|
// if we're already connected, throw an error
|
||||||
if (this.state !== 'close') throw new Error('cannot connect when state=' + this.state)
|
if (this.state !== 'close') throw new Error('cannot connect when state=' + this.state)
|
||||||
|
|
||||||
|
const options = this.connectOptions
|
||||||
|
|
||||||
this.state = 'connecting'
|
this.state = 'connecting'
|
||||||
this.emit ('connecting')
|
this.emit ('connecting')
|
||||||
|
|
||||||
const { ws, cancel } = Utils.openWebSocketConnection (5000, typeof options?.retryOnNetworkErrors === 'undefined' ? true : options?.retryOnNetworkErrors)
|
let tries = 0
|
||||||
const promise = Utils.promiseTimeout(options?.timeoutMs, (resolve, reject) => {
|
while (this.state === 'connecting') {
|
||||||
ws
|
tries += 1
|
||||||
.then (conn => this.conn = conn)
|
try {
|
||||||
.then (() => this.conn.on('message', data => this.onMessageRecieved(data as any)))
|
// if the first try failed, delay & connect again
|
||||||
.then (() => this.log(`connected to WhatsApp Web server, authenticating via ${options.reconnectID ? 'reconnect' : 'takeover'}`, MessageLogLevel.info))
|
await this.connectInternal (options, tries > 1 && 2000)
|
||||||
.then (() => this.authenticate(options?.reconnectID))
|
|
||||||
.then (() => {
|
|
||||||
this.conn.removeAllListeners ('error')
|
|
||||||
this.conn.removeAllListeners ('close')
|
|
||||||
this.conn.on ('close', () => this.unexpectedDisconnect (DisconnectReason.close))
|
|
||||||
})
|
|
||||||
.then (resolve)
|
|
||||||
.catch (reject)
|
|
||||||
})
|
|
||||||
.catch (err => {
|
|
||||||
cancel ()
|
|
||||||
throw err
|
|
||||||
}) as Promise<void>
|
|
||||||
|
|
||||||
|
this.phoneConnected = true
|
||||||
|
this.state = 'open'
|
||||||
|
} catch (error) {
|
||||||
|
const loggedOut = error instanceof BaileysError && UNAUTHORIZED_CODES.includes(error.status)
|
||||||
|
const willReconnect = !loggedOut && (tries <= (options?.maxRetries || 5)) && this.state === 'connecting'
|
||||||
|
|
||||||
|
this.log (`connect attempt ${tries} failed: ${error}${ willReconnect ? ', retrying...' : ''}`, MessageLogLevel.info)
|
||||||
|
|
||||||
|
if ((this.state as string) !== 'close' && !willReconnect) {
|
||||||
|
this.closeInternal (loggedOut ? DisconnectReason.invalidSession : error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!willReconnect) throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit ('open')
|
||||||
|
|
||||||
|
this.releasePendingRequests ()
|
||||||
|
this.startKeepAliveRequest()
|
||||||
|
|
||||||
|
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
|
||||||
|
|
||||||
|
this.conn.on ('close', () => this.unexpectedDisconnect (DisconnectReason.close))
|
||||||
|
|
||||||
|
return this
|
||||||
|
|
||||||
|
}
|
||||||
|
/** Meat of the connect logic */
|
||||||
|
protected async connectInternal (options: WAConnectOptions, delayMs?: number) {
|
||||||
|
// actual connect
|
||||||
|
const connect = () => {
|
||||||
|
const tasks: Promise<void>[] = []
|
||||||
|
const timeoutMs = options?.timeoutMs || 60*1000
|
||||||
|
|
||||||
|
const { ws, cancel } = Utils.openWebSocketConnection (5000, false)
|
||||||
|
|
||||||
|
let cancelTask: () => void
|
||||||
|
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
||||||
|
|
||||||
|
const {waitForChats, cancelChats} = this.receiveChatsAndContacts(timeoutMs, true)
|
||||||
|
tasks.push (waitForChats)
|
||||||
|
|
||||||
|
cancellations.push (cancelChats)
|
||||||
|
cancelTask = () => { cancelChats(); cancel() }
|
||||||
|
} else cancelTask = cancel
|
||||||
|
|
||||||
|
// determine whether reconnect should be used or not
|
||||||
|
const shouldUseReconnect = this.lastDisconnectReason !== DisconnectReason.replaced &&
|
||||||
|
this.lastDisconnectReason !== DisconnectReason.unknown &&
|
||||||
|
this.lastDisconnectReason !== DisconnectReason.intentional && this.user
|
||||||
|
const reconnectID = shouldUseReconnect ? this.user.jid.replace ('@s.whatsapp.net', '@c.us') : null
|
||||||
|
|
||||||
|
const promise = Utils.promiseTimeout(timeoutMs, (resolve, reject) => {
|
||||||
|
ws
|
||||||
|
.then (conn => this.conn = conn)
|
||||||
|
.then (() => this.conn.on('message', data => this.onMessageRecieved(data as any)))
|
||||||
|
.then (() => (
|
||||||
|
this.log(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`, MessageLogLevel.info)
|
||||||
|
))
|
||||||
|
.then (() => this.authenticate(reconnectID))
|
||||||
|
.then (() => (
|
||||||
|
this.conn
|
||||||
|
.removeAllListeners ('error')
|
||||||
|
.removeAllListeners('close')
|
||||||
|
))
|
||||||
|
.then (resolve)
|
||||||
|
.catch (reject)
|
||||||
|
})
|
||||||
|
.catch (err => {
|
||||||
|
this.removePendingCallbacks ()
|
||||||
|
throw err
|
||||||
|
}) as Promise<void>
|
||||||
|
|
||||||
|
tasks.push (promise)
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise: Promise.all (tasks),
|
||||||
|
cancel: cancelTask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let promise = Promise.resolve ()
|
||||||
|
let cancellations: (() => void)[] = []
|
||||||
|
|
||||||
|
const cancel = () => cancellations.forEach (cancel => cancel())
|
||||||
|
|
||||||
this.on ('close', cancel)
|
this.on ('close', cancel)
|
||||||
|
|
||||||
try {
|
if (delayMs) {
|
||||||
const tasks = [promise]
|
const {delay, cancel} = Utils.delayCancellable (delayMs)
|
||||||
|
promise = delay
|
||||||
const waitForChats = typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats
|
cancellations.push (cancel)
|
||||||
if (waitForChats) tasks.push (this.receiveChatsAndContacts(options?.timeoutMs, true))
|
|
||||||
|
|
||||||
await Promise.all (tasks)
|
|
||||||
|
|
||||||
this.phoneConnected = true
|
|
||||||
this.state = 'open'
|
|
||||||
|
|
||||||
this.emit ('open')
|
|
||||||
|
|
||||||
this.startKeepAliveRequest()
|
|
||||||
this.registerPhoneConnectionPoll ()
|
|
||||||
this.releasePendingRequests ()
|
|
||||||
|
|
||||||
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
|
|
||||||
|
|
||||||
return this
|
|
||||||
} catch (error) {
|
|
||||||
const loggedOut = error instanceof BaileysError && error.status === 401
|
|
||||||
if (loggedOut && this.cancelReconnect) this.cancelReconnect ()
|
|
||||||
|
|
||||||
if ((this.state as string) !== 'close') {
|
|
||||||
this.closeInternal (loggedOut ? 'invalid_session' : error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
this.off ('close', cancel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return promise
|
||||||
|
.then (() => {
|
||||||
|
const {promise, cancel} = connect ()
|
||||||
|
cancellations.push (cancel)
|
||||||
|
|
||||||
|
return promise
|
||||||
|
})
|
||||||
|
.finally (() => {
|
||||||
|
cancel()
|
||||||
|
this.off('close', cancel)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sets up callbacks to receive chats, contacts & messages.
|
* Sets up callbacks to receive chats, contacts & messages.
|
||||||
* Must be called immediately after connect
|
* Must be called immediately after connect
|
||||||
* @returns [chats, contacts]
|
* @returns [chats, contacts]
|
||||||
*/
|
*/
|
||||||
protected async receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) {
|
protected receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) {
|
||||||
|
this.contacts = {}
|
||||||
this.chats.clear ()
|
this.chats.clear ()
|
||||||
|
|
||||||
|
let receivedContacts = false
|
||||||
let receivedMessages = false
|
let receivedMessages = false
|
||||||
|
|
||||||
let resolveTask: () => void
|
let resolveTask: () => void
|
||||||
@@ -89,7 +149,12 @@ export class WAConnection extends Base {
|
|||||||
this.deregisterCallback(['action', 'add:unread'])
|
this.deregisterCallback(['action', 'add:unread'])
|
||||||
}
|
}
|
||||||
this.deregisterCallback(['response', 'type:chat'])
|
this.deregisterCallback(['response', 'type:chat'])
|
||||||
}
|
this.deregisterCallback(['response', 'type:contacts'])
|
||||||
|
}
|
||||||
|
const checkForResolution = () => {
|
||||||
|
if (receivedContacts && receivedMessages) resolveTask ()
|
||||||
|
}
|
||||||
|
|
||||||
// wait for messages to load
|
// wait for messages to load
|
||||||
const chatUpdate = json => {
|
const chatUpdate = json => {
|
||||||
receivedMessages = true
|
receivedMessages = true
|
||||||
@@ -104,7 +169,7 @@ export class WAConnection extends Base {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
// if received contacts before messages
|
// if received contacts before messages
|
||||||
if (isLast) resolveTask ()
|
if (isLast && receivedContacts) checkForResolution ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
||||||
@@ -115,9 +180,10 @@ export class WAConnection extends Base {
|
|||||||
}
|
}
|
||||||
// get chats
|
// get chats
|
||||||
this.registerCallback(['response', 'type:chat'], json => {
|
this.registerCallback(['response', 'type:chat'], json => {
|
||||||
if (json[1].duplicate) return
|
if (json[1].duplicate || !json[2]) return
|
||||||
|
|
||||||
json[2]?.forEach(([item, chat]: [any, WAChat]) => {
|
json[2]
|
||||||
|
.forEach(([item, chat]: [any, WAChat]) => {
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
|
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
|
||||||
return
|
return
|
||||||
@@ -133,24 +199,46 @@ export class WAConnection extends Base {
|
|||||||
this.chats.insert (chat) // chats data (log json to see what it looks like)
|
this.chats.insert (chat) // chats data (log json to see what it looks like)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.log (`received ${this.chats.all().length} chats`, MessageLogLevel.info)
|
this.log (`received ${json[2].length} chats`, MessageLogLevel.info)
|
||||||
// if there are no chats
|
if (json[2].length === 0) {
|
||||||
if (this.chats.all().length === 0) {
|
|
||||||
receivedMessages = true
|
receivedMessages = true
|
||||||
resolveTask ()
|
checkForResolution ()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// get contacts
|
||||||
|
this.registerCallback(['response', 'type:contacts'], json => {
|
||||||
|
if (json[1].duplicate || !json[2]) return
|
||||||
|
|
||||||
|
receivedContacts = true
|
||||||
|
|
||||||
|
json[2].forEach(([type, contact]: ['user', WAContact]) => {
|
||||||
|
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
|
||||||
|
|
||||||
|
contact.jid = Utils.whatsappID (contact.jid)
|
||||||
|
this.contacts[contact.jid] = contact
|
||||||
|
})
|
||||||
|
this.log (`received ${json[2].length} contacts`, MessageLogLevel.info)
|
||||||
|
checkForResolution ()
|
||||||
|
})
|
||||||
|
|
||||||
// wait for the chats & contacts to load
|
// wait for the chats & contacts to load
|
||||||
await Utils.promiseTimeout (timeoutMs, (resolve, reject) => {
|
const {delay, cancel} = Utils.delayCancellable (timeoutMs)
|
||||||
resolveTask = resolve
|
|
||||||
const rejectTask = (reason) => {
|
const waitForChats = Promise.race ([
|
||||||
reject (new Error(reason))
|
new Promise (resolve => resolveTask = resolve),
|
||||||
this.off ('close', rejectTask)
|
delay.then (() => { throw TimedOutError() })
|
||||||
}
|
])
|
||||||
this.on ('close', rejectTask)
|
.then (() => (
|
||||||
})
|
this.chats
|
||||||
|
.all ()
|
||||||
|
.forEach (chat => {
|
||||||
|
const respectiveContact = this.contacts[chat.jid]
|
||||||
|
chat.name = respectiveContact?.name || respectiveContact?.notify || chat.name
|
||||||
|
})
|
||||||
|
))
|
||||||
.finally (deregisterCallbacks)
|
.finally (deregisterCallbacks)
|
||||||
|
|
||||||
|
return { waitForChats, cancelChats: cancel }
|
||||||
}
|
}
|
||||||
private releasePendingRequests () {
|
private releasePendingRequests () {
|
||||||
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request
|
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request
|
||||||
@@ -221,62 +309,27 @@ export class WAConnection extends Base {
|
|||||||
}
|
}
|
||||||
/** Send a keep alive request every X seconds, server updates & responds with last seen */
|
/** Send a keep alive request every X seconds, server updates & responds with last seen */
|
||||||
private startKeepAliveRequest() {
|
private startKeepAliveRequest() {
|
||||||
|
this.keepAliveReq && clearInterval (this.keepAliveReq)
|
||||||
|
|
||||||
this.keepAliveReq = setInterval(() => {
|
this.keepAliveReq = setInterval(() => {
|
||||||
const diff = (new Date().getTime() - this.lastSeen.getTime())
|
if (!this.lastSeen) this.lastSeen = new Date ()
|
||||||
|
const diff = new Date().getTime() - this.lastSeen.getTime()
|
||||||
/*
|
/*
|
||||||
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
|
it could be that the network is down
|
||||||
*/
|
*/
|
||||||
if (diff > KEEP_ALIVE_INTERVAL_MS+5000) this.unexpectedDisconnect (DisconnectReason.lost)
|
if (diff > KEEP_ALIVE_INTERVAL_MS+5000) this.unexpectedDisconnect (DisconnectReason.lost)
|
||||||
else this.send ('?,,') // if its all good, send a keep alive request
|
else if (this.conn) this.send ('?,,') // if its all good, send a keep alive request
|
||||||
}, KEEP_ALIVE_INTERVAL_MS)
|
|
||||||
}
|
|
||||||
protected async reconnectLoop () {
|
|
||||||
this.cancelledReconnect = false
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const {delay, cancel} = Utils.delayCancellable (2500)
|
|
||||||
this.cancelReconnect = () => {
|
|
||||||
this.cancelledReconnect = true
|
|
||||||
this.cancelReconnect = null
|
|
||||||
cancel ()
|
|
||||||
}
|
|
||||||
|
|
||||||
await delay
|
|
||||||
|
|
||||||
try {
|
// poll phone connection as well,
|
||||||
// if an external connect causes the connection to be open
|
// 5000 ms for timeout
|
||||||
if (this.state === 'open') break
|
this.checkPhoneConnection (5000)
|
||||||
|
|
||||||
const shouldUseReconnect = this.lastDisconnectReason !== DisconnectReason.replaced && this.lastDisconnectReason !== DisconnectReason.unknown && this.user
|
|
||||||
const reconnectID = shouldUseReconnect ? this.user.jid.replace ('@s.whatsapp.net', '@c.us') : null
|
|
||||||
|
|
||||||
await this.connect ({ timeoutMs: 30000, retryOnNetworkErrors: true, reconnectID })
|
|
||||||
this.cancelReconnect = null
|
|
||||||
break
|
|
||||||
} catch (error) {
|
|
||||||
// don't continue reconnecting if error is 401
|
|
||||||
if (error instanceof BaileysError && error.status === 401) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
this.log (`error in reconnecting: ${error}, reconnecting...`, MessageLogLevel.info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
protected registerPhoneConnectionPoll () {
|
|
||||||
this.phoneCheck = setInterval (() => {
|
|
||||||
this.checkPhoneConnection (5000) // 5000 ms for timeout
|
|
||||||
.then (connected => {
|
.then (connected => {
|
||||||
if (this.phoneConnected != connected) {
|
this.phoneConnected !== connected && this.emit ('connection-phone-change', {connected})
|
||||||
this.emit ('connection-phone-change', {connected})
|
|
||||||
}
|
|
||||||
this.phoneConnected = connected
|
this.phoneConnected = connected
|
||||||
})
|
})
|
||||||
.catch (error => this.log(`error in getting phone connection: ${error}`, MessageLogLevel.info))
|
|
||||||
}, 15000)
|
}, KEEP_ALIVE_INTERVAL_MS)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Check if your phone is connected
|
* Check if your phone is connected
|
||||||
|
|||||||
@@ -31,9 +31,11 @@ export class WAConnection extends Base {
|
|||||||
const user = node[1] as WAContact
|
const user = node[1] as WAContact
|
||||||
user.jid = whatsappID(user.jid)
|
user.jid = whatsappID(user.jid)
|
||||||
|
|
||||||
|
this.contacts[user.jid] = user
|
||||||
|
|
||||||
const chat = this.chats.get (user.jid)
|
const chat = this.chats.get (user.jid)
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.name = user.name || user.notify
|
chat.name = user.name || user.notify || chat.name
|
||||||
this.emit ('chat-update', { jid: chat.jid, name: chat.name })
|
this.emit ('chat-update', { jid: chat.jid, name: chat.name })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,19 +105,6 @@ export class WAConnection extends Base {
|
|||||||
|
|
||||||
this.emit ('chat-update', { jid: chat.jid, count: chat.count })
|
this.emit ('chat-update', { jid: chat.jid, count: chat.count })
|
||||||
})
|
})
|
||||||
// get contacts
|
|
||||||
this.registerCallback(['response', 'type:contacts'], json => {
|
|
||||||
if (json[1].duplicate || !json[2]) return
|
|
||||||
|
|
||||||
const contacts: {[k: string]: WAContact} = {}
|
|
||||||
json[2].forEach(([type, contact]: ['user', WAContact]) => {
|
|
||||||
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
|
|
||||||
|
|
||||||
contact.jid = whatsappID (contact.jid)
|
|
||||||
contacts[contact.jid] = contact
|
|
||||||
})
|
|
||||||
this.emit ('contacts-received', contacts)
|
|
||||||
})
|
|
||||||
/*// genetic chat action
|
/*// genetic chat action
|
||||||
this.registerCallback (['Chat', 'cmd:action'], json => {
|
this.registerCallback (['Chat', 'cmd:action'], json => {
|
||||||
const data = json[1].data as WANode
|
const data = json[1].data as WANode
|
||||||
@@ -306,8 +295,6 @@ export class WAConnection extends Base {
|
|||||||
on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void): this
|
on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void): this
|
||||||
/** when a user's status is updated */
|
/** when a user's status is updated */
|
||||||
on (event: 'user-status-update', listener: (update: {jid: string, status?: string}) => void): this
|
on (event: 'user-status-update', listener: (update: {jid: string, status?: string}) => void): this
|
||||||
/** when a user receives contacts */
|
|
||||||
on (event: 'contacts-received', listener: (contacts: {[k: string]: WAContact}) => void): this
|
|
||||||
/** when a new chat is added */
|
/** when a new chat is added */
|
||||||
on (event: 'chat-new', listener: (chat: WAChat) => void): this
|
on (event: 'chat-new', listener: (chat: WAChat) => void): this
|
||||||
/** when a chat is updated (archived, deleted, pinned) */
|
/** when a chat is updated (archived, deleted, pinned) */
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export class BaileysError extends Error {
|
|||||||
this.context = context
|
this.context = context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const TimedOutError = () => new BaileysError ('timed out', { status: 408 })
|
||||||
|
export const CancelledError = () => new BaileysError ('cancelled', { status: 500 })
|
||||||
|
|
||||||
export interface WAQuery {
|
export interface WAQuery {
|
||||||
json: any[] | WANode
|
json: any[] | WANode
|
||||||
binaryTags?: WATag
|
binaryTags?: WATag
|
||||||
@@ -56,17 +59,17 @@ export enum ReconnectMode {
|
|||||||
onAllErrors = 2
|
onAllErrors = 2
|
||||||
}
|
}
|
||||||
export type WAConnectOptions = {
|
export type WAConnectOptions = {
|
||||||
/** timeout after which the connect will fail, set to null for an infinite timeout */
|
/** timeout after which the connect attempt will fail, set to null for default timeout value */
|
||||||
timeoutMs?: number
|
timeoutMs?: number
|
||||||
|
/** maximum attempts to connect */
|
||||||
|
maxRetries?: number
|
||||||
/** should the chats be waited for */
|
/** should the chats be waited for */
|
||||||
waitForChats?: boolean
|
waitForChats?: boolean
|
||||||
/** retry on network errors while connecting */
|
|
||||||
retryOnNetworkErrors?: boolean
|
|
||||||
/** use the 'reconnect' tag to reconnect instead of the 'takeover' tag */
|
|
||||||
reconnectID?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
||||||
|
|
||||||
|
export const UNAUTHORIZED_CODES = [401, 419]
|
||||||
/** Types of Disconnect Reasons */
|
/** Types of Disconnect Reasons */
|
||||||
export enum DisconnectReason {
|
export enum DisconnectReason {
|
||||||
/** The connection was closed intentionally */
|
/** The connection was closed intentionally */
|
||||||
@@ -344,7 +347,6 @@ export type BaileysEvent =
|
|||||||
'connection-phone-change' |
|
'connection-phone-change' |
|
||||||
'user-presence-update' |
|
'user-presence-update' |
|
||||||
'user-status-update' |
|
'user-status-update' |
|
||||||
'contacts-received' |
|
|
||||||
'chat-new' |
|
'chat-new' |
|
||||||
'chat-update' |
|
'chat-update' |
|
||||||
'message-new' |
|
'message-new' |
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {platform, release} from 'os'
|
|||||||
import WS from 'ws'
|
import WS from 'ws'
|
||||||
|
|
||||||
import Decoder from '../Binary/Decoder'
|
import Decoder from '../Binary/Decoder'
|
||||||
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto } from './Constants'
|
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto, TimedOutError, CancelledError } from './Constants'
|
||||||
|
|
||||||
const platformMap = {
|
const platformMap = {
|
||||||
'aix': 'AIX',
|
'aix': 'AIX',
|
||||||
@@ -80,7 +80,7 @@ export const delayCancellable = (ms: number) => {
|
|||||||
})
|
})
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
clearTimeout (timeout)
|
clearTimeout (timeout)
|
||||||
reject (new Error('cancelled'))
|
reject (CancelledError())
|
||||||
}
|
}
|
||||||
return { delay, cancel }
|
return { delay, cancel }
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>
|
|||||||
try {
|
try {
|
||||||
const content = await Promise.race([
|
const content = await Promise.race([
|
||||||
p,
|
p,
|
||||||
delay.then(() => pReject(new BaileysError('timed out', p)))
|
delay.then(() => pReject(TimedOutError()))
|
||||||
])
|
])
|
||||||
cancel ()
|
cancel ()
|
||||||
return content as T
|
return content as T
|
||||||
@@ -139,7 +139,7 @@ export const openWebSocketConnection = (timeoutMs: number, retryOnNetworkError:
|
|||||||
await delay (1000)
|
await delay (1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error ('cancelled')
|
throw CancelledError()
|
||||||
}
|
}
|
||||||
const cancel = () => cancelled = true
|
const cancel = () => cancelled = true
|
||||||
return { ws: connect(), cancel }
|
return { ws: connect(), cancel }
|
||||||
|
|||||||
Reference in New Issue
Block a user