mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-18 19:32:21 -06:00
Merge branch 'develop' into victoreduardos/jwt-webhook
This commit is contained in:
15
src/api/controllers/business.controller.ts
Normal file
15
src/api/controllers/business.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
14
src/api/dto/business.dto.ts
Normal file
14
src/api/dto/business.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export class Metadata {
|
||||
mentionsEveryOne?: boolean;
|
||||
mentioned?: string[];
|
||||
encoding?: boolean;
|
||||
notConvertSticker?: boolean;
|
||||
}
|
||||
|
||||
export class SendTextDto extends Metadata {
|
||||
|
||||
@@ -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,
|
||||
@@ -800,6 +828,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',
|
||||
@@ -809,7 +838,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
[message['mediaType']]: {
|
||||
[message['type']]: message['id'],
|
||||
preview_url: linkPreview,
|
||||
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
|
||||
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }),
|
||||
caption: message['caption'],
|
||||
},
|
||||
};
|
||||
@@ -977,8 +1006,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 +1110,7 @@ export class BusinessStartupService extends ChannelStartupService {
|
||||
const prepareMedia: any = {
|
||||
fileName: `${hash}.mp3`,
|
||||
mediaType: 'audio',
|
||||
media: audio,
|
||||
audio,
|
||||
};
|
||||
|
||||
if (isURL(audio)) {
|
||||
@@ -1101,15 +1132,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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -1128,38 +1132,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 +1225,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 +1333,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 +1468,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 +1586,6 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
const chatToInsert = {
|
||||
remoteJid: message.remoteJid,
|
||||
instanceId: this.instanceId,
|
||||
name: message.pushName || '',
|
||||
unreadMessages: 0,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
@@ -3573,6 +3651,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 +3989,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 +4695,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1106,7 +1106,7 @@ export class ChatwootService {
|
||||
|
||||
sendTelemetry('/message/sendWhatsAppAudio');
|
||||
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, true);
|
||||
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
|
||||
|
||||
return messageSent;
|
||||
}
|
||||
@@ -1898,7 +1898,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 +2199,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')}.\`_`;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -741,6 +741,10 @@ export class TypebotService {
|
||||
}
|
||||
}
|
||||
|
||||
if (session && !session.awaitUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session && session.status !== 'opened') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export const eventSchema: JSONSchema7 = {
|
||||
rabbitmq: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
nats: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
sqs: {
|
||||
$ref: '#/$defs/event',
|
||||
},
|
||||
|
||||
161
src/api/integrations/event/nats/nats.controller.ts
Normal file
161
src/api/integrations/event/nats/nats.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/api/integrations/event/nats/nats.router.ts
Normal file
36
src/api/integrations/event/nats/nats.router.ts
Normal 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();
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,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');
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ export class WebhookController extends EventController implements EventControlle
|
||||
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,
|
||||
@@ -121,7 +122,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,
|
||||
@@ -165,7 +166,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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/api/routes/business.router.ts
Normal file
37
src/api/routes/business.router.ts
Normal 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();
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -503,8 +503,29 @@ export class ChannelStartupService {
|
||||
where['remoteJid'] = remoteJid;
|
||||
}
|
||||
|
||||
return await this.prismaRepository.contact.findMany({
|
||||
const contactFindManyArgs: Prisma.ContactFindManyArgs = {
|
||||
where,
|
||||
};
|
||||
|
||||
if (query.offset) contactFindManyArgs.take = query.offset;
|
||||
if (query.page) {
|
||||
const validPage = Math.max(query.page as number, 1);
|
||||
contactFindManyArgs.skip = query.offset * (validPage - 1);
|
||||
}
|
||||
|
||||
const contacts = await this.prismaRepository.contact.findMany(contactFindManyArgs);
|
||||
|
||||
return contacts.map((contact) => {
|
||||
const remoteJid = contact.remoteJid;
|
||||
const isGroup = remoteJid.endsWith('@g.us');
|
||||
const isSaved = !!contact.pushName || !!contact.profilePicUrl;
|
||||
const type = isGroup ? 'group' : isSaved ? 'contact' : 'group_member';
|
||||
return {
|
||||
...contact,
|
||||
isGroup,
|
||||
isSaved,
|
||||
type,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -685,88 +706,93 @@ export class ChannelStartupService {
|
||||
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)}`
|
||||
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 limit = query?.take ? Prisma.sql`LIMIT ${query.take}` : Prisma.sql``;
|
||||
const offset = query?.skip ? Prisma.sql`OFFSET ${query.skip}` : 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;
|
||||
WITH rankedMessages AS (
|
||||
SELECT DISTINCT ON ("Message"."key"->>'remoteJid')
|
||||
"Contact"."id" as "contactId",
|
||||
"Message"."key"->>'remoteJid' as "remoteJid",
|
||||
COALESCE("Contact"."pushName", "Message"."pushName") as "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 "Message"
|
||||
LEFT JOIN "Contact" ON "Contact"."remoteJid" = "Message"."key"->>'remoteJid' AND "Contact"."instanceId" = "Message"."instanceId"
|
||||
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Message"."key"->>'remoteJid' AND "Chat"."instanceId" = "Message"."instanceId"
|
||||
WHERE "Message"."instanceId" = ${this.instanceId}
|
||||
${remoteJid ? Prisma.sql`AND "Message"."key"->>'remoteJid' = ${remoteJid}` : Prisma.sql``}
|
||||
${timestampFilter}
|
||||
ORDER BY "Message"."key"->>'remoteJid', "Message"."messageTimestamp" DESC
|
||||
)
|
||||
SELECT * FROM rankedMessages
|
||||
ORDER BY "updatedAt" DESC NULLS LAST
|
||||
${limit}
|
||||
${offset};
|
||||
`;
|
||||
|
||||
if (results && isArray(results) && results.length > 0) {
|
||||
const mappedResults = results.map((contact) => {
|
||||
const lastMessage = contact.lastMessageId
|
||||
const mappedResults = results.map((item) => {
|
||||
const lastMessage = item.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,
|
||||
id: item.lastMessageId,
|
||||
key: item.lastMessage_key,
|
||||
pushName: item.lastMessagePushName,
|
||||
participant: item.lastMessageParticipant,
|
||||
messageType: item.lastMessageMessageType,
|
||||
message: item.lastMessageMessage,
|
||||
contextInfo: item.lastMessageContextInfo,
|
||||
source: item.lastMessageSource,
|
||||
messageTimestamp: item.lastMessageMessageTimestamp,
|
||||
instanceId: item.lastMessageInstanceId,
|
||||
sessionId: item.lastMessageSessionId,
|
||||
status: item.lastMessageStatus,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: contact.id,
|
||||
remoteJid: contact.remoteJid,
|
||||
pushName: contact.pushName,
|
||||
profilePicUrl: contact.profilePicUrl,
|
||||
updatedAt: contact.updatedAt,
|
||||
windowStart: contact.windowStart,
|
||||
windowExpires: contact.windowExpires,
|
||||
windowActive: contact.windowActive,
|
||||
id: item.contactId || null,
|
||||
remoteJid: item.remoteJid,
|
||||
pushName: item.pushName,
|
||||
profilePicUrl: item.profilePicUrl,
|
||||
updatedAt: item.updatedAt,
|
||||
windowStart: item.windowStart,
|
||||
windowExpires: item.windowExpires,
|
||||
windowActive: item.windowActive,
|
||||
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
|
||||
unreadCount: 0,
|
||||
isSaved: !!item.contactId,
|
||||
};
|
||||
});
|
||||
|
||||
if (query?.take && query?.skip) {
|
||||
const skip = query.skip || 0;
|
||||
const take = query.take || 20;
|
||||
return mappedResults.slice(skip, skip + take);
|
||||
}
|
||||
|
||||
return mappedResults;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
17
src/validate/business.schema.ts
Normal file
17
src/validate/business.schema.ts
Normal 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' },
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Integrations Schema
|
||||
export * from './business.schema';
|
||||
export * from './chat.schema';
|
||||
export * from './group.schema';
|
||||
export * from './instance.schema';
|
||||
|
||||
Reference in New Issue
Block a user