mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Major redo with respect to chats/contacts -- read desc
Waiting for chats & contacts is hella unreliable, so I've put them as events 1. receive chats via the `chats-received` event. If new chats are found, the flag for that is sent as well 2. receive contacts via the `contacts-received` event 3. When WA sends older messages, the `chats-update` or `chat-update` event is triggered 4. Baileys keeps track of all the changed conversations between connects Connects almost always take less than 10 seconds!
This commit is contained in:
@@ -18,6 +18,7 @@ async function example() {
|
|||||||
conn.logger.level = 'debug' // set to 'debug' to see what kind of stuff you can implement
|
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
|
// attempt to reconnect at most 10 times in a row
|
||||||
conn.connectOptions.maxRetries = 10
|
conn.connectOptions.maxRetries = 10
|
||||||
|
conn.connectOptions.waitForChats = false
|
||||||
conn.chatOrderingKey = waChatKey(true) // order chats such that pinned chats are on top
|
conn.chatOrderingKey = waChatKey(true) // order chats such that pinned chats are on top
|
||||||
|
|
||||||
conn.on ('credentials-updated', () => {
|
conn.on ('credentials-updated', () => {
|
||||||
@@ -26,6 +27,12 @@ async function example() {
|
|||||||
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
|
||||||
fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file
|
fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file
|
||||||
})
|
})
|
||||||
|
conn.on('chats-received', ({ hasNewChats }) => {
|
||||||
|
console.log(`you have ${conn.chats.length} chats, new chats available: ${hasNewChats}`)
|
||||||
|
})
|
||||||
|
conn.on('contacts-received', () => {
|
||||||
|
console.log(`you have ${Object.keys(conn.contacts).length} chats`)
|
||||||
|
})
|
||||||
|
|
||||||
// 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')
|
||||||
@@ -33,9 +40,7 @@ async function example() {
|
|||||||
//conn.connectOptions.agent = ProxyAgent ('http://1.0.180.120:8080')
|
//conn.connectOptions.agent = ProxyAgent ('http://1.0.180.120:8080')
|
||||||
await conn.connect()
|
await conn.connect()
|
||||||
|
|
||||||
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.length + ' chats & ' + Object.keys(conn.contacts).length + ' contacts')
|
|
||||||
|
|
||||||
// uncomment to load all unread messages
|
// uncomment to load all unread messages
|
||||||
//const unread = await conn.loadAllUnreadMessages ()
|
//const unread = await conn.loadAllUnreadMessages ()
|
||||||
//console.log ('you have ' + unread.length + ' unread messages')
|
//console.log ('you have ' + unread.length + ' unread messages')
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -24,7 +24,7 @@ Create and cd to your NPM project directory and then in terminal, write:
|
|||||||
1. stable: `npm install @adiwajshing/baileys`
|
1. stable: `npm install @adiwajshing/baileys`
|
||||||
2. stabl-ish w quicker fixes & latest features: `npm install github:adiwajshing/baileys`
|
2. stabl-ish w quicker fixes & latest features: `npm install github:adiwajshing/baileys`
|
||||||
|
|
||||||
Do note, the library will most likely vary if you're using the NPM package, read that [here](https://www.npmjs.com/package/@adiwajshing/baileys)
|
Do note, the library will likely vary if you're using the NPM package, read that [here](https://www.npmjs.com/package/@adiwajshing/baileys)
|
||||||
|
|
||||||
Then import in your code using:
|
Then import in your code using:
|
||||||
``` ts
|
``` ts
|
||||||
@@ -43,14 +43,29 @@ import { WAConnection } from '@adiwajshing/baileys'
|
|||||||
|
|
||||||
async function connectToWhatsApp () {
|
async function connectToWhatsApp () {
|
||||||
const conn = new WAConnection()
|
const conn = new WAConnection()
|
||||||
|
// called when WA sends chats
|
||||||
await conn.connect ()
|
// this can take up to a few minutes if you have thousands of chats!
|
||||||
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")")
|
conn.on('chats-received', async ({ hasNewChats }) => {
|
||||||
// every chat object has a list of most recent messages
|
console.log(`you have ${conn.chats.length} chats, new chats available: ${hasNewChats}`)
|
||||||
console.log ("you have " + conn.chats.all().length + " chats")
|
|
||||||
|
|
||||||
const unread = await conn.loadAllUnreadMessages ()
|
const unread = await conn.loadAllUnreadMessages ()
|
||||||
console.log ("you have " + unread.length + " unread messages")
|
console.log ("you have " + unread.length + " unread messages")
|
||||||
|
})
|
||||||
|
// called when WA sends chats
|
||||||
|
// this can take up to a few minutes if you have thousands of contacts!
|
||||||
|
conn.on('contacts-received', () => {
|
||||||
|
console.log('you have ' + Object.keys(conn.contacts).length + ' contacts')
|
||||||
|
})
|
||||||
|
|
||||||
|
await conn.connect ()
|
||||||
|
conn.on('chat-update', chatUpdate => {
|
||||||
|
// `chatUpdate` is a partial object, containing the updated properties of the chat
|
||||||
|
// received a new message
|
||||||
|
if (chatUpdate.messages && chatUpdate.count) {
|
||||||
|
const message = chatUpdate.messages.all()[0]
|
||||||
|
console.log (message)
|
||||||
|
} else console.log (chatUpdate) // see updates (can be archived, pinned etc.)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
// run in main file
|
// run in main file
|
||||||
connectToWhatsApp ()
|
connectToWhatsApp ()
|
||||||
@@ -59,12 +74,7 @@ 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 set the following property to false:
|
Do note, the `conn.chats` object is a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons:
|
||||||
``` ts
|
|
||||||
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:
|
|
||||||
- Most applications require chats to be ordered in descending order of time. (`KeyedDB` does this in `log(N)` time)
|
- Most applications require chats to be ordered in descending order of time. (`KeyedDB` does this in `log(N)` time)
|
||||||
- Most applications require pagination of chats (Use `chats.paginated()`)
|
- Most applications require pagination of chats (Use `chats.paginated()`)
|
||||||
- Most applications require **O(1)** access to chats via the chat ID. (Use `chats.get(jid)` with `KeyedDB`)
|
- Most applications require **O(1)** access to chats via the chat ID. (Use `chats.get(jid)` with `KeyedDB`)
|
||||||
@@ -87,28 +97,19 @@ The entire `WAConnectOptions` struct is mentioned here with default values:
|
|||||||
``` ts
|
``` ts
|
||||||
conn.connectOptions = {
|
conn.connectOptions = {
|
||||||
/** 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_000
|
regenerateQRIntervalMs?: 30_000,
|
||||||
/** fails the connection if no data is received for X seconds */
|
/** fails the connection if no data is received for X seconds */
|
||||||
maxIdleTimeMs?: 15_000
|
maxIdleTimeMs?: 15_000,
|
||||||
/** maximum attempts to connect */
|
/** maximum attempts to connect */
|
||||||
maxRetries?: 5
|
maxRetries?: 5,
|
||||||
/** should the chats be waited for;
|
|
||||||
* should generally keep this as true, unless you only care about sending & receiving new messages
|
|
||||||
* & don't care about chat history
|
|
||||||
* */
|
|
||||||
waitForChats?: true
|
|
||||||
/** if set to true, the connect only waits for the last message of the chat
|
|
||||||
* setting to false, generally yields a faster connect
|
|
||||||
*/
|
|
||||||
waitOnlyForLastMessage?: false
|
|
||||||
/** max time for the phone to respond to a connectivity test */
|
/** max time for the phone to respond to a connectivity test */
|
||||||
phoneResponseTime?: 10_000
|
phoneResponseTime?: 10_000,
|
||||||
/** minimum time between new connections */
|
/** minimum time between new connections */
|
||||||
connectCooldownMs?: 3000
|
connectCooldownMs?: 3000,
|
||||||
/** agent used for WS connections (could be a proxy agent) */
|
/** agent used for WS connections (could be a proxy agent) */
|
||||||
agent?: Agent = undefined
|
agent?: Agent = undefined,
|
||||||
/** agent used for fetch requests -- uploading/downloading media */
|
/** agent used for fetch requests -- uploading/downloading media */
|
||||||
fetchAgent?: Agent = undefined
|
fetchAgent?: Agent = undefined,
|
||||||
/** always uses takeover for connecting */
|
/** always uses takeover for connecting */
|
||||||
alwaysUseTakeover: true
|
alwaysUseTakeover: true
|
||||||
} as WAConnectOptions
|
} as WAConnectOptions
|
||||||
@@ -200,8 +201,14 @@ on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void):
|
|||||||
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 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 contacts are sent by WA */
|
||||||
|
on (event: 'contacts-received', listener: () => void): this
|
||||||
|
/** when chats are sent by WA */
|
||||||
|
on (event: 'chats-received', listener: (update: {hasNewChats: boolean}) => void): this
|
||||||
|
/** when multiple chats are updated (new message, updated message, deleted, pinned, etc) */
|
||||||
|
on (event: 'chats-update', listener: (chats: (Partial<WAChat> & { jid: string })[]) => void): this
|
||||||
/** when a chat is updated (new message, updated message, deleted, pinned, etc) */
|
/** when a chat is updated (new message, updated message, deleted, pinned, etc) */
|
||||||
on (event: 'chat-update', listener: (chat: WAChatUpdate) => void): this
|
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
|
||||||
/** when a message's status is updated (deleted, delivered, read, sent etc.) */
|
/** when a message's status is updated (deleted, delivered, read, sent etc.) */
|
||||||
on (event: 'message-status-update', listener: (message: WAMessageStatusUpdate) => void): this
|
on (event: 'message-status-update', listener: (message: WAMessageStatusUpdate) => void): this
|
||||||
/** when participants are added to a group */
|
/** when participants are added to a group */
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST
|
|||||||
|
|
||||||
export const makeConnection = () => {
|
export const makeConnection = () => {
|
||||||
const conn = new WAConnection()
|
const conn = new WAConnection()
|
||||||
conn.connectOptions.maxIdleTimeMs = 45_000
|
conn.connectOptions.maxIdleTimeMs = 15_000
|
||||||
conn.logger.level = 'debug'
|
conn.logger.level = 'debug'
|
||||||
|
|
||||||
let evCounts = {}
|
let evCounts = {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as assert from 'assert'
|
import * as assert from 'assert'
|
||||||
import {WAConnection} from '../WAConnection/WAConnection'
|
import {WAConnection} from '../WAConnection/WAConnection'
|
||||||
import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode, DisconnectReason } from '../WAConnection/Constants'
|
import { AuthenticationCredentialsBase64, BaileysError, ReconnectMode, DisconnectReason, WAChat } from '../WAConnection/Constants'
|
||||||
import { delay } from '../WAConnection/Utils'
|
import { delay } from '../WAConnection/Utils'
|
||||||
import { assertChatDBIntegrity, makeConnection, testJid } from './Common'
|
import { assertChatDBIntegrity, makeConnection, testJid } from './Common'
|
||||||
|
|
||||||
@@ -291,14 +291,11 @@ describe ('Pending Requests', () => {
|
|||||||
it ('should correctly send updates', async () => {
|
it ('should correctly send updates', async () => {
|
||||||
const conn = makeConnection ()
|
const conn = makeConnection ()
|
||||||
conn.pendingRequestTimeoutMs = null
|
conn.pendingRequestTimeoutMs = null
|
||||||
|
|
||||||
conn.loadAuthInfo('./auth_info.json')
|
conn.loadAuthInfo('./auth_info.json')
|
||||||
|
|
||||||
|
const task = new Promise(resolve => conn.once('chats-received', resolve))
|
||||||
await conn.connect ()
|
await conn.connect ()
|
||||||
|
await task
|
||||||
conn.close ()
|
|
||||||
|
|
||||||
const result0 = await conn.connect ()
|
|
||||||
assert.deepEqual (result0.updatedChats, {})
|
|
||||||
|
|
||||||
conn.close ()
|
conn.close ()
|
||||||
|
|
||||||
@@ -306,19 +303,18 @@ describe ('Pending Requests', () => {
|
|||||||
oldChat.archive = 'true' // mark the first chat as archived
|
oldChat.archive = 'true' // mark the first chat as archived
|
||||||
oldChat.modify_tag = '1234' // change modify tag to detect change
|
oldChat.modify_tag = '1234' // change modify tag to detect change
|
||||||
|
|
||||||
// close the socket after a few seconds second to see if updates are correct after a reconnect
|
const promise = new Promise(resolve => conn.once('chats-update', resolve))
|
||||||
setTimeout (() => conn['conn'].close(), 5000)
|
|
||||||
|
|
||||||
const result = await conn.connect ()
|
const result = await conn.connect ()
|
||||||
assert.ok (!result.newConnection)
|
assert.ok (!result.newConnection)
|
||||||
|
|
||||||
const chat = result.updatedChats[oldChat.jid]
|
const chats = await promise as Partial<WAChat>[]
|
||||||
|
const chat = chats.find (c => c.jid === oldChat.jid)
|
||||||
assert.ok (chat)
|
assert.ok (chat)
|
||||||
|
|
||||||
assert.ok ('archive' in chat)
|
assert.ok ('archive' in chat)
|
||||||
assert.equal (Object.keys(chat).length, 2)
|
assert.strictEqual (Object.keys(chat).length, 3)
|
||||||
|
assert.strictEqual (Object.keys(chats).length, 1)
|
||||||
assert.equal (Object.keys(result.updatedChats).length, 1)
|
|
||||||
|
|
||||||
conn.close ()
|
conn.close ()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const logger = pino({ prettyPrint: { levelFirst: true, ignore: 'hostname', trans
|
|||||||
|
|
||||||
export class WAConnection extends EventEmitter {
|
export class WAConnection extends EventEmitter {
|
||||||
/** The version of WhatsApp Web we're telling the servers we are */
|
/** The version of WhatsApp Web we're telling the servers we are */
|
||||||
version: [number, number, number] = [2, 2045, 15]
|
version: [number, number, number] = [2, 2045, 19]
|
||||||
/** The Browser we're telling the WhatsApp Web servers we are */
|
/** The Browser we're telling the WhatsApp Web servers we are */
|
||||||
browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome')
|
browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome')
|
||||||
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
|
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
|
||||||
@@ -43,9 +43,9 @@ export class WAConnection extends EventEmitter {
|
|||||||
state: WAConnectionState = 'close'
|
state: WAConnectionState = 'close'
|
||||||
connectOptions: WAConnectOptions = {
|
connectOptions: WAConnectOptions = {
|
||||||
regenerateQRIntervalMs: 30_000,
|
regenerateQRIntervalMs: 30_000,
|
||||||
maxIdleTimeMs: 40_000,
|
maxIdleTimeMs: 15_000,
|
||||||
waitOnlyForLastMessage: false,
|
waitOnlyForLastMessage: false,
|
||||||
waitForChats: true,
|
waitForChats: false,
|
||||||
maxRetries: 10,
|
maxRetries: 10,
|
||||||
connectCooldownMs: 4000,
|
connectCooldownMs: 4000,
|
||||||
phoneResponseTime: 15_000,
|
phoneResponseTime: 15_000,
|
||||||
@@ -67,6 +67,7 @@ export class WAConnection extends EventEmitter {
|
|||||||
maxCachedMessages = 50
|
maxCachedMessages = 50
|
||||||
loadProfilePicturesForChatsAutomatically = true
|
loadProfilePicturesForChatsAutomatically = true
|
||||||
|
|
||||||
|
lastChatsReceived: Date
|
||||||
chats = new KeyedDB (Utils.waChatKey(false), value => value.jid)
|
chats = new KeyedDB (Utils.waChatKey(false), value => value.jid)
|
||||||
contacts: { [k: string]: WAContact } = {}
|
contacts: { [k: string]: WAContact } = {}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,13 @@ export class WAConnection extends Base {
|
|||||||
this.emit ('connection-validated', this.user)
|
this.emit ('connection-validated', this.user)
|
||||||
|
|
||||||
if (this.loadProfilePicturesForChatsAutomatically) {
|
if (this.loadProfilePicturesForChatsAutomatically) {
|
||||||
const response = await this.query({ json: ['query', 'ProfilePicThumb', this.user.jid], waitForOpen: false, expect200: false, requiresPhoneConnection: false, startDebouncedTimeout: true })
|
const response = await this.query({
|
||||||
|
json: ['query', 'ProfilePicThumb', this.user.jid],
|
||||||
|
waitForOpen: false,
|
||||||
|
expect200: false,
|
||||||
|
requiresPhoneConnection: false,
|
||||||
|
startDebouncedTimeout: true
|
||||||
|
})
|
||||||
this.user.imgUrl = response?.eurl || ''
|
this.user.imgUrl = response?.eurl || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as Utils from './Utils'
|
import * as Utils from './Utils'
|
||||||
import { WAMessage, WAChat, WANode, KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, WAContact, TimedOutError, CancelledError, WAOpenResult, DEFAULT_ORIGIN, WS_URL } from './Constants'
|
import { KEEP_ALIVE_INTERVAL_MS, BaileysError, WAConnectOptions, DisconnectReason, UNAUTHORIZED_CODES, CancelledError, WAOpenResult, DEFAULT_ORIGIN, WS_URL } 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'
|
||||||
import WS from 'ws'
|
import WS from 'ws'
|
||||||
import KeyedDB from '@adiwajshing/keyed-db'
|
|
||||||
|
|
||||||
const DEF_CALLBACK_PREFIX = 'CB:'
|
const DEF_CALLBACK_PREFIX = 'CB:'
|
||||||
const DEF_TAG_PREFIX = 'TAG:'
|
const DEF_TAG_PREFIX = 'TAG:'
|
||||||
@@ -22,7 +21,7 @@ export class WAConnection extends Base {
|
|||||||
|
|
||||||
let tries = 0
|
let tries = 0
|
||||||
let lastConnect = this.lastDisconnectTime
|
let lastConnect = this.lastDisconnectTime
|
||||||
var updates
|
let updates: any
|
||||||
while (this.state === 'connecting') {
|
while (this.state === 'connecting') {
|
||||||
tries += 1
|
tries += 1
|
||||||
try {
|
try {
|
||||||
@@ -48,9 +47,7 @@ export class WAConnection extends Base {
|
|||||||
if (!willReconnect) throw error
|
if (!willReconnect) throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const result: WAOpenResult = { user: this.user, newConnection, ...(updates || {}) }
|
||||||
const updatedChats = !!this.lastDisconnectTime && updates
|
|
||||||
const result: WAOpenResult = { user: this.user, newConnection, updatedChats }
|
|
||||||
this.emit ('open', result)
|
this.emit ('open', result)
|
||||||
|
|
||||||
this.logger.info ('opened connection to WhatsApp Web')
|
this.logger.info ('opened connection to WhatsApp Web')
|
||||||
@@ -73,7 +70,7 @@ export class WAConnection extends Base {
|
|||||||
!this.connectOptions.alwaysUseTakeover
|
!this.connectOptions.alwaysUseTakeover
|
||||||
const reconnectID = shouldUseReconnect && this.user.jid.replace ('@s.whatsapp.net', '@c.us')
|
const reconnectID = shouldUseReconnect && this.user.jid.replace ('@s.whatsapp.net', '@c.us')
|
||||||
|
|
||||||
this.conn = new WS(WS_URL, null, {
|
this.conn = new WS(WS_URL, null, {
|
||||||
origin: DEFAULT_ORIGIN,
|
origin: DEFAULT_ORIGIN,
|
||||||
timeout: this.connectOptions.maxIdleTimeMs,
|
timeout: this.connectOptions.maxIdleTimeMs,
|
||||||
agent: options.agent,
|
agent: options.agent,
|
||||||
@@ -90,12 +87,12 @@ export class WAConnection extends Base {
|
|||||||
|
|
||||||
this.conn.on ('open', async () => {
|
this.conn.on ('open', async () => {
|
||||||
this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`)
|
this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`)
|
||||||
let waitForChats: Promise<{[k: string]: Partial<WAChat>}>
|
let waitForChats: Promise<any>
|
||||||
// add wait for chats promise if required
|
// add wait for chats promise if required
|
||||||
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
||||||
const {wait, cancelChats} = this.receiveChatsAndContacts(this.connectOptions.waitOnlyForLastMessage)
|
const {wait, cancellations} = this.receiveChatsAndContacts(this.connectOptions.waitOnlyForLastMessage)
|
||||||
waitForChats = wait
|
waitForChats = wait
|
||||||
rejections.push (cancelChats)
|
rejections.push (...cancellations)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const [, result] = await Promise.all (
|
const [, result] = await Promise.all (
|
||||||
@@ -116,7 +113,7 @@ export class WAConnection extends Base {
|
|||||||
})
|
})
|
||||||
this.conn.on('error', rejectAll)
|
this.conn.on('error', rejectAll)
|
||||||
this.conn.on('close', () => rejectAll(new Error(DisconnectReason.close)))
|
this.conn.on('close', () => rejectAll(new Error(DisconnectReason.close)))
|
||||||
}) as Promise<void | { [k: string]: Partial<WAChat> }>
|
}) as Promise<void | any>
|
||||||
)
|
)
|
||||||
|
|
||||||
this.on ('ws-close', rejectAll)
|
this.on ('ws-close', rejectAll)
|
||||||
@@ -140,135 +137,30 @@ export class WAConnection extends Base {
|
|||||||
* Must be called immediately after connect
|
* Must be called immediately after connect
|
||||||
*/
|
*/
|
||||||
protected receiveChatsAndContacts(waitOnlyForLast: boolean) {
|
protected receiveChatsAndContacts(waitOnlyForLast: boolean) {
|
||||||
const chats = new KeyedDB(this.chatOrderingKey, c => c.jid)
|
const rejectableWaitForEvent = (event: string) => {
|
||||||
const contacts = {}
|
let rejectTask = (_: Error) => {}
|
||||||
|
const task = new Promise((resolve, reject) => {
|
||||||
let receivedChats = false
|
this.once (event, data => {
|
||||||
let receivedContacts = false
|
this.startDebouncedTimeout() // start timeout again
|
||||||
let receivedMessages = false
|
resolve(data)
|
||||||
|
|
||||||
let resolveTask: () => void
|
|
||||||
let rejectTask: (e: Error) => void
|
|
||||||
const checkForResolution = () => receivedContacts && receivedChats && receivedMessages && resolveTask ()
|
|
||||||
// wait for messages to load
|
|
||||||
const messagesUpdate = json => {
|
|
||||||
this.startDebouncedTimeout () // restart debounced timeout
|
|
||||||
|
|
||||||
const isLast = json[1].last || waitOnlyForLast
|
|
||||||
const messages = json[2] as WANode[]
|
|
||||||
|
|
||||||
if (messages) {
|
|
||||||
messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => {
|
|
||||||
const jid = message.key.remoteJid
|
|
||||||
const chat = chats.get(jid)
|
|
||||||
if (chat) {
|
|
||||||
const fm = chat.messages.all()[0]
|
|
||||||
|
|
||||||
const prevEpoch = (fm && fm['epoch']) || 0
|
|
||||||
|
|
||||||
message['epoch'] = prevEpoch-1
|
|
||||||
chat.messages.insert (message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
rejectTask = reject
|
||||||
if (isLast) receivedMessages = true
|
|
||||||
// if received contacts before messages
|
|
||||||
if (isLast && receivedContacts) checkForResolution ()
|
|
||||||
}
|
|
||||||
const chatUpdate = json => {
|
|
||||||
if (json[1].duplicate || !json[2]) return
|
|
||||||
this.startDebouncedTimeout () // restart debounced timeout
|
|
||||||
|
|
||||||
json[2]
|
|
||||||
.forEach(([item, chat]: [any, WAChat]) => {
|
|
||||||
if (!chat) {
|
|
||||||
this.logger.warn (`unexpectedly got null chat: ${item}`, chat)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.jid = Utils.whatsappID (chat.jid)
|
|
||||||
chat.t = +chat.t
|
|
||||||
chat.count = +chat.count
|
|
||||||
chat.messages = Utils.newMessagesDB()
|
|
||||||
|
|
||||||
// chats data (log json to see what it looks like)
|
|
||||||
!chats.get (chat.jid) && chats.insert (chat)
|
|
||||||
})
|
})
|
||||||
|
return { reject: rejectTask, task }
|
||||||
this.logger.info (`received ${json[2].length} chats`)
|
|
||||||
receivedChats = true
|
|
||||||
|
|
||||||
if (json[2].length === 0) receivedMessages = true
|
|
||||||
checkForResolution ()
|
|
||||||
}
|
}
|
||||||
const contactsUpdate = json => {
|
const events = [ 'chats-received', 'contacts-received', 'CB:action,add:last' ]
|
||||||
if (json[1].duplicate || !json[2]) return
|
if (!waitOnlyForLast) events.push('CB:action,add:before', 'CB:action,add:unread')
|
||||||
this.startDebouncedTimeout () // restart debounced timeout
|
|
||||||
|
|
||||||
receivedContacts = true
|
const cancellations = []
|
||||||
|
const wait = Promise.all (
|
||||||
json[2].forEach(([type, contact]: ['user', WAContact]) => {
|
events.map (ev => {
|
||||||
if (!contact) return this.logger.info (`unexpectedly got null contact: ${type}`, contact)
|
const {reject, task} = rejectableWaitForEvent(ev)
|
||||||
|
cancellations.push(reject)
|
||||||
contact.jid = Utils.whatsappID (contact.jid)
|
return task
|
||||||
contacts[contact.jid] = contact
|
|
||||||
})
|
})
|
||||||
this.logger.info (`received ${json[2].length} contacts`)
|
).then(([update]) => update as { hasNewChats: boolean })
|
||||||
checkForResolution ()
|
|
||||||
}
|
return { wait, cancellations }
|
||||||
const registerCallbacks = () => {
|
|
||||||
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
|
|
||||||
this.on(DEF_CALLBACK_PREFIX + 'action,add:last', messagesUpdate)
|
|
||||||
this.on(DEF_CALLBACK_PREFIX + 'action,add:before', messagesUpdate)
|
|
||||||
this.on(DEF_CALLBACK_PREFIX + 'action,add:unread', messagesUpdate)
|
|
||||||
// get chats
|
|
||||||
this.on(DEF_CALLBACK_PREFIX + 'response,type:chat', chatUpdate)
|
|
||||||
// get contacts
|
|
||||||
this.on(DEF_CALLBACK_PREFIX + 'response,type:contacts', contactsUpdate)
|
|
||||||
}
|
|
||||||
const deregisterCallbacks = () => {
|
|
||||||
this.off(DEF_CALLBACK_PREFIX + 'action,add:last', messagesUpdate)
|
|
||||||
this.off(DEF_CALLBACK_PREFIX + 'action,add:before', messagesUpdate)
|
|
||||||
this.off(DEF_CALLBACK_PREFIX + 'action,add:unread', messagesUpdate)
|
|
||||||
this.off(DEF_CALLBACK_PREFIX + 'response,type:chat', chatUpdate)
|
|
||||||
this.off(DEF_CALLBACK_PREFIX + 'response,type:contacts', contactsUpdate)
|
|
||||||
}
|
|
||||||
// wait for the chats & contacts to load
|
|
||||||
const wait = (async () => {
|
|
||||||
try {
|
|
||||||
registerCallbacks ()
|
|
||||||
|
|
||||||
await new Promise ((resolve, reject) => {
|
|
||||||
resolveTask = resolve
|
|
||||||
rejectTask = reject
|
|
||||||
})
|
|
||||||
|
|
||||||
const oldChats = this.chats
|
|
||||||
const updatedChats: { [k: string]: Partial<WAChat> } = {}
|
|
||||||
|
|
||||||
chats.all().forEach (chat => {
|
|
||||||
const respectiveContact = contacts[chat.jid]
|
|
||||||
chat.name = respectiveContact?.name || respectiveContact?.notify || chat.name
|
|
||||||
|
|
||||||
const oldChat = oldChats.get(chat.jid)
|
|
||||||
if (!oldChat) {
|
|
||||||
updatedChats[chat.jid] = chat
|
|
||||||
} else if (oldChat.t < chat.t || oldChat.modify_tag !== chat.modify_tag) {
|
|
||||||
const changes = Utils.shallowChanges (oldChat, chat)
|
|
||||||
delete changes.messages
|
|
||||||
updatedChats[chat.jid] = changes
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.chats = chats
|
|
||||||
this.contacts = contacts
|
|
||||||
|
|
||||||
return updatedChats
|
|
||||||
} finally {
|
|
||||||
deregisterCallbacks ()
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
return { wait, cancelChats: () => rejectTask (CancelledError()) }
|
|
||||||
}
|
}
|
||||||
private onMessageRecieved(message: string | Buffer) {
|
private onMessageRecieved(message: string | Buffer) {
|
||||||
if (message[0] === '!') {
|
if (message[0] === '!') {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as QR from 'qrcode-terminal'
|
import * as QR from 'qrcode-terminal'
|
||||||
import { WAConnection as Base } from './3.Connect'
|
import { WAConnection as Base } from './3.Connect'
|
||||||
import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WAOpenResult, Presence, AuthenticationCredentials, WAParticipantAction, WAGroupMetadata, WAUser } from './Constants'
|
import { WAMessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, PresenceUpdate, BaileysEvent, DisconnectReason, WAOpenResult, Presence, AuthenticationCredentials, WAParticipantAction, WAGroupMetadata, WAUser, WANode } from './Constants'
|
||||||
import { whatsappID, unixTimestampSeconds, isGroupID, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey, newMessagesDB } from './Utils'
|
import { whatsappID, unixTimestampSeconds, isGroupID, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey, newMessagesDB, shallowChanges } from './Utils'
|
||||||
import KeyedDB from '@adiwajshing/keyed-db'
|
import KeyedDB from '@adiwajshing/keyed-db'
|
||||||
import { Mutex } from './Mutex'
|
import { Mutex } from './Mutex'
|
||||||
|
|
||||||
@@ -9,6 +9,123 @@ export class WAConnection extends Base {
|
|||||||
|
|
||||||
constructor () {
|
constructor () {
|
||||||
super ()
|
super ()
|
||||||
|
// chats received
|
||||||
|
this.on('CB:response,type:chat', json => {
|
||||||
|
if (json[1].duplicate || !json[2]) return
|
||||||
|
const chats = new KeyedDB(this.chatOrderingKey, c => c.jid)
|
||||||
|
|
||||||
|
json[2].forEach(([item, chat]: [any, WAChat]) => {
|
||||||
|
if (!chat) {
|
||||||
|
this.logger.warn (`unexpectedly got null chat: ${item}`, chat)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chat.jid = whatsappID (chat.jid)
|
||||||
|
chat.t = +chat.t
|
||||||
|
chat.count = +chat.count
|
||||||
|
chat.messages = newMessagesDB()
|
||||||
|
// chats data (log json to see what it looks like)
|
||||||
|
!chats.get (chat.jid) && chats.insert (chat)
|
||||||
|
})
|
||||||
|
this.logger.info (`received ${json[2].length} chats`)
|
||||||
|
|
||||||
|
const oldChats = this.chats
|
||||||
|
const updatedChats = []
|
||||||
|
let hasNewChats = false
|
||||||
|
|
||||||
|
chats.all().forEach (chat => {
|
||||||
|
const respectiveContact = this.contacts[chat.jid]
|
||||||
|
chat.name = respectiveContact?.name || respectiveContact?.notify || chat.name
|
||||||
|
|
||||||
|
const oldChat = oldChats.get(chat.jid)
|
||||||
|
if (!oldChat) {
|
||||||
|
hasNewChats = true
|
||||||
|
} else {
|
||||||
|
chat.messages = oldChat.messages
|
||||||
|
if (oldChat.t !== chat.t || oldChat.modify_tag !== chat.modify_tag) {
|
||||||
|
const changes = shallowChanges (oldChat, chat)
|
||||||
|
delete changes.messages
|
||||||
|
updatedChats.push({ jid: chat.jid, ...changes })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.chats = chats
|
||||||
|
this.lastChatsReceived = new Date()
|
||||||
|
|
||||||
|
updatedChats.length > 0 && this.emit('chats-update', updatedChats)
|
||||||
|
|
||||||
|
this.emit('chats-received', { hasNewChats })
|
||||||
|
})
|
||||||
|
// we store these last messages
|
||||||
|
const lastMessages = {}
|
||||||
|
// messages received
|
||||||
|
const messagesUpdate = (json, style: 'prepend' | 'append') => {
|
||||||
|
const messages = json[2] as WANode[]
|
||||||
|
if (messages) {
|
||||||
|
const updates: { [k: string]: KeyedDB<WAMessage, string> } = {}
|
||||||
|
messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => {
|
||||||
|
const jid = message.key.remoteJid
|
||||||
|
const chat = this.chats.get(jid)
|
||||||
|
const mKeyID = WA_MESSAGE_ID(message)
|
||||||
|
if (chat && !chat.messages.get(mKeyID)) {
|
||||||
|
if (style === 'prepend') {
|
||||||
|
const fm = chat.messages.get(lastMessages[jid])
|
||||||
|
if (!fm) return
|
||||||
|
const prevEpoch = fm['epoch']
|
||||||
|
message['epoch'] = prevEpoch-1
|
||||||
|
} else if (style === 'append') {
|
||||||
|
const lm = chat.messages.all()[chat.messages.length-1]
|
||||||
|
const prevEpoch = (lm && lm['epoch']) || 0
|
||||||
|
message['epoch'] = prevEpoch+100 // hacky way to allow more previous messages
|
||||||
|
}
|
||||||
|
chat.messages.insert (message)
|
||||||
|
|
||||||
|
updates[jid] = updates[jid] || newMessagesDB()
|
||||||
|
updates[jid].insert(message)
|
||||||
|
|
||||||
|
lastMessages[jid] = mKeyID
|
||||||
|
} else if (!chat) this.logger.debug({ jid }, `chat not found`)
|
||||||
|
})
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
this.emit ('chats-update',
|
||||||
|
Object.keys(updates).map(jid => ({ jid, messages: updates[jid] }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.on('CB:action,add:last', json => messagesUpdate(json, 'append'))
|
||||||
|
this.on('CB:action,add:before', json => messagesUpdate(json, 'prepend'))
|
||||||
|
this.on('CB:action,add:unread', json => messagesUpdate(json, 'prepend'))
|
||||||
|
|
||||||
|
// contacts received
|
||||||
|
this.on('CB:response,type:contacts', json => {
|
||||||
|
if (json[1].duplicate || !json[2]) return
|
||||||
|
const contacts: { [_: string]: WAContact } = {}
|
||||||
|
|
||||||
|
json[2].forEach(([type, contact]: ['user', WAContact]) => {
|
||||||
|
if (!contact) return this.logger.info (`unexpectedly got null contact: ${type}`, contact)
|
||||||
|
|
||||||
|
contact.jid = whatsappID (contact.jid)
|
||||||
|
contacts[contact.jid] = contact
|
||||||
|
})
|
||||||
|
// update chat names
|
||||||
|
const updatedChats = []
|
||||||
|
this.chats.all().forEach(c => {
|
||||||
|
const contact = contacts[c.jid]
|
||||||
|
if (contact) {
|
||||||
|
const name = contact?.name || contact?.notify || c.name
|
||||||
|
if (name !== c.name) {
|
||||||
|
updatedChats.push({ jid: c.jid, name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
updatedChats.length > 0 && this.emit('chats-update', updatedChats)
|
||||||
|
|
||||||
|
this.logger.info (`received ${json[2].length} contacts`)
|
||||||
|
this.contacts = contacts
|
||||||
|
|
||||||
|
this.emit('contacts-received')
|
||||||
|
})
|
||||||
|
|
||||||
// new messages
|
// new messages
|
||||||
this.on('CB:action,add:relay,message', json => {
|
this.on('CB:action,add:relay,message', json => {
|
||||||
const message = json[2][0][2] as WAMessage
|
const message = json[2][0][2] as WAMessage
|
||||||
@@ -386,6 +503,12 @@ export class WAConnection extends Base {
|
|||||||
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 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 contacts are sent by WA */
|
||||||
|
on (event: 'contacts-received', listener: () => void): this
|
||||||
|
/** when chats are sent by WA */
|
||||||
|
on (event: 'chats-received', listener: (update: {hasNewChats: boolean}) => void): this
|
||||||
|
/** when multiple chats are updated (new message, updated message, deleted, pinned, etc) */
|
||||||
|
on (event: 'chats-update', listener: (chats: (Partial<WAChat> & { jid: string })[]) => void): this
|
||||||
/** when a chat is updated (new message, updated message, deleted, pinned, etc) */
|
/** when a chat is updated (new message, updated message, deleted, pinned, etc) */
|
||||||
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
|
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
|
||||||
/**
|
/**
|
||||||
@@ -407,7 +530,7 @@ export class WAConnection extends Base {
|
|||||||
/** when WA sends back a pong */
|
/** when WA sends back a pong */
|
||||||
on (event: 'received-pong', listener: () => void): this
|
on (event: 'received-pong', listener: () => void): this
|
||||||
|
|
||||||
on (event: string, listener: (json: any) => void): this
|
on (event: BaileysEvent | string, listener: (json: any) => void): this
|
||||||
|
|
||||||
on (event: BaileysEvent | string, listener: (...args: any[]) => void) { return super.on (event, listener) }
|
on (event: BaileysEvent | string, listener: (...args: any[]) => void) { return super.on (event, listener) }
|
||||||
emit (event: BaileysEvent | string, ...args: any[]) { return super.emit (event, ...args) }
|
emit (event: BaileysEvent | string, ...args: any[]) { return super.emit (event, ...args) }
|
||||||
|
|||||||
@@ -79,9 +79,15 @@ export type WAConnectOptions = {
|
|||||||
maxIdleTimeMs?: number
|
maxIdleTimeMs?: number
|
||||||
/** maximum attempts to connect */
|
/** maximum attempts to connect */
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
/** should the chats be waited for */
|
/**
|
||||||
|
* @deprecated -- use the `chats-received` & `contacts-received` events
|
||||||
|
* should the chats be waited for
|
||||||
|
* */
|
||||||
waitForChats?: boolean
|
waitForChats?: boolean
|
||||||
/** if set to true, the connect only waits for the last message of the chat */
|
/**
|
||||||
|
* @deprecated -- use the `chats-received` & `contacts-received` events
|
||||||
|
* if set to true, the connect only waits for the last message of the chat
|
||||||
|
* */
|
||||||
waitOnlyForLastMessage?: boolean
|
waitOnlyForLastMessage?: boolean
|
||||||
/** max time for the phone to respond to a connectivity test */
|
/** max time for the phone to respond to a connectivity test */
|
||||||
phoneResponseTime?: number
|
phoneResponseTime?: number
|
||||||
@@ -376,9 +382,7 @@ export interface WAOpenResult {
|
|||||||
/** Was this connection opened via a QR scan */
|
/** Was this connection opened via a QR scan */
|
||||||
newConnection: boolean
|
newConnection: boolean
|
||||||
user: WAUser
|
user: WAUser
|
||||||
updatedChats?: {
|
hasNewChats?: boolean
|
||||||
[k: string]: Partial<WAChat>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupSettingChange {
|
export enum GroupSettingChange {
|
||||||
@@ -423,6 +427,8 @@ export type BaileysEvent =
|
|||||||
'connection-phone-change' |
|
'connection-phone-change' |
|
||||||
'user-presence-update' |
|
'user-presence-update' |
|
||||||
'user-status-update' |
|
'user-status-update' |
|
||||||
|
'contacts-received' |
|
||||||
|
'chats-received' |
|
||||||
'chat-new' |
|
'chat-new' |
|
||||||
'chat-update' |
|
'chat-update' |
|
||||||
'message-status-update' |
|
'message-status-update' |
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const isGroupID = (jid: string) => jid?.endsWith ('@g.us')
|
|||||||
|
|
||||||
export const newMessagesDB = (messages: WAMessage[] = []) => {
|
export const newMessagesDB = (messages: WAMessage[] = []) => {
|
||||||
const db = new KeyedDB(waMessageKey, WA_MESSAGE_ID)
|
const db = new KeyedDB(waMessageKey, WA_MESSAGE_ID)
|
||||||
messages.forEach(m => db.insert(m))
|
messages.forEach(m => !db.get(WA_MESSAGE_ID(m)) && db.insert(m))
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user