From 67d305be783e8466d9e4e9f5b0034bb3166f76b4 Mon Sep 17 00:00:00 2001 From: alechkos <93551621+alechkos@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:37:30 +0300 Subject: [PATCH] feat: send polls (#2377) * feat: added Poll class * feat: logic for sending polls added * updated poll properties on returned message instance * docs updated * usage example updated * module fixed * 'pollSendOptions' changed to 'options' * window.compareWwebVersions fixed * small update to improve the performance of findImpl fix * added option for custom message secret * usage example updated --- example.js | 18 +++++++++++++++- index.d.ts | 29 +++++++++++++++++++++++++- index.js | 1 + src/Client.js | 7 +++++-- src/structures/Message.js | 16 +++++++++++++- src/structures/Poll.js | 44 +++++++++++++++++++++++++++++++++++++++ src/structures/index.js | 1 + src/util/Constants.js | 1 + src/util/Injected.js | 20 +++++++++++++++++- 9 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 src/structures/Poll.js diff --git a/example.js b/example.js index 98e4ee4..53315c4 100644 --- a/example.js +++ b/example.js @@ -1,4 +1,4 @@ -const { Client, Location, List, Buttons, LocalAuth } = require('./index'); +const { Client, Location, Poll, List, Buttons, LocalAuth } = require('./index'); const client = new Client({ authStrategy: new LocalAuth(), @@ -290,6 +290,22 @@ client.on('message', async msg => { client.sendMessage(msg.from, list); } else if (msg.body === '!reaction') { msg.react('👍'); + } else if (msg.body === '!sendpoll') { + /** By default the poll is created as a single choice poll: */ + await msg.reply(new Poll('Winter or Summer?', ['Winter', 'Summer'])); + /** If you want to provide a multiple choice poll, add allowMultipleAnswers as true: */ + await msg.reply(new Poll('Cats or Dogs?', ['Cats', 'Dogs'], { allowMultipleAnswers: true })); + /** + * You can provide a custom message secret, it can be used as a poll ID: + * @note It has to be a unique vector with a length of 32 + */ + await msg.reply( + new Poll('Cats or Dogs?', ['Cats', 'Dogs'], { + messageSecret: [ + 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ] + }) + ); } else if (msg.body === '!edit') { if (msg.hasQuotedMsg) { const quotedMsg = await msg.getQuotedMessage(); diff --git a/index.d.ts b/index.d.ts index c916158..38014be 100644 --- a/index.d.ts +++ b/index.d.ts @@ -707,6 +707,7 @@ declare namespace WAWebJS { PROTOCOL = 'protocol', REACTION = 'reaction', TEMPLATE_BUTTON_REPLY = 'template_button_reply', + POLL_CREATION = 'poll_creation', } /** Client status */ @@ -866,6 +867,11 @@ declare namespace WAWebJS { selectedRowId?: string, /** Returns message in a raw format */ rawData: object, + pollName: string, + /** Avaiaible poll voting options */ + pollOptions: string[], + /** False for a single choice poll, true for a multiple choice poll */ + allowMultipleAnswers: boolean, /* * Reloads this Message object's data in-place with the latest values from WhatsApp Web. * Note that the Message must still be in the web app cache for this to work, otherwise will return null. @@ -946,6 +952,27 @@ declare namespace WAWebJS { constructor(latitude: number, longitude: number, options?: LocationSendOptions) } + /** Poll send options */ + export interface PollSendOptions { + /** False for a single choice poll, true for a multiple choice poll (false by default) */ + allowMultipleAnswers?: boolean, + /** + * The custom message secret, can be used as a poll ID + * @note It has to be a unique vector with a length of 32 + */ + messageSecret: ?Array + } + + /** Represents a Poll on WhatsApp */ + export interface Poll { + pollName: string, + pollOptions: Array<{ + name: string, + localId: number + }>, + options: PollSendOptions + } + export interface Label { /** Label name */ name: string, @@ -1037,7 +1064,7 @@ declare namespace WAWebJS { static fromUrl: (url: string, options?: MediaFromURLOptions) => Promise } - export type MessageContent = string | MessageMedia | Location | Contact | Contact[] | List | Buttons + export type MessageContent = string | MessageMedia | Location | Poll | Contact | Contact[] | List | Buttons /** * Represents a Contact on WhatsApp diff --git a/index.js b/index.js index b498064..9b7bf85 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ module.exports = { BusinessContact: require('./src/structures/BusinessContact'), ClientInfo: require('./src/structures/ClientInfo'), Location: require('./src/structures/Location'), + Poll: require('./src/structures/Poll'), ProductMetadata: require('./src/structures/ProductMetadata'), List: require('./src/structures/List'), Buttons: require('./src/structures/Buttons'), diff --git a/src/Client.js b/src/Client.js index c5e47f3..d4f5ffd 100644 --- a/src/Client.js +++ b/src/Client.js @@ -11,7 +11,7 @@ const { ExposeStore, LoadUtils } = require('./util/Injected'); const ChatFactory = require('./factories/ChatFactory'); const ContactFactory = require('./factories/ContactFactory'); const WebCacheFactory = require('./webCache/WebCacheFactory'); -const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures'); +const { ClientInfo, Message, MessageMedia, Contact, Location, Poll, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures'); const LegacySessionAuth = require('./authStrategies/LegacySessionAuth'); const NoAuth = require('./authStrategies/NoAuth'); @@ -820,7 +820,7 @@ class Client extends EventEmitter { /** * Send a message to a specific chatId * @param {string} chatId - * @param {string|MessageMedia|Location|Contact|Array|Buttons|List} content + * @param {string|MessageMedia|Location|Poll|Contact|Array|Buttons|List} content * @param {MessageSendOptions} [options] - Options used when sending the message * * @returns {Promise} Message that was just sent @@ -857,6 +857,9 @@ class Client extends EventEmitter { } else if (content instanceof Location) { internalOptions.location = content; content = ''; + } else if (content instanceof Poll) { + internalOptions.poll = content; + content = ''; } else if (content instanceof Contact) { internalOptions.contactCard = content.id._serialized; content = ''; diff --git a/src/structures/Message.js b/src/structures/Message.js index 99a86a8..5886b82 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -51,7 +51,7 @@ class Message extends Base { * Message content * @type {string} */ - this.body = this.hasMedia ? data.caption || '' : data.body || ''; + this.body = this.hasMedia ? data.caption || '' : data.body || data.pollName || ''; /** * Message type @@ -271,6 +271,20 @@ class Message extends Base { this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId; } + if (this.type === MessageTypes.POLL_CREATION) { + this.pollName = data.pollName; + this.pollOptions = data.pollOptions; + this.allowMultipleAnswers = Boolean(!data.pollSelectableOptionsCount); + this.pollInvalidated = data.pollInvalidated; + this.isSentCagPollCreation = data.isSentCagPollCreation; + + delete this._data.pollName; + delete this._data.pollOptions; + delete this._data.pollSelectableOptionsCount; + delete this._data.pollInvalidated; + delete this._data.isSentCagPollCreation; + } + return super._patch(data); } diff --git a/src/structures/Poll.js b/src/structures/Poll.js new file mode 100644 index 0000000..71d7a98 --- /dev/null +++ b/src/structures/Poll.js @@ -0,0 +1,44 @@ +'use strict'; + +/** + * Poll send options + * @typedef {Object} PollSendOptions + * @property {boolean} [allowMultipleAnswers=false] If false it is a single choice poll, otherwise it is a multiple choice poll (false by default) + * @property {?Array} messageSecret The custom message secret, can be used as a poll ID. NOTE: it has to be a unique vector with a length of 32 + */ + +/** Represents a Poll on WhatsApp */ +class Poll { + /** + * @param {string} pollName + * @param {Array} pollOptions + * @param {PollSendOptions} options + */ + constructor(pollName, pollOptions, options = {}) { + /** + * The name of the poll + * @type {string} + */ + this.pollName = pollName.trim(); + + /** + * The array of poll options + * @type {Array.<{name: string, localId: number}>} + */ + this.pollOptions = pollOptions.map((option, index) => ({ + name: option.trim(), + localId: index + })); + + /** + * The send options for the poll + * @type {PollSendOptions} + */ + this.options = { + allowMultipleAnswers: options.allowMultipleAnswers === true, + messageSecret: options.messageSecret + }; + } +} + +module.exports = Poll; diff --git a/src/structures/index.js b/src/structures/index.js index ec449f0..796d514 100644 --- a/src/structures/index.js +++ b/src/structures/index.js @@ -19,4 +19,5 @@ module.exports = { List: require('./List'), Payment: require('./Payment'), Reaction: require('./Reaction'), + Poll: require('./Poll'), }; diff --git a/src/util/Constants.js b/src/util/Constants.js index 276e5e6..f213f84 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -108,6 +108,7 @@ exports.MessageTypes = { PROTOCOL: 'protocol', REACTION: 'reaction', TEMPLATE_BUTTON_REPLY: 'template_button_reply', + POLL_CREATION: 'poll_creation', }; /** diff --git a/src/util/Injected.js b/src/util/Injected.js index d94588a..b71f0b7 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -142,7 +142,7 @@ exports.ExposeStore = (moduleRaidStr) => { const modifiedFunction = (...args) => callback(originalFunction, ...args); module[target.index][target.property] = modifiedFunction; }; - + window.injectToFunction({ moduleId: 'mediaTypeFromProtobuf', index: 0, property: 'mediaTypeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage ? null : func(...args); }); window.injectToFunction({ moduleId: 'typeAttributeFromProtobuf', index: 0, property: 'typeAttributeFromProtobuf' }, (func, ...args) => { const [proto] = args; return proto.locationMessage || proto.groupInviteMessage ? 'text' : func(...args); }); @@ -215,6 +215,23 @@ exports.LoadUtils = () => { delete options.location; } + let _pollOptions = {}; + if (options.poll) { + const { pollName, pollOptions } = options.poll; + const { allowMultipleAnswers, messageSecret } = options.poll.options; + _pollOptions = { + type: 'poll_creation', + pollName: pollName, + pollOptions: pollOptions, + pollSelectableOptionsCount: allowMultipleAnswers ? 0 : 1, + messageSecret: + Array.isArray(messageSecret) && messageSecret.length === 32 + ? new Uint8Array(messageSecret) + : window.crypto.getRandomValues(new Uint8Array(32)) + }; + delete options.poll; + } + let vcardOptions = {}; if (options.contactCard) { let contact = window.Store.Contact.get(options.contactCard); @@ -334,6 +351,7 @@ exports.LoadUtils = () => { type: 'chat', ...ephemeralFields, ...locationOptions, + ..._pollOptions, ...attOptions, ...(attOptions.toJSON ? attOptions.toJSON() : {}), ...quotedMsgOptions,