feat(list-message): add WhatsApp limit validation for list messages
Validates sections, rows, and string lengths before sending: - Max 10 sections, 10 rows/section, 30 total rows - Section title max 24 chars, row title max 24 chars - Row description max 72 chars, row ID max 200 chars - buttonText max 20 chars - At least 1 section and 1 row per section required Applied to all 3 list message paths: generateListMessage, generateListMessageLegacy, and inline sections handler. https://claude.ai/code/session_01SJdSHiUxtwzV8bb5dedodb
This commit is contained in:
+101
-3
@@ -695,13 +695,105 @@ export const generateCarouselMessage = async (
|
||||
* await sock.sendMessage(jid, msg)
|
||||
* ```
|
||||
*/
|
||||
export const generateListMessage = (options: ListMessageOptions): WAMessageContent => {
|
||||
const { buttonText, sections, text, title, footer } = options
|
||||
// ========== List Message Limits (WhatsApp enforced) ==========
|
||||
|
||||
const LIST_LIMITS = {
|
||||
MAX_SECTIONS: 10,
|
||||
MAX_ROWS_PER_SECTION: 10,
|
||||
MAX_TOTAL_ROWS: 30,
|
||||
MAX_SECTION_TITLE: 24,
|
||||
MAX_ROW_TITLE: 24,
|
||||
MAX_ROW_DESCRIPTION: 72,
|
||||
MAX_ROW_ID: 200,
|
||||
MAX_BUTTON_TEXT: 20,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Validates and sanitizes list message sections according to WhatsApp limits.
|
||||
* Truncates strings that exceed max length, throws on structural violations.
|
||||
*/
|
||||
const validateListSections = (
|
||||
sections: Array<{ title: string; rows: Array<{ id?: string; rowId?: string; title: string; description?: string }> }>,
|
||||
buttonText?: string
|
||||
): void => {
|
||||
if (!sections || sections.length === 0) {
|
||||
throw new Boom('At least one section is required', { statusCode: 400 })
|
||||
}
|
||||
|
||||
if (sections.length > LIST_LIMITS.MAX_SECTIONS) {
|
||||
throw new Boom(
|
||||
`Maximum ${LIST_LIMITS.MAX_SECTIONS} sections allowed, got ${sections.length}`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (buttonText && buttonText.length > LIST_LIMITS.MAX_BUTTON_TEXT) {
|
||||
throw new Boom(
|
||||
`buttonText max ${LIST_LIMITS.MAX_BUTTON_TEXT} characters, got ${buttonText.length}`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
let totalRows = 0
|
||||
for (const section of sections) {
|
||||
if (section.title && section.title.length > LIST_LIMITS.MAX_SECTION_TITLE) {
|
||||
throw new Boom(
|
||||
`Section title max ${LIST_LIMITS.MAX_SECTION_TITLE} characters, got ${section.title.length}: "${section.title}"`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (!section.rows || section.rows.length === 0) {
|
||||
throw new Boom('Each section must have at least one row', { statusCode: 400 })
|
||||
}
|
||||
|
||||
if (section.rows.length > LIST_LIMITS.MAX_ROWS_PER_SECTION) {
|
||||
throw new Boom(
|
||||
`Maximum ${LIST_LIMITS.MAX_ROWS_PER_SECTION} rows per section, got ${section.rows.length} in "${section.title}"`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
for (const row of section.rows) {
|
||||
const rowId = row.id || row.rowId || ''
|
||||
if (rowId.length > LIST_LIMITS.MAX_ROW_ID) {
|
||||
throw new Boom(
|
||||
`Row ID max ${LIST_LIMITS.MAX_ROW_ID} characters, got ${rowId.length}`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (row.title && row.title.length > LIST_LIMITS.MAX_ROW_TITLE) {
|
||||
throw new Boom(
|
||||
`Row title max ${LIST_LIMITS.MAX_ROW_TITLE} characters, got ${row.title.length}: "${row.title}"`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
if (row.description && row.description.length > LIST_LIMITS.MAX_ROW_DESCRIPTION) {
|
||||
throw new Boom(
|
||||
`Row description max ${LIST_LIMITS.MAX_ROW_DESCRIPTION} characters, got ${row.description.length}: "${row.description.substring(0, 30)}..."`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
totalRows += section.rows.length
|
||||
}
|
||||
|
||||
if (totalRows > LIST_LIMITS.MAX_TOTAL_ROWS) {
|
||||
throw new Boom(
|
||||
`Maximum ${LIST_LIMITS.MAX_TOTAL_ROWS} total rows allowed, got ${totalRows}`,
|
||||
{ statusCode: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const generateListMessage = (options: ListMessageOptions): WAMessageContent => {
|
||||
const { buttonText, sections, text, title, footer } = options
|
||||
|
||||
validateListSections(sections, buttonText)
|
||||
|
||||
// Build sections for single_select
|
||||
const formattedSections = sections.map(section => ({
|
||||
title: section.title,
|
||||
@@ -1014,6 +1106,8 @@ export const generateListMessageLegacy = (
|
||||
buttonText: string,
|
||||
footer?: string
|
||||
): WAMessageContent => {
|
||||
validateListSections(listInfo.sections, buttonText)
|
||||
|
||||
return {
|
||||
listMessage: WAProto.Message.ListMessage.fromObject({
|
||||
title,
|
||||
@@ -1158,7 +1252,11 @@ export const generateWAMessageContent = async (
|
||||
m.templateMessage = templateMessage
|
||||
options.logger?.warn('[EXPERIMENTAL] Sending templateMessage - this may not work and can cause bans')
|
||||
} else if (hasNonNullishProperty(message, 'sections')) {
|
||||
// Process list messages
|
||||
// Process list messages - validate limits
|
||||
validateListSections(
|
||||
(message as any).sections,
|
||||
(message as any).buttonText
|
||||
)
|
||||
const listMessage: proto.Message.IListMessage = {
|
||||
title: (message as any).title,
|
||||
description: (message as any).text,
|
||||
|
||||
Reference in New Issue
Block a user