From a098d61b03798bac675e516bcad4d69a082a0361 Mon Sep 17 00:00:00 2001 From: Pedro Lopez Date: Mon, 3 Feb 2020 23:40:49 -0400 Subject: [PATCH] feat: send messages with attachments close #3 --- example.js | 7 ++++ index.js | 4 ++ src/Client.js | 25 +++++++++--- src/structures/Chat.js | 9 +++-- src/structures/Message.js | 39 ++++++++++-------- src/structures/MessageMedia.js | 31 +++++++++++++++ src/util/Injected.js | 72 +++++++++++++++++++++++++++++++++- 7 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 src/structures/MessageMedia.js diff --git a/example.js b/example.js index d5f0e00..245cd29 100644 --- a/example.js +++ b/example.js @@ -116,6 +116,13 @@ client.on('message', async msg => { Timestamp: ${quotedMsg.timestamp} Has Media? ${quotedMsg.hasMedia} `); + } else if(msg.body == '!resendmedia' && msg.hasQuotedMsg) { + const quotedMsg = await msg.getQuotedMessage(); + if(quotedMsg.hasMedia) { + const attachmentData = await quotedMsg.downloadMedia(); + client.sendMessage(msg.from, attachmentData, {caption: 'Here\'s your requested media.'}); + } + } }); diff --git a/index.js b/index.js index 8ce52f8..abe07e8 100644 --- a/index.js +++ b/index.js @@ -10,5 +10,9 @@ module.exports = { PrivateChat: require('./src/structures/PrivateChat'), GroupChat: require('./src/structures/GroupChat'), Message: require('./src/structures/Message'), + MessageMedia: require('./src/structures/MessageMedia'), + Contact: require('./src/structures/Contact'), + PrivateContact: require('./src/structures/PrivateContact'), + BusinessContact: require('./src/structures/BusinessContact'), ClientInfo: require('./src/structures/ClientInfo') }; \ No newline at end of file diff --git a/src/Client.js b/src/Client.js index 528d0e0..399d415 100644 --- a/src/Client.js +++ b/src/Client.js @@ -12,6 +12,7 @@ const ChatFactory = require('./factories/ChatFactory'); const ContactFactory = require('./factories/ContactFactory'); const ClientInfo = require('./structures/ClientInfo'); const Message = require('./structures/Message'); +const MessageMedia = require('./structures/MessageMedia'); /** * Starting point for interacting with the WhatsApp Web API @@ -177,13 +178,27 @@ class Client extends EventEmitter { /** * Send a message to a specific chatId * @param {string} chatId - * @param {string} message + * @param {string|MessageMedia} content + * @param {object} options */ - async sendMessage(chatId, message) { - const newMessage = await this.pupPage.evaluate(async (chatId, message) => { - const msg = await window.WWebJS.sendMessage(window.Store.Chat.get(chatId), message); + async sendMessage(chatId, content, options={}) { + let internalOptions = { + caption: options.caption, + quotedMessageId: options.quotedMessageId + }; + + if(content instanceof MessageMedia) { + internalOptions.attachment = content; + content = ''; + } else if(options.media instanceof MessageMedia) { + internalOptions.media = options.media; + internalOptions.caption = content; + } + + const newMessage = await this.pupPage.evaluate(async (chatId, message, options) => { + const msg = await window.WWebJS.sendMessage(window.Store.Chat.get(chatId), message, options); return msg.serialize(); - }, chatId, message); + }, chatId, content, internalOptions); return new Message(this, newMessage); } diff --git a/src/structures/Chat.js b/src/structures/Chat.js index 54c2464..b20e0cb 100644 --- a/src/structures/Chat.js +++ b/src/structures/Chat.js @@ -26,11 +26,12 @@ class Chat extends Base { } /** - * Sends a message to this chat. - * @param {string} message + * Send a message to this chat + * @param {string|MessageMedia} content + * @param {object} options */ - async sendMessage(message) { - return this.client.sendMessage(this.id._serialized, message); + async sendMessage(content, options) { + return this.client.sendMessage(this.id._serialized, content, options); } } diff --git a/src/structures/Message.js b/src/structures/Message.js index f52fb52..c58c14e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -1,6 +1,7 @@ 'use strict'; const Base = require('./Base'); +const MessageMedia = require('./MessageMedia'); /** * Represents a Message on WhatsApp @@ -36,6 +37,7 @@ class Message extends Base { /** * Returns the Chat this message was sent in + * @returns {Chat} */ getChat() { return this.client.getChatById(this._getChatId()); @@ -43,6 +45,7 @@ class Message extends Base { /** * Returns the Contact this message was sent from + * @returns {Contact} */ getContact() { return this.client.getContactById(this._getChatId()); @@ -50,6 +53,7 @@ class Message extends Base { /** * Returns the quoted message, if any + * @returns {Message} */ async getQuotedMessage() { if (!this.hasQuotedMsg) return undefined; @@ -66,46 +70,49 @@ class Message extends Base { * Sends a message as a reply. If chatId is specified, it will be sent * through the specified Chat. If not, it will send the message * in the same Chat as the original message was sent. - * @param {string} message + * + * @param {string|MessageMedia} content * @param {?string} chatId + * @param {object} options + * @returns {Message} */ - async reply(message, chatId) { + async reply(content, chatId, options={}) { if (!chatId) { chatId = this._getChatId(); } - - const newMessage = await this.client.pupPage.evaluate(async (chatId, quotedMessageId, message) => { - let quotedMessage = window.Store.Msg.get(quotedMessageId); - if(quotedMessage.canReply()) { - const chat = window.Store.Chat.get(chatId); - const newMessage = await window.WWebJS.sendMessage(chat, message, quotedMessage.msgContextInfo(chat)); - return newMessage.serialize(); - } else { - throw new Error('This message cannot be replied to.'); - } - }, chatId, this.id._serialized, message); - return new Message(this.client, newMessage); + options = { + ...options, + quotedMessageId: this.id._serialized + }; + + return this.client.sendMessage(chatId, content, options); } + /** + * Downloads and returns the attatched message media + * @returns {MessageMedia} + */ async downloadMedia() { if (!this.hasMedia) { return undefined; } - return await this.client.pupPage.evaluate(async (msgId) => { + const {data, mimetype, filename} = await this.client.pupPage.evaluate(async (msgId) => { const msg = window.Store.Msg.get(msgId); const buffer = await window.WWebJS.downloadBuffer(msg.clientUrl); const decrypted = await window.Store.CryptoLib.decryptE2EMedia(msg.type, buffer, msg.mediaKey, msg.mimetype); const data = await window.WWebJS.readBlobAsync(decrypted._blob); return { - data, + data: data.split(',')[1], mimetype: msg.mimetype, filename: msg.filename }; }, this.id._serialized); + + return new MessageMedia(mimetype, data, filename); } } diff --git a/src/structures/MessageMedia.js b/src/structures/MessageMedia.js new file mode 100644 index 0000000..b0c5f0b --- /dev/null +++ b/src/structures/MessageMedia.js @@ -0,0 +1,31 @@ +'use strict'; + +/** + * Media attached to a message + * @param {string} mimetype MIME type of the attachment + * @param {string} data Base64-encoded data of the file + * @param {?string} filename Document file name + */ +class MessageMedia { + constructor(mimetype, data, filename) { + /** + * MIME type of the attachment + * @type {string} + */ + this.mimetype = mimetype; + + /** + * Base64 encoded data that represents the file + * @type {string} + */ + this.data = data; + + /** + * Name of the file (for documents) + * @type {?string} + */ + this.filename = filename; + } +} + +module.exports = MessageMedia; \ No newline at end of file diff --git a/src/util/Injected.js b/src/util/Injected.js index 55d530d..2e86302 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -16,12 +16,32 @@ exports.ExposeStore = (moduleRaidStr) => { window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0]; window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default; window.Store.Invite = window.mR.findModule('sendJoinGroupViaInvite')[0]; + window.Store.OpaqueData = window.mR.findModule('getOrCreateOpaqueDataForPath')[0]; + window.Store.MediaPrep = window.mR.findModule('MediaPrep')[0]; + window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0]; + window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0]; + window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0]; }; exports.LoadUtils = () => { window.WWebJS = {}; window.WWebJS.sendMessage = async (chat, content, options) => { + let attOptions = {}; + if (options.attachment) { + attOptions = await window.WWebJS.processMediaData(options.attachment); + delete options.attachment; + } + + let quotedMsgOptions = {}; + if (options.quotedMessageId) { + let quotedMessage = window.Store.Msg.get(options.quotedMessageId); + if(quotedMessage.canReply()) { + quotedMsgOptions = quotedMessage.msgContextInfo(chat); + } + delete options.quotedMessageId; + } + const newMsgId = new window.Store.MsgKey({ from: window.Store.Conn.me, to: chat.id, @@ -29,6 +49,7 @@ exports.LoadUtils = () => { }); const message = { + ...options, id: newMsgId, ack: 0, body: content, @@ -39,13 +60,46 @@ exports.LoadUtils = () => { t: parseInt(new Date().getTime() / 1000), isNewMsg: true, type: 'chat', - ...options + ...attOptions, + ...quotedMsgOptions }; await window.Store.SendMessage.addAndSendMsgToChat(chat, message); return window.Store.Msg.get(newMsgId._serialized); }; + window.WWebJS.processMediaData = async (mediaInfo) => { + const file = window.WWebJS.mediaInfoToFile(mediaInfo); + const mData = await window.Store.OpaqueData.default.createFromData(file, file.type); + const mediaPrep = window.Store.MediaPrep.prepRawMedia(mData, {}); + const mediaData = await mediaPrep.waitForPrep(); + const mediaObject = window.Store.MediaObject.getOrCreateMediaObject(mediaData.filehash); + + const mediaType = window.Store.MediaTypes.msgToMediaType({ + type: mediaData.type, + isGif: mediaData.isGif + }); + + const uploadedMedia = await window.Store.MediaUpload.uploadMedia(mediaData.mimetype, mediaObject, mediaType); + if (!uploadedMedia) { + throw new Error('upload failed: media entry was not created'); + } + + mediaData.set({ + clientUrl: uploadedMedia.mmsUrl, + directPath: uploadedMedia.directPath, + mediaKey: uploadedMedia.mediaKey, + mediaKeyTimestamp: uploadedMedia.mediaKeyTimestamp, + filehash: mediaObject.filehash, + uploadhash: uploadedMedia.uploadHash, + size: mediaObject.size, + streamingSidecar: uploadedMedia.sidecar, + firstFrameSidecar: uploadedMedia.firstFrameSidecar + }); + + return mediaData; + }; + window.WWebJS.getChatModel = chat => { let res = chat.serialize(); res.isGroup = chat.isGroup; @@ -89,6 +143,22 @@ exports.LoadUtils = () => { return contacts.map(contact => window.WWebJS.getContactModel(contact)); }; + window.WWebJS.mediaInfoToFile = ({data, mimetype, filename}) => { + const binaryData = atob(data); + + const buffer = new ArrayBuffer(binaryData.length); + const view = new Uint8Array(buffer); + for(let i=0; i < binaryData.length; i++) { + view[i] = binaryData.charCodeAt(i); + } + + const blob = new Blob([buffer], {type: mimetype}); + return new File([blob], filename, { + type: mimetype, + lastModified: Date.now() + }); + }; + window.WWebJS.downloadBuffer = (url) => { return new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest();