Merge branch 'develop' into main

This commit is contained in:
Davidson Gomes
2025-05-13 06:28:07 -03:00
committed by GitHub
55 changed files with 2372 additions and 955 deletions

View File

@@ -0,0 +1,15 @@
import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BusinessController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
}
public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
}
}

View File

@@ -170,6 +170,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},
@@ -258,6 +261,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},

View File

@@ -18,6 +18,14 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { BadRequestException } from '@exceptions';
import { isBase64, isURL } from 'class-validator';
function isEmoji(str: string) {
if (str === '') return true;
const emojiRegex =
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
return emojiRegex.test(str);
}
export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {}
@@ -81,8 +89,8 @@ export class SendMessageController {
}
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
throw new BadRequestException('"reaction" must be an emoji');
if (!isEmoji(data.reaction)) {
throw new BadRequestException('Reaction must be a single emoji or empty string');
}
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
}

View File

@@ -0,0 +1,14 @@
export class NumberDto {
number: string;
}
export class getCatalogDto {
number?: string;
limit?: number;
cursor?: string;
}
export class getCollectionsDto {
number?: string;
limit?: number;
}

View File

@@ -44,6 +44,7 @@ export class Metadata {
mentionsEveryOne?: boolean;
mentioned?: string[];
encoding?: boolean;
notConvertSticker?: boolean;
}
export class SendTextDto extends Metadata {

View File

@@ -206,6 +206,20 @@ export class BusinessStartupService extends ChannelStartupService {
return content;
}
private messageLocationJson(received: any) {
const message = received.messages[0];
let content: any = {
locationMessage: {
degreesLatitude: message.location.latitude,
degreesLongitude: message.location.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
return content;
}
private messageContactsJson(received: any) {
const message = received.messages[0];
let content: any = {};
@@ -283,6 +297,9 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template':
messageType = 'conversation';
break;
case 'location':
messageType = 'locationMessage';
break;
default:
messageType = 'conversation';
break;
@@ -438,6 +455,17 @@ export class BusinessStartupService extends ChannelStartupService {
source: 'unknown',
instanceId: this.instanceId,
};
} else if (received?.messages[0].location) {
messageRaw = {
key,
pushName,
message: this.messageLocationJson(received),
contextInfo: this.messageLocationJson(received)?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else {
messageRaw = {
key,
@@ -724,7 +752,6 @@ export class BusinessStartupService extends ChannelStartupService {
try {
let quoted: any;
let webhookUrl: any;
const linkPreview = options?.linkPreview != false ? undefined : false;
if (options?.quoted) {
const m = options?.quoted;
@@ -792,7 +819,7 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
text: {
body: message['conversation'],
preview_url: linkPreview,
preview_url: Boolean(options?.linkPreview),
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;
@@ -800,6 +827,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
const isVideo = message['mimetype']?.startsWith('video/');
content = {
messaging_product: 'whatsapp',
@@ -808,8 +836,8 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
preview_url: Boolean(options?.linkPreview),
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }),
caption: message['caption'],
},
};
@@ -977,8 +1005,10 @@ export class BusinessStartupService extends ChannelStartupService {
private async getIdMedia(mediaMessage: any) {
const formData = new FormData();
const media = mediaMessage.media || mediaMessage.audio;
if (!media) throw new Error('Media or audio not found');
const fileStream = createReadStream(mediaMessage.media);
const fileStream = createReadStream(media);
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype);
@@ -1079,7 +1109,7 @@ export class BusinessStartupService extends ChannelStartupService {
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
audio,
};
if (isURL(audio)) {
@@ -1101,15 +1131,7 @@ export class BusinessStartupService extends ChannelStartupService {
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data };
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');
}
if (file) mediaData.audio = file.buffer.toString('base64');
const message = await this.processAudio(mediaData.audio, data.number);

View File

@@ -1,3 +1,4 @@
import { getCollectionsDto } from '@api/dto/business.dto';
import { OfferCallDto } from '@api/dto/call.dto';
import {
ArchiveChatDto,
@@ -91,6 +92,7 @@ import makeWASocket, {
BufferedEventData,
BufferJSON,
CacheStore,
CatalogCollection,
Chat,
ConnectionState,
Contact,
@@ -100,6 +102,7 @@ import makeWASocket, {
fetchLatestBaileysVersion,
generateWAMessageFromContent,
getAggregateVotesInPollMessage,
GetCatalogOptions,
getContentType,
getDevice,
GroupMetadata,
@@ -113,6 +116,7 @@ import makeWASocket, {
MiscMessageGenerationOptions,
ParticipantAction,
prepareWAMessageMedia,
Product,
proto,
UserFacingSocketConfig,
WABrowserDescription,
@@ -226,7 +230,10 @@ export class BaileysStartupService extends ChannelStartupService {
private authStateProvider: AuthStateProvider;
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache({
stdTTL: 300000,
useClones: false
});
private endSession = false;
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
@@ -1128,38 +1135,73 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) {
const editedMessage =
received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage;
if (editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
const editedMessage =
received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
if (received.message?.protocolMessage?.editedMessage && editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const oldMessage = await this.getMessage(editedMessage.key, true);
if ((oldMessage as any)?.id) {
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
? editedMessage.timestampMs?.toNumber()
: (editedMessage.timestampMs as number);
await this.prismaRepository.message.update({
where: { id: (oldMessage as any).id },
data: {
message: editedMessage.editedMessage as any,
messageTimestamp: editedMessageTimestamp,
status: 'EDITED',
},
});
await this.prismaRepository.messageUpdate.create({
data: {
fromMe: editedMessage.key.fromMe,
keyId: editedMessage.key.id,
remoteJid: editedMessage.key.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
messageId: (oldMessage as any).id,
},
});
}
}
if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
// if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
// this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
await this.baileysCache.set(received.key.id, {
message: received,
retry: 0,
});
// await this.baileysCache.set(received.key.id, {
// message: received,
// retry: 0,
// });
// continue;
// }
// const retryCache = (await this.baileysCache.get(received.key.id)) || null;
// if (retryCache) {
// this.logger.info('Recovered message lost');
// await this.baileysCache.delete(received.key.id);
// }
// Cache to avoid duplicate messages
const messageKey = `${this.instance.id}_${received.key.id}`;
const cached = await this.baileysCache.get(messageKey);
if (cached && !editedMessage) {
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
continue;
}
const retryCache = (await this.baileysCache.get(received.key.id)) || null;
if (retryCache) {
this.logger.info('Recovered message lost');
await this.baileysCache.delete(received.key.id);
}
await this.baileysCache.set(messageKey, true, 30 * 60);
if (
(type !== 'notify' && type !== 'append') ||
@@ -1186,7 +1228,9 @@ export class BaileysStartupService extends ChannelStartupService {
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
received.pushName.trim().length > 0 &&
!received.key.fromMe &&
!received.key.remoteJid.includes('@g.us')
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
@@ -1292,7 +1336,12 @@ export class BaileysStartupService extends ChannelStartupService {
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
'Content-Type': mimetype,
});
@@ -1422,6 +1471,17 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey);
if (cached) {
this.logger.info(`Message duplicated ignored: ${key.id}`);
continue;
}
await this.baileysCache.set(updateKey, true, 30 * 60);
if (status[update.status] === 'READ' && key.fromMe) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
@@ -1529,7 +1589,6 @@ export class BaileysStartupService extends ChannelStartupService {
const chatToInsert = {
remoteJid: message.remoteJid,
instanceId: this.instanceId,
name: message.pushName || '',
unreadMessages: 0,
};
@@ -2654,9 +2713,6 @@ export class BaileysStartupService extends ChannelStartupService {
prepareMedia[mediaType].fileName = mediaMessage.fileName;
if (mediaMessage.mediatype === 'video') {
prepareMedia[mediaType].jpegThumbnail = Uint8Array.from(
readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')),
);
prepareMedia[mediaType].gifPlayback = false;
}
@@ -2703,21 +2759,43 @@ export class BaileysStartupService extends ChannelStartupService {
imageBuffer = Buffer.from(response.data, 'binary');
}
const webpBuffer = await sharp(imageBuffer).webp().toBuffer();
const isAnimated = this.isAnimated(image, imageBuffer);
return webpBuffer;
if (isAnimated) {
return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer();
} else {
return await sharp(imageBuffer).webp().toBuffer();
}
} catch (error) {
console.error('Erro ao converter a imagem para WebP:', error);
throw error;
}
}
private isAnimatedWebp(buffer: Buffer): boolean {
if (buffer.length < 12) return false;
return buffer.indexOf(Buffer.from('ANIM')) !== -1;
}
private isAnimated(image: string, buffer: Buffer): boolean {
const lowerCaseImage = image.toLowerCase();
if (lowerCaseImage.includes('.gif')) return true;
if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer);
return false;
}
public async mediaSticker(data: SendStickerDto, file?: any) {
const mediaData: SendStickerDto = { ...data };
if (file) mediaData.sticker = file.buffer.toString('base64');
const convert = await this.convertToWebP(data.sticker);
const convert = data?.notConvertSticker
? Buffer.from(data.sticker, 'base64')
: await this.convertToWebP(data.sticker);
const gifPlayback = data.sticker.includes('.gif');
const result = await this.sendMessageWithTyping(
data.number,
@@ -2923,7 +3001,29 @@ export class BaileysStartupService extends ChannelStartupService {
.noVideo()
.audioCodec('libopus')
.addOutputOptions('-avoid_negative_ts make_zero')
.audioBitrate('128k')
.audioFrequency(48000)
.audioChannels(1)
.outputOptions([
'-write_xing',
'0',
'-compression_level',
'10',
'-application',
'voip',
'-fflags',
'+bitexact',
'-flags',
'+bitexact',
'-id3v2_version',
'0',
'-map_metadata',
'-1',
'-map_chapters',
'-1',
'-write_bext',
'0',
])
.pipe(outputAudioStream, { end: true })
.on('error', function (error) {
console.log('error', error);
@@ -3573,6 +3673,18 @@ export class BaileysStartupService extends ChannelStartupService {
status: 'DELETED',
},
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: response.key.remoteJid,
fromMe: response.key.fromMe,
participant: response.key?.remoteJid,
status: 'DELETED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({
data: messageUpdate,
});
} else {
await this.prismaRepository.message.deleteMany({
where: {
@@ -3899,13 +4011,84 @@ export class BaileysStartupService extends ChannelStartupService {
}
try {
return await this.client.sendMessage(jid, {
const oldMessage: any = await this.getMessage(data.key, true);
if (!oldMessage) throw new NotFoundException('Message not found');
if (oldMessage?.key?.remoteJid !== jid) {
throw new BadRequestException('RemoteJid does not match');
}
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
// 15 minutes in milliseconds
throw new BadRequestException('Message is older than 15 minutes');
}
const messageSent = await this.client.sendMessage(jid, {
...(options as any),
edit: data.key,
});
if (messageSent) {
const editedMessage =
messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
if (editedMessage) {
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'send.message.update',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
const messageId = messageSent.message?.protocolMessage?.key?.id;
if (messageId) {
let message = await this.prismaRepository.message.findFirst({
where: {
key: {
path: ['id'],
equals: messageId,
},
},
});
if (!message) throw new NotFoundException('Message not found');
if (!(message.key.valueOf() as any).fromMe) {
new BadRequestException('You cannot edit others messages');
}
if ((message.key.valueOf() as any)?.deleted) {
new BadRequestException('You cannot edit deleted messages');
}
if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') {
oldMessage.message.conversation = data.text;
} else {
oldMessage.message[oldMessage.messageType].caption = data.text;
}
message = await this.prismaRepository.message.update({
where: { id: message.id },
data: {
message: oldMessage.message,
status: 'EDITED',
messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds
},
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: messageSent.key.remoteJid,
fromMe: messageSent.key.fromMe,
participant: messageSent.key?.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({
data: messageUpdate,
});
}
}
}
return messageSent;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.toString());
throw error;
}
}
@@ -4534,4 +4717,137 @@ export class BaileysStartupService extends ChannelStartupService {
return response;
}
//Business Controller
public async fetchCatalog(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit || 10;
const cursor = null;
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor });
let nextPageCursor = catalog.nextPageCursor;
let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
let pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
let productsCatalog = catalog.products || [];
let countLoops = 0;
while (fetcherHasMore && countLoops < 4) {
catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor });
nextPageCursor = catalog.nextPageCursor;
nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
productsCatalog = [...productsCatalog, ...catalog.products];
countLoops++;
}
return {
wuid: info?.jid || jid,
numberExists: info?.exists,
isBusiness: business.isBusiness,
catalogLength: productsCatalog.length,
catalog: productsCatalog,
};
} catch (error) {
console.log(error);
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCatalog({
jid,
limit,
cursor,
}: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor });
if (!catalog) {
return {
products: undefined,
nextPageCursor: undefined,
};
}
return catalog;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
public async fetchCollections(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque)
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
const collections = await this.getCollections(info?.jid, limit);
return {
wuid: info?.jid || jid,
name: info?.name,
numberExists: info?.exists,
isBusiness: business.isBusiness,
collectionsLength: collections?.length,
collections: collections,
};
} catch (error) {
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCollections(jid?: string | undefined, limit?: number): Promise<CatalogCollection[]> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const result = await this.client.getCollections(jid, limit);
if (!result) {
return [
{
id: undefined,
name: undefined,
products: [],
status: undefined,
},
];
}
return result.collections;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
}

View File

@@ -698,34 +698,33 @@ export class ChatwootService {
return null;
}
if (contactConversations.payload.length) {
let conversation: any;
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
);
if (inboxConversation) {
if (this.provider.reopenConversation) {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
if (this.provider.conversationPending && conversation.status !== 'open') {
if (conversation) {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: conversation.id,
data: {
status: 'pending',
},
});
}
if (this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
}
} else {
conversation = contactConversations.payload.find(
inboxConversation = contactConversations.payload.find(
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (conversation) {
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
}
@@ -1106,7 +1105,7 @@ export class ChatwootService {
sendTelemetry('/message/sendWhatsAppAudio');
const messageSent = await waInstance?.audioWhatsapp(data, true);
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
return messageSent;
}
@@ -1653,7 +1652,7 @@ export class ChatwootService {
stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
@@ -1898,7 +1897,7 @@ export class ChatwootService {
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage;
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
return;
}
@@ -2199,7 +2198,7 @@ export class ChatwootService {
}
}
if (event === 'messages.edit') {
if (event === 'messages.edit' || event === 'send.message.update') {
const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;

View File

@@ -1018,10 +1018,6 @@ export class TypebotController extends ChatbotController implements ChatbotContr
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.typebotService.processTypebot(

View File

@@ -741,6 +741,10 @@ export class TypebotService {
}
}
if (session && !session.awaitUser) {
return;
}
if (session && session.status !== 'opened') {
return;
}

View File

@@ -132,6 +132,7 @@ export class EventController {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@@ -151,5 +152,8 @@ export class EventController {
'TYPEBOT_CHANGE_STATUS',
'REMOVE_INSTANCE',
'LOGOUT_INSTANCE',
'INSTANCE_CREATE',
'INSTANCE_DELETE',
'STATUS_INSTANCE',
];
}

View File

@@ -26,6 +26,11 @@ export class EventDto {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;
@@ -63,6 +68,11 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;

View File

@@ -1,3 +1,4 @@
import { NatsController } from '@api/integrations/event/nats/nats.controller';
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
@@ -13,6 +14,7 @@ export class EventManager {
private websocketController: WebsocketController;
private webhookController: WebhookController;
private rabbitmqController: RabbitmqController;
private natsController: NatsController;
private sqsController: SqsController;
private pusherController: PusherController;
@@ -23,6 +25,7 @@ export class EventManager {
this.websocket = new WebsocketController(prismaRepository, waMonitor);
this.webhook = new WebhookController(prismaRepository, waMonitor);
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
this.nats = new NatsController(prismaRepository, waMonitor);
this.sqs = new SqsController(prismaRepository, waMonitor);
this.pusher = new PusherController(prismaRepository, waMonitor);
}
@@ -67,6 +70,14 @@ export class EventManager {
return this.rabbitmqController;
}
public set nats(nats: NatsController) {
this.natsController = nats;
}
public get nats() {
return this.natsController;
}
public set sqs(sqs: SqsController) {
this.sqsController = sqs;
}
@@ -85,6 +96,7 @@ export class EventManager {
public init(httpServer: Server): void {
this.websocket.init(httpServer);
this.rabbitmq.init();
this.nats.init();
this.sqs.init();
this.pusher.init();
}
@@ -103,6 +115,7 @@ export class EventManager {
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);
await this.nats.emit(eventData);
await this.sqs.emit(eventData);
await this.webhook.emit(eventData);
await this.pusher.emit(eventData);
@@ -125,6 +138,14 @@ export class EventManager {
},
});
if (data.nats)
await this.nats.set(instanceName, {
nats: {
enabled: true,
events: data.nats?.events,
},
});
if (data.sqs)
await this.sqs.set(instanceName, {
sqs: {

View File

@@ -1,3 +1,4 @@
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
@@ -14,6 +15,7 @@ export class EventRouter {
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
this.router.use('/websocket', new WebsocketRouter(...guards).router);
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
this.router.use('/nats', new NatsRouter(...guards).router);
this.router.use('/pusher', new PusherRouter(...guards).router);
this.router.use('/sqs', new SqsRouter(...guards).router);
}

View File

@@ -16,6 +16,9 @@ export const eventSchema: JSONSchema7 = {
rabbitmq: {
$ref: '#/$defs/event',
},
nats: {
$ref: '#/$defs/event',
},
sqs: {
$ref: '#/$defs/event',
},

View File

@@ -0,0 +1,161 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Log, Nats } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { connect, NatsConnection, StringCodec } from 'nats';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
export class NatsController extends EventController implements EventControllerInterface {
public natsClient: NatsConnection | null = null;
private readonly logger = new Logger('NatsController');
private readonly sc = StringCodec();
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Nats>('NATS')?.ENABLED, 'nats');
}
public async init(): Promise<void> {
if (!this.status) {
return;
}
try {
const uri = configService.get<Nats>('NATS').URI;
this.natsClient = await connect({ servers: uri });
this.logger.info('NATS initialized');
if (configService.get<Nats>('NATS')?.GLOBAL_ENABLED) {
await this.initGlobalSubscriptions();
}
} catch (error) {
this.logger.error('Failed to connect to NATS:');
this.logger.error(error);
throw error;
}
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('nats')) {
return;
}
if (!this.status || !this.natsClient) {
return;
}
const instanceNats = await this.get(instanceName);
const natsLocal = instanceNats?.events;
const natsGlobal = configService.get<Nats>('NATS').GLOBAL_ENABLED;
const natsEvents = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
event,
instance: instanceName,
data,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
// Instância específica
if (instanceNats?.enabled) {
if (Array.isArray(natsLocal) && natsLocal.includes(we)) {
const subject = `${instanceName}.${event.toLowerCase()}`;
try {
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (instance): ${error}`);
}
}
}
// Global
if (natsGlobal && natsEvents[we]) {
try {
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS-Global`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (global): ${error}`);
}
}
}
private async initGlobalSubscriptions(): Promise<void> {
this.logger.info('Initializing global subscriptions');
const events = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
if (!events) {
this.logger.warn('No events to initialize on NATS');
return;
}
const eventKeys = Object.keys(events);
for (const event of eventKeys) {
if (events[event] === false) continue;
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
// Criar uma subscription para cada evento
try {
const subscription = this.natsClient.subscribe(subject);
this.logger.info(`Subscribed to: ${subject}`);
// Processar mensagens (exemplo básico)
(async () => {
for await (const msg of subscription) {
try {
const data = JSON.parse(this.sc.decode(msg.data));
// Aqui você pode adicionar a lógica de processamento
this.logger.debug(`Received message on ${subject}:`);
this.logger.debug(data);
} catch (error) {
this.logger.error(`Error processing message on ${subject}:`);
this.logger.error(error);
}
}
})();
} catch (error) {
this.logger.error(`Failed to subscribe to ${subject}:`);
this.logger.error(error);
}
}
}
}

View File

@@ -0,0 +1,36 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { EventDto } from '@api/integrations/event/event.dto';
import { HttpStatus } from '@api/routes/index.router';
import { eventManager } from '@api/server.module';
import { eventSchema, instanceSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class NatsRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {
const response = await this.dataValidate<EventDto>({
request: req,
schema: eventSchema,
ClassRef: EventDto,
execute: (instance, data) => eventManager.nats.set(instance.instanceName, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => eventManager.nats.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -1,10 +1,11 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { SQS } from '@aws-sdk/client-sqs';
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
import { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
import { EventDto } from '../event.dto';
export class SqsController extends EventController implements EventControllerInterface {
private sqs: SQS;
@@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt
return this.sqs;
}
override async set(instanceName: string, data: EventDto): Promise<any> {
if (!this.status) {
return;
}
if (!data[this.name]?.enabled) {
data[this.name].events = [];
} else {
if (0 === data[this.name].events.length) {
data[this.name].events = EventController.events;
}
}
await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled);
const payload: any = {
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
update: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
},
create: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
};
console.log('*** payload: ', payload);
return this.prisma[this.name].upsert(payload);
}
public async emit({
instanceName,
origin,
@@ -121,70 +155,92 @@ export class SqsController extends EventController implements EventControllerInt
}
}
public async initQueues(instanceName: string, events: string[]) {
if (!events || !events.length) return;
private async saveQueues(instanceName: string, events: string[], enable: boolean) {
if (enable) {
const eventsFinded = await this.listQueuesByInstance(instanceName);
console.log('eventsFinded', eventsFinded);
const queues = events.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
for (const event of events) {
const normalizedEvent = event.toLowerCase();
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
if (eventsFinded.includes(normalizedEvent)) {
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
continue;
}
this.sqs.createQueue(
{
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
},
(err, data) => {
if (err) {
this.logger.error(`Error creating queue ${queueName}: ${err.message}`);
} else {
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
}
},
);
});
const queueName = `${instanceName}_${normalizedEvent}.fifo`;
try {
const createCommand = new CreateQueueCommand({
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
});
const data = await this.sqs.send(createCommand);
this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
} catch (err: any) {
this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
}
}
}
}
public async removeQueues(instanceName: string, events: any) {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
private async listQueuesByInstance(instanceName: string) {
let existingQueues: string[] = [];
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
if (listData.QueueUrls && listData.QueueUrls.length > 0) {
// Extrai o nome da fila a partir da URL
existingQueues = listData.QueueUrls.map((queueUrl) => {
const parts = queueUrl.split('/');
return parts[parts.length - 1];
});
}
} catch (error: any) {
this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`);
return;
}
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
// Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
return existingQueues
.map((queueName) => {
// Espera-se que o nome seja `${instanceName}_${event}.fifo`
if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) {
return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase();
}
return '';
})
.filter((event) => event !== '');
}
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
// Para uma futura feature de exclusão forçada das queues
private async removeQueuesByInstance(instanceName: string) {
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
this.sqs.getQueueUrl(
{
QueueName: queueName,
},
(err, data) => {
if (err) {
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
} else {
const queueUrl = data.QueueUrl;
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
this.logger.info(`No queues found for instance ${instanceName}`);
return;
}
this.sqs.deleteQueue(
{
QueueUrl: queueUrl,
},
(deleteErr) => {
if (deleteErr) {
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
} else {
this.logger.info(`Queue ${queueName} deleted`);
}
},
);
}
},
);
});
for (const queueUrl of listData.QueueUrls) {
try {
const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
await this.sqs.send(deleteCommand);
this.logger.info(`Queue ${queueUrl} deleted`);
} catch (err: any) {
this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
}
}
} catch (err: any) {
this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`);
}
}
}

View File

@@ -6,7 +6,7 @@ import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import * as jwt from 'jsonwebtoken';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
@@ -18,7 +18,7 @@ export class WebhookController extends EventController implements EventControlle
}
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
if (!isURL(data.webhook.url, { require_tld: false })) {
if (!/^(https?:\/\/)/.test(data.webhook.url)) {
throw new BadRequestException('Invalid "url" property');
}
@@ -74,10 +74,20 @@ export class WebhookController extends EventController implements EventControlle
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
const webhookHeaders = { ...((instance?.headers as Record<string, string>) || {}) };
if (webhookHeaders && 'jwt_key' in webhookHeaders) {
const jwtKey = webhookHeaders['jwt_key'];
const jwtToken = this.generateJwtToken(jwtKey);
webhookHeaders['Authorization'] = `Bearer ${jwtToken}`;
delete webhookHeaders['jwt_key'];
}
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const regex = /^(https?:\/\/)/;
const webhookData = {
event,
@@ -111,7 +121,7 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
if (instance?.enabled && regex.test(instance.url)) {
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
@@ -155,7 +165,7 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (isURL(globalURL)) {
if (regex.test(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await this.retryWebhookRequest(
@@ -230,4 +240,24 @@ export class WebhookController extends EventController implements EventControlle
}
}
}
private generateJwtToken(authToken: string): string {
try {
const payload = {
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration
app: 'evolution',
action: 'webhook',
};
const token = jwt.sign(payload, authToken, { algorithm: 'HS256' });
return token;
} catch (error) {
this.logger.error({
local: 'WebhookController.generateJwtToken',
message: `JWT generation failed: ${error?.message}`,
});
throw error;
}
}
}

View File

@@ -1,6 +1,6 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Cors, Log, Websocket } from '@config/env.config';
import { Auth, configService, Cors, Log, Websocket } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Server } from 'http';
import { Server as SocketIO } from 'socket.io';
@@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl
}
this.socket = new SocketIO(httpServer, {
cors: {
origin: this.cors,
cors: { origin: this.cors },
allowRequest: async (req, callback) => {
try {
const url = new URL(req.url || '', 'http://localhost');
const params = new URLSearchParams(url.search);
// Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4)
if (params.has('EIO')) {
return callback(null, true);
}
const apiKey = params.get('apikey') || (req.headers.apikey as string);
if (!apiKey) {
this.logger.error('Connection rejected: apiKey not provided');
return callback('apiKey is required', false);
}
const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } });
if (!instance) {
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
if (apiKey !== globalToken) {
this.logger.error('Connection rejected: invalid global token');
return callback('Invalid global token', false);
}
}
callback(null, true);
} catch (error) {
this.logger.error('Authentication error:');
this.logger.error(error);
callback('Authentication error', false);
}
},
});
@@ -101,10 +133,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-WebsocketGlobal`,
...message,
});
this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
}
}
@@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.of(`/${instanceName}`).emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-Websocket`,
...message,
});
this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
}
}
} catch (err) {

View File

@@ -63,9 +63,9 @@ const createBucket = async () => {
if (!exists) {
await minioClient.makeBucket(bucketName);
}
await setBucketPolicy();
if (!BUCKET.SKIP_POLICY) {
await setBucketPolicy();
}
logger.info(`S3 Bucket ${bucketName} - ON`);
return true;
} catch (error) {

View File

@@ -1,7 +1,7 @@
import { Auth, ConfigService, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import axios from 'axios';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
type ResponseSuccess = { status: number; data?: any };
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
@@ -36,7 +36,7 @@ export class ProviderFiles {
} catch (error) {
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
const pid = process.pid;
execSync(`kill -9 ${pid}`);
execFileSync('kill', ['-9', `${pid}`]);
}
}
}

View File

@@ -0,0 +1,37 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { NumberDto } from '@api/dto/chat.dto';
import { businessController } from '@api/server.module';
import { catalogSchema, collectionsSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class BusinessRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('getCatalog'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: catalogSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCatalog(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getCollections'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: collectionsSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCollections(instance, data),
});
return res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -207,7 +207,6 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileNameDto>({
request: req,

View File

@@ -11,6 +11,7 @@ import fs from 'fs';
import mimeTypes from 'mime-types';
import path from 'path';
import { BusinessRouter } from './business.router';
import { CallRouter } from './call.router';
import { ChatRouter } from './chat.router';
import { GroupRouter } from './group.router';
@@ -82,6 +83,7 @@ router
.use('/message', new MessageRouter(...guards).router)
.use('/call', new CallRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router)
.use('/business', new BusinessRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router)
.use('/template', new TemplateRouter(configService, ...guards).router)
.use('/settings', new SettingsRouter(...guards).router)

View File

@@ -15,7 +15,6 @@ export class InstanceRouter extends RouterBroker {
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

@@ -3,6 +3,7 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
import { eventEmitter } from '@config/event.config';
import { Logger } from '@config/logger.config';
import { BusinessController } from './controllers/business.controller';
import { CallController } from './controllers/call.controller';
import { ChatController } from './controllers/chat.controller';
import { GroupController } from './controllers/group.controller';
@@ -98,6 +99,7 @@ export const instanceController = new InstanceController(
export const sendMessageController = new SendMessageController(waMonitor);
export const callController = new CallController(waMonitor);
export const chatController = new ChatController(waMonitor);
export const businessController = new BusinessController(waMonitor);
export const groupController = new GroupController(waMonitor);
export const labelController = new LabelController(waMonitor);

View File

@@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs';
import { join } from 'path';
@@ -91,6 +91,7 @@ export class WAMonitoringService {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
Sqs: true,
Websocket: true,
Setting: true,
@@ -168,7 +169,8 @@ export class WAMonitoringService {
public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
execFileSync('rm', ['-rf', instancePath]);
}
const instance = await this.prismaRepository.instance.findFirst({
@@ -190,6 +192,7 @@ export class WAMonitoringService {
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });

View File

@@ -15,6 +15,7 @@ export enum Events {
MESSAGES_UPDATE = 'messages.update',
MESSAGES_DELETE = 'messages.delete',
SEND_MESSAGE = 'send.message',
SEND_MESSAGE_UPDATE = 'send.message.update',
CONTACTS_SET = 'contacts.set',
CONTACTS_UPSERT = 'contacts.upsert',
CONTACTS_UPDATE = 'contacts.update',

View File

@@ -72,6 +72,7 @@ export type EventsRabbitmq = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@@ -97,7 +98,16 @@ export type Rabbitmq = {
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY: string;
PREFIX_KEY?: string;
};
export type Nats = {
ENABLED: boolean;
URI: string;
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY?: string;
};
export type Sqs = {
@@ -131,6 +141,7 @@ export type EventsWebhook = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@@ -163,6 +174,7 @@ export type EventsPusher = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@@ -251,6 +263,7 @@ export type S3 = {
PORT?: number;
USE_SSL?: boolean;
REGION?: string;
SKIP_POLICY?: boolean;
};
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
@@ -263,6 +276,7 @@ export interface Env {
PROVIDER: ProviderSession;
DATABASE: Database;
RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
@@ -356,7 +370,7 @@ export class ConfigService {
RABBITMQ: {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || 'evolution',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY,
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.RABBITMQ_URI || '',
EVENTS: {
@@ -370,6 +384,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true',
@@ -389,6 +404,43 @@ export class ConfigService {
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
NATS: {
ENABLED: process.env?.NATS_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.NATS_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.NATS_PREFIX_KEY,
EXCHANGE_NAME: process.env?.NATS_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.NATS_URI || '',
EVENTS: {
APPLICATION_STARTUP: process.env?.NATS_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.NATS_EVENTS_INSTANCE_CREATE === 'true',
INSTANCE_DELETE: process.env?.NATS_EVENTS_INSTANCE_DELETE === 'true',
QRCODE_UPDATED: process.env?.NATS_EVENTS_QRCODE_UPDATED === 'true',
MESSAGES_SET: process.env?.NATS_EVENTS_MESSAGES_SET === 'true',
MESSAGES_UPSERT: process.env?.NATS_EVENTS_MESSAGES_UPSERT === 'true',
MESSAGES_EDITED: process.env?.NATS_EVENTS_MESSAGES_EDITED === 'true',
MESSAGES_UPDATE: process.env?.NATS_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.NATS_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.NATS_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.NATS_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.NATS_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.NATS_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.NATS_EVENTS_CONTACTS_UPSERT === 'true',
PRESENCE_UPDATE: process.env?.NATS_EVENTS_PRESENCE_UPDATE === 'true',
CHATS_SET: process.env?.NATS_EVENTS_CHATS_SET === 'true',
CHATS_UPDATE: process.env?.NATS_EVENTS_CHATS_UPDATE === 'true',
CHATS_UPSERT: process.env?.NATS_EVENTS_CHATS_UPSERT === 'true',
CHATS_DELETE: process.env?.NATS_EVENTS_CHATS_DELETE === 'true',
CONNECTION_UPDATE: process.env?.NATS_EVENTS_CONNECTION_UPDATE === 'true',
LABELS_EDIT: process.env?.NATS_EVENTS_LABELS_EDIT === 'true',
LABELS_ASSOCIATION: process.env?.NATS_EVENTS_LABELS_ASSOCIATION === 'true',
GROUPS_UPSERT: process.env?.NATS_EVENTS_GROUPS_UPSERT === 'true',
GROUP_UPDATE: process.env?.NATS_EVENTS_GROUPS_UPDATE === 'true',
GROUP_PARTICIPANTS_UPDATE: process.env?.NATS_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
CALL: process.env?.NATS_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
SQS: {
ENABLED: process.env?.SQS_ENABLED === 'true',
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '',
@@ -421,6 +473,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.PUSHER_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === 'true',
@@ -477,6 +530,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true',
@@ -555,6 +609,7 @@ export class ConfigService {
PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
USE_SSL: process.env?.S3_USE_SSL === 'true',
REGION: process.env?.S3_REGION,
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
},
AUTHENTICATION: {
API_KEY: {

View File

@@ -3,19 +3,19 @@ import { configService, S3 } from '@config/env.config';
const getTypeMessage = (msg: any) => {
let mediaId: string;
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message.mediaUrl;
else mediaId = msg.key.id;
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message?.mediaUrl;
else mediaId = msg.key?.id;
const types = {
conversation: msg?.message?.conversation,
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
contactMessage: msg?.message?.contactMessage?.displayName,
locationMessage: msg?.message?.locationMessage?.degreesLatitude,
locationMessage: msg?.message?.locationMessage?.degreesLatitude.toString(),
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.title,
listResponseMessage: msg?.message?.listResponseMessage?.title || msg?.listResponseMessage?.title,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
templateButtonReplyMessage:
msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId,

View File

@@ -0,0 +1,17 @@
import { JSONSchema7 } from 'json-schema';
export const catalogSchema: JSONSchema7 = {
type: 'object',
properties: {
number: { type: 'string' },
limit: { type: 'number' },
},
};
export const collectionsSchema: JSONSchema7 = {
type: 'object',
properties: {
number: { type: 'string' },
limit: { type: 'number' },
},
};

View File

@@ -68,6 +68,7 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@@ -104,6 +105,44 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
'PRESENCE_UPDATE',
'CHATS_SET',
'CHATS_UPSERT',
'CHATS_UPDATE',
'CHATS_DELETE',
'GROUPS_UPSERT',
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
// NATS
natsEnabled: { type: 'boolean' },
natsEvents: {
type: 'array',
minItems: 0,
items: {
type: 'string',
enum: [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_EDITED',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@@ -140,6 +179,7 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',

View File

@@ -1,4 +1,5 @@
// Integrations Schema
export * from './business.schema';
export * from './chat.schema';
export * from './group.schema';
export * from './instance.schema';