feat: send list and buttons

This commit is contained in:
Davidson Gomes 2024-10-18 15:33:21 -03:00
parent c4bcd1fafe
commit 1787238c63
5 changed files with 193 additions and 28 deletions

View File

@ -3,6 +3,8 @@
### Features
* Fake Call function
* Send List with Baileys
* Send Buttons with Baileys
* Added unreadMessages to chats
* Pusher event integration
* Add support for splitMessages and timePerChar in Integrations
@ -12,7 +14,11 @@
* Fixed prefilledVariables in startTypebot
* Fix duplicate file upload
* Mark as read from me and groups
* fetch chats query
* Fetch chats query
* Ads messages in chatwoot
* Add indexes to improve performance in Evolution
* Add logical or permanent message deletion based on env config
* Add support for fetching multiple instances by key
# 2.1.2 (2024-10-06 10:09)

View File

@ -90,14 +90,22 @@ export class SendAudioDto extends Metadata {
audio: string;
}
class Button {
text: string;
id: string;
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
export class Button {
type: TypeButton;
displayText: string;
id?: string;
url?: string;
copyCode?: string;
phoneNumber?: string;
}
export class SendButtonDto extends Metadata {
export class SendButtonsDto extends Metadata {
thumbnailUrl?: string;
title: string;
description: string;
footerText?: string;
description?: string;
footer?: string;
buttons: Button[];
}

View File

@ -31,11 +31,14 @@ import {
import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
import {
Button,
ContactMessage,
MediaMessage,
Options,
SendAudioDto,
SendButtonsDto,
SendContactDto,
SendListDto,
SendLocationDto,
SendMediaDto,
SendPollDto,
@ -44,6 +47,7 @@ import {
SendStickerDto,
SendTextDto,
StatusMessage,
TypeButton,
} from '@api/dto/sendMessage.dto';
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
@ -117,7 +121,7 @@ import makeWASocket, {
import { Label } from 'baileys/lib/Types/Label';
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
import { spawn } from 'child_process';
import { isBase64, isURL } from 'class-validator';
import { isArray, isBase64, isURL } from 'class-validator';
import { randomBytes } from 'crypto';
import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg';
@ -582,6 +586,23 @@ export class BaileysStartupService extends ChannelStartupService {
cachedGroupMetadata: this.getGroupMetadataCache,
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
patchMessageBeforeSending(message) {
if (
message.deviceSentMessage?.message?.listMessage?.listType === proto.Message.ListMessage.ListType.PRODUCT_LIST
) {
message = JSON.parse(JSON.stringify(message));
message.deviceSentMessage.message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
if (message.listMessage?.listType == proto.Message.ListMessage.ListType.PRODUCT_LIST) {
message = JSON.parse(JSON.stringify(message));
message.listMessage.listType = proto.Message.ListMessage.ListType.SINGLE_SELECT;
}
return message;
},
};
this.endSession = false;
@ -1768,6 +1789,28 @@ export class BaileysStartupService extends ChannelStartupService {
if (messageId) option.messageId = messageId;
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase();
if (message['viewOnceMessage']) {
const m = generateWAMessageFromContent(sender, message, {
timestamp: new Date(),
userJid: this.instance.wuid,
messageId,
quoted,
});
const id = await this.client.relayMessage(sender, message, { messageId });
m.key = {
id: id,
remoteJid: sender,
participant: isJidUser(sender) ? sender : undefined,
fromMe: true,
};
for (const [key, value] of Object.entries(m)) {
if (!value || (isArray(value) && value.length) === 0) {
delete m[key];
}
}
return m;
}
if (
!message['audio'] &&
!message['poll'] &&
@ -2684,8 +2727,95 @@ export class BaileysStartupService extends ChannelStartupService {
);
}
public async buttonMessage() {
throw new BadRequestException('Method not available on WhatsApp Baileys');
private toJSONString(button: Button): string {
const toString = (obj: any) => JSON.stringify(obj);
const json = {
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
reply: () => toString({ display_text: button.displayText, id: button.id }),
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
url: () =>
toString({
display_text: button.displayText,
url: button.url,
merchant_url: button.url,
}),
};
return json[button.type]?.() || '';
}
private readonly mapType = new Map<TypeButton, string>([
['reply', 'quick_reply'],
['copy', 'cta_copy'],
['url', 'cta_url'],
['call', 'cta_call'],
]);
public async buttonMessage(data: SendButtonsDto) {
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 = {
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(),
}),
},
},
},
},
};
console.log(JSON.stringify(message));
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) {
@ -2709,8 +2839,27 @@ export class BaileysStartupService extends ChannelStartupService {
);
}
public async listMessage() {
throw new BadRequestException('Method not available on WhatsApp Baileys');
public async listMessage(data: SendListDto) {
return await this.sendMessageWithTyping(
data.number,
{
listMessage: {
title: data.title,
description: data.description,
buttonText: data?.buttonText,
footerText: data?.footerText,
sections: data.sections,
listType: 2,
},
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
);
}
public async contactMessage(data: SendContactDto) {

View File

@ -1,7 +1,7 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import {
SendAudioDto,
SendButtonDto,
SendButtonsDto,
SendContactDto,
SendListDto,
SendLocationDto,
@ -16,7 +16,7 @@ import {
import { sendMessageController } from '@api/server.module';
import {
audioMessageSchema,
buttonMessageSchema,
buttonsMessageSchema,
contactMessageSchema,
listMessageSchema,
locationMessageSchema,
@ -159,10 +159,10 @@ export class MessageRouter extends RouterBroker {
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendButtons'), ...guards, async (req, res) => {
const response = await this.dataValidate<SendButtonDto>({
const response = await this.dataValidate<SendButtonsDto>({
request: req,
schema: buttonMessageSchema,
ClassRef: SendButtonDto,
schema: buttonsMessageSchema,
ClassRef: SendButtonsDto,
execute: (instance, data) => sendMessageController.sendButtons(instance, data),
});

View File

@ -371,31 +371,33 @@ export const listMessageSchema: JSONSchema7 = {
required: ['number', 'title', 'footerText', 'buttonText', 'sections'],
};
export const buttonMessageSchema: JSONSchema7 = {
export const buttonsMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
thumbnailUrl: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
footerText: { type: 'string' },
footer: { type: 'string' },
buttons: {
type: 'array',
minItems: 1,
uniqueItems: true,
items: {
type: 'object',
properties: {
text: { type: 'string' },
type: {
type: 'string',
enum: ['reply', 'copy', 'url', 'call'],
},
displayText: { type: 'string' },
id: { type: 'string' },
url: { type: 'string' },
phoneNumber: { type: 'string' },
},
required: ['text', 'id'],
...isNotEmpty('text', 'id'),
required: ['type', 'displayText'],
...isNotEmpty('id', 'url', 'phoneNumber'),
},
},
media: { type: 'string' },
fileName: { type: 'string' },
mediatype: { type: 'string', enum: ['image', 'document', 'video'] },
delay: {
type: 'integer',
description: 'Enter a value in milliseconds',
@ -413,5 +415,5 @@ export const buttonMessageSchema: JSONSchema7 = {
},
},
},
required: ['number', 'title', 'buttons'],
required: ['number'],
};