diff --git a/example.js b/example.js index 99dc7ea..ab8c740 100644 --- a/example.js +++ b/example.js @@ -189,7 +189,20 @@ client.on('message', async msg => { client.interface.openChatWindowAt(quotedMsg.id._serialized); } } else if (msg.body === '!buttons') { - let button = new Buttons('Button body',[{body:'bt1'},{body:'bt2'},{body:'bt3'}],'title','footer'); + let button = new Buttons( + 'Button body', + [ + { body: 'whatsapp-web.js', url: 'https://wwebjs.dev/' }, + { body: 'Call me', number: '+1 (805) 457-4992' }, + { body: 'third special button', number: '+1 (202) 968-6161' },// Limited to 2 especial buttons, this one will be ignored + { body: 'Some text' }, + { body: 'Another text' }, + { body: 'Another another text' }, + { body: 'Fourth button' }// Limited to 3 regular buttons, this one will be ignored + ], + 'title', + 'footer' + ); client.sendMessage(msg.from, button); } else if (msg.body === '!list') { let sections = [{title:'sectionTitle',rows:[{title:'ListItem1', description: 'desc'},{title:'ListItem2'}]}]; diff --git a/src/structures/Buttons.js b/src/structures/Buttons.js index 16e6cb4..1b5b85d 100644 --- a/src/structures/Buttons.js +++ b/src/structures/Buttons.js @@ -71,11 +71,18 @@ class Buttons { * Returns: [{ buttonId:'customId',buttonText:{'displayText':'button1'},type: 1 },{buttonId:'n3XKsL',buttonText:{'displayText':'button2'},type:1},{buttonId:'NDJk0a',buttonText:{'displayText':'button3'},type:1}] */ _format(buttons){ - buttons = buttons.slice(0,3); // phone users can only see 3 buttons, so lets limit this + // phone users can only see 3 regular buttons (not url or phone) and 2 especial buttons, so lets limit this + const especialButtons = buttons.filter(button => button.url || button.number).slice(0,2); + const regularButtons = buttons.filter(button => !button.url && !button.number).slice(0,3); + buttons = especialButtons.concat(regularButtons); + return buttons.map((btn) => { + if (btn.url && btn.number) throw 'button can\'t be with url and number together'; return { buttonId: btn.id ? String(btn.id) : Util.generateHash(6), - buttonText: {displayText: btn.body}, + url: btn.url, + phoneNumber: btn.number, + buttonText: btn.body, type: 1 }; }); @@ -83,4 +90,4 @@ class Buttons { } -module.exports = Buttons; \ No newline at end of file +module.exports = Buttons; diff --git a/src/util/Injected.js b/src/util/Injected.js index fcf3b42..e2e4525 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -77,40 +77,112 @@ exports.ExposeStore = (moduleRaidStr) => { window.mR.findModule(selector.name)[selector.index][selector.property] = (...args) => callback(oldFunct, args); }; - window.findProxyModel = (name) => { - const baseName = name.replace(/Model$/, ''); - - const names = [baseName]; - - // ChatModel => "chat" - names.push(baseName.replace(/^(\w)/, (l) => l.toLowerCase())); - - // CartItemModel => "cart-item" - // ProductListModel => "product_list" - const parts = baseName.split(/(?=[A-Z])/); - - names.push(parts.join('-').toLowerCase()); - names.push(parts.join('_').toLowerCase()); - - return window.mR.findModule( - (m) => - names.includes( - m.default?.prototype?.proxyName || - m[name]?.prototype?.proxyName || - m[baseName]?.prototype?.proxyName - ) - ); - }; - - window.injectToFunction({index: 0, name: 'createMsgProtobuf', property: 'createMsgProtobuf'}, (func, args) => { + window.injectToFunction({ + index: 0, + name: 'createMsgProtobuf', + property: 'createMsgProtobuf' + }, (func, args) => { + const [message] = args; const proto = func(...args); - if (proto.listMessage) { + if (message.hydratedButtons) { + const hydratedTemplate = { + hydratedButtons: message.hydratedButtons, + }; + + if (message.footer) { + hydratedTemplate.hydratedFooterText = message.footer; + } + + if (message.caption) { + hydratedTemplate.hydratedContentText = message.caption; + } + + if (message.title) { + hydratedTemplate.hydratedTitleText = message.title; + } + + if (proto.conversation) { + hydratedTemplate.hydratedContentText = proto.conversation; + delete proto.conversation; + } else if (proto.extendedTextMessage?.text) { + hydratedTemplate.hydratedContentText = proto.extendedTextMessage.text; + delete proto.extendedTextMessage; + } else { + // Search media part in message + let found; + const mediaPart = [ + 'documentMessage', + 'imageMessage', + 'locationMessage', + 'videoMessage', + ]; + for (const part of mediaPart) { + if (part in proto) { + found = part; + break; + } + } + + if (!found) { + return proto; + } + + // Media message doesn't allow title + hydratedTemplate[found] = proto[found]; + + // Copy title to caption if not setted + if ( + hydratedTemplate.hydratedTitleText && + !hydratedTemplate.hydratedContentText + ) { + hydratedTemplate.hydratedContentText = + hydratedTemplate.hydratedTitleText; + } + + // Remove title for media messages + delete hydratedTemplate.hydratedTitleText; + + if (found === 'locationMessage') { + if ( + !hydratedTemplate.hydratedContentText && + (message[found].name || message[found].address) + ) { + hydratedTemplate.hydratedContentText = + message[found].name && message[found].address + ? `${message[found].name}\n${message[found].address}` + : message[found].name || message[found].address || ''; + } + } + + // Ensure a content text; + hydratedTemplate.hydratedContentText = + hydratedTemplate.hydratedContentText || ' '; + + delete proto[found]; + } + + proto.templateMessage = { + hydratedTemplate, + }; + } + + return proto; + }); + + window.injectToFunction({ + index: 0, + name: 'createMsgProtobuf', + property: 'createMsgProtobuf' + }, + (func, args) => { + const proto = func(...args); + if (proto.templateMessage) { proto.viewOnceMessage = { message: { - listMessage: proto.listMessage - } + templateMessage: proto.templateMessage, + }, }; - delete proto.listMessage; + delete proto.templateMessage; } if (proto.buttonsMessage) { proto.viewOnceMessage = { @@ -120,20 +192,91 @@ exports.ExposeStore = (moduleRaidStr) => { }; delete proto.buttonsMessage; } + if (proto.listMessage) { + proto.viewOnceMessage = { + message: { + listMessage: proto.listMessage, + }, + }; + delete proto.listMessage; + } return proto; }); - window.injectToFunction({index: 0, name: 'typeAttributeFromProtobuf', property: 'typeAttributeFromProtobuf'}, (func, args) => { + window.injectToFunction({ + index: 0, + name: 'typeAttributeFromProtobuf', + property: 'typeAttributeFromProtobuf' + }, (func, args) => { const [proto] = args; - if ( - proto.buttonsMessage?.headerType === 1 || - proto.buttonsMessage?.headerType === 2 - ) { + if (proto.templateMessage?.hydratedTemplate) { + const keys = Object.keys(proto.templateMessage?.hydratedTemplate); + const messagePart = [ + 'documentMessage', + 'imageMessage', + 'locationMessage', + 'videoMessage', + ]; + if (messagePart.some((part) => keys.includes(part))) { + return 'media'; + } return 'text'; } + return func(...args); + }); - if (proto.listMessage) { - return 'text'; + window.injectToFunction({ + index: 0, + name: 'typeAttributeFromProtobuf', + property: 'typeAttributeFromProtobuf' + }, (func, args) => { + const [proto] = args; + + if (proto.ephemeralMessage) { + const { message: n } = proto.ephemeralMessage; + return n ? func(n) : 'text'; + } + if (proto.deviceSentMessage) { + const { message: n } = proto.deviceSentMessage; + return n ? func(n) : 'text'; + } + if (proto.viewOnceMessage) { + const { message: n } = proto.viewOnceMessage; + return n ? func(n) : 'text'; + } + + return func(...args); + }); + + window.injectToFunction({ + index: 0, + name: 'mediaTypeFromProtobuf', + property: 'mediaTypeFromProtobuf' + }, (func, args) => { + const [proto] = args; + if (proto.templateMessage?.hydratedTemplate) { + return func(proto.templateMessage.hydratedTemplate); + } + return func(...args); + }); + + window.injectToFunction({ + index: 0, + name: 'mediaTypeFromProtobuf', + property: 'mediaTypeFromProtobuf' + }, (func, args) => { + const [proto] = args; + if (proto.deviceSentMessage) { + const {message: n} = proto.deviceSentMessage; + return n ? func(n) : null; + } + if (proto.ephemeralMessage) { + const {message: n} = proto.ephemeralMessage; + return n ? func(n) : null; + } + if (proto.viewOnceMessage) { + const {message: n} = proto.viewOnceMessage; + return n ? func (n) : null; } return func(...args); @@ -152,6 +295,75 @@ exports.LoadUtils = () => { return false; }; + + window.WWebJS.prepareMessageButtons = (buttonsOptions) => { + const returnObject = {}; + + (window.Store.ReplyButtonModel) || (window.Store.ReplyButtonModel = window.mR.findModule(m => m.default && m?.default?.prototype?.proxyName === 'replyButton')[0].default); + (window.Store.TemplateButtonModel) || (window.Store.TemplateButtonModel = window.mR.findModule(m => m.default && m?.default?.prototype?.proxyName === 'templateButton')[0].default); + (window.Store.TemplateButtonCollection) || (window.Store.TemplateButtonCollection = window.mR.findModule('TemplateButtonCollection')[0].TemplateButtonCollection); + (window.Store.ButtonCollection) || (window.Store.ButtonCollection = window.mR.findModule('ButtonCollection')[0].ButtonCollection); + + if (!buttonsOptions.buttons) { + return returnObject; + } + if (!Array.isArray(buttonsOptions.buttons)) { + throw 'Buttons options is not a array'; + } + + returnObject.title = buttonsOptions.title; + returnObject.footer = buttonsOptions.footer; + returnObject.isFromTemplate = !0; + returnObject.buttons = new window.Store.TemplateButtonCollection; + returnObject.hydratedButtons = buttonsOptions.buttons.map((e, t) => { + if ('phoneNumber' in e) { + return { + index: t, callButton: { + displayText: e.buttonText, phoneNumber: e.phoneNumber + } + }; + } else if ('url' in e) { + return { + index: t, urlButton: { + displayText: e.buttonText, url: e.url + } + }; + } else { + return { + index: t, quickReplyButton: { + displayText: e.buttonText, id: e.buttonId || `${t}` + } + }; + } + + }); + + returnObject.buttons.add(returnObject.hydratedButtons.map((e, t) => { + var r, n, o, i; + const s = `${null != e.index ? e.index : t}`; + if (e.urlButton) { + return new window.Store.TemplateButtonModel({ + id: s, + displayText: null === (r = e.urlButton) || void 0 === r ? void 0 : r.displayText, + url: null === (n = e.urlButton) || void 0 === n ? void 0 : n.url, + subtype: 'url' + }); + } else if (e.callButton) { + return new window.Store.TemplateButtonModel({ + id: s, displayText: e.callButton.displayText, phoneNumber: e.callButton.phoneNumber, subtype: 'call' + }); + } else { + return new window.Store.TemplateButtonModel({ + id: s, + displayText: null === (o = e.quickReplyButton) || void 0 === o ? void 0 : o.displayText, + selectionId: null === (i = e.quickReplyButton) || void 0 === i ? void 0 : i.id, + subtype: 'quick_reply' + }); + } + })); + + return returnObject; + }; window.WWebJS.sendMessage = async (chat, content, options = {}) => { let attOptions = {}; @@ -240,7 +452,7 @@ exports.LoadUtils = () => { } } } - + let buttonOptions = {}; if(options.buttons){ let caption; @@ -250,22 +462,10 @@ exports.LoadUtils = () => { } else { caption = options.caption ? options.caption : ' '; //Caption can't be empty } - // UI needs to stop glitching - const ButtonsCollection = window.mR.findModule('ButtonCollection')[0].ButtonCollection; - const ReplyButtonModel = window.findProxyModel('ReplyButtonModel')[0].default; - const collection = new ButtonsCollection(ReplyButtonModel); - - const quickButtons = options.buttons.buttons.map(a => { - return new ReplyButtonModel({id: a.buttonId, displayText: a.buttonText.displayText}); - }); - collection.add(quickButtons); + buttonOptions = window.WWebJS.prepareMessageButtons(options.buttons); buttonOptions = { - isDynamicReplyButtonsMsg: true, - title: options.buttons.title ? options.buttons.title : undefined, - footer: options.buttons.footer ? options.buttons.footer : undefined, - dynamicReplyButtons: options.buttons.buttons, - replyButtons: collection, + ...buttonOptions, caption: caption }; delete options.buttons;