feat: typebot send buttons

This commit is contained in:
Davidson Gomes 2024-10-29 16:03:00 -03:00
parent fbccf2eb2a
commit 65a9c78d86
2 changed files with 124 additions and 54 deletions

View File

@ -139,9 +139,8 @@ import P from 'pino';
import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
import qrcodeTerminal from 'qrcode-terminal'; import qrcodeTerminal from 'qrcode-terminal';
import sharp from 'sharp'; import sharp from 'sharp';
import { PassThrough } from 'stream'; import { PassThrough, Readable } from 'stream';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { Readable } from 'stream';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
@ -372,7 +371,7 @@ export class BaileysStartupService extends ChannelStartupService {
qrcodeTerminal.generate(qr, { small: true }, (qrcode) => qrcodeTerminal.generate(qr, { small: true }, (qrcode) =>
this.logger.log( this.logger.log(
`\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` +
qrcode, qrcode,
), ),
); );
@ -976,18 +975,18 @@ export class BaileysStartupService extends ChannelStartupService {
const messagesRepository = new Set( const messagesRepository = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ?? chatwootImport.getRepositoryMessagesCache(instance) ??
( (
await this.prismaRepository.message.findMany({ await this.prismaRepository.message.findMany({
select: { key: true }, select: { key: true },
where: { instanceId: this.instanceId }, where: { instanceId: this.instanceId },
}) })
).map((message) => { ).map((message) => {
const key = message.key as { const key = message.key as {
id: string; id: string;
}; };
return key.id; return key.id;
}), }),
); );
if (chatwootImport.getRepositoryMessagesCache(instance) === null) { if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
@ -1138,7 +1137,7 @@ export class BaileysStartupService extends ChannelStartupService {
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid }, where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
}); });
if (!!existingChat) { if (existingChat) {
const chatToInsert = { const chatToInsert = {
remoteJid: received.key.remoteJid, remoteJid: received.key.remoteJid,
instanceId: this.instanceId, instanceId: this.instanceId,
@ -1474,7 +1473,7 @@ export class BaileysStartupService extends ChannelStartupService {
where: { instanceId: this.instanceId, remoteJid: message.remoteJid }, where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
}); });
if (!!existingChat) { if (existingChat) {
const chatToInsert = { const chatToInsert = {
remoteJid: message.remoteJid, remoteJid: message.remoteJid,
instanceId: this.instanceId, instanceId: this.instanceId,
@ -2507,9 +2506,7 @@ export class BaileysStartupService extends ChannelStartupService {
const prepareMedia = await prepareWAMessageMedia( const prepareMedia = await prepareWAMessageMedia(
{ {
[type]: isURL(mediaMessage.media) [type]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'),
? { url: mediaMessage.media }
: Buffer.from(mediaMessage.media, 'base64'),
} as any, } as any,
{ upload: this.client.waUploadToServer }, { upload: this.client.waUploadToServer },
); );
@ -2564,7 +2561,7 @@ export class BaileysStartupService extends ChannelStartupService {
if (mediaMessage.mediatype === 'ptv') { if (mediaMessage.mediatype === 'ptv') {
prepareMedia[mediaType] = prepareMedia[type + 'Message']; prepareMedia[mediaType] = prepareMedia[type + 'Message'];
mimetype = 'video/mp4'; mimetype = 'video/mp4';
if (!prepareMedia[mediaType]) { if (!prepareMedia[mediaType]) {
throw new Error('Failed to prepare video message'); throw new Error('Failed to prepare video message');
} }
@ -2588,7 +2585,6 @@ export class BaileysStartupService extends ChannelStartupService {
this.logger.verbose(`Video duration: ${duration} seconds`); this.logger.verbose(`Video duration: ${duration} seconds`);
prepareMedia[mediaType].seconds = duration; prepareMedia[mediaType].seconds = duration;
} catch (error) { } catch (error) {
this.logger.error('Error getting video duration:'); this.logger.error('Error getting video duration:');
this.logger.error(error); this.logger.error(error);
@ -2954,43 +2950,43 @@ export class BaileysStartupService extends ChannelStartupService {
currency: button.currency, currency: button.currency,
total_amount: { total_amount: {
value: 0, value: 0,
offset: 100 offset: 100,
}, },
reference_id: this.generateRandomId(), reference_id: this.generateRandomId(),
type: "physical-goods", type: 'physical-goods',
order: { order: {
status: "pending", status: 'pending',
subtotal: { subtotal: {
value: 0, value: 0,
offset: 100 offset: 100,
}, },
order_type: "ORDER", order_type: 'ORDER',
items: [ items: [
{ {
name: "", name: '',
amount: { amount: {
value: 0, value: 0,
offset: 100 offset: 100,
}, },
quantity: 0, quantity: 0,
sale_amount: { sale_amount: {
value: 0, value: 0,
offset: 100 offset: 100,
} },
} },
] ],
}, },
payment_settings: [ payment_settings: [
{ {
type: "pix_static_code", type: 'pix_static_code',
pix_static_code: { pix_static_code: {
merchant_name: button.name, merchant_name: button.name,
key: button.key, key: button.key,
key_type: this.mapKeyType.get(button.keyType) key_type: this.mapKeyType.get(button.keyType),
} },
} },
], ],
share_payment_status: false share_payment_status: false,
}), }),
}; };
@ -3018,11 +3014,11 @@ export class BaileysStartupService extends ChannelStartupService {
throw new BadRequestException('At least one button is required'); throw new BadRequestException('At least one button is required');
} }
const hasReplyButtons = data.buttons.some(btn => btn.type === 'reply'); const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply');
const hasPixButton = data.buttons.some(btn => btn.type === 'pix'); const hasPixButton = data.buttons.some((btn) => btn.type === 'pix');
const hasOtherButtons = data.buttons.some(btn => btn.type !== 'reply' && btn.type !== 'pix'); const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix');
if (hasReplyButtons) { if (hasReplyButtons) {
if (data.buttons.length > 3) { if (data.buttons.length > 3) {
@ -3046,10 +3042,12 @@ export class BaileysStartupService extends ChannelStartupService {
message: { message: {
interactiveMessage: { interactiveMessage: {
nativeFlowMessage: { nativeFlowMessage: {
buttons: [{ buttons: [
name: this.mapType.get('pix'), {
buttonParamsJson: this.toJSONString(data.buttons[0]), name: this.mapType.get('pix'),
}], buttonParamsJson: this.toJSONString(data.buttons[0]),
},
],
messageParamsJson: JSON.stringify({ messageParamsJson: JSON.stringify({
from: 'api', from: 'api',
templateId: v4(), templateId: v4(),

View File

@ -223,16 +223,88 @@ export class TypebotService {
formattedText = formattedText.replace(/\n$/, ''); formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage( if (formattedText.includes('[buttons]')) {
{ const buttonJson = {
number: remoteJid.split('@')[0], number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000, thumbnailUrl: undefined,
text: formattedText, title: '',
}, description: '',
false, footer: '',
); buttons: [],
};
sendTelemetry('/message/sendText'); const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
}
}
}
await instance.buttonMessage(buttonJson);
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
sendTelemetry('/message/sendText');
}
} }
if (message.type === 'image') { if (message.type === 'image') {