evolution-api/src/api/services/monitor.service.ts
Thiago Borges 490ba48501 teste
2025-07-09 11:28:29 -03:00

431 lines
14 KiB
TypeScript

import { InstanceDto } from '@api/dto/instance.dto';
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { channelController } from '@api/server.module';
import { Events, Integration } from '@api/types/wa.types';
import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions';
import { execFileSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs';
import { join } from 'path';
import { ChatwootService } from '../../api/integrations/chatbot/chatwoot/services/chatwoot.service';
import { CacheService } from './cache.service';
export class WAMonitoringService {
constructor(
private readonly eventEmitter: EventEmitter2,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
private readonly providerFiles: ProviderFiles,
private readonly cache: CacheService,
private readonly chatwootCache: CacheService,
private readonly baileysCache: CacheService,
) {
this.removeInstance();
this.noConnection();
Object.assign(this.db, configService.get<Database>('DATABASE'));
Object.assign(this.redis, configService.get<CacheConf>('CACHE'));
}
private readonly db: Partial<Database> = {};
private readonly redis: Partial<CacheConf> = {};
private readonly logger = new Logger('WAMonitoringService');
public readonly waInstances: Record<string, any> = {};
private readonly providerSession = Object.freeze(this.configService.get<ProviderSession>('PROVIDER'));
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
setTimeout(
async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
}
},
1000 * 60 * time,
);
}
}
public async instanceInfo(instanceNames?: string[]): Promise<any> {
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
if (inexistentInstances.length > 0) {
throw new NotFoundException(
`Instance${inexistentInstances.length > 1 ? 's' : ''} "${inexistentInstances.join(', ')}" not found`,
);
}
}
const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const where =
instanceNames && instanceNames.length > 0
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const instances = await this.prismaRepository.instance.findMany({
where,
include: {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
Sqs: true,
Websocket: true,
Setting: true,
_count: {
select: {
Message: true,
Contact: true,
Chat: true,
},
},
},
});
return instances;
}
public async instanceInfoById(instanceId?: string, number?: string) {
let instanceName: string;
if (instanceId) {
instanceName = await this.prismaRepository.instance.findFirst({ where: { id: instanceId } }).then((r) => r?.name);
if (!instanceName) {
throw new NotFoundException(`Instance "${instanceId}" not found`);
}
} else if (number) {
instanceName = await this.prismaRepository.instance.findFirst({ where: { number } }).then((r) => r?.name);
if (!instanceName) {
throw new NotFoundException(`Instance "${number}" not found`);
}
}
if (!instanceName) {
throw new NotFoundException(`Instance "${instanceId}" not found`);
}
if (instanceName && !this.waInstances[instanceName]) {
throw new NotFoundException(`Instance "${instanceName}" not found`);
}
const instanceNames = instanceName ? [instanceName] : null;
return this.instanceInfo(instanceNames);
}
public async cleaningUp(instanceName: string) {
let instanceDbId: string;
if (this.db.SAVE_DATA.INSTANCE) {
const findInstance = await this.prismaRepository.instance.findFirst({
where: { name: instanceName },
});
if (findInstance) {
const instance = await this.prismaRepository.instance.update({
where: { id: findInstance.id },
data: { connectionStatus: 'close' },
});
rmSync(join(INSTANCE_DIR, instance.id), { recursive: true, force: true });
instanceDbId = instance.id;
await this.prismaRepository.session.deleteMany({ where: { sessionId: instance.id } });
}
}
if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) {
await this.cache.delete(instanceName);
if (instanceDbId) {
await this.cache.delete(instanceDbId);
}
}
if (this.providerSession?.ENABLED) {
await this.providerFiles.removeSession(instanceName);
}
}
public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
execFileSync('rm', ['-rf', instancePath]);
}
const instance = await this.prismaRepository.instance.findFirst({
where: { name: instanceName },
});
if (!instance) return;
rmSync(join(INSTANCE_DIR, instance.id), { recursive: true, force: true });
await this.prismaRepository.session.deleteMany({ where: { sessionId: instance.id } });
await this.prismaRepository.chat.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.contact.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.messageUpdate.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.message.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.webhook.deleteMany({ where: { instanceId: instance.id } });
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 } });
await this.prismaRepository.websocket.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.setting.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.label.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.instance.delete({ where: { name: instanceName } });
}
public async loadInstance() {
try {
if (this.providerSession?.ENABLED) {
await this.loadInstancesFromProvider();
} else if (this.db.SAVE_DATA.INSTANCE) {
await this.loadInstancesFromDatabasePostgres();
} else if (this.redis.REDIS.ENABLED && this.redis.REDIS.SAVE_INSTANCES) {
await this.loadInstancesFromRedis();
}
} catch (error) {
this.logger.error(error);
}
}
public async saveInstance(data: any) {
try {
const clientName = await this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
// Pegue o token do .env/config
const token = process.env.SERPRO_CLIENT_SECRET;
// Garanta que nome está preenchido!
if (!data.instanceName) throw new Error('instanceName é obrigatório no saveInstance!');
await this.prismaRepository.instance.upsert({
where: { name: data.instanceName },
create: {
id: data.instanceId || data.instanceName, // sempre defina!
name: data.instanceName,
ownerJid: data.ownerJid,
profileName: data.profileName,
profilePicUrl: data.profilePicUrl,
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: token, // PEGA DO ENV!
clientName: clientName,
businessId: data.businessId,
},
update: {
ownerJid: data.ownerJid,
profileName: data.profileName,
profilePicUrl: data.profilePicUrl,
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: token, // ATUALIZA DO ENV!
clientName: clientName,
businessId: data.businessId,
},
});
} catch (error) {
this.logger.error(error);
}
}
public deleteInstance(instanceName: string) {
try {
this.eventEmitter.emit('remove.instance', instanceName, 'inner');
} catch (error) {
this.logger.error(error);
}
}
private async setInstance(instanceData: InstanceDto) {
const instance = channelController.init(instanceData, {
configService: this.configService,
eventEmitter: this.eventEmitter,
prismaRepository: this.prismaRepository,
cache: this.cache,
chatwootCache: this.chatwootCache,
baileysCache: this.baileysCache,
providerFiles: this.providerFiles,
});
if (!instance) return;
instance.setInstance({
instanceId: instanceData.instanceId,
instanceName: instanceData.instanceName,
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
});
await instance.connectToWhatsapp();
this.waInstances[instanceData.instanceName] = instance;
}
private async loadInstancesFromRedis() {
const keys = await this.cache.keys();
if (keys?.length > 0) {
await Promise.all(
keys.map(async (k) => {
const instanceData = await this.prismaRepository.instance.findUnique({
where: { id: k.split(':')[1] },
});
if (!instanceData) {
return;
}
const instance = {
instanceId: k.split(':')[1],
instanceName: k.split(':')[2],
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
};
this.setInstance(instance);
}),
);
}
}
private async loadInstancesFromDatabasePostgres() {
const clientName = await this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const instances = await this.prismaRepository.instance.findMany({
where: { clientName: clientName },
});
if (instances.length === 0) {
return;
}
await Promise.all(
instances.map(async (instance) => {
this.setInstance({
instanceId: instance.id,
instanceName: instance.name,
integration: instance.integration,
token: instance.token,
number: instance.number,
businessId: instance.businessId,
});
}),
);
}
private async loadInstancesFromProvider() {
const [instances] = await this.providerFiles.allInstances();
if (!instances?.data) {
return;
}
await Promise.all(
instances?.data?.map(async (instanceId: string) => {
const instance = await this.prismaRepository.instance.findUnique({
where: { id: instanceId },
});
this.setInstance({
instanceId: instance.id,
instanceName: instance.name,
integration: instance.integration,
token: instance.token,
businessId: instance.businessId,
});
}),
);
}
private removeInstance() {
this.eventEmitter.on('remove.instance', async (instanceName: string) => {
try {
await this.waInstances[instanceName]?.sendDataWebhook(Events.REMOVE_INSTANCE, null);
this.cleaningUp(instanceName);
this.cleaningStoreData(instanceName);
} finally {
this.logger.warn(`Instance "${instanceName}" - REMOVED`);
}
try {
delete this.waInstances[instanceName];
} catch (error) {
this.logger.error(error);
}
});
this.eventEmitter.on('logout.instance', async (instanceName: string) => {
try {
await this.waInstances[instanceName]?.sendDataWebhook(Events.LOGOUT_INSTANCE, null);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
this.waInstances[instanceName]?.clearCacheChatwoot();
}
this.cleaningUp(instanceName);
} finally {
this.logger.warn(`Instance "${instanceName}" - LOGOUT`);
}
});
}
private noConnection() {
this.eventEmitter.on('no.connection', async (instanceName) => {
try {
await this.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName);
this.waInstances[instanceName]?.client?.ws?.close();
this.waInstances[instanceName].instance.qrcode = { count: 0 };
this.waInstances[instanceName].stateConnection.state = 'close';
} catch (error) {
this.logger.error({
localError: 'noConnection',
warn: 'Error deleting instance from memory.',
error,
});
} finally {
this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`);
}
});
}
}