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