From 164706ff61da3b50055a5ad4310bdd7fbf918c58 Mon Sep 17 00:00:00 2001 From: Berg Pinheiro Date: Fri, 11 Sep 2026 00:32:00 -0300 Subject: [PATCH] feat(GOWS): implement forwardMessage (closes #2256, fixes #1439) --- src/core/engines/gows/grpc/gows.ts | 116 ++++++++++ src/core/engines/gows/grpc/gows_pb.js | 235 ++++++++++++++++++++- src/core/engines/gows/session.gows.core.ts | 46 +++- 3 files changed, 394 insertions(+), 3 deletions(-) diff --git a/src/core/engines/gows/grpc/gows.ts b/src/core/engines/gows/grpc/gows.ts index 15f55f22..195e9674 100644 --- a/src/core/engines/gows/grpc/gows.ts +++ b/src/core/engines/gows/grpc/gows.ts @@ -4331,6 +4331,96 @@ export namespace messages { return EventMessage.deserialize(bytes); } } + export class ForwardOptions extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + messageId?: string; + force?: boolean; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("messageId" in data && data.messageId != undefined) { + this.messageId = data.messageId; + } + if ("force" in data && data.force != undefined) { + this.force = data.force; + } + } + } + get messageId() { + return pb_1.Message.getFieldWithDefault(this, 1, "") as string; + } + set messageId(value: string) { + pb_1.Message.setField(this, 1, value); + } + get force() { + return pb_1.Message.getFieldWithDefault(this, 2, false) as boolean; + } + set force(value: boolean) { + pb_1.Message.setField(this, 2, value); + } + static fromObject(data: { + messageId?: string; + force?: boolean; + }): ForwardOptions { + const message = new ForwardOptions({}); + if (data.messageId != null) { + message.messageId = data.messageId; + } + if (data.force != null) { + message.force = data.force; + } + return message; + } + toObject() { + const data: { + messageId?: string; + force?: boolean; + } = {}; + if (this.messageId != null) { + data.messageId = this.messageId; + } + if (this.force != null) { + data.force = this.force; + } + 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.messageId.length) + writer.writeString(1, this.messageId); + if (this.force != false) + writer.writeBool(2, this.force); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): ForwardOptions { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new ForwardOptions(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 1: + message.messageId = reader.readString(); + break; + case 2: + message.force = reader.readBool(); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): ForwardOptions { + return ForwardOptions.deserialize(bytes); + } + } export class MessageRequest extends pb_1.Message { #one_of_decls: number[][] = []; constructor(data?: any[] | { @@ -4353,6 +4443,7 @@ export namespace messages { location?: Location; pollVote?: PollVoteMessage; mentions?: string[]; + forward?: ForwardOptions; }) { super(); pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [11, 13, 19], this.#one_of_decls); @@ -4414,6 +4505,9 @@ export namespace messages { if ("mentions" in data && data.mentions != undefined) { this.mentions = data.mentions; } + if ("forward" in data && data.forward != undefined) { + this.forward = data.forward; + } } } get session() { @@ -4560,6 +4654,15 @@ export namespace messages { set mentions(value: string[]) { pb_1.Message.setField(this, 19, value); } + get forward() { + return pb_1.Message.getWrapperField(this, ForwardOptions, 20) as ForwardOptions; + } + set forward(value: ForwardOptions) { + pb_1.Message.setWrapperField(this, 20, value); + } + get has_forward() { + return pb_1.Message.getField(this, 20) != null; + } static fromObject(data: { session?: ReturnType; jid?: string; @@ -4580,6 +4683,7 @@ export namespace messages { location?: ReturnType; pollVote?: ReturnType; mentions?: string[]; + forward?: ReturnType; }): MessageRequest { const message = new MessageRequest({}); if (data.session != null) { @@ -4639,6 +4743,9 @@ export namespace messages { if (data.mentions != null) { message.mentions = data.mentions; } + if (data.forward != null) { + message.forward = ForwardOptions.fromObject(data.forward); + } return message; } toObject() { @@ -4662,6 +4769,7 @@ export namespace messages { location?: ReturnType; pollVote?: ReturnType; mentions?: string[]; + forward?: ReturnType; } = {}; if (this.session != null) { data.session = this.session.toObject(); @@ -4720,6 +4828,9 @@ export namespace messages { if (this.mentions != null) { data.mentions = this.mentions; } + if (this.forward != null) { + data.forward = this.forward.toObject(); + } return data; } serialize(): Uint8Array; @@ -4764,6 +4875,8 @@ export namespace messages { writer.writeMessage(18, this.pollVote, () => this.pollVote.serialize(writer)); if (this.mentions.length) writer.writeRepeatedString(19, this.mentions); + if (this.has_forward) + writer.writeMessage(20, this.forward, () => this.forward.serialize(writer)); if (!w) return writer.getResultBuffer(); } @@ -4830,6 +4943,9 @@ export namespace messages { case 19: pb_1.Message.addToRepeatedField(message, 19, reader.readString()); break; + case 20: + reader.readMessage(message.forward, () => message.forward = ForwardOptions.deserialize(reader)); + break; default: reader.skipField(); } } diff --git a/src/core/engines/gows/grpc/gows_pb.js b/src/core/engines/gows/grpc/gows_pb.js index 3290f58e..80c205c3 100644 --- a/src/core/engines/gows/grpc/gows_pb.js +++ b/src/core/engines/gows/grpc/gows_pb.js @@ -42,6 +42,7 @@ goog.exportSymbol('proto.messages.EntityByIdRequest', null, global); goog.exportSymbol('proto.messages.EventJson', null, global); goog.exportSymbol('proto.messages.EventLocation', null, global); goog.exportSymbol('proto.messages.EventMessage', null, global); +goog.exportSymbol('proto.messages.ForwardOptions', null, global); goog.exportSymbol('proto.messages.GetChatsRequest', null, global); goog.exportSymbol('proto.messages.GetContactsRequest', null, global); goog.exportSymbol('proto.messages.GetLabelsRequest', null, global); @@ -965,6 +966,27 @@ if (goog.DEBUG && !COMPILED) { */ proto.messages.EventMessage.displayName = 'proto.messages.EventMessage'; } +/** + * 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.ForwardOptions = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.messages.ForwardOptions, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.messages.ForwardOptions.displayName = 'proto.messages.ForwardOptions'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -10048,6 +10070,166 @@ proto.messages.EventMessage.prototype.hasLocation = function() { + + +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.ForwardOptions.prototype.toObject = function(opt_includeInstance) { + return proto.messages.ForwardOptions.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.ForwardOptions} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.ForwardOptions.toObject = function(includeInstance, msg) { + var f, obj = { + messageid: jspb.Message.getFieldWithDefault(msg, 1, ""), + force: jspb.Message.getBooleanFieldWithDefault(msg, 2, 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.ForwardOptions} + */ +proto.messages.ForwardOptions.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.messages.ForwardOptions; + return proto.messages.ForwardOptions.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.messages.ForwardOptions} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.messages.ForwardOptions} + */ +proto.messages.ForwardOptions.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.setMessageid(value); + break; + case 2: + var value = /** @type {boolean} */ (reader.readBool()); + msg.setForce(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.messages.ForwardOptions.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.messages.ForwardOptions.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.ForwardOptions} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.messages.ForwardOptions.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getMessageid(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } + f = message.getForce(); + if (f) { + writer.writeBool( + 2, + f + ); + } +}; + + +/** + * optional string messageId = 1; + * @return {string} + */ +proto.messages.ForwardOptions.prototype.getMessageid = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.messages.ForwardOptions} returns this + */ +proto.messages.ForwardOptions.prototype.setMessageid = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + +/** + * optional bool force = 2; + * @return {boolean} + */ +proto.messages.ForwardOptions.prototype.getForce = function() { + return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 2, false)); +}; + + +/** + * @param {boolean} value + * @return {!proto.messages.ForwardOptions} returns this + */ +proto.messages.ForwardOptions.prototype.setForce = function(value) { + return jspb.Message.setProto3BooleanField(this, 2, value); +}; + + + /** * List of repeated fields within this message type. * @private {!Array} @@ -10105,7 +10287,8 @@ proto.messages.MessageRequest.toObject = function(includeInstance, msg) { list: (f = msg.getList()) && proto.messages.ListMessage.toObject(includeInstance, f), location: (f = msg.getLocation()) && proto.messages.Location.toObject(includeInstance, f), pollvote: (f = msg.getPollvote()) && proto.messages.PollVoteMessage.toObject(includeInstance, f), - mentionsList: (f = jspb.Message.getRepeatedField(msg, 19)) == null ? undefined : f + mentionsList: (f = jspb.Message.getRepeatedField(msg, 19)) == null ? undefined : f, + forward: (f = msg.getForward()) && proto.messages.ForwardOptions.toObject(includeInstance, f) }; if (includeInstance) { @@ -10229,6 +10412,11 @@ proto.messages.MessageRequest.deserializeBinaryFromReader = function(msg, reader var value = /** @type {string} */ (reader.readString()); msg.addMentions(value); break; + case 20: + var value = new proto.messages.ForwardOptions; + reader.readMessage(value,proto.messages.ForwardOptions.deserializeBinaryFromReader); + msg.setForward(value); + break; default: reader.skipField(); break; @@ -10402,6 +10590,14 @@ proto.messages.MessageRequest.serializeBinaryToWriter = function(message, writer f ); } + f = message.getForward(); + if (f != null) { + writer.writeMessage( + 20, + f, + proto.messages.ForwardOptions.serializeBinaryToWriter + ); + } }; @@ -10995,6 +11191,43 @@ proto.messages.MessageRequest.prototype.clearMentionsList = function() { }; +/** + * optional ForwardOptions forward = 20; + * @return {?proto.messages.ForwardOptions} + */ +proto.messages.MessageRequest.prototype.getForward = function() { + return /** @type{?proto.messages.ForwardOptions} */ ( + jspb.Message.getWrapperField(this, proto.messages.ForwardOptions, 20)); +}; + + +/** + * @param {?proto.messages.ForwardOptions|undefined} value + * @return {!proto.messages.MessageRequest} returns this +*/ +proto.messages.MessageRequest.prototype.setForward = function(value) { + return jspb.Message.setWrapperField(this, 20, value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.messages.MessageRequest} returns this + */ +proto.messages.MessageRequest.prototype.clearForward = function() { + return this.setForward(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.messages.MessageRequest.prototype.hasForward = function() { + return jspb.Message.getField(this, 20) != null; +}; + + diff --git a/src/core/engines/gows/session.gows.core.ts b/src/core/engines/gows/session.gows.core.ts index cca858f4..2c344167 100644 --- a/src/core/engines/gows/session.gows.core.ts +++ b/src/core/engines/gows/session.gows.core.ts @@ -1355,8 +1355,50 @@ export class WhatsappSessionGoWSCore extends WhatsappSession { return this.messageResponse(jid, data); } - forwardMessage(request: MessageForwardRequest): Promise { - throw new NotImplementedByEngineError(); + @Activity() + async forwardMessage(request: MessageForwardRequest): Promise { + // Only the id is needed - the chat is resolved separately below - so the + // short form is accepted too, as in editMessage and sendPollVote. + const key = parseMessageIdSerialized(request.messageId, true); + const jid = await this.hooks.wid.chat.promise( + request.chatId, + 'forwardMessage', + ); + const message = new messages.MessageRequest({ + id: request.id, + jid: jid, + session: this.session, + forward: new messages.ForwardOptions({ + messageId: key.id, + // NOWEB marks your own messages as forwarded as well, so keep the two + // engines telling the recipient the same thing. + force: true, + }), + }); + let response: messages.MessageResponse; + try { + response = await promisify(this.client.SendMessage)(message); + } catch (error) { + // Asking to forward a message that is not there is the caller's mistake, + // not a server fault - the other engines answer 422 for it too. + if (error?.code === grpc.status.NOT_FOUND) { + throw new UnprocessableEntityException( + `Message with id '${request.messageId}' not found`, + ); + } + // The engine refuses what it will not forward - a poll, a type with + // nowhere to carry the markers, a session with message storage off. That + // is an answer, not a failure, so it must not read as one. + if ( + error?.code === grpc.status.INVALID_ARGUMENT || + error?.code === grpc.status.FAILED_PRECONDITION + ) { + throw new UnprocessableEntityException(error.details ?? error.message); + } + throw error; + } + const data = response.toObject(); + return this.messageResponse(jid, data) as any; } private async sendMedia(type: messages.MediaType, request: any) {