evolution-api/src/api/integrations/event/sqs/sqs.controller.ts

304 lines
9.8 KiB
TypeScript

import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
import { configService, HttpServer, Log, S3, 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;
private readonly logger = new Logger('SqsController');
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Sqs>('SQS')?.ENABLED, 'sqs');
}
public async init(): Promise<void> {
if (!this.status) {
return;
}
const awsConfig = configService.get<Sqs>('SQS');
this.sqs = new SQS({
credentials: {
accessKeyId: awsConfig.ACCESS_KEY_ID,
secretAccessKey: awsConfig.SECRET_ACCESS_KEY,
},
region: awsConfig.REGION,
});
this.logger.info('SQS initialized');
const sqsConfig = configService.get<Sqs>('SQS');
if (this.sqs && sqsConfig.GLOBAL_ENABLED) {
const sqsEvents = Object.keys(sqsConfig.EVENTS).filter((e) => sqsConfig.EVENTS[e]);
await this.saveQueues(sqsConfig.GLOBAL_PREFIX_NAME, sqsEvents, true);
}
}
private set channel(sqs: SQS) {
this.sqs = sqs;
}
public get channel(): SQS {
return this.sqs;
}
override async set(instanceName: string, data: EventDto): Promise<any> {
if (!this.status || configService.get<Sqs>('SQS').GLOBAL_ENABLED) {
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,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('sqs')) {
return;
}
if (!this.status) {
return;
}
if (this.sqs) {
const serverConfig = configService.get<HttpServer>('SERVER');
const sqsConfig = configService.get<Sqs>('SQS');
const we = event.replace(/[.-]/gm, '_').toUpperCase();
let sqsEvents = [];
if (sqsConfig.GLOBAL_ENABLED) {
sqsEvents = Object.keys(sqsConfig.EVENTS).filter((e) => sqsConfig.EVENTS[e]);
} else {
const instanceSqs = await this.get(instanceName);
if (instanceSqs?.enabled && Array.isArray(instanceSqs?.events)) {
sqsEvents = instanceSqs?.events;
}
}
if (Array.isArray(sqsEvents) && sqsEvents.includes(we)) {
const prefixName = sqsConfig.GLOBAL_ENABLED ? sqsConfig.GLOBAL_PREFIX_NAME : instanceName;
const eventFormatted =
sqsConfig.GLOBAL_ENABLED && sqsConfig.GLOBAL_FORCE_SINGLE_QUEUE
? 'singlequeue'
: `${event.replace('.', '_').toLowerCase()}`;
const queueName = `${prefixName}_${eventFormatted}.fifo`;
const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`;
const message = {
event,
instance: instanceName,
dataType: 'json',
data,
server: serverConfig.NAME,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
const jsonStr = JSON.stringify(message);
const size = Buffer.byteLength(jsonStr, 'utf8');
if (size > sqsConfig.MAX_PAYLOAD_SIZE) {
if (!configService.get<S3>('S3').ENABLE) {
this.logger.error(
`${instanceName} - ${eventFormatted} - SQS ignored: payload (${size} bytes) exceeds SQS size limit (${sqsConfig.MAX_PAYLOAD_SIZE} bytes) and S3 storage is not enabled.`,
);
return;
}
const buffer = Buffer.from(jsonStr, 'utf8');
const fullName = `messages/${instanceName}_${eventFormatted}_${Date.now()}.json`;
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
const fileUrl = await s3Service.getObjectUrl(fullName);
message.data = { fileUrl };
message.dataType = 's3';
}
const messageGroupId = sqsConfig.GLOBAL_ENABLED
? `${serverConfig.NAME}-${eventFormatted}-${instanceName}`
: 'evolution';
const isGlobalEnabled = sqsConfig.GLOBAL_ENABLED;
const params = {
MessageBody: JSON.stringify(message),
MessageGroupId: messageGroupId,
QueueUrl: sqsUrl,
...(!isGlobalEnabled && {
MessageDeduplicationId: `${instanceName}_${eventFormatted}_${Date.now()}`,
}),
};
this.sqs.sendMessage(params, (err) => {
if (err) {
this.logger.error({
local: `${origin}.sendData-SQS`,
params: JSON.stringify(message),
sqsUrl: sqsUrl,
message: err?.message,
hostName: err?.hostname,
code: err?.code,
stack: err?.stack,
name: err?.name,
url: queueName,
server_url: serverUrl,
});
} else if (configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS')) {
const logData = {
local: `${origin}.sendData-SQS`,
...message,
};
this.logger.log(logData);
}
});
}
}
}
private async saveQueues(prefixName: string, events: string[], enable: boolean) {
if (enable) {
const sqsConfig = configService.get<Sqs>('SQS');
const eventsFinded = await this.listQueues(prefixName);
console.log('eventsFinded', eventsFinded);
for (const event of events) {
const normalizedEvent =
sqsConfig.GLOBAL_ENABLED && sqsConfig.GLOBAL_FORCE_SINGLE_QUEUE ? 'singlequeue' : event.toLowerCase();
if (eventsFinded.includes(normalizedEvent)) {
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
continue;
}
const queueName = `${prefixName}_${normalizedEvent}.fifo`;
try {
const isGlobalEnabled = sqsConfig.GLOBAL_ENABLED;
const createCommand = new CreateQueueCommand({
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
...(isGlobalEnabled && { ContentBasedDeduplication: '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}`);
}
if (sqsConfig.GLOBAL_ENABLED && sqsConfig.GLOBAL_FORCE_SINGLE_QUEUE) {
break;
}
}
}
}
private async listQueues(prefixName: string) {
let existingQueues: string[] = [];
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${prefixName}_`,
});
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 ${prefixName}: ${error.message}`);
return;
}
// 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(`${prefixName}_`) && queueName.endsWith('.fifo')) {
return queueName.substring(prefixName.length + 1, queueName.length - 5).toLowerCase();
}
return '';
})
.filter((event) => event !== '');
}
// Para uma futura feature de exclusão forçada das queues
private async removeQueuesByInstance(prefixName: string) {
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${prefixName}_`,
});
const listData = await this.sqs.send(listCommand);
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
this.logger.info(`No queues found for ${prefixName}`);
return;
}
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 ${prefixName}: ${err.message}`);
}
}
}