refactor: event folder

This commit is contained in:
Davidson Gomes
2024-08-20 15:27:32 -03:00
parent 64ed0faa83
commit 7b79591e42
35 changed files with 1100 additions and 1539 deletions

View File

@@ -1,53 +1,271 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { EventController } from '@api/controllers/event.controller';
import { RabbitmqDto } from '@api/integrations/event/rabbitmq/dto/rabbitmq.dto';
import { RabbitmqService } from '@api/integrations/event/rabbitmq/services/rabbitmq.service';
import { configService, Rabbitmq } from '@config/env.config';
import { BadRequestException } from '@exceptions';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { configService, Log, Rabbitmq } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import * as amqp from 'amqplib/callback_api';
export class RabbitmqController {
constructor(private readonly rabbitmqService: RabbitmqService) {}
export class RabbitmqController extends EventController {
public amqpChannel: amqp.Channel | null = null;
private readonly logger = new Logger(RabbitmqController.name);
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor);
}
public async createRabbitmq(instance: InstanceDto, data: RabbitmqDto) {
if (!configService.get<Rabbitmq>('RABBITMQ').ENABLED) throw new BadRequestException('Rabbitmq is disabled');
public init(): void {
if (!configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) {
return;
}
new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
amqp.connect(uri, (error, connection) => {
if (error) {
reject(error);
return;
}
connection.createChannel((channelError, channel) => {
if (channelError) {
reject(channelError);
return;
}
const exchangeName = 'evolution_exchange';
channel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
this.amqpChannel = channel;
this.logger.info('AMQP initialized');
resolve();
});
});
}).then(() => {
if (configService.get<Rabbitmq>('RABBITMQ')?.GLOBAL_ENABLED) this.initGlobalQueues();
});
}
private set channel(channel: amqp.Channel) {
this.amqpChannel = channel;
}
public get channel(): amqp.Channel {
return this.amqpChannel;
}
public async set(instanceName: string, data: RabbitmqDto): Promise<wa.LocalRabbitmq> {
if (!data.enabled) {
data.events = [];
} else {
if (0 === data.events.length) {
data.events = this.events;
}
}
if (data.events.length === 0) {
data.events = [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_EDITED',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'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',
];
}
try {
await this.get(instanceName);
return this.rabbitmqService.create(instance, data);
return this.prisma.rabbitmq.update({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
data,
});
} catch (err) {
return this.prisma.rabbitmq.create({
data: {
enabled: data.enabled,
events: data.events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
});
}
}
public async findRabbitmq(instance: InstanceDto) {
return this.rabbitmqService.find(instance);
public async get(instanceName: string): Promise<wa.LocalWebsocket> {
if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found');
}
const data = await this.prisma.rabbitmq.findUnique({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
});
if (!data) {
throw new NotFoundException('Rabbitmq not found');
}
return data;
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
}: {
instanceName: string;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Rabbitmq>('RABBITMQ')?.ENABLED) {
return;
}
const instanceRabbitmq = await this.get(instanceName);
const rabbitmqLocal = instanceRabbitmq.events;
const rabbitmqGlobal = configService.get<Rabbitmq>('RABBITMQ').GLOBAL_ENABLED;
const rabbitmqEvents = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
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,
};
if (instanceRabbitmq.enabled && this.amqpChannel) {
if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) {
const exchangeName = instanceName ?? 'evolution_exchange';
let retry = 0;
while (retry < 3) {
try {
await this.amqpChannel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
const eventName = event.replace(/_/g, '.').toLowerCase();
const queueName = `${instanceName}.${eventName}`;
await this.amqpChannel.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
await this.amqpChannel.bindQueue(queueName, exchangeName, eventName);
await this.amqpChannel.publish(exchangeName, event, Buffer.from(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-RabbitMQ`,
...message,
};
this.logger.log(logData);
}
break;
} catch (error) {
retry++;
}
}
}
}
if (rabbitmqGlobal && rabbitmqEvents[we] && this.amqpChannel) {
const exchangeName = 'evolution_exchange';
let retry = 0;
while (retry < 3) {
try {
await this.amqpChannel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
const queueName = event;
await this.amqpChannel.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
await this.amqpChannel.bindQueue(queueName, exchangeName, event);
await this.amqpChannel.publish(exchangeName, event, Buffer.from(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-RabbitMQ-Global`,
...message,
};
this.logger.log(logData);
}
break;
} catch (error) {
retry++;
}
}
}
}
private async initGlobalQueues(): Promise<void> {
this.logger.info('Initializing global queues');
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
if (!events) {
this.logger.warn('No events to initialize on AMQP');
return;
}
const eventKeys = Object.keys(events);
eventKeys.forEach((event) => {
if (events[event] === false) return;
const queueName = `${event.replace(/_/g, '.').toLowerCase()}`;
const exchangeName = 'evolution_exchange';
this.amqpChannel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
this.amqpChannel.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
this.amqpChannel.bindQueue(queueName, exchangeName, event);
});
}
}

View File

@@ -1,4 +1,13 @@
import { Constructor } from '@api/dto/integration.dto';
export class RabbitmqDto {
enabled: boolean;
events?: string[];
}
export function RabbitMQInstanceMixin<TBase extends Constructor>(Base: TBase) {
return class extends Base {
rabbitmqEnabled?: boolean;
rabbitmqEvents?: string[];
};
}

View File

@@ -1,137 +0,0 @@
import { configService, Rabbitmq } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { JsonValue } from '@prisma/client/runtime/library';
import * as amqp from 'amqplib/callback_api';
const logger = new Logger('AMQP');
let amqpChannel: amqp.Channel | null = null;
export const initAMQP = () => {
return new Promise<void>((resolve, reject) => {
const uri = configService.get<Rabbitmq>('RABBITMQ').URI;
amqp.connect(uri, (error, connection) => {
if (error) {
reject(error);
return;
}
connection.createChannel((channelError, channel) => {
if (channelError) {
reject(channelError);
return;
}
const exchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME || 'evolution_exchange';
channel.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
amqpChannel = channel;
logger.info('AMQP initialized');
resolve();
});
});
});
};
export const getAMQP = (): amqp.Channel | null => {
return amqpChannel;
};
export const initGlobalQueues = () => {
logger.info('Initializing global queues');
const events = configService.get<Rabbitmq>('RABBITMQ').EVENTS;
if (!events) {
logger.warn('No events to initialize on AMQP');
return;
}
const eventKeys = Object.keys(events);
eventKeys.forEach((event) => {
if (events[event] === false) return;
const queueName = `${event.replace(/_/g, '.').toLowerCase()}`;
const amqp = getAMQP();
const exchangeName = configService.get<Rabbitmq>('RABBITMQ').EXCHANGE_NAME || 'evolution_exchange';
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
amqp.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
amqp.bindQueue(queueName, exchangeName, event);
});
};
export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return;
const queues = events.map((event) => {
return `${event.replace(/_/g, '.').toLowerCase()}`;
});
queues.forEach((event) => {
const amqp = getAMQP();
const exchangeName = instanceName ?? 'evolution_exchange';
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
const queueName = `${instanceName}.${event}`;
amqp.assertQueue(queueName, {
durable: true,
autoDelete: false,
arguments: {
'x-queue-type': 'quorum',
},
});
amqp.bindQueue(queueName, exchangeName, event);
});
};
export const removeQueues = (instanceName: string, events: JsonValue) => {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
const channel = getAMQP();
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '.').toLowerCase()}`;
});
const exchangeName = instanceName ?? 'evolution_exchange';
queues.forEach((event) => {
const amqp = getAMQP();
amqp.assertExchange(exchangeName, 'topic', {
durable: true,
autoDelete: false,
});
const queueName = `${instanceName}.${event}`;
amqp.deleteQueue(queueName);
});
channel.deleteExchange(exchangeName);
};

View File

@@ -15,7 +15,7 @@ export class RabbitmqRouter extends RouterBroker {
request: req,
schema: rabbitmqSchema,
ClassRef: RabbitmqDto,
execute: (instance, data) => rabbitmqController.createRabbitmq(instance, data),
execute: (instance, data) => rabbitmqController.set(instance.instanceName, data),
});
res.status(HttpStatus.CREATED).json(response);
@@ -25,7 +25,7 @@ export class RabbitmqRouter extends RouterBroker {
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => rabbitmqController.findRabbitmq(instance),
execute: (instance) => rabbitmqController.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);

View File

@@ -1,33 +0,0 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { RabbitmqDto } from '@api/integrations/event/rabbitmq/dto/rabbitmq.dto';
import { initQueues } from '@api/integrations/event/rabbitmq/libs/amqp.server';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { Rabbitmq } from '@prisma/client';
export class RabbitmqService {
constructor(private readonly waMonitor: WAMonitoringService) {}
private readonly logger = new Logger('RabbitmqService');
public create(instance: InstanceDto, data: RabbitmqDto) {
this.waMonitor.waInstances[instance.instanceName].setRabbitmq(data);
initQueues(instance.instanceName, data.events);
return { rabbitmq: { ...instance, rabbitmq: data } };
}
public async find(instance: InstanceDto): Promise<Rabbitmq> {
try {
const result = await this.waMonitor.waInstances[instance.instanceName].findRabbitmq();
if (Object.keys(result).length === 0) {
throw new Error('Rabbitmq not found');
}
return result;
} catch (error) {
return null;
}
}
}

View File

@@ -1,53 +1,245 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { EventController } from '@api/controllers/event.controller';
import { SqsDto } from '@api/integrations/event/sqs/dto/sqs.dto';
import { SqsService } from '@api/integrations/event/sqs/services/sqs.service';
import { configService, Sqs } from '@config/env.config';
import { BadRequestException } from '@exceptions';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { SQS } from '@aws-sdk/client-sqs';
import { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
export class SqsController {
constructor(private readonly sqsService: SqsService) {}
export class SqsController extends EventController {
private sqs: SQS;
private readonly logger = new Logger(SqsController.name);
public async createSqs(instance: InstanceDto, data: SqsDto) {
if (!configService.get<Sqs>('SQS').ENABLED) throw new BadRequestException('Sqs is disabled');
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor);
}
public init(): void {
if (!configService.get<Sqs>('SQS')?.ENABLED) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
new Promise<void>((resolve, reject) => {
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');
resolve();
});
}
private set channel(sqs: SQS) {
this.sqs = sqs;
}
public get channel(): SQS {
return this.sqs;
}
public async set(instanceName: string, data: SqsDto): Promise<wa.LocalSqs> {
if (!data.enabled) {
data.events = [];
} else {
if (0 === data.events.length) {
data.events = this.events;
}
}
if (data.events.length === 0) {
data.events = [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_EDITED',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'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',
];
}
try {
await this.get(instanceName);
return this.sqsService.create(instance, data);
return this.prisma.sqs.update({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
data,
});
} catch (err) {
return this.prisma.sqs.create({
data: {
enabled: data.enabled,
events: data.events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
});
}
}
public async findSqs(instance: InstanceDto) {
return this.sqsService.find(instance);
public async get(instanceName: string): Promise<wa.LocalSqs> {
if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found');
}
const data = await this.prisma.sqs.findUnique({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
});
if (!data) {
throw new NotFoundException('Sqs not found');
}
return data;
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
}: {
instanceName: string;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Sqs>('SQS')?.ENABLED) {
return;
}
const instanceSqs = await this.get(instanceName);
const sqsLocal = instanceSqs.events;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
if (instanceSqs.enabled) {
if (this.sqs) {
if (Array.isArray(sqsLocal) && sqsLocal.includes(we)) {
const eventFormatted = `${event.replace('.', '_').toLowerCase()}`;
const queueName = `${instanceName}_${eventFormatted}.fifo`;
const sqsConfig = configService.get<Sqs>('SQS');
const sqsUrl = `https://sqs.${sqsConfig.REGION}.amazonaws.com/${sqsConfig.ACCOUNT_ID}/${queueName}`;
const message = {
event,
instance: instanceName,
data,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
const params = {
MessageBody: JSON.stringify(message),
MessageGroupId: 'evolution',
MessageDeduplicationId: `${instanceName}_${eventFormatted}_${Date.now()}`,
QueueUrl: sqsUrl,
};
this.sqs.sendMessage(params, (err) => {
if (err) {
this.logger.error({
local: `${origin}.sendData-SQS`,
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);
}
}
});
}
}
}
}
public async initQueues(instanceName: string, events: string[]) {
if (!events || !events.length) return;
const queues = events.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
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}`);
}
},
);
});
}
public async removeQueues(instanceName: string, events: any) {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
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;
this.sqs.deleteQueue(
{
QueueUrl: queueUrl,
},
(deleteErr) => {
if (deleteErr) {
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
} else {
this.logger.info(`Queue ${queueName} deleted`);
}
},
);
}
},
);
});
}
}

View File

@@ -1,4 +1,13 @@
import { Constructor } from '@api/dto/integration.dto';
export class SqsDto {
enabled: boolean;
events?: string[];
}
export function SQSInstanceMixin<TBase extends Constructor>(Base: TBase) {
return class extends Base {
sqsEnabled?: boolean;
sqsEvents?: string[];
};
}

View File

@@ -1,101 +0,0 @@
import { SQS } from '@aws-sdk/client-sqs';
import { configService, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { JsonValue } from '@prisma/client/runtime/library';
const logger = new Logger('SQS');
let sqs: SQS;
export const initSQS = () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return new Promise<void>((resolve, reject) => {
const awsConfig = configService.get<Sqs>('SQS');
sqs = new SQS({
credentials: {
accessKeyId: awsConfig.ACCESS_KEY_ID,
secretAccessKey: awsConfig.SECRET_ACCESS_KEY,
},
region: awsConfig.REGION,
});
logger.info('SQS initialized');
resolve();
});
};
export const getSQS = (): SQS => {
return sqs;
};
export const initQueues = (instanceName: string, events: string[]) => {
if (!events || !events.length) return;
const queues = events.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
const sqs = getSQS();
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
sqs.createQueue(
{
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
},
(err, data) => {
if (err) {
logger.error(`Error creating queue ${queueName}: ${err.message}`);
} else {
logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
}
},
);
});
};
export const removeQueues = (instanceName: string, events: JsonValue) => {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
const sqs = getSQS();
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
sqs.getQueueUrl(
{
QueueName: queueName,
},
(err, data) => {
if (err) {
logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
} else {
const queueUrl = data.QueueUrl;
sqs.deleteQueue(
{
QueueUrl: queueUrl,
},
(deleteErr) => {
if (deleteErr) {
logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
} else {
logger.info(`Queue ${queueName} deleted`);
}
},
);
}
},
);
});
};

View File

@@ -15,7 +15,7 @@ export class SqsRouter extends RouterBroker {
request: req,
schema: sqsSchema,
ClassRef: SqsDto,
execute: (instance, data) => sqsController.createSqs(instance, data),
execute: (instance, data) => sqsController.set(instance.instanceName, data),
});
res.status(HttpStatus.CREATED).json(response);
@@ -25,7 +25,7 @@ export class SqsRouter extends RouterBroker {
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => sqsController.findSqs(instance),
execute: (instance) => sqsController.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);

View File

@@ -1,33 +0,0 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { SqsDto } from '@api/integrations/event/sqs/dto/sqs.dto';
import { initQueues } from '@api/integrations/event/sqs/libs/sqs.server';
import { WAMonitoringService } from '@api/services/monitor.service';
import { Logger } from '@config/logger.config';
import { Sqs } from '@prisma/client';
export class SqsService {
constructor(private readonly waMonitor: WAMonitoringService) {}
private readonly logger = new Logger('SqsService');
public create(instance: InstanceDto, data: SqsDto) {
this.waMonitor.waInstances[instance.instanceName].setSqs(data);
initQueues(instance.instanceName, data.events);
return { sqs: { ...instance, sqs: data } };
}
public async find(instance: InstanceDto): Promise<Sqs> {
try {
const result = await this.waMonitor.waInstances[instance.instanceName].findSqs();
if (Object.keys(result).length === 0) {
throw new Error('Sqs not found');
}
return result;
} catch (error) {
return null;
}
}
}

View File

@@ -0,0 +1,252 @@
import { EventController } from '@api/controllers/event.controller';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook, Websocket } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException, NotFoundException } from '@exceptions';
import axios from 'axios';
import { isURL } from 'class-validator';
import { WebhookDto } from '../dto/webhook.dto';
export class WebhookController extends EventController {
private readonly logger = new Logger(WebhookController.name);
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor);
}
public async set(instanceName: string, data: WebhookDto): Promise<wa.LocalWebHook> {
if (!isURL(data.url, { require_tld: false })) {
throw new BadRequestException('Invalid "url" property');
}
if (!data.enabled) {
data.events = [];
} else {
if (0 === data.events.length) {
data.events = this.events;
}
}
try {
await this.get(instanceName);
return this.prisma.webhook.update({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
data,
});
} catch (err) {
return this.prisma.webhook.create({
data: {
enabled: data.enabled,
events: data.events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
url: data.url,
webhookBase64: data.webhookBase64,
webhookByEvents: data.webhookByEvents,
},
});
}
}
public async get(instanceName: string): Promise<wa.LocalWebHook> {
if (undefined === this.monitor.waInstances[instanceName]) {
throw new NotFoundException('Instance not found');
}
const data = await this.prisma.webhook.findUnique({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
});
if (!data) {
throw new NotFoundException('Webhook not found');
}
return data;
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
local,
}: {
instanceName: string;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
local?: boolean;
}): Promise<void> {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
return;
}
const instanceWebhook = await this.get(instanceName);
const webhookGlobal = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instanceWebhook.events;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const webhookData = {
event,
instance: instanceName,
data,
destination: instanceWebhook.url,
date_time: dateTime,
sender,
server_url: serverUrl,
apikey: apiKey,
};
if (local) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string;
if (instanceWebhook.webhookByEvents) {
baseURL = `${instanceWebhook.url}/${transformedWe}`;
} else {
baseURL = instanceWebhook.url;
}
if (enabledLog) {
const logData = {
local: `${origin}.sendData-Webhook`,
url: baseURL,
...webhookData,
};
this.logger.log(logData);
}
try {
if (instanceWebhook.enabled && isURL(instanceWebhook.url, { require_tld: false })) {
const httpService = axios.create({ baseURL });
await httpService.post('', webhookData);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook`,
message: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: baseURL,
server_url: serverUrl,
});
}
}
}
if (webhookGlobal.GLOBAL?.ENABLED) {
if (webhookGlobal.EVENTS[we]) {
const globalWebhook = configService.get<Webhook>('WEBHOOK').GLOBAL;
let globalURL;
if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalWebhook.URL}/${transformedWe}`;
} else {
globalURL = globalWebhook.URL;
}
if (enabledLog) {
const logData = {
local: `${origin}.sendData-Webhook-Global`,
url: globalURL,
...webhookData,
};
this.logger.log(logData);
}
try {
if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', webhookData);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook-Global`,
message: error?.message,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: globalURL,
server_url: serverUrl,
});
}
}
}
}
public async receiveWebhook(data: any) {
if (data.object === 'whatsapp_business_account') {
if (data.entry[0]?.changes[0]?.field === 'message_template_status_update') {
const template = await this.prismaRepository.template.findFirst({
where: { templateId: `${data.entry[0].changes[0].value.message_template_id}` },
});
if (!template) {
console.log('template not found');
return;
}
const { webhookUrl } = template;
await axios.post(webhookUrl, data.entry[0].changes[0].value, {
headers: {
'Content-Type': 'application/json',
},
});
return;
}
data.entry?.forEach(async (entry: any) => {
const numberId = entry.changes[0].value.metadata.phone_number_id;
if (!numberId) {
this.logger.error('WebhookService -> receiveWebhook -> numberId not found');
return;
}
const instance = await this.prismaRepository.instance.findFirst({
where: { number: numberId },
});
if (!instance) {
this.logger.error('WebhookService -> receiveWebhook -> instance not found');
return;
}
await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data);
return;
});
}
return;
}
}

View File

@@ -0,0 +1,18 @@
import { Constructor } from '@api/dto/integration.dto';
export class WebhookDto {
enabled?: boolean;
url?: string;
events?: string[];
webhookByEvents?: boolean;
webhookBase64?: boolean;
}
export function WebhookInstanceMixin<TBase extends Constructor>(Base: TBase) {
return class extends Base {
webhookUrl?: string;
webhookByEvents?: boolean;
webhookBase64?: boolean;
webhookEvents?: string[];
};
}

View File

@@ -0,0 +1,38 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { webhookController } from '@api/server.module';
import { ConfigService } from '@config/env.config';
import { instanceSchema, webhookSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { WebhookDto } from '../dto/webhook.dto';
export class WebhookRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {
const response = await this.dataValidate<WebhookDto>({
request: req,
schema: webhookSchema,
ClassRef: WebhookDto,
execute: (instance, data) => webhookController.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) => webhookController.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -0,0 +1,65 @@
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';
const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => {
const properties = {};
propertyNames.forEach(
(property) =>
(properties[property] = {
minLength: 1,
description: `The "${property}" cannot be empty`,
}),
);
return {
if: {
propertyNames: {
enum: [...propertyNames],
},
},
then: { properties },
};
};
export const websocketSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
enabled: { type: 'boolean', enum: [true, false] },
events: {
type: 'array',
minItems: 0,
items: {
type: 'string',
enum: [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_EDITED',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'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',
],
},
},
},
required: ['enabled'],
...isNotEmpty('enabled'),
};

View File

@@ -3,7 +3,7 @@ import { WebsocketDto } from '@api/integrations/event/websocket/dto/websocket.dt
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { configService, Cors, HttpServer, Log, Websocket } from '@config/env.config';
import { configService, Cors, Log, Websocket } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import { Server } from 'http';
@@ -109,11 +109,19 @@ export class WebsocketController extends EventController {
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
}: {
instanceName: string;
origin: string;
event: string;
data: Object;
serverUrl: string;
dateTime: string;
sender: string;
apiKey?: string;
}): Promise<void> {
if (!configService.get<Websocket>('WEBSOCKET')?.ENABLED) {
return;
@@ -121,14 +129,14 @@ export class WebsocketController extends EventController {
const configEv = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBSOCKET');
const serverUrl = configService.get<HttpServer>('SERVER').URL;
const date = new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString();
const message = {
event,
instanceName,
instance: instanceName,
data,
serverUrl,
date,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
if (configService.get<Websocket>('WEBSOCKET')?.GLOBAL_EVENTS) {

View File

@@ -1,4 +1,13 @@
import { Constructor } from '@api/dto/integration.dto';
export class WebsocketDto {
enabled: boolean;
events?: string[];
}
export function WebsocketInstanceMixin<TBase extends Constructor>(Base: TBase) {
return class extends Base {
websocketEnabled?: boolean;
websocketEvents?: string[];
};
}