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
This commit is contained in:
alechkos 2023-10-19 19:37:30 +03:00 • committed by GitHub
commit 67d305be78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 131 additions and 6 deletions

View file

@ -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({ const client = new Client({
authStrategy: new LocalAuth(), authStrategy: new LocalAuth(),
@ -290,6 +290,22 @@ client.on('message', async msg => {
client.sendMessage(msg.from, list); client.sendMessage(msg.from, list);
} else if (msg.body === '!reaction') { } else if (msg.body === '!reaction') {
msg.react('👍'); 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') { } else if (msg.body === '!edit') {
if (msg.hasQuotedMsg) { if (msg.hasQuotedMsg) {
const quotedMsg = await msg.getQuotedMessage(); const quotedMsg = await msg.getQuotedMessage();

29
index.d.ts vendored
View file

@ -707,6 +707,7 @@ declare namespace WAWebJS {
PROTOCOL = 'protocol', PROTOCOL = 'protocol',
REACTION = 'reaction', REACTION = 'reaction',
TEMPLATE_BUTTON_REPLY = 'template_button_reply', TEMPLATE_BUTTON_REPLY = 'template_button_reply',
POLL_CREATION = 'poll_creation',
} }
/** Client status */ /** Client status */
@ -866,6 +867,11 @@ declare namespace WAWebJS {
selectedRowId?: string, selectedRowId?: string,
/** Returns message in a raw format */ /** Returns message in a raw format */
rawData: object, 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. * 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. * 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) 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<number>
}
/** Represents a Poll on WhatsApp */
export interface Poll {
pollName: string,
pollOptions: Array<{
name: string,
localId: number
}>,
options: PollSendOptions
}
export interface Label { export interface Label {
/** Label name */ /** Label name */
name: string, name: string,
@ -1037,7 +1064,7 @@ declare namespace WAWebJS {
static fromUrl: (url: string, options?: MediaFromURLOptions) => Promise<MessageMedia> static fromUrl: (url: string, options?: MediaFromURLOptions) => Promise<MessageMedia>
} }
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 * Represents a Contact on WhatsApp

View file

@ -18,6 +18,7 @@ module.exports = {
BusinessContact: require('./src/structures/BusinessContact'), BusinessContact: require('./src/structures/BusinessContact'),
ClientInfo: require('./src/structures/ClientInfo'), ClientInfo: require('./src/structures/ClientInfo'),
Location: require('./src/structures/Location'), Location: require('./src/structures/Location'),
Poll: require('./src/structures/Poll'),
ProductMetadata: require('./src/structures/ProductMetadata'), ProductMetadata: require('./src/structures/ProductMetadata'),
List: require('./src/structures/List'), List: require('./src/structures/List'),
Buttons: require('./src/structures/Buttons'), Buttons: require('./src/structures/Buttons'),

View file

@ -11,7 +11,7 @@ const { ExposeStore, LoadUtils } = require('./util/Injected');
const ChatFactory = require('./factories/ChatFactory'); const ChatFactory = require('./factories/ChatFactory');
const ContactFactory = require('./factories/ContactFactory'); const ContactFactory = require('./factories/ContactFactory');
const WebCacheFactory = require('./webCache/WebCacheFactory'); 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 LegacySessionAuth = require('./authStrategies/LegacySessionAuth');
const NoAuth = require('./authStrategies/NoAuth'); const NoAuth = require('./authStrategies/NoAuth');
@ -820,7 +820,7 @@ class Client extends EventEmitter {
/** /**
* Send a message to a specific chatId * Send a message to a specific chatId
* @param {string} chatId * @param {string} chatId
* @param {string|MessageMedia|Location|Contact|Array<Contact>|Buttons|List} content * @param {string|MessageMedia|Location|Poll|Contact|Array<Contact>|Buttons|List} content
* @param {MessageSendOptions} [options] - Options used when sending the message * @param {MessageSendOptions} [options] - Options used when sending the message
* *
* @returns {Promise<Message>} Message that was just sent * @returns {Promise<Message>} Message that was just sent
@ -857,6 +857,9 @@ class Client extends EventEmitter {
} else if (content instanceof Location) { } else if (content instanceof Location) {
internalOptions.location = content; internalOptions.location = content;
content = ''; content = '';
} else if (content instanceof Poll) {
internalOptions.poll = content;
content = '';
} else if (content instanceof Contact) { } else if (content instanceof Contact) {
internalOptions.contactCard = content.id._serialized; internalOptions.contactCard = content.id._serialized;
content = ''; content = '';

View file

@ -51,7 +51,7 @@ class Message extends Base {
* Message content * Message content
* @type {string} * @type {string}
*/ */
this.body = this.hasMedia ? data.caption || '' : data.body || ''; this.body = this.hasMedia ? data.caption || '' : data.body || data.pollName || '';
/** /**
* Message type * Message type
@ -271,6 +271,20 @@ class Message extends Base {
this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId; 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); return super._patch(data);
} }

44
src/structures/Poll.js Normal file
View file

@ -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<number>} 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<string>} 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;

View file

@ -19,4 +19,5 @@ module.exports = {
List: require('./List'), List: require('./List'),
Payment: require('./Payment'), Payment: require('./Payment'),
Reaction: require('./Reaction'), Reaction: require('./Reaction'),
Poll: require('./Poll'),
}; };

View file

@ -108,6 +108,7 @@ exports.MessageTypes = {
PROTOCOL: 'protocol', PROTOCOL: 'protocol',
REACTION: 'reaction', REACTION: 'reaction',
TEMPLATE_BUTTON_REPLY: 'template_button_reply', TEMPLATE_BUTTON_REPLY: 'template_button_reply',
POLL_CREATION: 'poll_creation',
}; };
/** /**

View file

@ -142,7 +142,7 @@ exports.ExposeStore = (moduleRaidStr) => {
const modifiedFunction = (...args) => callback(originalFunction, ...args); const modifiedFunction = (...args) => callback(originalFunction, ...args);
module[target.index][target.property] = modifiedFunction; 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: '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); }); 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; 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 = {}; let vcardOptions = {};
if (options.contactCard) { if (options.contactCard) {
let contact = window.Store.Contact.get(options.contactCard); let contact = window.Store.Contact.get(options.contactCard);
@ -334,6 +351,7 @@ exports.LoadUtils = () => {
type: 'chat', type: 'chat',
...ephemeralFields, ...ephemeralFields,
...locationOptions, ...locationOptions,
..._pollOptions,
...attOptions, ...attOptions,
...(attOptions.toJSON ? attOptions.toJSON() : {}), ...(attOptions.toJSON ? attOptions.toJSON() : {}),
...quotedMsgOptions, ...quotedMsgOptions,