Skip to content

Commit 65a9c78

Browse files
committed
feat: typebot send buttons
1 parent fbccf2e commit 65a9c78

File tree

2 files changed

+125
-55
lines changed

2 files changed

+125
-55
lines changed

src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Lines changed: 45 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,8 @@ import P from 'pino';
139139
import qrcode, { QRCodeToDataURLOptions } from 'qrcode';
140140
import qrcodeTerminal from 'qrcode-terminal';
141141
import sharp from 'sharp';
142-
import { PassThrough } from 'stream';
142+
import { PassThrough, Readable } from 'stream';
143143
import { v4 } from 'uuid';
144-
import { Readable } from 'stream';
145144

146145
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
147146

@@ -372,7 +371,7 @@ export class BaileysStartupService extends ChannelStartupService {
372371
qrcodeTerminal.generate(qr, { small: true }, (qrcode) =>
373372
this.logger.log(
374373
`\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` +
375-
qrcode,
374+
qrcode,
376375
),
377376
);
378377

@@ -976,18 +975,18 @@ export class BaileysStartupService extends ChannelStartupService {
976975

977976
const messagesRepository = new Set(
978977
chatwootImport.getRepositoryMessagesCache(instance) ??
979-
(
980-
await this.prismaRepository.message.findMany({
981-
select: { key: true },
982-
where: { instanceId: this.instanceId },
983-
})
984-
).map((message) => {
985-
const key = message.key as {
986-
id: string;
987-
};
988-
989-
return key.id;
990-
}),
978+
(
979+
await this.prismaRepository.message.findMany({
980+
select: { key: true },
981+
where: { instanceId: this.instanceId },
982+
})
983+
).map((message) => {
984+
const key = message.key as {
985+
id: string;
986+
};
987+
988+
return key.id;
989+
}),
991990
);
992991

993992
if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
@@ -1138,7 +1137,7 @@ export class BaileysStartupService extends ChannelStartupService {
11381137
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
11391138
});
11401139

1141-
if (!!existingChat) {
1140+
if (existingChat) {
11421141
const chatToInsert = {
11431142
remoteJid: received.key.remoteJid,
11441143
instanceId: this.instanceId,
@@ -1474,7 +1473,7 @@ export class BaileysStartupService extends ChannelStartupService {
14741473
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
14751474
});
14761475

1477-
if (!!existingChat) {
1476+
if (existingChat) {
14781477
const chatToInsert = {
14791478
remoteJid: message.remoteJid,
14801479
instanceId: this.instanceId,
@@ -2507,9 +2506,7 @@ export class BaileysStartupService extends ChannelStartupService {
25072506

25082507
const prepareMedia = await prepareWAMessageMedia(
25092508
{
2510-
[type]: isURL(mediaMessage.media)
2511-
? { url: mediaMessage.media }
2512-
: Buffer.from(mediaMessage.media, 'base64'),
2509+
[type]: isURL(mediaMessage.media) ? { url: mediaMessage.media } : Buffer.from(mediaMessage.media, 'base64'),
25132510
} as any,
25142511
{ upload: this.client.waUploadToServer },
25152512
);
@@ -2564,7 +2561,7 @@ export class BaileysStartupService extends ChannelStartupService {
25642561
if (mediaMessage.mediatype === 'ptv') {
25652562
prepareMedia[mediaType] = prepareMedia[type + 'Message'];
25662563
mimetype = 'video/mp4';
2567-
2564+
25682565
if (!prepareMedia[mediaType]) {
25692566
throw new Error('Failed to prepare video message');
25702567
}
@@ -2588,7 +2585,6 @@ export class BaileysStartupService extends ChannelStartupService {
25882585

25892586
this.logger.verbose(`Video duration: ${duration} seconds`);
25902587
prepareMedia[mediaType].seconds = duration;
2591-
25922588
} catch (error) {
25932589
this.logger.error('Error getting video duration:');
25942590
this.logger.error(error);
@@ -2954,43 +2950,43 @@ export class BaileysStartupService extends ChannelStartupService {
29542950
currency: button.currency,
29552951
total_amount: {
29562952
value: 0,
2957-
offset: 100
2953+
offset: 100,
29582954
},
29592955
reference_id: this.generateRandomId(),
2960-
type: "physical-goods",
2956+
type: 'physical-goods',
29612957
order: {
2962-
status: "pending",
2958+
status: 'pending',
29632959
subtotal: {
29642960
value: 0,
2965-
offset: 100
2961+
offset: 100,
29662962
},
2967-
order_type: "ORDER",
2963+
order_type: 'ORDER',
29682964
items: [
29692965
{
2970-
name: "",
2966+
name: '',
29712967
amount: {
29722968
value: 0,
2973-
offset: 100
2969+
offset: 100,
29742970
},
29752971
quantity: 0,
29762972
sale_amount: {
29772973
value: 0,
2978-
offset: 100
2979-
}
2980-
}
2981-
]
2974+
offset: 100,
2975+
},
2976+
},
2977+
],
29822978
},
29832979
payment_settings: [
29842980
{
2985-
type: "pix_static_code",
2981+
type: 'pix_static_code',
29862982
pix_static_code: {
29872983
merchant_name: button.name,
29882984
key: button.key,
2989-
key_type: this.mapKeyType.get(button.keyType)
2990-
}
2991-
}
2985+
key_type: this.mapKeyType.get(button.keyType),
2986+
},
2987+
},
29922988
],
2993-
share_payment_status: false
2989+
share_payment_status: false,
29942990
}),
29952991
};
29962992

@@ -3018,11 +3014,11 @@ export class BaileysStartupService extends ChannelStartupService {
30183014
throw new BadRequestException('At least one button is required');
30193015
}
30203016

3021-
const hasReplyButtons = data.buttons.some(btn => btn.type === 'reply');
3022-
3023-
const hasPixButton = data.buttons.some(btn => btn.type === 'pix');
3024-
3025-
const hasOtherButtons = data.buttons.some(btn => btn.type !== 'reply' && btn.type !== 'pix');
3017+
const hasReplyButtons = data.buttons.some((btn) => btn.type === 'reply');
3018+
3019+
const hasPixButton = data.buttons.some((btn) => btn.type === 'pix');
3020+
3021+
const hasOtherButtons = data.buttons.some((btn) => btn.type !== 'reply' && btn.type !== 'pix');
30263022

30273023
if (hasReplyButtons) {
30283024
if (data.buttons.length > 3) {
@@ -3046,10 +3042,12 @@ export class BaileysStartupService extends ChannelStartupService {
30463042
message: {
30473043
interactiveMessage: {
30483044
nativeFlowMessage: {
3049-
buttons: [{
3050-
name: this.mapType.get('pix'),
3051-
buttonParamsJson: this.toJSONString(data.buttons[0]),
3052-
}],
3045+
buttons: [
3046+
{
3047+
name: this.mapType.get('pix'),
3048+
buttonParamsJson: this.toJSONString(data.buttons[0]),
3049+
},
3050+
],
30533051
messageParamsJson: JSON.stringify({
30543052
from: 'api',
30553053
templateId: v4(),

src/api/integrations/chatbot/typebot/services/typebot.service.ts

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -223,16 +223,88 @@ export class TypebotService {
223223

224224
formattedText = formattedText.replace(/\n$/, '');
225225

226-
await instance.textMessage(
227-
{
226+
if (formattedText.includes('[buttons]')) {
227+
const buttonJson = {
228228
number: remoteJid.split('@')[0],
229-
delay: settings?.delayMessage || 1000,
230-
text: formattedText,
231-
},
232-
false,
233-
);
229+
thumbnailUrl: undefined,
230+
title: '',
231+
description: '',
232+
footer: '',
233+
buttons: [],
234+
};
234235

235-
sendTelemetry('/message/sendText');
236+
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
237+
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
238+
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
239+
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
240+
241+
if (titleMatch) buttonJson.title = titleMatch[1].trim();
242+
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
243+
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
244+
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
245+
246+
const buttonTypes = {
247+
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
248+
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
249+
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
250+
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
251+
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
252+
};
253+
254+
for (const [type, pattern] of Object.entries(buttonTypes)) {
255+
let match;
256+
while ((match = pattern.exec(formattedText)) !== null) {
257+
const content = match[1].trim();
258+
const button: any = { type };
259+
260+
switch (type) {
261+
case 'pix':
262+
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
263+
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
264+
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
265+
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
266+
break;
267+
268+
case 'reply':
269+
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
270+
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
271+
break;
272+
273+
case 'copy':
274+
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
275+
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
276+
break;
277+
278+
case 'call':
279+
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
280+
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
281+
break;
282+
283+
case 'url':
284+
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
285+
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
286+
break;
287+
}
288+
289+
if (Object.keys(button).length > 1) {
290+
buttonJson.buttons.push(button);
291+
}
292+
}
293+
}
294+
295+
await instance.buttonMessage(buttonJson);
296+
} else {
297+
await instance.textMessage(
298+
{
299+
number: remoteJid.split('@')[0],
300+
delay: settings?.delayMessage || 1000,
301+
text: formattedText,
302+
},
303+
false,
304+
);
305+
306+
sendTelemetry('/message/sendText');
307+
}
236308
}
237309

238310
if (message.type === 'image') {

0 commit comments

Comments
 (0)