Initial V3.0

This commit is contained in:
Adhiraj
2020-08-19 14:19:30 +05:30
parent 3825824d71
commit 95d2567e76
20 changed files with 1593 additions and 1027 deletions

View File

@@ -7,39 +7,44 @@ import {
WALocationMessage,
MessageLogLevel,
WAMessageType,
ReconnectMode,
} from '../src/WAConnection/WAConnection'
import * as fs from 'fs'
async function example() {
const conn = new WAConnection() // instantiate
conn.autoReconnect = true // auto reconnect on disconnect
conn.autoReconnect = ReconnectMode.onConnectionLost // only automatically reconnect when the connection breaks
conn.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] = await conn.connect('./auth_info.json', 20 * 1000)
const unread = chats.all().flatMap (chat => chat.messages.slice(chat.messages.length-chat.count))
// loads the auth file credentials if present
if (fs.existsSync('./auth_info.json')) conn.loadAuthInfo ('./auth_info.json')
// connect or timeout in 20 seconds
await conn.connect(20 * 1000)
console.log('oh hello ' + user.name + ' (' + user.id + ')')
console.log('you have ' + chats.all().length + ' chats & ' + contacts.length + ' contacts')
const unread = await conn.loadAllUnreadMessages ()
console.log('oh hello ' + conn.user.name + ' (' + conn.user.id + ')')
console.log('you have ' + conn.chats.all().length + ' chats & ' + Object.keys(conn.contacts).length + ' contacts')
console.log ('you have ' + unread.length + ' unread messages')
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
/* Note: one can take this auth_info.json file and login again from any computer without having to scan the QR code,
and get full access to one's WhatsApp. Despite the convenience, be careful with this file */
conn.setOnPresenceUpdate(json => console.log(json.id + ' presence is ' + json.type))
conn.setOnMessageStatusChange(json => {
const participant = json.participant ? ' (' + json.participant + ')' : '' // participant exists when the message is from a group
console.log(`${json.to}${participant} acknlowledged message(s) ${json.ids} as ${json.type}`)
conn.on ('user-presence-update', json => console.log(json.id + ' presence is ' + json.type))
conn.on ('message-update', message => {
//const participant = json.participant ? ' (' + json.participant + ')' : '' // participant exists when the message is from a group
//console.log(`${json.to}${participant} acknlowledged message(s) ${json.ids} as ${json.type}`)
})
// set to false to NOT relay your own sent messages
conn.setOnUnreadMessage(true, async (m) => {
conn.on('message-new', async (m) => {
const messageStubType = WAMessageType[m.messageStubType] || 'MESSAGE'
console.log('got notification of type: ' + messageStubType)
const messageContent = m.message
// if it is not a regular text or media message
if (!messageContent) return
if (m.key.fromMe) {
console.log('relayed my own message')
return
@@ -112,14 +117,9 @@ async function example() {
const batterylevel = parseInt(batteryLevelStr)
console.log('battery level: ' + batterylevel)
})
conn.setOnUnexpectedDisconnect(reason => {
if (reason === 'replaced') {
// uncomment to reconnect whenever the connection gets taken over from somewhere else
// await conn.connect ()
} else {
console.log ('oh no got disconnected: ' + reason)
}
})
conn.on('closed', ({reason, isReconnecting}) => (
console.log ('oh no got disconnected: ' + reason + ', reconnecting: ' + isReconnecting)
))
}
example().catch((err) => console.log(`encountered error: ${err}`))

206
README.md
View File

@@ -2,7 +2,7 @@
Baileys does not require Selenium or any other browser to be interface with WhatsApp Web, it does so directly using a **WebSocket**. Not running Selenium or Chromimum saves you like **half a gig** of ram :/
Thank you to [@Sigalor](https://github.com/sigalor/whatsapp-web-reveng) for writing the guide to reverse engineering WhatsApp Web and thanks to [@Rhymen](https://github.com/Rhymen/go-whatsapp/) for the __go__ implementation.
Thank you to [@Sigalor](https://github.com/sigalor/whatsapp-web-reveng) for writing his observations on the workings of WhatsApp Web and thanks to [@Rhymen](https://github.com/Rhymen/go-whatsapp/) for the __go__ implementation.
Baileys is type-safe, extensible and simple to use. If you require more functionality than provided, it'll super easy for you to write an extension. More on this [here](#WritingCustomFunctionality).
@@ -21,7 +21,9 @@ To run the example script, download or clone the repo and then type the followin
## Install
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` (major changes incoming right now)
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)
Then import in your code using:
``` ts
@@ -40,16 +42,14 @@ import { WAConnection } from '@adiwajshing/baileys'
async function connectToWhatsApp () {
const conn = new WAConnection()
const [user, chats, contacts] = await conn.connect ()
console.log ("oh hello " + user.name + " (" + user.id + ")")
console.log ("you have " + chats.length + " chats")
// 20 second timeout
await conn.connect (20*1000)
console.log ("oh hello " + conn.user.name + " (" + conn.user.id + ")")
// 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 " + conn.chats.all().length + " chats")
const unread = await conn.loadAllUnreadMessages ()
console.log ("you have " + unread.length + " unread messages")
}
@@ -59,23 +59,10 @@ 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 use the following function:
``` ts
import { WAConnection } from '@adiwajshing/baileys'
async function connectToWhatsApp () {
const conn = new WAConnection()
const user = await conn.connectSlim ()
console.log ("oh hello " + user.name + " (" + user.id + ")")
conn.receiveChatsAndContacts () // wait for chats & contacts in the background
.then (([chats, contacts]) => {
console.log ("you have " + chats.all().length + " chats and " + contacts.length + " contacts")
})
}
// run in main file
connectToWhatsApp ()
.catch (err => console.log("unexpected error: " + err) ) // catch any errors
await conn.connect (20*1000, false)
```
Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwajshing/keyed-db). This is done for the following reasons:
@@ -87,100 +74,94 @@ Do note, the `chats` object returned is now a [KeyedDB](https://github.com/adiwa
You obviously don't want to keep scanning the QR code every time you want to connect.
So, do the following the first time you connect:
So, do the following every time you open a new connection:
``` ts
import * as fs from 'fs'
const conn = new WAConnection()
conn.connectSlim() // connect first
.then (user => {
const creds = conn.base64EncodedAuthInfo () // contains all the keys you need to restore a session
fs.writeFileSync('./auth_info.json', JSON.stringify(creds, null, '\t')) // save JSON to file
})
await conn.connect() // connect first
const creds = conn.base64EncodedAuthInfo () // contains all the keys you need to restore a session
fs.writeFileSync('./auth_info.json', JSON.stringify(creds, null, '\t')) // save JSON to file
```
Then, to restore a session:
``` ts
const conn = new WAConnection()
conn.connectSlim('./auth_info.json') // will load JSON credentials from file
.then (user => {
// yay connected without scanning QR
})
conn.loadAuthInfo ('./auth_info.json') // will load JSON credentials from file
await conn.connect()
// yay connected without scanning QR
/*
Optionally, you can load the credentials yourself from somewhere
& pass in the JSON object to connectSlim () as well.
& pass in the JSON object to loadAuthInfo () as well.
*/
```
If you're considering switching from a Chromium/Puppeteer based library, you can use WhatsApp Web's Browser credentials to restore sessions too:
``` ts
conn.loadAuthInfoFromBrowser ('./auth_info_browser.json')
conn.connectSlim(null, 20*1000) // use loaded credentials & timeout in 20s
.then (user => {
// yay! connected using browser keys & without scanning QR
})
conn.loadAuthInfo ('./auth_info_browser.json') // use loaded credentials & timeout in 20s
await conn.connect() // works the same
```
See the browser credentials type [here](/src/WAConnection/Constants.ts).
See the browser credentials type in the docs.
## QR Overriding
If you want to do some custom processing with the QR code used to authenticate, you can override the following method:
``` ts
conn.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
const str = ref + ',' + publicKey + ',' + clientID // the QR string
// Now, use 'str' to display in QR UI or send somewhere
conn.on('qr', qr => {
// Now, use the 'qr' string to display in QR UI or send somewhere
}
const user = await conn.connect ()
await conn.connect ()
```
If you need to regenerate the QR, you can also do so using:
The QR will auto-regenerate and will fire a new `qr` event after 30 seconds, if you don't want to regenerate or want to change the re-gen interval:
``` ts
let generateQR: async () => void // call generateQR on some timeout or error
conn.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
generateQR = async () => {
ref = await conn.generateNewQRCode () // returns a new ref code to use for QR generation
const str = ref + ',' + publicKey + ',' + clientID // the QR string
// re-print str as QR or update in UI or send somewhere
//QR.generate(str, { small: true })
}
}
const user = await conn.connect ()
conn.regenerateQRIntervalMs = null // no QR regen
conn.regenerateQRIntervalMs = 20000 // QR regen every 20 seconds
```
## Handling Events
Implement the following callbacks in your code:
Baileys now uses the EventEmitter syntax for events.
They're all nicely typed up, so you shouldn't have any issues with an Intellisense editor like VS Code.
- Called when you have a pending unread message or recieve a new message
``` ts
import { getNotificationType } from '@adiwajshing/baileys'
// set first param to `true` if you want to receive outgoing messages that may be sent from your phone
conn.setOnUnreadMessage (false, (m: WAMessage) => {
// get what type of notification it is -- message, group add notification etc.
const [notificationType, messageType] = getNotificationType(m)
``` ts
/** when the connection has opened successfully */
on (event: 'open', listener: () => void): this
/** when the connection is opening */
on (event: 'connecting', listener: () => void): this
/** when the connection has closed */
on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
/** when a new QR is generated, ready for scanning */
on (event: 'qr', listener: (qr: string) => void): this
/** when the connection to the phone changes */
on (event: 'connection-phone-change', listener: (state: {connected: boolean}) => void): this
/** when a user's presence is updated */
on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void): this
/** when a user's status is updated */
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 a chat is updated (archived, deleted, pinned, read, unread, name changed) */
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
/** when a new message is relayed */
on (event: 'message-new', listener: (message: WAMessage) => void): this
/** when a message is updated (deleted, delivered, read) */
on (event: 'message-update', listener: (message: WAMessage) => void): this
/** when participants are added to a group */
on (event: 'group-participants-add', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are removed or leave from a group */
on (event: 'group-participants-remove', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are promoted in a group */
on (event: 'group-participants-promote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are demoted in a group */
on (event: 'group-participants-demote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when the group settings is updated */
on (event: 'group-settings-update', listener: (update: {jid: string, restrict?: string, announce?: string, actor?: string}) => void): this
/** when the group description is updated */
on (event: 'group-description-update', listener: (update: {jid: string, description?: string, actor?: string}) => void): this
```
console.log("got notification of type: " + notificationType) // message, groupAdd, groupLeave
console.log("message type: " + messageType) // conversation, imageMessage, videoMessage, contactMessage etc.
})
```
- Called when you recieve an update on someone's presence, they went offline or online
``` ts
conn.setOnPresenceUpdate ((json: PresenceUpdate) => console.log(json.id + " presence is " + json.type))
```
- Called when your message gets delivered or read
``` ts
conn.setOnMessageStatusChange ((json: MessageStatusUpdate) => {
let sent = json.to
if (json.participant) // participant exists when the message is from a group
sent += " ("+json.participant+")" // mention as the one sent to
// log that they acknowledged the message
console.log(sent + " acknowledged message(s) " + json.ids + " as " + json.type + " at " + json.timestamp)
})
```
- Called when the connection gets disconnected (either the server loses internet, the phone gets unpaired, or the connection is taken over from somewhere)
``` ts
conn.setOnUnexpectedDisconnect (reason => console.log ("disconnected unexpectedly: " + reason) )
```
## Sending Messages
Send like, all types of messages with a single function:
@@ -191,7 +172,7 @@ const id = 'abcd@s.whatsapp.net' // the WhatsApp ID
// send a simple text!
conn.sendMessage (id, 'oh hello there', MessageType.text)
// send a location!
conn.sendMessage(id, {degreeslatitude: 24.121231, degreesLongitude: 55.1121221}, MessageType.location)
conn.sendMessage(id, {degreesLatitude: 24.121231, degreesLongitude: 55.1121221}, MessageType.location)
// send a contact!
const vcard = 'BEGIN:VCARD\n' // metadata of the contact card
+ 'VERSION:3.0\n'
@@ -205,11 +186,10 @@ const buffer = fs.readFileSync("Media/ma_gif.mp4") // load some gif
const options: MessageOptions = {mimetype: Mimetype.gif, caption: "hello!"} // some metadata & caption
conn.sendMessage(id, buffer, MessageType.video, options)
```
To note:
- `id` is the WhatsApp ID of the person or group you're sending the message to.
- It must be in the format ```[country code][phone number]@s.whatsapp.net```, for example ```+19999999999@s.whatsapp.net``` for people. For groups, it must be in the format ``` 123456789-123345@g.us ```.
- **Do not attach** `@c.us` for individual people IDs, It won't work.
- Please do not explicitly disable ID validation (in `MessageOptions`) because then your messages may fail for no apparent reason.
- For media messages, the thumbnail can be generated automatically for images & stickers. Thumbnails for videos can also be generated automatically, though, you need to have `ffmpeg` installed on your system.
- **MessageOptions**: some extra info about the message. It can have the following __optional__ values:
``` ts
@@ -218,7 +198,6 @@ To note:
contextInfo: { forwardingScore: 2, isForwarded: true }, // some random context info
// (can show a forwarded message with this too)
timestamp: Date(), // optional, if you want to manually set the timestamp of the message
validateID: true, // if you want to validate the ID before sending the message, true by default
caption: "hello there!", // (for media messages) the caption to send with the media (cannot be sent with stickers though)
thumbnail: "23GD#4/==", /* (for location & media messages) has to be a base 64 encoded JPEG if you want to send a custom thumb,
or set to null if you don't want to send a thumbnail.
@@ -239,45 +218,44 @@ await conn.forwardMessage ('455@s.whatsapp.net', message) // WA forward the mess
```
## Reading Messages
``` ts
const id = '1234-123@g.us'
const messageID = 'AHASHH123123AHGA' // id of the message you want to read
await conn.sendReadReceipt(id, messageID) // mark as read
await conn.sendReadReceipt (id) // mark all messages in chat as read
await conn.sendReadReceipt(id, messageID, 1) // mark the mentioned message as read
await conn.sendReadReceipt(id, null, 'unread') // mark the chat as unread
await conn.sendReadReceipt(id, null, -2) // mark the chat as unread
```
- `id` is in the same format as mentioned earlier.
- The message ID is the unique identifier of the message that you are marking as read.
- On a `WAMessage`, the `messageID` can be accessed using ```messageID = message.key.id```.
The message ID is the unique identifier of the message that you are marking as read. On a `WAMessage`, the `messageID` can be accessed using ```messageID = message.key.id```.
## Update Presence
``` ts
import { Presence } from '@adiwajshing/baileys'
await conn.updatePresence(id, Presence.available)
conn.updatePresence(id, Presence.available)
```
This lets the person/group with ``` id ``` know whether you're online, offline, typing etc. where ``` presence ``` can be one of the following:
``` ts
export enum Presence {
available = 'available', // "online"
unavailable = 'unavailable', // "offline"
composing = 'composing', // "typing..."
recording = 'recording', // "recording..."
paused = 'paused', // I have no clue
}
```
## Downloading Media
The presence expires after about 10 seconds.
## Downloading Media Messages
If you want to save the media you received
``` ts
import { MessageType, extensionForMediaMessage } from '@adiwajshing/baileys'
conn.setOnUnreadMessage (false, async m => {
import { MessageType } from '@adiwajshing/baileys'
conn.on ('message-new', async m => {
if (!m.message) return // if there is no text or media message
const messageType = Object.keys (m.message)[0]// get what type of message it is -- text, image, video
// if the message is not a text message
if (messageType !== MessageType.text && messageType !== MessageType.extendedText) {
@@ -322,6 +300,13 @@ await conn.deleteChat (jid) // will delete the chat (can be a group or broadcast
## Misc
- To load chats in a paginated manner
``` ts
const {chats, cursor} = await conn.loadChats (25)
if (cursor) {
const moreChats = await conn.loadChats (25, cursor) // load the next 25 chats
}
```
- To check if a given ID is on WhatsApp
``` ts
const id = 'xyz@s.whatsapp.net'
@@ -331,12 +316,12 @@ await conn.deleteChat (jid) // will delete the chat (can be a group or broadcast
- To query chat history on a group or with someone
``` ts
// query the last 25 messages (replace 25 with the number of messages you want to query)
const messages = await conn.loadConversation ("xyz-abc@g.us", 25)
const messages = await conn.loadMessages ("xyz-abc@g.us", 25)
console.log("got back " + messages.length + " messages")
```
You can also load the entire conversation history if you want
``` ts
await conn.loadEntireConversation ("xyz@c.us", message => console.log("Loaded message with ID: " + message.key.id))
await conn.loadAllMessages ("xyz@c.us", message => console.log("Loaded message with ID: " + message.key.id))
console.log("queried all messages") // promise resolves once all messages are retreived
```
- To get the status of some person
@@ -353,12 +338,12 @@ await conn.deleteChat (jid) // will delete the chat (can be a group or broadcast
``` ts
const jid = '111234567890-1594482450@g.us' // can be your own too
const img = fs.readFileSync ('new-profile-picture.jpeg') // can be PNG also
await conn.updateProfilePicture (jid, newPP)
await conn.updateProfilePicture (jid, img)
```
- To get someone's presence (if they're typing, online)
``` ts
// the presence update is fetched and called here
conn.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
conn.on ('user-presence-update', json => console.log(json.id + " presence is " + json.type))
await conn.requestPresenceUpdate ("xyz@c.us") // request the update
```
- To search through messages
@@ -369,7 +354,6 @@ await conn.deleteChat (jid) // will delete the chat (can be a group or broadcast
const response2 = await conn.searchMessages ('so cool', '1234@c.us', 25, 1) // search in given chat
```
Of course, replace ``` xyz ``` with an actual ID.
Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
## Groups
- To create a group
@@ -472,7 +456,7 @@ This will enable you to see all sorts of messages WhatsApp sends in the console.
``` ts
conn.registerCallback (["Conn", "pushname"], json => {
const pushname = json[1].pushname
conn.userMetaData.name = pushname // update on client too
conn.user.name = pushname // update on client too
console.log ("Name updated: " + pushname)
})
```
@@ -483,4 +467,4 @@ A little more testing will reveal that almost all WhatsApp messages are in the f
Note: except for the first parameter (in the above cases, ```"action"``` or ```"Conn"```), all the other parameters are optional.
### Note
This library is in no way affiliated with WhatsApp. Use at your own discretion. Do not spam people with this.
This library was originally a project for **CS-2362 at Ashoka University** and is in no way affiliated with WhatsApp. Use at your own discretion. Do not spam people with this.

View File

@@ -31,24 +31,24 @@
"url": "git@github.com:adiwajshing/baileys.git"
},
"dependencies": {
"@adiwajshing/keyed-db": "^0.1.1",
"@adiwajshing/keyed-db": "^0.1.2",
"curve25519-js": "0.0.4",
"futoin-hkdf": "^1.3.2",
"jimp": "^0.14.0",
"node-fetch": "^2.6.0",
"protobufjs": "^6.9.0",
"protobufjs": "^6.10.1",
"qrcode-terminal": "^0.12.0",
"ws": "^7.3.0"
"ws": "^7.3.1"
},
"devDependencies": {
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.14",
"@types/node": "^14.0.27",
"@types/ws": "^7.2.6",
"assert": "^2.0.0",
"dotenv": "^8.2.0",
"mocha": "^8.0.1",
"ts-node-dev": "^1.0.0-pre.49",
"mocha": "^8.1.1",
"ts-node-dev": "^1.0.0-pre.57",
"typedoc": "^0.18.0",
"typescript": "^3.9.5"
"typescript": "^3.9.7"
}
}

View File

@@ -1,4 +1,4 @@
import { WAConnection, MessageLogLevel, MessageOptions, MessageType } from '../WAConnection/WAConnection'
import { WAConnection, MessageLogLevel, MessageOptions, MessageType, unixTimestampSeconds } from '../WAConnection/WAConnection'
import * as assert from 'assert'
import {promises as fs} from 'fs'
@@ -7,22 +7,29 @@ export const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST
export async function sendAndRetreiveMessage(conn: WAConnection, content, type: MessageType, options: MessageOptions = {}) {
const response = await conn.sendMessage(testJid, content, type, options)
const messages = await conn.loadConversation(testJid, 10, null, true)
const {messages} = await conn.loadMessages(testJid, 10)
const message = messages.find (m => m.key.id === response.key.id)
assert.ok(message)
const chat = conn.chats.get(testJid)
assert.ok (chat.messages.find(m => m.key.id === response.key.id))
assert.ok (chat.t >= (unixTimestampSeconds()-5) )
return message
}
export function WAConnectionTest(name: string, func: (conn: WAConnection) => void) {
export const WAConnectionTest = (name: string, func: (conn: WAConnection) => void) => (
describe(name, () => {
const conn = new WAConnection()
conn.logLevel = MessageLogLevel.info
before(async () => {
//conn.logLevel = MessageLogLevel.unhandled
const file = './auth_info.json'
await conn.connectSlim(file)
await conn.loadAuthInfo(file).connect()
await fs.writeFile(file, JSON.stringify(conn.base64EncodedAuthInfo(), null, '\t'))
})
after(() => conn.close())
func(conn)
})
}
)

View File

@@ -1,21 +1,25 @@
import * as assert from 'assert'
import * as QR from 'qrcode-terminal'
import {WAConnection} from '../WAConnection/WAConnection'
import { AuthenticationCredentialsBase64 } from '../WAConnection/Constants'
import { createTimeout } from '../WAConnection/Utils'
import { AuthenticationCredentialsBase64, BaileysError, MessageLogLevel } from '../WAConnection/Constants'
import { delay, promiseTimeout } from '../WAConnection/Utils'
describe('QR Generation', () => {
it('should generate QR', async () => {
const conn = new WAConnection()
let calledQR = false
conn.onReadyForPhoneAuthentication = ([ref, curveKey, clientID]) => {
assert.ok(ref, 'ref nil')
assert.ok(curveKey, 'curve key nil')
assert.ok(clientID, 'client ID nil')
calledQR = true
}
await assert.rejects(async () => conn.connectSlim(null, 5000), 'should have failed connect')
assert.equal(calledQR, true, 'QR not called')
conn.regenerateQRIntervalMs = 5000
let calledQR = 0
conn.removeAllListeners ('qr')
conn.on ('qr', qr => calledQR += 1)
await conn.connect(15000)
.then (() => assert.fail('should not have succeeded'))
.catch (error => {
assert.equal (error.message, 'timed out')
})
assert.equal (conn['pendingRequests'].length, 0)
assert.equal (Object.keys(conn['callbacks']).filter(key => !key.startsWith('function:')).length, 0)
assert.ok(calledQR >= 2, 'QR not called')
})
})
@@ -23,54 +27,49 @@ describe('Test Connect', () => {
let auth: AuthenticationCredentialsBase64
it('should connect', async () => {
console.log('please be ready to scan with your phone')
const conn = new WAConnection()
const user = await conn.connectSlim(null)
assert.ok(user)
assert.ok(user.id)
await conn.connect (null)
assert.ok(conn.user?.id)
assert.ok(conn.user?.phone)
assert.ok (conn.user?.imgUrl || conn.user.imgUrl === '')
conn.close()
auth = conn.base64EncodedAuthInfo()
})
it('should re-generate QR & connect', async () => {
const conn = new WAConnection()
conn.onReadyForPhoneAuthentication = async ([ref, publicKey, clientID]) => {
for (let i = 0; i < 2; i++) {
console.log ('called QR ' + i + ' times')
await createTimeout (3000)
ref = await conn.generateNewQRCode ()
}
const str = ref + ',' + publicKey + ',' + clientID
QR.generate(str, { small: true })
}
const user = await conn.connectSlim(null)
assert.ok(user)
assert.ok(user.id)
conn.close()
})
it('should reconnect', async () => {
const conn = new WAConnection()
const [user, chats, contacts] = await conn.connect(auth, 20*1000)
await conn
.loadAuthInfo (auth)
.connect (20*1000)
.then (conn => {
assert.ok(conn.user)
assert.ok(conn.user.id)
assert.ok(user)
assert.ok(user.id)
assert.ok(chats)
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)
}
await conn.logout()
await assert.rejects(async () => conn.connectSlim(auth), 'reconnect should have failed')
const chatArray = conn.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])
}
}
const contactValues = Object.values(conn.contacts)
if (contactValues[0]) {
assert.ok(contactValues[0].jid)
}
})
.then (() => conn.logout())
.then (() => conn.loadAuthInfo(auth))
.then (() => (
conn.connect()
.then (() => assert.fail('should not have reconnected'))
.catch (err => {
assert.ok (err instanceof BaileysError)
assert.ok ((err as BaileysError).status >= 400)
})
))
.finally (() => conn.close())
})
})
describe ('Pending Requests', async () => {
@@ -78,21 +77,17 @@ describe ('Pending Requests', async () => {
const conn = new WAConnection ()
conn.pendingRequestTimeoutMs = null
await conn.connectSlim ()
await conn.loadAuthInfo('./auth_info.json').connect ()
await createTimeout (2000)
await delay (2000)
conn.close ()
const task: Promise<any> = new Promise ((resolve, reject) => {
conn.query(['query', 'Status', conn.userMetaData.id])
.then (json => resolve(json))
.catch (error => reject ('should not have failed, got error: ' + error))
})
const task: Promise<any> = conn.query({json: ['query', 'Status', conn.user.id]})
await createTimeout (2000)
await delay (2000)
await conn.connectSlim ()
conn.connect ()
const json = await task
assert.ok (json.status)

View File

@@ -1,12 +1,18 @@
import { MessageType, GroupSettingChange, createTimeout, ChatModification, whatsappID } from '../WAConnection/WAConnection'
import { MessageType, GroupSettingChange, delay, ChatModification } from '../WAConnection/WAConnection'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
import { WAConnectionTest, testJid } from './Common'
WAConnectionTest('Groups', (conn) => {
let gid: string
it('should create a group', async () => {
const response = await conn.groupCreate('Cool Test Group', [testJid])
assert.ok (conn.chats.get(response.gid))
const {chats} = await conn.loadChats(10, null)
assert.equal (chats[0].jid, response.gid) // first chat should be new group
gid = response.gid
console.log('created group: ' + JSON.stringify(response))
})
it('should retreive group invite code', async () => {
@@ -22,8 +28,18 @@ WAConnectionTest('Groups', (conn) => {
it('should update the group description', async () => {
const newDesc = 'Wow this was set from Baileys'
const waitForEvent = new Promise (resolve => {
conn.on ('group-description-update', ({jid, actor}) => {
if (jid === gid) {
assert.ok (actor, conn.user.id)
resolve ()
}
})
})
await conn.groupUpdateDescription (gid, newDesc)
await createTimeout (1000)
await waitForEvent
conn.removeAllListeners ('group-description-update')
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.desc, newDesc)
@@ -32,39 +48,102 @@ WAConnectionTest('Groups', (conn) => {
await conn.sendMessage(gid, 'hello', MessageType.text)
})
it('should quote a message on the group', async () => {
const messages = await conn.loadConversation (gid, 20)
const {messages} = await conn.loadMessages (gid, 100)
const quotableMessage = messages.find (m => m.message)
assert.ok (quotableMessage, 'need at least one message')
const response = await conn.sendMessage(gid, 'hello', MessageType.extendedText, {quoted: messages[0]})
const messagesNew = await conn.loadConversation(gid, 10, null, true)
const message = messagesNew.find (m => m.key.id === response.key.id)?.message?.extendedTextMessage
const response = await conn.sendMessage(gid, 'hello', MessageType.extendedText, {quoted: quotableMessage})
const loaded = await conn.loadMessages(gid, 10)
const message = loaded.messages.find (m => m.key.id === response.key.id)?.message?.extendedTextMessage
assert.ok(message)
assert.equal (message.contextInfo.stanzaId, quotableMessage.key.id)
})
it('should update the subject', async () => {
const subject = 'V Cool Title'
const subject = 'Baileyz ' + Math.floor(Math.random()*5)
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', ({jid, title}) => {
if (jid === gid) {
assert.equal (title, subject)
resolve ()
}
})
})
await conn.groupUpdateSubject(gid, subject)
await waitForEvent
conn.removeAllListeners ('chat-update')
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject)
})
it('should update the group settings', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('group-settings-update', ({jid, announce}) => {
if (jid === gid) {
assert.equal (announce, 'true')
resolve ()
}
})
})
await conn.groupSettingChange (gid, GroupSettingChange.messageSend, true)
await createTimeout (5000)
await waitForEvent
conn.removeAllListeners ('group-settings-update')
await delay (2000)
await conn.groupSettingChange (gid, GroupSettingChange.settingsChange, true)
})
it('should remove someone from a group', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('group-participants-remove', ({jid, participants}) => {
if (jid === gid) {
assert.equal (participants[0], testJid)
resolve ()
}
})
})
await conn.groupRemove(gid, [testJid])
await waitForEvent
conn.removeAllListeners ('group-participants-remove')
})
it('should leave the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', ({jid, read_only}) => {
if (jid === gid) {
assert.equal (read_only, 'true')
resolve ()
}
})
})
await conn.groupLeave(gid)
await waitForEvent
conn.removeAllListeners ('chat-update')
await conn.groupMetadataMinimal (gid)
})
it('should archive the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', ({jid, archive}) => {
if (jid === gid) {
assert.equal (archive, 'true')
resolve ()
}
})
})
await conn.modifyChat(gid, ChatModification.archive)
await waitForEvent
conn.removeAllListeners ('chat-update')
})
it('should delete the group', async () => {
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', (chat) => {
if (chat.jid === gid) {
assert.equal (chat['delete'], 'true')
resolve ()
}
})
})
await conn.deleteChat(gid)
await waitForEvent
conn.removeAllListeners ('chat-update')
})
})

View File

@@ -1,18 +1,18 @@
import { MessageType, Mimetype, createTimeout } from '../WAConnection/WAConnection'
import { MessageType, Mimetype, delay, promiseTimeout, WAMessage, WA_MESSAGE_STATUS_TYPE } from '../WAConnection/WAConnection'
import {promises as fs} from 'fs'
import * as assert from 'assert'
import { WAConnectionTest, testJid, sendAndRetreiveMessage } from './Common'
WAConnectionTest('Messages', (conn) => {
it('should send a text message', async () => {
const message = await sendAndRetreiveMessage(conn, 'hello fren', MessageType.text)
assert.strictEqual(message.message.conversation, 'hello fren')
//const message = await sendAndRetreiveMessage(conn, 'hello fren', MessageType.text)
//assert.strictEqual(message.message.conversation || message.message.extendedTextMessage?.text, 'hello fren')
})
it('should forward a message', async () => {
let messages = await conn.loadConversation (testJid, 1)
let messages = await conn.loadMessages (testJid, 1)
await conn.forwardMessage (testJid, messages[0], true)
messages = await conn.loadConversation (testJid, 1)
messages = await conn.loadMessages (testJid, 1)
const message = messages[0]
const content = message.message[ Object.keys(message.message)[0] ]
assert.equal (content?.contextInfo?.isForwarded, true)
@@ -28,7 +28,7 @@ WAConnectionTest('Messages', (conn) => {
assert.ok (received.jpegThumbnail)
})
it('should quote a message', async () => {
const messages = await conn.loadConversation(testJid, 2)
const messages = await conn.loadMessages(testJid, 2)
const message = await sendAndRetreiveMessage(conn, 'hello fren 2', MessageType.extendedText, {
quoted: messages[0],
})
@@ -48,21 +48,36 @@ WAConnectionTest('Messages', (conn) => {
//const message2 = await sendAndRetreiveMessage (conn, 'this is a quote', MessageType.extendedText)
})
it('should send an image & quote', async () => {
const messages = await conn.loadConversation(testJid, 1)
const messages = await conn.loadMessages(testJid, 1)
const content = await fs.readFile('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(conn, content, MessageType.image, { quoted: messages[0] })
await conn.downloadMediaMessage(message) // check for successful decoding
assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id)
})
it('should send a text message & delete it', async () => {
it('should send a message & delete it', async () => {
const message = await sendAndRetreiveMessage(conn, 'hello fren', MessageType.text)
await createTimeout (2000)
await delay (2000)
await conn.deleteMessage (testJid, message.key)
})
it('should clear the most recent message', async () => {
const messages = await conn.loadConversation (testJid, 1)
await createTimeout (2000)
const messages = await conn.loadMessages (testJid, 1)
await delay (2000)
await conn.clearMessage (messages[0].key)
})
})
})
WAConnectionTest('Message Events', (conn) => {
it('should deliver a message', async () => {
const waitForUpdate =
promiseTimeout(15000, resolve => {
conn.on('message-update', message => {
if (message.key.id === response.key.id) {
resolve(message)
}
})
}) as Promise<WAMessage>
const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text)
const m = await waitForUpdate
assert.ok (m.status >= WA_MESSAGE_STATUS_TYPE.DELIVERY_ACK)
})
})

View File

@@ -1,20 +1,9 @@
import { MessageType, Presence, ChatModification, promiseTimeout, createTimeout } from '../WAConnection/WAConnection'
import { Presence, ChatModification, delay } from '../WAConnection/WAConnection'
import {promises as fs} from 'fs'
import * as assert from 'assert'
import fetch from 'node-fetch'
import { WAConnectionTest, testJid } from './Common'
WAConnectionTest('Presence', (conn) => {
it('should update presence', async () => {
const presences = Object.values(Presence)
for (const i in presences) {
const response = await conn.updatePresence(testJid, presences[i])
assert.strictEqual(response.status, 200)
await createTimeout(1500)
}
})
})
WAConnectionTest('Misc', (conn) => {
it('should tell if someone has an account on WhatsApp', async () => {
const response = await conn.isOnWhatsApp(testJid)
@@ -30,16 +19,28 @@ WAConnectionTest('Misc', (conn) => {
it('should update status', async () => {
const newStatus = 'v cool status'
const waitForEvent = new Promise (resolve => {
conn.on ('user-status-update', ({jid, status}) => {
if (jid === conn.user.id) {
assert.equal (status, newStatus)
conn.removeAllListeners ('user-status-update')
resolve ()
}
})
})
const response = await conn.getStatus()
assert.strictEqual(typeof response.status, 'string')
await createTimeout (1000)
await delay (1000)
await conn.setStatus (newStatus)
const response2 = await conn.getStatus()
assert.equal (response2.status, newStatus)
await createTimeout (1000)
await waitForEvent
await delay (1000)
await conn.setStatus (response.status) // update back
})
@@ -47,18 +48,18 @@ WAConnectionTest('Misc', (conn) => {
await conn.getStories()
})
it('should change the profile picture', async () => {
await createTimeout (5000)
await delay (5000)
const ppUrl = await conn.getProfilePicture(conn.userMetaData.id)
const ppUrl = await conn.getProfilePicture(conn.user.id)
const fetched = await fetch(ppUrl, { headers: { Origin: 'https://web.whatsapp.com' } })
const buff = await fetched.buffer ()
const newPP = await fs.readFile ('./Media/cat.jpeg')
const response = await conn.updateProfilePicture (conn.userMetaData.id, newPP)
const response = await conn.updateProfilePicture (conn.user.id, newPP)
await createTimeout (10000)
await delay (10000)
await conn.updateProfilePicture (conn.userMetaData.id, buff) // revert back
await conn.updateProfilePicture (conn.user.id, buff) // revert back
})
it('should return the profile picture', async () => {
const response = await conn.getProfilePicture(testJid)
@@ -70,22 +71,32 @@ WAConnectionTest('Misc', (conn) => {
assert.ok(response)
})
it('should mark a chat unread', async () => {
await conn.sendReadReceipt(testJid, null, 'unread')
const waitForEvent = new Promise (resolve => {
conn.on ('chat-update', ({jid, count}) => {
if (jid === testJid) {
assert.ok (count < 0)
conn.removeAllListeners ('chat-update')
resolve ()
}
})
})
await conn.sendReadReceipt(testJid, null, -2)
await waitForEvent
})
it('should archive & unarchive', async () => {
await conn.modifyChat (testJid, ChatModification.archive)
await createTimeout (2000)
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unarchive)
})
it('should pin & unpin a chat', async () => {
const response = await conn.modifyChat (testJid, ChatModification.pin)
await createTimeout (2000)
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unpin, {stamp: response.stamp})
})
it('should mute & unmute a chat', async () => {
const mutedate = new Date (new Date().getTime() + 8*60*60*1000) // 8 hours in the future
await conn.modifyChat (testJid, ChatModification.mute, {stamp: mutedate})
await createTimeout (2000)
await delay (2000)
await conn.modifyChat (testJid, ChatModification.unmute, {stamp: mutedate})
})
it('should return search results', async () => {
@@ -96,18 +107,14 @@ WAConnectionTest('Misc', (conn) => {
assert.ok (response.messages.length >= 0)
}
})
})
WAConnectionTest('Events', (conn) => {
it('should deliver a message', async () => {
const waitForUpdate = () =>
new Promise((resolve) => {
conn.setOnMessageStatusChange((update) => {
if (update.ids.includes(response.key.id)) {
resolve()
}
})
})
const response = await conn.sendMessage(testJid, 'My Name Jeff', MessageType.text)
await promiseTimeout(15000, waitForUpdate())
it('should update presence', async () => {
const presences = Object.values(Presence)
for (const i in presences) {
const response = await conn.updatePresence(testJid, presences[i])
assert.strictEqual(response.status, 200)
await delay(1500)
}
})
})
})

View File

@@ -1,4 +1,3 @@
import * as QR from 'qrcode-terminal'
import * as fs from 'fs'
import WS from 'ws'
import * as Utils from './Utils'
@@ -6,120 +5,80 @@ import Encoder from '../Binary/Encoder'
import Decoder from '../Binary/Decoder'
import {
AuthenticationCredentials,
UserMetaData,
WAUser,
WANode,
AuthenticationCredentialsBase64,
WATag,
MessageLogLevel,
AuthenticationCredentialsBrowser,
BaileysError,
WAConnectionMode,
WAMessage,
PresenceUpdate,
MessageStatusUpdate,
WAMetric,
WAFlag,
DisconnectReason,
WAConnectionState,
AnyAuthenticationCredentials,
WAContact,
WAChat,
WAQuery,
ReconnectMode,
} from './Constants'
import { EventEmitter } from 'events'
import KeyedDB from '@adiwajshing/keyed-db'
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
const generateQRCode = function ([ref, publicKey, clientID]) {
const str = ref + ',' + publicKey + ',' + clientID
QR.generate(str, { small: true })
}
export class WAConnection {
export class WAConnection extends EventEmitter {
/** The version of WhatsApp Web we're telling the servers we are */
version: [number, number, number] = [2, 2027, 10]
version: [number, number, number] = [2, 2033, 7]
/** 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. */
userMetaData: UserMetaData = { id: null, name: null, phone: null }
/** Should reconnect automatically after an unexpected disconnect */
autoReconnect = true
lastSeen: Date = null
user: WAUser
/** What level of messages to log to the console */
logLevel: MessageLogLevel = MessageLogLevel.info
/** Should requests be queued when the connection breaks in between; if false, then an error will be thrown */
pendingRequestTimeoutMs: number = null
connectionMode: WAConnectionMode = WAConnectionMode.onlyRequireValidation
/** What to do when you need the phone to authenticate the connection (generate QR code by default) */
onReadyForPhoneAuthentication = generateQRCode
protected unexpectedDisconnectCallback: (err: string) => any
/** The connection state */
state: WAConnectionState = 'closed'
/** New QR generation interval, set to null if you don't want to regenerate */
regenerateQRIntervalMs = 30*1000
autoReconnect = ReconnectMode.onConnectionLost
/** Whether the phone is connected */
phoneConnected: boolean = false
maxCachedMessages = 25
contacts: {[k: string]: WAContact} = {}
chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */
protected authInfo: AuthenticationCredentials = {
clientID: null,
serverToken: null,
clientToken: null,
encKey: null,
macKey: null,
}
protected authInfo: AuthenticationCredentials = null
/** Curve keys to initially authenticate */
protected curveKeys: { private: Uint8Array; public: Uint8Array }
/** The websocket connection */
protected conn: WS = null
protected msgCount = 0
protected keepAliveReq: NodeJS.Timeout
protected callbacks = {}
protected callbacks: {[k: string]: any} = {}
protected encoder = new Encoder()
protected decoder = new Decoder()
protected pendingRequests: (() => void)[] = []
protected reconnectLoop: () => Promise<void>
protected pendingRequests: {resolve: () => void, reject: (error) => void}[] = []
protected referenceDate = new Date () // used for generating tags
protected lastSeen: Date = null // last keep alive received
protected qrTimeout: NodeJS.Timeout
protected phoneCheck: NodeJS.Timeout
protected cancelledReconnect = false
protected cancelReconnect: () => void
constructor () {
super ()
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind))
}
async unexpectedDisconnect (error: string) {
this.close()
if ((error === 'lost' || error === 'closed') && this.autoReconnect) {
await this.reconnectLoop ()
} else if (this.unexpectedDisconnectCallback) {
this.unexpectedDisconnectCallback (error)
}
}
/** Set the callback for message status updates (when a message is delivered, read etc.) */
setOnMessageStatusChange(callback: (update: MessageStatusUpdate) => void) {
const func = json => {
json = json[1]
let ids = json.id
if (json.cmd === 'ack') {
ids = [json.id]
}
const data: MessageStatusUpdate = {
from: json.from,
to: json.to,
participant: json.participant,
timestamp: new Date(json.t * 1000),
ids: ids,
type: (+json.ack)+1,
}
callback(data)
}
this.registerCallback('Msg', func)
this.registerCallback('MsgInfo', func)
}
/**
* Set the callback for new/unread messages; if someone sends you a message, this callback will be fired
* @param callbackOnMyMessages - should the callback be fired on a message you sent from the phone
*/
setOnUnreadMessage(callbackOnMyMessages = false, callback: (m: WAMessage) => void) {
this.registerCallback(['action', 'add:relay', 'message'], (json) => {
const message = json[2][0][2]
if (!message.key.fromMe || callbackOnMyMessages) {
// if this message was sent to us, notify
callback(message as WAMessage)
} else {
this.log(`[Unhandled] message - ${JSON.stringify(message)}`, MessageLogLevel.unhandled)
}
})
}
/** Set the callback for presence updates; if someone goes offline/online, this callback will be fired */
setOnPresenceUpdate(callback: (p: PresenceUpdate) => void) {
this.registerCallback('Presence', json => callback(json[1]))
}
/** Set the callback for unexpected disconnects including take over events, log out events etc. */
setOnUnexpectedDisconnect(callback: (error: string) => void) {
this.unexpectedDisconnectCallback = callback
async unexpectedDisconnect (error?: DisconnectReason) {
const willReconnect = this.autoReconnect === ReconnectMode.onAllErrors || (this.autoReconnect === ReconnectMode.onConnectionLost && (error === 'lost' || error === 'closed'))
this.log (`got disconnected, reason ${error || 'unknown'}${willReconnect ? ', reconnecting in a few seconds...' : ''}`, MessageLogLevel.info)
this.closeInternal(error, willReconnect)
willReconnect && this.reconnectLoop ()
}
/**
* base 64 encode the authentication credentials and return them
@@ -135,68 +94,42 @@ export class WAConnection {
macKey: this.authInfo.macKey.toString('base64'),
}
}
/**
* Clear authentication info so a new connection can be created
*/
/** Clear authentication info so a new connection can be created */
clearAuthInfo () {
this.authInfo = {
clientID: null,
serverToken: null,
clientToken: null,
encKey: null,
macKey: null,
}
this.authInfo = null
return this
}
/**
* Load in the authentication credentials
* @param authInfo the authentication credentials or path to auth credentials JSON
* @param authInfo the authentication credentials or file path to auth credentials
*/
loadAuthInfoFromBase64(authInfo: AuthenticationCredentialsBase64 | string) {
if (!authInfo) {
throw new Error('given authInfo is null')
}
if (typeof authInfo === 'string') {
this.log(`loading authentication credentials from ${authInfo}`, MessageLogLevel.info)
const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists
authInfo = JSON.parse(file) as AuthenticationCredentialsBase64
}
this.authInfo = {
clientID: authInfo.clientID,
serverToken: authInfo.serverToken,
clientToken: authInfo.clientToken,
encKey: Buffer.from(authInfo.encKey, 'base64'), // decode from base64
macKey: Buffer.from(authInfo.macKey, 'base64'), // decode from base64
}
}
/**
* Load in the authentication credentials
* @param authInfo the authentication credentials or path to browser credentials JSON
*/
loadAuthInfoFromBrowser(authInfo: AuthenticationCredentialsBrowser | string) {
loadAuthInfo(authInfo: AnyAuthenticationCredentials | string) {
if (!authInfo) throw new Error('given authInfo is null')
if (typeof authInfo === 'string') {
this.log(`loading authentication credentials from ${authInfo}`, MessageLogLevel.info)
const file = fs.readFileSync(authInfo, { encoding: 'utf-8' }) // load a closed session back if it exists
authInfo = JSON.parse(file) as AuthenticationCredentialsBrowser
authInfo = JSON.parse(file) as AnyAuthenticationCredentials
}
const secretBundle: {encKey: string, macKey: string} = typeof authInfo === 'string' ? JSON.parse (authInfo): authInfo
this.authInfo = {
clientID: authInfo.WABrowserId.replace(/\"/g, ''),
serverToken: authInfo.WAToken2.replace(/\"/g, ''),
clientToken: authInfo.WAToken1.replace(/\"/g, ''),
encKey: Buffer.from(secretBundle.encKey, 'base64'), // decode from base64
macKey: Buffer.from(secretBundle.macKey, 'base64'), // decode from base64
}
}
/**
* Register for a callback for a certain function, will cancel automatically after one execution
* @param {[string, object, string] | string} parameters name of the function along with some optional specific parameters
*/
async registerCallbackOneTime(parameters) {
const json = await new Promise((resolve, _) => this.registerCallback(parameters, resolve))
this.deregisterCallback(parameters)
return json
if ('clientID' in authInfo) {
this.authInfo = {
clientID: authInfo.clientID,
serverToken: authInfo.serverToken,
clientToken: authInfo.clientToken,
encKey: Buffer.isBuffer(authInfo.encKey) ? authInfo.encKey : Buffer.from(authInfo.encKey, 'base64'),
macKey: Buffer.isBuffer(authInfo.macKey) ? authInfo.macKey : Buffer.from(authInfo.macKey, 'base64'),
}
} else {
const secretBundle: {encKey: string, macKey: string} = typeof authInfo === 'string' ? JSON.parse (authInfo): authInfo
this.authInfo = {
clientID: authInfo.WABrowserId.replace(/\"/g, ''),
serverToken: authInfo.WAToken2.replace(/\"/g, ''),
clientToken: authInfo.WAToken1.replace(/\"/g, ''),
encKey: Buffer.from(secretBundle.encKey, 'base64'), // decode from base64
macKey: Buffer.from(secretBundle.macKey, 'base64'), // decode from base64
}
}
return this
}
/**
* Register for a callback for a certain function
@@ -247,30 +180,20 @@ export class WAConnection {
* @param timeoutMs timeout after which the promise will reject
*/
async waitForMessage(tag: string, json: Object = null, timeoutMs: number = null) {
let promise = new Promise(
let promise = Utils.promiseTimeout(timeoutMs,
(resolve, reject) => (this.callbacks[tag] = { queryJSON: json, callback: resolve, errCallback: reject }),
)
if (timeoutMs) {
promise = Utils.promiseTimeout(timeoutMs, promise).catch((err) => {
delete this.callbacks[tag]
throw err
})
}
.catch((err) => {
delete this.callbacks[tag]
throw err
})
return promise as Promise<any>
}
/**
* Query something from the WhatsApp servers and error on a non-200 status
* @param json the query itself
* @param [binaryTags] the tags to attach if the query is supposed to be sent encoded in binary
* @param [timeoutMs] timeout after which the query will be failed (set to null to disable a timeout)
* @param [tag] the tag to attach to the message
*/
async queryExpecting200(json: any[] | WANode, binaryTags?: WATag, timeoutMs?: number, tag?: string) {
const response = await this.query(json, binaryTags, timeoutMs, tag)
if (response.status && Math.floor(+response.status / 100) !== 2) {
throw new BaileysError(`Unexpected status code in '${json[0] || 'generic query'}': ${response.status}`, {query: json})
}
return response
/** Generic function for action, set queries */
async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) {
const json = ['action', {epoch: this.msgCount.toString(), type: 'set'}, nodes]
const result = await this.query({ json, binaryTags, tag, expect200: true }) as Promise<{status: number}>
return result
}
/**
* Query something from the WhatsApp servers
@@ -280,17 +203,18 @@ export class WAConnection {
* @param tag the tag to attach to the message
* recieved JSON
*/
async query(json: any[] | WANode, binaryTags?: WATag, timeoutMs?: number, tag?: string) {
async query({json, binaryTags, tag, timeoutMs, expect200, waitForOpen}: WAQuery) {
waitForOpen = typeof waitForOpen === 'undefined' ? true : waitForOpen
await this.waitForConnection (waitForOpen)
if (binaryTags) tag = await this.sendBinary(json as WANode, binaryTags, tag)
else tag = await this.sendJSON(json, tag)
return this.waitForMessage(tag, json, timeoutMs)
}
/** Generic function for action, set queries */
async setQuery (nodes: WANode[], binaryTags: WATag = [WAMetric.group, WAFlag.ignore], tag?: string) {
const json = ['action', {epoch: this.msgCount.toString(), type: 'set'}, nodes]
const result = await this.queryExpecting200(json, binaryTags, null, tag) as Promise<{status: number}>
return result
const response = await this.waitForMessage(tag, json, timeoutMs)
if (expect200 && response.status && Math.floor(+response.status / 100) !== 2) {
throw new BaileysError(`Unexpected status code in '${json[0] || 'generic query'}': ${response.status}`, {query: json})
}
return response
}
/**
* Send a binary encoded message
@@ -299,9 +223,7 @@ export class WAConnection {
* @param tag the tag to attach to the message
* @return the message tag
*/
protected async sendBinary(json: WANode, tags: WATag, tag?: string) {
if (!this.conn || this.conn.readyState !== this.conn.OPEN) await this.waitForConnection ()
protected sendBinary(json: WANode, tags: WATag, tag: string = null) {
const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format
let buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey
@@ -313,7 +235,7 @@ export class WAConnection {
sign, // the HMAC sign of the message
buff, // the actual encrypted buffer
])
await this.send(buff) // send it off
this.send(buff) // send it off
return tag
}
/**
@@ -322,23 +244,22 @@ export class WAConnection {
* @param tag the tag to attach to the message
* @return the message tag
*/
protected async sendJSON(json: any[] | WANode, tag: string = null) {
protected sendJSON(json: any[] | WANode, tag: string = null) {
tag = tag || this.generateMessageTag()
await this.send(tag + ',' + JSON.stringify(json))
this.send(`${tag},${JSON.stringify(json)}`)
return tag
}
/** Send some message to the WhatsApp servers */
protected async send(m) {
if (!this.conn || this.conn.readyState !== this.conn.OPEN) await this.waitForConnection ()
protected send(m) {
this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages
return this.conn.send(m)
}
protected async waitForConnection () {
protected async waitForConnection (waitForOpen: boolean) {
if (!waitForOpen || this.state === 'open') return
const timeout = this.pendingRequestTimeoutMs
try {
const task = new Promise (resolve => this.pendingRequests.push(resolve))
await Utils.promiseTimeout (timeout, task)
await Utils.promiseTimeout (timeout, (resolve, reject) => this.pendingRequests.push({resolve, reject}))
} catch {
throw new Error('cannot send message, disconnected from WhatsApp')
}
@@ -347,38 +268,51 @@ export class WAConnection {
* Disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
* @see close() if you just want to close the connection
*/
async logout() {
if (!this.conn) throw new Error("You're not even connected, you can't log out")
async logout () {
if (this.state !== 'open') throw new Error("You're not even connected, you can't log out")
await new Promise(resolve => this.conn.send('goodbye,["admin","Conn","disconnect"]', null, resolve))
this.authInfo = null
this.close()
}
/** Close the connection to WhatsApp Web */
close() {
close () {
this.closeInternal ('intentional')
this.cancelReconnect && this.cancelReconnect ()
this.cancelledReconnect = true
this.pendingRequests.forEach (({reject}) => reject(new Error('closed')))
this.pendingRequests = []
}
protected closeInternal (reason?: DisconnectReason, isReconnecting: boolean = false) {
this.qrTimeout && clearTimeout (this.qrTimeout)
this.phoneCheck && clearTimeout (this.phoneCheck)
this.state = 'closed'
this.msgCount = 0
if (this.conn) {
this.conn.removeAllListeners ('close')
this.conn.close()
this.conn = null
}
const keys = Object.keys(this.callbacks)
keys.forEach(key => {
this.conn?.removeAllListeners ('close')
this.conn?.close()
this.conn = null
this.phoneConnected = false
Object.keys(this.callbacks).forEach(key => {
if (!key.includes('function:')) {
this.callbacks[key].errCallback('connection closed')
this.callbacks[key].errCallback(new Error('closed'))
delete this.callbacks[key]
}
})
if (this.keepAliveReq) {
clearInterval(this.keepAliveReq)
}
if (this.keepAliveReq) clearInterval(this.keepAliveReq)
this.emit ('closed', { reason, isReconnecting })
}
protected async reconnectLoop () {
}
generateMessageTag () {
return `${Math.round(this.referenceDate.getTime())/1000}.--${this.msgCount}`
return `${Utils.unixTimestampSeconds(this.referenceDate)}.--${this.msgCount}`
}
protected log(text, level: MessageLogLevel) {
if (this.logLevel >= level)
console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`)
(this.logLevel >= level) && console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`)
}
}

View File

@@ -7,35 +7,29 @@ export class WAConnection extends Base {
/** Authenticate the connection */
protected async authenticate() {
if (!this.authInfo.clientID) {
// if no auth info is present, that is, a new session has to be established
// generate a client ID
this.authInfo = {
clientID: Utils.generateClientID(),
clientToken: null,
serverToken: null,
encKey: null,
macKey: null,
}
// if no auth info is present, that is, a new session has to be established
// generate a client ID
if (!this.authInfo?.clientID) {
this.authInfo = { clientID: Utils.generateClientID() } as any
}
this.referenceDate = new Date () // refresh reference date
const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true]
const json = ['admin', 'init', this.version, this.browserDescription, this.authInfo?.clientID, true]
return this.queryExpecting200(data)
return this.query({json, expect200: true, waitForOpen: false})
.then(json => {
// we're trying to establish a new connection or are trying to log in
if (this.authInfo.encKey && this.authInfo.macKey) {
if (this.authInfo?.encKey && this.authInfo?.macKey) {
// if we have the info to restore a closed session
const data = [
const json = [
'admin',
'login',
this.authInfo.clientToken,
this.authInfo.serverToken,
this.authInfo.clientID,
this.authInfo?.clientToken,
this.authInfo?.serverToken,
this.authInfo?.clientID,
'takeover',
]
return this.query(data, null, null, 's1') // wait for response with tag "s1"
return this.query({ json, tag: 's1', waitForOpen: false }) // wait for response with tag "s1"
}
return this.generateKeysForAuth(json.ref) // generate keys which will in turn be the QR
})
@@ -62,31 +56,29 @@ export class WAConnection extends Base {
this.validateNewConnection(json[1]) // validate the connection
this.log('validated connection successfully', MessageLogLevel.info)
await this.sendPostConnectQueries ()
this.sendPostConnectQueries ()
this.lastSeen = new Date() // set last seen to right now
return this.userMetaData
})
}
/**
* Send the same queries WA Web sends after connect
*/
async sendPostConnectQueries () {
await this.sendBinary (['query', {type: 'contacts', epoch: '1'}, null], [ WAMetric.queryContact, WAFlag.ignore ])
await this.sendBinary (['query', {type: 'chat', epoch: '1'}, null], [ WAMetric.queryChat, WAFlag.ignore ])
await this.sendBinary (['query', {type: 'status', epoch: '1'}, null], [ WAMetric.queryStatus, WAFlag.ignore ])
await this.sendBinary (['query', {type: 'quick_reply', epoch: '1'}, null], [ WAMetric.queryQuickReply, WAFlag.ignore ])
await this.sendBinary (['query', {type: 'label', epoch: '1'}, null], [ WAMetric.queryLabel, WAFlag.ignore ])
await this.sendBinary (['query', {type: 'emoji', epoch: '1'}, null], [ WAMetric.queryEmoji, WAFlag.ignore ])
await this.sendBinary (['action', {type: 'set', epoch: '1'}, [['presence', {type: Presence.available}, null]] ], [ WAMetric.presence, 160 ])
sendPostConnectQueries () {
this.sendBinary (['query', {type: 'contacts', epoch: '1'}, null], [ WAMetric.queryContact, WAFlag.ignore ])
this.sendBinary (['query', {type: 'chat', epoch: '1'}, null], [ WAMetric.queryChat, WAFlag.ignore ])
this.sendBinary (['query', {type: 'status', epoch: '1'}, null], [ WAMetric.queryStatus, WAFlag.ignore ])
this.sendBinary (['query', {type: 'quick_reply', epoch: '1'}, null], [ WAMetric.queryQuickReply, WAFlag.ignore ])
this.sendBinary (['query', {type: 'label', epoch: '1'}, null], [ WAMetric.queryLabel, WAFlag.ignore ])
this.sendBinary (['query', {type: 'emoji', epoch: '1'}, null], [ WAMetric.queryEmoji, WAFlag.ignore ])
this.sendBinary (['action', {type: 'set', epoch: '1'}, [['presence', {type: Presence.available}, null]] ], [ WAMetric.presence, 160 ])
}
/**
* Refresh QR Code
* @returns the new ref
*/
async generateNewQRCode() {
const data = ['admin', 'Conn', 'reref']
const response = await this.query(data)
async generateNewQRCodeRef() {
const response = await this.query({json: ['admin', 'Conn', 'reref'], expect200: true, waitForOpen: false})
return response.ref as string
}
/**
@@ -97,12 +89,13 @@ export class WAConnection extends Base {
private validateNewConnection(json) {
const onValidationSuccess = () => {
// set metadata: one's WhatsApp ID [cc][number]@s.whatsapp.net, name on WhatsApp, info about the phone
this.userMetaData = {
id: json.wid.replace('@c.us', '@s.whatsapp.net'),
this.user = {
id: Utils.whatsappID(json.wid),
name: json.pushname,
phone: json.phone,
imgUrl: null
}
return this.userMetaData
return this.user
}
if (!json.secret) {
@@ -154,18 +147,40 @@ export class WAConnection extends Base {
protected respondToChallenge(challenge: string) {
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
const signed = Utils.hmacSign(bytes, this.authInfo.macKey).toString('base64') // sign the challenge string with our macKey
const data = ['admin', 'challenge', signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID
const json = ['admin', 'challenge', signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID
this.log('resolving login challenge', MessageLogLevel.info)
return this.queryExpecting200(data)
return this.query({json, expect200: true, waitForOpen: false})
}
/** When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends */
protected async generateKeysForAuth(ref: string) {
this.curveKeys = Curve.generateKeyPair(Utils.randomBytes(32))
this.onReadyForPhoneAuthentication([
ref,
Buffer.from(this.curveKeys.public).toString('base64'),
this.authInfo.clientID,
])
return this.waitForMessage('s1', [])
const publicKey = Buffer.from(this.curveKeys.public).toString('base64')
const emitQR = () => {
const qr = [ref, publicKey, this.authInfo.clientID].join(',')
this.emit ('qr', qr)
}
const regenQR = () => {
this.qrTimeout = setTimeout (() => {
if (this.state === 'open') return
this.log ('regenerated QR', MessageLogLevel.info)
this.generateNewQRCodeRef ()
.then (newRef => ref = newRef)
.then (emitQR)
.then (regenQR)
.catch (err => this.log (`error in QR gen: ${err}`, MessageLogLevel.info))
}, this.regenerateQRIntervalMs)
}
if (this.regenerateQRIntervalMs) {
regenQR ()
}
const json = await this.waitForMessage('s1', [])
this.qrTimeout && clearTimeout (this.qrTimeout)
this.qrTimeout = null
return json
}
}

View File

@@ -1,74 +1,77 @@
import WS from 'ws'
import KeyedDB from '@adiwajshing/keyed-db'
import * as Utils from './Utils'
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel, WANode, WAConnectionMode } from './Constants'
import { WAMessage, WAChat, WAContact, MessageLogLevel, WANode, KEEP_ALIVE_INTERVAL_MS } from './Constants'
import {WAConnection as Base} from './1.Validation'
import Decoder from '../Binary/Decoder'
export class WAConnection extends Base {
/**
* 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]
* @param timeoutMs timeout after which the connect will fail, set to null for an infinite timeout
* @param waitForChats should the chats be waited for
*/
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, KeyedDB<WAChat>, WAContact[]]
} catch (error) {
this.close ()
throw error
}
}
/**
* Connect to WhatsAppWeb, resolves without waiting for chats & contacts
* @param [authInfo] credentials to log back in
* @param [timeoutMs] timeout after which the connect will fail, set to null for an infinite timeout
* @return [userMetaData, chats, contacts, unreadMessages]
*/
async connectSlim(authInfo: AuthenticationCredentialsBase64 | string = null, timeoutMs: number = null) {
async connect(timeoutMs: number = null, waitForChats: boolean = true) {
// if we're already connected, throw an error
if (this.conn) throw new Error('already connected or connecting')
// set authentication credentials if required
try {
this.loadAuthInfoFromBase64(authInfo)
} catch {}
if (this.state !== 'closed') throw new Error('cannot connect when state=' + this.state)
this.state = 'connecting'
this.emit ('connecting')
this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' })
const promise: Promise<UserMetaData> = new Promise((resolve, reject) => {
const promise: Promise<void> = Utils.promiseTimeout(timeoutMs, (resolve, reject) => {
this.conn.on('open', () => {
this.log('connected to WhatsApp Web, authenticating...', MessageLogLevel.info)
this.log('connected to WhatsApp Web server, authenticating...', MessageLogLevel.info)
// start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
this.authenticate()
.then(user => {
.then(() => {
this.startKeepAliveRequest()
this.conn.removeAllListeners ('error')
this.conn.on ('close', () => this.unexpectedDisconnect ('closed'))
resolve(user)
this.state = 'open'
resolve()
})
.catch(reject)
})
this.conn.on('message', m => this.onMessageRecieved(m))
// if there was an error in the WebSocket
this.conn.on('error', error => { this.close(); reject(error) })
this.conn.on('error', error => { this.closeInternal(error.message as any); reject(error) })
})
const user = await Utils.promiseTimeout(timeoutMs, promise).catch(err => {this.close(); throw err})
if (this.connectionMode === WAConnectionMode.onlyRequireValidation) this.releasePendingRequests ()
return user
try {
await promise
waitForChats && await this.receiveChatsAndContacts(timeoutMs, true)
this.phoneConnected = true
this.state = 'open'
this.user.imgUrl = await this.getProfilePicture (this.user.id).catch (err => '')
this.emit ('open')
this.releasePendingRequests ()
this.log ('opened connection to WhatsApp Web', MessageLogLevel.info)
return this
} catch (error) {
this.closeInternal (error.message)
throw error
}
}
/** Get the URL to download the profile picture of a person/group */
async getProfilePicture(jid: string | null) {
const response = await this.query({ json: ['query', 'ProfilePicThumb', jid || this.user.id] })
return response.eurl as string
}
/**
* Sets up callbacks to receive chats, contacts & unread messages.
* Sets up callbacks to receive chats, contacts & messages.
* Must be called immediately after connect
* @returns [chats, contacts]
*/
async receiveChatsAndContacts(timeoutMs: number = null) {
let contacts: WAContact[] = []
const chats: KeyedDB<WAChat> = new KeyedDB (Utils.waChatUniqueKey, value => value.jid)
protected async receiveChatsAndContacts(timeoutMs: number = null, stopAfterMostRecentMessage: boolean=false) {
this.contacts = {}
this.chats.clear ()
let receivedContacts = false
let receivedMessages = false
@@ -76,75 +79,97 @@ export class WAConnection extends Base {
this.log('waiting for chats & contacts', MessageLogLevel.info) // wait for the message with chats
const waitForConvos = () =>
new Promise(resolve => {
Utils.promiseTimeout(timeoutMs, resolve => {
convoResolve = () => {
// de-register the callbacks, so that they don't get called again
this.deregisterCallback(['action', 'add:last'])
this.deregisterCallback(['action', 'add:before'])
this.deregisterCallback(['action', 'add:unread'])
if (!stopAfterMostRecentMessage) {
this.deregisterCallback(['action', 'add:before'])
this.deregisterCallback(['action', 'add:unread'])
}
resolve()
}
const chatUpdate = json => {
receivedMessages = true
const isLast = json[1].last
const isLast = json[1].last || (json[1].add === 'last' && stopAfterMostRecentMessage)
const messages = json[2] as WANode[]
if (messages) {
messages.reverse().forEach (([, __, message]: ['message', null, WAMessage]) => {
messages.reverse().forEach (([,, message]: ['message', null, WAMessage]) => {
const jid = message.key.remoteJid
const chat = chats.get(jid)
const chat = this.chats.get(jid)
chat?.messages.unshift (message)
})
}
// if received contacts before messages
if (isLast && receivedContacts) convoResolve ()
}
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
this.registerCallback(['action', 'add:last'], chatUpdate)
this.registerCallback(['action', 'add:before'], chatUpdate)
this.registerCallback(['action', 'add:unread'], chatUpdate)
})
const waitForChats = async () => {
let json = await this.registerCallbackOneTime(['response', 'type:chat'])
if (json[1].duplicate) json = await this.registerCallbackOneTime (['response', 'type:chat'])
if (!json[2]) return
json[2]
.map(([item, chat]: [any, WAChat]) => {
if (!chat) {
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
return
if (!stopAfterMostRecentMessage) {
this.registerCallback(['action', 'add:before'], chatUpdate)
this.registerCallback(['action', 'add:unread'], chatUpdate)
}
chat.jid = Utils.whatsappID (chat.jid)
chat.count = +chat.count
chat.messages = []
chats.insert (chat) // chats data (log json to see what it looks like)
})
.filter (Boolean)
const waitForChats = async () => (
Utils.promiseTimeout (timeoutMs, resolve => {
this.registerCallback(['response', 'type:chat'], json => {
if (json[1].duplicate || !json[2]) return
json[2]
.forEach(([item, chat]: [any, WAChat]) => {
if (!chat) {
this.log (`unexpectedly got null chat: ${item}, ${chat}`, MessageLogLevel.info)
return
}
chat.jid = Utils.whatsappID (chat.jid)
chat.t = +chat.t
chat.count = +chat.count
chat.messages = []
this.chats.insert (chat) // chats data (log json to see what it looks like)
})
this.deregisterCallback(['response', 'type:chat'])
if (this.chats.all().length > 0) waitForConvos().then (resolve)
else resolve ()
})
})
)
const waitForContacts = async () => (
new Promise (resolve => {
this.registerCallback(['response', 'type:contacts'], json => {
if (json[1].duplicate) return
if (chats.all().length > 0) return waitForConvos()
}
const waitForContacts = async () => {
let json = await this.registerCallbackOneTime(['response', 'type:contacts'])
if (json[1].duplicate) json = await this.registerCallbackOneTime (['response', 'type:contacts'])
receivedContacts = true
json[2].forEach(([type, contact]: ['user', WAContact]) => {
if (!contact) return this.log (`unexpectedly got null contact: ${type}, ${contact}`, MessageLogLevel.info)
contact.jid = Utils.whatsappID (contact.jid)
this.contacts[contact.jid] = contact
})
// if you receive contacts after messages
// should probably resolve the promise
if (receivedMessages) convoResolve()
resolve ()
contacts = json[2].map(item => item[1])
receivedContacts = true
// if you receive contacts after messages
// should probably resolve the promise
if (receivedMessages) convoResolve()
}
this.deregisterCallback(['response', 'type:contacts'])
})
})
)
// wait for the chats & contacts to load
const promise = Promise.all([waitForChats(), waitForContacts()])
await Utils.promiseTimeout (timeoutMs, promise)
await Promise.all( [waitForChats(), waitForContacts()] )
if (this.connectionMode === WAConnectionMode.requireChatsAndContacts) this.releasePendingRequests ()
return [chats, contacts] as [KeyedDB<WAChat>, WAContact[]]
this.chats.all ().forEach (chat => {
const respectiveContact = this.contacts[chat.jid]
chat.title = respectiveContact?.name || respectiveContact?.notify
})
}
private releasePendingRequests () {
this.pendingRequests.forEach (send => send()) // send off all pending request
this.pendingRequests.forEach (({resolve}) => resolve()) // send off all pending request
this.pendingRequests = []
}
private onMessageRecieved(message) {
@@ -213,21 +238,45 @@ export class WAConnection extends Base {
}
/** Send a keep alive request every X seconds, server updates & responds with last seen */
private startKeepAliveRequest() {
const refreshInterval = 20
this.keepAliveReq = setInterval(() => {
const diff = (new Date().getTime() - this.lastSeen.getTime()) / 1000
const diff = (new Date().getTime() - this.lastSeen.getTime())
/*
check if it's been a suspicious amount of time since the server responded with our last seen
it could be that the network is down
*/
if (diff > refreshInterval + 5) this.unexpectedDisconnect ('lost')
if (diff > KEEP_ALIVE_INTERVAL_MS+5000) this.unexpectedDisconnect ('lost')
else this.send ('?,,') // if its all good, send a keep alive request
}, refreshInterval * 1000)
}, KEEP_ALIVE_INTERVAL_MS)
}
protected async reconnectLoop () {
this.cancelledReconnect = false
try {
while (true) {
const {delay, cancel} = Utils.delayCancellable (5000)
this.cancelReconnect = cancel
await delay
try {
await this.connect ()
this.cancelReconnect = null
} catch (error) {
this.log (`error in reconnecting: ${error}, reconnecting...`, MessageLogLevel.info)
}
}
} catch {
reconnectLoop = async () => {
// attempt reconnecting if the user wants us to
this.log('network is down, reconnecting...', MessageLogLevel.info)
return this.connectSlim(null, 25*1000).catch(this.reconnectLoop)
}
}
/**
* Check if your phone is connected
* @param timeoutMs max time for the phone to respond
*/
async checkPhoneConnection(timeoutMs = 5000) {
try {
const response = await this.query({json: ['admin', 'test'], timeoutMs})
return response[1] as boolean
} catch (error) {
return false
}
}
}

View File

@@ -0,0 +1,314 @@
import * as QR from 'qrcode-terminal'
import { WAConnection as Base } from './3.Connect'
import { MessageStatusUpdate, WAMessage, WAContact, WAChat, WAMessageProto, WA_MESSAGE_STUB_TYPE, WA_MESSAGE_STATUS_TYPE, MessageLogLevel, PresenceUpdate, BaileysEvent } from './Constants'
import { whatsappID, unixTimestampSeconds, isGroupID } from './Utils'
export class WAConnection extends Base {
constructor () {
super ()
this.registerOnMessageStatusChange ()
this.registerOnUnreadMessage ()
this.registerOnPresenceUpdate ()
this.registerPhoneConnectionPoll ()
// If a message has been updated (usually called when a video message gets its upload url)
this.registerCallback (['action', 'add:update', 'message'], json => {
const message: WAMessage = json[2][0][2]
const jid = whatsappID(message.key.remoteJid)
const chat = this.chats.get(jid)
if (!chat) return
const messageIndex = chat.messages.findIndex(m => m.key.id === message.key.id)
if (messageIndex >= 0) chat.messages[messageIndex] = message
this.emit ('message-update', message)
})
// If a user's contact has changed
this.registerCallback (['action', null, 'user'], json => {
const node = json[2][0]
if (node) {
const user = node[1] as WAContact
user.jid = whatsappID(user.jid)
this.contacts[user.jid] = user
const chat = this.chats.get (user.jid)
if (chat) {
chat.title = user.name || user.notify
this.emit ('chat-update', { jid: chat.jid, title: chat.title })
}
}
})
// chat archive, pin etc.
this.registerCallback(['action', null, 'chat'], json => {
json = json[2][0]
const updateType = json[1].type
const jid = whatsappID(json[1]?.jid)
const chat = this.chats.get(jid)
if (!chat) return
const FUNCTIONS = {
'delete': () => {
chat['delete'] = 'true'
this.chats.delete(chat)
return 'delete'
},
'clear': () => {
json[2].forEach(item => chat.messages.filter(m => m.key.id !== item[1].index))
return 'clear'
},
'archive': () => {
chat.archive = 'true'
return 'archive'
},
'unarchive': () => {
delete chat.archive
return 'archive'
},
'pin': () => {
chat.pin = json[1].pin
return 'pin'
}
}
const func = FUNCTIONS [updateType]
if (func) {
const property = func ()
this.emit ('chat-update', { jid, [property]: chat[property] || null })
}
})
// profile picture updates
this.registerCallback(['Cmd', 'type:picture'], async json => {
const jid = whatsappID(json[1].jid)
const chat = this.chats.get(jid)
if (!chat) return
await this.setProfilePicture (chat)
this.emit ('chat-update', { jid, imgUrl: chat.imgUrl })
})
// status updates
this.registerCallback(['Status'], async json => {
const jid = whatsappID(json[1].id)
this.emit ('user-status-update', { jid, status: json[1].status })
})
// read updates
this.registerCallback (['action', null, 'read'], async json => {
const update = json[2][0][1]
const chat = this.chats.get ( whatsappID(update.jid) )
if (update.type === 'false') chat.count = -1
else chat.count = 0
this.emit ('chat-update', { jid: chat.jid, count: chat.count })
})
this.on ('qr', qr => QR.generate(qr, { small: true }))
}
/** Set the callback for message status updates (when a message is delivered, read etc.) */
protected registerOnMessageStatusChange() {
const func = json => {
json = json[1]
let ids = json.id
if (json.cmd === 'ack') ids = [json.id]
const update: MessageStatusUpdate = {
from: json.from,
to: json.to,
participant: json.participant,
timestamp: new Date(json.t * 1000),
ids: ids,
type: (+json.ack)+1,
}
const chat = this.chats.get( whatsappID(update.to) )
if (!chat) return
this.chatUpdatedMessage (update.ids, update.type, chat)
}
this.registerCallback('Msg', func)
this.registerCallback('MsgInfo', func)
}
protected registerOnUnreadMessage() {
this.registerCallback(['action', 'add:relay', 'message'], json => {
const message = json[2][0][2] as WAMessage
this.chatAddMessageAppropriate (message)
})
}
/** Set the callback for presence updates; if someone goes offline/online, this callback will be fired */
protected registerOnPresenceUpdate() {
this.registerCallback('Presence', json => this.emit('user-presence-update', json[1]))
}
/** inserts an empty chat into the DB */
protected async chatAdd (jid: string, title?: string) {
const chat: WAChat = {
jid: jid,
t: unixTimestampSeconds(),
messages: [],
count: 0,
modify_tag: '',
spam: 'false',
title
}
await this.setProfilePicture (chat)
this.chats.insert (chat)
this.emit ('chat-new', chat)
return chat
}
/** find a chat or return an error */
protected assertChatGet = jid => {
const chat = this.chats.get (jid)
if (!chat) throw new Error (`chat '${jid}' not found`)
return chat
}
/** Adds the given message to the appropriate chat, if the chat doesn't exist, it is created */
protected async chatAddMessageAppropriate (message: WAMessage) {
const jid = whatsappID (message.key.remoteJid)
const chat = this.chats.get(jid) || await this.chatAdd (jid)
this.chatAddMessage (message, chat)
}
protected chatAddMessage (message: WAMessage, chat: WAChat) {
// add to count if the message isn't from me & there exists a message
if (!message.key.fromMe && message.message) chat.count += 1
const protocolMessage = message.message?.protocolMessage
// if it's a message to delete another message
if (protocolMessage) {
switch (protocolMessage.type) {
case WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE:
const found = chat.messages.find(m => m.key.id === protocolMessage.key.id)
if (found && found.message) {
//this.log ('deleting message: ' + protocolMessage.key.id + ' in chat: ' + protocolMessage.key.remoteJid)
found.messageStubType = WA_MESSAGE_STUB_TYPE.REVOKE
found.message = null
this.emit ('message-update', found)
}
break
default:
break
}
} else if (!chat.messages.find(m => m.key.id === message.key.id)) {
// this.log ('adding new message from ' + chat.jid)
chat.messages.push(message)
chat.messages = chat.messages.slice (-5) // only keep the last 5 messages
// only update if it's an actual message
if (message.message) this.chatUpdateTime (chat)
this.emit ('message-new', message)
// check if the message is an action
if (message.messageStubType) {
const jid = chat.jid
let actor = whatsappID (message.participant)
let participants: string[]
switch (message.messageStubType) {
case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_LEAVE:
case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_REMOVE:
participants = message.messageStubParameters.map (whatsappID)
this.emit ('group-participants-remove', { jid, actor, participants})
// mark the chat read only if you left the group
if (participants.includes(this.user.id)) {
chat.read_only = 'true'
this.emit ('chat-update', { jid, read_only: chat.read_only })
}
break
case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_ADD:
case WA_MESSAGE_STUB_TYPE.GROUP_PARTICIPANT_INVITE:
participants = message.messageStubParameters.map (whatsappID)
this.emit ('group-participants-add', { jid, participants, actor })
break
case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_ANNOUNCE:
const announce = message.messageStubParameters[0] === 'on' ? 'true' : 'false'
this.emit ('group-settings-update', { jid, announce, actor })
break
case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_ANNOUNCE:
const restrict = message.messageStubParameters[0] === 'on' ? 'true' : 'false'
this.emit ('group-settings-update', { jid, restrict, actor })
break
case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_DESCRIPTION:
this.emit ('group-description-update', { jid, actor })
break
case WA_MESSAGE_STUB_TYPE.GROUP_CHANGE_SUBJECT:
chat.title = message.messageStubParameters[0]
this.emit ('chat-update', { jid, title: chat.title })
break
}
}
}
}
protected chatUpdatedMessage (messageIDs: string[], status: number, chat: WAChat) {
for (let msg of chat.messages) {
if (messageIDs.includes(msg.key.id)) {
if (isGroupID(chat.jid)) msg.status = WA_MESSAGE_STATUS_TYPE.SERVER_ACK
else msg.status = status
this.emit ('message-update', msg)
}
}
}
protected chatUpdateTime = chat => this.chats.updateKey (chat, c => c.t = unixTimestampSeconds())
/** sets the profile picture of a chat */
protected async setProfilePicture (chat: WAChat) {
chat.imgUrl = await this.getProfilePicture (chat.jid).catch (err => '')
}
protected registerPhoneConnectionPoll () {
this.phoneCheck = setInterval (() => {
this.checkPhoneConnection (7500) // 7500 ms for timeout
.then (connected => {
if (this.phoneConnected != connected) {
this.emit ('connection-phone-change', {connected})
}
this.phoneConnected = connected
})
.catch (error => this.log(`error in getting phone connection: ${error}`, MessageLogLevel.info))
}, 20000)
}
// Add all event types
/** when the connection has opened successfully */
on (event: 'open', listener: () => void): this
/** when the connection is opening */
on (event: 'connecting', listener: () => void): this
/** when the connection has closed */
on (event: 'closed', listener: (err: {reason?: string, isReconnecting: boolean}) => void): this
/** when a new QR is generated, ready for scanning */
on (event: 'qr', listener: (qr: string) => void): this
/** when the connection to the phone changes */
on (event: 'connection-phone-change', listener: (state: {connected: boolean}) => void): this
/** when a user's presence is updated */
on (event: 'user-presence-update', listener: (update: PresenceUpdate) => void): this
/** when a user's status is updated */
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 a chat is updated (archived, deleted, pinned) */
on (event: 'chat-update', listener: (chat: Partial<WAChat> & { jid: string }) => void): this
/** when a new message is relayed */
on (event: 'message-new', listener: (message: WAMessage) => void): this
/** when a message is updated (deleted, delivered, read) */
on (event: 'message-update', listener: (message: WAMessage) => void): this
/** when participants are added to a group */
on (event: 'group-participants-add', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are removed or leave from a group */
on (event: 'group-participants-remove', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are promoted in a group */
on (event: 'group-participants-promote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when participants are demoted in a group */
on (event: 'group-participants-demote', listener: (update: {jid: string, participants: string[], actor?: string}) => void): this
/** when the group settings is updated */
on (event: 'group-settings-update', listener: (update: {jid: string, restrict?: string, announce?: string, actor?: string}) => void): this
/** when the group description is updated */
on (event: 'group-description-update', listener: (update: {jid: string, description?: string, actor?: string}) => void): this
on (event: BaileysEvent, listener: (...args: any[]) => void) { return super.on (event, listener) }
emit (event: BaileysEvent, ...args: any[]) { return super.emit (event, ...args) }
}

View File

@@ -1,170 +0,0 @@
import {WAConnection as Base} from './3.Connect'
import { Presence, WABroadcastListInfo, WAProfilePictureChange } from './Constants'
import {
WAMessage,
WANode,
WAMetric,
WAFlag,
} from '../WAConnection/Constants'
import { generateProfilePicture } from './Utils'
// All user related functions -- get profile picture, set status etc.
export class WAConnection extends Base {
/** Query whether a given number is registered on WhatsApp */
isOnWhatsApp = (jid: string) => this.query(['query', 'exist', jid]).then((m) => m.status === 200)
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
async updatePresence(jid: string | null, type: Presence) {
const json = [
'action',
{ epoch: this.msgCount.toString(), type: 'set' },
[['presence', { type: type, to: jid }, null]],
]
return this.queryExpecting200(json, [WAMetric.group, WAFlag.acknowledge]) as Promise<{ status: number }>
}
/** Request an update on the presence of a user */
requestPresenceUpdate = async (jid: string) => this.queryExpecting200(['action', 'presence', 'subscribe', jid])
/** Query the status of the person (see groupMetadata() for groups) */
async getStatus (jid?: string) {
return this.query(['query', 'Status', jid || this.userMetaData.id]) as Promise<{ status: string }>
}
async setStatus (status: string) {
return this.setQuery (
[
[
'status',
null,
Buffer.from (status, 'utf-8')
]
]
)
}
/** Get the URL to download the profile picture of a person/group */
async getProfilePicture(jid: string | null) {
const response = await this.queryExpecting200(['query', 'ProfilePicThumb', jid || this.userMetaData.id])
return response.eurl as string
}
/** Get your contacts */
async getContacts() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'contacts' }, null]
const response = await this.query(json, [6, WAFlag.ignore]) // this has to be an encrypted query
return response
}
/** Get the stories of your contacts */
async getStories() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'status' }, null]
const response = await this.queryExpecting200(json, [30, WAFlag.ignore]) as WANode
if (Array.isArray(response[2])) {
return response[2].map (row => (
{
unread: row[1]?.unread,
count: row[1]?.count,
messages: Array.isArray(row[2]) ? row[2].map (m => m[2]) : []
} as {unread: number, count: number, messages: WAMessage[]}
))
}
return []
}
/** Fetch your chats */
async getChats() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'chat' }, null]
return this.query(json, [5, WAFlag.ignore]) // this has to be an encrypted query
}
/** Query broadcast list info */
async getBroadcastListInfo(jid: string) { return this.queryExpecting200(['query', 'contact', jid]) as Promise<WABroadcastListInfo> }
/** Delete the chat of a given ID */
async deleteChat (jid: string) {
return this.setQuery ([ ['chat', {type: 'delete', jid: jid}, null] ], [12, WAFlag.ignore]) as Promise<{status: number}>
}
/**
* Check if your phone is connected
* @param timeoutMs max time for the phone to respond
*/
async isPhoneConnected(timeoutMs = 5000) {
try {
const response = await this.query(['admin', 'test'], null, timeoutMs)
return response[1] as boolean
} catch (error) {
return false
}
}
/**
* Load the conversation with a group or person
* @param count the number of messages to load
* @param [indexMessage] the data for which message to offset the query by
* @param [mostRecentFirst] retreive the most recent message first or retreive from the converation start
*/
async loadConversation(
jid: string,
count: number,
indexMessage: { id: string; fromMe: boolean } = null,
mostRecentFirst = true,
) {
const json = [
'query',
{
epoch: this.msgCount.toString(),
type: 'message',
jid: jid,
kind: mostRecentFirst ? 'before' : 'after',
count: count.toString(),
index: indexMessage?.id,
owner: indexMessage?.fromMe === false ? 'false' : 'true',
},
null,
]
const response = await this.queryExpecting200(json, [WAMetric.queryMessages, WAFlag.ignore])
return response[2] ? (response[2] as WANode[]).map((item) => item[2] as WAMessage) : []
}
/**
* Load the entire friggin conversation with a group or person
* @param onMessage callback for every message retreived
* @param [chunkSize] the number of messages to load in a single request
* @param [mostRecentFirst] retreive the most recent message first or retreive from the converation start
*/
loadEntireConversation(jid: string, onMessage: (m: WAMessage) => void, chunkSize = 25, mostRecentFirst = true) {
let offsetID = null
const loadMessage = async () => {
const json = await this.loadConversation(jid, chunkSize, offsetID, mostRecentFirst)
// callback with most recent message first (descending order of date)
let lastMessage
if (mostRecentFirst) {
for (let i = json.length - 1; i >= 0; i--) {
onMessage(json[i])
lastMessage = json[i]
}
} else {
for (let i = 0; i < json.length; i++) {
onMessage(json[i])
lastMessage = json[i]
}
}
// if there are still more messages
if (json.length >= chunkSize) {
offsetID = lastMessage.key // get the last message
return new Promise((resolve, reject) => {
// send query after 200 ms
setTimeout(() => loadMessage().then(resolve).catch(reject), 200)
})
}
}
return loadMessage() as Promise<void>
}
async updateProfilePicture (jid: string, img: Buffer) {
const data = await generateProfilePicture (img)
const tag = this.generateMessageTag ()
const query: WANode = [
'picture',
{ jid: jid, id: tag, type: 'set' },
[
['image', null, data.img],
['preview', null, data.preview]
]
]
return this.setQuery ([query], [WAMetric.picture, 136], tag) as Promise<WAProfilePictureChange>
}
}

172
src/WAConnection/5.User.ts Normal file
View File

@@ -0,0 +1,172 @@
import {WAConnection as Base} from './4.Events'
import { Presence, WABroadcastListInfo, WAProfilePictureChange, WAChat, ChatModification } from './Constants'
import {
WAMessage,
WANode,
WAMetric,
WAFlag,
} from '../WAConnection/Constants'
import { generateProfilePicture, waChatUniqueKey, whatsappID, unixTimestampSeconds } from './Utils'
// All user related functions -- get profile picture, set status etc.
export class WAConnection extends Base {
/** Query whether a given number is registered on WhatsApp */
isOnWhatsApp = (jid: string) => this.query({json: ['query', 'exist', jid]}).then((m) => m.status === 200)
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
async updatePresence(jid: string | null, type: Presence) {
const json = [
'action',
{ epoch: this.msgCount.toString(), type: 'set' },
[['presence', { type: type, to: jid }, null]],
]
return this.query({json, binaryTags: [WAMetric.group, WAFlag.acknowledge]}) as Promise<{ status: number }>
}
/** Request an update on the presence of a user */
requestPresenceUpdate = async (jid: string) => this.query({json: ['action', 'presence', 'subscribe', jid]})
/** Query the status of the person (see groupMetadata() for groups) */
async getStatus (jid?: string) {
const status: { status: string } = await this.query({json: ['query', 'Status', jid || this.user.id]})
return status
}
async setStatus (status: string) {
const response = await this.setQuery (
[
[
'status',
null,
Buffer.from (status, 'utf-8')
]
]
)
this.emit ('user-status-update', { jid: this.user.id, status })
return response
}
/** Get your contacts */
async getContacts() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'contacts' }, null]
const response = await this.query({ json, binaryTags: [6, WAFlag.ignore] }) // this has to be an encrypted query
return response
}
/** Get the stories of your contacts */
async getStories() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'status' }, null]
const response = await this.query({json, binaryTags: [30, WAFlag.ignore], expect200: true}) as WANode
if (Array.isArray(response[2])) {
return response[2].map (row => (
{
unread: row[1]?.unread,
count: row[1]?.count,
messages: Array.isArray(row[2]) ? row[2].map (m => m[2]) : []
} as {unread: number, count: number, messages: WAMessage[]}
))
}
return []
}
/** Fetch your chats */
async getChats() {
const json = ['query', { epoch: this.msgCount.toString(), type: 'chat' }, null]
return this.query({ json, binaryTags: [5, WAFlag.ignore]}) // this has to be an encrypted query
}
/** Query broadcast list info */
async getBroadcastListInfo(jid: string) { return this.query({json: ['query', 'contact', jid], expect200: true}) as Promise<WABroadcastListInfo> }
/** Delete the chat of a given ID */
async deleteChat (jid: string) {
const response = await this.setQuery ([ ['chat', {type: 'delete', jid: jid}, null] ], [12, WAFlag.ignore]) as {status: number}
const chat = this.chats.get (jid)
if (chat) {
this.chats.delete (chat)
this.emit ('chat-update', { jid, delete: 'true' })
}
return response
}
/**
* Load chats in a paginated manner + gets the profile picture
* @param before chats before the given cursor
* @param count number of results to return
* @param searchString optionally search for users
* @returns the chats & the cursor to fetch the next page
*/
async loadChats (count: number, before: number | null, searchString?: string) {
let db = this.chats
if (searchString) {
db = db.filter (value => value.title?.includes (searchString) || value.jid?.startsWith(searchString))
}
const chats = db.paginated (before, count)
await Promise.all (
chats.map (async chat => (
chat.imgUrl === undefined && await this.setProfilePicture (chat)
))
)
const cursor = (chats[chats.length-1] && chats.length >= count) ? waChatUniqueKey (chats[chats.length-1]) : null
return { chats, cursor }
}
async updateProfilePicture (jid: string, img: Buffer) {
jid = whatsappID (jid)
const data = await generateProfilePicture (img)
const tag = this.generateMessageTag ()
const query: WANode = [
'picture',
{ jid: jid, id: tag, type: 'set' },
[
['image', null, data.img],
['preview', null, data.preview]
]
]
const response = await (this.setQuery ([query], [WAMetric.picture, 136], tag) as Promise<WAProfilePictureChange>)
if (jid === this.user.id) this.user.imgUrl = response.eurl
else if (this.chats.get(jid)) {
this.chats.get(jid).imgUrl = response.eurl
this.emit ('chat-update', { jid, imgUrl: response.eurl })
}
return response
}
/**
* Modify a given chat (archive, pin etc.)
* @param jid the ID of the person/group you are modifiying
* @param options.stamp the timestamp of pinning/muting the chat. Is required when unpinning/unmuting
*/
async modifyChat (jid: string, type: ChatModification, options: {stamp: Date | string} = {stamp: new Date()}) {
jid = whatsappID (jid)
let chatAttrs: Record<string, string> = {jid: jid}
if ((type === ChatModification.unpin || type === ChatModification.unmute) && !options?.stamp) {
throw new Error('options.stamp must be set to the timestamp of the time of pinning/unpinning of the chat')
}
const strStamp = options.stamp &&
(typeof options.stamp === 'string' ? options.stamp : unixTimestampSeconds(options.stamp).toString ())
switch (type) {
case ChatModification.pin:
case ChatModification.mute:
chatAttrs.type = type
chatAttrs[type] = strStamp
break
case ChatModification.unpin:
case ChatModification.unmute:
chatAttrs.type = type.replace ('un', '') // replace 'unpin' with 'pin'
chatAttrs.previous = strStamp
break
default:
chatAttrs.type = type
break
}
let response = await this.setQuery ([['chat', chatAttrs, null]]) as {status: number, stamp: string}
response.stamp = strStamp
const chat = this.chats.get (jid)
if (chat) {
if (type.includes('un')) {
type = type.replace ('un', '') as ChatModification
delete chat[type.replace('un','')]
this.emit ('chat-update', { jid, [type]: false })
} else {
chat[type] = chatAttrs[type] || 'true'
this.emit ('chat-update', { jid, [type]: chat[type] })
}
}
return response
}
}

View File

@@ -1,4 +1,4 @@
import {WAConnection as Base} from './4.User'
import {WAConnection as Base} from './5.User'
import fetch from 'node-fetch'
import {promises as fs} from 'fs'
import {
@@ -9,215 +9,18 @@ import {
MediaPathMap,
WALocationMessage,
WAContactMessage,
WASendMessageResponse,
WAMessageKey,
ChatModification,
MessageInfo,
WATextMessage,
WAUrlInfo,
WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE
} from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID } from './Utils'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils'
export class WAConnection extends Base {
/** Get the message info, who has read it, who its been delivered to */
async messageInfo (jid: string, messageID: string) {
const query = ['query', {type: 'message_info', index: messageID, jid: jid, epoch: this.msgCount.toString()}, null]
const response = (await this.queryExpecting200 (query, [22, WAFlag.ignore]))[2]
const info: MessageInfo = {reads: [], deliveries: []}
if (response) {
//console.log (response)
const reads = response.filter (node => node[0] === 'read')
if (reads[0]) {
info.reads = reads[0][2].map (item => item[1])
}
const deliveries = response.filter (node => node[0] === 'delivery')
if (deliveries[0]) {
info.deliveries = deliveries[0][2].map (item => item[1])
}
}
return info
}
/**
* Send a read receipt to the given ID for a certain message
* @param jid the ID of the person/group whose message you want to mark read
* @param messageID optionally, the message ID
* @param type whether to read or unread the message
*/
async sendReadReceipt(jid: string, messageID?: string, type: 'read' | 'unread' = 'read') {
const attributes = {
jid: jid,
count: type === 'read' ? '1' : '-2',
index: messageID,
owner: messageID ? 'false' : null
}
return this.setQuery ([['read', attributes, null]])
}
/**
* Modify a given chat (archive, pin etc.)
* @param jid the ID of the person/group you are modifiying
* @param options.stamp the timestamp of pinning/muting the chat. Is required when unpinning/unmuting
*/
async modifyChat (jid: string, type: ChatModification, options: {stamp: Date | string} = {stamp: new Date()}) {
let chatAttrs: Record<string, string> = {jid: jid}
if ((type === ChatModification.unpin || type === ChatModification.unmute) && !options?.stamp) {
throw new Error('options.stamp must be set to the timestamp of the time of pinning/unpinning of the chat')
}
const strStamp = options.stamp &&
(typeof options.stamp === 'string' ? options.stamp : Math.round(options.stamp.getTime ()/1000).toString ())
switch (type) {
case ChatModification.pin:
case ChatModification.mute:
chatAttrs.type = type
chatAttrs[type] = strStamp
break
case ChatModification.unpin:
case ChatModification.unmute:
chatAttrs.type = type.replace ('un', '') // replace 'unpin' with 'pin'
chatAttrs.previous = strStamp
break
default:
chatAttrs.type = type
break
}
let response = await this.setQuery ([['chat', chatAttrs, null]]) as any
response.stamp = strStamp
return response as {status: number, stamp: string}
}
async loadMessage (jid: string, messageID: string) {
let messages
try {
messages = await this.loadConversation (jid, 1, {id: messageID, fromMe: true}, false)
} catch {
messages = await this.loadConversation (jid, 1, {id: messageID, fromMe: false}, false)
}
var index = null
if (messages.length > 0) index = messages[0].key
const actual = await this.loadConversation (jid, 1, index)
return actual[0]
}
/** Query a string to check if it has a url, if it does, return required extended text message */
async generateLinkPreview (text: string) {
const query = ['query', {type: 'url', url: text, epoch: this.msgCount.toString()}, null]
const response = await this.queryExpecting200 (query, [26, WAFlag.ignore])
if (response[1]) response[1].jpegThumbnail = response[2]
const data = response[1] as WAUrlInfo
const content = {text} as WATextMessage
content.canonicalUrl = data['canonical-url']
content.matchedText = data['matched-text']
content.jpegThumbnail = data.jpegThumbnail
content.description = data.description
content.title = data.title
content.previewType = 0
return content
}
/**
* Search WhatsApp messages with a given text string
* @param txt the search string
* @param inJid the ID of the chat to search in, set to null to search all chats
* @param count number of results to return
* @param page page number of results (starts from 1)
*/
async searchMessages(txt: string, inJid: string | null, count: number, page: number) {
const json = [
'query',
{
epoch: this.msgCount.toString(),
type: 'search',
search: txt,
count: count.toString(),
page: page.toString(),
jid: inJid
},
null,
]
const response: WANode = await this.queryExpecting200(json, [WAMetric.group, WAFlag.ignore]) // encrypt and send off
const messages = response[2] ? response[2].map (row => row[2]) : []
return { last: response[1]['last'] === 'true', messages: messages as WAMessage[] }
}
/**
* Delete a message in a chat for yourself
* @param messageKey key of the message you want to delete
*/
async clearMessage (messageKey: WAMessageKey) {
const tag = Math.round(Math.random ()*1000000)
const attrs: WANode = [
'chat',
{ jid: messageKey.remoteJid, modify_tag: tag.toString(), type: 'clear' },
[
['item', {owner: `${messageKey.fromMe}`, index: messageKey.id}, null]
]
]
return this.setQuery ([attrs])
}
/**
* Fetches the latest url & media key for the given message.
* You may need to call this when the message is old & the content is deleted off of the WA servers
* @param message
*/
async updateMediaMessage (message: WAMessage) {
const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage
if (!content) throw new BaileysError (`given message ${message.key.id} is not a media message`, message)
const query = ['query',{type: 'media', index: message.key.id, owner: message.key.fromMe ? 'true' : 'false', jid: message.key.remoteJid, epoch: this.msgCount.toString()},null]
const response = await this.query (query, [WAMetric.queryMedia, WAFlag.ignore])
if (parseInt(response[1].code) !== 200) throw new BaileysError ('unexpected status ' + response[1].code, response)
Object.keys (response[1]).forEach (key => content[key] = response[1][key]) // update message
}
/**
* Delete a message in a chat for everyone
* @param id the person or group where you're trying to delete the message
* @param messageKey key of the message you want to delete
*/
async deleteMessage (id: string, messageKey: WAMessageKey) {
const json: WAMessageContent = {
protocolMessage: {
key: messageKey,
type: WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE
}
}
const waMessage = this.generateWAMessage (id, json, {})
await this.relayWAMessage (waMessage)
return waMessage
}
/**
* Forward a message like WA does
* @param id the id to forward the message to
* @param message the message to forward
* @param forceForward will show the message as forwarded even if it is from you
*/
async forwardMessage(id: string, message: WAMessage, forceForward: boolean=false) {
const content = message.message
if (!content) throw new Error ('no content in message')
let key = Object.keys(content)[0]
let score = content[key].contextInfo?.forwardingScore || 0
score += message.key.fromMe && !forceForward ? 0 : 1
if (key === MessageType.text) {
content[MessageType.extendedText] = { text: content[key] }
delete content[MessageType.text]
key = MessageType.extendedText
}
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
else content[key].contextInfo = {}
const waMessage = this.generateWAMessage (id, content, {})
await this.relayWAMessage (waMessage)
return waMessage
}
/**
* Send a message to the given ID (can be group, single, or broadcast)
* @param id
* @param message
* @param type
* @param options
* @param id the id to send to
* @param message the message can be a buffer, plain string, location message, extended text message
* @param type type of message
* @param options Extra options
*/
async sendMessage(
id: string,
@@ -293,7 +96,7 @@ export class WAConnection extends Base {
await generateThumbnail(buffer, mediaType, options)
// send a query JSON to obtain the url & auth token to upload our media
const json = (await this.query(['query', 'mediaConn'])).media_conn
const json = (await this.query({json: ['query', 'mediaConn']})).media_conn
const auth = json.auth // the auth token
let hostname = 'https://' + json.hosts[0].hostname // first hostname available
hostname += MediaPathMap[mediaType] + '/' + fileEncSha256B64 // append path
@@ -331,7 +134,7 @@ export class WAConnection extends Base {
id = whatsappID (id)
const key = Object.keys(message)[0]
const timestamp = options.timestamp.getTime()/1000
const timestamp = unixTimestampSeconds(options.timestamp)
const quoted = options.quoted
if (options.contextInfo) message[key].contextInfo = options.contextInfo
@@ -361,7 +164,7 @@ export class WAConnection extends Base {
message: message,
messageTimestamp: timestamp,
messageStubParameters: [],
participant: id.includes('@g.us') ? this.userMetaData.id : null,
participant: id.includes('@g.us') ? this.user.id : null,
status: WA_MESSAGE_STATUS_TYPE.PENDING
}
return messageJSON as WAMessage
@@ -369,8 +172,22 @@ export class WAConnection extends Base {
/** Relay (send) a WAMessage; more advanced functionality to send a built WA Message, you may want to stick with sendMessage() */
async relayWAMessage(message: WAMessage) {
const json = ['action', {epoch: this.msgCount.toString(), type: 'relay'}, [['message', null, message]]]
const flag = message.key.remoteJid === this.userMetaData.id ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself
await this.queryExpecting200(json, [WAMetric.message, flag], null, message.key.id)
const flag = message.key.remoteJid === this.user.id ? WAFlag.acknowledge : WAFlag.ignore // acknowledge when sending message to oneself
await this.query({json, binaryTags: [WAMetric.message, flag], tag: message.key.id})
await this.chatAddMessageAppropriate (message)
}
/**
* Fetches the latest url & media key for the given message.
* You may need to call this when the message is old & the content is deleted off of the WA servers
* @param message
*/
async updateMediaMessage (message: WAMessage) {
const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage
if (!content) throw new BaileysError (`given message ${message.key.id} is not a media message`, message)
const query = ['query',{type: 'media', index: message.key.id, owner: message.key.fromMe ? 'true' : 'false', jid: message.key.remoteJid, epoch: this.msgCount.toString()},null]
const response = await this.query ({json: query, binaryTags: [WAMetric.queryMedia, WAFlag.ignore], expect200: true})
Object.keys (response[1]).forEach (key => content[key] = response[1][key]) // update message
}
/**
* Securely downloads the media from the message.

View File

@@ -0,0 +1,261 @@
import {WAConnection as Base} from './6.MessagesSend'
import {
MessageType,
WAMessageKey,
MessageInfo,
WATextMessage,
WAUrlInfo,
WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE
} from './Constants'
import { whatsappID } from './Utils'
export class WAConnection extends Base {
async loadAllUnreadMessages () {
const tasks = this.chats.all()
.filter(chat => chat.count > 0)
.map (chat => this.loadMessages(chat.jid, chat.count))
const list = await Promise.all (tasks)
const combined: WAMessage[] = []
list.forEach (({messages}) => combined.push(...messages))
return combined
}
/** Get the message info, who has read it, who its been delivered to */
async messageInfo (jid: string, messageID: string) {
const query = ['query', {type: 'message_info', index: messageID, jid: jid, epoch: this.msgCount.toString()}, null]
const response = (await this.query ({json: query, binaryTags: [22, WAFlag.ignore], expect200: true}))[2]
const info: MessageInfo = {reads: [], deliveries: []}
if (response) {
//console.log (response)
const reads = response.filter (node => node[0] === 'read')
if (reads[0]) {
info.reads = reads[0][2].map (item => item[1])
}
const deliveries = response.filter (node => node[0] === 'delivery')
if (deliveries[0]) {
info.deliveries = deliveries[0][2].map (item => item[1])
}
}
return info
}
/**
* Read/unread messages of a chat; will mark the entire chat read by default
* @param jid the ID of the person/group whose message you want to mark read
* @param messageID optionally, the message ID
* @param count number of messages to read, set to < 0 to unread a message
*/
async sendReadReceipt(jid: string, messageID?: string, count?: number) {
jid = whatsappID (jid)
const chat = this.chats.get(jid)
count = count || Math.abs(chat?.count || 1)
const attributes = {
jid: jid,
count: count.toString(),
index: messageID,
owner: messageID ? 'false' : null
}
const read = await this.setQuery ([['read', attributes, null]])
if (chat) {
chat.count = count < 0 ? -1 : chat.count-count
this.emit ('chat-update', {jid, count: chat.count})
}
return read
}
/**
* Load the conversation with a group or person
* @param count the number of messages to load
* @param before the data for which message to offset the query by
* @param mostRecentFirst retreive the most recent message first or retreive from the converation start
*/
async loadMessages (
jid: string,
count: number,
before: { id?: string; fromMe?: boolean } = null,
mostRecentFirst = true
) {
jid = whatsappID(jid)
const retreive = async (count: number, indexMessage: any) => {
const json = [
'query',
{
epoch: this.msgCount.toString(),
type: 'message',
jid: jid,
kind: mostRecentFirst ? 'before' : 'after',
count: count.toString(),
index: indexMessage?.id,
owner: indexMessage?.fromMe === false ? 'false' : 'true',
},
null,
]
const response = await this.query({json, binaryTags: [WAMetric.queryMessages, WAFlag.ignore], expect200: true})
const messages = response[2] ? (response[2] as WANode[]).map((item) => item[2] as WAMessage) : []
return messages
}
const chat = this.chats.get (jid)
let messages: WAMessage[]
if (!before && chat && mostRecentFirst) {
messages = chat.messages
if (messages.length < count) {
const extra = await retreive (count-messages.length, messages[0]?.key)
messages.unshift (...extra)
}
} else messages = await retreive (count, before)
const cursor = messages[0] && messages[0].key
return {messages, cursor}
}
/**
* Load the entire friggin conversation with a group or person
* @param onMessage callback for every message retreived
* @param chunkSize the number of messages to load in a single request
* @param mostRecentFirst retreive the most recent message first or retreive from the converation start
*/
loadAllMessages(jid: string, onMessage: (m: WAMessage) => void, chunkSize = 25, mostRecentFirst = true) {
let offsetID = null
const loadMessage = async () => {
const {messages} = await this.loadMessages(jid, chunkSize, offsetID, mostRecentFirst)
// callback with most recent message first (descending order of date)
let lastMessage
if (mostRecentFirst) {
for (let i = messages.length - 1; i >= 0; i--) {
onMessage(messages[i])
lastMessage = messages[i]
}
} else {
for (let i = 0; i < messages.length; i++) {
onMessage(messages[i])
lastMessage = messages[i]
}
}
// if there are still more messages
if (messages.length >= chunkSize) {
offsetID = lastMessage.key // get the last message
return new Promise((resolve, reject) => {
// send query after 200 ms
setTimeout(() => loadMessage().then(resolve).catch(reject), 200)
})
}
}
return loadMessage() as Promise<void>
}
/** Load a single message specified by the ID */
async loadMessage (jid: string, messageID: string) {
let messages: WAMessage[]
try {
messages = (await this.loadMessages (jid, 1, {id: messageID, fromMe: true}, false)).messages
} catch {
messages = (await this.loadMessages (jid, 1, {id: messageID, fromMe: false}, false)).messages
}
var index = null
if (messages.length > 0) index = messages[0].key
const actual = await this.loadMessages (jid, 1, index)
return actual.messages[0]
}
/** Query a string to check if it has a url, if it does, return required extended text message */
async generateLinkPreview (text: string) {
const query = ['query', {type: 'url', url: text, epoch: this.msgCount.toString()}, null]
const response = await this.query ({json: query, binaryTags: [26, WAFlag.ignore], expect200: true})
if (response[1]) response[1].jpegThumbnail = response[2]
const data = response[1] as WAUrlInfo
const content = {text} as WATextMessage
content.canonicalUrl = data['canonical-url']
content.matchedText = data['matched-text']
content.jpegThumbnail = data.jpegThumbnail
content.description = data.description
content.title = data.title
content.previewType = 0
return content
}
/**
* Search WhatsApp messages with a given text string
* @param txt the search string
* @param inJid the ID of the chat to search in, set to null to search all chats
* @param count number of results to return
* @param page page number of results (starts from 1)
*/
async searchMessages(txt: string, inJid: string | null, count: number, page: number) {
const json = [
'query',
{
epoch: this.msgCount.toString(),
type: 'search',
search: txt,
count: count.toString(),
page: page.toString(),
jid: inJid
},
null,
]
const response: WANode = await this.query({json, binaryTags: [WAMetric.group, WAFlag.ignore], expect200: true}) // encrypt and send off
const messages = response[2] ? response[2].map (row => row[2]) : []
return { last: response[1]['last'] === 'true', messages: messages as WAMessage[] }
}
/**
* Delete a message in a chat for yourself
* @param messageKey key of the message you want to delete
*/
async clearMessage (messageKey: WAMessageKey) {
const tag = Math.round(Math.random ()*1000000)
const attrs: WANode = [
'chat',
{ jid: messageKey.remoteJid, modify_tag: tag.toString(), type: 'clear' },
[
['item', {owner: `${messageKey.fromMe}`, index: messageKey.id}, null]
]
]
return this.setQuery ([attrs])
}
/**
* Delete a message in a chat for everyone
* @param id the person or group where you're trying to delete the message
* @param messageKey key of the message you want to delete
*/
async deleteMessage (id: string, messageKey: WAMessageKey) {
const json: WAMessageContent = {
protocolMessage: {
key: messageKey,
type: WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE
}
}
const waMessage = this.generateWAMessage (id, json, {})
await this.relayWAMessage (waMessage)
return waMessage
}
/**
* Forward a message like WA does
* @param id the id to forward the message to
* @param message the message to forward
* @param forceForward will show the message as forwarded even if it is from you
*/
async forwardMessage(id: string, message: WAMessage, forceForward: boolean=false) {
const content = message.message
if (!content) throw new Error ('no content in message')
let key = Object.keys(content)[0]
let score = content[key].contextInfo?.forwardingScore || 0
score += message.key.fromMe && !forceForward ? 0 : 1
if (key === MessageType.text) {
content[MessageType.extendedText] = { text: content[key] }
delete content[MessageType.text]
key = MessageType.extendedText
}
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
else content[key].contextInfo = {}
const waMessage = this.generateWAMessage (id, content, {})
await this.relayWAMessage (waMessage)
return waMessage
}
}

View File

@@ -1,4 +1,4 @@
import {WAConnection as Base} from './5.Messages'
import {WAConnection as Base} from './7.MessagesExtra'
import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants'
import { GroupSettingChange } from './Constants'
import { generateMessageID } from '../WAConnection/Utils'
@@ -10,23 +10,23 @@ export class WAConnection extends Base {
const json: WANode = [
'group',
{
author: this.userMetaData.id,
author: this.user.id,
id: tag,
type: type,
jid: jid,
subject: subject,
},
participants ? participants.map(str => ['participant', { jid: str }, null]) : additionalNodes,
participants ? participants.map(jid => ['participant', { jid }, null]) : additionalNodes,
]
const result = await this.setQuery ([json], [WAMetric.group, WAFlag.ignore], tag)
const result = await this.setQuery ([json], [WAMetric.group, 136], tag)
return result
}
/** Get the metadata of the group */
groupMetadata = (jid: string) => this.queryExpecting200(['query', 'GroupMetadata', jid]) as Promise<WAGroupMetadata>
groupMetadata = (jid: string) => this.query({json: ['query', 'GroupMetadata', jid], expect200: true}) as Promise<WAGroupMetadata>
/** Get the metadata (works after you've left the group also) */
groupMetadataMinimal = async (jid: string) => {
const query = ['query', {type: 'group', jid: jid, epoch: this.msgCount.toString()}, null]
const response = await this.queryExpecting200(query, [WAMetric.group, WAFlag.ignore])
const response = await this.query({json: query, binaryTags: [WAMetric.group, WAFlag.ignore], expect200: true})
const json = response[2][0]
const creatorDesc = json[1]
const participants = json[2] ? json[2].filter (item => item[0] === 'participant') : []
@@ -46,20 +46,39 @@ export class WAConnection extends Base {
* @param title like, the title of the group
* @param participants people to include in the group
*/
groupCreate = (title: string, participants: string[]) =>
this.groupQuery('create', null, title, participants) as Promise<WAGroupCreateResponse>
groupCreate = async (title: string, participants: string[]) => {
const response = await this.groupQuery('create', null, title, participants) as WAGroupCreateResponse
await this.chatAdd (response.gid, title)
return response
}
/**
* Leave a group
* @param jid the ID of the group
*/
groupLeave = (jid: string) => this.groupQuery('leave', jid) as Promise<{ status: number }>
groupLeave = async (jid: string) => {
const response = await this.groupQuery('leave', jid)
const chat = this.chats.get (jid)
if (chat) chat.read_only = 'true'
return response
}
/**
* Update the subject of the group
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
*/
groupUpdateSubject = (jid: string, title: string) =>
this.groupQuery('subject', jid, title) as Promise<{ status: number }>
groupUpdateSubject = async (jid: string, title: string) => {
const chat = this.chats.get (jid)
if (chat?.title === title) throw new Error ('redundant change')
const response = await this.groupQuery('subject', jid, title)
if (chat) {
chat.title = title
//this.emit ('chat-update', {jid, title})
}
return response
}
/**
* Update the group description
* @param {string} jid the ID of the group
@@ -72,7 +91,8 @@ export class WAConnection extends Base {
{id: generateMessageID(), prev: metadata?.descId},
Buffer.from (description, 'utf-8')
]
return this.groupQuery ('description', jid, null, null, [node])
const response = await this.groupQuery ('description', jid, null, null, [node])
return response
}
/**
* Add somebody to the group
@@ -114,7 +134,7 @@ export class WAConnection extends Base {
/** Get the invite link of the given group */
async groupInviteCode(jid: string) {
const json = ['query', 'inviteCode', jid]
const response = await this.queryExpecting200(json)
const response = await this.query({json})
return response.code as string
}
}

View File

@@ -1,6 +1,32 @@
import { WA } from '../Binary/Constants'
import { proto } from '../../WAMessage/WAMessage'
export const KEEP_ALIVE_INTERVAL_MS = 20*1000
// export the WAMessage Prototypes
export { proto as WAMessageProto }
export type WANode = WA.Node
export type WAMessage = proto.WebMessageInfo
export type WAMessageContent = proto.IMessage
export type WAContactMessage = proto.ContactMessage
export type WAMessageKey = proto.IMessageKey
export type WATextMessage = proto.ExtendedTextMessage
export type WAContextInfo = proto.IContextInfo
export import WA_MESSAGE_STUB_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STUBTYPE
export import WA_MESSAGE_STATUS_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS
export interface WALocationMessage {
degreesLatitude: number
degreesLongitude: number
address?: string
}
/** Reverse stub type dictionary */
export const WAMessageType = function () {
const types = WA_MESSAGE_STUB_TYPE
const dict: Record<number, string> = {}
Object.keys(types).forEach(element => dict[ types[element] ] = element)
return dict
}()
export class BaileysError extends Error {
status?: number
@@ -13,7 +39,25 @@ export class BaileysError extends Error {
this.context = context
}
}
export interface WAQuery {
json: any[] | WANode
binaryTags?: WATag
timeoutMs?: number
tag?: string
expect200?: boolean
waitForOpen?: boolean
}
export enum ReconnectMode {
/** does not reconnect */
off = 0,
/** reconnects only when the connection is 'lost' or 'closed' */
onConnectionLost = 1,
/** reconnects on all disconnects, including take overs */
onAllErrors = 2
}
export type WAConnectionState = 'open' | 'connecting' | 'closed'
export type DisconnectReason = 'closed' | 'lost' | 'replaced' | 'intentional'
export enum MessageLogLevel {
none=0,
info=1,
@@ -40,21 +84,14 @@ export interface AuthenticationCredentialsBrowser {
WAToken1: string
WAToken2: string
}
export interface UserMetaData {
export type AnyAuthenticationCredentials = AuthenticationCredentialsBrowser | AuthenticationCredentialsBase64 | AuthenticationCredentials
export interface WAUser {
id: string
name: string
phone: string
imgUrl: string
}
export type WANode = WA.Node
export type WAMessage = proto.WebMessageInfo
export type WAMessageContent = proto.IMessage
export enum WAConnectionMode {
/** Baileys will let requests through after a simple connect */
onlyRequireValidation = 0,
/** Baileys will let requests through only after chats & contacts are received */
requireChatsAndContacts = 1
}
export interface WAGroupCreateResponse {
status: number
gid?: string
@@ -68,6 +105,10 @@ export interface WAGroupMetadata {
desc?: string
descOwner?: string
descId?: string
/** is set when the group only allows admins to change group settings */
restrict?: 'true'
/** is set when the group only allows admins to write messages */
announce?: 'true'
participants: [{ id: string; isAdmin: boolean; isSuperAdmin: boolean }]
}
export interface WAGroupModification {
@@ -83,16 +124,22 @@ export interface WAContact {
short?: string
}
export interface WAChat {
t: string
jid: string
t: number
/** number of unread messages, is < 0 if the chat is manually marked unread */
count: number
archive?: 'true' | 'false'
read_only?: 'true' | 'false'
mute?: string
pin?: string
spam: 'false' | 'true'
jid: string
modify_tag: string
// Baileys added properties
messages: WAMessage[]
title?: string
imgUrl?: string
}
export enum WAMetric {
debugLog = 1,
@@ -133,8 +180,6 @@ export enum WAFlag {
}
/** Tag used with binary queries */
export type WATag = [WAMetric, WAFlag]
// export the WAMessage Prototype as well
export { proto as WAMessageProto } from '../../WAMessage/WAMessage'
/** set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send */
export enum Presence {
@@ -263,22 +308,21 @@ export interface WASendMessageResponse {
messageID: string
message: WAMessage
}
export interface WALocationMessage {
degreesLatitude: number
degreesLongitude: number
address?: string
}
export import WA_MESSAGE_STUB_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STUBTYPE
export import WA_MESSAGE_STATUS_TYPE = proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS
/** Reverse stub type dictionary */
export const WAMessageType = function () {
const types = WA_MESSAGE_STUB_TYPE
const dict: Record<number, string> = {}
Object.keys(types).forEach(element => dict[ types[element] ] = element)
return dict
}()
export type WAContactMessage = proto.ContactMessage
export type WAMessageKey = proto.IMessageKey
export type WATextMessage = proto.ExtendedTextMessage
export type WAContextInfo = proto.IContextInfo
export type BaileysEvent =
'open' |
'connecting' |
'closed' |
'qr' |
'connection-phone-change' |
'user-presence-update' |
'user-status-update' |
'chat-new' |
'chat-update' |
'message-new' |
'message-update' |
'group-participants-add' |
'group-participants-remove' |
'group-participants-promote' |
'group-participants-demote' |
'group-settings-update' |
'group-description-update'

View File

@@ -27,11 +27,10 @@ function hashCode(s: string) {
h = Math.imul(31, h) + s.charCodeAt(i) | 0;
return h;
}
export const waChatUniqueKey = (c: WAChat) => ((+c.t*100000) + (hashCode(c.jid)%100000))*-1 // -1 to sort descending
export const waChatUniqueKey = (c: WAChat) => ((c.t*100000) + (hashCode(c.jid)%100000))*-1 // -1 to sort descending
export const whatsappID = (jid: string) => jid?.replace ('@c.us', '@s.whatsapp.net')
export const isGroupID = (jid: string) => jid?.includes ('@g.us')
export function whatsappID (jid: string) {
return jid.replace ('@c.us', '@s.whatsapp.net')
}
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
export function aesDecrypt(buffer: Buffer, key: Buffer) {
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
@@ -67,25 +66,49 @@ export function hkdf(buffer: Buffer, expandedLength: number, info = null) {
export function randomBytes(length) {
return Crypto.randomBytes(length)
}
export const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
/** unix timestamp of a date in seconds */
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000)
export const delay = (ms: number) => delayCancellable (ms).delay
export const delayCancellable = (ms: number) => {
let timeout: NodeJS.Timeout
let reject: (error) => void
const delay: Promise<void> = new Promise((resolve, _reject) => {
timeout = setTimeout(resolve, ms)
reject = _reject
})
const cancel = () => {
clearTimeout (timeout)
reject (new Error('cancelled'))
}
return { delay, cancel }
}
export async function promiseTimeout<T>(ms: number, promise: (resolve: (v?: T)=>void, reject: (error) => void) => void) {
if (!ms) return new Promise (promise)
export async function promiseTimeout<T>(ms: number, promise: Promise<T>) {
if (!ms) return promise
// Create a promise that rejects in <ms> milliseconds
let timeoutI
const timeout = new Promise(
(_, reject) => timeoutI = setTimeout(() => reject(new BaileysError ('Timed out', promise)), ms)
)
const {delay, cancel} = delayCancellable (ms)
let pReject: (error) => void
const p = new Promise ((resolve, reject) => {
promise (resolve, reject)
pReject = reject
})
try {
const content = await Promise.race([promise, timeout])
const content = await Promise.race([
p,
delay.then(() => pReject(new BaileysError('timed out', p)))
])
cancel ()
return content as T
} finally {
clearTimeout (timeoutI)
cancel ()
}
}
// whatsapp requires a message tag for every message, we just use the timestamp as one
export function generateMessageTag(epoch?: number) {
let tag = Math.round(new Date().getTime()/1000).toString()
let tag = unixTimestampSeconds().toString()
if (epoch) tag += '.--' + epoch // attach epoch if provided
return tag
}

View File

@@ -1,3 +1,3 @@
export * from './6.Groups'
export * from './8.Groups'
export * from './Utils'
export * from './Constants'