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 ### Features
* Fake Call function * Fake Call function
* Send List with Baileys
* Send Buttons with Baileys
* Added unreadMessages to chats * Added unreadMessages to chats
* Pusher event integration * Pusher event integration
* Add support for splitMessages and timePerChar in Integrations * Add support for splitMessages and timePerChar in Integrations
@ -12,7 +14,11 @@
* Fixed prefilledVariables in startTypebot * Fixed prefilledVariables in startTypebot
* Fix duplicate file upload * Fix duplicate file upload
* Mark as read from me and groups * 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) # 2.1.2 (2024-10-06 10:09)

View File

@ -90,14 +90,22 @@ export class SendAudioDto extends Metadata {
audio: string; audio: string;
} }
class Button { export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
text: string;
id: string; 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; title: string;
description: string; description?: string;
footerText?: string; footer?: string;
buttons: Button[]; buttons: Button[];
} }

View File

@ -31,11 +31,14 @@ import {
import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto'; import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
import { HandleLabelDto, LabelDto } from '@api/dto/label.dto'; import { HandleLabelDto, LabelDto } from '@api/dto/label.dto';
import { import {
Button,
ContactMessage, ContactMessage,
MediaMessage, MediaMessage,
Options, Options,
SendAudioDto, SendAudioDto,
SendButtonsDto,
SendContactDto, SendContactDto,
SendListDto,
SendLocationDto, SendLocationDto,
SendMediaDto, SendMediaDto,
SendPollDto, SendPollDto,
@ -44,6 +47,7 @@ import {
SendStickerDto, SendStickerDto,
SendTextDto, SendTextDto,
StatusMessage, StatusMessage,
TypeButton,
} from '@api/dto/sendMessage.dto'; } from '@api/dto/sendMessage.dto';
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper'; import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server'; 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 { Label } from 'baileys/lib/Types/Label';
import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation'; import { LabelAssociation } from 'baileys/lib/Types/LabelAssociation';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { isBase64, isURL } from 'class-validator'; import { isArray, isBase64, isURL } from 'class-validator';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import ffmpeg from 'fluent-ffmpeg'; import ffmpeg from 'fluent-ffmpeg';
@ -582,6 +586,23 @@ export class BaileysStartupService extends ChannelStartupService {
cachedGroupMetadata: this.getGroupMetadataCache, cachedGroupMetadata: this.getGroupMetadataCache,
userDevicesCache: this.userDevicesCache, userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 }, 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; this.endSession = false;
@ -1768,6 +1789,28 @@ export class BaileysStartupService extends ChannelStartupService {
if (messageId) option.messageId = messageId; if (messageId) option.messageId = messageId;
else option.messageId = '3EB0' + randomBytes(18).toString('hex').toUpperCase(); 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 ( if (
!message['audio'] && !message['audio'] &&
!message['poll'] && !message['poll'] &&
@ -2684,8 +2727,95 @@ export class BaileysStartupService extends ChannelStartupService {
); );
} }
public async buttonMessage() { private toJSONString(button: Button): string {
throw new BadRequestException('Method not available on WhatsApp Baileys'); 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) { public async locationMessage(data: SendLocationDto) {
@ -2709,8 +2839,27 @@ export class BaileysStartupService extends ChannelStartupService {
); );
} }
public async listMessage() { public async listMessage(data: SendListDto) {
throw new BadRequestException('Method not available on WhatsApp Baileys'); 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) { public async contactMessage(data: SendContactDto) {

View File

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

View File

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