mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
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:
366
README.md
366
README.md
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 )
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
282
WhatsAppWeb.js
282
WhatsAppWeb.js
@@ -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
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user