evolution-api/src/api/services/monitor.service.ts
Rafael Santana ecd4b913b6 feat: add support for fetching multiple instances by key
This commit adds a new feature to fetch instances by key in the InstanceController. If the provided key does not match the environment key, the controller will search for instances with the matching token. If instances are found, the names are extracted and passed to the waMonitor to retrieve instance information.

Also, update the waMonitor's instanceInfo method to accept an array of instance names instead of a single name.

Fixes #990
2024-10-17 16:42:52 -05:00

396 lines
13 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 { execSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs';
import { join } from 'path';
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> {
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
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const instances = await this.prismaRepository.instance.findMany({
where,
include: {
Chatwoot: true,
Proxy: true,
Rabbitmq: 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`);
}
return this.instanceInfo([instanceName]);
}
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: { name: instanceName },
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) {
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
}
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.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;
await this.prismaRepository.instance.create({
data: {
id: data.instanceId,
name: data.instanceName,
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : data.status ?? 'open',
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: data.hash,
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`);
}
});
}
}