feat(messages): add multi-product message support

feat(messages): add multi-product message support
This commit is contained in:
Renato Alcara
2026-01-26 02:55:57 -03:00
committed by GitHub
2 changed files with 232 additions and 0 deletions
+81
View File
@@ -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 ==========
/**
+151
View File
@@ -25,6 +25,7 @@ import type {
MessageWithContextInfo,
NativeButton,
NativeFlowButton,
ProductListMessageOptions,
WAMediaUpload,
WAMessage,
WAMessageContent,
@@ -748,6 +749,140 @@ 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 valid titles and products
for (const section of productSections) {
// Validate section title
if (!section.title || typeof section.title !== 'string' || section.title.trim().length === 0) {
throw new Boom('Each section must have a non-empty title', { statusCode: 400 })
}
// Validate section has products
if (!section.products || section.products.length === 0) {
throw new Boom(`Section "${section.title}" must have at least one product`, { statusCode: 400 })
}
// Validate each product has a valid productId
for (const product of section.products) {
if (!product || typeof product.productId !== 'string' || product.productId.trim().length === 0) {
throw new Boom(`Each product in section "${section.title}" must have a non-empty productId`, { 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 (with validation)
if (headerImage) {
if (!headerImage.productId || typeof headerImage.productId !== 'string' || headerImage.productId.trim().length === 0) {
throw new Boom('headerImage.productId must be a non-empty string', { statusCode: 400 })
}
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 +1003,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,
description: productMsg.productList.description,
buttonText: productMsg.productList.buttonText,
footerText: productMsg.productList.footerText,
businessOwnerJid: productMsg.productList.businessOwnerJid,
productSections: productMsg.productList.productSections,
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')) {