feat(messages): add multi-product message support
feat(messages): add multi-product message support
This commit is contained in:
@@ -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 ==========
|
||||
|
||||
/**
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
Reference in New Issue
Block a user