diff --git a/package.json b/package.json index bb469865..6f92aaf3 100644 --- a/package.json +++ b/package.json @@ -64,12 +64,13 @@ "js-yaml": "^4.1.0", "jsonschema": "^1.4.1", "jsonwebtoken": "^8.5.1", + "libphonenumber-js": "^1.10.39", "link-preview-js": "^3.0.4", "mongoose": "^6.10.5", "node-cache": "^5.1.2", "node-mime-types": "^1.1.0", "pino": "^8.11.0", - "proxy-agent": "^6.2.1", + "proxy-agent": "^6.3.0", "qrcode": "^1.5.1", "qrcode-terminal": "^0.12.0", "redis": "^4.6.5", diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 7f101a96..32e25fac 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -987,3 +987,14 @@ export const typebotSchema: JSONSchema7 = { required: ['enabled', 'url', 'typebot', 'expire'], ...isNotEmpty('enabled', 'url', 'typebot', 'expire'), }; + +export const proxySchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + proxy: { type: 'string' }, + }, + required: ['enabled', 'proxy'], + ...isNotEmpty('enabled', 'proxy'), +}; diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 9c4a65df..b5d99687 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -13,6 +13,7 @@ import { ChatwootService } from '../services/chatwoot.service'; import { WAMonitoringService } from '../services/monitor.service'; import { RabbitmqService } from '../services/rabbitmq.service'; import { SettingsService } from '../services/settings.service'; +import { TypebotService } from '../services/typebot.service'; import { WebhookService } from '../services/webhook.service'; import { WebsocketService } from '../services/websocket.service'; import { WAStartupService } from '../services/whatsapp.service'; @@ -30,6 +31,7 @@ export class InstanceController { private readonly settingsService: SettingsService, private readonly websocketService: WebsocketService, private readonly rabbitmqService: RabbitmqService, + private readonly typebotService: TypebotService, private readonly cache: RedisCache, ) {} @@ -59,6 +61,9 @@ export class InstanceController { websocket_events, rabbitmq_enabled, rabbitmq_events, + typebot_url, + typebot, + typebot_expire, }: InstanceDto) { try { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -223,6 +228,25 @@ export class InstanceController { } } + if (typebot_url) { + try { + if (!isURL(typebot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in typebot_url'); + } + + this.logger.verbose('creating typebot'); + + this.typebotService.create(instance, { + enabled: true, + url: typebot_url, + typebot: typebot, + expire: typebot_expire, + }); + } catch (error) { + this.logger.log(error); + } + } + this.logger.verbose('creating settings'); const settings: wa.LocalSettings = { reject_call: reject_call || false, @@ -266,6 +290,12 @@ export class InstanceController { enabled: rabbitmq_enabled, events: rabbitmqEvents, }, + typebot: { + enabled: typebot_url ? true : false, + url: typebot_url, + typebot, + expire: typebot_expire, + }, settings, qrcode: getQrcode, }; @@ -349,6 +379,12 @@ export class InstanceController { enabled: rabbitmq_enabled, events: rabbitmqEvents, }, + typebot: { + enabled: typebot_url ? true : false, + url: typebot_url, + typebot, + expire: typebot_expire, + }, settings, chatwoot: { enabled: true, diff --git a/src/whatsapp/controllers/proxy.controller.ts b/src/whatsapp/controllers/proxy.controller.ts new file mode 100644 index 00000000..1656d830 --- /dev/null +++ b/src/whatsapp/controllers/proxy.controller.ts @@ -0,0 +1,26 @@ +import { Logger } from '../../config/logger.config'; +import { InstanceDto } from '../dto/instance.dto'; +import { ProxyDto } from '../dto/proxy.dto'; +import { ProxyService } from '../services/proxy.service'; + +const logger = new Logger('ProxyController'); + +export class ProxyController { + constructor(private readonly proxyService: ProxyService) {} + + public async createProxy(instance: InstanceDto, data: ProxyDto) { + logger.verbose('requested createProxy from ' + instance.instanceName + ' instance'); + + if (!data.enabled) { + logger.verbose('proxy disabled'); + data.proxy = ''; + } + + return this.proxyService.create(instance, data); + } + + public async findProxy(instance: InstanceDto) { + logger.verbose('requested findProxy from ' + instance.instanceName + ' instance'); + return this.proxyService.find(instance); + } +} diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 0788b9a7..a58a20d5 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -22,4 +22,9 @@ export class InstanceDto { websocket_events?: string[]; rabbitmq_enabled?: boolean; rabbitmq_events?: string[]; + typebot_url?: string; + typebot?: string; + typebot_expire?: number; + proxy_enabled?: boolean; + proxy_proxy?: string; } diff --git a/src/whatsapp/dto/proxy.dto.ts b/src/whatsapp/dto/proxy.dto.ts new file mode 100644 index 00000000..0b6b2e70 --- /dev/null +++ b/src/whatsapp/dto/proxy.dto.ts @@ -0,0 +1,4 @@ +export class ProxyDto { + enabled: boolean; + proxy: string; +} diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index c44ffe7e..5d71911d 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -3,6 +3,7 @@ export * from './chat.model'; export * from './chatwoot.model'; export * from './contact.model'; export * from './message.model'; +export * from './proxy.model'; export * from './rabbitmq.model'; export * from './settings.model'; export * from './typebot.model'; diff --git a/src/whatsapp/models/proxy.model.ts b/src/whatsapp/models/proxy.model.ts new file mode 100644 index 00000000..3dea4f0c --- /dev/null +++ b/src/whatsapp/models/proxy.model.ts @@ -0,0 +1,18 @@ +import { Schema } from 'mongoose'; + +import { dbserver } from '../../libs/db.connect'; + +export class ProxyRaw { + _id?: string; + enabled?: boolean; + proxy?: string; +} + +const proxySchema = new Schema({ + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + proxy: { type: String, required: true }, +}); + +export const ProxyModel = dbserver?.model(ProxyRaw.name, proxySchema, 'proxy'); +export type IProxyModel = typeof ProxyModel; diff --git a/src/whatsapp/repository/proxy.repository.ts b/src/whatsapp/repository/proxy.repository.ts new file mode 100644 index 00000000..169e798f --- /dev/null +++ b/src/whatsapp/repository/proxy.repository.ts @@ -0,0 +1,62 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { ConfigService } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { IProxyModel, ProxyRaw } from '../models'; + +export class ProxyRepository extends Repository { + constructor(private readonly proxyModel: IProxyModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('ProxyRepository'); + + public async create(data: ProxyRaw, instance: string): Promise { + try { + this.logger.verbose('creating proxy'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving proxy to db'); + const insert = await this.proxyModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('proxy saved to db: ' + insert.modifiedCount + ' proxy'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving proxy to store'); + + this.writeStore({ + path: join(this.storePath, 'proxy'), + fileName: instance, + data, + }); + + this.logger.verbose('proxy saved to store in path: ' + join(this.storePath, 'proxy') + '/' + instance); + + this.logger.verbose('proxy created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding proxy'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding proxy in db'); + return await this.proxyModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding proxy in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'proxy', instance + '.json'), { + encoding: 'utf-8', + }), + ) as ProxyRaw; + } catch (error) { + return {}; + } + } +} diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index e724ef02..2cd4931e 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -10,6 +10,7 @@ import { ChatwootRepository } from './chatwoot.repository'; import { ContactRepository } from './contact.repository'; import { MessageRepository } from './message.repository'; import { MessageUpRepository } from './messageUp.repository'; +import { ProxyRepository } from './proxy.repository'; import { RabbitmqRepository } from './rabbitmq.repository'; import { SettingsRepository } from './settings.repository'; import { TypebotRepository } from './typebot.repository'; @@ -27,6 +28,7 @@ export class RepositoryBroker { public readonly websocket: WebsocketRepository, public readonly rabbitmq: RabbitmqRepository, public readonly typebot: TypebotRepository, + public readonly proxy: ProxyRepository, public readonly auth: AuthRepository, private configService: ConfigService, dbServer?: MongoClient, @@ -60,6 +62,7 @@ export class RepositoryBroker { const websocketDir = join(storePath, 'websocket'); const rabbitmqDir = join(storePath, 'rabbitmq'); const typebotDir = join(storePath, 'typebot'); + const proxyDir = join(storePath, 'proxy'); const tempDir = join(storePath, 'temp'); if (!fs.existsSync(authDir)) { @@ -106,6 +109,10 @@ export class RepositoryBroker { this.logger.verbose('creating typebot dir: ' + typebotDir); fs.mkdirSync(typebotDir, { recursive: true }); } + if (!fs.existsSync(proxyDir)) { + this.logger.verbose('creating proxy dir: ' + proxyDir); + fs.mkdirSync(proxyDir, { recursive: true }); + } if (!fs.existsSync(tempDir)) { this.logger.verbose('creating temp dir: ' + tempDir); fs.mkdirSync(tempDir, { recursive: true }); diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index f5ad08e8..a84e815d 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -8,6 +8,7 @@ import { ChatRouter } from './chat.router'; import { ChatwootRouter } from './chatwoot.router'; import { GroupRouter } from './group.router'; import { InstanceRouter } from './instance.router'; +import { ProxyRouter } from './proxy.router'; import { RabbitmqRouter } from './rabbitmq.router'; import { MessageRouter } from './sendMessage.router'; import { SettingsRouter } from './settings.router'; @@ -50,6 +51,7 @@ router .use('/settings', new SettingsRouter(...guards).router) .use('/websocket', new WebsocketRouter(...guards).router) .use('/rabbitmq', new RabbitmqRouter(...guards).router) - .use('/typebot', new TypebotRouter(...guards).router); + .use('/typebot', new TypebotRouter(...guards).router) + .use('/proxy', new ProxyRouter(...guards).router); export { HttpStatus, router }; diff --git a/src/whatsapp/routers/proxy.router.ts b/src/whatsapp/routers/proxy.router.ts new file mode 100644 index 00000000..2ae0141b --- /dev/null +++ b/src/whatsapp/routers/proxy.router.ts @@ -0,0 +1,52 @@ +import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; +import { instanceNameSchema, proxySchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { ProxyDto } from '../dto/proxy.dto'; +import { proxyController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; + +const logger = new Logger('ProxyRouter'); + +export class ProxyRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setProxy'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: proxySchema, + ClassRef: ProxyDto, + execute: (instance, data) => proxyController.createProxy(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findProxy'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => proxyController.findProxy(instance), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/services/proxy.service.ts b/src/whatsapp/services/proxy.service.ts new file mode 100644 index 00000000..c6631671 --- /dev/null +++ b/src/whatsapp/services/proxy.service.ts @@ -0,0 +1,33 @@ +import { Logger } from '../../config/logger.config'; +import { InstanceDto } from '../dto/instance.dto'; +import { ProxyDto } from '../dto/proxy.dto'; +import { ProxyRaw } from '../models'; +import { WAMonitoringService } from './monitor.service'; + +export class ProxyService { + constructor(private readonly waMonitor: WAMonitoringService) {} + + private readonly logger = new Logger(ProxyService.name); + + public create(instance: InstanceDto, data: ProxyDto) { + this.logger.verbose('create proxy: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setProxy(data); + + return { proxy: { ...instance, proxy: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find proxy: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findProxy(); + + if (Object.keys(result).length === 0) { + throw new Error('Proxy not found'); + } + + return result; + } catch (error) { + return { enabled: false, proxy: '' }; + } + } +} diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index a1237b49..f71300ea 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -44,6 +44,8 @@ import { getMIMEType } from 'node-mime-types'; import { release } from 'os'; import { join } from 'path'; import P from 'pino'; +import { ProxyAgent } from 'proxy-agent'; +// import { ProxyAgent } from 'proxy-agent'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; import sharp from 'sharp'; @@ -111,7 +113,7 @@ import { SendTextDto, StatusMessage, } from '../dto/sendMessage.dto'; -import { RabbitmqRaw, SettingsRaw, TypebotRaw } from '../models'; +import { ProxyRaw, RabbitmqRaw, SettingsRaw, TypebotRaw } from '../models'; import { ChatRaw } from '../models/chat.model'; import { ChatwootRaw } from '../models/chatwoot.model'; import { ContactRaw } from '../models/contact.model'; @@ -148,6 +150,7 @@ export class WAStartupService { private readonly localWebsocket: wa.LocalWebsocket = {}; private readonly localRabbitmq: wa.LocalRabbitmq = {}; private readonly localTypebot: wa.LocalTypebot = {}; + private readonly localProxy: wa.LocalProxy = {}; public stateConnection: wa.StateConnection = { state: 'close' }; public readonly storePath = join(ROOT_DIR, 'store'); private readonly msgRetryCounterCache: CacheStore = new NodeCache(); @@ -529,6 +532,41 @@ export class WAStartupService { return data; } + private async loadProxy() { + this.logger.verbose('Loading proxy'); + const data = await this.repository.proxy.find(this.instanceName); + + this.localProxy.enabled = data?.enabled; + this.logger.verbose(`Proxy enabled: ${this.localProxy.enabled}`); + + this.localProxy.proxy = data?.proxy; + this.logger.verbose(`Proxy proxy: ${this.localProxy.proxy}`); + + this.logger.verbose('Proxy loaded'); + } + + public async setProxy(data: ProxyRaw) { + this.logger.verbose('Setting proxy'); + await this.repository.proxy.create(data, this.instanceName); + this.logger.verbose(`Proxy proxy: ${data.proxy}`); + Object.assign(this.localProxy, data); + this.logger.verbose('Proxy set'); + + this.client?.ws?.close(); + } + + public async findProxy() { + this.logger.verbose('Finding proxy'); + const data = await this.repository.proxy.find(this.instanceName); + + if (!data) { + this.logger.verbose('Proxy not found'); + throw new NotFoundException('Proxy not found'); + } + + return data; + } + public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; @@ -990,6 +1028,7 @@ export class WAStartupService { this.loadWebsocket(); this.loadRabbitmq(); this.loadTypebot(); + this.loadProxy(); this.instance.authState = await this.defineAuthState(); @@ -999,7 +1038,18 @@ export class WAStartupService { const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; this.logger.verbose('Browser: ' + JSON.stringify(browser)); + let options; + + if (this.localProxy.enabled) { + this.logger.verbose('Proxy enabled'); + options = { + agent: new ProxyAgent(this.localProxy.proxy as any), + fetchAgent: new ProxyAgent(this.localProxy.proxy as any), + }; + } + const socketConfig: UserFacingSocketConfig = { + ...options, auth: { creds: this.instance.authState.state.creds, keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index d54b0366..84113b42 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -94,6 +94,11 @@ export declare namespace wa { sessions?: Session[]; }; + export type LocalProxy = { + enabled?: boolean; + proxy?: string; + }; + export type StateConnection = { instance?: string; state?: WAConnectionState | 'refused'; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 2cb045ae..d8ed5a62 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -7,6 +7,7 @@ import { ChatController } from './controllers/chat.controller'; import { ChatwootController } from './controllers/chatwoot.controller'; import { GroupController } from './controllers/group.controller'; import { InstanceController } from './controllers/instance.controller'; +import { ProxyController } from './controllers/proxy.controller'; import { RabbitmqController } from './controllers/rabbitmq.controller'; import { SendMessageController } from './controllers/sendMessage.controller'; import { SettingsController } from './controllers/settings.controller'; @@ -21,6 +22,7 @@ import { ContactModel, MessageModel, MessageUpModel, + ProxyModel, RabbitmqModel, SettingsModel, TypebotModel, @@ -33,6 +35,7 @@ import { ChatwootRepository } from './repository/chatwoot.repository'; import { ContactRepository } from './repository/contact.repository'; import { MessageRepository } from './repository/message.repository'; import { MessageUpRepository } from './repository/messageUp.repository'; +import { ProxyRepository } from './repository/proxy.repository'; import { RabbitmqRepository } from './repository/rabbitmq.repository'; import { RepositoryBroker } from './repository/repository.manager'; import { SettingsRepository } from './repository/settings.repository'; @@ -42,6 +45,7 @@ import { WebsocketRepository } from './repository/websocket.repository'; import { AuthService } from './services/auth.service'; import { ChatwootService } from './services/chatwoot.service'; import { WAMonitoringService } from './services/monitor.service'; +import { ProxyService } from './services/proxy.service'; import { RabbitmqService } from './services/rabbitmq.service'; import { SettingsService } from './services/settings.service'; import { TypebotService } from './services/typebot.service'; @@ -57,6 +61,7 @@ const messageUpdateRepository = new MessageUpRepository(MessageUpModel, configSe const typebotRepository = new TypebotRepository(TypebotModel, configService); const webhookRepository = new WebhookRepository(WebhookModel, configService); const websocketRepository = new WebsocketRepository(WebsocketModel, configService); +const proxyRepository = new ProxyRepository(ProxyModel, configService); const rabbitmqRepository = new RabbitmqRepository(RabbitmqModel, configService); const chatwootRepository = new ChatwootRepository(ChatwootModel, configService); const settingsRepository = new SettingsRepository(SettingsModel, configService); @@ -73,6 +78,7 @@ export const repository = new RepositoryBroker( websocketRepository, rabbitmqRepository, typebotRepository, + proxyRepository, authRepository, configService, dbserver?.getClient(), @@ -96,6 +102,10 @@ const websocketService = new WebsocketService(waMonitor); export const websocketController = new WebsocketController(websocketService); +const proxyService = new ProxyService(waMonitor); + +export const proxyController = new ProxyController(proxyService); + const rabbitmqService = new RabbitmqService(waMonitor); export const rabbitmqController = new RabbitmqController(rabbitmqService); @@ -119,6 +129,7 @@ export const instanceController = new InstanceController( settingsService, websocketService, rabbitmqService, + typebotService, cache, ); export const viewsController = new ViewsController(waMonitor, configService);