diff --git a/example.js b/example.js index 8f87101..4958e87 100644 --- a/example.js +++ b/example.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const { Client, Location } = require('./index'); +const { Client, Location, List, Buttons } = require('./index'); const SESSION_FILE_PATH = './session.json'; let sessionCfg; @@ -204,6 +204,13 @@ client.on('message', async msg => { const quotedMsg = await msg.getQuotedMessage(); 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'); + client.sendMessage(msg.from, button); + } else if (msg.body === '!list') { + let sections = [{title:'sectionTitle',rows:[{title:'ListItem1', description: 'desc'},{title:'ListItem2'}]}]; + let list = new List('List body','btnText',sections,'Title','footer'); + client.sendMessage(msg.from, list); } }); diff --git a/index.d.ts b/index.d.ts index 78c5b2c..7e7cc69 100644 --- a/index.d.ts +++ b/index.d.ts @@ -567,6 +567,12 @@ declare namespace WAWebJS { businessOwnerJid?: string, /** Product JID */ productId?: string, + /** Message buttons */ + dynamicReplyButtons?: object, + /** Selected button ID */ + selectedButtonId?: string, + /** Selected list row ID */ + selectedRowId?: string, /** Accept the Group V4 Invite in message */ acceptGroupV4Invite: () => Promise<{status: number}>, /** Deletes the message from the chat */ @@ -1149,6 +1155,27 @@ declare namespace WAWebJS { /** Object with participants */ participants: object } + + /** Message type List */ + export class List { + body: string + buttonText: string + sections: Array + title?: string | null + footer?: string | null + + constructor(body: string, buttonText: string, sections: Array, title?: string | null, footer?: string | null) + } + + /** Message type buttons */ + export class Buttons { + body: string | MessageMedia + buttons: Array> + title?: string | null + footer?: string | null + + constructor(body: string, buttons: Array>, title?: string | null, footer?: string | null) + } } export = WAWebJS diff --git a/index.js b/index.js index 44ad5c8..3d2b64f 100644 --- a/index.js +++ b/index.js @@ -19,5 +19,7 @@ module.exports = { ClientInfo: require('./src/structures/ClientInfo'), Location: require('./src/structures/Location'), ProductMetadata: require('./src/structures/ProductMetadata'), + List: require('./src/structures/List'), + Buttons: require('./src/structures/Buttons'), ...Constants }; diff --git a/src/Client.js b/src/Client.js index 3fb9555..fc33d22 100644 --- a/src/Client.js +++ b/src/Client.js @@ -11,7 +11,7 @@ const { WhatsWebURL, DefaultOptions, Events, WAState } = require('./util/Constan const { ExposeStore, LoadUtils } = require('./util/Injected'); const ChatFactory = require('./factories/ChatFactory'); const ContactFactory = require('./factories/ContactFactory'); -const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification , Label, Call } = require('./structures'); +const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification , Label, Call, Buttons, List} = require('./structures'); /** * Starting point for interacting with the WhatsApp Web API * @extends {EventEmitter} @@ -479,7 +479,7 @@ class Client extends EventEmitter { /** * Send a message to a specific chatId * @param {string} chatId - * @param {string|MessageMedia|Location|Contact|Array} content + * @param {string|MessageMedia|Location|Contact|Array|Buttons|List} content * @param {MessageSendOptions} [options] - Options used when sending the message * * @returns {Promise} Message that was just sent @@ -516,6 +516,13 @@ class Client extends EventEmitter { } else if(Array.isArray(content) && content.length > 0 && content[0] instanceof Contact) { internalOptions.contactCardList = content.map(contact => contact.id._serialized); content = ''; + } else if(content instanceof Buttons){ + if(content.type !== 'chat'){internalOptions.attachment = content.body;} + internalOptions.buttons = content; + content = ''; + } else if(content instanceof List){ + internalOptions.list = content; + content = ''; } if (internalOptions.sendMediaAsSticker && internalOptions.attachment) { diff --git a/src/structures/Buttons.js b/src/structures/Buttons.js new file mode 100644 index 0000000..96442ed --- /dev/null +++ b/src/structures/Buttons.js @@ -0,0 +1,68 @@ +'use strict'; + +const MessageMedia = require('./MessageMedia'); +const Util = require('../util/Util'); + +/** + * Message type buttons + */ +class Buttons { + /** + * @param {string|MessageMedia} body + * @param {Array>} buttons + * @param {string?} title + * @param {string?} footer + */ + constructor(body, buttons, title, footer) { + /** + * Message body + * @type {string|MessageMedia} + */ + this.body = body; + + /** + * title of message + * @type {string} + */ + this.title = title; + + /** + * footer of message + * @type {string} + */ + this.footer = footer; + + if (body instanceof MessageMedia) { + this.type = 'media'; + this.title = ''; + }else{ + this.type = 'chat'; + } + + /** + * buttons of message + * @type {Array>} + */ + this.buttons = this._format(buttons); + if(!this.buttons.length){ throw '[BT01] No buttons';} + + } + + /** + * Creates button array from simple array + * @param {Array>} buttons + * @returns {Array>} + * @example + * Input: [{id:'customId',body:'button1'},{body:'button2'},{body:'button3'},{body:'button4'}] + * 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 + return buttons.map((btn) => { + return {'buttonId':btn.id ? btn.id : Util.generateHash(6),'buttonText':{'displayText':btn.body},'type':1}; + }); + } + +} + +module.exports = Buttons; \ No newline at end of file diff --git a/src/structures/List.js b/src/structures/List.js new file mode 100644 index 0000000..cda1186 --- /dev/null +++ b/src/structures/List.js @@ -0,0 +1,79 @@ +'use strict'; + +const Util = require('../util/Util'); + +/** + * Message type List + */ +class List { + /** + * @param {string} body + * @param {string} buttonText + * @param {Array} sections + * @param {string?} title + * @param {string?} footer + */ + constructor(body, buttonText, sections, title, footer) { + /** + * Message body + * @type {string} + */ + this.description = body; + + /** + * List button text + * @type {string} + */ + this.buttonText = buttonText; + + /** + * title of message + * @type {string} + */ + this.title = title; + + + /** + * footer of message + * @type {string} + */ + this.footer = footer; + + /** + * sections of message + * @type {Array} + */ + this.sections = this._format(sections); + + } + + /** + * Creates section array from simple array + * @param {Array} sections + * @returns {Array} + * @example + * Input: [{title:'sectionTitle',rows:[{id:'customId', title:'ListItem2', description: 'desc'},{title:'ListItem2'}]}}] + * Returns: [{'title':'sectionTitle','rows':[{'rowId':'customId','title':'ListItem1','description':'desc'},{'rowId':'oGSRoD','title':'ListItem2','description':''}]}] + */ + _format(sections){ + if(!sections.length){throw '[LT02] List without sections';} + if(sections.length > 1){throw '[LT05] Lists with more than one section are having problems';} + return sections.map( (section) =>{ + if(!section.rows.length){throw '[LT03] Section without rows';} + return { + title: section.title ? section.title : undefined, + rows: section.rows.map( (row) => { + if(!row.title){throw '[LT04] Row without title';} + return { + rowId: row.id ? row.id : Util.generateHash(6), + title: row.title, + description: row.description ? row.description : '' + }; + }) + }; + }); + } + +} + +module.exports = List; \ No newline at end of file diff --git a/src/structures/Message.js b/src/structures/Message.js index c10a675..0ff7c05 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -206,6 +206,21 @@ class Message extends Base { */ this.links = data.links; + /** Buttons */ + if (data.dynamicReplyButtons) { + this.dynamicReplyButtons = data.dynamicReplyButtons; + } + + /** Selected Button Id **/ + if (data.selectedButtonId) { + this.selectedButtonId = data.selectedButtonId; + } + + /** Selected List row Id **/ + if (data.listResponse && data.listResponse.singleSelectReply.selectedRowId) { + this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId; + } + return super._patch(data); } diff --git a/src/structures/index.js b/src/structures/index.js index 025db1f..60a198c 100644 --- a/src/structures/index.js +++ b/src/structures/index.js @@ -15,5 +15,7 @@ module.exports = { Order: require('./Order'), Product: require('./Product'), Call: require('./Call'), + Buttons: require('./Buttons'), + List: require('./List'), Payment: require('./Payment') -}; \ No newline at end of file +}; diff --git a/src/util/Constants.js b/src/util/Constants.js index 30a4138..04abdeb 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -75,6 +75,8 @@ exports.MessageTypes = { PRODUCT: 'product', UNKNOWN: 'unknown', GROUP_INVITE: 'groups_v4_invite', + LIST: 'list', + BUTTONS_RESPONSE: 'buttons_response', PAYMENT: 'payment' }; diff --git a/src/util/Injected.js b/src/util/Injected.js index 53b39bd..b4328dd 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -153,7 +153,47 @@ exports.LoadUtils = () => { options = { ...options, ...preview }; } } + + let extraOptions = {}; + if(options.buttons){ + let caption; + if(options.buttons.type === 'chat') { + content = options.buttons.body; + caption = content; + }else{ + caption = options.caption ? options.caption : ' '; //Caption can't be empty + } + extraOptions = { + productHeaderImageRejected: false, + isFromTemplate: false, + isDynamicReplyButtonsMsg: true, + title: options.buttons.title ? options.buttons.title : undefined, + footer: options.buttons.footer ? options.buttons.footer : undefined, + dynamicReplyButtons: options.buttons.buttons, + replyButtons: options.buttons.buttons, + caption: caption + }; + delete options.buttons; + } + if(options.list){ + if(window.Store.Conn.platform === 'smba' || window.Store.Conn.platform === 'smbi'){ + throw '[LT01] Whatsapp business can\'t send this yet'; + } + extraOptions = { + ...extraOptions, + type: 'list', + footer: options.list.footer, + list: { + ...options.list, + listType: 1 + }, + body: options.list.description + }; + delete options.list; + delete extraOptions.list.footer; + } + const newMsgId = new window.Store.MsgKey({ fromMe: true, remote: chat.id, @@ -175,7 +215,8 @@ exports.LoadUtils = () => { ...locationOptions, ...attOptions, ...quotedMsgOptions, - ...vcardOptions + ...vcardOptions, + ...extraOptions }; await window.Store.SendMessage.addAndSendMsgToChat(chat, message); @@ -282,8 +323,11 @@ exports.LoadUtils = () => { if (msg.buttons) { msg.buttons = msg.buttons.serialize(); } + if (msg.dynamicReplyButtons) { + msg.dynamicReplyButtons = JSON.parse(JSON.stringify(msg.dynamicReplyButtons)); + } if(msg.replyButtons) { - msg.replyButtons = msg.replyButtons.serialize(); + msg.replyButtons = JSON.parse(JSON.stringify(msg.replyButtons)); } delete msg.pendingAckUpdate;