diff --git a/.DS_Store b/.DS_Store index ebf005c..1b30681 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 78d4e8a..1f2a37a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Baileys - 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. + 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. @@ -27,6 +27,9 @@ Baileys is super easy to use: ``` 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 */ } ``` @@ -105,13 +108,12 @@ Baileys is super easy to use: ``` This lets the person with ``` id ``` know your status. where ``` presence ``` can be one of the following: ``` javascript - static Presence = { - available: "available", // "online" - unavailable: "unavailable", // offline - composing: "composing", // "typing..." - recording: "recording", // "recording..." - paused: "paused" // I have no clue - } + [ + WhatsAppWeb.Presence.available, // online + WhatsAppWeb.Presence.unavailable, // offline + WhatsAppWeb.Presence.composing, // typing... + WhatsAppWeb.Presence.recording // recording... + ] ``` * Once you want to close your session, you can get your authentication credentials using: @@ -125,18 +127,46 @@ Baileys is super easy to use: client.login(authJSON) ``` This will use the credentials to connect & log back in. No need to call ``` connect() ``` after calling this function -* If you want to query whether a number is registered on WhatsApp, use: +* To query things like chat history and all, use: + * To check if a given ID is on WhatsApp ``` javascript - client.isOnWhatsApp ("[countrycode][some10digitnumber]@s.whatsapp.net") - .then ((exists, id) => { - if (exists) { - console.log(id + " is on WhatsApp") - } else { - console.log(id + " is not on WhatsApp :(") - } - }) + client.isOnWhatsApp ("xyz@c.us") + .then ((exists, id) => console.log(id + (exists ? " exists " : " does not exist") + "on WhatsApp")) ``` - Of course, replace ``` [countrycode][some10digitnumber] ``` with an actual country code & number. + * 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) + .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") + .then (json => console.log("status: " + json.status)) + ``` + * To get someone's profile picture + ``` javascript + client.getStatus ("xyz@c.us") // + .then (json => console.log("download profile picture from: " + json.eurl)) + ``` + * 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) + } + + ``` + + Of course, replace ``` xyz ``` with an actual country code & number. + Also, append ``` @c.us ``` for individuals & ``` @g.us ``` for groups. Do check out & run [example.js](example/example.js) to see example usage of all these functions. diff --git a/WhatsAppWeb.Query.js b/WhatsAppWeb.Query.js new file mode 100644 index 0000000..4cbd541 --- /dev/null +++ b/WhatsAppWeb.Query.js @@ -0,0 +1,81 @@ +/* + 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]) + } + // check the presence status of a given jid + WhatsAppWeb.prototype.requestPresenceUpdate = function (jid) { + return this.query(["action","presence","subscribe",jid]) + } + // check the presence status of a given jid + WhatsAppWeb.prototype.getStatus = function (jid) { + 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 + } + 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, true) // this has to be an encrypted query + } + // load messages from a group or sender + WhatsAppWeb.prototype.getMessages = function (jid, count, beforeMessage=null) { + // construct JSON + let json = [ + "query", + { + epoch: this.msgCount.toString(), + type: "message", + jid: jid, + kind: "before", + owner: "true", + count: count.toString() + }, + null + ] + // if we have some index before which we want to query + if (beforeMessage) { + json[1].index = beforeMessage.id + json[1].owner = beforeMessage.fromMe ? "true" : "false" + } + return this.query(json, true) + } + // loads all the conversation you've had with given ID + WhatsAppWeb.prototype.getAllMessages = function (jid, onMessage, chunkSize=25) { + var offsetID = null + + const loadMessage = () => { + return this.getMessages(jid, chunkSize, offsetID) + .then (json => { + if (json[2]) { + // callback with most recent message first (descending order of date) + for (var i = json[2].length-1; i >= 0;i--) { + onMessage(json[2][i][2]) + } + // if there are still more messages + if (json[2].length >= chunkSize) { + offsetID = json[2][0][2].key // get the oldest message + return new Promise ( (resolve, reject) => { + // send query after 200 ms + setTimeout( () => loadMessage().then (resolve).catch(reject), 200) + } ) + } + } + }) + } + + return loadMessage() + } +} \ No newline at end of file diff --git a/WhatsAppWeb.Recv.js b/WhatsAppWeb.Recv.js index 89a5322..b1d57e2 100644 --- a/WhatsAppWeb.Recv.js +++ b/WhatsAppWeb.Recv.js @@ -22,14 +22,19 @@ module.exports = function(WhatsAppWeb) { } var data = message.slice(commaIndex+1, message.length) - if (data.length === 0) { - // got an empty message, usually get one after sending a message or something, just return then - return - } // 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) + if (data.length === 0) { + // got an empty message, usually get one after sending a message or something + if (this.callbacks[messageTag]) { + const q = this.callbacks[messageTag] + q.callback() + delete this.callbacks[messageTag] + } + return + } let json if (data[0] === "[" || data[0] === "{") { // if the first character is a "[", then the data must just be plain JSON array or object @@ -102,7 +107,7 @@ module.exports = function(WhatsAppWeb) { /* 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 between the sender and us + if json[1].add equals last: contains the last message of the conversation */ json = json[2] // json[2] is the relevant part @@ -126,6 +131,7 @@ module.exports = function(WhatsAppWeb) { case "response": // 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 @@ -137,9 +143,14 @@ module.exports = function(WhatsAppWeb) { } } }) - + return + } else if (json[1].type === "contacts") { + if (this.handlers.gotContact) { + this.handlers.gotContact(json[2]) + } + return } - return + break case "Presence": if (this.handlers.presenceUpdated) { this.handlers.presenceUpdated(json[1].id, json[1].type) @@ -153,9 +164,10 @@ module.exports = function(WhatsAppWeb) { if the recieved JSON wasn't an array, then we must have recieved a status for a request we made */ if (this.status === Status.connected) { + // if this message is responding to a query - if (this.queryCallbacks[messageTag]) { - const q = this.queryCallbacks[messageTag] + if (this.callbacks[messageTag]) { + const q = this.callbacks[messageTag] if (q.queryJSON[1] === "exist") { q.callback(json.status == 200, q.queryJSON[2]) } else if (q.queryJSON[1] === "mediaConn") { @@ -163,7 +175,7 @@ module.exports = function(WhatsAppWeb) { } else { q.callback(json) } - delete this.queryCallbacks[messageTag] + delete this.callbacks[messageTag] } } else { // if we're trying to establish a new connection or are trying to log in diff --git a/WhatsAppWeb.Send.js b/WhatsAppWeb.Send.js index 1134071..a9b476a 100644 --- a/WhatsAppWeb.Send.js +++ b/WhatsAppWeb.Send.js @@ -11,22 +11,11 @@ module.exports = function(WhatsAppWeb) { 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] + ] ] - this.sendBinary(json, [10, 128]) // encrypt and send off - - if (this.chats[ jid ]) { - this.chats[jid].user.count = 0 // reset read count - } - } - // check if given number is registered on WhatsApp - WhatsAppWeb.prototype.isOnWhatsApp = function (jid) { - const json = [ - "query", - "exist", - jid - ] - return this.query(json) + 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) { @@ -35,11 +24,7 @@ module.exports = function(WhatsAppWeb) { { epoch: this.msgCount.toString(), type: "set" }, [ ["presence", {type: type, to: jid}, null] ] ] - this.sendBinary(json, [10, 128]) - } - // check the presence status of a given jid - WhatsAppWeb.prototype.requestPresenceUpdate = function (jid) { - this.query(["action","presence","subscribe",jid]) + 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) { @@ -105,10 +90,9 @@ module.exports = function(WhatsAppWeb) { // url safe Base64 encode the SHA256 hash of the body const fileEncSha256B64 = Utils.sha256(body).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '') - const promise = - Utils.generateThumbnail(buffer, mediaType, info) + return Utils.generateThumbnail(buffer, mediaType, info) .then (() => this.query(["query", "mediaConn"])) // send a query JSON to obtain the url & auth token to upload our media - .then ((json) => { + .then (json => { const auth = json.auth // the auth token let hostname = "https://" + json.hosts[0].hostname // first hostname available hostname += mediaPathMap[mediaType] + "/" + fileEncSha256B64 // append path @@ -143,8 +127,6 @@ module.exports = function(WhatsAppWeb) { //console.log(message) return this.sendMessage(id, message, timestamp) }) - - return promise } // generic send message construct WhatsAppWeb.prototype.sendMessage = function (id, message, timestamp=null) { @@ -169,8 +151,20 @@ module.exports = function(WhatsAppWeb) { {epoch: this.msgCount.toString(), type: "relay" }, [ ['message', null, messageJSON] ] ] - this.sendBinary(json, [16, 128]) - return messageJSON + return this.query(json, [16, 128]) + } + // 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) { @@ -188,14 +182,6 @@ module.exports = function(WhatsAppWeb) { this.send(buff) // send it off return tag } - // send query message to WhatsApp servers; returns a promise - WhatsAppWeb.prototype.query = function (json) { - const promise = new Promise((resolve, reject) => { - const tag = this.sendJSON(json) // send - this.queryCallbacks[tag] = {queryJSON: json, callback: resolve, errCallback: reject} - }) - return promise - } // send a JSON message to WhatsApp servers WhatsAppWeb.prototype.sendJSON = function (json) { const str = JSON.stringify(json) diff --git a/WhatsAppWeb.Session.js b/WhatsAppWeb.Session.js index 50aa7b8..e2168a4 100644 --- a/WhatsAppWeb.Session.js +++ b/WhatsAppWeb.Session.js @@ -55,9 +55,21 @@ module.exports = function (WhatsAppWeb) { // 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 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 + } + this.status = Status.CONNECTED + + this.didConnectSuccessfully() + } + 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 - return this.didConnectSuccessfully() + return onValidationSuccess() } const secret = Buffer.from(json.secret, 'base64') @@ -89,14 +101,7 @@ module.exports = function (WhatsAppWeb) { serverToken: json.serverToken, clientID: this.authInfo.clientID } - 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 - } - this.status = Status.CONNECTED - - this.didConnectSuccessfully() + onValidationSuccess() } else { // if the checksums didn't match this.close() this.gotError([5, "HMAC validation failed"]) @@ -195,8 +200,7 @@ module.exports = function (WhatsAppWeb) { if (this.status !== Status.creatingNewConnection) { // if we're not in the process of connecting return } - const json = ["admin", "Conn", "reref"] - this.sendJSON(json) + this.sendJSON(["admin", "Conn", "reref"]) } } \ No newline at end of file diff --git a/WhatsAppWeb.js b/WhatsAppWeb.js index 224015c..bbe3c33 100644 --- a/WhatsAppWeb.js +++ b/WhatsAppWeb.js @@ -16,7 +16,7 @@ class WhatsAppWeb { // set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send static Presence = { available: "available", // "online" - unavailable: "unavailable", // offline + unavailable: "unavailable", // "offline" composing: "composing", // "typing..." recording: "recording", // "recording..." paused: "paused" // I have no clue @@ -39,12 +39,21 @@ class WhatsAppWeb { 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.handlers = {} // data for the event handlers 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 - this.queryCallbacks = {} + // object to hold the event handlers + this.handlers = { + onError: null, + onConnected: null, + presenceUpdated: null, + onDisconnect: null, + onUnreadMessage: null, + gotContact: null + } + + this.callbacks = {} this.encoder = new BinaryCoding.Encoder() this.decoder = new BinaryCoding.Decoder() @@ -65,9 +74,8 @@ class WhatsAppWeb { if (this.reconnectLoop) { // if we connected after being disconnected clearInterval(this.reconnectLoop) // kill the loop to reconnect us - } else { // if we connected for the first time, i.e. not after being disconnected - if (this.handlers.onConnected) // tell the handler that we're connected - this.handlers.onConnected() + } 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 @@ -87,5 +95,6 @@ class WhatsAppWeb { require("./WhatsAppWeb.Session.js")(WhatsAppWeb) require("./WhatsAppWeb.Recv.js")(WhatsAppWeb) require("./WhatsAppWeb.Send.js")(WhatsAppWeb) +require("./WhatsAppWeb.Query")(WhatsAppWeb) module.exports = WhatsAppWeb \ No newline at end of file