Skip to content

Commit 1787238

Browse files
committed
feat: send list and buttons
1 parent c4bcd1f commit 1787238

File tree

5 files changed

+193
-28
lines changed

5 files changed

+193
-28
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
### Features
44

55
* Fake Call function
6+
* Send List with Baileys
7+
* Send Buttons with Baileys
68
* Added unreadMessages to chats
79
* Pusher event integration
810
* Add support for splitMessages and timePerChar in Integrations
@@ -12,7 +14,11 @@
1214
* Fixed prefilledVariables in startTypebot
1315
* Fix duplicate file upload
1416
* Mark as read from me and groups
15-
* fetch chats query
17+
* Fetch chats query
18+
* Ads messages in chatwoot
19+
* Add indexes to improve performance in Evolution
20+
* Add logical or permanent message deletion based on env config
21+
* Add support for fetching multiple instances by key
1622

1723
# 2.1.2 (2024-10-06 10:09)
1824

src/api/dto/sendMessage.dto.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,22 @@ export class SendAudioDto extends Metadata {
9090
audio: string;
9191
}
9292

93-
class Button {
94-
text: string;
95-
id: string;
93+
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
94+
95+
export class Button {
96+
type: TypeButton;
97+
displayText: string;
98+
id?: string;
99+
url?: string;
100+
copyCode?: string;
101+
phoneNumber?: string;
96102
}
97-
export class SendButtonDto extends Metadata {
103+
104+
export class SendButtonsDto extends Metadata {
105+
thumbnailUrl?: string;
98106
title: string;
99-
description: string;
100-
footerText?: string;
107+
description?: string;
108+
footer?: string;
101109
buttons: Button[];
102110
}
103111

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

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,14 @@ import {
3131
import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
3232
import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
3333
import {
34+
Button,
3435
ContactMessage,
3536
MediaMessage,
3637
Options,
3738
SendAudioDto,
39+
SendButtonsDto,
3840
SendContactDto,
41+
SendListDto,
3942
SendLocationDto,
4043
SendMediaDto,
4144
SendPollDto,
@@ -44,6 +47,7 @@ import {
4447
SendStickerDto,
4548
SendTextDto,
4649
StatusMessage,
50+
TypeButton,
4751
} from '@api/dto/sendMessage.dto';
4852
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
4953
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
@@ -117,7 +121,7 @@ import makeWASocket, {
117121
import { Label } from 'baileys/lib/Types/Label';
118122
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
119123
import { spawn } from 'child_process';
120-
import { isBase64, isURL } from 'class-validator';
124+
import { isArray, isBase64, isURL } from 'class-validator';
121125
import { randomBytes } from 'crypto';
122126
import EventEmitter2 from 'eventemitter2';
123127
import ffmpeg from 'fluent-ffmpeg';
@@ -582,6 +586,23 @@ export class BaileysStartupService extends ChannelStartupService {
582586
cachedGroupMetadata: this.getGroupMetadataCache,
583587
userDevicesCache: this.userDevicesCache,
584588
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
589+
patchMessageBeforeSending(message) {
590+
if (
591+
message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST
592+
) {
593+
message = JSON.parse(JSON.stringify(message));
594+
595+
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
596+
}
597+
598+
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
599+
message = JSON.parse(JSON.stringify(message));
600+
601+
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
602+
}
603+
604+
return message;
605+
},
585606
};
586607

587608
this.endSession = false;
@@ -1768,6 +1789,28 @@ export class BaileysStartupService extends ChannelStartupService {
17681789
if (messageId) option.messageId = messageId;
17691790
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
17701791

1792+
if (message['viewOnceMessage']) {
1793+
const m = generateWAMessageFromContent(sender, message, {
1794+
timestamp: new Date(),
1795+
userJid: this.instance.wuid,
1796+
messageId,
1797+
quoted,
1798+
});
1799+
const id = await this.client.relayMessage(sender, message, { messageId });
1800+
m.key = {
1801+
id: id,
1802+
remoteJid: sender,
1803+
participant: isJidUser(sender) ? sender : undefined,
1804+
fromMe: true,
1805+
};
1806+
for (const [key, value] of Object.entries(m)) {
1807+
if (!value || (isArray(value) && value.length) === 0) {
1808+
delete m[key];
1809+
}
1810+
}
1811+
return m;
1812+
}
1813+
17711814
if (
17721815
!message['audio'] &&
17731816
!message['poll'] &&
@@ -2684,8 +2727,95 @@ export class BaileysStartupService extends ChannelStartupService {
26842727
);
26852728
}
26862729

2687-
public async buttonMessage() {
2688-
throw new BadRequestException('Method not available on WhatsApp Baileys');
2730+
private toJSONString(button: Button): string {
2731+
const toString = (obj: any) => JSON.stringify(obj);
2732+
2733+
const json = {
2734+
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
2735+
reply: () => toString({ display_text: button.displayText, id: button.id }),
2736+
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
2737+
url: () =>
2738+
toString({
2739+
display_text: button.displayText,
2740+
url: button.url,
2741+
merchant_url: button.url,
2742+
}),
2743+
};
2744+
2745+
return json[button.type]?.() || '';
2746+
}
2747+
2748+
private readonly mapType = new Map<TypeButton, string>([
2749+
['reply', 'quick_reply'],
2750+
['copy', 'cta_copy'],
2751+
['url', 'cta_url'],
2752+
['call', 'cta_call'],
2753+
]);
2754+
2755+
public async buttonMessage(data: SendButtonsDto) {
2756+
const generate = await (async () => {
2757+
if (data?.thumbnailUrl) {
2758+
return await this.prepareMediaMessage({
2759+
mediatype: 'image',
2760+
media: data.thumbnailUrl,
2761+
});
2762+
}
2763+
})();
2764+
2765+
const buttons = data.buttons.map((value) => {
2766+
return {
2767+
name: this.mapType.get(value.type),
2768+
buttonParamsJson: this.toJSONString(value),
2769+
};
2770+
});
2771+
2772+
const message: proto.IMessage = {
2773+
viewOnceMessage: {
2774+
message: {
2775+
interactiveMessage: {
2776+
body: {
2777+
text: (() => {
2778+
let t = '*' + data.title + '*';
2779+
if (data?.description) {
2780+
t += '\n\n';
2781+
t += data.description;
2782+
t += '\n';
2783+
}
2784+
return t;
2785+
})(),
2786+
},
2787+
footer: {
2788+
text: data?.footer,
2789+
},
2790+
header: (() => {
2791+
if (generate?.message?.imageMessage) {
2792+
return {
2793+
hasMediaAttachment: !!generate.message.imageMessage,
2794+
imageMessage: generate.message.imageMessage,
2795+
};
2796+
}
2797+
})(),
2798+
nativeFlowMessage: {
2799+
buttons: buttons,
2800+
messageParamsJson: JSON.stringify({
2801+
from: 'api',
2802+
templateId: v4(),
2803+
}),
2804+
},
2805+
},
2806+
},
2807+
},
2808+
};
2809+
2810+
console.log(JSON.stringify(message));
2811+
2812+
return await this.sendMessageWithTyping(data.number, message, {
2813+
delay: data?.delay,
2814+
presence: 'composing',
2815+
quoted: data?.quoted,
2816+
mentionsEveryOne: data?.mentionsEveryOne,
2817+
mentioned: data?.mentioned,
2818+
});
26892819
}
26902820

26912821
public async locationMessage(data: SendLocationDto) {
@@ -2709,8 +2839,27 @@ export class BaileysStartupService extends ChannelStartupService {
27092839
);
27102840
}
27112841

2712-
public async listMessage() {
2713-
throw new BadRequestException('Method not available on WhatsApp Baileys');
2842+
public async listMessage(data: SendListDto) {
2843+
return await this.sendMessageWithTyping(
2844+
data.number,
2845+
{
2846+
listMessage: {
2847+
title: data.title,
2848+
description: data.description,
2849+
buttonText: data?.buttonText,
2850+
footerText: data?.footerText,
2851+
sections: data.sections,
2852+
listType: 2,
2853+
},
2854+
},
2855+
{
2856+
delay: data?.delay,
2857+
presence: 'composing',
2858+
quoted: data?.quoted,
2859+
mentionsEveryOne: data?.mentionsEveryOne,
2860+
mentioned: data?.mentioned,
2861+
},
2862+
);
27142863
}
27152864

27162865
public async contactMessage(data: SendContactDto) {

src/api/routes/sendMessage.router.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { RouterBroker } from '@api/abstract/abstract.router';
22
import {
33
SendAudioDto,
4-
SendButtonDto,
4+
SendButtonsDto,
55
SendContactDto,
66
SendListDto,
77
SendLocationDto,
@@ -16,7 +16,7 @@ import {
1616
import { sendMessageController } from '@api/server.module';
1717
import {
1818
audioMessageSchema,
19-
buttonMessageSchema,
19+
buttonsMessageSchema,
2020
contactMessageSchema,
2121
listMessageSchema,
2222
locationMessageSchema,
@@ -159,10 +159,10 @@ export class MessageRouter extends RouterBroker {
159159
return res.status(HttpStatus.CREATED).json(response);
160160
})
161161
.post(this.routerPath('sendButtons'), ...guards, async (req, res) => {
162-
const response = await this.dataValidate<SendButtonDto>({
162+
const response = await this.dataValidate<SendButtonsDto>({
163163
request: req,
164-
schema: buttonMessageSchema,
165-
ClassRef: SendButtonDto,
164+
schema: buttonsMessageSchema,
165+
ClassRef: SendButtonsDto,
166166
execute: (instance, data) => sendMessageController.sendButtons(instance, data),
167167
});
168168

src/validate/message.schema.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -371,31 +371,33 @@ export const listMessageSchema: JSONSchema7 = {
371371
required: ['number', 'title', 'footerText', 'buttonText', 'sections'],
372372
};
373373

374-
export const buttonMessageSchema: JSONSchema7 = {
374+
export const buttonsMessageSchema: JSONSchema7 = {
375375
$id: v4(),
376376
type: 'object',
377377
properties: {
378378
number: { ...numberDefinition },
379+
thumbnailUrl: { type: 'string' },
379380
title: { type: 'string' },
380381
description: { type: 'string' },
381-
footerText: { type: 'string' },
382+
footer: { type: 'string' },
382383
buttons: {
383384
type: 'array',
384-
minItems: 1,
385-
uniqueItems: true,
386385
items: {
387386
type: 'object',
388387
properties: {
389-
text: { type: 'string' },
388+
type: {
389+
type: 'string',
390+
enum: ['reply', 'copy', 'url', 'call'],
391+
},
392+
displayText: { type: 'string' },
390393
id: { type: 'string' },
394+
url: { type: 'string' },
395+
phoneNumber: { type: 'string' },
391396
},
392-
required: ['text', 'id'],
393-
...isNotEmpty('text', 'id'),
397+
required: ['type', 'displayText'],
398+
...isNotEmpty('id', 'url', 'phoneNumber'),
394399
},
395400
},
396-
media: { type: 'string' },
397-
fileName: { type: 'string' },
398-
mediatype: { type: 'string', enum: ['image', 'document', 'video'] },
399401
delay: {
400402
type: 'integer',
401403
description: 'Enter a value in milliseconds',
@@ -413,5 +415,5 @@ export const buttonMessageSchema: JSONSchema7 = {
413415
},
414416
},
415417
},
416-
required: ['number', 'title', 'buttons'],
418+
required: ['number'],
417419
};

0 commit comments

Comments
 (0)