evolution-api/src/api/controllers/instance.controller.ts
Davidson Gomes 540467293c Enhance settings and integrate Baileys controller for WhatsApp functionality
- Added `wavoipToken` field to `Setting` model in both MySQL and PostgreSQL schemas.
- Updated `package.json` and `package-lock.json` to include `mime-types` and `socket.io-client` dependencies.
- Introduced `BaileysController` and `BaileysRouter` for handling WhatsApp interactions.
- Refactored media type handling to use `mime-types` instead of `mime` across various services.
- Updated DTOs and validation schemas to accommodate the new `wavoipToken` field.
- Implemented voice call functionalities using the Wavoip service in the Baileys integration.
- Enhanced event handling in the WebSocket controller to support new features.
2025-01-16 11:58:33 -03:00

441 lines
16 KiB
TypeScript

import { InstanceDto, SetPresenceDto } from '@api/dto/instance.dto';
import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service';
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { channelController, eventManager } from '@api/server.module';
import { CacheService } from '@api/services/cache.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { SettingsService } from '@api/services/settings.service';
import { Events, Integration, wa } from '@api/types/wa.types';
import { Auth, Chatwoot, ConfigService, HttpServer, WaBusiness } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException, InternalServerErrorException, UnauthorizedException } from '@exceptions';
import { delay } from 'baileys';
import { isArray, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import { v4 } from 'uuid';
import { ProxyController } from './proxy.controller';
export class InstanceController {
constructor(
private readonly waMonitor: WAMonitoringService,
private readonly configService: ConfigService,
private readonly prismaRepository: PrismaRepository,
private readonly eventEmitter: EventEmitter2,
private readonly chatwootService: ChatwootService,
private readonly settingsService: SettingsService,
private readonly proxyService: ProxyController,
private readonly cache: CacheService,
private readonly chatwootCache: CacheService,
private readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) {}
private readonly logger = new Logger('InstanceController');
public async createInstance(instanceData: InstanceDto) {
try {
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) {
throw new BadRequestException('Invalid integration');
}
const instanceId = v4();
instanceData.instanceId = instanceId;
let hash: string;
if (!instanceData.token) hash = v4().toUpperCase();
else hash = instanceData.token;
await this.waMonitor.saveInstance({
instanceId,
integration: instanceData.integration,
instanceName: instanceData.instanceName,
hash,
number: instanceData.number,
businessId: instanceData.businessId,
status: instanceData.status,
});
instance.setInstance({
instanceName: instanceData.instanceName,
instanceId,
integration: instanceData.integration,
token: hash,
number: instanceData.number,
businessId: instanceData.businessId,
});
this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName);
// set events
await eventManager.setInstance(instance.instanceName, instanceData);
instance.sendDataWebhook(Events.INSTANCE_CREATE, {
instanceName: instanceData.instanceName,
instanceId: instanceId,
});
if (instanceData.proxyHost && instanceData.proxyPort && instanceData.proxyProtocol) {
const testProxy = await this.proxyService.testProxy({
host: instanceData.proxyHost,
port: instanceData.proxyPort,
protocol: instanceData.proxyProtocol,
username: instanceData.proxyUsername,
password: instanceData.proxyPassword,
});
if (!testProxy) {
throw new BadRequestException('Invalid proxy');
}
await this.proxyService.createProxy(instance, {
enabled: true,
host: instanceData.proxyHost,
port: instanceData.proxyPort,
protocol: instanceData.proxyProtocol,
username: instanceData.proxyUsername,
password: instanceData.proxyPassword,
});
}
const settings: wa.LocalSettings = {
rejectCall: instanceData.rejectCall === true,
msgCall: instanceData.msgCall || '',
groupsIgnore: instanceData.groupsIgnore === true,
alwaysOnline: instanceData.alwaysOnline === true,
readMessages: instanceData.readMessages === true,
readStatus: instanceData.readStatus === true,
syncFullHistory: instanceData.syncFullHistory === true,
wavoipToken: instanceData.wavoipToken || '',
};
await this.settingsService.create(instance, settings);
let webhookWaBusiness = null,
accessTokenWaBusiness = '';
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
if (!instanceData.number) {
throw new BadRequestException('number is required');
}
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
webhookWaBusiness = `${urlServer}/webhook/meta`;
accessTokenWaBusiness = this.configService.get<WaBusiness>('WA_BUSINESS').TOKEN_WEBHOOK;
}
if (!instanceData.chatwootAccountId || !instanceData.chatwootToken || !instanceData.chatwootUrl) {
let getQrcode: wa.QrCode;
if (instanceData.qrcode && instanceData.integration === Integration.WHATSAPP_BAILEYS) {
await instance.connectToWhatsapp(instanceData.number);
await delay(5000);
getQrcode = instance.qrCode;
}
const result = {
instance: {
instanceName: instance.instanceName,
instanceId: instanceId,
integration: instanceData.integration,
webhookWaBusiness,
accessTokenWaBusiness,
status: instance.connectionStatus.state,
},
hash,
webhook: {
webhookUrl: instanceData?.webhook?.url,
webhookHeaders: instanceData?.webhook?.headers,
webhookByEvents: instanceData?.webhook?.byEvents,
webhookBase64: instanceData?.webhook?.base64,
},
websocket: {
enabled: instanceData?.websocket?.enabled,
},
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},
settings,
qrcode: getQrcode,
};
return result;
}
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED)
throw new BadRequestException('Chatwoot is not enabled');
if (!instanceData.chatwootAccountId) {
throw new BadRequestException('accountId is required');
}
if (!instanceData.chatwootToken) {
throw new BadRequestException('token is required');
}
if (!instanceData.chatwootUrl) {
throw new BadRequestException('url is required');
}
if (!isURL(instanceData.chatwootUrl, { require_tld: false })) {
throw new BadRequestException('Invalid "url" property in chatwoot');
}
if (instanceData.chatwootSignMsg !== true && instanceData.chatwootSignMsg !== false) {
throw new BadRequestException('signMsg is required');
}
if (instanceData.chatwootReopenConversation !== true && instanceData.chatwootReopenConversation !== false) {
throw new BadRequestException('reopenConversation is required');
}
if (instanceData.chatwootConversationPending !== true && instanceData.chatwootConversationPending !== false) {
throw new BadRequestException('conversationPending is required');
}
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
try {
this.chatwootService.create(instance, {
enabled: true,
accountId: instanceData.chatwootAccountId,
token: instanceData.chatwootToken,
url: instanceData.chatwootUrl,
signMsg: instanceData.chatwootSignMsg || false,
nameInbox: instanceData.chatwootNameInbox ?? instance.instanceName.split('-cwId-')[0],
number: instanceData.number,
reopenConversation: instanceData.chatwootReopenConversation || false,
conversationPending: instanceData.chatwootConversationPending || false,
importContacts: instanceData.chatwootImportContacts ?? true,
mergeBrazilContacts: instanceData.chatwootMergeBrazilContacts ?? false,
importMessages: instanceData.chatwootImportMessages ?? true,
daysLimitImportMessages: instanceData.chatwootDaysLimitImportMessages ?? 60,
organization: instanceData.chatwootOrganization,
logo: instanceData.chatwootLogo,
autoCreate: instanceData.chatwootAutoCreate !== false,
});
} catch (error) {
this.logger.log(error);
}
return {
instance: {
instanceName: instance.instanceName,
instanceId: instanceId,
integration: instanceData.integration,
webhookWaBusiness,
accessTokenWaBusiness,
status: instance.connectionStatus.state,
},
hash,
webhook: {
webhookUrl: instanceData?.webhook?.url,
webhookHeaders: instanceData?.webhook?.headers,
webhookByEvents: instanceData?.webhook?.byEvents,
webhookBase64: instanceData?.webhook?.base64,
},
websocket: {
enabled: instanceData?.websocket?.enabled,
},
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},
settings,
chatwoot: {
enabled: true,
accountId: instanceData.chatwootAccountId,
token: instanceData.chatwootToken,
url: instanceData.chatwootUrl,
signMsg: instanceData.chatwootSignMsg || false,
reopenConversation: instanceData.chatwootReopenConversation || false,
conversationPending: instanceData.chatwootConversationPending || false,
mergeBrazilContacts: instanceData.chatwootMergeBrazilContacts ?? false,
importContacts: instanceData.chatwootImportContacts ?? true,
importMessages: instanceData.chatwootImportMessages ?? true,
daysLimitImportMessages: instanceData.chatwootDaysLimitImportMessages || 60,
number: instanceData.number,
nameInbox: instanceData.chatwootNameInbox ?? instance.instanceName,
webhookUrl: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
},
};
} catch (error) {
this.waMonitor.deleteInstance(instanceData.instanceName);
this.logger.error(isArray(error.message) ? error.message[0] : error.message);
throw new BadRequestException(isArray(error.message) ? error.message[0] : error.message);
}
}
public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) {
try {
const instance = this.waMonitor.waInstances[instanceName];
const state = instance?.connectionStatus?.state;
if (!state) {
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
}
if (state == 'open') {
return await this.connectionState({ instanceName });
}
if (state == 'connecting') {
return instance.qrCode;
}
if (state == 'close') {
await instance.connectToWhatsapp(number);
await delay(2000);
return instance.qrCode;
}
return {
instance: {
instanceName: instanceName,
status: state,
},
qrcode: instance?.qrCode,
};
} catch (error) {
this.logger.error(error);
return { error: true, message: error.toString() };
}
}
public async restartInstance({ instanceName }: InstanceDto) {
try {
const instance = this.waMonitor.waInstances[instanceName];
const state = instance?.connectionStatus?.state;
if (!state) {
throw new BadRequestException('The "' + instanceName + '" instance does not exist');
}
if (state == 'close') {
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
} else if (state == 'open') {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) instance.clearCacheChatwoot();
this.logger.info('restarting instance' + instanceName);
instance.client?.ws?.close();
instance.client?.end(new Error('restart'));
return await this.connectToWhatsapp({ instanceName });
} else if (state == 'connecting') {
instance.client?.ws?.close();
instance.client?.end(new Error('restart'));
return await this.connectToWhatsapp({ instanceName });
}
} catch (error) {
this.logger.error(error);
return { error: true, message: error.toString() };
}
}
public async connectionState({ instanceName }: InstanceDto) {
return {
instance: {
instanceName: instanceName,
state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state,
},
};
}
public async fetchInstances({ instanceName, instanceId, number }: InstanceDto, key: string) {
const env = this.configService.get<Auth>('AUTHENTICATION').API_KEY;
if (env.KEY !== key) {
const instancesByKey = await this.prismaRepository.instance.findMany({
where: {
token: key,
name: instanceName || undefined,
id: instanceId || undefined,
},
});
if (instancesByKey.length > 0) {
const names = instancesByKey.map((instance) => instance.name);
return this.waMonitor.instanceInfo(names);
} else {
throw new UnauthorizedException();
}
}
if (instanceId || number) {
return this.waMonitor.instanceInfoById(instanceId, number);
}
const instanceNames = instanceName ? [instanceName] : null;
return this.waMonitor.instanceInfo(instanceNames);
}
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
return await this.waMonitor.waInstances[instanceName].setPresence(data);
}
public async logout({ instanceName }: InstanceDto) {
const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'close') {
throw new BadRequestException('The "' + instanceName + '" instance is not connected');
}
try {
this.waMonitor.waInstances[instanceName]?.logoutInstance();
return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } };
} catch (error) {
throw new InternalServerErrorException(error.toString());
}
}
public async deleteInstance({ instanceName }: InstanceDto) {
const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'open') {
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
}
try {
const waInstances = this.waMonitor.waInstances[instanceName];
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
if (instance.state === 'connecting') {
await this.logout({ instanceName });
}
try {
waInstances?.sendDataWebhook(Events.INSTANCE_DELETE, {
instanceName,
instanceId: waInstances.instanceId,
});
} catch (error) {
this.logger.error(error);
}
this.eventEmitter.emit('remove.instance', instanceName, 'inner');
return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } };
} catch (error) {
throw new BadRequestException(error.toString());
}
}
}