Files
Baileys/WhatsAppWeb.Recv.js
Adhiraj b60bd03d21 Rewrite for extensibility & compactness
-This is a break from previous versions unfortunately
-Connecting is now a promise
-Chats, contacts & previously unread messages are supplied on connection
-Groups!
-Message confirmations are more reliable
-Timeout queries & connections
2020-05-14 20:13:32 +05:30

252 lines
11 KiB
JavaScript

const Utils = require("./WhatsAppWeb.Utils")
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 = {
/**
* 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 {
const commaIndex = message.indexOf(",") // all whatsapp messages have a tag and a comma, followed by the actual message
if (commaIndex < 0) { // if there was no comma, then this message must be not be valid
return this.gotError([2, "invalid message", message])
}
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).toString ()
if (data.length === 0) {
// got an empty message, usually get one after sending a query with the 128 tag
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
json = JSON.parse( data ) // parse the JSON
} 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
*/
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
const computedChecksum = Utils.hmacSign(data, this.authInfo.macKey) // compute the sign of the message we recieved using our macKey
if (checksum.equals(computedChecksum)) { // the checksum the server sent, must match the one we computed for the message to be valid
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 {
this.unexpectedDisconnect([7, "checksums don't match"])
return
}
} else {
// 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
}
/*
Check if this is a response to a message we sent
*/
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 }
}
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
}
}
if (this.logUnhandledMessages) {
this.log("[Unhandled] " + messageTag + ", " + JSON.stringify(json))
}
}
},
/**
* Type of notification
* @param {object} message
* @param {object} [message.message] should be present for actual encrypted messages
* @param {object} [message.messageStubType] should be present for group add, leave etc. notifications
* @return {[string, string]} [type of notification, specific type of message]
*/
getNotificationType: function (message) {
if (message.message) {
return ["message", Object.keys(message.message)[0]]
} else if (message.messageStubType) {
return [WhatsAppWeb.MessageStubTypes[message.messageStubType] , null]
} else {
return ["unknown", null]
}
},
/**
* Register for a callback for a certain function, will cancel automatically after one execution
* @param {[string, object, string] | string} parameters name of the function along with some optional specific parameters
* @return {promise<object>} when the function is received
*/
registerCallbackOneTime: function (parameters) {
return new Promise ((resolve, reject) => this.registerCallback (parameters, resolve))
.then (json => {
this.deregisterCallback (parameters)
return json
})
},
/**
* Register for a callback for a certain function
* @param {[string, string, string] | string} parameters name of the function along with some optional specific parameters
* @param {function(any)} callback
*/
registerCallback: function (parameters, callback) {
if (typeof parameters === "string") {
return this.registerCallback ([parameters], callback)
}
if (!Array.isArray (parameters)) {
throw "parameters (" + parameters + ") must be a string or array"
}
const func = "function:" + parameters[0]
const key = parameters[1] ?? ""
const key2 = parameters[2] ?? ""
if (!this.callbacks[func]) {
this.callbacks[func] = {}
}
if (!this.callbacks[func][key]) {
this.callbacks[func][key] = {}
}
this.callbacks[func][key][key2] = callback
},
/**
* Cancel all further callback events associated with the given parameters
* @param {[string, object, string] | string} parameters name of the function along with some optional specific parameters
*/
deregisterCallback: function (parameters) {
if (typeof parameters === "string") {
return this.deregisterCallback ([parameters])
}
if (!Array.isArray (parameters)) {
throw "parameters (" + parameters + ") must be a string or array"
}
const func = "function:" + parameters[0]
const key = parameters[1] ?? ""
const key2 = parameters[2] ?? ""
if (this.callbacks[func] && this.callbacks[func][key] && this.callbacks[func][key][key2]) {
delete this.callbacks[func][key][key2]
return
}
this.log ("WARNING: could not find " + JSON.stringify (parameters) + " to deregister")
},
/**
* Wait for a message with a certain tag to be received
* @param {string} tag the message tag to await
* @param {object} [json] query that was sent
* @param {number} [timeoutMs] timeout after which the promise will reject
*/
waitForMessage: function (tag, json, timeoutMs) {
const promise = new Promise((resolve, reject) =>
this.callbacks[tag] = {queryJSON: json, callback: resolve, errCallback: reject})
if (timeoutMs) {
return Utils.promiseTimeout (timeoutMs, promise)
.catch (err => {
delete this.callbacks[tag]
throw err
})
} else {
return promise
}
},
/**
* Decode a media message (video, image, document, audio) & save it to the given file
* @param {object} message the media message you want to decode
* @param {string} filename the name of the file where the media will be saved
* @return {Promise<object>} promise once the file is successfully saved, with the metadata
*/
decodeMediaMessage: function (message, filename) {
const getExtension = function (mimetype) {
const str = mimetype.split(";")[0].split("/")
return str[1]
}
/*
can infer media type from the key in the message
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
*/
let type = Object.keys(message)[0]
if (!type) {
return Promise.reject("unknown message type")
}
if (type === WhatsAppWeb.MessageType.extendedText || type === WhatsAppWeb.MessageType.text) {
return Promise.reject("cannot decode text message")
}
message = message[type]
// get the keys to decrypt the message
const mediaKeys = Utils.getMediaKeys(Buffer.from(message.mediaKey, 'base64'), type)
const iv = mediaKeys.iv
const cipherKey = mediaKeys.cipherKey
const macKey = mediaKeys.macKey
// download the message
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)
message.filename = trueFileName
return message
} else {
throw "HMAC sign does not match"
}
})
}
}