mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
better connections
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
Mimetype,
|
||||
WALocationMessage,
|
||||
MessageLogLevel,
|
||||
WAMessageType,
|
||||
WA_MESSAGE_STUB_TYPES,
|
||||
ReconnectMode,
|
||||
} from '../src/WAConnection/WAConnection'
|
||||
import * as fs from 'fs'
|
||||
@@ -18,8 +18,9 @@ async function example() {
|
||||
|
||||
// loads the auth file credentials if present
|
||||
if (fs.existsSync('./auth_info.json')) conn.loadAuthInfo ('./auth_info.json')
|
||||
// connect or timeout in 20 seconds
|
||||
await conn.connect(20 * 1000)
|
||||
conn.on ('qr', qr => console.log (qr))
|
||||
// connect or timeout in 30 seconds
|
||||
await conn.connect({ timeoutMs: 30 * 1000 })
|
||||
|
||||
const unread = await conn.loadAllUnreadMessages ()
|
||||
|
||||
@@ -38,7 +39,7 @@ async function example() {
|
||||
})
|
||||
// set to false to NOT relay your own sent messages
|
||||
conn.on('message-new', async (m) => {
|
||||
const messageStubType = WAMessageType[m.messageStubType] || 'MESSAGE'
|
||||
const messageStubType = WA_MESSAGE_STUB_TYPES[m.messageStubType] || 'MESSAGE'
|
||||
console.log('got notification of type: ' + messageStubType)
|
||||
|
||||
const messageContent = m.message
|
||||
@@ -117,7 +118,7 @@ async function example() {
|
||||
const batterylevel = parseInt(batteryLevelStr)
|
||||
console.log('battery level: ' + batterylevel)
|
||||
})
|
||||
conn.on('closed', ({reason, isReconnecting}) => (
|
||||
conn.on('close', ({reason, isReconnecting}) => (
|
||||
console.log ('oh no got disconnected: ' + reason + ', reconnecting: ' + isReconnecting)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ async function connectToWhatsApp () {
|
||||
const conn = new WAConnection()
|
||||
|
||||
// 20 second timeout
|
||||
await conn.connect (20*1000)
|
||||
await conn.connect ({timeoutMs: 30*1000})
|
||||
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")")
|
||||
// every chat object has a list of most recent messages
|
||||
console.log ("you have " + conn.chats.all().length + " chats")
|
||||
@@ -62,7 +62,7 @@ If the connection is successful, you will see a QR code printed on your terminal
|
||||
|
||||
If you don't want to wait for WhatsApp to send all your chats while connecting, you can use the following function:
|
||||
``` ts
|
||||
await conn.connect (20*1000, false)
|
||||
await conn.connect ({timeoutMs: 30*1000}, false)
|
||||
```
|
||||
|
||||
Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons:
|
||||
@@ -133,7 +133,7 @@ on (event: 'open', listener: () => void): this
|
||||
/** when the connection is opening */
|
||||
on (event: 'connecting', listener: () => void): this
|
||||
/** when the connection has closed */
|
||||
on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
|
||||
on (event: 'close', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
|
||||
/** when a new QR is generated, ready for scanning */
|
||||
on (event: 'qr', listener: (qr: string) => void): this
|
||||
/** when the connection to the phone changes */
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "npm run build",
|
||||
"test": "mocha --timeout 60000 -r ts-node/register src/Tests/Tests.*.ts",
|
||||
"test": "mocha --timeout 120000 -r ts-node/register src/Tests/Tests.*.ts",
|
||||
"lint": "eslint '*/*.ts' --quiet --fix",
|
||||
"build": "tsc",
|
||||
"build:docs": "typedoc",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as assert from 'assert'
|
||||
import {WAConnection} from '../WAConnection/WAConnection'
|
||||
import { AuthenticationCredentialsBase64, BaileysError, MessageLogLevel, ReconnectMode } from '../WAConnection/Constants'
|
||||
import { delay, promiseTimeout } from '../WAConnection/Utils'
|
||||
import { close } from 'fs'
|
||||
import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode } from '../WAConnection/Constants'
|
||||
import { delay } from '../WAConnection/Utils'
|
||||
|
||||
describe('QR Generation', () => {
|
||||
it('should generate QR', async () => {
|
||||
@@ -13,7 +12,7 @@ describe('QR Generation', () => {
|
||||
conn.removeAllListeners ('qr')
|
||||
conn.on ('qr', qr => calledQR += 1)
|
||||
|
||||
await conn.connect(15000)
|
||||
await conn.connect({ timeoutMs: 15000 })
|
||||
.then (() => assert.fail('should not have succeeded'))
|
||||
.catch (error => {
|
||||
assert.equal (error.message, 'timed out')
|
||||
@@ -42,7 +41,7 @@ describe('Test Connect', () => {
|
||||
const conn = new WAConnection()
|
||||
await conn
|
||||
.loadAuthInfo (auth)
|
||||
.connect (20*1000)
|
||||
.connect ({timeoutMs: 20*1000})
|
||||
.then (conn => {
|
||||
assert.ok(conn.user)
|
||||
assert.ok(conn.user.id)
|
||||
@@ -96,7 +95,7 @@ describe ('Reconnects', () => {
|
||||
conn.close ()
|
||||
}
|
||||
})
|
||||
it ('should reconnect connection', async () => {
|
||||
it ('should reconnect on broken connection', async () => {
|
||||
const conn = new WAConnection ()
|
||||
conn.autoReconnect = ReconnectMode.onConnectionLost
|
||||
|
||||
@@ -108,7 +107,7 @@ describe ('Reconnects', () => {
|
||||
|
||||
const task = new Promise (resolve => {
|
||||
let closes = 0
|
||||
conn.on ('closed', ({reason, isReconnecting}) => {
|
||||
conn.on ('close', ({reason, isReconnecting}) => {
|
||||
console.log (`closed: ${reason}`)
|
||||
assert.ok (reason)
|
||||
assert.ok (isReconnecting)
|
||||
@@ -116,14 +115,14 @@ describe ('Reconnects', () => {
|
||||
|
||||
// let it fail reconnect a few times
|
||||
if (closes > 4) {
|
||||
conn.removeAllListeners ('closed')
|
||||
conn.removeAllListeners ('close')
|
||||
conn.removeAllListeners ('connecting')
|
||||
resolve ()
|
||||
}
|
||||
})
|
||||
conn.on ('connecting', () => {
|
||||
// close again
|
||||
delay (500).then (closeConn)
|
||||
delay (3500).then (closeConn)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -143,7 +142,7 @@ describe ('Reconnects', () => {
|
||||
await delay (2000)
|
||||
} finally {
|
||||
conn.removeAllListeners ('connecting')
|
||||
conn.removeAllListeners ('closed')
|
||||
conn.removeAllListeners ('close')
|
||||
conn.removeAllListeners ('open')
|
||||
conn.close ()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class WAConnection extends EventEmitter {
|
||||
/** Should requests be queued when the connection breaks in between; if false, then an error will be thrown */
|
||||
pendingRequestTimeoutMs: number = null
|
||||
/** The connection state */
|
||||
state: WAConnectionState = 'closed'
|
||||
state: WAConnectionState = 'close'
|
||||
/** New QR generation interval, set to null if you don't want to regenerate */
|
||||
regenerateQRIntervalMs = 30*1000
|
||||
|
||||
@@ -73,7 +73,10 @@ export class WAConnection extends EventEmitter {
|
||||
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind))
|
||||
}
|
||||
async unexpectedDisconnect (error?: DisconnectReason) {
|
||||
const willReconnect = this.autoReconnect === ReconnectMode.onAllErrors || (this.autoReconnect === ReconnectMode.onConnectionLost && (error !== 'replaced'))
|
||||
const willReconnect =
|
||||
(this.autoReconnect === ReconnectMode.onAllErrors ||
|
||||
(this.autoReconnect === ReconnectMode.onConnectionLost && (error !== 'replaced'))) &&
|
||||
error !== 'invalid_session'
|
||||
|
||||
this.log (`got disconnected, reason ${error || 'unknown'}${willReconnect ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info)
|
||||
this.closeInternal(error, willReconnect)
|
||||
@@ -280,16 +283,15 @@ export class WAConnection extends EventEmitter {
|
||||
this.closeInternal ('intentional')
|
||||
|
||||
this.cancelReconnect && this.cancelReconnect ()
|
||||
this.cancelledReconnect = true
|
||||
|
||||
this.pendingRequests.forEach (({reject}) => reject(new Error('closed')))
|
||||
this.pendingRequests.forEach (({reject}) => reject(new Error('close')))
|
||||
this.pendingRequests = []
|
||||
}
|
||||
protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean=false) {
|
||||
this.qrTimeout && clearTimeout (this.qrTimeout)
|
||||
this.phoneCheck && clearTimeout (this.phoneCheck)
|
||||
|
||||
this.state = 'closed'
|
||||
this.state = 'close'
|
||||
this.msgCount = 0
|
||||
this.conn?.removeAllListeners ('close')
|
||||
this.conn?.close()
|
||||
@@ -299,13 +301,13 @@ export class WAConnection extends EventEmitter {
|
||||
Object.keys(this.callbacks).forEach(key => {
|
||||
if (!key.includes('function:')) {
|
||||
this.log (`cancelling message wait: ${key}`, MessageLogLevel.info)
|
||||
this.callbacks[key].errCallback(new Error('closed'))
|
||||
this.callbacks[key].errCallback(new Error('close'))
|
||||
delete this.callbacks[key]
|
||||
}
|
||||
})
|
||||
if (this.keepAliveReq) clearInterval(this.keepAliveReq)
|
||||
// reconnecting if the timeout is active for the reconnect loop
|
||||
this.emit ('closed', { reason, isReconnecting: this.cancelReconnect || isReconnecting })
|
||||
this.emit ('close', { reason, isReconnecting: this.cancelReconnect || isReconnecting})
|
||||
}
|
||||
protected async reconnectLoop () {
|
||||
|
||||
|
||||
@@ -39,9 +39,9 @@ export class WAConnection extends Base {
|
||||
case 401: // if the phone was unpaired
|
||||
throw new BaileysError ('unpaired from phone', json)
|
||||
case 429: // request to login was denied, don't know why it happens
|
||||
throw new BaileysError ('request denied, try reconnecting', json)
|
||||
throw new BaileysError ('request denied', json)
|
||||
default:
|
||||
throw new BaileysError ('unexpected status', json)
|
||||
throw new BaileysError ('unexpected status ' + json.status, json)
|
||||
}
|
||||
}
|
||||
// if its a challenge request (we get it when logging in)
|
||||
@@ -160,6 +160,7 @@ export class WAConnection extends Base {
|
||||
const qr = [ref, publicKey, this.authInfo.clientID].join(',')
|
||||
this.emit ('qr', qr)
|
||||
}
|
||||
|
||||
const regenQR = () => {
|
||||
this.qrTimeout = setTimeout (() => {
|
||||
if (this.state === 'open') return
|
||||
@@ -173,9 +174,9 @@ export class WAConnection extends Base {
|
||||
.catch (err => this.log (`error in QR gen: ${err}`, MessageLogLevel.info))
|
||||
}, this.regenerateQRIntervalMs)
|
||||
}
|
||||
if (this.regenerateQRIntervalMs) {
|
||||
regenQR ()
|
||||
}
|
||||
|
||||
emitQR ()
|
||||
if (this.regenerateQRIntervalMs) regenQR ()
|
||||
|
||||
const json = await this.waitForMessage('s1', [])
|
||||
this.qrTimeout && clearTimeout (this.qrTimeout)
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
import WS from 'ws'
|
||||
import * as Utils from './Utils'
|
||||
import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError } from './Constants'
|
||||
import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions } from './Constants'
|
||||
import {WAConnection as Base} from './1.Validation'
|
||||
import Decoder from '../Binary/Decoder'
|
||||
|
||||
export class WAConnection extends Base {
|
||||
/**
|
||||
* Connect to WhatsAppWeb
|
||||
* @param timeoutMs timeout after which the connect will fail, set to null for an infinite timeout
|
||||
* @param waitForChats should the chats be waited for
|
||||
* @param options the connect options
|
||||
*/
|
||||
async connect(timeoutMs: number = null, waitForChats: boolean = true) {
|
||||
async connect(options: WAConnectOptions = {}) {
|
||||
// if we're already connected, throw an error
|
||||
if (this.state !== 'closed') throw new Error('cannot connect when state=' + this.state)
|
||||
if (this.state !== 'close') throw new Error('cannot connect when state=' + this.state)
|
||||
|
||||
this.state = 'connecting'
|
||||
this.emit ('connecting')
|
||||
|
||||
this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' })
|
||||
|
||||
const promise: Promise<void> = Utils.promiseTimeout(timeoutMs, (resolve, reject) => {
|
||||
this.conn.on('open', () => {
|
||||
this.log('connected to WhatsApp Web server, authenticating...', MessageLogLevel.info)
|
||||
// start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
|
||||
this.authenticate()
|
||||
.then(() => {
|
||||
this.startKeepAliveRequest()
|
||||
|
||||
this.conn.removeAllListeners ('error')
|
||||
this.conn.removeAllListeners ('close')
|
||||
this.conn.on ('close', () => this.unexpectedDisconnect ('closed'))
|
||||
|
||||
this.state = 'open'
|
||||
resolve()
|
||||
})
|
||||
.catch(reject)
|
||||
const { ws, cancel } = Utils.openWebSocketConnection (5000, typeof options?.retryOnNetworkErrors === 'undefined' ? true : options?.retryOnNetworkErrors)
|
||||
const promise: Promise<void> = Utils.promiseTimeout(options?.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...', MessageLogLevel.info))
|
||||
.then (() => this.authenticate())
|
||||
.then (() => {
|
||||
this.startKeepAliveRequest()
|
||||
this.conn.removeAllListeners ('error')
|
||||
this.conn.removeAllListeners ('close')
|
||||
this.conn.on ('close', () => this.unexpectedDisconnect ('close'))
|
||||
})
|
||||
.then (resolve)
|
||||
.catch (err => {
|
||||
cancel ()
|
||||
reject (err)
|
||||
})
|
||||
this.conn.on('message', m => this.onMessageRecieved(m))
|
||||
// if there was an error in the WebSocket
|
||||
this.conn.on('error', reject)
|
||||
this.conn.on('close', () => reject(new Error('closed')))
|
||||
})
|
||||
|
||||
try {
|
||||
await promise
|
||||
waitForChats && await this.receiveChatsAndContacts(timeoutMs, true)
|
||||
|
||||
const waitForChats = typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats
|
||||
waitForChats && await this.receiveChatsAndContacts(options?.timeoutMs, true)
|
||||
|
||||
this.phoneConnected = true
|
||||
this.state = 'open'
|
||||
@@ -55,9 +50,12 @@ export class WAConnection extends Base {
|
||||
|
||||
this.releasePendingRequests ()
|
||||
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
|
||||
|
||||
return this
|
||||
} catch (error) {
|
||||
this.closeInternal (error.message)
|
||||
const loggedOut = error instanceof BaileysError && error.status >= 400
|
||||
if (loggedOut && this.cancelReconnect) this.cancelReconnect ()
|
||||
this.closeInternal (loggedOut ? 'invalid_session' : error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -77,101 +75,93 @@ export class WAConnection extends Base {
|
||||
|
||||
let receivedContacts = false
|
||||
let receivedMessages = false
|
||||
let convoResolve: () => void
|
||||
|
||||
const waitForConvos = () =>
|
||||
Utils.promiseTimeout(timeoutMs, resolve => {
|
||||
convoResolve = () => {
|
||||
// de-register the callbacks, so that they don't get called again
|
||||
this.deregisterCallback(['action', 'add:last'])
|
||||
if (!stopAfterMostRecentMessage) {
|
||||
this.deregisterCallback(['action', 'add:before'])
|
||||
this.deregisterCallback(['action', 'add:unread'])
|
||||
}
|
||||
resolve()
|
||||
}
|
||||
const chatUpdate = json => {
|
||||
receivedMessages = true
|
||||
const isLast = json[1].last || stopAfterMostRecentMessage
|
||||
const messages = json[2] as WANode[]
|
||||
let resolveTask: () => void
|
||||
const deregisterCallbacks = () => {
|
||||
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
||||
this.deregisterCallback(['action', 'add:last'])
|
||||
if (!stopAfterMostRecentMessage) {
|
||||
this.deregisterCallback(['action', 'add:before'])
|
||||
this.deregisterCallback(['action', 'add:unread'])
|
||||
}
|
||||
this.deregisterCallback(['response', 'type:chat'])
|
||||
this.deregisterCallback(['response', 'type:contacts'])
|
||||
}
|
||||
const checkForResolution = () => {
|
||||
if (receivedContacts && receivedMessages) resolveTask ()
|
||||
}
|
||||
|
||||
// wait for messages to load
|
||||
const chatUpdate = json => {
|
||||
receivedMessages = true
|
||||
const isLast = json[1].last || stopAfterMostRecentMessage
|
||||
const messages = json[2] as WANode[]
|
||||
|
||||
if (messages) {
|
||||
messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => {
|
||||
const jid = message.key.remoteJid
|
||||
const chat = this.chats.get(jid)
|
||||
chat?.messages.unshift (message)
|
||||
})
|
||||
}
|
||||
// if received contacts before messages
|
||||
if (isLast && receivedContacts) convoResolve ()
|
||||
}
|
||||
|
||||
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
||||
this.registerCallback(['action', 'add:last'], chatUpdate)
|
||||
|
||||
if (!stopAfterMostRecentMessage) {
|
||||
this.registerCallback(['action', 'add:before'], chatUpdate)
|
||||
this.registerCallback(['action', 'add:unread'], chatUpdate)
|
||||
}
|
||||
})
|
||||
const waitForChats = async () => (
|
||||
Utils.promiseTimeout (timeoutMs, resolve => {
|
||||
this.registerCallback(['response', 'type:chat'], json => {
|
||||
if (json[1].duplicate || !json[2]) return
|
||||
|
||||
json[2]
|
||||
.forEach(([item, chat]: [any, WAChat]) => {
|
||||
if (!chat) {
|
||||
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
|
||||
return
|
||||
}
|
||||
chat.jid = Utils.whatsappID (chat.jid)
|
||||
chat.t = +chat.t
|
||||
chat.count = +chat.count
|
||||
chat.messages = []
|
||||
|
||||
const oldChat = this.chats.get(chat.jid)
|
||||
oldChat && this.chats.delete (oldChat)
|
||||
|
||||
this.chats.insert (chat) // chats data (log json to see what it looks like)
|
||||
})
|
||||
|
||||
this.deregisterCallback(['response', 'type:chat'])
|
||||
|
||||
this.log ('received chats list', MessageLogLevel.info)
|
||||
|
||||
if (this.chats.all().length > 0) waitForConvos().then (resolve)
|
||||
else resolve ()
|
||||
})
|
||||
})
|
||||
)
|
||||
const waitForContacts = async () => (
|
||||
new Promise (resolve => {
|
||||
this.registerCallback(['response', 'type:contacts'], json => {
|
||||
if (json[1].duplicate) 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
|
||||
})
|
||||
// if you receive contacts after messages
|
||||
// should probably resolve the promise
|
||||
if (receivedMessages) convoResolve()
|
||||
resolve ()
|
||||
|
||||
this.deregisterCallback(['response', 'type:contacts'])
|
||||
|
||||
this.log ('received contacts list', MessageLogLevel.info)
|
||||
if (messages) {
|
||||
messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => {
|
||||
const jid = message.key.remoteJid
|
||||
const chat = this.chats.get(jid)
|
||||
chat?.messages.unshift (message)
|
||||
})
|
||||
})
|
||||
)
|
||||
// wait for the chats & contacts to load
|
||||
await Promise.all( [waitForChats(), waitForContacts()] )
|
||||
}
|
||||
// if received contacts before messages
|
||||
if (isLast && receivedContacts) checkForResolution ()
|
||||
}
|
||||
|
||||
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
||||
this.registerCallback(['action', 'add:last'], chatUpdate)
|
||||
if (!stopAfterMostRecentMessage) {
|
||||
this.registerCallback(['action', 'add:before'], chatUpdate)
|
||||
this.registerCallback(['action', 'add:unread'], chatUpdate)
|
||||
}
|
||||
|
||||
this.registerCallback(['response', 'type:chat'], json => {
|
||||
if (json[1].duplicate || !json[2]) return
|
||||
|
||||
json[2]
|
||||
.forEach(([item, chat]: [any, WAChat]) => {
|
||||
if (!chat) {
|
||||
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
|
||||
return
|
||||
}
|
||||
chat.jid = Utils.whatsappID (chat.jid)
|
||||
chat.t = +chat.t
|
||||
chat.count = +chat.count
|
||||
chat.messages = []
|
||||
|
||||
const oldChat = this.chats.get(chat.jid)
|
||||
oldChat && this.chats.delete (oldChat)
|
||||
|
||||
this.chats.insert (chat) // chats data (log json to see what it looks like)
|
||||
})
|
||||
|
||||
this.log ('received chats list', MessageLogLevel.info)
|
||||
})
|
||||
// get contacts
|
||||
this.registerCallback(['response', 'type:contacts'], json => {
|
||||
if (json[1].duplicate) 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 contacts list', MessageLogLevel.info)
|
||||
checkForResolution ()
|
||||
})
|
||||
// wait for the chats & contacts to load
|
||||
await Utils.promiseTimeout (timeoutMs, (resolve, reject) => {
|
||||
resolveTask = resolve
|
||||
const rejectTask = (reason) => {
|
||||
reject (new Error(reason))
|
||||
this.off ('close', rejectTask)
|
||||
}
|
||||
this.on ('close', rejectTask)
|
||||
}).finally (deregisterCallbacks)
|
||||
|
||||
this.chats.all ().forEach (chat => {
|
||||
const respectiveContact = this.contacts[chat.jid]
|
||||
chat.title = respectiveContact?.name || respectiveContact?.notify
|
||||
@@ -181,16 +171,15 @@ export class WAConnection extends Base {
|
||||
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request
|
||||
this.pendingRequests = []
|
||||
}
|
||||
private onMessageRecieved(message) {
|
||||
private onMessageRecieved(message: string | Buffer) {
|
||||
if (message[0] === '!') {
|
||||
// when the first character in the message is an '!', the server is updating the last seen
|
||||
const timestamp = message.slice(1, message.length)
|
||||
const timestamp = message.slice(1, message.length).toString ('utf-8')
|
||||
this.lastSeen = new Date(parseInt(timestamp))
|
||||
} else {
|
||||
const decrypted = Utils.decryptWA (message, this.authInfo.macKey, this.authInfo.encKey, new Decoder())
|
||||
if (!decrypted) {
|
||||
return
|
||||
}
|
||||
if (!decrypted) return
|
||||
|
||||
const [messageTag, json] = decrypted
|
||||
|
||||
if (this.logLevel === MessageLogLevel.all) {
|
||||
@@ -261,12 +250,16 @@ export class WAConnection extends Base {
|
||||
this.cancelledReconnect = false
|
||||
try {
|
||||
while (true) {
|
||||
const {delay, cancel} = Utils.delayCancellable (5000)
|
||||
this.cancelReconnect = cancel
|
||||
const {delay, cancel} = Utils.delayCancellable (2500)
|
||||
this.cancelReconnect = () => {
|
||||
this.cancelledReconnect = true
|
||||
this.cancelReconnect = null
|
||||
cancel ()
|
||||
}
|
||||
|
||||
await delay
|
||||
try {
|
||||
await this.connect ()
|
||||
await this.connect ({ timeoutMs: 30000, retryOnNetworkErrors: true })
|
||||
this.cancelReconnect = null
|
||||
break
|
||||
} catch (error) {
|
||||
|
||||
@@ -286,7 +286,7 @@ export class WAConnection extends Base {
|
||||
/** when the connection is opening */
|
||||
on (event: 'connecting', listener: () => void): this
|
||||
/** when the connection has closed */
|
||||
on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
|
||||
on (event: 'close', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
|
||||
/** when a new QR is generated, ready for scanning */
|
||||
on (event: 'qr', listener: (qr: string) => void): this
|
||||
/** when the connection to the phone changes */
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface WALocationMessage {
|
||||
address?: string
|
||||
}
|
||||
/** Reverse stub type dictionary */
|
||||
export const WAMessageType = function () {
|
||||
export const WA_MESSAGE_STUB_TYPES = function () {
|
||||
const types = WA_MESSAGE_STUB_TYPE
|
||||
const dict: Record<number, string> = {}
|
||||
Object.keys(types).forEach(element => dict[ types[element] ] = element)
|
||||
@@ -50,14 +50,22 @@ export interface WAQuery {
|
||||
export enum ReconnectMode {
|
||||
/** does not reconnect */
|
||||
off = 0,
|
||||
/** reconnects only when the connection is 'lost' or 'closed' */
|
||||
/** reconnects only when the connection is 'lost' or 'close' */
|
||||
onConnectionLost = 1,
|
||||
/** reconnects on all disconnects, including take overs */
|
||||
onAllErrors = 2
|
||||
}
|
||||
export type WAConnectOptions = {
|
||||
/** timeout after which the connect will fail, set to null for an infinite timeout */
|
||||
timeoutMs?: number
|
||||
/** should the chats be waited for */
|
||||
waitForChats?: boolean
|
||||
/** retry on network errors while connecting */
|
||||
retryOnNetworkErrors?: boolean
|
||||
}
|
||||
|
||||
export type WAConnectionState = 'open' | 'connecting' | 'closed'
|
||||
export type DisconnectReason = 'closed' | 'lost' | 'replaced' | 'intentional'
|
||||
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
||||
export type DisconnectReason = 'close' | 'lost' | 'replaced' | 'intentional' | 'invalid_session'
|
||||
export enum MessageLogLevel {
|
||||
none=0,
|
||||
info=1,
|
||||
@@ -305,7 +313,7 @@ export interface WASendMessageResponse {
|
||||
export type BaileysEvent =
|
||||
'open' |
|
||||
'connecting' |
|
||||
'closed' |
|
||||
'close' |
|
||||
'qr' |
|
||||
'connection-phone-change' |
|
||||
'user-presence-update' |
|
||||
|
||||
@@ -5,9 +5,10 @@ import {promises as fs} from 'fs'
|
||||
import fetch from 'node-fetch'
|
||||
import { exec } from 'child_process'
|
||||
import {platform, release} from 'os'
|
||||
import WS from 'ws'
|
||||
|
||||
import Decoder from '../Binary/Decoder'
|
||||
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageType, WAMessage, WAMessageContent, BaileysError, WAMessageProto } from './Constants'
|
||||
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageContent, BaileysError, WAMessageProto } from './Constants'
|
||||
|
||||
const platformMap = {
|
||||
'aix': 'AIX',
|
||||
@@ -18,7 +19,7 @@ const platformMap = {
|
||||
export const Browsers = {
|
||||
ubuntu: browser => ['Ubuntu', browser, '18.04'] as [string, string, string],
|
||||
macOS: browser => ['Mac OS', browser, '10.15.3'] as [string, string, string],
|
||||
baileys: browser => ['Baileys', browser, '2.0'] as [string, string, string],
|
||||
baileys: browser => ['Baileys', browser, '3.0'] as [string, string, string],
|
||||
/** The appropriate browser based on your OS & release */
|
||||
appropriate: browser => [ platformMap [platform()] || 'Ubuntu', browser, release() ] as [string, string, string]
|
||||
}
|
||||
@@ -106,6 +107,42 @@ export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>
|
||||
cancel ()
|
||||
}
|
||||
}
|
||||
|
||||
export const openWebSocketConnection = (timeoutMs: number, retryOnNetworkError: boolean) => {
|
||||
const newWS = async () => {
|
||||
const conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com', timeout: timeoutMs })
|
||||
await new Promise ((resolve, reject) => {
|
||||
conn.on('open', () => {
|
||||
conn.removeAllListeners ('error')
|
||||
conn.removeAllListeners ('close')
|
||||
conn.removeAllListeners ('open')
|
||||
|
||||
resolve ()
|
||||
})
|
||||
// if there was an error in the WebSocket
|
||||
conn.on('error', reject)
|
||||
conn.on('close', () => reject(new Error('close')))
|
||||
})
|
||||
return conn
|
||||
}
|
||||
let cancelled = false
|
||||
const connect = async () => {
|
||||
while (!cancelled) {
|
||||
try {
|
||||
const ws = await newWS()
|
||||
if (!cancelled) return ws
|
||||
break
|
||||
} catch (error) {
|
||||
if (!retryOnNetworkError) throw error
|
||||
await delay (1000)
|
||||
}
|
||||
}
|
||||
throw new Error ('cancelled')
|
||||
}
|
||||
const cancel = () => cancelled = true
|
||||
return { ws: connect(), cancel }
|
||||
}
|
||||
|
||||
// whatsapp requires a message tag for every message, we just use the timestamp as one
|
||||
export function generateMessageTag(epoch?: number) {
|
||||
let tag = unixTimestampSeconds().toString()
|
||||
@@ -138,7 +175,6 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf
|
||||
let json
|
||||
let tags = null
|
||||
if (typeof data === 'string') {
|
||||
// if the first character is a "[", then the data must just be plain JSON array or object
|
||||
json = JSON.parse(data) // parse the JSON
|
||||
} else {
|
||||
if (!macKey || !encKey) {
|
||||
@@ -159,7 +195,12 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf
|
||||
const computedChecksum = hmacSign(data, macKey) // compute the sign of the message we recieved using our macKey
|
||||
|
||||
if (!checksum.equals(computedChecksum)) {
|
||||
throw new Error (`Checksums don't match:\nog: ${checksum.toString('hex')}\ncomputed: ${computedChecksum.toString('hex')}`)
|
||||
throw new Error (`
|
||||
Checksums don't match:
|
||||
og: ${checksum.toString('hex')}
|
||||
computed: ${computedChecksum.toString('hex')}
|
||||
message: ${message.slice(0, 80).toString()}
|
||||
`)
|
||||
}
|
||||
// the checksum the server sent, must match the one we computed for the message to be valid
|
||||
const decrypted = aesDecrypt(data, encKey) // decrypt using AES
|
||||
|
||||
Reference in New Issue
Block a user