mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
feat: send list and buttons
This commit is contained in:
parent
c4bcd1fafe
commit
1787238c63
@ -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)
|
||||
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
});
|
||||
|
||||
|
@ -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'],
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user