Rewrite for extensibility & compactness

-This is a break from previous versions unfortunately
-Connecting is now a promise
-Chats, contacts & previously unread messages are supplied on connection
-Groups!
-Message confirmations are more reliable
-Timeout queries & connections
This commit is contained in:
Adhiraj
2020-05-14 20:13:32 +05:30
parent 512d45c5bd
commit b60bd03d21
10 changed files with 1216 additions and 778 deletions

BIN
.DS_Store vendored

Binary file not shown.

366
README.md
View File

@@ -1,115 +1,116 @@
# Baileys
# Baileys - WhatsApp Web API for Node.js
Reverse Engineered WhatsApp Web API in pure Node.js. 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/tree/484cfe758705761d76724e01839d6fc473dc10c4) for the __go__ reimplementation.
Baileys is super easy to use:
* Install from npm using
``` npm install github:adiwajshing/Baileys ```
* Then import in your code using
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__ reimplementation.
Baileys has also been written from the ground up to be very 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 at the end).
* __Install__
Create and cd to your NPM project directory and then in terminal, write: ``` npm install github:adiwajshing/Baileys ```
Then import in your code using:
``` javascript
const WhatsAppWeb = require('Baileys')
```
* Create an instance of Baileys & connect using
* __Connecting__
``` javascript
let client = new WhatsAppWeb()
const client = new WhatsAppWeb()
client.connect()
.then (([user, chats, contacts, unread]) => {
console.log ("oh hello " + user.name + " (" + user.id + ")")
console.log ("you have " + unread.length + " unread messages")
console.log ("you have " + chats.length + " chats")
})
.catch (err => console.log("unexpected error: " + err) )
```
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!
* Implement the following event handlers in your code:
``` javascript
client.handlers.onConnected = () => { /* when you're successfully authenticated with the WhatsApp Web servers */ }
```
``` javascript
client.handlers.onUnreadMessage = (message) => { /* called when you have a pending unread message or recieve a new message */ }
```
``` javascript
client.handlers.onError = (error) => { /* called when there was an error */ }
```
``` javascript
client.handlers.onGotContact = (contactArray) => { /* called when we recieve the contacts (contactArray is an array) */ }
```
``` javascript
client.handlers.presenceUpdated = (id, presence) => { /* called when you recieve an update on someone's presence */ }
```
``` javascript
client.handlers.onMessageStatusChanged = (id, messageID, status) => { /* called when your message gets delivered or read */ }
```
``` javascript
client.handlers.onDisconnect = () => { /* called when internet gets disconnected */ }
```
* Get the type of message using
``` javascript
client.handlers.onUnreadMessage = (m) => {
const messageType = client.getMessageType(m.message) // get what type of message it is -- text, image, video
}
```
* Decode a media message using
``` javascript
client.handlers.onUnreadMessage = (m) => {
const messageType = client.getMessageType(m.message) // get what type of message it is -- text, image, video
// if the message is not a text message
if (messageType !== WhatsAppWeb.MessageType.text && messageType !== WhatsAppWeb.MessageType.extendedText) {
client.decodeMediaMessage(m.message, "filename") // extension applied automatically
.then (meta => console.log(m.key.remoteJid + " sent media, saved at: " + meta.fileName))
.catch (err => console.log("error in decoding message: " + err))
}
}
```
* Send a text message using
``` javascript
client.sendTextMessage(id, txtMessage)
```
Or if you want to quote another message:
``` javascript
client.sendTextMessage(id, txtMessage, quotedMessage)
```
The id is the phone number of the person the message is being sent to, it must be in the format '[country code][phone number]@s.whatsapp.net', for example '+19999999999@s.whatsapp.net'
* Send a media (image, video, sticker, pdf) message using
``` javascript
client.sendMediaMessage(id, mediaBuffer, mediaType, info)
```
- The thumbnail can be generated automatically for images & stickers.
- ```mediaBuffer``` is just a Buffer containing the contents of the media you want to send
- ```mediaType``` represents the type of message you are sending. This can be one of the following:
``` javascript
[
WhatsAppWeb.MessageType.image, // an image message
WhatsAppWeb.MessageType.video, // a video message
WhatsAppWeb.MessageType.audio, // an audio message
WhatsAppWeb.MessageType.sticker // a sticker message
]
* __Handling Events__
Implement the following callbacks in your code:
- Called when you have a pending unread message or recieve a new message
``` javascript
client.setOnUnreadMessage (m => {
const [notificationType, messageType] = client.getNotificationType(m) // get what type of notification it is -- message, group add notification etc.
console.log("got notification of type: " + notificationType) // message, groupAdd, groupLeave
console.log("message type: " + messageType) // conversation, imageMessage, videoMessage etc.
})
- Called when you recieve an update on someone's presence, they went offline or online
``` javascript
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
```
- ```info``` is a JSON object, providing some information about the media. It can have the following __optional__ values:
``` javascript
info = {
caption: "hello there!", // the caption to send with the media (cannot be sent with stickers though)
thumbnail: null, /* 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.
Do not enter this field if you want to automatically generate a thumb
*/
mimetype: "application/pdf", /* specify the type of media (optional for all media types except documents),
for pdf files => set to "application/pdf",
for txt files => set to "application/txt"
etc.
*/
gif: true // only applicable to video messages, if the video should be treated as a GIF
}
- Called when your message gets delivered or read
``` javascript
client.setOnMessageStatusChange (json => {
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)
})
```
- Tested formats: png, jpeg, webp (sticker), mp4, ogg
- To automatically generate thumbnails for videos, you need to have ``` ffmpeg ``` installed on your system
* Send a read reciept using
- Called when the connection gets disconnected (either the server loses internet or the phone gets unpaired)
``` javascript
client.setOnUnexpectedDisconnect (err => console.log ("disconnected unexpectedly: " + err) )
```
* __Sending Messages__
It's super simple
- Send text messages using
``` javascript
client.sendTextMessage(id, "oh hello there!")
```
- Send text messages & quote another message using
``` javascript
client.sendTextMessage(id, "oh hello there", quotedMessage)
```
``` quotedMessage ``` is a message object
- Send a media (image, video, sticker, pdf) message using
``` javascript
const buffer = fs.readFileSync("example/ma_gif.mp4") // load some gif
const info = {gif: true, caption: "hello!"} // some metadata & caption
client.sendMediaMessage(id, buffer, WhatsAppWeb.MessageType.video, info)
```
- The thumbnail can be generated automatically for images & stickers. Though, to automatically generate thumbnails for videos, you need to have ``` ffmpeg ``` installed on your system
- ```mediaBuffer``` is just a Buffer containing the contents of the media you want to send
- ```mediaType``` represents the type of message you are sending. This can be one of the following:
``` javascript
[
WhatsAppWeb.MessageType.image, // an image message
WhatsAppWeb.MessageType.video, // a video message
WhatsAppWeb.MessageType.audio, // an audio message
WhatsAppWeb.MessageType.sticker // a sticker message
]
```
- ```info``` is a JSON object, providing some information about the media. It can have the following __optional__ values:
``` javascript
info = {
caption: "hello there!", // the caption to send with the media (cannot be sent with stickers though)
thumbnail: null, /* 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.
Do not enter this field if you want to automatically generate a thumb
*/
mimetype: "application/pdf", /* specify the type of media (optional for all media types except documents),
for pdf files => set to "application/pdf",
for txt files => set to "application/txt"
etc.
*/
gif: true // only applicable to video messages, if the video should be treated as a GIF
}
```
- Tested formats: png, jpeg, webp (sticker), mp4, ogg
``` 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 ```.
* __Sending Read Receipts__
``` javascript
client.sendReadReceipt(id, messageID)
```
The 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
* Update your status by using
The 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 message object, it can be accessed using ```messageID = message.key.id```.
* __Update Presence__
``` javascript
client.updatePresence(id, presence)
client.updatePresence(id, WhatsAppWeb.Presence.available)
```
This lets the person with ``` id ``` know your status. where ``` presence ``` can be one of the following:
This lets the person/group with ``` id ``` know whether you're online, offline, typing etc. where ``` presence ``` can be one of the following:
``` javascript
[
WhatsAppWeb.Presence.available, // online
@@ -118,64 +119,151 @@ Baileys is super easy to use:
WhatsAppWeb.Presence.recording // recording...
]
```
* Once you want to close your session, you can get your authentication credentials using:
* __Decoding Media__
If you want to save & process some images, videos, documents or stickers you received
``` javascript
client.setOnUnreadMessage (m => {
const messageType = client.getMessageType(m.message) // get what type of message it is -- text, image, video
// if the message is not a text message
if (messageType !== WhatsAppWeb.MessageType.text && messageType !== WhatsAppWeb.MessageType.extendedText) {
client.decodeMediaMessage(m.message, "filename") // extension applied automatically
.then (meta => console.log(m.key.remoteJid + " sent media, saved at: " + meta.filename))
.catch (err => console.log("error in decoding message: " + err))
}
}
```
* __Restoring Sessions__
Once you connect successfully, you can get your authentication credentials using
``` javascript
const authJSON = client.base64EncodedAuthInfo()
```
and then save this JSON to a file
* If you want to restore your session (i.e. log back in without having to scan the QR code), simply retreive your previously saved credentials and use
Then you can use this JSON to log back in without needing to scan a QR code using
``` javascript
const authJSON = JSON.parse( fs.readFileSync("auth_info.json") )
client.login(authJSON)
client.connect (authJSON)
.then (([user, chats, contacts, unread]) => console.log ("yay connected"))
```
This will use the credentials to connect & log back in. No need to call ``` connect() ``` after calling this function
* To query things like chat history and all, use:
* To check if a given ID is on WhatsApp
``` javascript
client.isOnWhatsApp ("xyz@c.us")
.then (([exists, id]) => console.log(id + (exists ? " exists " : " does not exist") + "on WhatsApp"))
```
* To query chat history on a group or with someone
``` javascript
client.getMessages ("xyz@c.us", 25) // query the last 25 messages (replace 25 with the number of messages you want to query)
* __Querying__
- To check if a given ID is on WhatsApp
``` javascript
client.isOnWhatsApp ("xyz@c.us")
.then (([exists, id]) => console.log(id + (exists ? " exists " : " does not exist") + "on WhatsApp"))
```
- To query chat history on a group or with someone
``` javascript
client.loadConversation ("xyz-abc@g.us", 25) // query the last 25 messages (replace 25 with the number of messages you want to query)
.then (messages => console.log("got back " + messages.length + " messages"))
```
* To query the entire chat history
``` javascript
client.getAllMessages ("xyz@c.us", (message) => {
console.log("GOT message with ID: " + message.key.id)
})
.then (() => console.log("queried all messages")) // promise ends once all messages are retreived
```
* To query someone's status
``` javascript
client.getStatus ("xyz@c.us")
```
You can also load the entire conversation history if you want
``` javascript
client.loadEntireConversation ("xyz@c.us", (message) => console.log("Loaded message with ID: " + message.key.id))
.then (() => console.log("queried all messages")) // promise resolves once all messages are retreived
```
- To get the status of some person/group
``` javascript
client.getStatus ("xyz@c.us") // leave empty to get your own
.then (json => console.log("status: " + json.status))
```
* To get someone's profile picture
``` javascript
client.getStatus ("xyz@c.us") //
```
- To get the display picture of some person/group
``` javascript
client.getStatus ("xyz@g.us") // leave empty to get your own
.then (json => console.log("download profile picture from: " + json.eurl))
```
* To get someone's presence (if they're typing, online)
``` javascript
```
- To get someone's presence (if they're typing, online)
``` javascript
client.requestPresenceUpdate ("xyz@c.us")
// the presence update is fetched and called here
client.handlers.presenceUpdated = (id, presence) => {
console.log(id + " has status: " + presence)
}
```
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
```
Of course, replace ``` xyz ``` with an actual country code & number.
Also, append ``` @c.us ``` for individuals & ``` @g.us ``` for groups.
Of course, replace ``` xyz ``` with an actual ID. Also, append ``` @c.us ``` for individuals & ``` @g.us ``` for groups.
* __Groups__
- To create a group
``` javascript
client.groupCreate ("My Fab Group", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // title & participants
.then (([json, _]) => {
console.log ("created group with id: " + json.gid)
client.sendTextMessage(json.gid, "hello everyone") // say hello to everyone on the group
})
```
- To add people to a group
``` javascript
client.groupAdd ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // id & people to add to the group
.then (([json, _]) => console.log("added successfully: " + (json.status===200)))
```
- To make someone admin on a group
``` javascript
client.groupMakeAdmin ("abcd-xyz@g.us", ["abcd@s.whatsapp.net", "efgh@s.whatsapp.net"]) // id & people to make admin
.then (([json, _]) => console.log("made admin successfully: " + (json.status===200)))
```
- To leave a group
``` javascript
client.groupLeave ("abcd-xyz@g.us")
.then (([json, _]) => console.log("left group successfully: " + (json.status===200)))
```
- To get the invite code for a group
``` javascript
client.groupInviteCode ("abcd-xyz@g.us")
.then (code => console.log(code))
```
* __Writing Custom Functionality__
Baileys is also written, keeping in mind, that you may require other functionality. Hence, instead of having to fork the project & re-write the internals, you can simply write extensions in your own code.
First, enable the logging of unhandled messages from WhatsApp by setting
``` javascript
client.logUnhandledMessages = true
```
This will enable you to see all sorts of messages WhatsApp sends in the console. Some examples:
Do check out & run [example.js](example/example.js) to see example usage of all these functions.
To run the example script, download or clone the repo and then type the following in terminal:
1. ``` cd path/to/Baileys/example ```
2. ``` node example.js ```
1. Functionality to track of the battery percentage of your phone.
You enable logging and you'll see a message about your battery pop up in the console:
``` [Baileys] [Unhandled] s22, ["action",null,[["battery",{"live":"false","value":"52"},null]]] ```
You now know what a battery update looks like. It'll have the following characteristics.
- Given ```const bMessage = ["action",null,[["battery",{"live":"false","value":"52"},null]]]```
- ```bMessage[0]``` is always ``` "action" ```
- ```bMessage[1]``` is always ``` null ```
- ```bMessage[2][0][0]``` is always ``` "battery" ```
Hence, you can register a callback for an event using the following:
``` javascript
client.registerCallback (["action", null, "battery"], json => {
const batteryLevelStr = json[2][0][1].value
const batterylevel = parseInt (batteryLevelStr)
console.log ("battery level: " + batterylevel + "%")
})
```
This callback will be fired any time a message is received matching the following criteria:
``` message [0] === "action" && message [1] === null && message[2][0][0] === "battery" ```
2. Functionality to keep track of the pushname changes on your phone.
You enable logging and you'll see an unhandled message about your pushanme pop up like this:
```[Baileys] [Unhandled] s24, ["Conn",{"pushname":"adiwajshing"}]```
You now know what a pushname update looks like. It'll have the following characteristics.
- Given ```const pMessage = ["Conn",{"pushname":"adiwajshing"}] ```
- ```pMessage[0]``` is always ``` "Conn" ```
- ```pMessage[1]``` always has the key ``` "pushname" ```
- ```pMessage[2]``` is always ``` undefined ```
Following this, one can implement the following callback:
``` javascript
client.registerCallback (["Conn", "pushname"], json => {
const pushname = json[1].pushname
client.userMetaData.name = pushname // update on client too
console.log ("Name updated: " + pushname)
})
```
This callback will be fired any time a message is received matching the following criteria:
``` message [0] === "Conn" && message [1].pushname ```
A little more testing will reveal that almost all WhatsApp messages are in the format illustrated above.
Note: except for the first parameter (in the above cases, ```"action"``` or ```"Conn"```), all the other parameters are optional.
* __Example__
Do check out & run [example.js](example/example.js) to see example usage of these functions.
To run the example script, download or clone the repo and then type the following in terminal:
1. ``` cd path/to/Baileys/example ```
2. ``` node example.js ```
# Note
I am in no way affiliated with WhatsApp. This was written for educational purposes. Use at your own discretion.

View File

@@ -1,45 +1,83 @@
/*
Contains the code for sending queries to WhatsApp
*/
module.exports = function(WhatsAppWeb) {
// check if given number is registered on WhatsApp
WhatsAppWeb.prototype.isOnWhatsApp = function (jid) {
return this.query(["query", "exist", jid]).then (([m, q]) => [m.status === 200, q[2]])
}
// check the presence status of a given jid
WhatsAppWeb.prototype.requestPresenceUpdate = function (jid) {
module.exports = {
/**
* Query whether a given number is registered on WhatsApp
* @param {string} jid the number you want to query, format as [number]@s.whatsapp.net
* @return {Promise<[boolean, string]>} Promise with an array [exists, jid]
*/
isOnWhatsApp: function (jid) {
return this.query(["query", "exist", jid]).then (([m, _]) => [m.status === 200, jid])
},
/**
* @param {string} jid the whatsapp ID of the person
* @return {Promise<[object, object]>}
*/
requestPresenceUpdate: function (jid) {
return this.query(["action","presence","subscribe",jid])
}
// check the presence status of a given jid
WhatsAppWeb.prototype.getStatus = function (jid) {
},
/**
* Query the status of the person
* @param {string} [jid] the whatsapp ID of the person
* @return {Promise<[object, object]>}
*/
getStatus: function (jid) {
jid = jid ?? this.userMetaData.id
return this.query(["query","Status",jid])
}
// check the presence status of a given jid
WhatsAppWeb.prototype.getProfilePicture = function (jid) {
if (!jid) {
jid = this.userMetaData.id
}
},
/**
* Get the URL to download the profile picture of a person/group
* @param {string} [jid] the whatsapp ID of the person/group (will get your own picture if not specified)
* @return {Promise<[object, object]>}
*/
getProfilePicture: function (jid) {
jid = jid ?? this.userMetaData.id
return this.query(["query","ProfilePicThumb",jid])
}
// query all the contacts
WhatsAppWeb.prototype.getContactList = function () {
const json = [
"query",
{epoch: this.msgCount.toString(), type: "contacts"},
null
]
return this.query(json, [10, 64]) // this has to be an encrypted query
}
// load messages from a group or sender
WhatsAppWeb.prototype.getMessages = function (jid, count, indexMessage=null, mode="before") {
},
/**
* @return {Promise<[object, object]>}
*/
getContacts: function () {
const json = ["query", {epoch: this.msgCount.toString(), type: "contacts"}, null]
return this.query(json, [10, 128]) // this has to be an encrypted query
},
/**
* @return {Promise<[object, object]>}
*/
getChats: function () {
const json = ["query", {epoch: this.msgCount.toString(), type: "chat"}, null]
return this.query(json, [10, 128]) // this has to be an encrypted query
},
/**
* Check if your phone is connected
* @param {number} timeoutMs max time for the phone to respond
* @return {Promise<[object, object]>}
*/
isPhoneConnected: function (timeoutMs=5000) {
return this.query (["admin", "test"], null, timeoutMs)
.then (([json, q]) => json[1])
.catch (err => false)
},
/**
* Load the conversation with a group or person
* @param {string} jid the id of the group or person
* @param {number} count the number of messages to load
* @param {object} [indexMessage] the data for which message to offset the query by
* @param {string} [indexMessage.id] the id of the message
* @param {object} [indexMessage.fromMe] whether the message was sent by yours truly
* @param {boolean} [mostRecentFirst] retreive the most recent message first or retreive from the converation start
* @return {Promise} Promise of the messages loaded
*/
loadConveration: function (jid, count, indexMessage=null, mostRecentFirst=true) {
// construct JSON
let json = [
"query",
{
{
epoch: this.msgCount.toString(),
type: "message",
jid: jid,
kind: mode,
kind: mostRecentFirst ? "before" : "after",
owner: "true",
count: count.toString()
},
@@ -50,20 +88,25 @@ module.exports = function(WhatsAppWeb) {
json[1].index = indexMessage.id
json[1].owner = indexMessage.fromMe ? "true" : "false"
}
return this.query(json, [10, 128])
}
// loads all the conversation you've had with given ID
WhatsAppWeb.prototype.getAllMessages = function (jid, onMessage, chunkSize=25, mode="before") {
},
/**
* Load the entire friggin conversation with a group or person
* @param {string} jid the id of the group or person
* @param {function} onMessage callback for every message retreived
* @param {number} [chunkSize] the number of messages to load in a single request
* @param {boolean} [mostRecentFirst] retreive the most recent message first or retreive from the converation start
*/
loadEntireConversation: function (jid, onMessage, chunkSize=25, mostRecentFirst=true) {
var offsetID = null
const loadMessage = () => {
return this.getMessages(jid, chunkSize, offsetID, mode)
return this.getMessages(jid, chunkSize, offsetID, mostRecentFirst)
.then (json => {
if (json[2]) {
// callback with most recent message first (descending order of date)
let lastMessage
if (mode === "before") {
if (mostRecentFirst) {
for (var i = json[2].length-1; i >= 0;i--) {
onMessage(json[2][i][2])
lastMessage = json[2][i][2]
@@ -87,5 +130,67 @@ module.exports = function(WhatsAppWeb) {
}
return loadMessage()
}
},
/**
* Create a group
* @param {string} title like, the title of the group
* @param {string[]} participants people to include in the group
* @return {Promise<[object, object]>}
*/
groupCreate: function (title, participants) {
return this.groupQuery ("create", null, title, participants)
},
/**
* Leave a group
* @param {string} jid the ID of the group
* @return {Promise<[object, object]>}
*/
groupLeave: function (jid) {
return this.groupQuery ("leave", jid)
},
/**
* Update the title of the group
* @param {string} jid the ID of the group
* @param {string} title the new title of the group
* @return {Promise<[object, object]>}
*/
groupUpdateTitle: function (jid, title) {
return this.groupQuery ("subject", jid, title)
},
/**
* Add somebody to the group
* @param {string} jid the ID of the group
* @param {string[]} participants the people to add
* @return {Promise<[object, object]>}
*/
groupAdd: function (jid, participants) {
return this.groupQuery ("add", jid, null, participants)
},
/**
* Remove somebody from the group
* @param {string} jid the ID of the group
* @param {string[]} participants the people to remove
* @return {Promise<[object, object]>}
*/
groupRemove: function (jid, participants) {
return this.groupQuery ("remove", jid, null, participants)
},
/**
* Make someone admin on the group
* @param {string} jid the ID of the group
* @param {string[]} participants the people to make admin
* @return {Promise<[object, object]>}
*/
groupMakeAdmin: function (jid, participants) {
return this.groupQuery ("promote", jid, null, participants)
},
/**
* Get the invite link of the group
* @param {string} jid the ID of the group
* @return {Promise<string>}
*/
groupInviteCode: function (jid) {
const json = ["query", "inviteCode", jid]
return this.query (json).then (([json, _]) => json.code)
}
}

View File

@@ -1,17 +1,18 @@
const Utils = require("./WhatsAppWeb.Utils")
const HKDF = require("futoin-hkdf")
const fs = require("fs")
const fetch = require("node-fetch")
/*
Contains the code for recieving messages and forwarding what to do with them to the correct functions
*/
module.exports = function(WhatsAppWeb) {
const Status = WhatsAppWeb.Status
WhatsAppWeb.prototype.onMessageRecieved = function (message) {
if (message[0] === "!") { // when the first character in the message is an '!', the server is updating on the last seen
module.exports = {
/**
* Called when a message is recieved on the socket
* @private
* @param {string|buffer} message
* @param {function(any)} reject
*/
onMessageRecieved: function (message) {
if (message[0] === "!") { // when the first character in the message is an '!', the server is updating the last seen
const timestamp = message.slice(1,message.length)
this.lastSeen = new Date( parseInt(timestamp) )
} else {
@@ -24,8 +25,7 @@ module.exports = function(WhatsAppWeb) {
var data = message.slice(commaIndex+1, message.length)
// get the message tag.
// If a query was done, the server will respond with the same message tag we sent the query with
const messageTag = message.slice(0, commaIndex)
//console.log(messageTag)
const messageTag = message.slice(0, commaIndex).toString ()
if (data.length === 0) {
// got an empty message, usually get one after sending a query with the 128 tag
return
@@ -34,14 +34,11 @@ module.exports = function(WhatsAppWeb) {
let json
if (data[0] === "[" || data[0] === "{") { // if the first character is a "[", then the data must just be plain JSON array or object
json = JSON.parse( data ) // parse the JSON
//console.log("JSON: " + data)
} else if (this.status === Status.connected) {
} else if (this.authInfo.macKey && this.authInfo.encKey) {
/*
If the data recieved was not a JSON, then it must be an encrypted message.
Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys
*/
data = Buffer.from(data, 'binary') // convert the string to a buffer
*/
const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message
data = data.slice(32, data.length) // the actual message
@@ -51,224 +48,157 @@ module.exports = function(WhatsAppWeb) {
const decrypted = Utils.aesDecrypt(data, this.authInfo.encKey) // decrypt using AES
json = this.decoder.read( decrypted ) // decode the binary message into a JSON array
} else {
return this.gotError([7, "checksums don't match"])
this.unexpectedDisconnect([7, "checksums don't match"])
return
}
//console.log("enc_json: " + JSON.stringify(json))
} else {
// if we recieved a message that was encrypted but we weren't conencted, then there must be an error
return this.gotError([3, "recieved encrypted message when not connected: " + this.status, message])
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
this.unexpectedDisconnect([3, "recieved encrypted message when auth creds not available", message])
return
}
// the first item in the recieved JSON, if it exists, it tells us what the message is about
switch (json[0]) {
case "Conn":
/*
we get this message when a new connection is established,
whether we're starting a new session or are logging back in.
Sometimes, we also recieve it when one opens their phone
*/
this.validateNewConnection(json[1])
return
case "Cmd":
/*
WhatsApp usually sends this when we're trying to restore a closed session,
WhatsApp will challenge us to see whether we still have the keys
*/
if (json[1].type === "challenge") { // if it really is a challenge
this.respondToChallenge(json[1].challenge)
}
return
case "action":
/*
this is when some action was taken on a chat or that we recieve a message.
json[1] tells us more about the message, it can be null
*/
//console.log (JSON.stringify (json))
if (!json[1]) { // if json[1] is null
json = json[2][0] // set json to the first element in json[2]; it contains the relevant part
if (json[0] === "read") { // if one marked a chat as read or unread on the phone
const id = json[1].jid.replace("@c.us", "@s.whatsapp.net") // format the sender's ID
if (this.chats[id] && json[1].type === 'false') { // if it was marked unread
this.chats[id].user.count = 1 // up the read count
this.clearUnreadMessages(id) // send notification to the handler about the unread message
} else { // if it was marked read
this.chats[id].user.count = 0 // set the read count to zero
}
}
} else if (json[1].add === "relay") { // if we just recieved a new message sent to us
json = json[2][0]
if (json[0] === "received") {
if (json[1].owner) {
let type
switch (json[1].type) {
case "read":
type = WhatsAppWeb.MessageStatus.read
break
case "message":
type = WhatsAppWeb.MessageStatus.received
break
default:
type = json[1].type
break
}
if (this.handlers.onMessageStatusChanged) {
this.handlers.onMessageStatusChanged (json[1].jid, json[1].index, type)
}
}
} else if (json[0] === "message") {
this.onNewMessage(json[2]) // handle this new message
}
} else if (json[1].add === "before" || json[1].add === "last") {
/*
if we're recieving a full chat log
if json[1].add equals before: if its non-recent messages
if json[1].add equals last: contains the last message of the conversation
*/
json = json[2] // json[2] is the relevant part
/* reverse for loop, because messages are sent ordered by most recent
I can order them by recency if I add them in reverse order */
for (var k = json.length-1;k >= 0;k--) {
const message = json[k]
const id = message[2].key.remoteJid
if (!this.chats[ id ]) { // if we haven't added this ID before, add them now
this.chats[ id ] = {user: { jid: id, count: 0 }, messages: []}
}
this.chats[id].messages.push(message[2]) // append this message to the array
}
const id = json[0][2].key.remoteJid // get the ID whose chats we just processed
this.clearUnreadMessages(id) // forward to the handler any any unread messages
}
return
case "response":
//console.log(json[1])
// if it is the list of all the people the WhatsApp account has chats with
if (json[1].type === "chat") {
json[2].forEach (chat => {
if (chat[0] === "chat" && chat[1].jid) {
const jid = chat[1].jid.replace("@c.us", "@s.whatsapp.net") // format ID
this.chats[ jid ] = {
user: {
jid: jid, // the ID of the person
count: chat[1].count}, // number of unread messages we have from them
messages: [ ] // empty messages, is filled by content in the previous section
}
}
})
return
} else if (json[1].type === "contacts") {
if (this.handlers.gotContact) {
this.handlers.gotContact(json[2])
}
return
} else if (json[1].type === "message") {
}
break
case "Presence":
if (this.handlers.presenceUpdated) {
this.handlers.presenceUpdated(json[1].id, json[1].type)
}
return
default:
break
}
/*
if the recieved JSON wasn't an array, then we must have recieved a status for a request we made
Check if this is a response to a message we sent
*/
if (this.status === Status.connected) {
// if this message is responding to a query
if (this.callbacks[messageTag]) {
const q = this.callbacks[messageTag]
q.callback([json, q.queryJSON])
delete this.callbacks[messageTag]
if (this.callbacks[messageTag]) {
const q = this.callbacks[messageTag]
//console.log (messageTag + ", " + q.queryJSON)
q.callback([json, q.queryJSON])
delete this.callbacks[messageTag]
return
}
/*
Check if this is a response to a message we are expecting
*/
if (this.callbacks["function:" + json[0]]) {
let callbacks = this.callbacks["function:" + json[0]]
var callbacks2
var callback
for (var key in json[1] ?? {}) {
callbacks2 = callbacks[key + ":" + json[1][key]]
if (callbacks2) { break }
}
} else {
// if we're trying to establish a new connection or are trying to log in
switch (json.status) {
case 200: // all good and we can procede to generate a QR code for new connection, or can now login given present auth info
if (this.status === Status.creatingNewConnection){ // if we're trying to start a connection
if (this.authInfo.encKey && this.authInfo.macKey) { // if we have the info to restore a closed session
this.status = Status.loggingIn
// create the login request
const data = ["admin", "login", this.authInfo.clientToken, this.authInfo.serverToken, this.authInfo.clientID, "takeover"]
this.sendJSON( data )
} else {
this.generateKeysForAuth(json.ref)
}
}
break
case 401: // if the phone was unpaired
this.close()
return this.gotError([json.status, "unpaired from phone", message])
case 429: // request to login was denied, don't know why it happens
this.close()
return this.gotError([ json.status, "request denied, try reconnecting", message ])
case 304: // request to generate a new key for a QR code was denied
console.log("reuse previous ref")
return this.gotError([ json.status, "request for new key denied", message ])
default:
return this.gotError([ json.status, "unknown error", message ])
if (!callbacks2) {
for (var key in json[1] ?? {}) {
callbacks2 = callbacks[key]
if (callbacks2) { break }
}
}
if (!callbacks2) {
callbacks2 = callbacks[""]
}
if (callbacks2) {
callback = callbacks2[ json[2] && json[2][0][0] ]
if (!callback) {
callback = callbacks2[""]
}
}
if (callback) {
callback (json)
return
}
}
}
}
// shoot off notifications to the handler that new unread message are available
WhatsAppWeb.prototype.clearUnreadMessages = function (id) {
const chat = this.chats[id] // get the chat
var j = 0
let unreadMessages = chat.user.count
while (unreadMessages > 0 && chat.messages[j]) {
if (!chat.messages[j].key.fromMe && this.handlers.onUnreadMessage) { // only forward if the message is from the sender
this.handlers.onUnreadMessage( chat.messages[j] ) // send off the unread message
unreadMessages -= 1 // reduce
if (this.logUnhandledMessages) {
this.log("[Unhandled] " + messageTag + ", " + JSON.stringify(json))
}
j += 1
}
}
// when a new message is recieved
WhatsAppWeb.prototype.onNewMessage = function (message) {
if (message && message.message) { // confirm that the message really is valid
if (!this.chats[message.key.remoteJid]) { // if we don't have any chats from this ID before, add them to our DB
this.chats[message.key.remoteJid] = {
user: { jid: message.key.remoteJid, count: 0 },
messages: [ message ]
}
} else {
// if the chat was already there, then insert the message at the front of the array
this.chats[ message.key.remoteJid ].messages.splice(0, 0, message)
}
if (!message.key.fromMe && this.handlers.onUnreadMessage) { // if this message was sent to us, notify the handler
this.handlers.onUnreadMessage ( message )
}
},
/**
* Type of notification
* @param {object} message
* @param {object} [message.message] should be present for actual encrypted messages
* @param {object} [message.messageStubType] should be present for group add, leave etc. notifications
* @return {[string, string]} [type of notification, specific type of message]
*/
getNotificationType: function (message) {
if (message.message) {
return ["message", Object.keys(message.message)[0]]
} else if (message.messageStubType) {
return [WhatsAppWeb.MessageStubTypes[message.messageStubType] , null]
} else {
return ["unknown", null]
}
},
/**
* 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
* @return {promise<object>} when the function is received
*/
registerCallbackOneTime: function (parameters) {
return new Promise ((resolve, reject) => this.registerCallback (parameters, resolve))
.then (json => {
this.deregisterCallback (parameters)
return json
})
},
/**
* Register for a callback for a certain function
* @param {[string, string, string] | string} parameters name of the function along with some optional specific parameters
* @param {function(any)} callback
*/
registerCallback: function (parameters, callback) {
if (typeof parameters === "string") {
return this.registerCallback ([parameters], callback)
}
if (!Array.isArray (parameters)) {
throw "parameters (" + parameters + ") must be a string or array"
}
const func = "function:" + parameters[0]
const key = parameters[1] ?? ""
const key2 = parameters[2] ?? ""
if (!this.callbacks[func]) {
this.callbacks[func] = {}
}
if (!this.callbacks[func][key]) {
this.callbacks[func][key] = {}
}
this.callbacks[func][key][key2] = callback
},
/**
* Cancel all further callback events associated with the given parameters
* @param {[string, object, string] | string} parameters name of the function along with some optional specific parameters
*/
deregisterCallback: function (parameters) {
if (typeof parameters === "string") {
return this.deregisterCallback ([parameters])
}
if (!Array.isArray (parameters)) {
throw "parameters (" + parameters + ") must be a string or array"
}
const func = "function:" + parameters[0]
const key = parameters[1] ?? ""
const key2 = parameters[2] ?? ""
if (this.callbacks[func] && this.callbacks[func][key] && this.callbacks[func][key][key2]) {
delete this.callbacks[func][key][key2]
return
}
this.log ("WARNING: could not find " + JSON.stringify (parameters) + " to deregister")
},
/**
* Wait for a message with a certain tag to be received
* @param {string} tag the message tag to await
* @param {object} [json] query that was sent
* @param {number} [timeoutMs] timeout after which the promise will reject
*/
waitForMessage: function (tag, json, timeoutMs) {
const promise = new Promise((resolve, reject) =>
this.callbacks[tag] = {queryJSON: json, callback: resolve, errCallback: reject})
if (timeoutMs) {
return Utils.promiseTimeout (timeoutMs, promise)
.catch (err => {
delete this.callbacks[tag]
throw err
})
} else {
return promise
}
}
// get what type of message it is
WhatsAppWeb.prototype.getMessageType = function (message) {
return Object.keys(message)[0]
/*for (var key in WhatsAppWeb.MessageType) {
if (WhatsAppWeb.MessageType[key] === relvantKey) {
return key
}
}*/
}
// decode a media message (video, image, document, audio) & save it to the given file; returns a promise with metadata
WhatsAppWeb.prototype.decodeMediaMessage = function (message, fileName) {
},
/**
* Decode a media message (video, image, document, audio) & save it to the given file
* @param {object} message the media message you want to decode
* @param {string} filename the name of the file where the media will be saved
* @return {Promise<object>} promise once the file is successfully saved, with the metadata
*/
decodeMediaMessage: function (message, filename) {
const getExtension = function (mimetype) {
const str = mimetype.split(";")[0].split("/")
return str[1]
@@ -277,7 +207,7 @@ module.exports = function(WhatsAppWeb) {
can infer media type from the key in the message
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
*/
let type = this.getMessageType(message)
let type = Object.keys(message)[0]
if (!type) {
return Promise.reject("unknown message type")
}
@@ -293,31 +223,30 @@ module.exports = function(WhatsAppWeb) {
const macKey = mediaKeys.macKey
// download the message
let p = fetch(message.url).then (res => res.buffer())
p = p.then(buffer => {
// first part is actual file
let file = buffer.slice(0, buffer.length-10)
// last 10 bytes is HMAC sign of file
let mac = buffer.slice(buffer.length-10, buffer.length)
// sign IV+file & check for match with mac
let testBuff = Buffer.concat([iv, file])
let sign = Utils.hmacSign(testBuff, macKey).slice(0, 10)
// our sign should equal the mac
if (sign.equals(mac)) {
let decrypted = Utils.aesDecryptWithIV(file, cipherKey, iv) // decrypt media
return fetch(message.url).then (res => res.buffer())
.then(buffer => {
// first part is actual file
let file = buffer.slice(0, buffer.length-10)
// last 10 bytes is HMAC sign of file
let mac = buffer.slice(buffer.length-10, buffer.length)
// sign IV+file & check for match with mac
let testBuff = Buffer.concat([iv, file])
let sign = Utils.hmacSign(testBuff, macKey).slice(0, 10)
// our sign should equal the mac
if (sign.equals(mac)) {
let decrypted = Utils.aesDecryptWithIV(file, cipherKey, iv) // decrypt media
const trueFileName = fileName + "." + getExtension(message.mimetype)
fs.writeFileSync(trueFileName, decrypted)
const trueFileName = filename + "." + getExtension(message.mimetype)
fs.writeFileSync(trueFileName, decrypted)
message.fileName = trueFileName
return message
} else {
throw "HMAC sign does not match"
}
})
return p
message.filename = trueFileName
return message
} else {
throw "HMAC sign does not match"
}
})
}
}

View File

@@ -1,35 +1,48 @@
const Utils = require("./WhatsAppWeb.Utils")
const fetch = require('node-fetch')
/*
Contains the code for sending stuff to WhatsApp
*/
module.exports = function(WhatsAppWeb) {
// send a read receipt to the given ID on a certain message
WhatsAppWeb.prototype.sendReadReceipt = function (jid, messageID) {
module.exports = {
/**
* Send a read receipt to the given ID for a certain message
* @param {string} jid the ID of the person/group whose message you read
* @param {string} messageID the message ID
* @return {Promise<[object, object]>}
*/
sendReadReceipt: function (jid, messageID) {
const json = [
"action",
{ epoch: this.msgCount.toString(), type: "set" },
[
["read", {count: "1", index: messageID, jid: jid, owner: "false"}, null]
]
[ ["read", {count: "1", index: messageID, jid: jid, owner: "false"}, null] ]
]
return this.query(json, [10, 128]) // encrypt and send off
}
// tell someone about your presence -- online, typing, offline etc.
WhatsAppWeb.prototype.updatePresence = function (jid, type) {
},
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param {string} jid the ID of the person/group who you are updating
* @param {string} type your presence
* @return {Promise<[object, object]>}
*/
updatePresence: function (jid, type) {
const json = [
"action",
{ epoch: this.msgCount.toString(), type: "set" },
[ ["presence", {type: type, to: jid}, null] ]
]
return this.query(json, [10, 128])
}
// send a text message to someone, optionally you can provide a quoted message & the timestamp for the message
WhatsAppWeb.prototype.sendTextMessage = function (id, txt, quoted=null, timestamp=null) {
return this.query(json, [10, 64])
},
/**
* Send a text message
* @param {string} id the JID of the person/group you're sending the message to
* @param {string} txt the actual text of the message
* @param {object} [quoted] the message you may wanna quote along with this message
* @param {Date} [timestamp] optionally set the timestamp of the message in Unix time MS
* @return {Promise<[object, object]>}
*/
sendTextMessage: function (id, txt, quoted, timestamp) {
if (typeof txt !== "string") {
return Promise.reject("")
return Promise.reject("expected text to be a string")
}
let message
if (quoted) {
@@ -48,9 +61,21 @@ module.exports = function(WhatsAppWeb) {
}
return this.sendMessage(id, message, timestamp)
}
// send a media message to someone, optionally you can provide a caption, thumbnail, mimetype & the timestamp for the message
WhatsAppWeb.prototype.sendMediaMessage = function (id, buffer, mediaType, info=null, timestamp=null) {
},
/**
* Send a media message
* @param {string} id the JID of the person/group you're sending the message to
* @param {Buffer} buffer the buffer of the actual media you're sending
* @param {string} mediaType the type of media, can be one of [imageMessage, documentMessage, stickerMessage, videoMessage]
* @param {Object} [info] object to hold some metadata or caption about the media
* @param {string} [info.caption] caption to go along with the media
* @param {string} [info.thumbnail] base64 encoded thumbnail for the media
* @param {string} [info.mimetype] specify the Mimetype of the media (required for document messages)
* @param {boolean} [info.gif] whether the media is a gif or not, only valid for video messages
* @param {Date} [timestamp] optionally set the timestamp of the message in Unix time MS
* @return {Promise<[object, object]>}
*/
sendMediaMessage: function (id, buffer, mediaType, info, timestamp) {
// path to upload the media
const mediaPathMap = {
imageMessage: "/mms/image",
@@ -70,13 +95,13 @@ module.exports = function(WhatsAppWeb) {
if (!info) {
info = {}
}
if (mediaType === WhatsAppWeb.MessageType.text || mediaType === WhatsAppWeb.MessageType.extendedText) {
if (mediaType === "conversation" || mediaType === "extendedTextMessage") {
return Promise.reject("use sendTextMessage() to send text messages")
}
if (mediaType === WhatsAppWeb.MessageType.document && !info.mimetype) {
if (mediaType === "documentMessage" && !info.mimetype) {
return Promise.reject("mimetype required to send a document")
}
if (mediaType === WhatsAppWeb.MessageType.sticker && info.caption) {
if (mediaType === "stickerMessage" && info.caption) {
return Promise.reject("cannot send a caption with a sticker")
}
if (!info.mimetype) {
@@ -125,21 +150,27 @@ module.exports = function(WhatsAppWeb) {
fileLength: buffer.length,
jpegThumbnail: info.thumbnail
}
if (mediaType === WhatsAppWeb.MessageType.video && info.gif) {
if (mediaType === "videoMessage" && info.gif) {
message[mediaType].gifPlayback = info.gif
}
//console.log(message)
return this.sendMessage(id, message, timestamp)
})
}
// generic send message construct
WhatsAppWeb.prototype.sendMessage = function (id, message, timestamp=null) {
},
/**
* Generic send message function
* @private
* @param {string} id who to send the message to
* @param {object} message like, the message
* @param {Date} [timestamp] timestamp for the message
* @return {Promise<[object, object]>} array of the recieved JSON & the query JSON
*/
sendMessage: function (id, message, timestamp) {
if (!timestamp) { // if no timestamp was provided,
timestamp = new Date() // set timestamp to now
}
timestamp = timestamp.getTime()/1000
const messageJSON = {
let messageJSON = {
key: {
remoteJid: id,
fromMe: true,
@@ -149,34 +180,79 @@ module.exports = function(WhatsAppWeb) {
messageTimestamp: timestamp,
status: "ERROR"
}
if (id.includes ("@g.us")) {
messageJSON.participant = this.userMetaData.id
}
const json = [
"action",
{epoch: this.msgCount.toString(), type: "relay"},
[ ["message", null, messageJSON] ]
[ ["message", null, messageJSON] ]]
return this.query(json, [16, 128], null, messageJSON.key.id)
},
/**
* Generic function for group queries
* @param {string} type the type of query
* @param {string} [jid] the id of the group
* @param {string} [subject] title to attach to the group
* @param {string[]} [participants] the people the query will affect
* @return {Promise<[object, object]>} array of the recieved JSON & the query JSON
*/
groupQuery: function (type, jid, subject, participants) {
let json = [
"group",
{
author: this.userMetaData.id,
id: Utils.generateMessageTag(),
type: type
},
null
]
return this.query(json, [16, 64])
}
// send query message to WhatsApp servers; returns a promise
WhatsAppWeb.prototype.query = function (json, binaryTags=null) {
const promise = new Promise((resolve, reject) => {
let tag
if (binaryTags) {
tag = this.sendBinary(json, binaryTags)
} else {
tag = this.sendJSON(json)
}
this.callbacks[tag] = {queryJSON: json, callback: resolve, errCallback: reject}
})
return promise
}
// send a binary message, the tags parameter tell WhatsApp what the message is all about
WhatsAppWeb.prototype.sendBinary = function (json, tags) {
if (participants) {
json[2] = participants.map (str => ["participant", {jid: str}, null])
}
if (jid) {
json[1].jid = jid
}
if (subject) {
json[1].subject = subject
}
json = [
"action",
{type: "set", epoch: this.msgCount.toString()},
[json]
]
return this.query (json, [10, 128])
},
/**
* Query something from the WhatsApp servers
* @param {any[]} json the query itself
* @param {[number, number]} [binaryTags] the tags to attach if the query is supposed to be sent encoded in binary
* @param {Number} [timeoutMs] timeout after which the query will be failed (set to null to disable a timeout)
* @param {string} [tag] the tag to attach to the message
* @return {Promise<[object, object]>} array of the recieved JSON & the query JSON
*/
query: function (json, binaryTags, timeoutMs, tag) {
if (binaryTags) {
tag = this.sendBinary(json, binaryTags, tag)
} else {
tag = this.sendJSON(json, tag)
}
return this.waitForMessage (tag, json, timeoutMs)
},
/**
* Send a binary encoded message
* @private
* @param {[string, object, [string, object, object][]]} json the message to encode & send
* @param {[number, number]} tags the binary tags to tell WhatsApp what the message is all about
* @param {string} [tag] the tag to attach to the message
* @return {string} the message tag
*/
sendBinary: function (json, tags, tag) {
const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format
var buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey
const sign = Utils.hmacSign(buff, this.authInfo.macKey) // sign the message using HMAC and our macKey
const tag = Utils.generateMessageTag()
tag = tag ?? Utils.generateMessageTag()
buff = Buffer.concat([
Buffer.from(tag + ","), // generate & prefix the message tag
Buffer.from(tags), // prefix some bytes that tell whatsapp what the message is about
@@ -185,17 +261,27 @@ module.exports = function(WhatsAppWeb) {
])
this.send(buff) // send it off
return tag
}
// send a JSON message to WhatsApp servers
WhatsAppWeb.prototype.sendJSON = function (json) {
const str = JSON.stringify(json)
const tag = Utils.generateMessageTag()
},
/**
* Send a plain JSON message to the WhatsApp servers
* @private
* @param {[any]} json the message to send
* @param {string} [tag] the tag to attach to the message
* @return {string} the message tag
*/
sendJSON: function (json, tag) {
const str = JSON.stringify(json)
tag = tag ?? Utils.generateMessageTag()
this.send(tag + "," + str)
return tag
}
WhatsAppWeb.prototype.send = function (m) {
},
/**
* Send some message to the WhatsApp servers
* @private
* @param {any} json the message to send
*/
send: function (m) {
this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages
this.conn.send( m )
}
}

View File

@@ -2,79 +2,171 @@ const WebSocket = require('ws')
const Curve = require ('curve25519-js')
const Utils = require('./WhatsAppWeb.Utils')
const QR = require('qrcode-terminal')
/*
Contains the code for connecting to WhatsApp Web, establishing a new session & logging back in
*/
module.exports = function (WhatsAppWeb) {
const Status = WhatsAppWeb.Status
// connect to the WhatsApp Web servers
WhatsAppWeb.prototype.connect = function () {
if (this.status != Status.notConnected) {
return this.gotError([1, "already connected or connecting"])
}
this.status = Status.connecting
this.conn = new WebSocket("wss://web.whatsapp.com/ws", {origin: "https://web.whatsapp.com"})
this.conn.on('open', () => this.onConnect())
this.conn.on('message', (m) => this.onMessageRecieved(m)) // in WhatsAppWeb.Recv.js
this.conn.on('error', (error) => { // if there was an error in the WebSocket
this.close()
this.gotError([20, error])
})
this.conn.on('close', () => { })
}
// once a connection has been successfully established
WhatsAppWeb.prototype.onConnect = function () {
console.log("connected to WhatsApp Web")
this.status = Status.creatingNewConnection
if (!this.authInfo) { // if no auth info is present, that is, a new session has to be established
this.authInfo = { clientID: Utils.generateClientID() } // generate a client ID
}
const data = ["admin", "init", WhatsAppWeb.version, WhatsAppWeb.browserDescriptions, this.authInfo.clientID, true]
this.sendJSON( data )
}
// restore a previously closed session using the given authentication information
WhatsAppWeb.prototype.login = function (authInfo) {
this.authInfo = {
clientID: authInfo.clientID,
serverToken: authInfo.serverToken,
clientToken: authInfo.clientToken,
encKey: Buffer.from( authInfo.encKey, 'base64' ),
macKey: Buffer.from( authInfo.macKey, 'base64' )
module.exports = {
/**
* Connect to WhatsAppWeb
* @param {object} [authInfo] credentials to log back in
* @param {number} [timeoutMs] timeout after which the connect will fail, set to null for an infinite timeout
* @return {promise<[object, any[], any[], any[]]>} returns [userMetaData, chats, contacts, unreadMessages]
*/
connect: function (authInfo, timeoutMs) {
// set authentication credentials if required
if (authInfo) {
this.authInfo = Object.assign ({}, authInfo) // copy credentials
this.authInfo.encKey = Buffer.from(authInfo.encKey, 'base64') // decode from base64
this.authInfo.macKey = Buffer.from(authInfo.macKey, 'base64')
}
// if we're already connected, throw an error
if (this.conn) {
return Promise.reject([1, "already connected or connecting"])
}
this.conn = new WebSocket("wss://web.whatsapp.com/ws", {origin: "https://web.whatsapp.com"})
this.connect()
}
// once the QR code is scanned and we can validate our connection,
// or we resolved the challenge when logging back in
WhatsAppWeb.prototype.validateNewConnection = function (json) {
const promise = new Promise ( (resolve, reject) => {
this.conn.on('open', () => {
this.conn.on('message', (m) => this.onMessageRecieved(m)) // in WhatsAppWeb.Recv.js
this.beginAuthentication ().then (resolve).catch (reject)
})
this.conn.on('error', error => { // if there was an error in the WebSocket
this.close()
reject (error)
})
})
if (timeoutMs) {
return Utils.promiseTimeout (timeoutMs, promise)
.catch (error => {
this.close()
throw error
})
} else {
return promise
}
},
/** once a connection has been successfully established
* @private
* @return {promise<object[]>}
*/
beginAuthentication: function () {
this.log("connected to WhatsApp Web")
const onValidationSuccess = () => {
this.userMetaData = {
id: json.wid, // one's WhatsApp ID [cc][number]@s.whatsapp.net
name: json.pushname, // name set on whatsapp
phone: json.phone // information about the phone one has logged in to
if (!this.authInfo.clientID) { // if no auth info is present, that is, a new session has to be established
this.authInfo = { clientID: Utils.generateClientID() } // generate a client ID
}
let chats = []
let contacts = []
let unreadMessages = []
let unreadMap = {}
const data = ["admin", "init", this.version, this.browserDescriptions, this.authInfo.clientID, true]
return this.query(data)
.then (([json, _]) => {
// we're trying to establish a new connection or are trying to log in
switch (json.status) {
case 200: // all good and we can procede to generate a QR code for new connection, or can now login given present auth info
if (this.authInfo.encKey && this.authInfo.macKey) { // if we have the info to restore a closed session
const data = ["admin", "login", this.authInfo.clientToken, this.authInfo.serverToken, this.authInfo.clientID, "takeover"]
return this.query(data, null, null, "s1") // wait for response with tag "s1"
} else {
return this.generateKeysForAuth(json.ref)
}
case 401: // if the phone was unpaired
throw [json.status, "unpaired from phone", message]
case 429: // request to login was denied, don't know why it happens
throw [json.status, "request denied, try reconnecting", message]
case 304: // request to generate a new key for a QR code was denied
throw [json.status, "request for new key denied", message]
default:
throw [json.status, "unknown error", message]
}
this.status = Status.CONNECTED
})
.then (([json, q]) => {
if (json[1] && json[1].challenge) { // if its a challenge request (we get it when logging in)
return this.respondToChallenge(json[1].challenge)
.then (([json, _]) => {
if (json.status !== 200) { // throw an error if the challenge failed
throw [json.status, "unknown error", json]
}
return this.waitForMessage ("s2", []) // otherwise wait for the validation message
})
} else { // otherwise just chain the promise further
return [json, q]
}
})
.then (([json, _]) => {
this.validateNewConnection (json[1])
this.log("validated connection successfully")
this.lastSeen = new Date() // set last seen to right now
this.startKeepAliveRequest() // start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
}) // validate the connection
.then (() => {
this.log ("waiting for chats & contacts") // wait for the message with chats
this.didConnectSuccessfully()
const waitForConvos = new Promise ((resolve, _) => {
const chatUpdate = (json) => {
const isLast = json[1].last
json = json[2]
for (var k = json.length-1;k >= 0;k--) {
const message = json[k][2]
const jid = message.key.remoteJid.replace ("@s.whatsapp.net", "@c.us")
if (!message.key.fromMe && unreadMap[jid] > 0) { // only forward if the message is from the sender
unreadMessages.push (message)
unreadMap[jid] -= 1 // reduce
}
}
if (isLast) {
// de-register the callbacks, so that they don't get called again
this.deregisterCallback (["action", "add:last"])
this.deregisterCallback (["action", "add:before"])
resolve ()
}
}
// 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)
})
const waitForChats = this.registerCallbackOneTime (["response", "type:chat"]).then (json => {
chats = json[2] // chats data (log json to see what it looks like)
chats.forEach (chat => unreadMap [chat[1].jid] = chat[1].count) // store the number of unread messages for each sender
})
const waitForContacts = this.registerCallbackOneTime (["response", "type:contacts"])
.then (json => contacts = json[2])
// wait for the chats & contacts to load
return Promise.all ([waitForConvos, waitForChats, waitForContacts])
})
.then (() => {
// now we're successfully connected
this.log("connected successfully")
// resolve the promise
return [this.userMetaData, chats, contacts, unreadMessages]
})
.catch (err => {
this.close ()
throw err
})
},
/**
* Once the QR code is scanned and we can validate our connection, or we resolved the challenge when logging back in
* @private
* @param {object} json
*/
validateNewConnection: function (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"), name: json.pushname, phone: json.phone}
return this.userMetaData
}
if (json.connected) { // only if we're connected
if (!json.secret) { // if we didn't get a secret, that is we don't need it
if (!json.secret) { // if we didn't get a secret, we don't need it, we're validated
return onValidationSuccess()
}
const secret = Buffer.from(json.secret, 'base64')
if (secret.length !== 144) {
return this.gotError([4, "incorrect secret length: " + secret.length])
throw [4, "incorrect secret length: " + secret.length]
}
// generate shared key from our private key & the secret shared by the server
const sharedKey = Curve.sharedKey( this.curveKeys.private, secret.slice(0, 32) )
@@ -87,13 +179,12 @@ module.exports = function (WhatsAppWeb) {
const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey)
if ( hmac.equals(secret.slice(32, 64)) ) { // computed HMAC should equal secret[32:64]
if (hmac.equals(secret.slice(32, 64))) { // computed HMAC should equal secret[32:64]
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
// they are encrypted using key: expandedKey[0:32]
const encryptedAESKeys = Buffer.concat([ expandedKey.slice(64, expandedKey.length), secret.slice(64, secret.length) ])
const decryptedKeys = Utils.aesDecrypt(encryptedAESKeys, expandedKey.slice(0,32))
// this data is required to restore closed sessions
// set the credentials
this.authInfo = {
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
@@ -101,106 +192,101 @@ module.exports = function (WhatsAppWeb) {
serverToken: json.serverToken,
clientID: this.authInfo.clientID
}
onValidationSuccess()
return onValidationSuccess()
} else { // if the checksums didn't match
this.close()
this.gotError([5, "HMAC validation failed"])
throw [5, "HMAC validation failed"]
}
} else { // if we didn't get the connected field (usually we get this message when one opens WhatsApp on their phone)
if (this.status !== Status.connected) { // and we're not already connected
this.close()
this.gotError([6, "json connection failed", json])
}
throw [6, "json connection failed", json]
}
}
/*
when logging back in (restoring a previously closed session), WhatsApp may challenge one to check if one still has the encryption keys
WhatsApp does that by asking for us to sign a string it sends with our macKey
},
/**
* When logging back in (restoring a previously closed session), WhatsApp may challenge one to check if one still has the encryption keys
* WhatsApp does that by asking for us to sign a string it sends with our macKey
* @private
*/
WhatsAppWeb.prototype.respondToChallenge = function (challenge) {
respondToChallenge: function (challenge) {
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
console.log( "resolving challenge" )
this.sendJSON( data )
}
/*
when starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends
this.log("resolving login challenge")
return this.query(data)
},
/**
* When starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends
* @private
*/
WhatsAppWeb.prototype.generateKeysForAuth = function (ref) {
generateKeysForAuth: function (ref) {
this.curveKeys = Curve.generateKeyPair( Utils.randomBytes(32) )
const publicKeyStr = Buffer.from(this.curveKeys.public).toString('base64')
//console.log ("private key: " + Buffer.from(this.curveKeys.private) )
let str = ref + "," + publicKeyStr + "," + this.authInfo.clientID
console.log("authenticating... Converting to QR: " + str)
this.log("authenticating... Converting to QR: " + str)
QR.generate(str, {small: true})
}
// send a keep alive request every 25 seconds, server updates & responds with last seen
WhatsAppWeb.prototype.startKeepAliveRequest = function () {
return this.waitForMessage ("s1", [])
},
/**
* Send a keep alive request every X seconds, server updates & responds with last seen
* @private
*/
startKeepAliveRequest: function () {
const refreshInterval = 20
this.keepAliveReq = setInterval(() => {
const diff = (new Date().getTime()-this.lastSeen.getTime())/1000
/*
check if it's been a suspicious amount of time since the server responded with our last seen
could be that the network is down, or the phone got disconnected or unpaired
it could be that the network is down, or the phone got unpaired from our connection
*/
if (diff > 25+10) {
console.log("disconnected")
if (diff > refreshInterval+5) {
this.close()
if (this.handlers.onDisconnect)
this.handlers.onDisconnect()
if (this.autoReconnect) { // attempt reconnecting if the user wants us to
// keep trying to connect
this.reconnectLoop = setInterval( () => {
// only connect if we're not already in the prcoess of connectin
if (this.status === Status.notConnected) {
this.connect()
}
}, 10 * 1000)
this.log("disconnected unexpectedly, reconnecting...")
const reconnectLoop = () => this.connect (null, 25*1000).catch (reconnectLoop)
reconnectLoop () // keep trying to connect
} else {
this.unexpectedDisconnect ("lost connection unexpectedly")
}
} else { // if its all good, send a keep alive request
this.send( "?,," )
this.send("?,,")
}
}, 25 * 1000)
}
// disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
// use close() if you just want to close the connection
WhatsAppWeb.prototype.disconnect = function () {
if (this.status === Status.connected) {
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => {
this.close()
if (this.handlers.onDisconnect)
this.handlers.onDisconnect()
})
} else if (this.conn) {
this.close()
}
}
// close the connection
WhatsAppWeb.prototype.close = function () {
this.conn.close()
this.conn = null
this.status = Status.notConnected
}, refreshInterval * 1000)
},
/**
* Disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
* Use close() if you just want to close the connection
* @return {Promise<void>}
*/
logout: function () {
return new Promise ( (resolve, reject) => {
if (this.conn) {
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => {
this.authInfo = {}
resolve ()
})
} else {
throw "You're not even connected, you can't log out"
}
})
.then (() => this.close ())
},
/** Close the connection to WhatsApp Web */
close: function () {
this.msgCount = 0
this.chats = {}
if (this.conn) {
this.conn.close()
this.conn = null
}
const keys = Object.keys (this.callbacks)
keys.forEach (key => {
if (!key.includes ("function:")) {
this.callbacks[key].reject ("connection closed")
delete this.callbacks[key]
}
} )
if (this.keepAliveReq) {
clearInterval(this.keepAliveReq)
}
}
// request a new QR code from the server (HAVEN'T TESTED THIS OUT YET)
WhatsAppWeb.prototype.requestNewQRCode = function () {
if (this.status !== Status.creatingNewConnection) { // if we're not in the process of connecting
return
}
this.sendJSON(["admin", "Conn", "reref"])
}
}

View File

@@ -104,7 +104,16 @@ module.exports = {
},
// generate a buffer with random bytes of the specified length
randomBytes: function (length) { return Crypto.randomBytes(length) },
promiseTimeout: function(ms, promise) {
// Create a promise that rejects in <ms> milliseconds
let timeout = new Promise((_, reject) => {
let id = setTimeout(() => {
clearTimeout(id)
reject('Timed out')
}, ms)
})
return Promise.race([promise, timeout])
},
// whatsapp requires a message tag for every message, we just use the timestamp as one
generateMessageTag: function () { return new Date().getTime().toString() },
// generate a random 16 byte client ID

View File

@@ -1,19 +1,9 @@
const BinaryCoding = require('./binary_coding/binary_encoder.js')
class WhatsAppWeb {
static version = [0,4,1296] // the version of WhatsApp Web we're telling the servers we are
static browserDescriptions = ["Baileys", "Baileys"]
static Status = {
notConnected: 0,
connecting: 1,
creatingNewConnection: 3,
loggingIn: 4,
connected: 5
}
// set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send
/**
* set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send
*/
static Presence = {
available: "available", // "online"
unavailable: "unavailable", // "offline"
@@ -21,13 +11,17 @@ class WhatsAppWeb {
recording: "recording", // "recording..."
paused: "paused" // I have no clue
}
// set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send
/**
* Status of a message sent or received
*/
static MessageStatus = {
sent: "sent",
received: "received",
read: "read"
}
// set of message types that are supported by the library
/**
* set of message types that are supported by the library
*/
static MessageType = {
text: "conversation",
image: "imageMessage",
@@ -35,57 +29,113 @@ class WhatsAppWeb {
sticker: "stickerMessage",
document: "documentMessage",
extendedText: "extendedTextMessage"
}
/**
* Tells us what kind of message it is
*/
static MessageStubTypes = {
20: "addedToGroup",
32: "leftGroup",
39: "createdGroup"
}
constructor() {
this.conn = null // the websocket connection
this.authInfo = null // the auth info used to extablish new connections & restore connections
this.userMetaData = null // metadata of the user i.e. name, phone number, phone stats
this.chats = {} // all chats of the user, mapped by the user ID
this.msgCount = 0 // number of messages sent to the server; required field for sending messages etc.
this.autoReconnect = true // reconnect automatically after an unexpected disconnect
this.lastSeen = null // updated by sending a keep alive request to the server, and the server responds with our updated last seen
// object to hold the event handlers
this.handlers = {
onError: null,
onConnected: null,
presenceUpdated: null,
onDisconnect: null,
onUnreadMessage: null,
gotContact: null,
onMessageStatusChanged: null
}
/** The version of WhatsApp Web we're telling the servers we are */
this.version = [0,4,1296]
this.browserDescriptions = ["Baileys", "Baileys"]
/** The websocket connection
* @private
*/
this.conn = null
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */
this.authInfo = {
clientID: null,
serverToken: null,
clientToken: null,
encKey: null,
macKey: null
}
/** Metadata like WhatsApp id, name set on WhatsApp etc. */
this.userMetaData = {id: null, name: null, phone: null}
/** @private */
this.msgCount = 0 // (epoch) number of messages sent to the server; required field for sending messages etc.
/** Shoud reconnect automatically after an unexpected disconnect */
this.autoReconnect = true //
/** @private */
this.lastSeen = null // updated by sending a keep alive request to the server, and the server responds with our updated last seen
/** Log messages that are not handled, so you can debug & see what custom stuff you can implement */
this.logUnhandledMessages = false
/** @private */
this.callbacks = {}
this.encoder = new BinaryCoding.Encoder()
this.decoder = new BinaryCoding.Decoder()
this.status = WhatsAppWeb.Status.notConnected
}
// error is a json array: [errorCode, "error description", optionalDescription]
gotError (error) {
this.handlers.onError(error) // tell the handler, we got an error
}
// called when established a connection to the WhatsApp servers successfully
didConnectSuccessfully () {
console.log("connected successfully")
this.status = WhatsAppWeb.Status.connected // update our status
this.lastSeen = new Date() // set last seen to right now
this.startKeepAliveRequest() // start sending keep alive requests (keeps the WebSocket alive & updates our last seen)
if (this.reconnectLoop) { // if we connected after being disconnected
clearInterval(this.reconnectLoop) // kill the loop to reconnect us
} else if (this.handlers.onConnected) { // if we connected for the first time, i.e. not after being disconnected
this.handlers.onConnected()
}
}
// base 64 encode the authentication credentials and return them, these can then be saved used to login again
// see login () in WhatsAppWeb.Session
this.decoder = new BinaryCoding.Decoder()
this.unexpectedDisconnect = (err) => { this.close () }
}
/**
* Set the callback for unexpected disconnects
* @param {function(object)} callback
*/
setOnUnexpectedDisconnect (callback) {
this.unexpectedDisconnect = (err) => {
this.close ()
callback (err)
}
}
/**
* Set the callback for message status updates (when a message is delivered, read etc.)
* @param {function(object)} callback
*/
setOnMessageStatusChange (callback) {
const func = (json) => {
json = json[1]
var ids = json.id
if (json.cmd === "ack") {
ids = [json.id]
}
const ackTypes = [
WhatsAppWeb.MessageStatus.sent,
WhatsAppWeb.MessageStatus.received,
WhatsAppWeb.MessageStatus.read
]
const data = {
from: json.from,
to: json.to,
participant: json.participant,
timestamp: new Date (json.t*1000),
ids: ids,
type: ackTypes[json.ack-1] ?? "unknown (" + json.ack + ")"
}
callback (data)
}
this.registerCallback ("Msg", func)
this.registerCallback ("MsgInfo", func)
}
/**
* Set the callback for new/unread messages, if someone sends a message, this callback will be fired
* @param {function(object)} callback
*/
setOnUnreadMessage (callback) {
this.registerCallback (["action", "add:relay", "message"], (json) => {
const message = json[2][0][2]
if (!message.key.fromMe) { // if this message was sent to us, notify
callback (message)
}
})
}
/**
* Set the callback for presence updates; if someone goes offline/online, this callback will be fired
* @param {function(object)} callback
*/
setOnPresenceUpdate (callback) {
this.registerCallback ("Presence", (json) => callback(json[1]))
}
/**
* base 64 encode the authentication credentials and return them
* these can then be used to login again by passing the object to the connect () function.
* @see connect () in WhatsAppWeb.Session
*/
base64EncodedAuthInfo () {
return {
clientID: this.authInfo.clientID,
@@ -93,14 +143,114 @@ class WhatsAppWeb {
clientToken: this.authInfo.clientToken,
encKey: this.authInfo.encKey.toString('base64'),
macKey: this.authInfo.macKey.toString('base64')
}
}
}
log (text) {
console.log ("[Baileys] " + text)
}
}
/* import the rest of the code */
require("./WhatsAppWeb.Session.js")(WhatsAppWeb)
require("./WhatsAppWeb.Recv.js")(WhatsAppWeb)
require("./WhatsAppWeb.Send.js")(WhatsAppWeb)
require("./WhatsAppWeb.Query")(WhatsAppWeb)
/* Import the rest of the code */
const recv = require("./WhatsAppWeb.Recv")
/** Called when a message is recieved on the socket */
WhatsAppWeb.prototype.onMessageRecieved = recv.onMessageRecieved
/** The type of notification one recieved */
WhatsAppWeb.prototype.getNotificationType = recv.getNotificationType
/** Register for a callback for a certain function, will cancel automatically after one execution */
WhatsAppWeb.prototype.registerCallbackOneTime = recv.registerCallbackOneTime
/** Register for a callback for a certain function */
WhatsAppWeb.prototype.registerCallback = recv.registerCallback
/** Cancel all further callback events associated with the given parameters */
WhatsAppWeb.prototype.deregisterCallback = recv.deregisterCallback
/** Wait for a message with a certain tag to be received */
WhatsAppWeb.prototype.waitForMessage = recv.waitForMessage
/** Decode a media message (video, image, document, audio) & save it to the given file */
WhatsAppWeb.prototype.decodeMediaMessage = recv.decodeMediaMessage
const session = require("./WhatsAppWeb.Session")
WhatsAppWeb.prototype.connect = session.connect
WhatsAppWeb.prototype.beginAuthentication = session.beginAuthentication
WhatsAppWeb.prototype.validateNewConnection = session.validateNewConnection
WhatsAppWeb.prototype.respondToChallenge = session.respondToChallenge
WhatsAppWeb.prototype.generateKeysForAuth = session.generateKeysForAuth
WhatsAppWeb.prototype.startKeepAliveRequest = session.startKeepAliveRequest
WhatsAppWeb.prototype.logout = session.logout
WhatsAppWeb.prototype.close = session.close
const send = require("./WhatsAppWeb.Send")
/** Send a read receipt to the given ID for a certain message */
WhatsAppWeb.prototype.sendReadReceipt = send.sendReadReceipt
/** Tell someone about your presence -- online, typing, offline etc.
* @see WhatsAppWeb.Presence for all presence types
*/
WhatsAppWeb.prototype.updatePresence = send.updatePresence
/**
* Send a text message
* @param {string} id the JID of the person/group you're sending the message to
* @param {string} txt the actual text of the message
* @param {object} [quoted] the message you may wanna quote along with this message
* @param {Date} [timestamp] optionally set the timestamp of the message in Unix time MS
* @return {Promise<[object, object]>}
*/
WhatsAppWeb.prototype.sendTextMessage = send.sendTextMessage
/**
* Send a media message
* @param {string} id the JID of the person/group you're sending the message to
* @param {Buffer} buffer the buffer of the actual media you're sending
* @param {string} mediaType the type of media, can be one of WhatsAppWeb.MessageType
* @param {Object} [info] object to hold some metadata or caption about the media
* @param {string} [info.caption] caption to go along with the media
* @param {string} [info.thumbnail] base64 encoded thumbnail for the media
* @param {string} [info.mimetype] specify the Mimetype of the media (required for document messages)
* @param {boolean} [info.gif] whether the media is a gif or not, only valid for video messages
* @param {Date} [timestamp] optionally set the timestamp of the message in Unix time MS
* @return {Promise<[object, object]>}
*/
WhatsAppWeb.prototype.sendMediaMessage = send.sendMediaMessage
/** @private */
WhatsAppWeb.prototype.sendMessage = send.sendMessage
/** Generic function for group related queries */
WhatsAppWeb.prototype.groupQuery = send.groupQuery
/** Query something from the WhatsApp servers */
WhatsAppWeb.prototype.query = send.query
/** @private */
WhatsAppWeb.prototype.sendBinary = send.sendBinary
/** @private */
WhatsAppWeb.prototype.sendJSON = send.sendJSON
/** @private */
WhatsAppWeb.prototype.send = send.send
const query = require("./WhatsAppWeb.Query")
/** Query whether a given number is registered on WhatsApp */
WhatsAppWeb.prototype.isOnWhatsApp = query.isOnWhatsApp
/** Check the presence of a given person (online, offline) */
WhatsAppWeb.prototype.requestPresenceUpdate = query.requestPresenceUpdate
/** Query the status of the person */
WhatsAppWeb.prototype.getStatus = query.getStatus
/** Get the URL to download the profile picture of a person/group */
WhatsAppWeb.prototype.getProfilePicture = query.getProfilePicture
/** Query all your contacts */
WhatsAppWeb.prototype.getContacts = query.getContacts
/** Query all the people/groups you have a chat history with */
WhatsAppWeb.prototype.getChats = query.getChats
/** Query whether your phone is still connected to this WhatsApp Web */
WhatsAppWeb.prototype.isPhoneConnected = query.isPhoneConnected
/** Load the conversation with a group or person */
WhatsAppWeb.prototype.loadConveration = query.loadConveration
/** Load the entire friggin conversation with a group or person */
WhatsAppWeb.prototype.loadEntireConversation = query.loadEntireConversation
/** Create a group */
WhatsAppWeb.prototype.groupCreate = query.groupCreate
/** Leave a group */
WhatsAppWeb.prototype.groupLeave = query.groupLeave
/** Add somebody to the group */
WhatsAppWeb.prototype.groupAdd = query.groupAdd
/** Remove somebody from the group */
WhatsAppWeb.prototype.groupRemove = query.groupRemove
/** Make somebody admin on the group */
WhatsAppWeb.prototype.groupMakeAdmin = query.groupMakeAdmin
/** Get the invite code of the group */
WhatsAppWeb.prototype.groupInviteCode = query.groupInviteCode
module.exports = WhatsAppWeb

View File

@@ -1,4 +1,4 @@
const WhatsAppWeb = require("./WhatsAppWeb")
const WhatsAppWeb = require("../WhatsAppWeb")
const fs = require("fs")
/**
@@ -7,11 +7,6 @@ const fs = require("fs")
* */
function extractChats (authCreds, outputFile, produceAnonData=false, offset=null) {
let client = new WhatsAppWeb() // instantiate an instance
if (authCreds) { // login if creds are present
client.login(authCreds)
} else { // create a new connection otherwise
client.connect()
}
// internal extract function
const extract = function () {
let rows = 0
@@ -40,7 +35,7 @@ function extractChats (authCreds, outputFile, produceAnonData=false, offset=null
var curInput = ""
var curOutput = ""
var lastMessage
return client.getAllMessages (id, m => {
return client.loadEntireConversation (id, m => {
var text
if (!m.message) { // if message not present, return
return
@@ -79,7 +74,7 @@ function extractChats (authCreds, outputFile, produceAnonData=false, offset=null
}
lastMessage = m
}, 50, "after") // load from the start, in chunks of 50
}, 50, false) // load from the start, in chunks of 50
.then (() => console.log("finished extraction for " + id))
.then (() => {
if (index+1 < chats.length) {
@@ -91,20 +86,12 @@ function extractChats (authCreds, outputFile, produceAnonData=false, offset=null
extractChat(0)
.then (() => {
console.log("extracted all; total " + rows + " rows")
client.disconnect ()
client.logout ()
})
}
client.handlers.onConnected = () => {
// start extracting 4 seconds after the connection
setTimeout(extract, 4000)
}
client.handlers.onUnreadMessage = (message) => {
}
client.handlers.onError = (error) => {
console.log("got error: " + error)
}
client.connect (authCreds)
.then (() => extract())
.catch (err => console.log("got error: " + error))
}
let creds = null//JSON.parse(fs.readFileSync("auth_info.json"))
extractChats(creds, "output.csv", true, "919820038582@s.whatsapp.net")
extractChats(creds, "output.csv")

View File

@@ -1,90 +1,88 @@
const WhatsAppWeb = require("../WhatsAppWeb")
const fs = require("fs")
let client = new WhatsAppWeb() // instantiate
const client = new WhatsAppWeb() // instantiate
client.autoReconnect = true // auto reconnect on disconnect
client.logUnhandledMessages = false // set to true to see what kind of stuff you can implement
var authInfo = null
try {
const file = fs.readFileSync("auth_info.json") // load a closed session back if it exists
const authInfo = JSON.parse(file)
client.login( authInfo ) // log back in using the info we just loaded
} catch {
// if no auth info exists, start a new session
client.connect() // start a new session, with QR code scanning and what not
}
// called once the client connects successfully to the WhatsApp servers
client.handlers.onConnected = () => {
authInfo = JSON.parse(file)
} catch { }
client.connect (authInfo, 30*1000) // connect or timeout in 30 seconds
.then (([user, chats, contacts, unread]) => {
console.log ("oh hello " + user.name + " (" + user.id + ")")
console.log ("you have " + unread.length + " unread messages")
console.log ("you have " + chats.length + " chats & " + contacts.length + " contacts")
const authInfo = client.base64EncodedAuthInfo() // get all the auth info we need to restore this session
fs.writeFileSync("auth_info.json", JSON.stringify(authInfo, null, "\t")) // save this info to a file
/*
Note: one can take this 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
*/
}
// called when someone's presence is updated
client.handlers.presenceUpdated = (id, type) => {
console.log("presence of " + id + " is " + type)
}
// called when your message gets delivered or read
client.handlers.onMessageStatusChanged = (id, messageID, status) => {
console.log(id + " acknowledged message '" + messageID + "' status as " + status)
}
// called when you have a pending unread message or recieve a new message
client.handlers.onUnreadMessage = (m) => {
// console.log("recieved message: " + JSON.stringify(m)) // uncomment to see what the raw message looks like
/* 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 */
const messageType = client.getMessageType(m.message) // get what type of message it is -- text, image, video
console.log("got message of type: " + messageType)
client.setOnPresenceUpdate (json => console.log(json.id + " presence is " + json.type))
client.setOnMessageStatusChange (json => {
const participant = json.participant ? " ("+json.participant+")" : "" // participant exists when the message is from a group
console.log(json.to + participant +
" acknowledged message(s) " + json.ids +
" as " + json.type + " at " + json.timestamp)
})
client.setOnUnreadMessage (m => {
const [notificationType, messageType] = client.getNotificationType(m) // get what type of notification it is -- message, group add notification etc.
console.log("got notification of type: " + notificationType)
if (messageType === WhatsAppWeb.MessageType.text) { // if it is plain text
const text = m.message.conversation
console.log (m.key.remoteJid + " sent: " + text)
} else if (messageType === WhatsAppWeb.MessageType.extendedText) { // if it is a quoted thing
const text = m.message.extendedTextMessage.text // the actual text
const quotedMessage = m.message.extendedTextMessage.contextInfo.quotedMessage // message that was replied to
console.log (m.key.remoteJid + " sent: " + text + " and quoted a " + client.getMessageType(quotedMessage))
} else { // if it is a media (audio, image, video) message
// decode, decrypt & save the media.
// The extension to the is applied automatically based on the media type
client.decodeMediaMessage(m.message, "media_in_" + m.key.id)
.then (meta => console.log(m.key.remoteJid + " sent media, saved at: " + meta.fileName))
.catch (err => console.log("error in decoding message: " + err))
}
console.log("responding...")
/* send a message after at least a 1 second timeout after recieving a message, otherwise WhatsApp will reject the message otherwise */
setTimeout(() => client.sendReadReceipt(m.key.remoteJid, m.key.id), 2*1000) // send a read reciept for the message in 2 seconds
setTimeout(() => client.updatePresence(m.key.remoteJid, WhatsAppWeb.Presence.composing), 2.5*1000) // let them know you're typing in 2.5 seconds
setTimeout(() => {
let promise
if (Math.random() > 0.5) { // choose at random
promise = client.sendTextMessage(m.key.remoteJid, "hello!", m) // send a "hello!" & quote the message recieved
} else {
const buffer = fs.readFileSync("./ma_gif.mp4") // load the gif
const info = {
gif: true, // the video is a gif
caption: "hello!" // the caption
}
promise = client.sendMediaMessage (m.key.remoteJid, buffer, WhatsAppWeb.MessageType.video, info) // send this gif!
if (notificationType !== "message") {
return
}
promise.then (([m, q]) => {
const success = m.status === 200
const messageID = q[2][0][2].key.id
console.log("sent message with ID '" + messageID + "' successfully: " + success)
})
}, 4*1000) // send after 4 seconds
}
// called if an error occurs
client.handlers.onError = (err) => console.log(err)
client.handlers.onDisconnect = () => { /* internet got disconnected, save chats here or whatever; will reconnect automatically */ }
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
let sender = m.key.remoteJid
if (m.key.participant) { // participant exists if the message is in a group
sender += " ("+m.key.participant+")"
}
if (messageType === WhatsAppWeb.MessageType.text) {
const text = m.message.conversation
console.log (sender + " sent: " + text)
} else if (messageType === WhatsAppWeb.MessageType.extendedText) {
const text = m.message.extendedTextMessage.text
console.log (sender + " sent: " + text + " and quoted message: " + JSON.stringify(m.message))
} else { // if it is a media (audio, image, video) message
// decode, decrypt & save the media.
// The extension to the is applied automatically based on the media type
client.decodeMediaMessage(m.message, "media_in_" + m.key.id)
.then (meta => console.log(sender + " sent media, saved at: " + meta.fileName))
.catch (err => console.log("error in decoding message: " + err))
}
// send a reply after 3 seconds
setTimeout (() => {
client.sendReadReceipt (m.key.remoteJid, m.key.id) // send read receipt
.then (() => client.updatePresence(m.key.remoteJid, WhatsAppWeb.Presence.available)) // tell them we're available
.then (() => client.updatePresence(m.key.remoteJid, WhatsAppWeb.Presence.composing)) // tell them we're composing
.then (() => { // send the message
if (Math.random() > 0.5) { // choose at random
return client.sendTextMessage(m.key.remoteJid, "hello!", m) // send a "hello!" & quote the message recieved
} else {
const buffer = fs.readFileSync("./ma_gif.mp4") // load the gif
const info = {
gif: true, // the video is a gif
caption: "hello!" // the caption
}
return client.sendMediaMessage (m.key.remoteJid, buffer, WhatsAppWeb.MessageType.video, info) // send this gif!
}
})
.then (([m, q]) => { // check if it went successfully
const success = m.status === 200
const messageID = q[2][0][2].key.id
console.log("sent message with ID '" + messageID + "' successfully: " + success)
})
}, 3*1000)
})
/* custom functionality for tracking battery */
client.registerCallback (["action", null, "battery"], json => {
const batteryLevelStr = json[2][0][1].value
const batterylevel = parseInt (batteryLevelStr)
console.log ("battery level: " + batterylevel)
})
client.setOnUnexpectedDisconnect (err => console.log ("disconnected unexpectedly: " + err) )
})
readline.question("type exit to disconnect\n", (txt) => {
if (txt === "exit") {
client.close()
process.exit(0)
}
})
.catch (err => console.log ("encountered error: " + err))