mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Update init method to try login multiple times + use WA ttl for QR gen
This commit is contained in:
@@ -18,7 +18,6 @@ async function example() {
|
||||
conn.logger.level = 'debug' // set to 'debug' to see what kind of stuff you can implement
|
||||
// attempt to reconnect at most 10 times in a row
|
||||
conn.connectOptions.maxRetries = 10
|
||||
conn.connectOptions.waitForChats = false
|
||||
conn.chatOrderingKey = waChatKey(true) // order chats such that pinned chats are on top
|
||||
|
||||
conn.on ('credentials-updated', () => {
|
||||
|
||||
@@ -96,8 +96,6 @@ console.log ("oh hello " + conn.user.name + "! You connected via a proxy")
|
||||
The entire `WAConnectOptions` struct is mentioned here with default values:
|
||||
``` ts
|
||||
conn.connectOptions = {
|
||||
/** New QR generation interval, set to null if you don't want to regenerate */
|
||||
regenerateQRIntervalMs?: 30_000,
|
||||
/** fails the connection if no data is received for X seconds */
|
||||
maxIdleTimeMs?: 15_000,
|
||||
/** maximum attempts to connect */
|
||||
@@ -249,6 +247,8 @@ conn.sendMessage(id, buffer, MessageType.audio, options)
|
||||
To note:
|
||||
- `id` is the WhatsApp ID of the person or group you're sending the message to.
|
||||
- It must be in the format ```[country code][phone number]@s.whatsapp.net```, for example ```+19999999999@s.whatsapp.net``` for people. For groups, it must be in the format ``` 123456789-123345@g.us ```.
|
||||
- For broadcast lists it's `[timestamp of creation]@broadcast`.
|
||||
- For stories, the ID is `status@broadcast`.
|
||||
- For media messages, the thumbnail can be generated automatically for images & stickers. Thumbnails for videos can also be generated automatically, though, you need to have `ffmpeg` installed on your system.
|
||||
- **MessageOptions**: some extra info about the message. It can have the following __optional__ values:
|
||||
``` ts
|
||||
@@ -305,6 +305,7 @@ export enum Presence {
|
||||
available = 'available', // "online"
|
||||
composing = 'composing', // "typing..."
|
||||
recording = 'recording', // "recording..."
|
||||
paused = 'paused' // stopped typing, back to "online"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -7,20 +7,17 @@ import { assertChatDBIntegrity, makeConnection, testJid } from './Common'
|
||||
describe('QR Generation', () => {
|
||||
it('should generate QR', async () => {
|
||||
const conn = makeConnection ()
|
||||
conn.connectOptions.regenerateQRIntervalMs = 5000
|
||||
conn.connectOptions.maxRetries = 0
|
||||
|
||||
let calledQR = 0
|
||||
conn.removeAllListeners ('qr')
|
||||
conn.on ('qr', qr => calledQR += 1)
|
||||
conn.on ('qr', () => calledQR += 1)
|
||||
|
||||
await conn.connect()
|
||||
.then (() => assert.fail('should not have succeeded'))
|
||||
.catch (error => {
|
||||
assert.equal (error.message, 'timed out')
|
||||
})
|
||||
assert.deepEqual (conn['pendingRequests'], [])
|
||||
assert.deepEqual (
|
||||
Object.keys(conn['callbacks']).filter(key => !key.startsWith('function:')),
|
||||
.catch (error => {})
|
||||
assert.deepStrictEqual (
|
||||
Object.keys(conn.eventNames()).filter(key => key.startsWith('TAG:')),
|
||||
[]
|
||||
)
|
||||
assert.ok(calledQR >= 2, 'QR not called')
|
||||
|
||||
@@ -41,10 +41,7 @@ export class WAConnection extends EventEmitter {
|
||||
/** The connection state */
|
||||
state: WAConnectionState = 'close'
|
||||
connectOptions: WAConnectOptions = {
|
||||
regenerateQRIntervalMs: 30_000,
|
||||
maxIdleTimeMs: 15_000,
|
||||
waitOnlyForLastMessage: false,
|
||||
waitForChats: false,
|
||||
maxIdleTimeMs: 60_000,
|
||||
maxRetries: 10,
|
||||
connectCooldownMs: 4000,
|
||||
phoneResponseTime: 15_000,
|
||||
@@ -84,7 +81,7 @@ export class WAConnection extends EventEmitter {
|
||||
|
||||
protected referenceDate = new Date () // used for generating tags
|
||||
protected lastSeen: Date = null // last keep alive received
|
||||
protected qrTimeout: NodeJS.Timeout
|
||||
protected initTimeout: NodeJS.Timeout
|
||||
|
||||
protected lastDisconnectTime: Date = null
|
||||
protected lastDisconnectReason: DisconnectReason
|
||||
@@ -92,13 +89,6 @@ export class WAConnection extends EventEmitter {
|
||||
protected mediaConn: MediaConnInfo
|
||||
protected debounceTimeout: NodeJS.Timeout
|
||||
|
||||
constructor () {
|
||||
super ()
|
||||
this.setMaxListeners (20)
|
||||
this.on ('CB:Cmd,type:disconnect', json => (
|
||||
this.state === 'open' && this.unexpectedDisconnect(json[1].kind || 'unknown')
|
||||
))
|
||||
}
|
||||
/**
|
||||
* Connect to WhatsAppWeb
|
||||
* @param options the connect options
|
||||
@@ -132,6 +122,10 @@ export class WAConnection extends EventEmitter {
|
||||
macKey: this.authInfo.macKey.toString('base64'),
|
||||
}
|
||||
}
|
||||
/** Can you login to WA without scanning the QR */
|
||||
canLogin () {
|
||||
return !!this.authInfo?.encKey && !!this.authInfo?.macKey
|
||||
}
|
||||
/** Clear authentication info so a new connection can be created */
|
||||
clearAuthInfo () {
|
||||
this.authInfo = null
|
||||
@@ -175,7 +169,7 @@ 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, requiresPhoneConnection: boolean, timeoutMs?: number) {
|
||||
async waitForMessage(tag: string, requiresPhoneConnection: boolean, timeoutMs?: number) {
|
||||
if (!this.phoneCheckInterval && requiresPhoneConnection) {
|
||||
this.startPhoneCheckInterval ()
|
||||
}
|
||||
@@ -217,7 +211,7 @@ export class WAConnection extends EventEmitter {
|
||||
if (waitForOpen) await this.waitForConnection()
|
||||
|
||||
tag = tag || this.generateMessageTag (longTag)
|
||||
const promise = this.waitForMessage(tag, json, requiresPhoneConnection, timeoutMs)
|
||||
const promise = this.waitForMessage(tag, requiresPhoneConnection, timeoutMs)
|
||||
|
||||
if (this.logger.level === 'trace') {
|
||||
this.logger.trace ({ fromMe: true },`${tag},${JSON.stringify(json)}`)
|
||||
@@ -385,13 +379,12 @@ export class WAConnection extends EventEmitter {
|
||||
this.conn?.removeAllListeners ('open')
|
||||
this.conn?.removeAllListeners ('message')
|
||||
|
||||
this.qrTimeout && clearTimeout (this.qrTimeout)
|
||||
this.initTimeout && clearTimeout (this.initTimeout)
|
||||
this.debounceTimeout && clearTimeout (this.debounceTimeout)
|
||||
this.keepAliveReq && clearInterval(this.keepAliveReq)
|
||||
this.clearPhoneCheckInterval ()
|
||||
|
||||
this.emit ('ws-close', { reason: DisconnectReason.close })
|
||||
//this.rejectPendingConnection && this.rejectPendingConnection (new Error('close'))
|
||||
|
||||
try {
|
||||
this.conn?.close()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as Curve from 'curve25519-js'
|
||||
import * as Utils from './Utils'
|
||||
import {WAConnection as Base} from './0.Base'
|
||||
import { WAMetric, WAFlag, BaileysError, Presence, WAUser } from './Constants'
|
||||
import { WAMetric, WAFlag, BaileysError, Presence, WAUser, WAInitResponse } from './Constants'
|
||||
|
||||
export class WAConnection extends Base {
|
||||
|
||||
@@ -12,72 +12,73 @@ export class WAConnection extends Base {
|
||||
if (!this.authInfo?.clientID) {
|
||||
this.authInfo = { clientID: Utils.generateClientID() } as any
|
||||
}
|
||||
const canLogin = this.authInfo?.encKey && this.authInfo?.macKey
|
||||
const canLogin = this.canLogin()
|
||||
this.referenceDate = new Date () // refresh reference date
|
||||
let isNewUser = false
|
||||
|
||||
this.startDebouncedTimeout ()
|
||||
|
||||
const initQueries = [
|
||||
(async () => {
|
||||
const {ref} = await this.query({
|
||||
json: ['admin', 'init', this.version, this.browserDescription, this.authInfo?.clientID, true],
|
||||
expect200: true,
|
||||
waitForOpen: false,
|
||||
longTag: true,
|
||||
requiresPhoneConnection: false,
|
||||
startDebouncedTimeout: true
|
||||
})
|
||||
if (!canLogin) {
|
||||
this.stopDebouncedTimeout () // stop the debounced timeout for QR gen
|
||||
const result = await this.generateKeysForAuth (ref)
|
||||
this.startDebouncedTimeout () // restart debounced timeout
|
||||
return result
|
||||
}
|
||||
})()
|
||||
]
|
||||
const initQuery = (async () => {
|
||||
const {ref, ttl} = await this.query({
|
||||
json: ['admin', 'init', this.version, this.browserDescription, this.authInfo?.clientID, true],
|
||||
expect200: true,
|
||||
waitForOpen: false,
|
||||
longTag: true,
|
||||
requiresPhoneConnection: false,
|
||||
startDebouncedTimeout: true
|
||||
}) as WAInitResponse
|
||||
|
||||
if (!canLogin) {
|
||||
this.stopDebouncedTimeout () // stop the debounced timeout for QR gen
|
||||
this.generateKeysForAuth (ref, ttl)
|
||||
}
|
||||
})();
|
||||
if (canLogin) {
|
||||
// if we have the info to restore a closed session
|
||||
initQueries.push (
|
||||
(async () => {
|
||||
const json = [
|
||||
'admin',
|
||||
'login',
|
||||
this.authInfo?.clientToken,
|
||||
this.authInfo?.serverToken,
|
||||
this.authInfo?.clientID,
|
||||
]
|
||||
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,
|
||||
requiresPhoneConnection: false,
|
||||
startDebouncedTimeout: true
|
||||
}) // 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', [], true)
|
||||
}
|
||||
return response
|
||||
})()
|
||||
)
|
||||
const json = [
|
||||
'admin',
|
||||
'login',
|
||||
this.authInfo?.clientToken,
|
||||
this.authInfo?.serverToken,
|
||||
this.authInfo?.clientID,
|
||||
]
|
||||
const tag = this.generateMessageTag(true)
|
||||
|
||||
if (reconnect) json.push(...['reconnect', reconnect.replace('@s.whatsapp.net', '@c.us')])
|
||||
else json.push ('takeover')
|
||||
// send login every 10s
|
||||
const sendLoginReq = () => {
|
||||
this.logger.debug('sending login request')
|
||||
this.sendJSON(json, tag)
|
||||
this.initTimeout = setTimeout(sendLoginReq, 10_000)
|
||||
}
|
||||
sendLoginReq()
|
||||
}
|
||||
|
||||
await initQuery
|
||||
|
||||
const validationJSON = (await Promise.all (initQueries)).slice(-1)[0] // get the last result
|
||||
const newUser = await this.validateNewConnection(validationJSON[1]) // validate the connection
|
||||
// wait for response with tag "s1"
|
||||
let response = await this.waitForMessage('s1', false, undefined)
|
||||
this.startDebouncedTimeout()
|
||||
this.initTimeout && clearTimeout (this.initTimeout)
|
||||
this.initTimeout = null
|
||||
|
||||
if (response.status && response.status !== 200) {
|
||||
throw new BaileysError(`Unexpected error in login`, { response, status: response.status })
|
||||
}
|
||||
// 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', true)
|
||||
}
|
||||
|
||||
const newUser = await this.validateNewConnection(response[1]) // validate the connection
|
||||
if (newUser.jid !== this.user?.jid) {
|
||||
isNewUser = true
|
||||
// clear out old data
|
||||
this.chats.clear()
|
||||
this.contacts = {}
|
||||
}
|
||||
|
||||
this.user = newUser
|
||||
|
||||
this.logger.info('validated connection successfully')
|
||||
@@ -95,7 +96,6 @@ export class WAConnection extends Base {
|
||||
}
|
||||
|
||||
this.sendPostConnectQueries ()
|
||||
|
||||
this.logger.debug('sent init queries')
|
||||
|
||||
return { isNewUser }
|
||||
@@ -110,22 +110,21 @@ export class WAConnection extends Base {
|
||||
this.sendBinary (['query', {type: 'quick_reply', epoch: '1'}, null], [ WAMetric.queryQuickReply, WAFlag.ignore ])
|
||||
this.sendBinary (['query', {type: 'label', epoch: '1'}, null], [ WAMetric.queryLabel, WAFlag.ignore ])
|
||||
this.sendBinary (['query', {type: 'emoji', epoch: '1'}, null], [ WAMetric.queryEmoji, WAFlag.ignore ])
|
||||
this.sendBinary (['action', {type: 'set', epoch: '1'}, [['presence', {type: Presence.available}, null]] ], [ WAMetric.presence, 160 ])
|
||||
this.sendBinary (['action', {type: 'set', epoch: '1'}, [['presence', {type: Presence.available}, null]] ], [ WAMetric.presence, WAFlag.available ])
|
||||
}
|
||||
/**
|
||||
* Refresh QR Code
|
||||
* @returns the new ref
|
||||
*/
|
||||
async generateNewQRCodeRef() {
|
||||
async requestNewQRCodeRef() {
|
||||
const response = await this.query({
|
||||
json: ['admin', 'Conn', 'reref'],
|
||||
expect200: true,
|
||||
waitForOpen: false,
|
||||
longTag: true,
|
||||
timeoutMs: this.connectOptions.maxIdleTimeMs,
|
||||
requiresPhoneConnection: false
|
||||
})
|
||||
return response.ref as string
|
||||
return response as WAInitResponse
|
||||
}
|
||||
/**
|
||||
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
|
||||
@@ -211,41 +210,31 @@ export class WAConnection extends Base {
|
||||
return this.query({json, expect200: true, waitForOpen: false, startDebouncedTimeout: true})
|
||||
}
|
||||
/** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */
|
||||
protected async generateKeysForAuth(ref: string) {
|
||||
protected generateKeysForAuth(ref: string, ttl?: number) {
|
||||
this.curveKeys = Curve.generateKeyPair(Utils.randomBytes(32))
|
||||
const publicKey = Buffer.from(this.curveKeys.public).toString('base64')
|
||||
|
||||
const emitQR = () => {
|
||||
const qrLoop = ttl => {
|
||||
const qr = [ref, publicKey, this.authInfo.clientID].join(',')
|
||||
this.emit ('qr', qr)
|
||||
}
|
||||
|
||||
const regenQR = () => {
|
||||
this.qrTimeout = setTimeout (async () => {
|
||||
this.initTimeout = setTimeout (async () => {
|
||||
if (this.state === 'open') return
|
||||
|
||||
this.logger.debug ('regenerating QR')
|
||||
try {
|
||||
const newRef = await this.generateNewQRCodeRef ()
|
||||
const {ref: newRef, ttl} = await this.requestNewQRCodeRef()
|
||||
ref = newRef
|
||||
emitQR ()
|
||||
regenQR ()
|
||||
|
||||
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.connectOptions.regenerateQRIntervalMs)
|
||||
}, ttl || 20_000) // default is 20s, on the off-chance ttl is not present
|
||||
}
|
||||
|
||||
emitQR ()
|
||||
if (this.connectOptions.regenerateQRIntervalMs) regenQR ()
|
||||
|
||||
const json = await this.waitForMessage('s1', [], false)
|
||||
this.qrTimeout && clearTimeout (this.qrTimeout)
|
||||
this.qrTimeout = null
|
||||
|
||||
return json
|
||||
qrLoop (ttl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ export class WAConnection extends Base {
|
||||
/** Connect to WhatsApp Web */
|
||||
async connect () {
|
||||
// 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 BaileysError('cannot connect when state=' + this.state, { status: 409 })
|
||||
}
|
||||
|
||||
const options = this.connectOptions
|
||||
const newConnection = !this.authInfo
|
||||
@@ -60,6 +62,7 @@ export class WAConnection extends Base {
|
||||
protected async connectInternal (options: WAConnectOptions, delayMs?: number) {
|
||||
const rejections: ((e?: Error) => void)[] = []
|
||||
const rejectAll = (e: Error) => rejections.forEach (r => r(e))
|
||||
const rejectAllOnWSClose = ({ reason }) => rejectAll(new Error(reason))
|
||||
// actual connect
|
||||
const connect = () => (
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -86,10 +89,12 @@ export class WAConnection extends Base {
|
||||
this.conn.addEventListener('message', ({data}) => this.onMessageRecieved(data as any))
|
||||
|
||||
this.conn.on ('open', async () => {
|
||||
this.startKeepAliveRequest()
|
||||
this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`)
|
||||
|
||||
let waitForChats: Promise<{ hasNewChats: boolean }>
|
||||
// add wait for chats promise if required
|
||||
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
||||
if (options?.waitForChats) {
|
||||
const {wait, cancellations} = this.receiveChatsAndContacts(this.connectOptions.waitOnlyForLastMessage)
|
||||
waitForChats = wait
|
||||
rejections.push (...cancellations)
|
||||
@@ -101,7 +106,7 @@ export class WAConnection extends Base {
|
||||
waitForChats || undefined
|
||||
]
|
||||
)
|
||||
this.startKeepAliveRequest()
|
||||
|
||||
this.conn
|
||||
.removeAllListeners ('error')
|
||||
.removeAllListeners ('close')
|
||||
@@ -116,7 +121,7 @@ export class WAConnection extends Base {
|
||||
}) as Promise<{ hasNewChats?: boolean, isNewUser: boolean }>
|
||||
)
|
||||
|
||||
this.on ('ws-close', rejectAll)
|
||||
this.on ('ws-close', rejectAllOnWSClose)
|
||||
try {
|
||||
if (delayMs) {
|
||||
const {delay, cancel} = Utils.delayCancellable (delayMs)
|
||||
@@ -129,7 +134,7 @@ export class WAConnection extends Base {
|
||||
this.endConnection ()
|
||||
throw error
|
||||
} finally {
|
||||
this.off ('ws-close', rejectAll)
|
||||
this.off ('ws-close', rejectAllOnWSClose)
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -186,7 +191,7 @@ export class WAConnection extends Base {
|
||||
if (!json) return
|
||||
|
||||
if (this.logger.level === 'trace') {
|
||||
this.logger.trace(messageTag + ', ' + JSON.stringify(json))
|
||||
this.logger.trace(messageTag + ',' + JSON.stringify(json))
|
||||
}
|
||||
|
||||
let anyTriggered = false
|
||||
|
||||
@@ -9,6 +9,11 @@ export class WAConnection extends Base {
|
||||
|
||||
constructor () {
|
||||
super ()
|
||||
this.setMaxListeners (30)
|
||||
// on disconnects
|
||||
this.on ('CB:Cmd,type:disconnect', json => (
|
||||
this.state === 'open' && this.unexpectedDisconnect(json[1].kind || 'unknown')
|
||||
))
|
||||
// chats received
|
||||
this.on('CB:response,type:chat', json => {
|
||||
if (json[1].duplicate || !json[2]) return
|
||||
|
||||
@@ -21,6 +21,12 @@ export type WAGenericMediaMessage = proto.IVideoMessage | proto.IImageMessage |
|
||||
export import WA_MESSAGE_STUB_TYPE = proto.WebMessageInfo.WebMessageInfoStubType
|
||||
export import WA_MESSAGE_STATUS_TYPE = proto.WebMessageInfo.WebMessageInfoStatus
|
||||
|
||||
export type WAInitResponse = {
|
||||
ref: string
|
||||
ttl: number
|
||||
status: 200
|
||||
}
|
||||
|
||||
export interface WALocationMessage {
|
||||
degreesLatitude: number
|
||||
degreesLongitude: number
|
||||
@@ -73,7 +79,10 @@ export type WALoadChatOptions = {
|
||||
loadProfilePicture?: boolean
|
||||
}
|
||||
export type WAConnectOptions = {
|
||||
/** 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
|
||||
* @deprecated no need to set this as we use WA ttl
|
||||
* */
|
||||
regenerateQRIntervalMs?: number
|
||||
/** fails the connection if no data is received for X seconds */
|
||||
maxIdleTimeMs?: number
|
||||
|
||||
Reference in New Issue
Block a user