diff --git a/CHANGELOG.md b/CHANGELOG.md index 9501310b..c49cdee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/api/dto/sendMessage.dto.ts b/src/api/dto/sendMessage.dto.ts index 8d3ba1a0..c9a5e9cd 100644 --- a/src/api/dto/sendMessage.dto.ts +++ b/src/api/dto/sendMessage.dto.ts @@ -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[]; } diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index aaabae2c..9581d1e7 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -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([ + ['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) { diff --git a/src/api/routes/sendMessage.router.ts b/src/api/routes/sendMessage.router.ts index 06f70ad7..73f17713 100644 --- a/src/api/routes/sendMessage.router.ts +++ b/src/api/routes/sendMessage.router.ts @@ -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({ + const response = await this.dataValidate({ request: req, - schema: buttonMessageSchema, - ClassRef: SendButtonDto, + schema: buttonsMessageSchema, + ClassRef: SendButtonsDto, execute: (instance, data) => sendMessageController.sendButtons(instance, data), }); diff --git a/src/validate/message.schema.ts b/src/validate/message.schema.ts index 4e8651cc..ef0d6c7a 100644 --- a/src/validate/message.schema.ts +++ b/src/validate/message.schema.ts @@ -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'], };