merging with develop

This commit is contained in:
pedro-php
2025-03-28 10:36:34 -03:00
50 changed files with 2129 additions and 934 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,
@@ -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);

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,
@@ -378,7 +382,7 @@ export class BaileysStartupService extends ChannelStartupService {
qrcodeTerminal.generate(qr, { small: true }, (qrcode) =>
this.logger.log(
`\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` +
qrcode,
qrcode,
),
);
@@ -1019,18 +1023,18 @@ export class BaileysStartupService extends ChannelStartupService {
const messagesRepository: Set<string> = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ??
(
await this.prismaRepository.message.findMany({
select: { key: true },
where: { instanceId: this.instanceId },
})
).map((message) => {
const key = message.key as {
id: string;
};
(
await this.prismaRepository.message.findMany({
select: { key: true },
where: { instanceId: this.instanceId },
})
).map((message) => {
const key = message.key as {
id: string;
};
return key.id;
}),
return key.id;
}),
);
if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
@@ -1128,37 +1132,74 @@ 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,
);
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const editedMessage = received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
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) {
if (Long.isLong(editedMessage?.timestampMs)) {
editedMessage.timestampMs = editedMessage.timestampMs?.toNumber();
}
await this.prismaRepository.message.update({
where: { id: (oldMessage as any).id },
data: {
message: editedMessage.editedMessage as any,
messageTimestamp: editedMessage.timestampMs,
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') ||
@@ -1185,7 +1226,8 @@ export class BaileysStartupService extends ChannelStartupService {
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
received.pushName.trim().length > 0 &&
!received.key.remoteJid.includes('@g.us')
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
@@ -1421,6 +1463,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(
@@ -2716,7 +2769,9 @@ export class BaileysStartupService extends ChannelStartupService {
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,
@@ -3572,6 +3627,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: {
@@ -3898,28 +3965,75 @@ export class BaileysStartupService extends ChannelStartupService {
}
try {
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 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');
const updatedMessage =
messageSent.message?.protocolMessage || messageSent.message?.editedMessage?.message?.protocolMessage;
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,
});
if (updatedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'send.message.update',
{ instanceName: this.instance.name, instanceId: this.instance.id },
updatedMessage,
);
await this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, updatedMessage);
const editedMessage = messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
}
}
return messageSent;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.toString());
throw error;
}
}
@@ -4548,4 +4662,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

@@ -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;
}

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

@@ -152,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,6 @@ 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 { EmitData, EventController, EventControllerInterface } from '../event.controller';
@@ -18,7 +17,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');
}
@@ -78,6 +77,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,
@@ -111,7 +111,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 +155,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(

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

@@ -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

@@ -503,9 +503,17 @@ 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);
}
return await this.prismaRepository.contact.findMany(contactFindManyArgs);
}
public cleanMessageData(message: any) {
@@ -674,6 +682,13 @@ export class ChannelStartupService {
: createJid(query.where?.remoteJid)
: null;
const limit =
query.offset && !query.page
? Prisma.sql` LIMIT ${query.offset}`
: query.offset && query.page
? Prisma.sql` LIMIT ${query.offset} OFFSET ${((query.page as number) - 1) * query.offset}`
: Prisma.sql``;
const where = {
instanceId: this.instanceId,
};
@@ -700,6 +715,7 @@ export class ChannelStartupService {
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."name" as "chatName",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
CASE
@@ -730,6 +746,7 @@ export class ChannelStartupService {
ORDER BY
"Contact"."remoteJid",
"Message"."messageTimestamp" DESC
${limit}
)
SELECT * FROM rankedMessages
ORDER BY "updatedAt" DESC NULLS LAST;
@@ -758,6 +775,7 @@ export class ChannelStartupService {
id: contact.id,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
chatName: contact.chatName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,

View File

@@ -91,6 +91,7 @@ export class WAMonitoringService {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
Sqs: true,
Websocket: true,
Setting: true,
@@ -190,6 +191,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

@@ -98,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 = {
@@ -266,6 +275,7 @@ export interface Env {
PROVIDER: ProviderSession;
DATABASE: Database;
RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
@@ -359,7 +369,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: {
@@ -393,6 +403,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 || '',

View File

@@ -3,8 +3,8 @@ 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,
@@ -15,7 +15,7 @@ const getTypeMessage = (msg: any) => {
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

@@ -126,6 +126,43 @@ export const instanceSchema: JSONSchema7 = {
],
},
},
// 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',
'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',
],
},
},
// SQS
sqsEnabled: { type: 'boolean' },
sqsEvents: {

View File

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