diff --git a/index.d.ts b/index.d.ts index 4da695e..be5089a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events' import { RequestInit } from 'node-fetch' import * as puppeteer from 'puppeteer' +import PollVote from './src/structures/PollVote' declare namespace WAWebJS { @@ -268,6 +269,12 @@ declare namespace WAWebJS { /** Emitted when the RemoteAuth session is saved successfully on the external Database */ on(event: 'remote_session_saved', listener: () => void): this + + /** Emitted when a poll vote is received */ + on(event: 'poll_vote', listener: ( + /** The poll vote */ + vote: PollVote + ) => void): this } /** Current connection information */ @@ -506,7 +513,8 @@ declare namespace WAWebJS { DISCONNECTED = 'disconnected', STATE_CHANGED = 'change_state', BATTERY_CHANGED = 'change_battery', - REMOTE_SESSION_SAVED = 'remote_session_saved' + REMOTE_SESSION_SAVED = 'remote_session_saved', + POLL_VOTE = 'poll_vote' } /** Group notification types */ @@ -569,6 +577,7 @@ declare namespace WAWebJS { PROTOCOL = 'protocol', REACTION = 'reaction', TEMPLATE_BUTTON_REPLY = 'template_button_reply', + POLL_CREATION = 'poll_creation', } /** Client status */ @@ -721,6 +730,10 @@ declare namespace WAWebJS { selectedRowId?: string, /** Returns message in a raw format */ rawData: object, + /** Avaiaible poll voting options */ + pollOptions: string[], + /** The current poll votes, refresh with .refreshPollVotes() */ + pollVotes: PollVote[], /* * 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. @@ -766,6 +779,15 @@ declare namespace WAWebJS { * Gets the payment details associated with a given message */ getPayment: () => Promise, + /** + * Refreshes the current poll votes, only works with a poll_creation message + */ + refreshPollVotes: () => Promise, + /** + * Vote on the poll, only works with a poll_creation message + * @param {Array} selectedOptions The selected options from .pollOptions + */ + vote: (selectedOptions: string[]) => Promise, } /** ID that represents a message */ diff --git a/src/Client.js b/src/Client.js index d3c9bb4..0182e2a 100644 --- a/src/Client.js +++ b/src/Client.js @@ -13,6 +13,7 @@ const ContactFactory = require('./factories/ContactFactory'); const { ClientInfo, Message, MessageMedia, Contact, Location, GroupNotification, Label, Call, Buttons, List, Reaction } = require('./structures'); const LegacySessionAuth = require('./authStrategies/LegacySessionAuth'); const NoAuth = require('./authStrategies/NoAuth'); +const PollVote = require('./structures/PollVote'); /** * Starting point for interacting with the WhatsApp Web API @@ -45,6 +46,7 @@ const NoAuth = require('./authStrategies/NoAuth'); * @fires Client#group_update * @fires Client#disconnected * @fires Client#change_state + * @fires */ class Client extends EventEmitter { constructor(options = {}) { @@ -486,6 +488,21 @@ class Client extends EventEmitter { this.emit(Events.INCOMING_CALL, cll); }); + await page.exposeFunction('onPollVote', (vote) => { + if (vote.parentMsgKey) vote.pollCreationMessage = window.Store.Msg.get(vote.parentMsgKey).serialize(); + const vote_ = new PollVote(this, vote); + + /** + * Emitted when a poll vote is received + * @event Client#poll_vote + * @param {object} vote + * @param {string} vote.sender Sender of the vote + * @param {number} vote.senderTimestampMs Timestamp the vote was sent + * @param {Array} vote.selectedOptions Options selected + */ + this.emit(Events.POLL_VOTE, vote_); + }); + await page.exposeFunction('onReaction', (reactions) => { for (const reaction of reactions) { /** @@ -526,6 +543,7 @@ class Client extends EventEmitter { } } }); + window.Store.PollVote.on('add', (vote) => window.onPollVote) { const module = window.Store.createOrUpdateReactionsModule; diff --git a/src/structures/Message.js b/src/structures/Message.js index ee79c20..5e662c0 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -6,6 +6,7 @@ const Location = require('./Location'); const Order = require('./Order'); const Payment = require('./Payment'); const { MessageTypes } = require('../util/Constants'); +const PollVote = require('./PollVote'); /** * Represents a Message on WhatsApp @@ -240,6 +241,19 @@ class Message extends Base { this.selectedRowId = data.listResponse.singleSelectReply.selectedRowId; } + if (this.type == MessageTypes.POLL_CREATION) { + + /** Selectable poll options */ + this.pollOptions = data.pollOptions.map(option => { + return option.name; + }); + + /** Current poll votes, refresh with Message.refreshPollVotes() */ + this.pollVotes = data.pollVotes.map((pollVote) => { + return new PollVote(this.client, pollVote); + }); + } + return super._patch(data); } @@ -527,6 +541,34 @@ class Message extends Base { } return undefined; } + + /** + * Refresh the current poll votes + * @returns {Promise} + */ + async refreshPollVotes() { + if (this.type != MessageTypes.POLL_CREATION) throw 'Invalid usage! Can only be used with a pollCreation message'; + const pollVotes = await this.client.evaluate((parentMsgId) => { + return Store.PollVote.getForParent(parentMsgId).getModelsArray().map(a => a.serialize()) + }, this.id); + this.pollVotes = pollVotes.map((pollVote) => { + return new PollVote(this.client, pollVote); + }); + return; + } + + /** + * Vote to the poll. + * @param {Array} selectedOptions Array of options selected. + * @returns {Promise} + */ + async vote(selectedOptions) { + if (this.type != MessageTypes.POLL_CREATION) throw 'Invalid usage! Can only be used with a pollCreation message'; + + return this.client.evaluate((creationMsgId, selectedOptions) => { + window.WWebJS.votePoll(creationMsgId, selectedOptions); + }, this.id, selectedOptions); + } } module.exports = Message; diff --git a/src/structures/PollVote.js b/src/structures/PollVote.js new file mode 100644 index 0000000..c3f7a9a --- /dev/null +++ b/src/structures/PollVote.js @@ -0,0 +1,32 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a Poll Vote on WhatsApp + * @extends {Base} + */ +class PollVote extends Base { + constructor(client, data) { + super(client); + + if (data) this._patch(data); + } + + _patch(data) { + /** The options selected in this Poll vote */ + this.selectedOptions = data.selectedOptionLocalIds.filter(value => value == 1).map((value, selectedOptionLocalId) => { + return data.pollCreationMessage.pollOptions.find(a => a.localId == selectedOptionLocalId).name; + }); + + /** Sender of the Poll vote */ + this.sender = data.sender; + + /** Timestamp of the time it was sent in milliseconds */ + this.senderTimestampMs = data.senderTimestampMs; + + return super._patch(data); + } +} + +module.exports = PollVote; \ No newline at end of file diff --git a/src/util/Constants.js b/src/util/Constants.js index d062c7a..11f8b6c 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -52,7 +52,8 @@ exports.Events = { STATE_CHANGED: 'change_state', BATTERY_CHANGED: 'change_battery', INCOMING_CALL: 'incoming_call', - REMOTE_SESSION_SAVED: 'remote_session_saved' + REMOTE_SESSION_SAVED: 'remote_session_saved', + POLL_VOTE: 'poll_vote' }; /** @@ -96,6 +97,8 @@ 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 a0a7b12..0aeac79 100644 --- a/src/util/Injected.js +++ b/src/util/Injected.js @@ -6,65 +6,67 @@ exports.ExposeStore = (moduleRaidStr) => { // eslint-disable-next-line no-undef window.mR = moduleRaid(); window.Store = Object.assign({}, window.mR.findModule(m => m.default && m.default.Chat)[0].default); + window.Store.AppState = window.mR.findModule('Socket')[0].Socket; - window.Store.Conn = window.mR.findModule('Conn')[0].Conn; window.Store.BlockContact = window.mR.findModule('blockContact')[0]; window.Store.Call = window.mR.findModule('CallCollection')[0].CallCollection; + window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0]; window.Store.Cmd = window.mR.findModule('Cmd')[0].Cmd; + window.Store.Conn = window.mR.findModule('Conn')[0].Conn; + window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0]; + window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0]; window.Store.CryptoLib = window.mR.findModule('decryptE2EMedia')[0]; window.Store.DownloadManager = window.mR.findModule('downloadManager')[0].downloadManager; - window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].LegacyPhoneFeatures; - window.Store.GroupMetadata = window.mR.findModule('GroupMetadata')[0].default.GroupMetadata; - window.Store.Invite = window.mR.findModule('sendJoinGroupViaInvite')[0]; - window.Store.InviteInfo = window.mR.findModule('sendQueryGroupInvite')[0]; - window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection; - window.Store.MediaPrep = window.mR.findModule('MediaPrep')[0]; - window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0]; - window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0]; - window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0]; - window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0]; - window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default; - window.Store.MessageInfo = window.mR.findModule('sendQueryMsgInfo')[0]; - window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default; - window.Store.QueryExist = window.mR.findModule('queryExists')[0].queryExists; - window.Store.QueryProduct = window.mR.findModule('queryProduct')[0]; - window.Store.QueryOrder = window.mR.findModule('queryOrder')[0]; - window.Store.SendClear = window.mR.findModule('sendClear')[0]; - window.Store.SendDelete = window.mR.findModule('sendDelete')[0]; - window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0]; - window.Store.SendSeen = window.mR.findModule('sendSeen')[0]; - window.Store.User = window.mR.findModule('getMaybeMeUser')[0]; - window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default; - window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default; - window.Store.Validators = window.mR.findModule('findLinks')[0]; - window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0]; - window.Store.WidFactory = window.mR.findModule('createWid')[0]; - window.Store.ProfilePic = window.mR.findModule('profilePicResync')[0]; - window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0]; - window.Store.ChatState = window.mR.findModule('sendChatStateComposing')[0]; - window.Store.GroupParticipants = window.mR.findModule('sendPromoteParticipants')[0]; - window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0]; - window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups; - window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0]; - window.Store.ConversationMsgs = window.mR.findModule('loadEarlierMsgs')[0]; - window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg; - window.Store.createOrUpdateReactionsModule = window.mR.findModule('createOrUpdateReactions')[0]; window.Store.EphemeralFields = window.mR.findModule('getEphemeralFields')[0]; - window.Store.ReplyUtils = window.mR.findModule('canReplyMsg').length > 0 && window.mR.findModule('canReplyMsg')[0]; - window.Store.MsgActionChecks = window.mR.findModule('canSenderRevokeMsg')[0]; - window.Store.QuotedMsg = window.mR.findModule('getQuotedMsgObj')[0]; - window.Store.Socket = window.mR.findModule('deprecatedSendIq')[0]; - window.Store.SocketWap = window.mR.findModule('wap')[0]; - window.Store.StickerTools = { - ...window.mR.findModule('toWebpSticker')[0], - ...window.mR.findModule('addWebpMetadata')[0] - }; - + window.Store.Features = window.mR.findModule('FEATURE_CHANGE_EVENT')[0].LegacyPhoneFeatures; + window.Store.findCommonGroups = window.mR.findModule('findCommonGroups')[0].findCommonGroups; + window.Store.GroupMetadata = window.mR.findModule('GroupMetadata')[0].default.GroupMetadata; + window.Store.GroupParticipants = window.mR.findModule('sendPromoteParticipants')[0]; window.Store.GroupUtils = { ...window.mR.findModule('sendCreateGroup')[0], ...window.mR.findModule('sendSetGroupSubject')[0], ...window.mR.findModule('markExited')[0] }; + window.Store.Invite = window.mR.findModule('sendJoinGroupViaInvite')[0]; + window.Store.InviteInfo = window.mR.findModule('sendQueryGroupInvite')[0]; + window.Store.JoinInviteV4 = window.mR.findModule('sendJoinGroupViaInviteV4')[0]; + window.Store.Label = window.mR.findModule('LabelCollection')[0].LabelCollection; + window.Store.MediaObject = window.mR.findModule('getOrCreateMediaObject')[0]; + window.Store.MediaPrep = window.mR.findModule('MediaPrep')[0]; + window.Store.MediaTypes = window.mR.findModule('msgToMediaType')[0]; + window.Store.MediaUpload = window.mR.findModule('uploadMedia')[0]; + window.Store.MessageInfo = window.mR.findModule('sendQueryMsgInfo')[0]; + window.Store.MsgActionChecks = window.mR.findModule('canSenderRevokeMsg')[0]; + window.Store.MsgKey = window.mR.findModule((module) => module.default && module.default.fromString)[0].default; + window.Store.NumberInfo = window.mR.findModule('formattedPhoneNumber')[0]; + window.Store.OpaqueData = window.mR.findModule(module => module.default && module.default.createFromData)[0].default; + window.Store.PresenceUtils = window.mR.findModule('sendPresenceAvailable')[0]; + window.Store.ProfilePic = window.mR.findModule('profilePicResync')[0]; + window.Store.QueryExist = window.mR.findModule('queryExists')[0].queryExists; + window.Store.QueryOrder = window.mR.findModule('queryOrder')[0]; + window.Store.QueryProduct = window.mR.findModule('queryProduct')[0]; + window.Store.QuotedMsg = window.mR.findModule('getQuotedMsgObj')[0]; + window.Store.ReplyUtils = window.mR.findModule('canReplyMsg').length > 0 && window.mR.findModule('canReplyMsg')[0]; + window.Store.SendClear = window.mR.findModule('sendClear')[0]; + window.Store.SendDelete = window.mR.findModule('sendDelete')[0]; + window.Store.SendMessage = window.mR.findModule('addAndSendMsgToChat')[0]; + window.Store.sendReactionToMsg = window.mR.findModule('sendReactionToMsg')[0].sendReactionToMsg; + window.Store.SendSeen = window.mR.findModule('sendSeen')[0]; + window.Store.SendVote = window.mR.findModule('sendVote')[0]; + window.Store.Socket = window.mR.findModule('deprecatedSendIq')[0]; + window.Store.SocketWap = window.mR.findModule('wap')[0]; + window.Store.StatusUtils = window.mR.findModule('setMyStatus')[0]; + window.Store.StickerTools = { + ...window.mR.findModule('toWebpSticker')[0], + ...window.mR.findModule('addWebpMetadata')[0] + }; + window.Store.UploadUtils = window.mR.findModule((module) => (module.default && module.default.encryptAndUpload) ? module.default : null)[0].default; + window.Store.User = window.mR.findModule('getMaybeMeUser')[0]; + window.Store.UserConstructor = window.mR.findModule((module) => (module.default && module.default.prototype && module.default.prototype.isServer && module.default.prototype.isUser) ? module.default : null)[0].default; + window.Store.Validators = window.mR.findModule('findLinks')[0]; + window.Store.VCard = window.mR.findModule('vcardFromContactModel')[0]; + window.Store.WidFactory = window.mR.findModule('createWid')[0]; + if (!window.Store.Chat._find) { window.Store.Chat._find = e => { @@ -404,6 +406,10 @@ exports.LoadUtils = () => { msg.id = Object.assign({}, msg.id, { remote: msg.id.remote._serialized }); } + if (msg.type == 'poll_creation') { + msg.pollVotes = Store.PollVote.getForParent(msg.id).getModelsArray().map(a => a.serialize()); + } + delete msg.pendingAckUpdate; return msg; @@ -621,4 +627,16 @@ exports.LoadUtils = () => { ]); await window.Store.Socket.deprecatedCastStanza(stanza); }; + + window.WWebJS.votePoll = async (pollCreationMessageId, selectedOptions) => { + const msg = window.Store.Msg.get(pollCreationMessageId); + if (msg.type != 'poll_creation') throw 'Quoted message is not a poll creation message!'; + let localIdSet = new Set(); + msg.pollOptions.map(a => { + for (const option of selectedOptions) { + if (a.name == option) localIdSet.add(a.localId); + } + }) + await window.Store.SendVote.sendVote(msg, localIdSet); + } };