diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 484c9da2..d9d65d56 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -2020,12 +2020,16 @@ export const makeMessagesSocket = (config: SocketConfig) => { const userJid = authState.creds.me!.id - // Special path for carousel: call relayMessage directly - // This bypasses generateWAMessageFromContent (which adds contextInfo.expiration - // and calls WAProto.Message.create that may corrupt nested carousel structures) - // Matches the working implementation's approach: generateWAMessageContent → relayMessage + // Special path for carousel: call relayMessage directly with plain JS object + // This matches Pastorini's working approach: + // 1. Build message as plain JS object (not proto instance) + // 2. Pass directly to relayMessage (no WAProto.Message.fromObject/create) + // 3. protobuf encode() handles plain objects correctly during serialization + // Going through fromObject() can corrupt nested carousel InteractiveMessage oneOf fields if (typeof content === 'object' && 'nativeCarousel' in content && (content as any).nativeCarousel) { const messageId = generateMessageIDV2(sock.user?.id) + // generateWAMessageContent returns a plain JS object for carousel + // (not a proto instance) to preserve nested structure integrity const msgContent = await generateWAMessageContent(content, { logger, upload: waUploadToServer, @@ -2034,6 +2038,17 @@ export const makeMessagesSocket = (config: SocketConfig) => { jid, }) + // Pass the plain JS object directly to relayMessage + // This avoids proto.WebMessageInfo.fromObject() which would call + // Message.fromObject() on the nested message, potentially corrupting it + await relayMessage(jid, msgContent, { + messageId, + useCachedGroupMetadata: options.useCachedGroupMetadata, + additionalAttributes: {}, + statusJidList: options.statusJidList, + }) + + // Build WebMessageInfo only for event emission and return value const timestamp = unixTimestampSeconds() const fullMsg = proto.WebMessageInfo.fromObject({ key: { @@ -2048,13 +2063,6 @@ export const makeMessagesSocket = (config: SocketConfig) => { status: proto.WebMessageInfo.Status.PENDING }) as WAMessage - await relayMessage(jid, fullMsg.message!, { - messageId: fullMsg.key.id!, - useCachedGroupMetadata: options.useCachedGroupMetadata, - additionalAttributes: {}, - statusJidList: options.statusJidList, - }) - if (config.emitOwnEvents) { process.nextTick(async () => { const mutexKey = fullMsg.key.remoteJid || fullMsg.key.id || 'unknown' diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index 27507070..910992f2 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -641,8 +641,10 @@ export const generateCarouselMessage = async ( })) // Build the interactive message with carousel + // Root header MUST always be present for carousel messages (WhatsApp MD requirement) + // Missing root header causes error 479 on some devices const interactiveMessage: proto.Message.IInteractiveMessage = { - header: text ? { title: text } : undefined, + header: { title: text || '', hasMediaAttachment: false }, body: { text: text || '' }, footer: footer ? { text: footer } : undefined, carouselMessage: { @@ -1156,12 +1158,13 @@ export const generateWAMessageContent = async ( // Pass options for media processing if cards have images/videos const generated = await generateCarouselMessage(carouselOptions, options) m.viewOnceMessage = generated.viewOnceMessage - options.logger?.info('Sending carousel with viewOnceMessage wrapper + fromObject encoding') - // Return early with fromObject for proper deep conversion of nested carousel structure - // fromObject recursively converts nested plain objects to protobuf message instances - // whereas create() does shallow property copy which may not properly encode deep structures - // This matches the working implementation's encoding approach - return WAProto.Message.fromObject(m) + options.logger?.info('Sending carousel with viewOnceMessage wrapper (plain object, no proto conversion)') + // Return the plain JS object directly WITHOUT calling WAProto.Message.fromObject() + // This matches Pastorini's working approach where plain objects are passed directly + // to relayMessage. The fromObject() conversion can corrupt nested carousel structures + // by incorrectly handling oneOf fields in deeply nested InteractiveMessage cards. + // The protobuf encode() method handles plain objects correctly when serializing. + return m as WAMessageContent } // Check for nativeList else if (hasNonNullishProperty(message, 'nativeList')) {