mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-14 09:51:24 -06:00
Refactor Evolution Channel with new instance management and media handling features
- Added `setInstance` method to manage instance details and trigger events for instance creation. - Refactored `connectToWhatsapp` to improve error handling and streamline connection logic. - Enhanced `sendMessageWithTyping` to support various message types including buttons and lists, and integrated file handling for media messages. - Updated `processAudio` to handle audio conversion using an external API, improving audio processing capabilities. - Introduced new methods for sending button messages and handling audio messages, enhancing user interaction features. - Improved contact update logic to ensure accurate data synchronization with the database and external services.
This commit is contained in:
parent
666c0b514d
commit
3a04f7587e
@ -1,17 +1,28 @@
|
||||
import { MediaMessage, Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
||||
import { ProviderFiles } from '@api/provider/sessions';
|
||||
import { InstanceDto } from '@api/dto/instance.dto';
|
||||
import {
|
||||
MediaMessage,
|
||||
Options,
|
||||
SendAudioDto,
|
||||
SendButtonsDto,
|
||||
SendMediaDto,
|
||||
SendTextDto,
|
||||
} from '@api/dto/sendMessage.dto';
|
||||
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
|
||||
import { PrismaRepository } from '@api/repository/repository.service';
|
||||
import { chatbotController } from '@api/server.module';
|
||||
import { CacheService } from '@api/services/cache.service';
|
||||
import { ChannelStartupService } from '@api/services/channel.service';
|
||||
import { Events, wa } from '@api/types/wa.types';
|
||||
import { Chatwoot, ConfigService, Openai } from '@config/env.config';
|
||||
import { Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
|
||||
import { BadRequestException, InternalServerErrorException } from '@exceptions';
|
||||
import { createJid } from '@utils/createJid';
|
||||
import { status } from '@utils/renderStatus';
|
||||
import { isURL } from 'class-validator';
|
||||
import axios from 'axios';
|
||||
import { isBase64, isURL } from 'class-validator';
|
||||
import EventEmitter2 from 'eventemitter2';
|
||||
import FormData from 'form-data';
|
||||
import mime from 'mime';
|
||||
import mimeTypes from 'mime-types';
|
||||
import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export class EvolutionStartupService extends ChannelStartupService {
|
||||
@ -21,8 +32,6 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
public readonly prismaRepository: PrismaRepository,
|
||||
public readonly cache: CacheService,
|
||||
public readonly chatwootCache: CacheService,
|
||||
public readonly baileysCache: CacheService,
|
||||
private readonly providerFiles: ProviderFiles,
|
||||
) {
|
||||
super(configService, eventEmitter, prismaRepository, chatwootCache);
|
||||
|
||||
@ -57,6 +66,32 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
await this.closeClient();
|
||||
}
|
||||
|
||||
public setInstance(instance: InstanceDto) {
|
||||
this.logger.setInstance(instance.instanceId);
|
||||
|
||||
this.instance.name = instance.instanceName;
|
||||
this.instance.id = instance.instanceId;
|
||||
this.instance.integration = instance.integration;
|
||||
this.instance.number = instance.number;
|
||||
this.instance.token = instance.token;
|
||||
this.instance.businessId = instance.businessId;
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
this.chatwootService.eventWhatsapp(
|
||||
Events.STATUS_INSTANCE,
|
||||
{
|
||||
instanceName: this.instance.name,
|
||||
instanceId: this.instance.id,
|
||||
integration: instance.integration,
|
||||
},
|
||||
{
|
||||
instance: this.instance.name,
|
||||
status: 'created',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async profilePicture(number: string) {
|
||||
const jid = createJid(number);
|
||||
|
||||
@ -79,11 +114,12 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
public async connectToWhatsapp(data?: any): Promise<any> {
|
||||
if (!data) return;
|
||||
if (!data) {
|
||||
this.loadChatwoot();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.loadChatwoot();
|
||||
|
||||
this.eventHandler(data);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@ -100,6 +136,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
id: received.key.id || v4(),
|
||||
remoteJid: received.key.remoteJid,
|
||||
fromMe: received.key.fromMe,
|
||||
profilePicUrl: received.profilePicUrl,
|
||||
};
|
||||
messageRaw = {
|
||||
key,
|
||||
@ -111,7 +148,9 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED) {
|
||||
const isAudio = received?.message?.audioMessage;
|
||||
|
||||
if (this.configService.get<Openai>('OPENAI').ENABLED && isAudio) {
|
||||
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
@ -166,7 +205,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
|
||||
await this.updateContact({
|
||||
remoteJid: messageRaw.key.remoteJid,
|
||||
pushName: messageRaw.key.fromMe ? '' : messageRaw.key.fromMe == null ? '' : received.pushName,
|
||||
pushName: messageRaw.pushName,
|
||||
profilePicUrl: received.profilePicUrl,
|
||||
});
|
||||
}
|
||||
@ -176,35 +215,6 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
}
|
||||
|
||||
private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) {
|
||||
const contact = await this.prismaRepository.contact.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
|
||||
});
|
||||
|
||||
if (contact) {
|
||||
const contactRaw: any = {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data?.pushName,
|
||||
instanceId: this.instanceId,
|
||||
profilePicUrl: data?.profilePicUrl,
|
||||
};
|
||||
|
||||
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
|
||||
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
await this.chatwootService.eventWhatsapp(
|
||||
Events.CONTACTS_UPDATE,
|
||||
{ instanceName: this.instance.name, instanceId: this.instanceId },
|
||||
contactRaw,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prismaRepository.contact.updateMany({
|
||||
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
|
||||
data: contactRaw,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const contactRaw: any = {
|
||||
remoteJid: data.remoteJid,
|
||||
pushName: data?.pushName,
|
||||
@ -212,11 +222,40 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
profilePicUrl: data?.profilePicUrl,
|
||||
};
|
||||
|
||||
const existingContact = await this.prismaRepository.contact.findFirst({
|
||||
where: {
|
||||
remoteJid: data.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingContact) {
|
||||
await this.prismaRepository.contact.updateMany({
|
||||
where: {
|
||||
remoteJid: data.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
},
|
||||
data: contactRaw,
|
||||
});
|
||||
} else {
|
||||
await this.prismaRepository.contact.create({
|
||||
data: contactRaw,
|
||||
});
|
||||
}
|
||||
|
||||
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
|
||||
|
||||
await this.prismaRepository.contact.create({
|
||||
data: contactRaw,
|
||||
});
|
||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||
await this.chatwootService.eventWhatsapp(
|
||||
Events.CONTACTS_UPDATE,
|
||||
{
|
||||
instanceName: this.instance.name,
|
||||
instanceId: this.instanceId,
|
||||
integration: this.instance.integration,
|
||||
},
|
||||
contactRaw,
|
||||
);
|
||||
}
|
||||
|
||||
const chat = await this.prismaRepository.chat.findFirst({
|
||||
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
|
||||
@ -248,7 +287,13 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
});
|
||||
}
|
||||
|
||||
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
|
||||
protected async sendMessageWithTyping(
|
||||
number: string,
|
||||
message: any,
|
||||
options?: Options,
|
||||
file?: any,
|
||||
isIntegration = false,
|
||||
) {
|
||||
try {
|
||||
let quoted: any;
|
||||
let webhookUrl: any;
|
||||
@ -273,64 +318,187 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
webhookUrl = options.webhookUrl;
|
||||
}
|
||||
|
||||
let audioFile;
|
||||
|
||||
const messageId = v4();
|
||||
|
||||
let messageRaw: any = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
status: status[1],
|
||||
};
|
||||
let messageRaw: any;
|
||||
|
||||
if (message?.mediaType === 'image') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'imageMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message?.mediaType === 'video') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'videoMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message?.mediaType === 'audio') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'audioMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
|
||||
const buffer = Buffer.from(message.media, 'base64');
|
||||
audioFile = {
|
||||
buffer,
|
||||
mimetype: 'audio/mp4',
|
||||
originalname: `${messageId}.mp4`,
|
||||
};
|
||||
} else if (message?.mediaType === 'document') {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
mediaUrl: message.media,
|
||||
base64: isBase64(message.media) ? message.media : undefined,
|
||||
mediaUrl: isURL(message.media) ? message.media : undefined,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'documentMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message.buttonMessage) {
|
||||
messageRaw = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message.buttonMessage,
|
||||
buttons: message.buttonMessage.buttons,
|
||||
footer: message.buttonMessage.footer,
|
||||
body: message.buttonMessage.body,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'buttonMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else if (message.listMessage) {
|
||||
messageRaw = {
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message.listMessage,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'listMessage',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
} else {
|
||||
messageRaw = {
|
||||
...messageRaw,
|
||||
key: { fromMe: true, id: messageId, remoteJid: number },
|
||||
message: {
|
||||
...message,
|
||||
quoted,
|
||||
},
|
||||
messageType: 'conversation',
|
||||
messageTimestamp: Math.round(new Date().getTime() / 1000),
|
||||
webhookUrl,
|
||||
source: 'unknown',
|
||||
instanceId: this.instanceId,
|
||||
};
|
||||
}
|
||||
|
||||
if (messageRaw.message.contextInfo) {
|
||||
messageRaw.contextInfo = {
|
||||
...messageRaw.message.contextInfo,
|
||||
};
|
||||
}
|
||||
|
||||
if (messageRaw.contextInfo?.stanzaId) {
|
||||
const key: any = {
|
||||
id: messageRaw.contextInfo.stanzaId,
|
||||
};
|
||||
|
||||
const findMessage = await this.prismaRepository.message.findFirst({
|
||||
where: {
|
||||
instanceId: this.instanceId,
|
||||
key,
|
||||
},
|
||||
});
|
||||
|
||||
if (findMessage) {
|
||||
messageRaw.contextInfo.quotedMessage = findMessage.message;
|
||||
}
|
||||
}
|
||||
|
||||
const base64 = messageRaw.message.base64;
|
||||
delete messageRaw.message.base64;
|
||||
|
||||
if (base64 || file || audioFile) {
|
||||
if (this.configService.get<S3>('S3').ENABLE) {
|
||||
try {
|
||||
const fileBuffer = audioFile?.buffer || file?.buffer;
|
||||
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
|
||||
|
||||
let mediaType: string;
|
||||
let mimetype = audioFile?.mimetype || file.mimetype;
|
||||
|
||||
if (messageRaw.messageType === 'documentMessage') {
|
||||
mediaType = 'document';
|
||||
mimetype = !mimetype ? 'application/pdf' : mimetype;
|
||||
} else if (messageRaw.messageType === 'imageMessage') {
|
||||
mediaType = 'image';
|
||||
mimetype = !mimetype ? 'image/png' : mimetype;
|
||||
} else if (messageRaw.messageType === 'audioMessage') {
|
||||
mediaType = 'audio';
|
||||
mimetype = !mimetype ? 'audio/mp4' : mimetype;
|
||||
} else if (messageRaw.messageType === 'videoMessage') {
|
||||
mediaType = 'video';
|
||||
mimetype = !mimetype ? 'video/mp4' : mimetype;
|
||||
}
|
||||
|
||||
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
|
||||
|
||||
const size = buffer.byteLength;
|
||||
|
||||
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
|
||||
|
||||
await s3Service.uploadFile(fullName, buffer, size, {
|
||||
'Content-Type': mimetype,
|
||||
});
|
||||
|
||||
const mediaUrl = await s3Service.getObjectUrl(fullName);
|
||||
|
||||
messageRaw.message.mediaUrl = mediaUrl;
|
||||
} catch (error) {
|
||||
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(messageRaw);
|
||||
|
||||
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
|
||||
@ -376,6 +544,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
null,
|
||||
isIntegration,
|
||||
);
|
||||
return res;
|
||||
@ -440,33 +609,78 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
file,
|
||||
isIntegration,
|
||||
);
|
||||
|
||||
return mediaSent;
|
||||
}
|
||||
|
||||
public async processAudio(audio: string, number: string) {
|
||||
public async processAudio(audio: string, number: string, file: any) {
|
||||
number = number.replace(/\D/g, '');
|
||||
const hash = `${number}-${new Date().getTime()}`;
|
||||
|
||||
let mimetype: string | false;
|
||||
if (process.env.API_AUDIO_CONVERTER) {
|
||||
try {
|
||||
this.logger.verbose('Using audio converter API');
|
||||
const formData = new FormData();
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp4`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
};
|
||||
if (file) {
|
||||
formData.append('file', file.buffer, {
|
||||
filename: file.originalname,
|
||||
contentType: file.mimetype,
|
||||
});
|
||||
} else if (isURL(audio)) {
|
||||
formData.append('url', audio);
|
||||
} else {
|
||||
formData.append('base64', audio);
|
||||
}
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mimeTypes.lookup(audio);
|
||||
formData.append('format', 'mp4');
|
||||
|
||||
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
|
||||
headers: {
|
||||
...formData.getHeaders(),
|
||||
apikey: process.env.API_AUDIO_CONVERTER_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response?.data?.audio) {
|
||||
throw new InternalServerErrorException('Failed to convert audio');
|
||||
}
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp4`,
|
||||
mediaType: 'audio',
|
||||
media: response?.data?.audio,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
|
||||
return prepareMedia;
|
||||
} catch (error) {
|
||||
this.logger.error(error?.response?.data || error);
|
||||
throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error);
|
||||
}
|
||||
} else {
|
||||
mimetype = mimeTypes.lookup(prepareMedia.fileName);
|
||||
let mimetype: string;
|
||||
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
mimetype: 'audio/mpeg',
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
mimetype = mime.getType(audio);
|
||||
} else {
|
||||
mimetype = mime.getType(prepareMedia.fileName);
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
|
||||
prepareMedia.mimetype = mimetype;
|
||||
|
||||
return prepareMedia;
|
||||
}
|
||||
|
||||
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
|
||||
@ -479,7 +693,7 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
throw new Error('File or buffer is undefined.');
|
||||
}
|
||||
|
||||
const message = await this.processAudio(mediaData.audio, data.number);
|
||||
const message = await this.processAudio(mediaData.audio, data.number, file);
|
||||
|
||||
const audioSent = await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
@ -492,14 +706,34 @@ export class EvolutionStartupService extends ChannelStartupService {
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
file,
|
||||
isIntegration,
|
||||
);
|
||||
|
||||
return audioSent;
|
||||
}
|
||||
|
||||
public async buttonMessage() {
|
||||
throw new BadRequestException('Method not available on Evolution Channel');
|
||||
public async buttonMessage(data: SendButtonsDto, isIntegration = false) {
|
||||
return await this.sendMessageWithTyping(
|
||||
data.number,
|
||||
{
|
||||
buttonMessage: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
footer: data.footer,
|
||||
buttons: data.buttons,
|
||||
},
|
||||
},
|
||||
{
|
||||
delay: data?.delay,
|
||||
presence: 'composing',
|
||||
quoted: data?.quoted,
|
||||
mentionsEveryOne: data?.mentionsEveryOne,
|
||||
mentioned: data?.mentioned,
|
||||
},
|
||||
null,
|
||||
isIntegration,
|
||||
);
|
||||
}
|
||||
public async locationMessage() {
|
||||
throw new BadRequestException('Method not available on Evolution Channel');
|
||||
|
Loading…
Reference in New Issue
Block a user