diff --git a/Example/example.ts b/Example/example.ts index d75af81..6b105a8 100644 --- a/Example/example.ts +++ b/Example/example.ts @@ -17,11 +17,12 @@ async function example() { client.logLevel = MessageLogLevel.info // set to unhandled to see what kind of stuff you can implement // connect or timeout in 20 seconds (loads the auth file credentials if present) - const [user, chats, contacts, unread] = await client.connect('./auth_info.json', 20 * 1000) + const [user, chats, contacts] = await client.connect('./auth_info.json', 20 * 1000) + const unread = chats.all().flatMap (chat => chat.messages.slice(chat.messages.length-chat.count)) console.log('oh hello ' + user.name + ' (' + user.id + ')') - console.log('you have ' + unread.length + ' unread messages') - console.log('you have ' + chats.length + ' chats & ' + contacts.length + ' contacts') + console.log('you have ' + chats.all().length + ' chats & ' + contacts.length + ' contacts') + console.log ('you have ' + unread.length + ' unread messages') const authInfo = client.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 diff --git a/README.md b/README.md index cae41b6..0307da3 100644 --- a/README.md +++ b/README.md @@ -34,35 +34,53 @@ Set the phone number you can randomly send messages to in a `.env` file with `TE ## Connecting ``` ts -const client = new WAClient() +import { WAClient } from '@adiwajshing/baileys' -client.connect() -.then (([user, chats, contacts, unread]) => { +async function connectToWhatsApp () { + const client = new WAClient() + const [user, chats, contacts] = await client.connect () console.log ("oh hello " + user.name + " (" + user.id + ")") - console.log ("you have " + unread.length + " unread messages") console.log ("you have " + chats.length + " chats") -}) -.catch (err => console.log("unexpected error: " + err) ) + + // every chat object has a list of most recent messages + // can use that to retreive all your pending unread messages + // the 'count' property a chat object reflects the number of unread messages + // the 'count' property is -1 if the entire thread has been marked unread + const unread = chats.all().flatMap (chat => chat.messages.slice(chat.messages.length-chat.count)) + + console.log ("you have " + unread.length + " unread messages") +} + +// run in main file +connectToWhatsApp () +.catch (err => console.log("unexpected error: " + err) ) // catch any errors ``` + 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 use the following function: - ``` ts -const client = new WAClient() -client.connectSlim() // does not wait for chats & contacts -.then (user => { +import { WAClient } from '@adiwajshing/baileys' + +async function connectToWhatsApp () { + const client = new WAClient() + const user = await client.connectSlim () console.log ("oh hello " + user.name + " (" + user.id + ")") - + client.receiveChatsAndContacts () // wait for chats & contacts in the background - .then (([chats, contacts, unread]) => { - console.log ("you have " + unread.length + " unread messages") - console.log ("you have " + chats.length + " chats") + .then (([chats, contacts]) => { + console.log ("you have " + chats.all().length + " chats and " + contacts.length + " contacts") }) -}) -.catch (err => console.log("unexpected error: " + err)) +} +// run in main file +connectToWhatsApp () +.catch (err => console.log("unexpected error: " + err) ) // catch any errors ``` +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 pagination of chats (Use `chats.paginated()`) +- Most applications require **O(1)** access to chats via the chat ID. (Use `chats.get(jid)` with `KeyedDB`) + ## Saving & Restoring Sessions You obviously don't want to keep scanning the QR code every time you want to connect. diff --git a/package.json b/package.json index fcbd8b4..0051bac 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "url": "git@github.com:adiwajshing/baileys.git" }, "dependencies": { + "@adiwajshing/keyed-db": "github:adiwajshing/keyed-db", "curve25519-js": "0.0.4", "futoin-hkdf": "^1.3.2", "jimp": "^0.14.0", diff --git a/src/WAClient/Constants.ts b/src/WAClient/Constants.ts index 5e509cb..3383645 100644 --- a/src/WAClient/Constants.ts +++ b/src/WAClient/Constants.ts @@ -55,7 +55,7 @@ export enum Mimetype { png = 'image/png', mp4 = 'video/mp4', gif = 'video/gif', - pdf = 'appliction/pdf', + pdf = 'application/pdf', ogg = 'audio/ogg; codecs=opus', /** for stickers */ webp = 'image/webp', @@ -66,7 +66,7 @@ export interface MessageOptions { timestamp?: Date caption?: string thumbnail?: string - mimetype?: Mimetype + mimetype?: Mimetype | string validateID?: boolean, filename?: string } diff --git a/src/WAConnection/Connect.ts b/src/WAConnection/Connect.ts index 3d1324e..4d60273 100644 --- a/src/WAConnection/Connect.ts +++ b/src/WAConnection/Connect.ts @@ -1,6 +1,7 @@ import WS from 'ws' +import KeyedDB from '@adiwajshing/keyed-db' import * as Utils from './Utils' -import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel } from './Constants' +import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel, WANode } from './Constants' import WAConnectionValidator from './Validation' import Decoder from '../Binary/Decoder' @@ -9,13 +10,13 @@ export default class WAConnectionConnector extends WAConnectionValidator { * Connect to WhatsAppWeb * @param [authInfo] credentials or path to credentials to log back in * @param [timeoutMs] timeout after which the connect will fail, set to null for an infinite timeout - * @return returns [userMetaData, chats, contacts, unreadMessages] + * @return returns [userMetaData, chats, contacts] */ async connect(authInfo: AuthenticationCredentialsBase64 | string = null, timeoutMs: number = null) { try { const userInfo = await this.connectSlim(authInfo, timeoutMs) const chats = await this.receiveChatsAndContacts(timeoutMs) - return [userInfo, ...chats] as [UserMetaData, WAChat[], WAContact[], WAMessage[]] + return [userInfo, ...chats] as [UserMetaData, KeyedDB, WAContact[]] } catch (error) { this.close () throw error @@ -66,13 +67,11 @@ export default class WAConnectionConnector extends WAConnectionValidator { /** * Sets up callbacks to receive chats, contacts & unread messages. * Must be called immediately after connect - * @returns [chats, contacts, unreadMessages] + * @returns [chats, contacts] */ async receiveChatsAndContacts(timeoutMs: number = null) { - let chats: Array = [] - let contacts: Array = [] - let unreadMessages: Array = [] - let chatMap: Record = {} + let contacts: WAContact[] = [] + const chats: KeyedDB = new KeyedDB (Utils.waChatUniqueKey, value => value.jid) let receivedContacts = false let receivedMessages = false @@ -91,25 +90,17 @@ export default class WAConnectionConnector extends WAConnectionValidator { const chatUpdate = json => { receivedMessages = true const isLast = json[1].last - json = json[2] - if (json) { - for (let k = json.length - 1; k >= 0; k--) { - const message = json[k][2] + const messages = json[2] as WANode[] + + if (messages) { + messages.reverse().forEach (([, __, message]: ['message', null, WAMessage]) => { const jid = message.key.remoteJid.replace('@s.whatsapp.net', '@c.us') - const index = chatMap[jid] - if (!message.key.fromMe && index && index.count > 0) { - // only forward if the message is from the sender - unreadMessages.push(message) - index.count -= 1 // reduce - } - if (index) { - chats[index.index].messages.push (message) - } - } - } - if (isLast && receivedContacts) { // if received contacts before messages - convoResolve () + const chat = chats.get(jid) + chat?.messages.unshift (message) + }) } + // if received contacts before messages + if (isLast && receivedContacts) convoResolve () } // wait for actual messages to load, "last" is the most recent message, "before" contains prior messages this.registerCallback(['action', 'add:last'], chatUpdate) @@ -120,14 +111,13 @@ export default class WAConnectionConnector extends WAConnectionValidator { let json = await this.registerCallbackOneTime(['response', 'type:chat']) if (json[1].duplicate) json = await this.registerCallbackOneTime (['response', 'type:chat']) - json[2].forEach(chat => { - chat[1].count = parseInt(chat[1].count) - chat[1].messages = [] - chats.push(chat[1]) // chats data (log json to see what it looks like) - // store the number of unread messages for each sender - chatMap[chat[1].jid] = {index: chats.length-1, count: chat[1].count} //chat[1].count + json[2].forEach(([_, chat]: [any, WAChat]) => { + chat.count = +chat.count + chat.messages = [] + chats.insert (chat) // chats data (log json to see what it looks like) }) - if (chats.length > 0) return waitForConvos() + + if (chats.all().length > 0) return waitForConvos() } const waitForContacts = async () => { let json = await this.registerCallbackOneTime(['response', 'type:contacts']) @@ -142,7 +132,8 @@ export default class WAConnectionConnector extends WAConnectionValidator { // wait for the chats & contacts to load const promise = Promise.all([waitForChats(), waitForContacts()]) await Utils.promiseTimeout (timeoutMs, promise) - return [chats, contacts, unreadMessages] as [WAChat[], WAContact[], WAMessage[]] + + return [chats, contacts] as [KeyedDB, WAContact[]] } private onMessageRecieved(message) { if (message[0] === '!') { diff --git a/src/WAConnection/Tests.ts b/src/WAConnection/Tests.ts index 0c1f05e..9c0f9e8 100644 --- a/src/WAConnection/Tests.ts +++ b/src/WAConnection/Tests.ts @@ -50,25 +50,25 @@ describe('Test Connect', () => { }) it('should reconnect', async () => { const conn = new WAConnection() - const [user, chats, contacts, unread] = await conn.connect(auth, 20*1000) + const [user, chats, contacts] = await conn.connect(auth, 20*1000) assert.ok(user) assert.ok(user.id) assert.ok(chats) - if (chats.length > 0) { - assert.ok(chats[0].jid) - assert.ok(chats[0].count !== null) + + const chatArray = chats.all() + if (chatArray.length > 0) { + assert.ok(chatArray[0].jid) + assert.ok(chatArray[0].count !== null) + if (chatArray[0].messages.length > 0) { + assert.ok(chatArray[0].messages[0]) + } } assert.ok(contacts) if (contacts.length > 0) { assert.ok(contacts[0].jid) } - assert.ok(unread) - if (unread.length > 0) { - assert.ok(unread[0].key) - } - await conn.logout() await assert.rejects(async () => conn.connectSlim(auth), 'reconnect should have failed') }) diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index 54aa379..6887dec 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -2,7 +2,7 @@ import * as Crypto from 'crypto' import HKDF from 'futoin-hkdf' import Decoder from '../Binary/Decoder' import {platform, release} from 'os' -import { BaileysError } from './Constants' +import { BaileysError, WAChat } from './Constants' import UserAgent from 'user-agents' const platformMap = { @@ -18,6 +18,13 @@ export const Browsers = { /** The appropriate browser based on your OS & release */ appropriate: browser => [ platformMap [platform()] || 'Ubuntu', browser, release() ] as [string, string, string] } +function hashCode(s: string) { + for(var i = 0, h = 0; i < s.length; i++) + h = Math.imul(31, h) + s.charCodeAt(i) | 0; + return h; +} +export const waChatUniqueKey = (c: WAChat) => ((+c.t*1000) + (hashCode(c.jid)%1000))*-1 // -1 to sort descending + export function userAgentString (browser) { const agent = new UserAgent (new RegExp(browser)) return agent.toString () diff --git a/tsconfig.json b/tsconfig.json index 7b03fce..f30ced1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,8 @@ "noImplicitThis": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "declaration": true + "declaration": true, + "lib": ["es2019", "esnext.array"] }, "include": ["src/*/*.ts"], "exclude": ["node_modules", "src/*/Tests.ts"]