From 96cd89544328d3c14eedc7cedde03eb031a59a4b Mon Sep 17 00:00:00 2001 From: Adhiraj Date: Sun, 31 May 2020 13:09:04 +0530 Subject: [PATCH] Added Contact + Location Send/Receive + LiveLocation Receive --- README.md | 28 +++++++++++++----- WhatsAppWeb.Recv.js | 63 +++++++++++++++++++-------------------- WhatsAppWeb.Send.js | 67 ++++++++++++++++++++++++++++++------------ WhatsAppWeb.Session.js | 22 +++++--------- WhatsAppWeb.js | 47 ++++++++++++++--------------- example/example.js | 21 ++++++++++++- 6 files changed, 151 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index be71c60..662abdb 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ 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. + console.log("message type: " + messageType) // conversation, imageMessage, videoMessage, contactMessage etc. }) - Called when you recieve an update on someone's presence, they went offline or online ``` javascript @@ -66,6 +66,21 @@ client.sendTextMessage(id, "oh hello there", options) ``` ``` quotedMessage ``` is a message object + - Send a location using + ``` javascript + client.sendLocationMessage(id, 24.121231, 55.1121221) // the latitude, longitude of the location + ``` + - Send a contact using + ``` javascript + // format the contact as a VCARD + const vcard = 'BEGIN:VCARD\n' // metadata of the contact card + + 'VERSION:3.0\n' + + 'FN:Jeff Singh\n' // full name + + 'ORG:Ashoka Uni;\n' // the organization of the contact + + 'TEL;type=CELL;type=VOICE;waid=911234567890:+91 12345 67890\n' // WhatsApp ID + phone number + + 'END:VCARD' + client.sendContactMessage(id, "Jeff", vcard) + ``` - Send a media (image, video, sticker, pdf) message using ``` javascript const buffer = fs.readFileSync("example/ma_gif.mp4") // load some gif @@ -84,22 +99,21 @@ ] ``` - Tested formats: png, jpeg, webp (sticker), mp4, ogg - ```options``` is a JSON object, providing some information about the message. It can have the following __optional__ values: ``` javascript info = { caption: "hello there!", // (for media messages) the caption to send with the media (cannot be sent with stickers though) - thumbnail: null, /* (for media messages) 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 - */ + thumbnail: "23GD#4/==", /* (for location & media messages) 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", /* (for media messages) 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, // (for video messages) if the video should be treated as a GIF - quoted: quotedMessage, // the message you want to quote (can used with sending all kinds of messages now) + quoted: quotedMessage, // the message you want to quote (can be used with sending all kinds of messages) timestamp: Date() // optional, if you want to manually set the timestamp of the message } ``` diff --git a/WhatsAppWeb.Recv.js b/WhatsAppWeb.Recv.js index 7957fe9..2938422 100644 --- a/WhatsAppWeb.Recv.js +++ b/WhatsAppWeb.Recv.js @@ -130,7 +130,7 @@ module.exports = { */ registerCallbackOneTime: function (parameters) { return new Promise ((resolve, reject) => this.registerCallback (parameters, resolve)) - .then (json => { + .finally (json => { this.deregisterCallback (parameters) return json }) @@ -201,23 +201,25 @@ module.exports = { * 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 + * @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] - } + decodeMediaMessage: async function (message, filename) { + const getExtension = (mimetype) => mimetype.split(";")[0].split("/")[1] + /* - can infer media type from the key in the message + One 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") + throw "unknown message type" } if (type === "extendedTextMessage" || type === "conversation") { - return Promise.reject("cannot decode text message") + throw "cannot decode text message" + } + if (type === "locationMessage" || type === "liveLocationMessage") { + fs.writeFileSync (filename + ".jpeg", message[type].jpegThumbnail) + return {filename: filename + ".jpeg"} } message = message[type] @@ -228,30 +230,27 @@ module.exports = { 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 buffer = await fetch(message.url).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" - } - }) + 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 648f1d6..590bb14 100644 --- a/WhatsAppWeb.Send.js +++ b/WhatsAppWeb.Send.js @@ -3,6 +3,15 @@ const fetch = require('node-fetch') /* Contains the code for sending stuff to WhatsApp */ +/** + * @typedef {Object} MessageOptions + * @property {Object} [quoted] the message you may wanna quote along with this message + * @property {Date} [timestamp] optionally set the timestamp of the message in Unix time MS + * @property {string} [caption] (for media messages) caption to go along with the media + * @property {string} [thumbnail] (for media & location messages) base64 encoded thumbnail + * @property {string} [mimetype] (for media messages) specify the Mimetype of the media, required for document messages + * @property {boolean} [gif] (for video messages) whether the media is a gif or not + */ module.exports = { /** * Send a read receipt to the given ID for a certain message @@ -36,9 +45,7 @@ module.exports = { * 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} [options] some additional options - * @param {object} [options.quoted] the message you may wanna quote along with this message - * @param {Date} [options.timestamp] optionally set the timestamp of the message in Unix time MS + * @param {MessageOptions} [options] some additional options * @return {Promise<[object, object]>} */ sendTextMessage: function (id, txt, options={}) { @@ -53,18 +60,37 @@ module.exports = { } return this.sendMessage(id, message, options) }, + /** + * Send a contact message + * @param {string} id the JID of the person/group you're sending the message to + * @param {string} displayName the name of the person on the contact, will be displayed on WhatsApp + * @param {string} vcard the VCARD formatted contact + * @param {MessageOptions} [options] some additional options + * @return {Promise<[object, object]>} + */ + sendContactMessage: function (id, displayName, vcard, options={}) { + if (typeof displayName !== "string") { + return Promise.reject("expected text to be a string") + } + return this.sendMessage(id, {contactMessage: {displayName: displayName, vcard: vcard}}, options) + }, + /** + * Send a location message + * @param {string} id the JID of the person/group you're sending the message to + * @param {number} lat the latitude of the location + * @param {number} long the longitude of the location + * @param {MessageOptions} [options] some additional options + * @return {Promise<[object, object]>} + */ + sendLocationMessage: function (id, lat, long, options={}) { + return this.sendMessage(id, {locationMessage: {degreesLatitude: lat, degreesLongitude: long}}, options) + }, /** * 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} [options] additional information about the message - * @param {string} [options.caption] caption to go along with the media - * @param {string} [options.thumbnail] base64 encoded thumbnail for the media - * @param {string} [options.mimetype] specify the Mimetype of the media (required for document messages) - * @param {boolean} [options.gif] whether the media is a gif or not, only valid for video messages - * @param {object} [options.quoted] the message you may wanna quote along with this message - * @param {Date} [options.timestamp] optionally set the timestamp of the message in Unix time MS + * @param {MessageOptions} [options] additional information about the message * @return {Promise<[object, object]>} */ sendMediaMessage: function (id, buffer, mediaType, options={}) { @@ -133,14 +159,12 @@ module.exports = { .then (url => { let message = {} message[mediaType] = { - caption: options.caption, url: url, mediaKey: mediaKey.toString('base64'), mimetype: options.mimetype, fileEncSha256: fileEncSha256B64, fileSha256: fileSha256.toString('base64'), - fileLength: buffer.length, - jpegThumbnail: options.thumbnail + fileLength: buffer.length } if (mediaType === "videoMessage" && options.gif) { message[mediaType].gifPlayback = options.gif @@ -153,19 +177,17 @@ module.exports = { * @private * @param {string} id who to send the message to * @param {object} message like, the message - * @param {object} [options] some additional options - * @param {object} [options.quoted] the message you may wanna quote along with this message - * @param {Date} [options.timestamp] timestamp for the message + * @param {MessageOptions} [options] some additional options * @return {Promise<[object, object]>} array of the recieved JSON & the query JSON */ sendMessage: function (id, message, options) { if (!options.timestamp) { // if no timestamp was provided, options.timestamp = new Date() // set timestamp to now } + const key = Object.keys(message)[0] const timestamp = options.timestamp.getTime()/1000 const quoted = options.quoted if (quoted) { - const key = Object.keys(message)[0] const participant = quoted.key.participant || quoted.key.remoteJid message[key].contextInfo = { participant: participant, @@ -178,8 +200,15 @@ module.exports = { message[key].contextInfo.remoteJid = quoted.key.remoteJid } } - console.log(JSON.stringify(quoted)) - console.log(JSON.stringify(message)) + if (options.caption) { + message[key].caption = options.caption + } + if (options.thumbnail) { + if (typeof options.thumbnail !== "string") { + return Promise.reject("expected JPEG to be a base64 encoded string") + } + message[key].jpegThumbnail = options.thumbnail + } let messageJSON = { key: { diff --git a/WhatsAppWeb.Session.js b/WhatsAppWeb.Session.js index 720e83b..234da06 100644 --- a/WhatsAppWeb.Session.js +++ b/WhatsAppWeb.Session.js @@ -7,7 +7,7 @@ const Utils = require('./WhatsAppWeb.Utils') module.exports = { /** * Connect to WhatsAppWeb - * @param {object} [authInfo] credentials to log back in + * @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] */ @@ -24,7 +24,7 @@ module.exports = { } this.conn = new WebSocket("wss://web.whatsapp.com/ws", {origin: "https://web.whatsapp.com"}) - const promise = new Promise ( (resolve, reject) => { + let 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) @@ -34,15 +34,11 @@ module.exports = { reject (error) }) }) - if (timeoutMs) { - return Utils.promiseTimeout (timeoutMs, promise) - .catch (error => { - this.close() - throw error - }) - } else { - return promise - } + promise = timeoutMs ? Utils.promiseTimeout (timeoutMs, promise) : promise + return promise.catch (err => { + this.close () + throw err + }) }, /** once a connection has been successfully established * @private @@ -148,10 +144,6 @@ module.exports = { // 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 diff --git a/WhatsAppWeb.js b/WhatsAppWeb.js index 4aa3e3b..76233c0 100644 --- a/WhatsAppWeb.js +++ b/WhatsAppWeb.js @@ -1,5 +1,18 @@ const BinaryCoding = require('./binary_coding/binary_encoder.js') const QR = require('qrcode-terminal') +const Utils = require('./WhatsAppWeb.Utils') + +/** + * @typedef WhatsAppMessage + * @property {Object} key metadata about the sender + * @property {string} key.remoteJid sender ID (could be group or individual) + * @property {bool} key.fromMe + * @property {string} key.id ID of the message + * @property {string} [key.participant] if its a group, which individual sent it + * @property {Object} [message] the actual message + * @property {string} messageTimestamp unix timestamp + * @property {number} [duration] the duration of the live location + */ class WhatsAppWeb { /** @@ -30,7 +43,10 @@ class WhatsAppWeb { sticker: "stickerMessage", document: "documentMessage", audio: "audioMessage", - extendedText: "extendedTextMessage" + extendedText: "extendedTextMessage", + contact: "contactMessage", + location: "locationMessage", + liveLocation: "liveLocationMessage" } /** * Tells us what kind of message it is @@ -122,7 +138,7 @@ class WhatsAppWeb { } /** * Set the callback for new/unread messages, if someone sends a message, this callback will be fired - * @param {function(object)} callback + * @param {function(WhatsAppMessage)} callback */ setOnUnreadMessage (callback) { this.registerCallback (["action", "add:relay", "message"], (json) => { @@ -199,28 +215,13 @@ WhatsAppWeb.prototype.sendReadReceipt = send.sendReadReceipt * @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]>} - */ +/** Send a text message */ 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]>} - */ +/** Send a contact message */ +WhatsAppWeb.prototype.sendContactMessage = send.sendContactMessage +/** Send a location message */ +WhatsAppWeb.prototype.sendLocationMessage = send.sendLocationMessage +/** Send a media message */ WhatsAppWeb.prototype.sendMediaMessage = send.sendMediaMessage /** @private */ WhatsAppWeb.prototype.sendMessage = send.sendMessage diff --git a/example/example.js b/example/example.js index d3a9905..67c07cd 100644 --- a/example/example.js +++ b/example/example.js @@ -36,16 +36,32 @@ client.connect (authInfo, 30*1000) // connect or timeout in 30 seconds if (notificationType !== "message") { return } + 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 (messageType === WhatsAppWeb.MessageType.contact) { + + const contact = m.message.contactMessage + console.log (sender + " sent contact (" + contact.displayName + "): " + contact.vcard) + } else if (messageType === WhatsAppWeb.MessageType.location || messageType === WhatsAppWeb.MessageType.liveLocation) { + + const locMessage = m.message[messageType] + console.log (sender + " sent location (lat: " + locMessage.degreesLatitude + ", long: " + locMessage.degreesLongitude + "), saving thumbnail...") + client.decodeMediaMessage(m.message, "loc_thumb_in_" + m.key.id) + + if (messageType === WhatsAppWeb.MessageType.liveLocation) { + console.log (sender + " sent live location for duration: " + m.duration/60 + " minutes, seq number: " + locMessage.sequenceNumber) + } } 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 @@ -60,8 +76,11 @@ client.connect (authInfo, 30*1000) // connect or timeout in 30 seconds .then (() => client.updatePresence(m.key.remoteJid, WhatsAppWeb.Presence.composing)) // tell them we're composing .then (() => { // send the message let options = {quoted: m} - if (Math.random() > 0.7) { // choose at random + const rand = Math.random() + if (rand > 0.66) { // choose at random return client.sendTextMessage(m.key.remoteJid, "hello!", options) // send a "hello!" & quote the message recieved + } else if (rand > 0.33) { // choose at random + return client.sendLocationMessage(m.key.remoteJid, 32.123123, 12.12123123) // send a random location lol } else { const buffer = fs.readFileSync("example/ma_gif.mp4") // load the gif options.gif = true // the video is a gif