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:
Adhiraj Singh
2020-11-13 23:15:16 +05:30
parent eace0c1795
commit 6d02d405a7
10 changed files with 232 additions and 196 deletions

View File

@@ -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')
@@ -34,8 +41,6 @@ async function example() {
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')

View File

@@ -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
// this can take up to a few minutes if you have thousands of chats!
conn.on('chats-received', async ({ hasNewChats }) => {
console.log(`you have ${conn.chats.length} chats, new chats available: ${hasNewChats}`)
const unread = await conn.loadAllUnreadMessages ()
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 () await conn.connect ()
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")") conn.on('chat-update', chatUpdate => {
// every chat object has a list of most recent messages // `chatUpdate` is a partial object, containing the updated properties of the chat
console.log ("you have " + conn.chats.all().length + " chats") // received a new message
if (chatUpdate.messages && chatUpdate.count) {
const unread = await conn.loadAllUnreadMessages () const message = chatUpdate.messages.all()[0]
console.log ("you have " + unread.length + " unread messages") 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 */

View File

@@ -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 = {}

View File

@@ -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 ()
}) })

View File

@@ -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 } = {}

View File

@@ -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 || ''
} }

View File

@@ -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')
@@ -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 ()
}
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) => { return { wait, cancellations }
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] === '!') {

View File

@@ -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) }

View File

@@ -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' |

View File

@@ -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
} }