diff --git a/.DS_Store b/.DS_Store index 09f4d9e..826a342 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 32bc978..a103886 100644 --- a/README.md +++ b/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. \ No newline at end of file diff --git a/WhatsAppWeb.Query.js b/WhatsAppWeb.Query.js index 9c5b993..3803f5a 100644 --- a/WhatsAppWeb.Query.js +++ b/WhatsAppWeb.Query.js @@ -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} + */ + groupInviteCode: function (jid) { + const json = ["query", "inviteCode", jid] + return this.query (json).then (([json, _]) => json.code) + } } \ No newline at end of file diff --git a/WhatsAppWeb.Recv.js b/WhatsAppWeb.Recv.js index 84d0440..e490a9b 100644 --- a/WhatsAppWeb.Recv.js +++ b/WhatsAppWeb.Recv.js @@ -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} 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} 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" + } + }) } } \ No newline at end of file diff --git a/WhatsAppWeb.Send.js b/WhatsAppWeb.Send.js index 923c61e..15af6b9 100644 --- a/WhatsAppWeb.Send.js +++ b/WhatsAppWeb.Send.js @@ -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 ) } - } \ No newline at end of file diff --git a/WhatsAppWeb.Session.js b/WhatsAppWeb.Session.js index 6f2deaf..fea856b 100644 --- a/WhatsAppWeb.Session.js +++ b/WhatsAppWeb.Session.js @@ -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} + */ + 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} + */ + 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"]) - } - } \ No newline at end of file diff --git a/WhatsAppWeb.Utils.js b/WhatsAppWeb.Utils.js index b52d9b5..99b37ec 100644 --- a/WhatsAppWeb.Utils.js +++ b/WhatsAppWeb.Utils.js @@ -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 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 diff --git a/WhatsAppWeb.js b/WhatsAppWeb.js index 1c11dde..747317c 100644 --- a/WhatsAppWeb.js +++ b/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 \ No newline at end of file diff --git a/ConversationExtract.js b/example/ConversationExtract.js similarity index 82% rename from ConversationExtract.js rename to example/ConversationExtract.js index 27fb15d..d1a7333 100644 --- a/ConversationExtract.js +++ b/example/ConversationExtract.js @@ -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") \ No newline at end of file +extractChats(creds, "output.csv") \ No newline at end of file diff --git a/example/example.js b/example/example.js index 083da7c..7e10a94 100644 --- a/example/example.js +++ b/example/example.js @@ -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) - } -}) \ No newline at end of file +.catch (err => console.log ("encountered error: " + err)) \ No newline at end of file