Got rid of WAClient, deprecated code. Prep for V3

Layered classes based on hierarchy as well.
This commit is contained in:
Adhiraj
2020-08-16 17:51:29 +05:30
parent 1360bef9bb
commit 002d304041
23 changed files with 803 additions and 869 deletions

View File

@@ -1,40 +1,39 @@
import {
WAClient,
WAConnection,
MessageType,
decodeMediaMessage,
Presence,
MessageOptions,
Mimetype,
WALocationMessage,
MessageLogLevel,
WAMessageType,
} from '../src/WAClient/WAClient'
} from '../src/WAConnection/WAConnection'
import * as fs from 'fs'
async function example() {
const client = new WAClient() // instantiate
client.autoReconnect = true // auto reconnect on disconnect
client.logLevel = MessageLogLevel.info // set to unhandled to see what kind of stuff you can implement
const conn = new WAConnection() // instantiate
conn.autoReconnect = true // auto reconnect on disconnect
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 client.connect('./auth_info.json', 20 * 1000)
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))
console.log('oh hello ' + user.name + ' (' + user.id + ')')
console.log('you have ' + chats.all().length + ' chats & ' + contacts.length + ' contacts')
console.log ('you have ' + unread.length + ' unread messages')
const authInfo = client.base64EncodedAuthInfo() // get all the auth info we need to restore this session
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 */
client.setOnPresenceUpdate(json => console.log(json.id + ' presence is ' + json.type))
client.setOnMessageStatusChange(json => {
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}`)
})
// set to false to NOT relay your own sent messages
client.setOnUnreadMessage(true, async (m) => {
conn.setOnUnreadMessage(true, async (m) => {
const messageStubType = WAMessageType[m.messageStubType] || 'MESSAGE'
console.log('got notification of type: ' + messageStubType)
@@ -65,7 +64,7 @@ async function example() {
const locMessage = m.message[messageType] as WALocationMessage
console.log(`${sender} sent location (lat: ${locMessage.degreesLatitude}, long: ${locMessage.degreesLongitude})`)
decodeMediaMessage(m.message, './Media/media_loc_thumb_in_' + m.key.id) // save location thumbnail
await conn.downloadAndSaveMediaMessage(m, './Media/media_loc_thumb_in_' + m.key.id) // save location thumbnail
if (messageType === MessageType.liveLocation) {
console.log(`${sender} sent live location for duration: ${m.duration/60}`)
@@ -75,7 +74,7 @@ async function example() {
// decode, decrypt & save the media.
// The extension to the is applied automatically based on the media type
try {
const savedFile = await decodeMediaMessage(m.message, './Media/media_in_' + m.key.id)
const savedFile = await conn.downloadAndSaveMediaMessage(m, './Media/media_in_' + m.key.id)
console.log(sender + ' sent media, saved at: ' + savedFile)
} catch (err) {
console.log('error in decoding message: ' + err)
@@ -83,20 +82,18 @@ async function example() {
}
// send a reply after 3 seconds
setTimeout(async () => {
await client.sendReadReceipt(m.key.remoteJid, m.key.id) // send read receipt
await client.updatePresence(m.key.remoteJid, Presence.available) // tell them we're available
await client.updatePresence(m.key.remoteJid, Presence.composing) // tell them we're composing
await conn.sendReadReceipt(m.key.remoteJid, m.key.id) // send read receipt
await conn.updatePresence(m.key.remoteJid, Presence.available) // tell them we're available
await conn.updatePresence(m.key.remoteJid, Presence.composing) // tell them we're composing
const options: MessageOptions = { quoted: m }
let content
let type: MessageType
const rand = Math.random()
if (rand > 0.66) {
// choose at random
if (rand > 0.66) { // choose at random
content = 'hello!' // send a "hello!" & quote the message recieved
type = MessageType.text
} else if (rand > 0.33) {
// choose at random
} else if (rand > 0.33) { // choose at random
content = { degreesLatitude: 32.123123, degreesLongitude: 12.12123123 }
type = MessageType.location
} else {
@@ -104,21 +101,21 @@ async function example() {
options.mimetype = Mimetype.gif
type = MessageType.video
}
const response = await client.sendMessage(m.key.remoteJid, content, type, options)
console.log("sent message with ID '" + response.messageID + "' successfully: " + (response.status === 200))
const response = await conn.sendMessage(m.key.remoteJid, content, type, options)
console.log("sent message with ID '" + response.key.id + "' successfully: " + (response.status === 200))
}, 3 * 1000)
})
/* example of custom functionality for tracking battery */
client.registerCallback(['action', null, 'battery'], json => {
conn.registerCallback(['action', null, 'battery'], json => {
const batteryLevelStr = json[2][0][1].value
const batterylevel = parseInt(batteryLevelStr)
console.log('battery level: ' + batterylevel)
})
client.setOnUnexpectedDisconnect(reason => {
conn.setOnUnexpectedDisconnect(reason => {
if (reason === 'replaced') {
// uncomment to reconnect whenever the connection gets taken over from somewhere else
// await client.connect ()
// await conn.connect ()
} else {
console.log ('oh no got disconnected: ' + reason)
}

150
README.md
View File

@@ -21,11 +21,11 @@ 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`
2. stabl-ish w quicker fixes & latest features: `npm install github:adiwajshing/baileys` (major changes incoming right now)
Then import in your code using:
``` ts
import { WAClient } from '@adiwajshing/baileys'
import { WAConnection } from '@adiwajshing/baileys'
```
## Unit Tests
@@ -36,11 +36,11 @@ Set the phone number you can randomly send messages to in a `.env` file with `TE
## Connecting
``` ts
import { WAClient } from '@adiwajshing/baileys'
import { WAConnection } from '@adiwajshing/baileys'
async function connectToWhatsApp () {
const client = new WAClient()
const [user, chats, contacts] = await client.connect ()
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")
@@ -61,14 +61,14 @@ 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 { WAClient } from '@adiwajshing/baileys'
import { WAConnection } from '@adiwajshing/baileys'
async function connectToWhatsApp () {
const client = new WAClient()
const user = await client.connectSlim ()
const conn = new WAConnection()
const user = await conn.connectSlim ()
console.log ("oh hello " + user.name + " (" + user.id + ")")
client.receiveChatsAndContacts () // wait for chats & contacts in the background
conn.receiveChatsAndContacts () // wait for chats & contacts in the background
.then (([chats, contacts]) => {
console.log ("you have " + chats.all().length + " chats and " + contacts.length + " contacts")
})
@@ -91,18 +91,18 @@ So, do the following the first time you connect:
``` ts
import * as fs from 'fs'
const client = new WAClient()
client.connectSlim() // connect first
const conn = new WAConnection()
conn.connectSlim() // connect first
.then (user => {
const creds = client.base64EncodedAuthInfo () // contains all the keys you need to restore a session
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 client = new WAClient()
client.connectSlim('./auth_info.json') // will load JSON credentials from file
const conn = new WAConnection()
conn.connectSlim('./auth_info.json') // will load JSON credentials from file
.then (user => {
// yay connected without scanning QR
})
@@ -115,8 +115,8 @@ client.connectSlim('./auth_info.json') // will load JSON credentials from file
If you're considering switching from a Chromium/Puppeteer based library, you can use WhatsApp Web's Browser credentials to restore sessions too:
``` ts
client.loadAuthInfoFromBrowser ('./auth_info_browser.json')
client.connectSlim(null, 20*1000) // use loaded credentials & timeout in 20s
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
})
@@ -127,25 +127,25 @@ See the browser credentials type [here](/src/WAConnection/Constants.ts).
If you want to do some custom processing with the QR code used to authenticate, you can override the following method:
``` ts
client.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
conn.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
const str = ref + ',' + publicKey + ',' + clientID // the QR string
// Now, use 'str' to display in QR UI or send somewhere
}
const user = await client.connect ()
const user = await conn.connect ()
```
If you need to regenerate the QR, you can also do so using:
``` ts
let generateQR: async () => void // call generateQR on some timeout or error
client.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
conn.onReadyForPhoneAuthentication = ([ref, publicKey, clientID]) => {
generateQR = async () => {
ref = await client.generateNewQRCode () // returns a new ref code to use for QR generation
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 client.connect ()
const user = await conn.connect ()
```
## Handling Events
@@ -155,7 +155,7 @@ Implement the following callbacks in your code:
``` 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
client.setOnUnreadMessage (false, (m: WAMessage) => {
conn.setOnUnreadMessage (false, (m: WAMessage) => {
// get what type of notification it is -- message, group add notification etc.
const [notificationType, messageType] = getNotificationType(m)
@@ -165,11 +165,11 @@ Implement the following callbacks in your code:
```
- Called when you recieve an update on someone's presence, they went offline or online
``` ts
client.setOnPresenceUpdate ((json: PresenceUpdate) => console.log(json.id + " presence is " + json.type))
conn.setOnPresenceUpdate ((json: PresenceUpdate) => console.log(json.id + " presence is " + json.type))
```
- Called when your message gets delivered or read
``` ts
client.setOnMessageStatusChange ((json: MessageStatusUpdate) => {
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
@@ -179,7 +179,7 @@ Implement the following callbacks in your code:
```
- Called when the connection gets disconnected (either the server loses internet, the phone gets unpaired, or the connection is taken over from somewhere)
``` ts
client.setOnUnexpectedDisconnect (reason => console.log ("disconnected unexpectedly: " + reason) )
conn.setOnUnexpectedDisconnect (reason => console.log ("disconnected unexpectedly: " + reason) )
```
## Sending Messages
@@ -189,9 +189,9 @@ import { MessageType, MessageOptions, Mimetype } from '@adiwajshing/baileys'
const id = 'abcd@s.whatsapp.net' // the WhatsApp ID
// send a simple text!
client.sendMessage (id, 'oh hello there', MessageType.text)
conn.sendMessage (id, 'oh hello there', MessageType.text)
// send a location!
client.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'
@@ -199,11 +199,11 @@ const vcard = 'BEGIN:VCARD\n' // metadata of the contact card
+ 'ORG:Ashoka Uni;\n' // the organization of the contact
+ 'TEL;type=CELL;type=VOICE;waid=911234567890:+91 12345 67890\n' // WhatsApp ID + phone number
+ 'END:VCARD'
client.sendMessage(id, {displayname: "Jeff", vcard: vcard}, MessageType.contact)
conn.sendMessage(id, {displayname: "Jeff", vcard: vcard}, MessageType.contact)
// send a gif
const buffer = fs.readFileSync("Media/ma_gif.mp4") // load some gif
const options: MessageOptions = {mimetype: Mimetype.gif, caption: "hello!"} // some metadata & caption
client.sendMessage(id, buffer, MessageType.video, options)
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.
@@ -233,9 +233,9 @@ To note:
## Forwarding Messages
``` ts
const messages = await client.loadConversation ('1234@s.whatsapp.net', 1)
const messages = await conn.loadConversation ('1234@s.whatsapp.net', 1)
const message = messages[0] // get the last message from this conversation
await client.forwardMessage ('455@s.whatsapp.net', message) // WA forward the message!
await conn.forwardMessage ('455@s.whatsapp.net', message) // WA forward the message!
```
## Reading Messages
@@ -243,10 +243,10 @@ await client.forwardMessage ('455@s.whatsapp.net', message) // WA forward the me
const id = '1234-123@g.us'
const messageID = 'AHASHH123123AHGA' // id of the message you want to read
await client.sendReadReceipt(id, messageID) // mark as read
await client.sendReadReceipt (id) // mark all messages in chat as read
await conn.sendReadReceipt(id, messageID) // mark as read
await conn.sendReadReceipt (id) // mark all messages in chat as read
await client.sendReadReceipt(id, null, 'unread') // mark the chat as unread
await conn.sendReadReceipt(id, null, 'unread') // mark the chat as unread
```
- `id` is in the same format as mentioned earlier.
@@ -258,7 +258,7 @@ await client.sendReadReceipt(id, null, 'unread') // mark the chat as unread
``` ts
import { Presence } from '@adiwajshing/baileys'
client.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
@@ -275,15 +275,15 @@ export enum Presence {
If you want to save the media you received
``` ts
import { MessageType, extensionForMediaMessage } from '@adiwajshing/baileys'
client.setOnUnreadMessage (false, async m => {
conn.setOnUnreadMessage (false, 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) {
const buffer = await client.downloadMediaMessage(m) // to decrypt & use as a buffer
const buffer = await conn.downloadMediaMessage(m) // to decrypt & use as a buffer
const savedFilename = await client.downloadAndSaveMediaMessage (m) // to decrypt & save to file
const savedFilename = await conn.downloadAndSaveMediaMessage (m) // to decrypt & save to file
console.log(m.key.remoteJid + " sent media, saved at: " + savedFilename)
}
}
@@ -293,29 +293,29 @@ client.setOnUnreadMessage (false, async m => {
``` ts
const jid = '1234@s.whatsapp.net' // can also be a group
const response = await client.sendMessage (jid, 'hello!', MessageType.text) // send a message
const response = await conn.sendMessage (jid, 'hello!', MessageType.text) // send a message
await client.deleteMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message for everyone!
await client.clearMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message for only you!
await conn.deleteMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message for everyone!
await conn.clearMessage (jid, {id: response.messageID, remoteJid: jid, fromMe: true}) // will delete the sent message for only you!
```
## Modifying Chats
``` ts
const jid = '1234@s.whatsapp.net' // can also be a group
await client.modifyChat (jid, ChatModification.archive) // archive chat
await client.modifyChat (jid, ChatModification.unarchive) // unarchive chat
await conn.modifyChat (jid, ChatModification.archive) // archive chat
await conn.modifyChat (jid, ChatModification.unarchive) // unarchive chat
const response = await client.modifyChat (jid, ChatModification.pin) // pin the chat
await client.modifyChat (jid, ChatModification.unpin, {stamp: response.stamp})
const response = await conn.modifyChat (jid, ChatModification.pin) // pin the chat
await conn.modifyChat (jid, ChatModification.unpin, {stamp: response.stamp})
const mutedate = new Date (new Date().getTime() + 8*60*60*1000) // mute for 8 hours in the future
await client.modifyChat (jid, ChatModification.mute, {stamp: mutedate}) // mute
await conn.modifyChat (jid, ChatModification.mute, {stamp: mutedate}) // mute
setTimeout (() => {
client.modifyChat (jid, ChatModification.unmute, {stamp: mutedate})
conn.modifyChat (jid, ChatModification.unmute, {stamp: mutedate})
}, 5000) // unmute after 5 seconds
await client.deleteChat (jid) // will delete the chat (can be a group or broadcast list)
await conn.deleteChat (jid) // will delete the chat (can be a group or broadcast list)
```
**Note:** to unmute or unpin a chat, one must pass the timestamp of the pinning or muting. This is returned by the pin & mute functions. This is also available in the `WAChat` objects of the respective chats, as a `mute` or `pin` property.
@@ -325,48 +325,48 @@ await client.deleteChat (jid) // will delete the chat (can be a group or broadca
- To check if a given ID is on WhatsApp
``` ts
const id = 'xyz@s.whatsapp.net'
const exists = await client.isOnWhatsApp (id)
const exists = await conn.isOnWhatsApp (id)
console.log (`${id} ${exists ? " exists " : " does not exist"} on WhatsApp`)
```
- 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 client.loadConversation ("xyz-abc@g.us", 25)
const messages = await conn.loadConversation ("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 client.loadEntireConversation ("xyz@c.us", message => console.log("Loaded message with ID: " + message.key.id))
await conn.loadEntireConversation ("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
``` ts
const status = await client.getStatus ("xyz@c.us") // leave empty to get your own status
const status = await conn.getStatus ("xyz@c.us") // leave empty to get your own status
console.log("status: " + status)
```
- To get the display picture of some person/group
``` ts
const ppUrl = await client.getProfilePicture ("xyz@g.us") // leave empty to get your own
const ppUrl = await conn.getProfilePicture ("xyz@g.us") // leave empty to get your own
console.log("download profile picture from: " + ppUrl)
```
- To change your display picture or a group's
``` 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 client.updateProfilePicture (jid, newPP)
await conn.updateProfilePicture (jid, newPP)
```
- To get someone's presence (if they're typing, online)
``` ts
// the presence update is fetched and called here
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
await client.requestPresenceUpdate ("xyz@c.us") // request the update
conn.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
await conn.requestPresenceUpdate ("xyz@c.us") // request the update
```
- To search through messages
``` ts
const response = await client.searchMessages ('so cool', null, 25, 1) // search in all chats
const response = await conn.searchMessages ('so cool', null, 25, 1) // search in all chats
console.log (`got ${response.messages.length} messages in search`)
const response2 = await client.searchMessages ('so cool', '1234@c.us', 25, 1) // search in given chat
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.
@@ -375,47 +375,47 @@ Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
- To create a group
``` ts
// title & participants
const group = await client.groupCreate ("My Fab Group", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
const group = await conn.groupCreate ("My Fab Group", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
console.log ("created group with id: " + group.gid)
client.sendTextMessage(group.gid, "hello everyone") // say hello to everyone on the group
conn.sendMessage(group.gid, "hello everyone", MessageType.extendedText) // say hello to everyone on the group
```
- To add people to a group
``` ts
// id & people to add to the group (will throw error if it fails)
const response = await client.groupAdd ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
const response = await conn.groupAdd ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
```
- To make/demote admins on a group
``` ts
// id & people to make admin (will throw error if it fails)
await client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
await client.groupDemoteAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // demote admins
await conn.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"])
await conn.groupDemoteAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // demote admins
```
- To change group settings
``` ts
import { GroupSettingChange } from '@adiwajshing/baileys'
// only allow admins to send messages
await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.messageSend, true)
await conn.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.messageSend, true)
// allow everyone to modify the group's settings -- like display picture etc.
await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, false)
await conn.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, false)
// only allow admins to modify the group's settings
await client.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, true)
await conn.groupSettingChange ("abcd-xyz@g.us", GroupSettingChange.settingChange, true)
```
- To leave a group
``` ts
await client.groupLeave ("abcd-xyz@g.us") // (will throw error if it fails)
await conn.groupLeave ("abcd-xyz@g.us") // (will throw error if it fails)
```
- To get the invite code for a group
``` ts
const code = await client.groupInviteCode ("abcd-xyz@g.us")
const code = await conn.groupInviteCode ("abcd-xyz@g.us")
console.log("group code: " + code)
```
- To query the metadata of a group
``` ts
const metadata = await client.groupMetadata ("abcd-xyz@g.us")
const metadata = await conn.groupMetadata ("abcd-xyz@g.us")
console.log(json.id + ", title: " + json.subject + ", description: " + json.desc)
// Or if you've left the group -- call this
const metadata2 = await client.groupMetadataMinimal ("abcd-xyz@g.us")
const metadata2 = await conn.groupMetadataMinimal ("abcd-xyz@g.us")
```
## Broadcast Lists & Stories
@@ -425,7 +425,7 @@ Append ``` @s.whatsapp.net ``` for individuals & ``` @g.us ``` for groups.
- Broadcast IDs are in the format `12345678@broadcast`
- To query a broadcast list's recipients & name:
``` ts
const bList = await client.getBroadcastListInfo ("1234@broadcast")
const bList = await conn.getBroadcastListInfo ("1234@broadcast")
console.log (`list name: ${bList.name}, recps: ${bList.recipients}`)
```
@@ -434,7 +434,7 @@ Baileys is written, keeping in mind, that you may require other custom functiona
First, enable the logging of unhandled messages from WhatsApp by setting
``` ts
client.logLevel = MessageLogLevel.unhandled // set to MessageLogLevel.all to see all messages received
conn.logLevel = MessageLogLevel.unhandled // set to MessageLogLevel.all to see all messages received
```
This will enable you to see all sorts of messages WhatsApp sends in the console. Some examples:
@@ -450,7 +450,7 @@ This will enable you to see all sorts of messages WhatsApp sends in the console.
Hence, you can register a callback for an event using the following:
``` ts
client.registerCallback (["action", null, "battery"], json => {
conn.registerCallback (["action", null, "battery"], json => {
const batteryLevelStr = json[2][0][1].value
const batterylevel = parseInt (batteryLevelStr)
console.log ("battery level: " + batterylevel + "%")
@@ -470,9 +470,9 @@ This will enable you to see all sorts of messages WhatsApp sends in the console.
Following this, one can implement the following callback:
``` ts
client.registerCallback (["Conn", "pushname"], json => {
conn.registerCallback (["Conn", "pushname"], json => {
const pushname = json[1].pushname
client.userMetaData.name = pushname // update on client too
conn.userMetaData.name = pushname // update on client too
console.log ("Name updated: " + pushname)
})
```

View File

@@ -1,10 +1,10 @@
{
"name": "@adiwajshing/baileys",
"version": "2.3.1",
"version": "3.0.0",
"description": "WhatsApp Web API",
"homepage": "https://github.com/adiwajshing/Baileys",
"main": "lib/WAClient/WAClient.js",
"types": "lib/WAClient/WAClient.d.ts",
"main": "lib/WAConnection/WAConnection.js",
"types": "lib/WAConnection/WAConnection.d.ts",
"keywords": [
"whatsapp",
"js-whatsapp",
@@ -18,12 +18,12 @@
],
"scripts": {
"prepare": "npm run build",
"test": "mocha --timeout 60000 -r ts-node/register src/*/Tests.ts",
"test": "mocha --timeout 60000 -r ts-node/register src/Tests/Tests.*.ts",
"lint": "eslint '*/*.ts' --quiet --fix",
"build": "tsc",
"build:docs": "typedoc",
"example": "npx ts-node Example/example.ts",
"browser-decode": "npx ts-node src/WAConnection/BrowserMessageDecoding.ts"
"browser-decode": "npx ts-node src/BrowserMessageDecoding.ts"
},
"author": "Adhiraj Singh",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
import fs from 'fs'
import { decryptWA } from './Utils'
import Decoder from '../Binary/Decoder'
import { decryptWA } from './WAConnection/WAConnection'
import Decoder from './Binary/Decoder'
interface BrowserMessagesInfo {
encKey: string,

28
src/Tests/Common.ts Normal file
View File

@@ -0,0 +1,28 @@
import { WAConnection, MessageLogLevel, MessageOptions, MessageType } from '../WAConnection/WAConnection'
import * as assert from 'assert'
import fs from 'fs/promises'
require ('dotenv').config () // dotenv to load test jid
export const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
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 message = messages.find (m => m.key.id === response.key.id)
assert.ok(message)
return message
}
export function WAConnectionTest(name: string, func: (conn: WAConnection) => void) {
describe(name, () => {
const conn = new WAConnection()
conn.logLevel = MessageLogLevel.info
before(async () => {
const file = './auth_info.json'
await conn.connectSlim(file)
await fs.writeFile(file, JSON.stringify(conn.base64EncodedAuthInfo(), null, '\t'))
})
after(() => conn.close())
func(conn)
})
}

View File

@@ -1,6 +1,6 @@
import { strict as assert } from 'assert'
import Encoder from './Encoder'
import Decoder from './Decoder'
import Encoder from '../Binary/Encoder'
import Decoder from '../Binary/Decoder'
describe('Binary Coding Tests', () => {
const testVectors: [string, Object][] = [

View File

@@ -1,10 +1,10 @@
import * as assert from 'assert'
import * as QR from 'qrcode-terminal'
import WAConnection from './WAConnection'
import { AuthenticationCredentialsBase64 } from './Constants'
import { createTimeout } from './Utils'
import {WAConnection} from '../WAConnection/WAConnection'
import { AuthenticationCredentialsBase64 } from '../WAConnection/Constants'
import { createTimeout } from '../WAConnection/Utils'
describe('QR generation', () => {
describe('QR Generation', () => {
it('should generate QR', async () => {
const conn = new WAConnection()
let calledQR = false

59
src/Tests/Tests.Groups.ts Normal file
View File

@@ -0,0 +1,59 @@
import { MessageType, GroupSettingChange, createTimeout, ChatModification } from '../WAConnection/WAConnection'
import * as assert from 'assert'
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])
gid = response.gid
console.log('created group: ' + JSON.stringify(response))
})
it('should retreive group invite code', async () => {
const code = await conn.groupInviteCode(gid)
assert.ok(code)
assert.strictEqual(typeof code, 'string')
})
it('should retreive group metadata', async () => {
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.id, gid)
assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1)
})
it('should update the group description', async () => {
const newDesc = 'Wow this was set from Baileys'
await conn.groupUpdateDescription (gid, newDesc)
await createTimeout (1000)
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.desc, newDesc)
})
it('should send a message on the group', async () => {
await conn.sendMessage(gid, 'hello', MessageType.text)
})
it('should update the subject', async () => {
const subject = 'V Cool Title'
await conn.groupUpdateSubject(gid, subject)
const metadata = await conn.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject)
})
it('should update the group settings', async () => {
await conn.groupSettingChange (gid, GroupSettingChange.messageSend, true)
await createTimeout (5000)
await conn.groupSettingChange (gid, GroupSettingChange.settingsChange, true)
})
it('should remove someone from a group', async () => {
await conn.groupRemove(gid, [testJid])
})
it('should leave the group', async () => {
await conn.groupLeave(gid)
await conn.groupMetadataMinimal (gid)
})
it('should archive the group', async () => {
await conn.modifyChat(gid, ChatModification.archive)
})
it('should delete the group', async () => {
await conn.deleteChat(gid)
})
})

View File

@@ -0,0 +1,68 @@
import { MessageType, Mimetype, createTimeout } from '../WAConnection/WAConnection'
import fs from 'fs/promises'
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')
})
it('should forward a message', async () => {
let messages = await conn.loadConversation (testJid, 1)
await conn.forwardMessage (testJid, messages[0], true)
messages = await conn.loadConversation (testJid, 1)
const message = messages[0]
const content = message.message[ Object.keys(message.message)[0] ]
assert.equal (content?.contextInfo?.isForwarded, true)
})
it('should send a link preview', async () => {
const content = await conn.generateLinkPreview ('hello this is from https://www.github.com/adiwajshing/Baileys')
const message = await sendAndRetreiveMessage(conn, content, MessageType.text)
const received = message.message.extendedTextMessage
assert.strictEqual(received.text, content.text)
assert.ok (received.canonicalUrl)
assert.ok (received.title)
assert.ok (received.jpegThumbnail)
})
it('should quote a message', async () => {
const messages = await conn.loadConversation(testJid, 2)
const message = await sendAndRetreiveMessage(conn, 'hello fren 2', MessageType.extendedText, {
quoted: messages[0],
})
assert.strictEqual(message.message.extendedTextMessage.contextInfo.stanzaId, messages[0].key.id)
})
it('should send a gif', async () => {
const content = await fs.readFile('./Media/ma_gif.mp4')
const message = await sendAndRetreiveMessage(conn, content, MessageType.video, { mimetype: Mimetype.gif })
await conn.downloadAndSaveMediaMessage(message,'./Media/received_vid')
})
it('should send an image', async () => {
const content = await fs.readFile('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(conn, content, MessageType.image)
await conn.downloadMediaMessage(message)
//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 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 () => {
const message = await sendAndRetreiveMessage(conn, 'hello fren', MessageType.text)
await createTimeout (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)
await conn.clearMessage (messages[0].key)
})
})

113
src/Tests/Tests.Misc.ts Normal file
View File

@@ -0,0 +1,113 @@
import { MessageType, Presence, ChatModification, promiseTimeout, createTimeout } from '../WAConnection/WAConnection'
import fs from 'fs/promises'
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)
assert.strictEqual(response, true)
const responseFail = await conn.isOnWhatsApp('abcd@s.whatsapp.net')
assert.strictEqual(responseFail, false)
})
it('should return the status', async () => {
const response = await conn.getStatus(testJid)
assert.strictEqual(typeof response.status, 'string')
})
it('should update status', async () => {
const newStatus = 'v cool status'
const response = await conn.getStatus()
assert.strictEqual(typeof response.status, 'string')
await createTimeout (1000)
await conn.setStatus (newStatus)
const response2 = await conn.getStatus()
assert.equal (response2.status, newStatus)
await createTimeout (1000)
await conn.setStatus (response.status) // update back
})
it('should return the stories', async () => {
await conn.getStories()
})
it('should change the profile picture', async () => {
await createTimeout (5000)
const ppUrl = await conn.getProfilePicture(conn.userMetaData.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)
await createTimeout (10000)
await conn.updateProfilePicture (conn.userMetaData.id, buff) // revert back
})
it('should return the profile picture', async () => {
const response = await conn.getProfilePicture(testJid)
assert.ok(response)
assert.rejects(conn.getProfilePicture('abcd@s.whatsapp.net'))
})
it('should send typing indicator', async () => {
const response = await conn.updatePresence(testJid, Presence.composing)
assert.ok(response)
})
it('should mark a chat unread', async () => {
await conn.sendReadReceipt(testJid, null, 'unread')
})
it('should archive & unarchive', async () => {
await conn.modifyChat (testJid, ChatModification.archive)
await createTimeout (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 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 conn.modifyChat (testJid, ChatModification.unmute, {stamp: mutedate})
})
it('should return search results', async () => {
const jids = [null, testJid]
for (let i in jids) {
const response = await conn.searchMessages('Hello', jids[i], 25, 1)
assert.ok (response.messages)
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())
})
})

View File

@@ -1,155 +0,0 @@
import { WAMessage } from '../WAConnection/Constants'
import { proto } from '../../WAMessage/WAMessage'
/**
* set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send
*/
export enum Presence {
available = 'available', // "online"
unavailable = 'unavailable', // "offline"
composing = 'composing', // "typing..."
recording = 'recording', // "recording..."
paused = 'paused', // I have no clue
}
/**
* Status of a message sent or received
*/
export enum MessageStatus {
sent = 'sent',
received = 'received',
read = 'read',
}
/**
* set of message types that are supported by the library
*/
export enum MessageType {
text = 'conversation',
extendedText = 'extendedTextMessage',
contact = 'contactMessage',
location = 'locationMessage',
liveLocation = 'liveLocationMessage',
image = 'imageMessage',
video = 'videoMessage',
sticker = 'stickerMessage',
document = 'documentMessage',
audio = 'audioMessage',
product = 'productMessage'
}
export enum ChatModification {
archive='archive',
unarchive='unarchive',
pin='pin',
unpin='unpin',
mute='mute',
unmute='unmute'
}
export const HKDFInfoKeys = {
[MessageType.image]: 'WhatsApp Image Keys',
[MessageType.audio]: 'WhatsApp Audio Keys',
[MessageType.video]: 'WhatsApp Video Keys',
[MessageType.document]: 'WhatsApp Document Keys',
[MessageType.sticker]: 'WhatsApp Image Keys'
}
export enum Mimetype {
jpeg = 'image/jpeg',
png = 'image/png',
mp4 = 'video/mp4',
gif = 'video/gif',
pdf = 'application/pdf',
ogg = 'audio/ogg; codecs=opus',
/** for stickers */
webp = 'image/webp',
}
export interface MessageOptions {
quoted?: WAMessage
contextInfo?: WAContextInfo
timestamp?: Date
caption?: string
thumbnail?: string
mimetype?: Mimetype | string
validateID?: boolean,
filename?: string
}
export interface WABroadcastListInfo {
status: number
name: string
recipients?: {id: string}[]
}
export interface WAUrlInfo {
'canonical-url': string
'matched-text': string
title: string
description: string
jpegThumbnail?: Buffer
}
export interface WAProfilePictureChange {
status: number
tag: string
eurl: string
}
export interface MessageInfo {
reads: {jid: string, t: string}[]
deliveries: {jid: string, t: string}[]
}
export interface MessageStatusUpdate {
from: string
to: string
/** Which participant caused the update (only for groups) */
participant?: string
timestamp: Date
/** Message IDs read/delivered */
ids: string[]
/** Status of the Message IDs */
type: WA_MESSAGE_STATUS_TYPE
}
export enum GroupSettingChange {
messageSend = 'announcement',
settingsChange = 'locked',
}
export interface PresenceUpdate {
id: string
participant?: string
t?: string
type?: Presence
deny?: boolean
}
// path to upload the media
export const MediaPathMap = {
imageMessage: '/mms/image',
videoMessage: '/mms/video',
documentMessage: '/mms/document',
audioMessage: '/mms/audio',
stickerMessage: '/mms/image',
}
// gives WhatsApp info to process the media
export const MimetypeMap = {
imageMessage: Mimetype.jpeg,
videoMessage: Mimetype.mp4,
documentMessage: Mimetype.pdf,
audioMessage: Mimetype.ogg,
stickerMessage: Mimetype.webp,
}
export interface WASendMessageResponse {
status: number
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

View File

@@ -1,275 +0,0 @@
import { WAClient } from './WAClient'
import { MessageType, MessageOptions, Mimetype, Presence, ChatModification, GroupSettingChange } from './Constants'
import * as fs from 'fs'
import * as assert from 'assert'
import fetch from 'node-fetch'
import { decodeMediaMessage, validateJIDForSending } from './Utils'
import { promiseTimeout, createTimeout, Browsers, generateMessageTag } from '../WAConnection/Utils'
import { MessageLogLevel } from '../WAConnection/Constants'
require ('dotenv').config () // dotenv to load test jid
const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
const response = await client.sendMessage(testJid, content, type, options)
const messages = await client.loadConversation(testJid, 1, null, true)
assert.strictEqual(messages[0].key.id, response.messageID)
return messages[0]
}
function WAClientTest(name: string, func: (client: WAClient) => void) {
describe(name, () => {
const client = new WAClient()
client.logLevel = MessageLogLevel.info
before(async () => {
const file = './auth_info.json'
await client.connectSlim(file)
fs.writeFileSync(file, JSON.stringify(client.base64EncodedAuthInfo(), null, '\t'))
})
after(() => client.close())
func(client)
})
}
WAClientTest('Messages', (client) => {
it('should send a text message', async () => {
const message = await sendAndRetreiveMessage(client, 'hello fren', MessageType.text)
assert.strictEqual(message.message.conversation, 'hello fren')
})
it('should forward a message', async () => {
let messages = await client.loadConversation (testJid, 1)
await client.forwardMessage (testJid, messages[0])
messages = await client.loadConversation (testJid, 1)
const message = messages[0]
const content = message.message[ Object.keys(message.message)[0] ]
assert.equal (content?.contextInfo?.isForwarded, true)
})
it('should send a link preview', async () => {
const content = await client.generateLinkPreview ('hello this is from https://www.github.com/adiwajshing/Baileys')
const message = await sendAndRetreiveMessage(client, content, MessageType.text)
const received = message.message.extendedTextMessage
assert.strictEqual(received.text, content.text)
fs.writeFileSync ('Media/received-thumb.jpeg', content.jpegThumbnail)
})
it('should quote a message', async () => {
const messages = await client.loadConversation(testJid, 2)
const message = await sendAndRetreiveMessage(client, 'hello fren 2', MessageType.extendedText, {
quoted: messages[0],
})
assert.strictEqual(message.message.extendedTextMessage.contextInfo.stanzaId, messages[0].key.id)
})
it('should send a gif', async () => {
const content = fs.readFileSync('./Media/ma_gif.mp4')
const message = await sendAndRetreiveMessage(client, content, MessageType.video, { mimetype: Mimetype.gif })
await client.downloadAndSaveMediaMessage(message,'./Media/received_vid')
})
it('should send an image', async () => {
const content = fs.readFileSync('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(client, content, MessageType.image)
const file = await decodeMediaMessage(message.message, './Media/received_img')
//const message2 = await sendAndRetreiveMessage (client, 'this is a quote', MessageType.extendedText)
})
it('should send an image & quote', async () => {
const messages = await client.loadConversation(testJid, 1)
const content = fs.readFileSync('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(client, content, MessageType.image, { quoted: messages[0] })
const file = await decodeMediaMessage(message.message, './Media/received_img')
assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id)
})
it('should send a text message & delete it', async () => {
const message = await sendAndRetreiveMessage(client, 'hello fren', MessageType.text)
await createTimeout (2000)
await client.deleteMessage (testJid, message.key)
})
it('should clear the most recent message', async () => {
const messages = await client.loadConversation (testJid, 1)
await createTimeout (2000)
await client.clearMessage (messages[0].key)
})
})
describe('Validate WhatsApp IDs', () => {
it ('should correctly validate', () => {
assert.doesNotThrow (() => validateJIDForSending ('12345@s.whatsapp.net'))
assert.doesNotThrow (() => validateJIDForSending ('919999999999@s.whatsapp.net'))
assert.doesNotThrow (() => validateJIDForSending ('10203040506@s.whatsapp.net'))
assert.doesNotThrow (() => validateJIDForSending ('12345-3478@g.us'))
assert.doesNotThrow (() => validateJIDForSending ('1234567890-34712121238@g.us'))
assert.throws (() => validateJIDForSending ('123454677@c.us'))
assert.throws (() => validateJIDForSending ('+123454677@s.whatsapp.net'))
assert.throws (() => validateJIDForSending ('+12345-3478@g.us'))
})
})
WAClientTest('Presence', (client) => {
it('should update presence', async () => {
const presences = Object.values(Presence)
for (const i in presences) {
const response = await client.updatePresence(testJid, presences[i])
assert.strictEqual(response.status, 200)
await createTimeout(1500)
}
})
})
WAClientTest('Misc', (client) => {
it('should tell if someone has an account on WhatsApp', async () => {
const response = await client.isOnWhatsApp(testJid)
assert.strictEqual(response, true)
const responseFail = await client.isOnWhatsApp('abcd@s.whatsapp.net')
assert.strictEqual(responseFail, false)
})
it('should return the status', async () => {
const response = await client.getStatus(testJid)
assert.strictEqual(typeof response.status, 'string')
})
it('should update status', async () => {
const newStatus = 'v cool status'
const response = await client.getStatus()
assert.strictEqual(typeof response.status, 'string')
await createTimeout (1000)
await client.setStatus (newStatus)
const response2 = await client.getStatus()
assert.equal (response2.status, newStatus)
await createTimeout (1000)
await client.setStatus (response.status) // update back
})
it('should return the stories', async () => {
await client.getStories()
})
it('should change the profile picture', async () => {
await createTimeout (5000)
const ppUrl = await client.getProfilePicture(client.userMetaData.id)
const fetched = await fetch(ppUrl, { headers: { Origin: 'https://web.whatsapp.com' } })
const buff = await fetched.buffer ()
const newPP = fs.readFileSync ('./Media/cat.jpeg')
const response = await client.updateProfilePicture (client.userMetaData.id, newPP)
await createTimeout (10000)
await client.updateProfilePicture (client.userMetaData.id, buff) // revert back
})
it('should return the profile picture', async () => {
const response = await client.getProfilePicture(testJid)
assert.ok(response)
assert.rejects(client.getProfilePicture('abcd@s.whatsapp.net'))
})
it('should send typing indicator', async () => {
const response = await client.updatePresence(testJid, Presence.composing)
assert.ok(response)
})
it('should mark a chat unread', async () => {
await client.sendReadReceipt(testJid, null, 'unread')
})
it('should archive & unarchive', async () => {
await client.modifyChat (testJid, ChatModification.archive)
await createTimeout (2000)
await client.modifyChat (testJid, ChatModification.unarchive)
})
it('should pin & unpin a chat', async () => {
const response = await client.modifyChat (testJid, ChatModification.pin)
await createTimeout (2000)
await client.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 client.modifyChat (testJid, ChatModification.mute, {stamp: mutedate})
await createTimeout (2000)
await client.modifyChat (testJid, ChatModification.unmute, {stamp: mutedate})
})
it('should return search results', async () => {
const jids = [null, testJid]
for (let i in jids) {
const response = await client.searchMessages('Hello', jids[i], 25, 1)
assert.ok (response.messages)
assert.ok (response.messages.length >= 0)
}
})
})
WAClientTest('Groups', (client) => {
let gid: string
it('should create a group', async () => {
const response = await client.groupCreate('Cool Test Group', [testJid])
gid = response.gid
console.log('created group: ' + JSON.stringify(response))
})
it('should retreive group invite code', async () => {
const code = await client.groupInviteCode(gid)
assert.ok(code)
assert.strictEqual(typeof code, 'string')
})
it('should retreive group metadata', async () => {
const metadata = await client.groupMetadata(gid)
assert.strictEqual(metadata.id, gid)
assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1)
})
it('should update the group description', async () => {
const newDesc = 'Wow this was set from Baileys'
await client.groupUpdateDescription (gid, newDesc)
await createTimeout (1000)
const metadata = await client.groupMetadata(gid)
assert.strictEqual(metadata.desc, newDesc)
})
it('should send a message on the group', async () => {
await client.sendMessage(gid, 'hello', MessageType.text)
})
it('should update the subject', async () => {
const subject = 'V Cool Title'
await client.groupUpdateSubject(gid, subject)
const metadata = await client.groupMetadata(gid)
assert.strictEqual(metadata.subject, subject)
})
it('should update the group settings', async () => {
await client.groupSettingChange (gid, GroupSettingChange.messageSend, true)
await createTimeout (5000)
await client.groupSettingChange (gid, GroupSettingChange.settingsChange, true)
})
it('should remove someone from a group', async () => {
await client.groupRemove(gid, [testJid])
})
it('should leave the group', async () => {
await client.groupLeave(gid)
await client.groupMetadataMinimal (gid)
})
it('should archive the group', async () => {
await client.archiveChat(gid)
})
it('should delete the group', async () => {
await client.deleteChat(gid)
})
})
WAClientTest('Events', (client) => {
it('should deliver a message', async () => {
const waitForUpdate = () =>
new Promise((resolve) => {
client.setOnMessageStatusChange((update) => {
if (update.ids.includes(response.messageID)) {
resolve()
}
})
})
const response = await client.sendMessage(testJid, 'My Name Jeff', MessageType.text)
await promiseTimeout(10000, waitForUpdate())
})
/*it('should retreive all conversations', async () => {
const [chats] = await client.receiveChatsAndContacts (10000)
for (let chat of chats.all()) {
console.log ('receiving ' + chat.jid)
const convo = await client.loadConversation (chat.jid.replace('@s.whatsapp.net', '@c.us'), 25)
await createTimeout (200)
}
})*/
})

View File

@@ -1,193 +0,0 @@
import { MessageType, HKDFInfoKeys, MessageOptions, WAMessageType } from './Constants'
import Jimp from 'jimp'
import * as fs from 'fs'
import fetch from 'node-fetch'
import { WAMessage, WAMessageContent, BaileysError } from '../WAConnection/Constants'
import { hmacSign, aesDecryptWithIV, hkdf } from '../WAConnection/Utils'
import { proto } from '../../WAMessage/WAMessage'
import { randomBytes } from 'crypto'
import { exec } from 'child_process'
export function validateJIDForSending (jid: string) {
const regexp = /^[0-9]{1,20}(-[0-9]{1,20}@g.us|@s.whatsapp.net)$/
if (!regexp.test (jid)) {
throw new Error (
`Invalid WhatsApp id: ${jid}
1. Please ensure you suffix '@s.whatsapp.net' for individual numbers & '@g.us' for groups
2. Please do not put any alphabets or special characters like a '+' in the number. A '-' symbol in groups is fine`
)
}
}
/**
* Type of notification
* @deprecated use WA_MESSAGE_STUB_TYPE instead
* */
export function getNotificationType(message: WAMessage): [string, MessageType?] {
if (message.message) {
return ['message', Object.keys(message.message)[0] as MessageType]
} else if (message.messageStubType) {
return [WAMessageType[message.messageStubType], null]
} else {
return ['unknown', null]
}
}
/** generates all the keys required to encrypt/decrypt & sign a media message */
export function getMediaKeys(buffer, mediaType: MessageType) {
if (typeof buffer === 'string') {
buffer = Buffer.from (buffer.replace('data:;base64,', ''), 'base64')
}
// expand using HKDF to 112 bytes, also pass in the relevant app info
const expandedMediaKey = hkdf(buffer, 112, HKDFInfoKeys[mediaType])
return {
iv: expandedMediaKey.slice(0, 16),
cipherKey: expandedMediaKey.slice(16, 48),
macKey: expandedMediaKey.slice(48, 80),
}
}
/** Extracts video thumb using FFMPEG */
const extractVideoThumb = async (
path: string,
destPath: string,
time: string,
size: { width: number; height: number },
) =>
new Promise((resolve, reject) => {
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
exec(cmd, (err) => {
if (err) reject(err)
else resolve()
})
}) as Promise<void>
export const compressImage = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
return jimp.resize(48, 48).getBufferAsync (Jimp.MIME_JPEG)
}
export const generateProfilePicture = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
const min = Math.min(jimp.getWidth (), jimp.getHeight ())
const cropped = jimp.crop (0, 0, min, min)
return {
img: await cropped.resize(640, 640).getBufferAsync (Jimp.MIME_JPEG),
preview: await cropped.resize(96, 96).getBufferAsync (Jimp.MIME_JPEG)
}
}
/** generates a thumbnail for a given media, if required */
export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) {
if (info.thumbnail === null || info.thumbnail) {
// don't do anything if the thumbnail is already provided, or is null
if (mediaType === MessageType.audio) {
throw new Error('audio messages cannot have thumbnails')
}
} else if (mediaType === MessageType.image || mediaType === MessageType.sticker) {
const buff = await compressImage (buffer)
info.thumbnail = buff.toString('base64')
} else if (mediaType === MessageType.video) {
const filename = './' + randomBytes(5).toString('hex') + '.mp4'
const imgFilename = filename + '.jpg'
fs.writeFileSync(filename, buffer)
try {
await extractVideoThumb(filename, imgFilename, '00:00:00', { width: 48, height: 48 })
const buff = fs.readFileSync(imgFilename)
info.thumbnail = buff.toString('base64')
fs.unlinkSync(imgFilename)
} catch (err) {
console.log('could not generate video thumb: ' + err)
}
fs.unlinkSync(filename)
}
}
/**
* Decode a media message (video, image, document, audio) & return decrypted buffer
* @param message the media message you want to decode
*/
export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchHeaders: {[k: string]: string} = {}) {
/*
One can infer media type from the key in the message
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
*/
const type = Object.keys(message)[0] as MessageType
if (!type) {
throw new BaileysError('unknown message type', message)
}
if (type === MessageType.text || type === MessageType.extendedText) {
throw new BaileysError('cannot decode text message', message)
}
if (type === MessageType.location || type === MessageType.liveLocation) {
return new Buffer(message[type].jpegThumbnail)
}
let messageContent: proto.IVideoMessage | proto.IImageMessage | proto.IAudioMessage | proto.IDocumentMessage
if (message.productMessage) {
const product = message.productMessage.product?.productImage
if (!product) throw new BaileysError ('product has no image', message)
messageContent = product
} else {
messageContent = message[type]
}
// download the message
const headers = { Origin: 'https://web.whatsapp.com' }
const fetched = await fetch(messageContent.url, { headers })
const buffer = await fetched.buffer()
if (buffer.length <= 10) {
throw new BaileysError ('Empty buffer returned. File has possibly been deleted from WA servers. Run `client.updateMediaMessage()` to refresh the url', {status: 404})
}
const decryptedMedia = (type: MessageType) => {
// get the keys to decrypt the message
const mediaKeys = getMediaKeys(messageContent.mediaKey, type) //getMediaKeys(Buffer.from(messageContent.mediaKey, 'base64'), type)
// first part is actual file
const file = buffer.slice(0, buffer.length - 10)
// last 10 bytes is HMAC sign of file
const mac = buffer.slice(buffer.length - 10, buffer.length)
// sign IV+file & check for match with mac
const testBuff = Buffer.concat([mediaKeys.iv, file])
const sign = hmacSign(testBuff, mediaKeys.macKey).slice(0, 10)
// our sign should equal the mac
if (!sign.equals(mac)) throw new Error()
return aesDecryptWithIV(file, mediaKeys.cipherKey, mediaKeys.iv) // decrypt media
}
const allTypes = [type, ...Object.keys(HKDFInfoKeys)]
for (let i = 0; i < allTypes.length;i++) {
try {
const decrypted = decryptedMedia (allTypes[i] as MessageType)
if (i > 0) { console.log (`decryption of ${type} media with HKDF key of ${allTypes[i]}`) }
return decrypted
} catch {
if (i === 0) { console.log (`decryption of ${type} media with original HKDF key failed`) }
}
}
throw new BaileysError('Decryption failed, HMAC sign does not match', {status: 400})
}
export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
const type = Object.keys(message)[0] as MessageType
let extension: string
if (type === MessageType.location || type === MessageType.liveLocation || type === MessageType.product) {
extension = '.jpeg'
} else {
const messageContent = message[type] as
| proto.VideoMessage
| proto.ImageMessage
| proto.AudioMessage
| proto.DocumentMessage
extension = getExtension (messageContent.mimetype)
}
return extension
}
/**
* Decode a media message (video, image, document, audio) & save it to the given file
* @deprecated use `client.downloadAndSaveMediaMessage`
*/
export async function decodeMediaMessage(message: WAMessageContent, filename: string, attachExtension: boolean=true) {
const buffer = await decodeMediaMessageBuffer (message, {})
const extension = extensionForMediaMessage (message)
const trueFileName = attachExtension ? (filename + '.' + extension) : filename
fs.writeFileSync(trueFileName, buffer)
return trueFileName
}

View File

@@ -1,7 +0,0 @@
import WhatsAppWebMessages from './Messages'
export { WhatsAppWebMessages as WAClient }
export * from './Constants'
export * from './Utils'
export * from '../WAConnection/Constants'
export { Browsers } from '../WAConnection/Utils'

View File

@@ -14,6 +14,11 @@ import {
AuthenticationCredentialsBrowser,
BaileysError,
WAConnectionMode,
WAMessage,
PresenceUpdate,
MessageStatusUpdate,
WAMetric,
WAFlag,
} from './Constants'
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
@@ -22,7 +27,7 @@ const generateQRCode = function ([ref, publicKey, clientID]) {
QR.generate(str, { small: true })
}
export default class WAConnectionBase {
export class WAConnection {
/** The version of WhatsApp Web we're telling the servers we are */
version: [number, number, number] = [2, 2027, 10]
/** The Browser we're telling the WhatsApp Web servers we are */
@@ -61,9 +66,7 @@ export default class WAConnectionBase {
protected pendingRequests: (() => void)[] = []
protected reconnectLoop: () => Promise<void>
protected referenceDate = new Date () // used for generating tags
protected userAgentString: string
constructor () {
this.userAgentString = Utils.userAgentString (this.browserDescription[1])
this.registerCallback (['Cmd', 'type:disconnect'], json => this.unexpectedDisconnect(json[1].kind))
}
async unexpectedDisconnect (error: string) {
@@ -74,6 +77,46 @@ export default class WAConnectionBase {
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
@@ -243,6 +286,12 @@ export default class WAConnectionBase {
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
}
/**
* Send a binary encoded message
* @param json the message to encode & send

View File

@@ -1,10 +1,10 @@
import * as Curve from 'curve25519-js'
import * as Utils from './Utils'
import WAConnectionBase from './Base'
import { MessageLogLevel, WAMetric, WAFlag, BaileysError } from './Constants'
import { Presence } from '../WAClient/WAClient'
import {WAConnection as Base} from './0.Base'
import { MessageLogLevel, WAMetric, WAFlag, BaileysError, Presence } from './Constants'
export default class WAConnectionValidator extends WAConnectionBase {
export class WAConnection extends Base {
/** Authenticate the connection */
protected async authenticate() {
if (!this.authInfo.clientID) {
@@ -21,6 +21,7 @@ export default class WAConnectionValidator extends WAConnectionBase {
this.referenceDate = new Date () // refresh reference date
const data = ['admin', 'init', this.version, this.browserDescription, this.authInfo.clientID, true]
return this.queryExpecting200(data)
.then(json => {
// we're trying to establish a new connection or are trying to log in

View File

@@ -2,10 +2,10 @@ 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 WAConnectionValidator from './Validation'
import {WAConnection as Base} from './1.Validation'
import Decoder from '../Binary/Decoder'
export default class WAConnectionConnector extends WAConnectionValidator {
export class WAConnection extends Base {
/**
* Connect to WhatsAppWeb
* @param [authInfo] credentials or path to credentials to log back in

View File

@@ -1,58 +1,16 @@
import WAConnection from '../WAConnection/WAConnection'
import { MessageStatusUpdate, PresenceUpdate, Presence, WABroadcastListInfo, WAProfilePictureChange } from './Constants'
import {WAConnection as Base} from './3.Connect'
import { Presence, WABroadcastListInfo, WAProfilePictureChange } from './Constants'
import {
WAMessage,
WANode,
WAMetric,
WAFlag,
MessageLogLevel,
WATag,
} from '../WAConnection/Constants'
import { generateProfilePicture } from '../WAClient/Utils'
import { generateProfilePicture } from './Utils'
// All user related functions -- get profile picture, set status etc.
export default class WhatsAppWebBase extends WAConnection {
/** 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]))
}
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)
/**
@@ -75,13 +33,15 @@ export default class WhatsAppWebBase extends WAConnection {
return this.query(['query', 'Status', jid || this.userMetaData.id]) as Promise<{ status: string }>
}
async setStatus (status: string) {
return this.setQuery ([
return this.setQuery (
[
'status',
null,
Buffer.from (status, 'utf-8')
[
'status',
null,
Buffer.from (status, 'utf-8')
]
]
])
)
}
/** Get the URL to download the profile picture of a person/group */
async getProfilePicture(jid: string | null) {
@@ -157,10 +117,7 @@ export default class WhatsAppWebBase extends WAConnection {
},
null,
]
const response = await this.query(json, [WAMetric.queryMessages, WAFlag.ignore])
if (response.status) throw new Error(`error in query, got status: ${response.status}`)
const response = await this.queryExpecting200(json, [WAMetric.queryMessages, WAFlag.ignore])
return response[2] ? (response[2] as WANode[]).map((item) => item[2] as WAMessage) : []
}
/**
@@ -210,10 +167,4 @@ export default class WhatsAppWebBase extends WAConnection {
]
return this.setQuery ([query], [WAMetric.picture, 136], tag) as Promise<WAProfilePictureChange>
}
/** 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
}
}

View File

@@ -1,6 +1,6 @@
import WhatsAppWebGroups from './Groups'
import {WAConnection as Base} from './4.User'
import fetch from 'node-fetch'
import { promises as fs } from 'fs'
import fs from 'fs/promises'
import {
MessageOptions,
MessageType,
@@ -15,13 +15,11 @@ import {
MessageInfo,
WATextMessage,
WAUrlInfo,
WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE
} from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes } from '../WAConnection/Utils'
import { WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError, MessageLogLevel } from '../WAConnection/Constants'
import { validateJIDForSending, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage } from './Utils'
import { proto } from '../../WAMessage/WAMessage'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage } from './Utils'
export default class WhatsAppWebMessages extends WhatsAppWebGroups {
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]
@@ -56,16 +54,6 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
}
return this.setQuery ([['read', attributes, null]])
}
/**
* Mark a given chat as unread
* @deprecated since 2.0.0, use `sendReadReceipt (jid, null, 'unread')` instead
*/
async markChatUnread (jid: string) { return this.sendReadReceipt (jid, null, 'unread') }
/**
* Archive a chat
* @deprecated since 2.0.0, use `modifyChat (jid, ChatModification.archive)` instead
*/
async archiveChat (jid: string) { return this.modifyChat (jid, ChatModification.archive) }
/**
* Modify a given chat (archive, pin etc.)
* @param jid the ID of the person/group you are modifiying
@@ -190,24 +178,27 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
const json: WAMessageContent = {
protocolMessage: {
key: messageKey,
type: proto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE
type: WAMessageProto.ProtocolMessage.PROTOCOL_MESSAGE_TYPE.REVOKE
}
}
return this.sendMessageContent (id, json, {})
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) {
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 ? 0 : 1
score += message.key.fromMe && !forceForward ? 0 : 1
if (key === MessageType.text) {
content[MessageType.extendedText] = { text: content[key] }
delete content[MessageType.text]
@@ -216,18 +207,35 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
}
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
else content[key].contextInfo = {}
return this.sendMessageContent (id, content, {})
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
*/
async sendMessage(
id: string,
message: string | WATextMessage | WALocationMessage | WAContactMessage | Buffer,
type: MessageType,
options: MessageOptions = {},
) {
if (options.validateID === true || !('validateID' in options)) {
validateJIDForSending (id)
}
const waMessage = await this.prepareMessage (id, message, type, options)
await this.relayWAMessage (waMessage)
return waMessage
}
/** Prepares a message for sending via sendWAMessage () */
async prepareMessage(
id: string,
message: string | WATextMessage | WALocationMessage | WAContactMessage | Buffer,
type: MessageType,
options: MessageOptions = {},
) {
let m: WAMessageContent = {}
switch (type) {
case MessageType.text:
@@ -251,7 +259,7 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
m = await this.prepareMediaMessage(message as Buffer, type, options)
break
}
return this.sendMessageContent(id, m, options)
return this.generateWAMessage(id, m, options)
}
/** Prepare a media message for sending */
async prepareMediaMessage(buffer: Buffer, mediaType: MessageType, options: MessageOptions = {}) {
@@ -315,15 +323,13 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
}
return message as WAMessageContent
}
/** Send message content */
async sendMessageContent(id: string, message: WAMessageContent, options: MessageOptions) {
const messageJSON = this.generateWAMessage (id, message, options)
return this.sendWAMessage (messageJSON)
}
/** generates a WAMessage from the given content & options */
generateWAMessage(id: string, message: WAMessageContent, options: MessageOptions) {
if (!options.timestamp) options.timestamp = new Date() // set timestamp to now
// prevent an annoying bug (WA doesn't accept sending messages with '@c.us')
id = id.replace ('@c.us', '@s.whatsapp')
const key = Object.keys(message)[0]
const timestamp = options.timestamp.getTime()/1000
const quoted = options.quoted
@@ -356,29 +362,22 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
messageTimestamp: timestamp,
messageStubParameters: [],
participant: id.includes('@g.us') ? this.userMetaData.id : null,
status: WAMessageProto.proto.WebMessageInfo.WEB_MESSAGE_INFO_STATUS.PENDING
status: WA_MESSAGE_STATUS_TYPE.PENDING
}
return messageJSON as WAMessage
}
/**
* Send a WAMessage; more advanced functionality, you may want to stick with sendMessage()
* */
async sendWAMessage(message: WAMessage) {
/** 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
const response = await this.queryExpecting200(json, [WAMetric.message, flag], null, message.key.id)
return {
status: response.status as number,
messageID: message.key.id,
message: message as WAMessage
} as WASendMessageResponse
await this.queryExpecting200(json, [WAMetric.message, flag], null, message.key.id)
}
/**
* Securely downloads the media from the message.
* Renews the download url automatically, if necessary.
*/
async downloadMediaMessage (message: WAMessage) {
const fetchHeaders = { 'User-Agent': this.userAgentString }
const fetchHeaders = { }
try {
const buff = await decodeMediaMessageBuffer (message.message, fetchHeaders)
return buff

View File

@@ -1,9 +1,9 @@
import WhatsAppWebBase from './Base'
import { WAMessage, WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants'
import {WAConnection as Base} from './5.Messages'
import { WAMetric, WAFlag, WANode, WAGroupMetadata, WAGroupCreateResponse, WAGroupModification } from '../WAConnection/Constants'
import { GroupSettingChange } from './Constants'
import { generateMessageID } from '../WAConnection/Utils'
export default class WhatsAppWebGroups extends WhatsAppWebBase {
export class WAConnection extends Base {
/** Generic function for group queries */
async groupQuery(type: string, jid?: string, subject?: string, participants?: string[], additionalNodes?: WANode[]) {
const tag = this.generateMessageTag()

View File

@@ -133,6 +133,152 @@ export enum WAFlag {
}
/** Tag used with binary queries */
export type WATag = [WAMetric, WAFlag]
export * as WAMessageProto from '../../WAMessage/WAMessage'
// 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 {
available = 'available', // "online"
unavailable = 'unavailable', // "offline"
composing = 'composing', // "typing..."
recording = 'recording', // "recording..."
paused = 'paused', // I have no clue
}
/** Status of a message sent or received */
export enum MessageStatus {
sent = 'sent',
received = 'received',
read = 'read',
}
/** Set of message types that are supported by the library */
export enum MessageType {
text = 'conversation',
extendedText = 'extendedTextMessage',
contact = 'contactMessage',
location = 'locationMessage',
liveLocation = 'liveLocationMessage',
image = 'imageMessage',
video = 'videoMessage',
sticker = 'stickerMessage',
document = 'documentMessage',
audio = 'audioMessage',
product = 'productMessage'
}
export enum ChatModification {
archive='archive',
unarchive='unarchive',
pin='pin',
unpin='unpin',
mute='mute',
unmute='unmute'
}
export const HKDFInfoKeys = {
[MessageType.image]: 'WhatsApp Image Keys',
[MessageType.audio]: 'WhatsApp Audio Keys',
[MessageType.video]: 'WhatsApp Video Keys',
[MessageType.document]: 'WhatsApp Document Keys',
[MessageType.sticker]: 'WhatsApp Image Keys'
}
export enum Mimetype {
jpeg = 'image/jpeg',
png = 'image/png',
mp4 = 'video/mp4',
gif = 'video/gif',
pdf = 'application/pdf',
ogg = 'audio/ogg; codecs=opus',
/** for stickers */
webp = 'image/webp',
}
export interface MessageOptions {
quoted?: WAMessage
contextInfo?: WAContextInfo
timestamp?: Date
caption?: string
thumbnail?: string
mimetype?: Mimetype | string
filename?: string
}
export interface WABroadcastListInfo {
status: number
name: string
recipients?: {id: string}[]
}
export interface WAUrlInfo {
'canonical-url': string
'matched-text': string
title: string
description: string
jpegThumbnail?: Buffer
}
export interface WAProfilePictureChange {
status: number
tag: string
eurl: string
}
export interface MessageInfo {
reads: {jid: string, t: string}[]
deliveries: {jid: string, t: string}[]
}
export interface MessageStatusUpdate {
from: string
to: string
/** Which participant caused the update (only for groups) */
participant?: string
timestamp: Date
/** Message IDs read/delivered */
ids: string[]
/** Status of the Message IDs */
type: WA_MESSAGE_STATUS_TYPE
}
export enum GroupSettingChange {
messageSend = 'announcement',
settingsChange = 'locked',
}
export interface PresenceUpdate {
id: string
participant?: string
t?: string
type?: Presence
deny?: boolean
}
// path to upload the media
export const MediaPathMap = {
imageMessage: '/mms/image',
videoMessage: '/mms/video',
documentMessage: '/mms/document',
audioMessage: '/mms/audio',
stickerMessage: '/mms/image',
}
// gives WhatsApp info to process the media
export const MimetypeMap = {
imageMessage: Mimetype.jpeg,
videoMessage: Mimetype.mp4,
documentMessage: Mimetype.pdf,
audioMessage: Mimetype.ogg,
stickerMessage: Mimetype.webp,
}
export interface WASendMessageResponse {
status: number
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

View File

@@ -1,9 +1,13 @@
import * as Crypto from 'crypto'
import HKDF from 'futoin-hkdf'
import Decoder from '../Binary/Decoder'
import Jimp from 'jimp'
import fs from 'fs/promises'
import fetch from 'node-fetch'
import { exec } from 'child_process'
import {platform, release} from 'os'
import { BaileysError, WAChat } from './Constants'
import UserAgent from 'user-agents'
import Decoder from '../Binary/Decoder'
import { MessageType, HKDFInfoKeys, MessageOptions, WAChat, WAMessageType, WAMessage, WAMessageContent, BaileysError, WAMessageProto } from './Constants'
const platformMap = {
'aix': 'AIX',
@@ -25,10 +29,10 @@ function hashCode(s: string) {
}
export const waChatUniqueKey = (c: WAChat) => ((+c.t*100000) + (hashCode(c.jid)%100000))*-1 // -1 to sort descending
export function userAgentString (browser) {
/*export function userAgentString (browser) {
const agent = new UserAgent (new RegExp(browser))
return agent.toString ()
}
}*/
/** 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))
@@ -65,6 +69,7 @@ export function randomBytes(length) {
return Crypto.randomBytes(length)
}
export const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
export async function promiseTimeout<T>(ms: number, promise: Promise<T>) {
if (!ms) return promise
// Create a promise that rejects in <ms> milliseconds
@@ -139,4 +144,151 @@ export function decryptWA (message: string | Buffer, macKey: Buffer, encKey: Buf
json = decoder.read(decrypted) // decode the binary message into a JSON array
}
return [messageTag, json, tags]
}
/** generates all the keys required to encrypt/decrypt & sign a media message */
export function getMediaKeys(buffer, mediaType: MessageType) {
if (typeof buffer === 'string') {
buffer = Buffer.from (buffer.replace('data:;base64,', ''), 'base64')
}
// expand using HKDF to 112 bytes, also pass in the relevant app info
const expandedMediaKey = hkdf(buffer, 112, HKDFInfoKeys[mediaType])
return {
iv: expandedMediaKey.slice(0, 16),
cipherKey: expandedMediaKey.slice(16, 48),
macKey: expandedMediaKey.slice(48, 80),
}
}
/** Extracts video thumb using FFMPEG */
const extractVideoThumb = async (
path: string,
destPath: string,
time: string,
size: { width: number; height: number },
) =>
new Promise((resolve, reject) => {
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
exec(cmd, (err) => {
if (err) reject(err)
else resolve()
})
}) as Promise<void>
export const compressImage = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
return jimp.resize(48, 48).getBufferAsync (Jimp.MIME_JPEG)
}
export const generateProfilePicture = async (buffer: Buffer) => {
const jimp = await Jimp.read (buffer)
const min = Math.min(jimp.getWidth (), jimp.getHeight ())
const cropped = jimp.crop (0, 0, min, min)
return {
img: await cropped.resize(640, 640).getBufferAsync (Jimp.MIME_JPEG),
preview: await cropped.resize(96, 96).getBufferAsync (Jimp.MIME_JPEG)
}
}
/** generates a thumbnail for a given media, if required */
export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) {
if (info.thumbnail === null || info.thumbnail) {
// don't do anything if the thumbnail is already provided, or is null
if (mediaType === MessageType.audio) {
throw new Error('audio messages cannot have thumbnails')
}
} else if (mediaType === MessageType.image || mediaType === MessageType.sticker) {
const buff = await compressImage (buffer)
info.thumbnail = buff.toString('base64')
} else if (mediaType === MessageType.video) {
const filename = './' + randomBytes(5).toString('hex') + '.mp4'
const imgFilename = filename + '.jpg'
await fs.writeFile(filename, buffer)
try {
await extractVideoThumb(filename, imgFilename, '00:00:00', { width: 48, height: 48 })
const buff = await fs.readFile(imgFilename)
info.thumbnail = buff.toString('base64')
await fs.unlink(imgFilename)
} catch (err) {
console.log('could not generate video thumb: ' + err)
}
await fs.unlink(filename)
}
}
/**
* Decode a media message (video, image, document, audio) & return decrypted buffer
* @param message the media message you want to decode
*/
export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchHeaders: {[k: string]: string} = {}) {
/*
One can infer media type from the key in the message
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
*/
const type = Object.keys(message)[0] as MessageType
if (!type) {
throw new BaileysError('unknown message type', message)
}
if (type === MessageType.text || type === MessageType.extendedText) {
throw new BaileysError('cannot decode text message', message)
}
if (type === MessageType.location || type === MessageType.liveLocation) {
return new Buffer(message[type].jpegThumbnail)
}
let messageContent: WAMessageProto.IVideoMessage | WAMessageProto.IImageMessage | WAMessageProto.IAudioMessage | WAMessageProto.IDocumentMessage
if (message.productMessage) {
const product = message.productMessage.product?.productImage
if (!product) throw new BaileysError ('product has no image', message)
messageContent = product
} else {
messageContent = message[type]
}
// download the message
const headers = { Origin: 'https://web.whatsapp.com' }
const fetched = await fetch(messageContent.url, { headers })
const buffer = await fetched.buffer()
if (buffer.length <= 10) {
throw new BaileysError ('Empty buffer returned. File has possibly been deleted from WA servers. Run `client.updateMediaMessage()` to refresh the url', {status: 404})
}
const decryptedMedia = (type: MessageType) => {
// get the keys to decrypt the message
const mediaKeys = getMediaKeys(messageContent.mediaKey, type) //getMediaKeys(Buffer.from(messageContent.mediaKey, 'base64'), type)
// first part is actual file
const file = buffer.slice(0, buffer.length - 10)
// last 10 bytes is HMAC sign of file
const mac = buffer.slice(buffer.length - 10, buffer.length)
// sign IV+file & check for match with mac
const testBuff = Buffer.concat([mediaKeys.iv, file])
const sign = hmacSign(testBuff, mediaKeys.macKey).slice(0, 10)
// our sign should equal the mac
if (!sign.equals(mac)) throw new Error()
return aesDecryptWithIV(file, mediaKeys.cipherKey, mediaKeys.iv) // decrypt media
}
const allTypes = [type, ...Object.keys(HKDFInfoKeys)]
for (let i = 0; i < allTypes.length;i++) {
try {
const decrypted = decryptedMedia (allTypes[i] as MessageType)
if (i > 0) { console.log (`decryption of ${type} media with HKDF key of ${allTypes[i]}`) }
return decrypted
} catch {
if (i === 0) { console.log (`decryption of ${type} media with original HKDF key failed`) }
}
}
throw new BaileysError('Decryption failed, HMAC sign does not match', {status: 400})
}
export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
const type = Object.keys(message)[0] as MessageType
let extension: string
if (type === MessageType.location || type === MessageType.liveLocation || type === MessageType.product) {
extension = '.jpeg'
} else {
const messageContent = message[type] as
| WAMessageProto.VideoMessage
| WAMessageProto.ImageMessage
| WAMessageProto.AudioMessage
| WAMessageProto.DocumentMessage
extension = getExtension (messageContent.mimetype)
}
return extension
}

View File

@@ -1,2 +1,3 @@
import WAConnection from './Connect'
export default WAConnection
export * from './6.Groups'
export * from './Utils'
export * from './Constants'