From 15a4cc29622e872774795429d9746195c1d6d3a6 Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Mon, 26 Jan 2026 02:02:54 -0300 Subject: [PATCH] feat(messages): add multi-product message support Add generateProductListMessage function for sending multiple products from the WhatsApp Business catalog in a single message. Features: - ProductListMessageOptions type with full validation - Support for product sections (categories) - Optional header image from catalog - Integration with sendMessage via 'productList' property - Maximum 30 products limit per WhatsApp specs Usage: ```typescript const msg = generateProductListMessage({ title: 'Our Best Sellers', description: 'Check out our products!', buttonText: 'View Products', businessOwnerJid: '5511999999999@s.whatsapp.net', productSections: [ { title: 'Electronics', products: [{ productId: 'prod_001' }] } ] }) await sock.sendMessage(jid, msg) ``` Note: Requires WhatsApp Business account with catalog configured. Does NOT require Meta Business Manager integration. Co-Authored-By: Claude Opus 4.5 --- src/Types/Message.ts | 81 +++++++++++++++++++++++++ src/Utils/messages.ts | 135 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+) diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 7e726cbc..219c06ed 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -437,6 +437,87 @@ export type Carouselable = { } } +// ========== Product List Message Types ========== + +/** + * Product reference in a product list + * Uses the product ID from the WhatsApp Business catalog + */ +export type ProductItem = { + /** Product ID from the catalog */ + productId: string +} + +/** + * Section containing products in a product list message + */ +export type ProductSection = { + /** Section title */ + title: string + /** Products in this section */ + products: ProductItem[] +} + +/** + * Header image configuration for product list + * Can reference a product's image from the catalog + */ +export type ProductListHeaderImage = { + /** Product ID whose image to use as header */ + productId: string + /** Optional JPEG thumbnail */ + jpegThumbnail?: Buffer +} + +/** + * Options for generating a product list message (multi-product) + * Allows sending multiple products from the catalog in a single message + * + * @example + * ```typescript + * const msg = generateProductListMessage({ + * title: 'Our Best Sellers', + * description: 'Check out our most popular products!', + * buttonText: 'View Products', + * footerText: 'Tap to browse', + * businessOwnerJid: '5511999999999@s.whatsapp.net', + * productSections: [ + * { + * title: 'Electronics', + * products: [ + * { productId: 'prod_001' }, + * { productId: 'prod_002' } + * ] + * }, + * { + * title: 'Accessories', + * products: [ + * { productId: 'prod_003' } + * ] + * } + * ], + * headerImage: { productId: 'prod_001' } + * }) + * await sock.sendMessage(jid, msg) + * ``` + */ +export type ProductListMessageOptions = { + /** Message title */ + title: string + /** Message description/body text */ + description: string + /** Button text to open the product list */ + buttonText: string + /** Footer text (optional) */ + footerText?: string + /** Business owner JID (the catalog owner) */ + businessOwnerJid: string + /** Sections with products */ + productSections: ProductSection[] + /** Header image configuration (optional) */ + headerImage?: ProductListHeaderImage +} + // ========== Album Message Types ========== /** diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index d8b64a34..36aff81f 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -25,6 +25,7 @@ import type { MessageWithContextInfo, NativeButton, NativeFlowButton, + ProductListMessageOptions, WAMediaUpload, WAMessage, WAMessageContent, @@ -748,6 +749,124 @@ export const generateListMessage = (options: ListMessageOptions): WAMessageConte } } +/** + * Generates a product list message (multi-product) from the WhatsApp Business catalog + * Allows sending multiple products organized in sections + * + * Note: Requires a WhatsApp Business account with a configured catalog. + * Does NOT require Meta Business Manager integration. + * + * @example + * ```typescript + * const msg = generateProductListMessage({ + * title: 'Our Best Sellers', + * description: 'Check out our most popular products!', + * buttonText: 'View Products', + * footerText: 'Tap to browse our catalog', + * businessOwnerJid: '5511999999999@s.whatsapp.net', + * productSections: [ + * { + * title: 'Electronics', + * products: [ + * { productId: 'product_001' }, + * { productId: 'product_002' } + * ] + * }, + * { + * title: 'Accessories', + * products: [ + * { productId: 'product_003' }, + * { productId: 'product_004' } + * ] + * } + * ], + * headerImage: { productId: 'product_001' } + * }) + * await sock.sendMessage(jid, msg) + * ``` + */ +export const generateProductListMessage = (options: ProductListMessageOptions): WAMessageContent => { + const { + title, + description, + buttonText, + footerText, + businessOwnerJid, + productSections, + headerImage + } = options + + // Validation + if (!title || title.trim().length === 0) { + throw new Boom('Product list title is required', { statusCode: 400 }) + } + + if (!description || description.trim().length === 0) { + throw new Boom('Product list description is required', { statusCode: 400 }) + } + + if (!buttonText || buttonText.trim().length === 0) { + throw new Boom('Product list buttonText is required', { statusCode: 400 }) + } + + if (!businessOwnerJid || businessOwnerJid.trim().length === 0) { + throw new Boom('businessOwnerJid is required (catalog owner JID)', { statusCode: 400 }) + } + + if (!productSections || productSections.length === 0) { + throw new Boom('At least one product section is required', { statusCode: 400 }) + } + + // Validate sections have products + for (const section of productSections) { + if (!section.products || section.products.length === 0) { + throw new Boom(`Section "${section.title}" must have at least one product`, { statusCode: 400 }) + } + } + + // Count total products (max 30 per WhatsApp limits) + const totalProducts = productSections.reduce((sum, section) => sum + section.products.length, 0) + if (totalProducts > 30) { + throw new Boom(`Maximum 30 products allowed, got ${totalProducts}`, { statusCode: 400 }) + } + + // Build product sections for the protocol + const formattedSections = productSections.map(section => ({ + title: section.title, + products: section.products.map(product => ({ + productId: product.productId + })) + })) + + // Build product list info + const productListInfo: proto.Message.ListMessage.IProductListInfo = { + productSections: formattedSections, + businessOwnerJid: jidNormalizedUser(businessOwnerJid) + } + + // Add header image if provided + if (headerImage) { + productListInfo.headerImage = { + productId: headerImage.productId, + jpegThumbnail: headerImage.jpegThumbnail + } + } + + // Create list message with PRODUCT_LIST type + const listMessage: proto.Message.IListMessage = { + title, + description, + buttonText, + listType: proto.Message.ListMessage.ListType.PRODUCT_LIST, + productListInfo, + footerText: footerText || undefined + } + + return { + listMessage: WAProto.Message.ListMessage.fromObject(listMessage) + } +} + // ========== Legacy Message Functions ========== /** @@ -868,6 +987,22 @@ export const generateWAMessageContent = async ( m.viewOnceMessage = generated.viewOnceMessage options.logger?.info('Sending listMessage with viewOnceMessage wrapper') } + // Check for productList (multi-product message from catalog) + else if (hasNonNullishProperty(message, 'productList')) { + const productMsg = message as any + const productListOptions: ProductListMessageOptions = { + title: productMsg.productList.title || productMsg.title || '', + description: productMsg.productList.description || productMsg.text || '', + buttonText: productMsg.productList.buttonText, + footerText: productMsg.productList.footerText || productMsg.footer, + businessOwnerJid: productMsg.productList.businessOwnerJid, + productSections: productMsg.productList.sections, + headerImage: productMsg.productList.headerImage + } + const generated = generateProductListMessage(productListOptions) + m.listMessage = generated.listMessage + options.logger?.info('Sending productListMessage (multi-product from catalog)') + } // ⚠️ EXPERIMENTAL: Check for interactive messages FIRST (buttons, lists, templates) // These use the older API which may not work reliably else if (hasNonNullishProperty(message, 'text') && hasNonNullishProperty(message, 'buttons')) {