This commit is contained in:
João Victor Souza
2025-02-06 18:02:36 -03:00
67 changed files with 14456 additions and 589 deletions

View File

@@ -63,6 +63,9 @@ export class InstanceController {
instanceId,
integration: instanceData.integration,
instanceName: instanceData.instanceName,
ownerJid: instanceData.ownerJid,
profileName: instanceData.profileName,
profilePicUrl: instanceData.profilePicUrl,
hash,
number: instanceData.number,
businessId: instanceData.businessId,
@@ -119,6 +122,7 @@ export class InstanceController {
readMessages: instanceData.readMessages === true,
readStatus: instanceData.readStatus === true,
syncFullHistory: instanceData.syncFullHistory === true,
wavoipToken: instanceData.wavoipToken || '',
};
await this.settingsService.create(instance, settings);
@@ -409,15 +413,11 @@ export class InstanceController {
public async deleteInstance({ instanceName }: InstanceDto) {
const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'open') {
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
}
try {
const waInstances = this.waMonitor.waInstances[instanceName];
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
if (instance.state === 'connecting') {
if (instance.state === 'connecting' || instance.state === 'open') {
await this.logout({ instanceName });
}

View File

@@ -10,7 +10,10 @@ import axios from 'axios';
const logger = new Logger('ProxyController');
export class ProxyController {
constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
constructor(
private readonly proxyService: ProxyService,
private readonly waMonitor: WAMonitoringService,
) {}
public async createProxy(instance: InstanceDto, data: ProxyDto) {
if (!this.waMonitor.waInstances[instance.instanceName]) {

View File

@@ -1,4 +1,5 @@
import { IntegrationDto } from '@api/integrations/integration.dto';
import { JsonValue } from '@prisma/client/runtime/library';
import { WAPresence } from 'baileys';
export class InstanceDto extends IntegrationDto {
@@ -10,6 +11,9 @@ export class InstanceDto extends IntegrationDto {
integration?: string;
token?: string;
status?: string;
ownerJid?: string;
profileName?: string;
profilePicUrl?: string;
// settings
rejectCall?: boolean;
msgCall?: string;
@@ -18,12 +22,35 @@ export class InstanceDto extends IntegrationDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
// proxy
proxyHost?: string;
proxyPort?: string;
proxyProtocol?: string;
proxyUsername?: string;
proxyPassword?: string;
webhook?: {
enabled?: boolean;
events?: string[];
headers?: JsonValue;
url?: string;
byEvents?: boolean;
base64?: boolean;
};
chatwootAccountId?: string;
chatwootConversationPending?: boolean;
chatwootAutoCreate?: boolean;
chatwootDaysLimitImportMessages?: number;
chatwootImportContacts?: boolean;
chatwootImportMessages?: boolean;
chatwootLogo?: string;
chatwootMergeBrazilContacts?: boolean;
chatwootNameInbox?: string;
chatwootOrganization?: string;
chatwootReopenConversation?: boolean;
chatwootSignMsg?: boolean;
chatwootToken?: string;
chatwootUrl?: string;
}
export class SetPresenceDto {

View File

@@ -6,4 +6,5 @@ export class SettingsDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
}

View File

@@ -75,8 +75,6 @@ export class ChannelController {
data.prismaRepository,
data.cache,
data.chatwootCache,
data.baileysCache,
data.providerFiles,
);
}

View File

@@ -2,14 +2,16 @@ import { Router } from 'express';
import { EvolutionRouter } from './evolution/evolution.router';
import { MetaRouter } from './meta/meta.router';
import { BaileysRouter } from './whatsapp/baileys.router';
export class ChannelRouter {
public readonly router: Router;
constructor(configService: any) {
constructor(configService: any, ...guards: any[]) {
this.router = Router();
this.router.use('/', new EvolutionRouter(configService).router);
this.router.use('/', new MetaRouter(configService).router);
this.router.use('/baileys', new BaileysRouter(...guards).router);
}
}

View File

@@ -1,16 +1,27 @@
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 { status } from '@utils/renderStatus';
import { isURL } from 'class-validator';
import { createJid } from '@utils/createJid';
import axios from 'axios';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import mime from 'mime';
import FormData from 'form-data';
import mimeTypes from 'mime-types';
import { join } from 'path';
import { v4 } from 'uuid';
export class EvolutionStartupService extends ChannelStartupService {
@@ -20,8 +31,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);
@@ -56,8 +65,34 @@ 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 = this.createJid(number);
const jid = createJid(number);
return {
wuid: jid,
@@ -78,11 +113,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);
@@ -99,6 +135,7 @@ export class EvolutionStartupService extends ChannelStartupService {
id: received.key.id || v4(),
remoteJid: received.key.remoteJid,
fromMe: received.key.fromMe,
profilePicUrl: received.profilePicUrl,
};
messageRaw = {
key,
@@ -110,7 +147,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,
@@ -165,7 +204,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,
});
}
@@ -175,35 +214,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,
@@ -211,11 +221,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 },
@@ -247,7 +286,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;
@@ -272,64 +317,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);
@@ -375,6 +543,7 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
null,
isIntegration,
);
return res;
@@ -396,7 +565,7 @@ export class EvolutionStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
caption: mediaMessage?.caption,
@@ -407,9 +576,9 @@ export class EvolutionStartupService extends ChannelStartupService {
};
if (isURL(mediaMessage.media)) {
mimetype = mime.getType(mediaMessage.media);
mimetype = mimeTypes.lookup(mediaMessage.media);
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
}
prepareMedia.mimetype = mimetype;
@@ -439,33 +608,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;
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 = mime.getType(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 = mime.getType(prepareMedia.fileName);
let mimetype: string;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
mimetype: 'audio/mpeg',
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio).toString();
} else {
mimetype = mimeTypes.lookup(prepareMedia.fileName).toString();
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
@@ -478,7 +692,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,
@@ -491,14 +705,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');

View File

@@ -22,13 +22,14 @@ import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid';
import { status } from '@utils/renderStatus';
import axios from 'axios';
import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import mime from 'mime';
import mimeTypes from 'mime-types';
import { join } from 'path';
export class BusinessStartupService extends ChannelStartupService {
@@ -70,6 +71,10 @@ export class BusinessStartupService extends ChannelStartupService {
await this.closeClient();
}
private isMediaMessage(message: any) {
return message.document || message.image || message.audio || message.video;
}
private async post(message: any, params: string) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
@@ -84,7 +89,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
return {
wuid: jid,
@@ -128,9 +133,7 @@ export class BusinessStartupService extends ChannelStartupService {
this.eventHandler(content);
this.phoneNumber = this.createJid(
content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id,
);
this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());
@@ -227,7 +230,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (!contact.phones[0]?.wa_id) {
contact.phones[0].wa_id = this.createJid(contact.phones[0].phone);
contact.phones[0].wa_id = createJid(contact.phones[0].phone);
}
result +=
@@ -301,12 +304,7 @@ export class BusinessStartupService extends ChannelStartupService {
remoteJid: this.phoneNumber,
fromMe: received.messages[0].from === received.metadata.phone_number_id,
};
if (
received?.messages[0].document ||
received?.messages[0].image ||
received?.messages[0].audio ||
received?.messages[0].video
) {
if (this.isMediaMessage(received?.messages[0])) {
messageRaw = {
key,
pushName,
@@ -331,15 +329,19 @@ export class BusinessStartupService extends ChannelStartupService {
const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
const mediaType = message.messages[0].document
? 'document'
: message.messages[0].image
? 'image'
: message.messages[0].audio
? 'audio'
: 'video';
let mediaType;
const mimetype = result.headers['content-type'];
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
} else {
mediaType = 'video';
}
const mimetype = result.data?.mime_type || result.headers['content-type'];
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
@@ -352,15 +354,19 @@ export class BusinessStartupService extends ChannelStartupService {
const size = result.headers['content-length'] || buffer.data.byteLength;
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
await this.prismaRepository.media.create({
data: {
messageId: received.messages[0].id,
messageId: createdMessage.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
@@ -371,6 +377,7 @@ export class BusinessStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
@@ -458,16 +465,23 @@ export class BusinessStartupService extends ChannelStartupService {
},
});
const audioMessage = received?.messages[0]?.audio;
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText &&
received?.message?.audioMessage
audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
received,
this.client.updateMediaMessage,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
() => {},
);
}
}
@@ -497,9 +511,11 @@ export class BusinessStartupService extends ChannelStartupService {
}
}
await this.prismaRepository.message.create({
data: messageRaw,
});
if (!this.isMediaMessage(received?.messages[0])) {
await this.prismaRepository.message.create({
data: messageRaw,
});
}
const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
@@ -783,6 +799,8 @@ export class BusinessStartupService extends ChannelStartupService {
return await this.post(content, 'messages');
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
content = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
@@ -791,6 +809,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
caption: message['caption'],
},
};
@@ -895,7 +914,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
const messageRaw: any = {
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: this.createJid(number) },
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) },
message: this.convertMessageToRaw(message, content),
messageType: this.renderMessageType(content.type),
messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
@@ -997,7 +1016,7 @@ export class BusinessStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
caption: mediaMessage?.caption,
@@ -1008,11 +1027,11 @@ export class BusinessStartupService extends ChannelStartupService {
};
if (isURL(mediaMessage.media)) {
mimetype = mime.getType(mediaMessage.media);
mimetype = mimeTypes.lookup(mediaMessage.media);
prepareMedia.id = mediaMessage.media;
prepareMedia.type = 'link';
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
@@ -1055,7 +1074,7 @@ export class BusinessStartupService extends ChannelStartupService {
number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`;
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
@@ -1064,11 +1083,11 @@ export class BusinessStartupService extends ChannelStartupService {
};
if (isURL(audio)) {
mimetype = mime.getType(audio);
mimetype = mimeTypes.lookup(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else {
mimetype = mime.getType(prepareMedia.fileName);
mimetype = mimeTypes.lookup(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
@@ -1084,6 +1103,9 @@ export class BusinessStartupService extends ChannelStartupService {
if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else if (isURL(mediaData.audio)) {
// DO NOTHING
// mediaData.audio = mediaData.audio;
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
@@ -1251,7 +1273,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (!contact.wuid) {
contact.wuid = this.createJid(contact.phoneNumber);
contact.wuid = createJid(contact.phoneNumber);
}
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';

View File

@@ -0,0 +1,60 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BaileysController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async onWhatsapp({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysOnWhatsapp(body?.jid);
}
public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs);
}
public async assertSessions({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysAssertSessions(body?.jids, body?.force);
}
public async createParticipantNodes({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs);
}
public async getUSyncDevices({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices);
}
public async generateMessageTag({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGenerateMessageTag();
}
public async sendNode({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSendNode(body?.stanza);
}
public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext);
}
public async getAuthState({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetAuthState();
}
}

View File

@@ -0,0 +1,105 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { baileysController } from '@api/server.module';
import { instanceSchema } from '@validate/instance.schema';
import { RequestHandler, Router } from 'express';
export class BaileysRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.onWhatsapp(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.profilePictureUrl(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('assertSessions'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.assertSessions(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.createParticipantNodes(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getUSyncDevices(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.generateMessageTag(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('sendNode'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.sendNode(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getAuthState'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getAuthState(instance),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -0,0 +1,78 @@
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
export interface ServerToClientEvents {
withAck: (d: string, callback: (e: number) => void) => void;
onWhatsApp: onWhatsAppType;
profilePictureUrl: ProfilePictureUrlType;
assertSessions: AssertSessionsType;
createParticipantNodes: CreateParticipantNodesType;
getUSyncDevices: GetUSyncDevicesType;
generateMessageTag: GenerateMessageTagType;
sendNode: SendNodeType;
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
}
export interface ClientToServerEvents {
init: (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'CB:call': (packet: any) => void;
'CB:ack,class:call': (packet: any) => void;
'connection.update:status': (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'connection.update:qr': (qr: string) => void;
}
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
export type onWhatsAppCallback = (
response: {
exists: boolean;
jid: string;
}[],
) => void;
export type ProfilePictureUrlType = (
jid: string,
type: 'image' | 'preview',
timeoutMs: number | undefined,
callback: ProfilePictureUrlCallback,
) => void;
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
export type AssertSessionsCallback = (response: boolean) => void;
export type CreateParticipantNodesType = (
jids: string[],
message: any,
extraAttrs: any,
callback: CreateParticipantNodesCallback,
) => void;
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
export type GetUSyncDevicesType = (
jids: string[],
useCache: boolean,
ignoreZeroDevices: boolean,
callback: GetUSyncDevicesTypeCallback,
) => void;
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
export type GenerateMessageTagTypeCallback = (response: string) => void;
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
export type SendNodeTypeCallback = (response: boolean) => void;
export type SignalRepositoryDecryptMessageType = (
jid: string,
type: 'pkmsg' | 'msg',
ciphertext: Buffer,
callback: SignalRepositoryDecryptMessageCallback,
) => void;
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;

View File

@@ -0,0 +1,181 @@
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
import { io, Socket } from 'socket.io-client';
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
let baileys_connection_state: WAConnectionState = 'close';
export const useVoiceCallsBaileys = async (
wavoip_token: string,
baileys_sock: WASocket,
status?: WAConnectionState,
logger?: boolean,
) => {
baileys_connection_state = status ?? 'close';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
transports: ['websocket'],
path: `/${wavoip_token}/websocket`,
});
socket.on('connect', () => {
if (logger) console.log('[*] - Wavoip connected', socket.id);
socket.emit(
'init',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
baileys_connection_state,
);
});
socket.on('disconnect', () => {
if (logger) console.log('[*] - Wavoip disconnect');
});
socket.on('connect_error', (error) => {
if (socket.active) {
if (logger)
console.log(
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
error,
);
} else {
if (logger) console.log('[*] - Wavoip connection error', error.message);
}
});
socket.on('onWhatsApp', async (jid, callback) => {
try {
const response: any = await baileys_sock.onWhatsApp(jid);
callback(response);
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
} catch (error) {
if (logger) console.error('[*] Error on call onWhatsApp function', error);
}
});
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
try {
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
callback(response);
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
} catch (error) {
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
}
});
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids, force);
callback(response);
if (logger) console.log('[*] Success on call assertSessions function', response);
} catch (error) {
if (logger) console.error('[*] Error on call assertSessions function', error);
}
});
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
try {
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
callback(response, true);
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
} catch (error) {
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
}
});
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
try {
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
callback(response);
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
} catch (error) {
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
}
});
socket.on('generateMessageTag', async (callback) => {
try {
const response = await baileys_sock.generateMessageTag();
callback(response);
if (logger) console.log('[*] Success on call generateMessageTag function', response);
} catch (error) {
if (logger) console.error('[*] Error on call generateMessageTag function', error);
}
});
socket.on('sendNode', async (stanza, callback) => {
try {
console.log('sendNode', JSON.stringify(stanza));
const response = await baileys_sock.sendNode(stanza);
callback(true);
if (logger) console.log('[*] Success on call sendNode function', response);
} catch (error) {
if (logger) console.error('[*] Error on call sendNode function', error);
}
});
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
try {
const response = await baileys_sock.signalRepository.decryptMessage({
jid: jid,
type: type,
ciphertext: ciphertext,
});
callback(response);
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
} catch (error) {
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
}
});
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
const { connection } = update;
if (connection) {
baileys_connection_state = connection;
socket
.timeout(1000)
.emit(
'connection.update:status',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
connection,
);
}
if (update.qr) {
socket.timeout(1000).emit('connection.update:qr', update.qr);
}
});
baileys_sock.ws.on('CB:call', (packet) => {
if (logger) console.log('[*] Signling received');
socket.volatile.timeout(1000).emit('CB:call', packet);
});
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
if (logger) console.log('[*] Signling ack received');
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
});
return socket;
};

View File

@@ -76,7 +76,9 @@ import {
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions';
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
import { Boom } from '@hapi/boom';
import { createId as cuid } from '@paralleldrive/cuid2';
import { Instance } from '@prisma/client';
import { createJid } from '@utils/createJid';
import { makeProxyAgent } from '@utils/makeProxyAgent';
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
import { status } from '@utils/renderStatus';
@@ -130,7 +132,7 @@ import ffmpeg from 'fluent-ffmpeg';
import FormData from 'form-data';
import { readFileSync } from 'fs';
import Long from 'long';
import mime from 'mime';
import mimeTypes from 'mime-types';
import NodeCache from 'node-cache';
import cron from 'node-cron';
import { release } from 'os';
@@ -142,6 +144,8 @@ import sharp from 'sharp';
import { PassThrough, Readable } from 'stream';
import { v4 } from 'uuid';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
// Adicione a função getVideoDuration no início do arquivo
@@ -270,7 +274,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async getProfileStatus() {
const status = await this.client.fetchStatus(this.instance.wuid);
return status?.status;
return status[0]?.status;
}
public get profilePictureUrl() {
@@ -309,6 +313,9 @@ export class BaileysStartupService extends ChannelStartupService {
instance: this.instance.name,
state: 'refused',
statusReason: DisconnectReason.connectionClosed,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
});
this.endSession = true;
@@ -388,11 +395,6 @@ export class BaileysStartupService extends ChannelStartupService {
state: connection,
statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200,
};
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
if (connection === 'close') {
@@ -434,6 +436,11 @@ export class BaileysStartupService extends ChannelStartupService {
this.eventEmitter.emit('logout.instance', this.instance.name, 'inner');
this.client?.ws?.close();
this.client.end(new Error('Close connection'));
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
}
@@ -481,6 +488,21 @@ export class BaileysStartupService extends ChannelStartupService {
);
this.syncChatwootLostMessages();
}
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
...this.stateConnection,
});
}
if (connection === 'connecting') {
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
}
@@ -672,8 +694,30 @@ export class BaileysStartupService extends ChannelStartupService {
this.client = makeWASocket(socketConfig);
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
}
this.eventHandler();
this.client.ws.on('CB:call', (packet) => {
console.log('CB:call', packet);
const payload = {
event: 'CB:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.client.ws.on('CB:ack,class:call', (packet) => {
console.log('CB:ack,class:call', packet);
const payload = {
event: 'CB:ack,class:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.phoneNumber = number;
return this.client;
@@ -973,7 +1017,7 @@ export class BaileysStartupService extends ChannelStartupService {
const messagesRaw: any[] = [];
const messagesRepository = new Set(
const messagesRepository: Set<string> = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ??
(
await this.prismaRepository.message.findMany({
@@ -1135,21 +1179,25 @@ export class BaileysStartupService extends ChannelStartupService {
}
const existingChat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
select: { id: true, name: true },
});
if (existingChat) {
const chatToInsert = {
remoteJid: received.key.remoteJid,
instanceId: this.instanceId,
name: received.pushName || '',
unreadMessages: 0,
};
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
try {
await this.prismaRepository.chat.update({
where: { id: existingChat.id },
data: { name: received.pushName },
});
} catch (error) {
console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`);
}
}
}
@@ -1243,7 +1291,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mime.getType(fileName).toString();
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
'Content-Type': mimetype,
@@ -1276,17 +1324,21 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.localWebhook.enabled) {
if (isMedia && this.localWebhook.webhookBase64) {
const buffer = await downloadMediaMessage(
{ key: received.key, message: received?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
try {
const buffer = await downloadMediaMessage(
{ key: received.key, message: received?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
} catch (error) {
this.logger.error(['Error converting media to base64', error?.message]);
}
}
}
@@ -1483,9 +1535,16 @@ export class BaileysStartupService extends ChannelStartupService {
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
try {
await this.prismaRepository.chat.update({
where: {
id: existingChat.id,
},
data: chatToInsert,
});
} catch (error) {
console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`);
}
}
}
}
@@ -1523,6 +1582,8 @@ export class BaileysStartupService extends ChannelStartupService {
private readonly labelHandle = {
[Events.LABELS_EDIT]: async (label: Label) => {
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
const labelsRepository = await this.prismaRepository.label.findMany({
where: { instanceId: this.instanceId },
});
@@ -1557,7 +1618,6 @@ export class BaileysStartupService extends ChannelStartupService {
create: labelData,
});
}
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
}
},
@@ -1565,26 +1625,18 @@ export class BaileysStartupService extends ChannelStartupService {
data: { association: LabelAssociation; type: 'remove' | 'add' },
database: Database,
) => {
this.logger.info(
`labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}`,
);
if (database.SAVE_DATA.CHATS) {
const chats = await this.prismaRepository.chat.findMany({
where: { instanceId: this.instanceId },
});
const chat = chats.find((c) => c.remoteJid === data.association.chatId);
if (chat) {
const labelsArray = Array.isArray(chat.labels) ? chat.labels.map((event) => String(event)) : [];
let labels = [...labelsArray];
const instanceId = this.instanceId;
const chatId = data.association.chatId;
const labelId = data.association.labelId;
if (data.type === 'remove') {
labels = labels.filter((label) => label !== data.association.labelId);
} else if (data.type === 'add') {
labels = [...labels, data.association.labelId];
}
await this.prismaRepository.chat.update({
where: { id: chat.id },
data: {
labels,
},
});
if (data.type === 'add') {
await this.addLabel(labelId, instanceId, chatId);
} else if (data.type === 'remove') {
await this.removeLabel(labelId, instanceId, chatId);
}
}
@@ -1762,7 +1814,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image');
@@ -1780,12 +1832,12 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async getStatus(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
return {
wuid: jid,
status: (await this.client.fetchStatus(jid))?.status,
status: (await this.client.fetchStatus(jid))[0]?.status,
};
} catch (error) {
return {
@@ -1796,7 +1848,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async fetchProfile(instanceName: string, number?: string) {
const jid = number ? this.createJid(number) : this.client?.user?.id;
const jid = number ? createJid(number) : this.client?.user?.id;
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
@@ -1852,7 +1904,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async offerCall({ number, isVideo, callDuration }: OfferCallDto) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
const call = await this.client.offerCall(jid, isVideo);
@@ -2111,6 +2163,7 @@ export class BaileysStartupService extends ChannelStartupService {
const cache = this.configService.get<CacheConf>('CACHE');
if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner');
else group = await this.getGroupMetadataCache(sender);
// group = await this.findGroup({ groupJid: sender }, 'inner');
} catch (error) {
throw new NotFoundException('Group not found');
}
@@ -2121,9 +2174,9 @@ export class BaileysStartupService extends ChannelStartupService {
if (options?.mentionsEveryOne) {
mentions = group.participants.map((participant) => participant.id);
} else if (options.mentioned?.length) {
} else if (options?.mentioned?.length) {
mentions = options.mentioned.map((mention) => {
const jid = this.createJid(mention);
const jid = createJid(mention);
if (isJidGroup(jid)) {
return null;
}
@@ -2205,7 +2258,7 @@ export class BaileysStartupService extends ChannelStartupService {
const { buffer, mediaType, fileName, size } = media;
const mimetype = mime.getType(fileName).toString();
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
@@ -2245,17 +2298,21 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.localWebhook.enabled) {
if (isMedia && this.localWebhook.webhookBase64) {
const buffer = await downloadMediaMessage(
{ key: messageRaw.key, message: messageRaw?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
try {
const buffer = await downloadMediaMessage(
{ key: messageRaw.key, message: messageRaw?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
} catch (error) {
this.logger.error(['Error converting media to base64', error?.message]);
}
}
}
@@ -2527,12 +2584,12 @@ export class BaileysStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
if (mediaMessage.mimetype) {
mimetype = mediaMessage.mimetype;
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
if (!mimetype && isURL(mediaMessage.media)) {
let config: any = {
@@ -3195,7 +3252,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
if (!contact.wuid) {
contact.wuid = this.createJid(contact.phoneNumber);
contact.wuid = createJid(contact.phoneNumber);
}
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';
@@ -3245,7 +3302,7 @@ export class BaileysStartupService extends ChannelStartupService {
};
data.numbers.forEach((number) => {
const jid = this.createJid(number);
const jid = createJid(number);
if (isJidGroup(jid)) {
jids.groups.push({ number, jid });
@@ -3438,7 +3495,7 @@ export class BaileysStartupService extends ChannelStartupService {
archive: data.archive,
lastMessages: [last_message],
},
this.createJid(number),
createJid(number),
);
return {
@@ -3475,7 +3532,7 @@ export class BaileysStartupService extends ChannelStartupService {
markRead: false,
lastMessages: [last_message],
},
this.createJid(number),
createJid(number),
);
return {
@@ -3497,25 +3554,31 @@ export class BaileysStartupService extends ChannelStartupService {
const messageId = response.message?.protocolMessage?.key?.id;
if (messageId) {
const isLogicalDeleted = configService.get<Database>('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE;
let message = await this.prismaRepository.message.findUnique({
where: { id: messageId },
let message = await this.prismaRepository.message.findFirst({
where: {
key: {
path: ['id'],
equals: messageId,
},
},
});
if (isLogicalDeleted) {
if (!message) return response;
const existingKey = typeof message?.key === 'object' && message.key !== null ? message.key : {};
message = await this.prismaRepository.message.update({
where: { id: messageId },
where: { id: message.id },
data: {
key: {
...existingKey,
deleted: true,
},
status: 'DELETED',
},
});
} else {
await this.prismaRepository.message.deleteMany({
where: {
id: messageId,
id: message.id,
},
});
}
@@ -3524,7 +3587,7 @@ export class BaileysStartupService extends ChannelStartupService {
instanceId: message.instanceId,
key: message.key,
messageType: message.messageType,
status: message.status,
status: 'DELETED',
source: message.source,
messageTimestamp: message.messageTimestamp,
pushName: message.pushName,
@@ -3587,7 +3650,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
const typeMessage = getContentType(msg.message);
const ext = mime.getExtension(mediaMessage?.['mimetype']);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);
const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`;
if (convertToMp4 && typeMessage === 'audioMessage') {
@@ -3680,7 +3743,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async fetchBusinessProfile(number: string): Promise<NumberBusiness> {
try {
const jid = number ? this.createJid(number) : this.instance.wuid;
const jid = number ? createJid(number) : this.instance.wuid;
const profile = await this.client.getBusinessProfile(jid);
@@ -3828,7 +3891,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async updateMessage(data: UpdateMessageDto) {
const jid = this.createJid(data.number);
const jid = createJid(data.number);
const options = await this.formatUpdateMessage(data);
@@ -3876,11 +3939,13 @@ export class BaileysStartupService extends ChannelStartupService {
try {
if (data.action === 'add') {
await this.client.addChatLabel(contact.jid, data.labelId);
await this.addLabel(data.labelId, this.instanceId, contact.jid);
return { numberJid: contact.jid, labelId: data.labelId, add: true };
}
if (data.action === 'remove') {
await this.client.removeChatLabel(contact.jid, data.labelId);
await this.removeLabel(data.labelId, this.instanceId, contact.jid);
return { numberJid: contact.jid, labelId: data.labelId, remove: true };
}
@@ -4116,7 +4181,7 @@ export class BaileysStartupService extends ChannelStartupService {
const inviteUrl = inviteCode.inviteUrl;
const numbers = id.numbers.map((number) => this.createJid(number));
const numbers = id.numbers.map((number) => createJid(number));
const description = id.description ?? '';
const msg = `${description}\n\n${inviteUrl}`;
@@ -4187,7 +4252,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async updateGParticipant(update: GroupUpdateParticipantDto) {
try {
const participants = update.participants.map((p) => this.createJid(p));
const participants = update.participants.map((p) => createJid(p));
const updateParticipants = await this.client.groupParticipantsUpdate(
update.groupJid,
participants,
@@ -4225,6 +4290,7 @@ export class BaileysStartupService extends ChannelStartupService {
throw new BadRequestException('Unable to leave the group', error.toString());
}
}
public async templateMessage() {
throw new Error('Method not available in the Baileys service');
}
@@ -4261,6 +4327,19 @@ export class BaileysStartupService extends ChannelStartupService {
delete messageRaw.message.documentWithCaptionMessage;
}
const quotedMessage = messageRaw?.contextInfo?.quotedMessage;
if (quotedMessage) {
if (quotedMessage.extendedTextMessage) {
quotedMessage.conversation = quotedMessage.extendedTextMessage.text;
delete quotedMessage.extendedTextMessage;
}
if (quotedMessage.documentWithCaptionMessage) {
quotedMessage.documentMessage = quotedMessage.documentWithCaptionMessage.message.documentMessage;
delete quotedMessage.documentWithCaptionMessage;
}
}
return messageRaw;
}
@@ -4328,4 +4407,133 @@ export class BaileysStartupService extends ChannelStartupService {
return unreadMessages;
}
private async addLabel(labelId: string, instanceId: string, chatId: string) {
const id = cuid();
await this.prismaRepository.$executeRawUnsafe(
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
DO
UPDATE
SET "labels" = (
SELECT to_jsonb(array_agg(DISTINCT elem))
FROM (
SELECT jsonb_array_elements_text("Chat"."labels") AS elem
UNION
SELECT $1::text AS elem
) sub
),
"updatedAt" = NOW();`,
labelId,
instanceId,
chatId,
id,
);
}
private async removeLabel(labelId: string, instanceId: string, chatId: string) {
const id = cuid();
await this.prismaRepository.$executeRawUnsafe(
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
DO
UPDATE
SET "labels" = COALESCE (
(
SELECT jsonb_agg(elem)
FROM jsonb_array_elements_text("Chat"."labels") AS elem
WHERE elem <> $1
),
'[]'::jsonb
),
"updatedAt" = NOW();`,
labelId,
instanceId,
chatId,
id,
);
}
public async baileysOnWhatsapp(jid: string) {
const response = await this.client.onWhatsApp(jid);
return response;
}
public async baileysProfilePictureUrl(jid: string, type: 'image' | 'preview', timeoutMs: number) {
const response = await this.client.profilePictureUrl(jid, type, timeoutMs);
return response;
}
public async baileysAssertSessions(jids: string[], force: boolean) {
const response = await this.client.assertSessions(jids, force);
return response;
}
public async baileysCreateParticipantNodes(jids: string[], message: proto.IMessage, extraAttrs: any) {
const response = await this.client.createParticipantNodes(jids, message, extraAttrs);
const convertedResponse = {
...response,
nodes: response.nodes.map((node: any) => ({
...node,
content: node.content?.map((c: any) => ({
...c,
content: c.content instanceof Uint8Array ? Buffer.from(c.content).toString('base64') : c.content,
})),
})),
};
return convertedResponse;
}
public async baileysSendNode(stanza: any) {
console.log('stanza', JSON.stringify(stanza));
const response = await this.client.sendNode(stanza);
return response;
}
public async baileysGetUSyncDevices(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) {
const response = await this.client.getUSyncDevices(jids, useCache, ignoreZeroDevices);
return response;
}
public async baileysGenerateMessageTag() {
const response = await this.client.generateMessageTag();
return response;
}
public async baileysSignalRepositoryDecryptMessage(jid: string, type: 'pkmsg' | 'msg', ciphertext: string) {
try {
const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
const response = await this.client.signalRepository.decryptMessage({
jid,
type,
ciphertext: ciphertextBuffer,
});
return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response;
} catch (error) {
this.logger.error('Error decrypting message:');
this.logger.error(error);
throw error;
}
}
public async baileysGetAuthState() {
const response = {
me: this.client.authState.creds.me,
account: this.client.authState.creds.account,
};
return response;
}
}

View File

@@ -184,7 +184,6 @@ export class ChatbotController {
public async findBotTrigger(
botRepository: any,
settingsRepository: any,
content: string,
instance: InstanceDto,
session?: IntegrationSession,
@@ -192,7 +191,7 @@ export class ChatbotController {
let findBot: null;
if (!session) {
findBot = await findBotByTrigger(botRepository, settingsRepository, content, instance.instanceId);
findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
if (!findBot) {
return;

View File

@@ -28,7 +28,7 @@ import dayjs from 'dayjs';
import FormData from 'form-data';
import Jimp from 'jimp';
import Long from 'long';
import mime from 'mime';
import mimeTypes from 'mime-types';
import path from 'path';
import { Readable } from 'stream';
@@ -704,7 +704,7 @@ export class ChatwootService {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
if (this.provider.conversationPending) {
if (this.provider.conversationPending && conversation.status !== 'open') {
if (conversation) {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
@@ -1066,7 +1066,7 @@ export class ChatwootService {
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
try {
const parsedMedia = path.parse(decodeURIComponent(media));
let mimeType = mime.getType(parsedMedia?.ext) || '';
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
let fileName = parsedMedia?.name + parsedMedia?.ext;
if (!mimeType) {
@@ -1958,7 +1958,7 @@ export class ChatwootService {
}
if (!nameFile) {
nameFile = `${Math.random().toString(36).substring(7)}.${mime.getExtension(downloadBase64.mimetype) || ''}`;
nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`;
}
const fileData = Buffer.from(downloadBase64.base64, 'base64');
@@ -1970,11 +1970,21 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
if (!body.key.fromMe) {
content = `**${participantName}:**\n\n${bodyMessage}`;
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
} else {
content = `${bodyMessage}`;
}
@@ -2047,8 +2057,8 @@ export class ChatwootService {
if (isAdsMessage) {
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
const extension = mime.getExtension(imgBuffer.headers['content-type']);
const mimeType = extension && mime.getType(extension);
const extension = mimeTypes.extension(imgBuffer.headers['content-type']);
const mimeType = extension && mimeTypes.lookup(extension);
if (!mimeType) {
this.logger.warn('mimetype of Ads message not found');
@@ -2056,7 +2066,7 @@ export class ChatwootService {
}
const random = Math.random().toString(36).substring(7);
const nameFile = `${random}.${mime.getExtension(mimeType)}`;
const nameFile = `${random}.${mimeTypes.extension(mimeType)}`;
const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData);
@@ -2099,11 +2109,21 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
if (!body.key.fromMe) {
content = `**${participantName}**\n\n${bodyMessage}`;
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
} else {
content = `${bodyMessage}`;
}

View File

@@ -756,13 +756,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as DifyModel;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({

View File

@@ -224,63 +224,43 @@ export class DifyService {
headers: {
Authorization: `Bearer ${dify.apiKey}`,
},
responseType: 'stream',
});
let conversationId;
let answer = '';
const stream = response.data;
const reader = new Readable().wrap(stream);
const data = response.data.replaceAll('data: ', '');
reader.on('data', (chunk) => {
const data = chunk.toString().replace(/data:\s*/g, '');
const events = data.split('\n').filter((line) => line.trim() !== '');
if (data.trim() === '' || !data.startsWith('{')) {
return;
}
for (const eventString of events) {
if (eventString.trim().startsWith('{')) {
const event = JSON.parse(eventString);
try {
const events = data.split('\n').filter((line) => line.trim() !== '');
for (const eventString of events) {
if (eventString.trim().startsWith('{')) {
const event = JSON.parse(eventString);
if (event?.event === 'agent_message') {
console.log('event:', event);
conversationId = conversationId ?? event?.conversation_id;
answer += event?.answer;
}
}
if (event?.event === 'agent_message') {
console.log('event:', event);
conversationId = conversationId ?? event?.conversation_id;
answer += event?.answer;
}
} catch (error) {
console.error('Error parsing stream data:', error);
}
});
}
reader.on('end', async () => {
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
if (instance.integration === Integration.WHATSAPP_BAILEYS)
await instance.client.sendPresenceUpdate('paused', remoteJid);
const message = answer;
const message = answer;
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.sendMessageWhatsApp(instance, remoteJid, message, settings);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
sessionId: conversationId,
},
});
});
reader.on('error', (error) => {
console.error('Error reading stream:', error);
await this.prismaRepository.integrationSession.update({
where: {
id: session.id,
},
data: {
status: 'opened',
awaitUser: true,
sessionId: conversationId,
},
});
return;
@@ -428,8 +408,8 @@ export class DifyService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {

View File

@@ -728,13 +728,7 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as EvolutionBot;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvolutionBot;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({

View File

@@ -190,8 +190,8 @@ export class EvolutionBotService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -274,8 +274,8 @@ export class EvolutionBotService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -728,13 +728,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as Flowise;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as Flowise;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({

View File

@@ -189,8 +189,8 @@ export class FlowiseService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -273,8 +273,8 @@ export class FlowiseService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -965,13 +965,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as OpenaiBot;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as OpenaiBot;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({

View File

@@ -234,8 +234,8 @@ export class OpenaiService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -318,8 +318,8 @@ export class OpenaiService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -943,13 +943,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as TypebotModel;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as TypebotModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({

View File

@@ -13,6 +13,7 @@ export type EmitData = {
sender: string;
apiKey?: string;
local?: boolean;
integration?: string[];
};
export interface EventControllerInterface {
@@ -23,7 +24,7 @@ export interface EventControllerInterface {
export class EventController {
public prismaRepository: PrismaRepository;
private waMonitor: WAMonitoringService;
protected waMonitor: WAMonitoringService;
private integrationStatus: boolean;
private integrationName: string;

View File

@@ -99,6 +99,7 @@ export class EventManager {
sender: string;
apiKey?: string;
local?: boolean;
integration?: string[];
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);

View File

@@ -120,7 +120,11 @@ export class PusherController extends EventController implements EventController
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('pusher')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -73,7 +73,12 @@ export class RabbitmqController extends EventController implements EventControll
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('rabbitmq')) {
return;
}
if (!this.status) {
return;
}
@@ -82,6 +87,7 @@ export class RabbitmqController extends EventController implements EventControll
const rabbitmqLocal = instanceRabbitmq?.events;
const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
const rabbitmqEvents = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
@@ -154,7 +160,9 @@ export class RabbitmqController extends EventController implements EventControll
autoDelete: false,
});
const queueName = event;
const queueName = prefixKey
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: event.replace(/_/g, '.').toLowerCase();
await this.amqpChannel.assertQueue(queueName, {
durable: true,
@@ -190,6 +198,7 @@ export class RabbitmqController extends EventController implements EventControll
const rabbitmqExchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME;
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
const prefixKey = configService.get<Rabbitmq>('RABBITMQ').PREFIX_KEY;
if (!events) {
this.logger.warn('No events to initialize on AMQP');
@@ -202,7 +211,10 @@ export class RabbitmqController extends EventController implements EventControll
eventKeys.forEach((event) => {
if (events[event] === false) return;
const queueName = `${event.replace(/_/g, '.').toLowerCase()}`;
const queueName =
prefixKey !== ''
? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}`
: `${event.replace(/_/g, '.').toLowerCase()}`;
const exchangeName = rabbitmqExchangeName;
this.amqpChannel.assertExchange(exchangeName, 'topic', {

View File

@@ -54,7 +54,12 @@ export class SqsController extends EventController implements EventControllerInt
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('sqs')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -5,7 +5,7 @@ import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import axios from 'axios';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
@@ -64,13 +64,14 @@ export class WebhookController extends EventController implements EventControlle
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
if (!instance || !instance?.enabled) {
if (integration && !integration.includes('webhook')) {
return;
}
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
@@ -82,14 +83,14 @@ export class WebhookController extends EventController implements EventControlle
event,
instance: instanceName,
data,
destination: instance?.url,
destination: instance?.url || `${webhookConfig.GLOBAL.URL}/${transformedWe}`,
date_time: dateTime,
sender,
server_url: serverUrl,
apikey: apiKey,
};
if (local) {
if (local && instance?.enabled) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string;
@@ -116,12 +117,12 @@ export class WebhookController extends EventController implements EventControlle
headers: webhookHeaders as Record<string, string> | undefined,
});
await httpService.post('', webhookData);
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook`,
message: error?.message,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -157,12 +158,18 @@ export class WebhookController extends EventController implements EventControlle
if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', webhookData);
await this.retryWebhookRequest(
httpService,
webhookData,
`${origin}.sendData-Webhook-Global`,
globalURL,
serverUrl,
);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook-Global`,
message: error?.message,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -176,4 +183,51 @@ export class WebhookController extends EventController implements EventControlle
}
}
}
private async retryWebhookRequest(
httpService: AxiosInstance,
webhookData: any,
origin: string,
baseURL: string,
serverUrl: string,
maxRetries = 10,
delaySeconds = 30,
): Promise<void> {
let attempts = 0;
while (attempts < maxRetries) {
try {
await httpService.post('', webhookData);
if (attempts > 0) {
this.logger.log({
local: `${origin}`,
message: `Sucesso no envio após ${attempts + 1} tentativas`,
url: baseURL,
});
}
return;
} catch (error) {
attempts++;
this.logger.error({
local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: baseURL,
server_url: serverUrl,
});
if (attempts === maxRetries) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
}
}
}
}

View File

@@ -8,7 +8,10 @@ import { instanceSchema, webhookSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class WebhookRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {

View File

@@ -35,6 +35,16 @@ export class WebsocketController extends EventController implements EventControl
socket.on('disconnect', () => {
this.logger.info('User disconnected');
});
socket.on('sendNode', async (data) => {
try {
await this.waMonitor.waInstances[data.instanceId].baileysSendNode(data.stanza);
this.logger.info('Node sent successfully');
} catch (error) {
this.logger.error('Error sending node:');
this.logger.error(error);
}
});
});
this.logger.info('Socket.io initialized');
@@ -65,7 +75,12 @@ export class WebsocketController extends EventController implements EventControl
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('websocket')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -8,7 +8,7 @@ import { StorageRouter } from '@api/integrations/storage/storage.router';
import { configService } from '@config/env.config';
import { Router } from 'express';
import fs from 'fs';
import mime from 'mime';
import mimeTypes from 'mime-types';
import path from 'path';
import { CallRouter } from './call.router';
@@ -49,7 +49,7 @@ router.get('/assets/*', (req, res) => {
const filePath = path.join(basePath, 'assets/', fileName);
if (fs.existsSync(filePath)) {
res.set('Content-Type', mime.getType(filePath) || 'text/css');
res.set('Content-Type', mimeTypes.lookup(filePath) || 'text/css');
res.send(fs.readFileSync(filePath));
} else {
res.status(404).send('File not found');
@@ -87,7 +87,7 @@ router
.use('/settings', new SettingsRouter(...guards).router)
.use('/proxy', new ProxyRouter(...guards).router)
.use('/label', new LabelRouter(...guards).router)
.use('', new ChannelRouter(configService).router)
.use('', new ChannelRouter(configService, ...guards).router)
.use('', new EventRouter(configService, ...guards).router)
.use('', new ChatbotRouter(...guards).router)
.use('', new StorageRouter(...guards).router);

View File

@@ -8,10 +8,14 @@ import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class InstanceRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
super();
this.router
.post('/create', ...guards, async (req, res) => {
console.log('create instance', req.body);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,

View File

@@ -9,7 +9,10 @@ import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class TemplateRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {

View File

@@ -15,6 +15,7 @@ import { TemplateController } from './controllers/template.controller';
import { ChannelController } from './integrations/channel/channel.controller';
import { EvolutionController } from './integrations/channel/evolution/evolution.controller';
import { MetaController } from './integrations/channel/meta/meta.controller';
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
import { ChatbotController } from './integrations/chatbot/chatbot.controller';
import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller';
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
@@ -107,7 +108,7 @@ export const channelController = new ChannelController(prismaRepository, waMonit
// channels
export const evolutionController = new EvolutionController(prismaRepository, waMonitor);
export const metaController = new MetaController(prismaRepository, waMonitor);
export const baileysController = new BaileysController(waMonitor);
// chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);

View File

@@ -12,7 +12,8 @@ import { Events, wa } from '@api/types/wa.types';
import { Auth, Chatwoot, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import { Contact, Message } from '@prisma/client';
import { Contact, Message, Prisma } from '@prisma/client';
import { createJid } from '@utils/createJid';
import { WASocket } from 'baileys';
import { isArray } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
@@ -151,6 +152,7 @@ export class ChannelStartupService {
this.localSettings.readMessages = data?.readMessages;
this.localSettings.readStatus = data?.readStatus;
this.localSettings.syncFullHistory = data?.syncFullHistory;
this.localSettings.wavoipToken = data?.wavoipToken;
}
public async setSettings(data: SettingsDto) {
@@ -166,6 +168,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
},
create: {
rejectCall: data.rejectCall,
@@ -175,6 +178,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
instanceId: this.instanceId,
},
});
@@ -186,6 +190,12 @@ export class ChannelStartupService {
this.localSettings.readMessages = data?.readMessages;
this.localSettings.readStatus = data?.readStatus;
this.localSettings.syncFullHistory = data?.syncFullHistory;
this.localSettings.wavoipToken = data?.wavoipToken;
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
this.client.ws.close();
this.client.ws.connect();
}
}
public async findSettings() {
@@ -207,6 +217,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
};
}
@@ -419,7 +430,7 @@ export class ChannelStartupService {
return data;
}
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) {
public async sendDataWebhook<T = any>(event: Events, data: T, local = true, integration?: string[]) {
const serverUrl = this.configService.get<HttpServer>('SERVER').URL;
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
@@ -439,6 +450,7 @@ export class ChannelStartupService {
sender: this.wuid,
apiKey: expose && instanceApikey ? instanceApikey : null,
local,
integration,
});
}
@@ -476,47 +488,11 @@ export class ChannelStartupService {
}
}
public createJid(number: string): string {
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
return number;
}
if (number.includes('@broadcast')) {
return number;
}
number = number
?.replace(/\s/g, '')
.replace(/\+/g, '')
.replace(/\(/g, '')
.replace(/\)/g, '')
.split(':')[0]
.split('@')[0];
if (number.includes('-') && number.length >= 24) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = number.replace(/\D/g, '');
if (number.length >= 18) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = this.formatMXOrARNumber(number);
number = this.formatBRNumber(number);
return `${number}@s.whatsapp.net`;
}
public async fetchContacts(query: Query<Contact>) {
const remoteJid = query?.where?.remoteJid
? query?.where?.remoteJid.includes('@')
? query.where?.remoteJid
: this.createJid(query.where?.remoteJid)
: createJid(query.where?.remoteJid)
: null;
const where = {
@@ -532,6 +508,64 @@ export class ChannelStartupService {
});
}
public cleanMessageData(message: any) {
if (!message) return message;
const cleanedMessage = { ...message };
const mediaUrl = cleanedMessage.message.mediaUrl;
delete cleanedMessage.message.base64;
if (cleanedMessage.message) {
// Limpa imageMessage
if (cleanedMessage.message.imageMessage) {
cleanedMessage.message.imageMessage = {
caption: cleanedMessage.message.imageMessage.caption,
};
}
// Limpa videoMessage
if (cleanedMessage.message.videoMessage) {
cleanedMessage.message.videoMessage = {
caption: cleanedMessage.message.videoMessage.caption,
};
}
// Limpa audioMessage
if (cleanedMessage.message.audioMessage) {
cleanedMessage.message.audioMessage = {
seconds: cleanedMessage.message.audioMessage.seconds,
};
}
// Limpa stickerMessage
if (cleanedMessage.message.stickerMessage) {
cleanedMessage.message.stickerMessage = {};
}
// Limpa documentMessage
if (cleanedMessage.message.documentMessage) {
cleanedMessage.message.documentMessage = {
caption: cleanedMessage.message.documentMessage.caption,
name: cleanedMessage.message.documentMessage.name,
};
}
// Limpa documentWithCaptionMessage
if (cleanedMessage.message.documentWithCaptionMessage) {
cleanedMessage.message.documentWithCaptionMessage = {
caption: cleanedMessage.message.documentWithCaptionMessage.caption,
name: cleanedMessage.message.documentWithCaptionMessage.name,
};
}
}
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
return cleanedMessage;
}
public async fetchMessages(query: Query<Message>) {
const keyFilters = query?.where?.key as {
id?: string;
@@ -540,12 +574,23 @@ export class ChannelStartupService {
participants?: string;
};
const timestampFilter = {};
if (query?.where?.messageTimestamp) {
if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) {
timestampFilter['messageTimestamp'] = {
gte: Math.floor(new Date(query.where.messageTimestamp['gte']).getTime() / 1000),
lte: Math.floor(new Date(query.where.messageTimestamp['lte']).getTime() / 1000),
};
}
}
const count = await this.prismaRepository.message.count({
where: {
instanceId: this.instanceId,
id: query?.where?.id,
source: query?.where?.source,
messageType: query?.where?.messageType,
...timestampFilter,
AND: [
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
@@ -569,6 +614,7 @@ export class ChannelStartupService {
id: query?.where?.id,
source: query?.where?.source,
messageType: query?.where?.messageType,
...timestampFilter,
AND: [
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
@@ -625,113 +671,103 @@ export class ChannelStartupService {
const remoteJid = query?.where?.remoteJid
? query?.where?.remoteJid.includes('@')
? query.where?.remoteJid
: this.createJid(query.where?.remoteJid)
: createJid(query.where?.remoteJid)
: null;
let results = [];
const where = {
instanceId: this.instanceId,
};
if (!remoteJid) {
results = await this.prismaRepository.$queryRaw`
SELECT
"Chat"."id",
"Chat"."remoteJid",
"Chat"."name",
"Chat"."labels",
"Chat"."createdAt",
"Chat"."updatedAt",
"Contact"."pushName",
"Contact"."profilePicUrl",
"Chat"."unreadMessages",
(ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id,
(ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key,
(ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name,
(ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant,
(ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type,
(ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message,
(ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info,
(ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source,
(ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp,
(ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id,
(ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id,
(ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status
FROM "Chat"
LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid"
LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid"
WHERE
"Chat"."instanceId" = ${this.instanceId}
GROUP BY
"Chat"."id",
"Chat"."remoteJid",
"Contact"."id"
ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC;
`;
} else {
results = await this.prismaRepository.$queryRaw`
SELECT
"Chat"."id",
"Chat"."remoteJid",
"Chat"."name",
"Chat"."labels",
"Chat"."createdAt",
"Chat"."updatedAt",
"Contact"."pushName",
"Contact"."profilePicUrl",
"Chat"."unreadMessages",
(ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id,
(ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key,
(ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name,
(ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant,
(ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type,
(ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message,
(ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info,
(ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source,
(ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp,
(ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id,
(ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id,
(ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status
FROM "Chat"
LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid"
LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid"
WHERE
"Chat"."instanceId" = ${this.instanceId} AND "Chat"."remoteJid" = ${remoteJid} and "Message"."messageType" != 'reactionMessage'
GROUP BY
"Chat"."id",
"Chat"."remoteJid",
"Contact"."id"
ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC;
`;
if (remoteJid) {
where['remoteJid'] = remoteJid;
}
const timestampFilter =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
const results = await this.prismaRepository.$queryRaw`
WITH rankedMessages AS (
SELECT DISTINCT ON ("Contact"."remoteJid")
"Contact"."id",
"Contact"."remoteJid",
"Contact"."pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
CASE
WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true
ELSE false
END as "windowActive",
"Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key,
"Message"."pushName" AS lastMessagePushName,
"Message"."participant" AS lastMessageParticipant,
"Message"."messageType" AS lastMessageMessageType,
"Message"."message" AS lastMessageMessage,
"Message"."contextInfo" AS lastMessageContextInfo,
"Message"."source" AS lastMessageSource,
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
"Message"."instanceId" AS lastMessageInstanceId,
"Message"."sessionId" AS lastMessageSessionId,
"Message"."status" AS lastMessageStatus
FROM "Contact"
INNER JOIN "Message" ON "Message"."key"->>'remoteJid' = "Contact"."remoteJid"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Contact"."remoteJid"
AND "Chat"."instanceId" = "Contact"."instanceId"
WHERE
"Contact"."instanceId" = ${this.instanceId}
AND "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Contact"."remoteJid" = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY
"Contact"."remoteJid",
"Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST;
`;
if (results && isArray(results) && results.length > 0) {
return results.map((chat) => {
const mappedResults = results.map((contact) => {
const lastMessage = contact.lastMessageId
? {
id: contact.lastMessageId,
key: contact.lastMessageKey,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
}
: undefined;
return {
id: chat.id,
remoteJid: chat.remoteJid,
name: chat.name,
labels: chat.labels,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
pushName: chat.pushName,
profilePicUrl: chat.profilePicUrl,
unreadMessages: chat.unreadMessages,
lastMessage: chat.last_message_id
? {
id: chat.last_message_id,
key: chat.last_message_key,
pushName: chat.last_message_push_name,
participant: chat.last_message_participant,
messageType: chat.last_message_message_type,
message: chat.last_message_message,
contextInfo: chat.last_message_context_info,
source: chat.last_message_source,
messageTimestamp: chat.last_message_message_timestamp,
instanceId: chat.last_message_instance_id,
sessionId: chat.last_message_session_id,
status: chat.last_message_status,
}
: undefined,
id: contact.id,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
};
});
return mappedResults;
}
return [];

View File

@@ -42,20 +42,23 @@ export class WAMonitoringService {
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
setTimeout(async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
setTimeout(
async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
}
}, 1000 * 60 * time);
},
1000 * 60 * time,
);
}
}
@@ -218,8 +221,11 @@ export class WAMonitoringService {
data: {
id: data.instanceId,
name: data.instanceName,
ownerJid: data.ownerJid,
profileName: data.profileName,
profilePicUrl: data.profilePicUrl,
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : data.status ?? 'open',
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: data.hash,

View File

@@ -85,6 +85,7 @@ export declare namespace wa {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
};
export type LocalEvent = {