From d15c434b4c9eaff6479e4734a1162651fb2ce0e1 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 26 Jan 2026 13:07:00 -0300 Subject: [PATCH 1/3] fix(baileys): interactive buttons via deviceSentMessage + CTA limits --- .../whatsapp/whatsapp.baileys.service.ts | 155 +++++++++--------- 1 file changed, 81 insertions(+), 74 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..58935c1a1 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3311,91 +3311,46 @@ export class BaileysStartupService extends ChannelStartupService { ]); public async buttonMessage(data: SendButtonsDto) { - if (data.buttons.length === 0) { - throw new BadRequestException('At least one button is required'); - } - - const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); - - const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + if (data.buttons.length === 0) { + throw new BadRequestException('At least one button is required'); + } - const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); + const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); + const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); - if (hasReplyButtons) { - if (data.buttons.length > 3) { - throw new BadRequestException('Maximum of 3 reply buttons allowed'); - } - if (hasOtherButtons) { - throw new BadRequestException('Reply buttons cannot be mixed with other button types'); - } + // Reply rules + if (hasReplyButtons) { + if (data.buttons.length > 3) { + throw new BadRequestException('Maximum of 3 reply buttons allowed'); } - - if (hasPixButton) { - if (data.buttons.length > 1) { - throw new BadRequestException('Only one PIX button is allowed'); - } - if (hasOtherButtons) { - throw new BadRequestException('PIX button cannot be mixed with other button types'); - } - - const message: proto.IMessage = { - viewOnceMessage: { - message: { - interactiveMessage: { - nativeFlowMessage: { - buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), - }, - }, - }, - }, - }; - - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); + if (hasOtherButtons) { + throw new BadRequestException('Reply buttons cannot be mixed with other button types'); } + } - const generate = await (async () => { - if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); - } - })(); + // CTA rules (url/call/copy) - WhatsApp limits to 2 CTAs + if (hasOtherButtons && !hasReplyButtons && !hasPixButton) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed (url/call/copy)'); + } + } - const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; - }); + // PIX rules + if (hasPixButton) { + if (data.buttons.length > 1) { + throw new BadRequestException('Only one PIX button is allowed'); + } + if (hasOtherButtons) { + throw new BadRequestException('PIX button cannot be mixed with other button types'); + } const message: proto.IMessage = { - viewOnceMessage: { + deviceSentMessage: { message: { interactiveMessage: { - body: { - text: (() => { - let t = '*' + data.title + '*'; - if (data?.description) { - t += '\n\n'; - t += data.description; - t += '\n'; - } - return t; - })(), - }, - footer: { text: data?.footer }, - header: (() => { - if (generate?.message?.imageMessage) { - return { - hasMediaAttachment: !!generate.message.imageMessage, - imageMessage: generate.message.imageMessage, - }; - } - })(), nativeFlowMessage: { - buttons: buttons, + buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), }, }, @@ -3412,6 +3367,58 @@ export class BaileysStartupService extends ChannelStartupService { }); } + const generate = await (async () => { + if (data?.thumbnailUrl) { + return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); + } + })(); + + const buttons = data.buttons.map((value) => { + return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; + }); + + const message: proto.IMessage = { + deviceSentMessage: { + message: { + interactiveMessage: { + body: { + text: (() => { + let t = '*' + data.title + '*'; + if (data?.description) { + t += '\n\n'; + t += data.description; + t += '\n'; + } + return t; + })(), + }, + footer: { text: data?.footer }, + header: (() => { + if (generate?.message?.imageMessage) { + return { + hasMediaAttachment: !!generate.message.imageMessage, + imageMessage: generate.message.imageMessage, + }; + } + })(), + nativeFlowMessage: { + buttons, + messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + }, + }, + }, + }, + }; + + return await this.sendMessageWithTyping(data.number, message, { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }); +} + public async locationMessage(data: SendLocationDto) { return await this.sendMessageWithTyping( data.number, From 08f8d055d4bdd2be807b13e3c9113fd1e3658f43 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 26 Jan 2026 13:19:50 -0300 Subject: [PATCH 2/3] fix(baileys): interactive buttons via deviceSentMessage + CTA limits --- .../whatsapp/whatsapp.baileys.service.ts | 111 +++++++++++------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 58935c1a1..a45bf348d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3310,48 +3310,57 @@ export class BaileysStartupService extends ChannelStartupService { ['random', 'EVP'], ]); - public async buttonMessage(data: SendButtonsDto) { - if (data.buttons.length === 0) { + aqui? + +public async buttonMessage(data: SendButtonsDto) { + if (!data.buttons || data.buttons.length === 0) { throw new BadRequestException('At least one button is required'); } const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); - const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix'); + const hasCTAButtons = data.buttons.some( + (btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy', + ); + + /* ========================= + * REGRAS DE VALIDAÇÃO + * ========================= */ - // Reply rules + // Reply if (hasReplyButtons) { if (data.buttons.length > 3) { throw new BadRequestException('Maximum of 3 reply buttons allowed'); } - if (hasOtherButtons) { - throw new BadRequestException('Reply buttons cannot be mixed with other button types'); - } - } - - // CTA rules (url/call/copy) - WhatsApp limits to 2 CTAs - if (hasOtherButtons && !hasReplyButtons && !hasPixButton) { - if (data.buttons.length > 2) { - throw new BadRequestException('Maximum of 2 CTA buttons allowed (url/call/copy)'); + if (hasCTAButtons || hasPixButton) { + throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); } } - // PIX rules + // PIX if (hasPixButton) { if (data.buttons.length > 1) { throw new BadRequestException('Only one PIX button is allowed'); } - if (hasOtherButtons) { + if (hasReplyButtons || hasCTAButtons) { throw new BadRequestException('PIX button cannot be mixed with other button types'); } const message: proto.IMessage = { - deviceSentMessage: { + viewOnceMessage: { message: { interactiveMessage: { nativeFlowMessage: { - buttons: [{ name: this.mapType.get('pix'), buttonParamsJson: this.toJSONString(data.buttons[0]) }], - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + buttons: [ + { + name: this.mapType.get('pix'), + buttonParamsJson: this.toJSONString(data.buttons[0]), + }, + ], + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, @@ -3367,43 +3376,63 @@ export class BaileysStartupService extends ChannelStartupService { }); } - const generate = await (async () => { - if (data?.thumbnailUrl) { - return await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }); + // CTA (url / call / copy) + if (hasCTAButtons) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed'); + } + if (hasReplyButtons) { + throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); } - })(); + } - const buttons = data.buttons.map((value) => { - return { name: this.mapType.get(value.type), buttonParamsJson: this.toJSONString(value) }; - }); + /* ========================= + * HEADER (opcional) + * ========================= */ + + const generatedMedia = data?.thumbnailUrl + ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) + : null; + + /* ========================= + * BOTÕES + * ========================= */ + + const buttons = data.buttons.map((btn) => ({ + name: this.mapType.get(btn.type), + buttonParamsJson: this.toJSONString(btn), + })); + + /* ========================= + * MENSAGEM FINAL + * ========================= */ const message: proto.IMessage = { - deviceSentMessage: { + viewOnceMessage: { message: { interactiveMessage: { body: { text: (() => { - let t = '*' + data.title + '*'; + let text = `*${data.title}*`; if (data?.description) { - t += '\n\n'; - t += data.description; - t += '\n'; + text += `\n\n${data.description}`; } - return t; + return text; })(), }, - footer: { text: data?.footer }, - header: (() => { - if (generate?.message?.imageMessage) { - return { - hasMediaAttachment: !!generate.message.imageMessage, - imageMessage: generate.message.imageMessage, - }; - } - })(), + footer: data?.footer ? { text: data.footer } : undefined, + header: generatedMedia?.message?.imageMessage + ? { + hasMediaAttachment: true, + imageMessage: generatedMedia.message.imageMessage, + } + : undefined, nativeFlowMessage: { buttons, - messageParamsJson: JSON.stringify({ from: 'api', templateId: v4() }), + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), }, }, }, From a4f8e95d1829ea1a2bcd7dc3c8002265b6ac0ed3 Mon Sep 17 00:00:00 2001 From: Bruno Fernandes Date: Mon, 26 Jan 2026 13:55:41 -0300 Subject: [PATCH 3/3] chore(lint): fix formatting and remove stray text --- .../whatsapp/whatsapp.baileys.service.ts | 208 +++++++++--------- 1 file changed, 102 insertions(+), 106 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index a45bf348d..f18795c54 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -3310,53 +3310,121 @@ export class BaileysStartupService extends ChannelStartupService { ['random', 'EVP'], ]); - aqui? - -public async buttonMessage(data: SendButtonsDto) { - if (!data.buttons || data.buttons.length === 0) { - throw new BadRequestException('At least one button is required'); - } + public async buttonMessage(data: SendButtonsDto) { + if (!data.buttons || data.buttons.length === 0) { + throw new BadRequestException('At least one button is required'); + } - const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); - const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); - const hasCTAButtons = data.buttons.some( - (btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy', - ); + const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply'); + const hasPixButton = data.buttons.some((btn) => btn.type === 'pix'); + const hasCTAButtons = data.buttons.some((btn) => btn.type === 'url' || btn.type === 'call' || btn.type === 'copy'); - /* ========================= - * REGRAS DE VALIDAÇÃO - * ========================= */ + /* ========================= + * REGRAS DE VALIDAÇÃO + * ========================= */ - // Reply - if (hasReplyButtons) { - if (data.buttons.length > 3) { - throw new BadRequestException('Maximum of 3 reply buttons allowed'); - } - if (hasCTAButtons || hasPixButton) { - throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); + // Reply + if (hasReplyButtons) { + if (data.buttons.length > 3) { + throw new BadRequestException('Maximum of 3 reply buttons allowed'); + } + if (hasCTAButtons || hasPixButton) { + throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons'); + } } - } - // PIX - if (hasPixButton) { - if (data.buttons.length > 1) { - throw new BadRequestException('Only one PIX button is allowed'); + // PIX + if (hasPixButton) { + if (data.buttons.length > 1) { + throw new BadRequestException('Only one PIX button is allowed'); + } + if (hasReplyButtons || hasCTAButtons) { + throw new BadRequestException('PIX button cannot be mixed with other button types'); + } + + const message: proto.IMessage = { + viewOnceMessage: { + message: { + interactiveMessage: { + nativeFlowMessage: { + buttons: [ + { + name: this.mapType.get('pix'), + buttonParamsJson: this.toJSONString(data.buttons[0]), + }, + ], + messageParamsJson: JSON.stringify({ + from: 'api', + templateId: v4(), + }), + }, + }, + }, + }, + }; + + return await this.sendMessageWithTyping(data.number, message, { + delay: data?.delay, + presence: 'composing', + quoted: data?.quoted, + mentionsEveryOne: data?.mentionsEveryOne, + mentioned: data?.mentioned, + }); } - if (hasReplyButtons || hasCTAButtons) { - throw new BadRequestException('PIX button cannot be mixed with other button types'); + + // CTA (url / call / copy) + if (hasCTAButtons) { + if (data.buttons.length > 2) { + throw new BadRequestException('Maximum of 2 CTA buttons allowed'); + } + if (hasReplyButtons) { + throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); + } } + /* ========================= + * HEADER (opcional) + * ========================= */ + + const generatedMedia = data?.thumbnailUrl + ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) + : null; + + /* ========================= + * BOTÕES + * ========================= */ + + const buttons = data.buttons.map((btn) => ({ + name: this.mapType.get(btn.type), + buttonParamsJson: this.toJSONString(btn), + })); + + /* ========================= + * MENSAGEM FINAL + * ========================= */ + const message: proto.IMessage = { viewOnceMessage: { message: { interactiveMessage: { + body: { + text: (() => { + let text = `*${data.title}*`; + if (data?.description) { + text += `\n\n${data.description}`; + } + return text; + })(), + }, + footer: data?.footer ? { text: data.footer } : undefined, + header: generatedMedia?.message?.imageMessage + ? { + hasMediaAttachment: true, + imageMessage: generatedMedia.message.imageMessage, + } + : undefined, nativeFlowMessage: { - buttons: [ - { - name: this.mapType.get('pix'), - buttonParamsJson: this.toJSONString(data.buttons[0]), - }, - ], + buttons, messageParamsJson: JSON.stringify({ from: 'api', templateId: v4(), @@ -3376,78 +3444,6 @@ public async buttonMessage(data: SendButtonsDto) { }); } - // CTA (url / call / copy) - if (hasCTAButtons) { - if (data.buttons.length > 2) { - throw new BadRequestException('Maximum of 2 CTA buttons allowed'); - } - if (hasReplyButtons) { - throw new BadRequestException('CTA buttons cannot be mixed with reply buttons'); - } - } - - /* ========================= - * HEADER (opcional) - * ========================= */ - - const generatedMedia = data?.thumbnailUrl - ? await this.prepareMediaMessage({ mediatype: 'image', media: data.thumbnailUrl }) - : null; - - /* ========================= - * BOTÕES - * ========================= */ - - const buttons = data.buttons.map((btn) => ({ - name: this.mapType.get(btn.type), - buttonParamsJson: this.toJSONString(btn), - })); - - /* ========================= - * MENSAGEM FINAL - * ========================= */ - - const message: proto.IMessage = { - viewOnceMessage: { - message: { - interactiveMessage: { - body: { - text: (() => { - let text = `*${data.title}*`; - if (data?.description) { - text += `\n\n${data.description}`; - } - return text; - })(), - }, - footer: data?.footer ? { text: data.footer } : undefined, - header: generatedMedia?.message?.imageMessage - ? { - hasMediaAttachment: true, - imageMessage: generatedMedia.message.imageMessage, - } - : undefined, - nativeFlowMessage: { - buttons, - messageParamsJson: JSON.stringify({ - from: 'api', - templateId: v4(), - }), - }, - }, - }, - }, - }; - - return await this.sendMessageWithTyping(data.number, message, { - delay: data?.delay, - presence: 'composing', - quoted: data?.quoted, - mentionsEveryOne: data?.mentionsEveryOne, - mentioned: data?.mentioned, - }); -} - public async locationMessage(data: SendLocationDto) { return await this.sendMessageWithTyping( data.number,