mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-18 11:22:21 -06:00
merging with develop
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,
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -152,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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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,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,
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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,
|
||||
|
||||
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' },
|
||||
},
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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