From 82f296b17d8e844edcc6579fc34d8a9578c205da Mon Sep 17 00:00:00 2001 From: devlikepro Date: Sun, 25 May 2025 16:22:13 +0700 Subject: [PATCH] [core] GOWS - Polls fix devlikeapro/waha#899 --- src/core/engines/gows/grpc/gows.ts | 139 ++++++++++ src/core/engines/gows/grpc/gows_pb.js | 291 ++++++++++++++++++++- src/core/engines/gows/session.gows.core.ts | 78 +++++- src/structures/chatting.dto.ts | 1 + src/structures/webhooks.dto.ts | 1 + 5 files changed, 506 insertions(+), 4 deletions(-) diff --git a/src/core/engines/gows/grpc/gows.ts b/src/core/engines/gows/grpc/gows.ts index af013b54..15c92873 100644 --- a/src/core/engines/gows/grpc/gows.ts +++ b/src/core/engines/gows/grpc/gows.ts @@ -3262,6 +3262,7 @@ export namespace messages { preview?: LinkPreview; contacts?: vCardContact[]; event?: EventMessage; + poll?: PollMessage; }) { super(); pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [11, 13], this.#one_of_decls); @@ -3308,6 +3309,9 @@ export namespace messages { if ("event" in data && data.event != undefined) { this.event = data.event; } + if ("poll" in data && data.poll != undefined) { + this.poll = data.poll; + } } } get session() { @@ -3412,6 +3416,15 @@ export namespace messages { get has_event() { return pb_1.Message.getField(this, 14) != null; } + get poll() { + return pb_1.Message.getWrapperField(this, PollMessage, 15) as PollMessage; + } + set poll(value: PollMessage) { + pb_1.Message.setWrapperField(this, 15, value); + } + get has_poll() { + return pb_1.Message.getField(this, 15) != null; + } static fromObject(data: { session?: ReturnType; jid?: string; @@ -3427,6 +3440,7 @@ export namespace messages { preview?: ReturnType; contacts?: ReturnType[]; event?: ReturnType; + poll?: ReturnType; }): MessageRequest { const message = new MessageRequest({}); if (data.session != null) { @@ -3471,6 +3485,9 @@ export namespace messages { if (data.event != null) { message.event = EventMessage.fromObject(data.event); } + if (data.poll != null) { + message.poll = PollMessage.fromObject(data.poll); + } return message; } toObject() { @@ -3489,6 +3506,7 @@ export namespace messages { preview?: ReturnType; contacts?: ReturnType[]; event?: ReturnType; + poll?: ReturnType; } = {}; if (this.session != null) { data.session = this.session.toObject(); @@ -3532,6 +3550,9 @@ export namespace messages { if (this.event != null) { data.event = this.event.toObject(); } + if (this.poll != null) { + data.poll = this.poll.toObject(); + } return data; } serialize(): Uint8Array; @@ -3566,6 +3587,8 @@ export namespace messages { writer.writeRepeatedMessage(13, this.contacts, (item: vCardContact) => item.serialize(writer)); if (this.has_event) writer.writeMessage(14, this.event, () => this.event.serialize(writer)); + if (this.has_poll) + writer.writeMessage(15, this.poll, () => this.poll.serialize(writer)); if (!w) return writer.getResultBuffer(); } @@ -3617,6 +3640,9 @@ export namespace messages { case 14: reader.readMessage(message.event, () => message.event = EventMessage.deserialize(reader)); break; + case 15: + reader.readMessage(message.poll, () => message.poll = PollMessage.deserialize(reader)); + break; default: reader.skipField(); } } @@ -8790,6 +8816,119 @@ export namespace messages { return GetLidsRequest.deserialize(bytes); } } + export class PollMessage extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + name?: string; + options?: string[]; + multipleAnswers?: boolean; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [2], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("name" in data && data.name != undefined) { + this.name = data.name; + } + if ("options" in data && data.options != undefined) { + this.options = data.options; + } + if ("multipleAnswers" in data && data.multipleAnswers != undefined) { + this.multipleAnswers = data.multipleAnswers; + } + } + } + get name() { + return pb_1.Message.getFieldWithDefault(this, 1, "") as string; + } + set name(value: string) { + pb_1.Message.setField(this, 1, value); + } + get options() { + return pb_1.Message.getFieldWithDefault(this, 2, []) as string[]; + } + set options(value: string[]) { + pb_1.Message.setField(this, 2, value); + } + get multipleAnswers() { + return pb_1.Message.getFieldWithDefault(this, 3, false) as boolean; + } + set multipleAnswers(value: boolean) { + pb_1.Message.setField(this, 3, value); + } + static fromObject(data: { + name?: string; + options?: string[]; + multipleAnswers?: boolean; + }): PollMessage { + const message = new PollMessage({}); + if (data.name != null) { + message.name = data.name; + } + if (data.options != null) { + message.options = data.options; + } + if (data.multipleAnswers != null) { + message.multipleAnswers = data.multipleAnswers; + } + return message; + } + toObject() { + const data: { + name?: string; + options?: string[]; + multipleAnswers?: boolean; + } = {}; + if (this.name != null) { + data.name = this.name; + } + if (this.options != null) { + data.options = this.options; + } + if (this.multipleAnswers != null) { + data.multipleAnswers = this.multipleAnswers; + } + return data; + } + serialize(): Uint8Array; + serialize(w: pb_1.BinaryWriter): void; + serialize(w?: pb_1.BinaryWriter): Uint8Array | void { + const writer = w || new pb_1.BinaryWriter(); + if (this.name.length) + writer.writeString(1, this.name); + if (this.options.length) + writer.writeRepeatedString(2, this.options); + if (this.multipleAnswers != false) + writer.writeBool(3, this.multipleAnswers); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): PollMessage { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new PollMessage(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 1: + message.name = reader.readString(); + break; + case 2: + pb_1.Message.addToRepeatedField(message, 2, reader.readString()); + break; + case 3: + message.multipleAnswers = reader.readBool(); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): PollMessage { + return PollMessage.deserialize(bytes); + } + } interface GrpcUnaryServiceInterface { (message: P, metadata: grpc_1.Metadata, options: grpc_1.CallOptions, callback: grpc_1.requestCallback): grpc_1.ClientUnaryCall; (message: P, metadata: grpc_1.Metadata, callback: grpc_1.requestCallback): grpc_1.ClientUnaryCall; diff --git a/src/core/engines/gows/grpc/gows_pb.js b/src/core/engines/gows/grpc/gows_pb.js index b5d02bee..97351bbe 100644 --- a/src/core/engines/gows/grpc/gows_pb.js +++ b/src/core/engines/gows/grpc/gows_pb.js @@ -81,6 +81,7 @@ goog.exportSymbol('proto.messages.PairCodeRequest', null, global); goog.exportSymbol('proto.messages.PairCodeResponse', null, global); goog.exportSymbol('proto.messages.ParticipantAction', null, global); goog.exportSymbol('proto.messages.PhoneInfo', null, global); +goog.exportSymbol('proto.messages.PollMessage', null, global); goog.exportSymbol('proto.messages.Presence', null, global); goog.exportSymbol('proto.messages.PresenceRequest', null, global); goog.exportSymbol('proto.messages.ProfileNameRequest', null, global); @@ -1788,6 +1789,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.messages.GetLidsRequest.displayName = 'proto.messages.GetLidsRequest'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.messages.PollMessage = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.messages.PollMessage.repeatedFields_, null); +}; +goog.inherits(proto.messages.PollMessage, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.messages.PollMessage.displayName = 'proto.messages.PollMessage'; +} @@ -7903,7 +7925,8 @@ proto.messages.MessageRequest.toObject = function(includeInstance, msg) { preview: (f = msg.getPreview()) && proto.messages.LinkPreview.toObject(includeInstance, f), contactsList: jspb.Message.toObjectList(msg.getContactsList(), proto.messages.vCardContact.toObject, includeInstance), - event: (f = msg.getEvent()) && proto.messages.EventMessage.toObject(includeInstance, f) + event: (f = msg.getEvent()) && proto.messages.EventMessage.toObject(includeInstance, f), + poll: (f = msg.getPoll()) && proto.messages.PollMessage.toObject(includeInstance, f) }; if (includeInstance) { @@ -8003,6 +8026,11 @@ proto.messages.MessageRequest.deserializeBinaryFromReader = function(msg, reader reader.readMessage(value,proto.messages.EventMessage.deserializeBinaryFromReader); msg.setEvent(value); break; + case 15: + var value = new proto.messages.PollMessage; + reader.readMessage(value,proto.messages.PollMessage.deserializeBinaryFromReader); + msg.setPoll(value); + break; default: reader.skipField(); break; @@ -8137,6 +8165,14 @@ proto.messages.MessageRequest.serializeBinaryToWriter = function(message, writer proto.messages.EventMessage.serializeBinaryToWriter ); } + f = message.getPoll(); + if (f != null) { + writer.writeMessage( + 15, + f, + proto.messages.PollMessage.serializeBinaryToWriter + ); + } }; @@ -8545,6 +8581,43 @@ proto.messages.MessageRequest.prototype.hasEvent = function() { }; +/** + * optional PollMessage poll = 15; + * @return {?proto.messages.PollMessage} + */ +proto.messages.MessageRequest.prototype.getPoll = function() { + return /** @type{?proto.messages.PollMessage} */ ( + jspb.Message.getWrapperField(this, proto.messages.PollMessage, 15)); +}; + + +/** + * @param {?proto.messages.PollMessage|undefined} value + * @return {!proto.messages.MessageRequest} returns this +*/ +proto.messages.MessageRequest.prototype.setPoll = function(value) { + return jspb.Message.setWrapperField(this, 15, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.messages.MessageRequest} returns this + */ +proto.messages.MessageRequest.prototype.clearPoll = function() { + return this.setPoll(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.messages.MessageRequest.prototype.hasPoll = function() { + return jspb.Message.getField(this, 15) != null; +}; + + @@ -18327,6 +18400,222 @@ proto.messages.GetLidsRequest.prototype.hasSession = function() { }; + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.messages.PollMessage.repeatedFields_ = [2]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.messages.PollMessage.prototype.toObject = function(opt_includeInstance) { + return proto.messages.PollMessage.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.messages.PollMessage} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.PollMessage.toObject = function(includeInstance, msg) { + var f, obj = { + name: jspb.Message.getFieldWithDefault(msg, 1, ""), + optionsList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f, + multipleanswers: jspb.Message.getBooleanFieldWithDefault(msg, 3, false) + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.messages.PollMessage} + */ +proto.messages.PollMessage.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.messages.PollMessage; + return proto.messages.PollMessage.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.messages.PollMessage} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.messages.PollMessage} + */ +proto.messages.PollMessage.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setName(value); + break; + case 2: + var value = /** @type {string} */ (reader.readString()); + msg.addOptions(value); + break; + case 3: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setMultipleanswers(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.messages.PollMessage.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.messages.PollMessage.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.messages.PollMessage} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.PollMessage.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getName(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getOptionsList(); + if (f.length > 0) { + writer.writeRepeatedString( + 2, + f + ); + } + f = message.getMultipleanswers(); + if (f) { + writer.writeBool( + 3, + f + ); + } +}; + + +/** + * optional string name = 1; + * @return {string} + */ +proto.messages.PollMessage.prototype.getName = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.messages.PollMessage} returns this + */ +proto.messages.PollMessage.prototype.setName = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * repeated string options = 2; + * @return {!Array} + */ +proto.messages.PollMessage.prototype.getOptionsList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 2)); +}; + + +/** + * @param {!Array} value + * @return {!proto.messages.PollMessage} returns this + */ +proto.messages.PollMessage.prototype.setOptionsList = function(value) { + return jspb.Message.setField(this, 2, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.messages.PollMessage} returns this + */ +proto.messages.PollMessage.prototype.addOptions = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 2, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.messages.PollMessage} returns this + */ +proto.messages.PollMessage.prototype.clearOptionsList = function() { + return this.setOptionsList([]); +}; + + +/** + * optional bool multipleAnswers = 3; + * @return {boolean} + */ +proto.messages.PollMessage.prototype.getMultipleanswers = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.messages.PollMessage} returns this + */ +proto.messages.PollMessage.prototype.setMultipleanswers = function(value) { + return jspb.Message.setProto3BooleanField(this, 3, value); +}; + + /** * @enum {number} */ diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index 9d9f8e7c..78f17edc 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -72,12 +72,12 @@ import { CheckNumberStatusQuery, EditMessageRequest, MessageContactVcardRequest, - MessageDestination, MessageFileRequest, MessageForwardRequest, MessageImageRequest, MessageLinkCustomPreviewRequest, MessageLocationRequest, + MessagePollRequest, MessageReactionRequest, MessageReplyRequest, MessageTextRequest, @@ -124,7 +124,11 @@ import { DeleteStatusRequest, TextStatus, } from '@waha/structures/status.dto'; -import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto'; +import { + EnginePayload, + PollVotePayload, + WAMessageAckBody, +} from '@waha/structures/webhooks.dto'; import { PaginatorInMemory } from '@waha/utils/Paginator'; import { sleep, waitUntil } from '@waha/utils/promiseTimeout'; import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent'; @@ -181,6 +185,8 @@ enum WhatsMeowEvent { LABEL_ASSOCIATION_CHAT = 'events.LabelAssociationChat', // Events EVENT_MESSAGE_RESPONSE = 'gows.EventMessageResponse', + // Polls + POLL_VOTE_EVENT = 'gows.PollVoteEvent', } const gRPCClientConfig = { @@ -472,7 +478,27 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { this.events2.get(WAHAEvents.LABEL_UPSERT).switch(labelUpsert$); this.events2.get(WAHAEvents.LABEL_DELETED).switch(labelDeleted$); - // Handle event message response + // + // Polls + // + const pollVoteEvent$ = all$.pipe( + onlyEvent(WhatsMeowEvent.POLL_VOTE_EVENT), + map(this.toPollVotePayload.bind(this)), + filter(Boolean), + ); + + // Split into successful and failed responses + const [pollVoteSuccess$, pollVoteFailed$] = partition( + pollVoteEvent$, + (payload: PollVotePayload) => !!payload.vote.selectedOptions, + ); + + this.events2.get(WAHAEvents.POLL_VOTE).switch(pollVoteSuccess$); + this.events2.get(WAHAEvents.POLL_VOTE_FAILED).switch(pollVoteFailed$); + + // + // Event Message + // const eventMessageResponse$ = all$.pipe( onlyEvent(WhatsMeowEvent.EVENT_MESSAGE_RESPONSE), map(this.toEventResponsePayload.bind(this)), @@ -490,6 +516,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { .get(WAHAEvents.EVENT_RESPONSE_FAILED) .switch(eventResponseFailed$); + // + // Labels + // // Handle label association events const labelAssociationEvents$ = all$.pipe( onlyEvent(WhatsMeowEvent.LABEL_ASSOCIATION_CHAT), @@ -695,6 +724,23 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { return this.messageResponse(jid, data); } + async sendPoll(request: MessagePollRequest) { + const jid = toJID(request.chatId); + const message = new messages.MessageRequest({ + jid: jid, + session: this.session, + replyTo: getMessageIdFromSerialized(request.reply_to), + poll: new messages.PollMessage({ + name: request.poll.name, + options: request.poll.options, + multipleAnswers: request.poll.multipleAnswers, + }), + }); + const response = await promisify(this.client.SendMessage)(message); + const data = response.toObject(); + return this.messageResponse(jid, data); + } + public async deleteMessage(chatId: string, messageId: string) { const jid = toJID(this.ensureSuffix(chatId)); const key = parseMessageIdSerialized(messageId); @@ -1796,6 +1842,32 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { }; } + private toPollVotePayload(event: any): PollVotePayload { + // Extract event creation message key from the message + const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey; + const key: WAMessageKey = { + remoteJid: creationKey.remoteJID, + fromMe: creationKey.fromMe, + id: creationKey.ID, + participant: creationKey.participant, + }; + const fromToParticipant = getFromToParticipant(event); + const pollCreationKey = getDestination(key); + return { + poll: pollCreationKey, + vote: { + id: buildMessageId(event), + from: toCusFormat(fromToParticipant.from), + fromMe: event.Info.IsFromMe, + to: toCusFormat(fromToParticipant.to), + participant: toCusFormat(fromToParticipant.participant), + selectedOptions: event.Votes, + timestamp: event.Message.pollUpdateMessage.senderTimestampMS, + }, + _data: event, + }; + } + private toEventResponsePayload(event: any): EventResponsePayload { const msg = this.toWAMessage(event); let response: EventResponse | null = null; diff --git a/src/structures/chatting.dto.ts b/src/structures/chatting.dto.ts index 424e1189..e467f2c1 100644 --- a/src/structures/chatting.dto.ts +++ b/src/structures/chatting.dto.ts @@ -420,6 +420,7 @@ export class MessageDestination { to: string; from: string; fromMe: boolean; + participant?: string; } export class MessageButtonReply extends ChatRequest { diff --git a/src/structures/webhooks.dto.ts b/src/structures/webhooks.dto.ts index 3df61fcb..fe551fa2 100644 --- a/src/structures/webhooks.dto.ts +++ b/src/structures/webhooks.dto.ts @@ -87,6 +87,7 @@ export class PollVote extends MessageDestination { export class PollVotePayload { vote: PollVote; poll: MessageDestination; + _data?: any; } export class WAMessageRevokedBody {