Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 110 additions & 74 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3310,93 +3310,57 @@ export class BaileysStartupService extends ChannelStartupService {
['random', 'EVP'],
]);

public async buttonMessage(data: SendButtonsDto) {
if (data.buttons.length === 0) {
throw new BadRequestException('At least one button is required');
}
aqui?
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Outdated

const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply');
public async buttonMessage(data: SendButtonsDto) {
if (!data.buttons || data.buttons.length === 0) {
throw new BadRequestException('At least one button is required');
}

const hasPixButton = data.buttons.some((btn) => btn.type === 'pix');
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',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The new CTA detection and validation logic no longer guards against mixing PIX/reply with any future or unknown button types.

Previously, hasOtherButtons treated any non-reply/pix type as β€œother”, so reply/PIX could not be mixed with any other button type. Now hasCTAButtons only covers url/call/copy, so any new or existing types outside this list can be mixed with reply/PIX without validation catching it. If reply/PIX must remain exclusive with all other types, either keep a generic hasOtherButtons check alongside the CTA check, or treat any unsupported btn.type as β€œother” in the validation logic.

Suggested implementation:

  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',
  );
  // Treat any non-reply/non-pix button type as "other" to guard against future/unknown types
  const hasOtherButtons = data.buttons.some(
    (btn) => btn.type !== 'reply' && btn.type !== 'pix',
  );

  if ((hasReplyButtons || hasPixButton) && hasOtherButtons) {
    throw new BadRequestException(
      'Reply and PIX buttons cannot be combined with other button types',
    );
  }
  1. If there is existing validation later in buttonMessage that attempted to prevent mixing reply/pix with other types (e.g., relying on a prior hasOtherButtons or only on hasCTAButtons), remove or adjust it to avoid duplicate/conflicting checks, since this new hasOtherButtons guard now centralizes that rule.
  2. If hasCTAButtons is intended to drive any CTA-specific behavior (like building different payload structures), ensure that code uses the new hasCTAButtons variable defined here rather than recomputing or assuming specific types elsewhere.

);

const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix');
/* =========================
* REGRAS DE VALIDAÇÃO
* ========================= */

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
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 (hasCTAButtons || hasPixButton) {
throw new BadRequestException('Reply buttons cannot be mixed with CTA or PIX buttons');
}
}

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) };
});
// 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: {
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,
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(),
}),
},
},
},
Expand All @@ -3412,6 +3376,78 @@ export class BaileysStartupService extends ChannelStartupService {
});
}

// 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,
Expand Down