diff --git a/Example/example.ts b/Example/example.ts index 6356464..6f586d0 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -18,6 +18,7 @@ async function example() { conn.logger.level = 'debug' // set to 'debug' to see what kind of stuff you can implement // attempt to reconnect at most 10 times in a row conn.connectOptions.maxRetries = 10 + conn.connectOptions.waitForChats = false conn.chatOrderingKey = waChatKey(true) // order chats such that pinned chats are on top conn.on ('credentials-updated', () => { @@ -26,6 +27,12 @@ async function example() { 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 }) + 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 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') await conn.connect() - console.log('oh hello ' + conn.user.name + ' (' + conn.user.jid + ')') - console.log('you have ' + conn.chats.length + ' chats & ' + Object.keys(conn.contacts).length + ' contacts') - + console.log('oh hello ' + conn.user.name + ' (' + conn.user.jid + ')') // uncomment to load all unread messages //const unread = await conn.loadAllUnreadMessages () //console.log ('you have ' + unread.length + ' unread messages') diff --git a/README.md b/README.md index b92abb6..57d27cb 100644 --- a/README.md +++ b/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` 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: ``` ts @@ -43,14 +43,29 @@ import { WAConnection } from '@adiwajshing/baileys' async function connectToWhatsApp () { const conn = new WAConnection() - - await conn.connect () - 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") + // 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") + 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 () + 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 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 you don't want to wait for WhatsApp to send all your chats while connecting, you can set the following property to false: -``` 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: +Do note, the `conn.chats` object is 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 pagination of chats (Use `chats.paginated()`) - 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 conn.connectOptions = { /** 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 */ - maxIdleTimeMs?: 15_000 + maxIdleTimeMs?: 15_000, /** maximum attempts to connect */ - 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 + maxRetries?: 5, /** max time for the phone to respond to a connectivity test */ - phoneResponseTime?: 10_000 + phoneResponseTime?: 10_000, /** minimum time between new connections */ - connectCooldownMs?: 3000 + connectCooldownMs?: 3000, /** agent used for WS connections (could be a proxy agent) */ - agent?: Agent = undefined + agent?: Agent = undefined, /** agent used for fetch requests -- uploading/downloading media */ - fetchAgent?: Agent = undefined + fetchAgent?: Agent = undefined, /** always uses takeover for connecting */ alwaysUseTakeover: true } 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 /** when a new chat is added */ 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 & { jid: string })[]) => void): this /** 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 & { jid: string }) => void): this /** when a message's status is updated (deleted, delivered, read, sent etc.) */ on (event: 'message-status-update', listener: (message: WAMessageStatusUpdate) => void): this /** when participants are added to a group */ diff --git a/src/Tests/Common.ts b/src/Tests/Common.ts index 7cbba23..dcba19a 100644 --- a/src/Tests/Common.ts +++ b/src/Tests/Common.ts @@ -7,7 +7,7 @@ export const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST export const makeConnection = () => { const conn = new WAConnection() - conn.connectOptions.maxIdleTimeMs = 45_000 + conn.connectOptions.maxIdleTimeMs = 15_000 conn.logger.level = 'debug' let evCounts = {} diff --git a/src/Tests/Tests.Connect.ts b/src/Tests/Tests.Connect.ts index 7d2c8a1..ce10bfc 100644 --- a/src/Tests/Tests.Connect.ts +++ b/src/Tests/Tests.Connect.ts @@ -1,6 +1,6 @@ import * as assert from 'assert' 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 { assertChatDBIntegrity, makeConnection, testJid } from './Common' @@ -291,14 +291,11 @@ describe ('Pending Requests', () => { it ('should correctly send updates', async () => { const conn = makeConnection () conn.pendingRequestTimeoutMs = null - conn.loadAuthInfo('./auth_info.json') + + const task = new Promise(resolve => conn.once('chats-received', resolve)) await conn.connect () - - conn.close () - - const result0 = await conn.connect () - assert.deepEqual (result0.updatedChats, {}) + await task conn.close () @@ -306,19 +303,18 @@ describe ('Pending Requests', () => { oldChat.archive = 'true' // mark the first chat as archived 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 - setTimeout (() => conn['conn'].close(), 5000) + const promise = new Promise(resolve => conn.once('chats-update', resolve)) const result = await conn.connect () assert.ok (!result.newConnection) - const chat = result.updatedChats[oldChat.jid] + const chats = await promise as Partial[] + const chat = chats.find (c => c.jid === oldChat.jid) assert.ok (chat) assert.ok ('archive' in chat) - assert.equal (Object.keys(chat).length, 2) - - assert.equal (Object.keys(result.updatedChats).length, 1) + assert.strictEqual (Object.keys(chat).length, 3) + assert.strictEqual (Object.keys(chats).length, 1) conn.close () }) diff --git a/src/WAConnection/0.Base.ts b/src/WAConnection/0.Base.ts index 46905e4..5674d7f 100644 --- a/src/WAConnection/0.Base.ts +++ b/src/WAConnection/0.Base.ts @@ -32,7 +32,7 @@ const logger = pino({ prettyPrint: { levelFirst: true, ignore: 'hostname', trans export class WAConnection extends EventEmitter { /** 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 */ browserDescription: [string, string, string] = Utils.Browsers.baileys ('Chrome') /** Metadata like WhatsApp id, name set on WhatsApp etc. */ @@ -43,9 +43,9 @@ export class WAConnection extends EventEmitter { state: WAConnectionState = 'close' connectOptions: WAConnectOptions = { regenerateQRIntervalMs: 30_000, - maxIdleTimeMs: 40_000, + maxIdleTimeMs: 15_000, waitOnlyForLastMessage: false, - waitForChats: true, + waitForChats: false, maxRetries: 10, connectCooldownMs: 4000, phoneResponseTime: 15_000, @@ -67,6 +67,7 @@ export class WAConnection extends EventEmitter { maxCachedMessages = 50 loadProfilePicturesForChatsAutomatically = true + lastChatsReceived: Date chats = new KeyedDB (Utils.waChatKey(false), value => value.jid) contacts: { [k: string]: WAContact } = {} diff --git a/src/WAConnection/1.Validation.ts b/src/WAConnection/1.Validation.ts index 7bbe133..ab218fb 100644 --- a/src/WAConnection/1.Validation.ts +++ b/src/WAConnection/1.Validation.ts @@ -76,7 +76,13 @@ export class WAConnection extends Base { this.emit ('connection-validated', this.user) 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 || '' } diff --git a/src/WAConnection/3.Connect.ts b/src/WAConnection/3.Connect.ts index 2cc4ae6..8e51806 100644 --- a/src/WAConnection/3.Connect.ts +++ b/src/WAConnection/3.Connect.ts @@ -1,9 +1,8 @@ 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 Decoder from '../Binary/Decoder' import WS from 'ws' -import KeyedDB from '@adiwajshing/keyed-db' const DEF_CALLBACK_PREFIX = 'CB:' const DEF_TAG_PREFIX = 'TAG:' @@ -22,7 +21,7 @@ export class WAConnection extends Base { let tries = 0 let lastConnect = this.lastDisconnectTime - var updates + let updates: any while (this.state === 'connecting') { tries += 1 try { @@ -48,9 +47,7 @@ export class WAConnection extends Base { if (!willReconnect) throw error } } - - const updatedChats = !!this.lastDisconnectTime && updates - const result: WAOpenResult = { user: this.user, newConnection, updatedChats } + const result: WAOpenResult = { user: this.user, newConnection, ...(updates || {}) } this.emit ('open', result) this.logger.info ('opened connection to WhatsApp Web') @@ -73,7 +70,7 @@ export class WAConnection extends Base { !this.connectOptions.alwaysUseTakeover 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, timeout: this.connectOptions.maxIdleTimeMs, agent: options.agent, @@ -90,12 +87,12 @@ export class WAConnection extends Base { this.conn.on ('open', async () => { this.logger.info(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`) - let waitForChats: Promise<{[k: string]: Partial}> + let waitForChats: Promise // add wait for chats promise if required 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 - rejections.push (cancelChats) + rejections.push (...cancellations) } try { const [, result] = await Promise.all ( @@ -116,7 +113,7 @@ export class WAConnection extends Base { }) this.conn.on('error', rejectAll) this.conn.on('close', () => rejectAll(new Error(DisconnectReason.close))) - }) as Promise }> + }) as Promise ) this.on ('ws-close', rejectAll) @@ -140,135 +137,30 @@ export class WAConnection extends Base { * Must be called immediately after connect */ protected receiveChatsAndContacts(waitOnlyForLast: boolean) { - const chats = new KeyedDB(this.chatOrderingKey, c => c.jid) - const contacts = {} - - let receivedChats = false - let receivedContacts = false - let receivedMessages = false - - 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) - } + const rejectableWaitForEvent = (event: string) => { + let rejectTask = (_: Error) => {} + const task = new Promise((resolve, reject) => { + this.once (event, data => { + this.startDebouncedTimeout() // start timeout again + resolve(data) }) - } - 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) + rejectTask = reject }) - - this.logger.info (`received ${json[2].length} chats`) - receivedChats = true - - if (json[2].length === 0) receivedMessages = true - checkForResolution () + return { reject: rejectTask, task } } - const contactsUpdate = json => { - if (json[1].duplicate || !json[2]) return - this.startDebouncedTimeout () // restart debounced timeout + const events = [ 'chats-received', 'contacts-received', 'CB:action,add:last' ] + if (!waitOnlyForLast) events.push('CB:action,add:before', 'CB:action,add:unread') - receivedContacts = true - - json[2].forEach(([type, contact]: ['user', WAContact]) => { - if (!contact) return this.logger.info (`unexpectedly got null contact: ${type}`, contact) - - contact.jid = Utils.whatsappID (contact.jid) - contacts[contact.jid] = contact + const cancellations = [] + const wait = Promise.all ( + events.map (ev => { + const {reject, task} = rejectableWaitForEvent(ev) + cancellations.push(reject) + return task }) - this.logger.info (`received ${json[2].length} contacts`) - 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) => { - resolveTask = resolve - rejectTask = reject - }) - - const oldChats = this.chats - const updatedChats: { [k: string]: Partial } = {} - - 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()) } + ).then(([update]) => update as { hasNewChats: boolean }) + + return { wait, cancellations } } private onMessageRecieved(message: string | Buffer) { if (message[0] === '!') { diff --git a/src/WAConnection/4.Events.ts b/src/WAConnection/4.Events.ts index 425f967..f4f5675 100644 --- a/src/WAConnection/4.Events.ts +++ b/src/WAConnection/4.Events.ts @@ -1,7 +1,7 @@ import * as QR from 'qrcode-terminal' 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 { whatsappID, unixTimestampSeconds, isGroupID, GET_MESSAGE_ID, WA_MESSAGE_ID, waMessageKey, newMessagesDB } from './Utils' +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, shallowChanges } from './Utils' import KeyedDB from '@adiwajshing/keyed-db' import { Mutex } from './Mutex' @@ -9,6 +9,123 @@ export class WAConnection extends Base { constructor () { 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 } = {} + 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 this.on('CB:action,add:relay,message', json => { 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 /** when a new chat is added */ 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 & { jid: string })[]) => void): this /** when a chat is updated (new message, updated message, deleted, pinned, etc) */ on (event: 'chat-update', listener: (chat: Partial & { jid: string }) => void): this /** @@ -407,7 +530,7 @@ export class WAConnection extends Base { /** when WA sends back a pong */ 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) } emit (event: BaileysEvent | string, ...args: any[]) { return super.emit (event, ...args) } diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 3e02973..add30c4 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -79,9 +79,15 @@ export type WAConnectOptions = { maxIdleTimeMs?: number /** maximum attempts to connect */ maxRetries?: number - /** should the chats be waited for */ + /** + * @deprecated -- use the `chats-received` & `contacts-received` events + * should the chats be waited for + * */ 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 /** max time for the phone to respond to a connectivity test */ phoneResponseTime?: number @@ -376,9 +382,7 @@ export interface WAOpenResult { /** Was this connection opened via a QR scan */ newConnection: boolean user: WAUser - updatedChats?: { - [k: string]: Partial - } + hasNewChats?: boolean } export enum GroupSettingChange { @@ -423,6 +427,8 @@ export type BaileysEvent = 'connection-phone-change' | 'user-presence-update' | 'user-status-update' | + 'contacts-received' | + 'chats-received' | 'chat-new' | 'chat-update' | 'message-status-update' | diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index b4db866..733d4e4 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -41,7 +41,7 @@ export const isGroupID = (jid: string) => jid?.endsWith ('@g.us') export const newMessagesDB = (messages: WAMessage[] = []) => { 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 }