From 5a3f5f60b62cd69d84c7891b1b306659559acce8 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 20 Jul 2023 10:08:57 -0300 Subject: [PATCH 01/97] test: container mode with chatwoot --- src/whatsapp/whatsapp.module.ts | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 46f8ecd1..dbc80427 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -116,16 +116,16 @@ export async function initInstance() { configService.get('AUTHENTICATION').INSTANCE.WEBHOOK_URL; logger.verbose('Instance webhook: ' + instanceWebhook); - const chatwootAccountId = - configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_ACCOUNT_ID; - logger.verbose('Chatwoot account id: ' + chatwootAccountId); + // const chatwootAccountId = + // configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_ACCOUNT_ID; + // logger.verbose('Chatwoot account id: ' + chatwootAccountId); - const chatwootToken = - configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_TOKEN; - logger.verbose('Chatwoot token: ' + chatwootToken); + // const chatwootToken = + // configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_TOKEN; + // logger.verbose('Chatwoot token: ' + chatwootToken); - const chatwootUrl = configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_URL; - logger.verbose('Chatwoot url: ' + chatwootUrl); + // const chatwootUrl = configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_URL; + // logger.verbose('Chatwoot url: ' + chatwootUrl); instance.instanceName = instanceName; @@ -148,21 +148,21 @@ export async function initInstance() { } } - if (chatwootUrl && chatwootToken && chatwootAccountId) { - logger.verbose('Creating chatwoot for instance: ' + instanceName); - try { - chatwootService.create(instance, { - enabled: true, - url: chatwootUrl, - token: chatwootToken, - account_id: chatwootAccountId, - sign_msg: false, - }); - logger.verbose('Chatwoot created'); - } catch (error) { - logger.log(error); - } - } + // if (chatwootUrl && chatwootToken && chatwootAccountId) { + // logger.verbose('Creating chatwoot for instance: ' + instanceName); + // try { + // chatwootService.create(instance, { + // enabled: true, + // url: chatwootUrl, + // token: chatwootToken, + // account_id: chatwootAccountId, + // sign_msg: false, + // }); + // logger.verbose('Chatwoot created'); + // } catch (error) { + // logger.log(error); + // } + // } try { const state = instance.connectionStatus?.state; From 4a1aa9130bb2e1b2d9a26104d57bea6e6de82894 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 08:26:05 -0300 Subject: [PATCH 02/97] version: 1.3.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c182838..b56b045d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.3.0", + "version": "1.3.1", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 763e30bd1d4f4c4547ec65ba61018e89bc39e156 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 09:34:39 -0300 Subject: [PATCH 03/97] fix: fix in update settings that needed to restart after updated --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/whatsapp/services/whatsapp.service.ts | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d70a8685..384a28af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.3.2 (homolog) + +### Fixed + +* Fix in update settings that needed to restart after updated + # 1.3.1 (2023-07-20 07:48) ### Fixed diff --git a/package.json b/package.json index b56b045d..7bc6f476 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.3.1", + "version": "1.3.2", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 65cb40bb..6fead74a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2364,6 +2364,9 @@ export class WAStartupService { public async fetchMessages(query: MessageQuery) { this.logger.verbose('Fetching messages'); if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } query.where.owner = this.instance.name; } else { query = { @@ -2379,6 +2382,9 @@ export class WAStartupService { public async fetchStatusMessage(query: MessageUpQuery) { this.logger.verbose('Fetching status messages'); if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } query.where.owner = this.instance.name; } else { query = { @@ -2423,8 +2429,19 @@ export class WAStartupService { this.logger.verbose('Groups add privacy updated'); // reinicia a instancia + this.client?.ws?.close(); - return { update: 'success', data: await this.client.fetchPrivacySettings() }; + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; } catch (error) { throw new InternalServerErrorException( 'Error updating privacy settings', From 796287a776cc900e5b3d9fcaaf8363419e3f8f21 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 11:38:36 -0300 Subject: [PATCH 04/97] fix: Adjustments to search endpoint for contacts, chats, messages and Status messages --- CHANGELOG.md | 2 ++ src/whatsapp/repository/auth.repository.ts | 2 +- src/whatsapp/services/monitor.service.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 23 ++++++++++++++++++++-- src/whatsapp/types/wa.types.ts | 1 + 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 384a28af..875d7ac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Fixed * Fix in update settings that needed to restart after updated +* Correction in the use of the api with mongodb +* Adjustments to search endpoint for contacts, chats, messages and Status messages # 1.3.1 (2023-07-20 07:48) diff --git a/src/whatsapp/repository/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index c795737c..0d7e177f 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -1,5 +1,5 @@ import { join } from 'path'; -import { Auth, ConfigService } from '../../config/env.config'; +import { Auth, ConfigService, Database } from '../../config/env.config'; import { IInsert, Repository } from '../abstract/abstract.repository'; import { IAuthModel, AuthRaw } from '../models'; import { readFileSync } from 'fs'; diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 8b347c21..8fdac88a 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -90,7 +90,7 @@ export class WAMonitoringService { const findChatwoot = await this.waInstances[key].findChatwoot(); - if (findChatwoot.enabled) { + if (findChatwoot && findChatwoot.enabled) { chatwoot = { ...findChatwoot, webhook_url: `${urlServer}/chatwoot/webhook/${key}`, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 6fead74a..8035bbfb 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -328,7 +328,7 @@ export class WAStartupService { if (!data) { this.logger.verbose('Chatwoot not found'); - throw new NotFoundException('Chatwoot not found'); + return null; } this.logger.verbose(`Chatwoot account id: ${data.account_id}`); @@ -351,7 +351,7 @@ export class WAStartupService { const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; const tokenStore = await this.repository.auth.find(this.instanceName); - const instanceApikey = tokenStore.apikey || 'Apikey not found'; + const instanceApikey = tokenStore?.apikey || 'Apikey not found'; const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; @@ -1190,6 +1190,22 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); return; } @@ -2351,6 +2367,9 @@ export class WAStartupService { this.logger.verbose('Fetching contacts'); if (query?.where) { query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } } else { query = { where: { diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 169df515..6869545f 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -63,6 +63,7 @@ export declare namespace wa { | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' + | 'DELETED' | 'PLAYED'; } From 897f8164b90af48202e4b9d696429afef2855a97 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 12:13:03 -0300 Subject: [PATCH 05/97] fix: Now when deleting the instance, the data referring to it in mongodb is also deleted --- CHANGELOG.md | 1 + src/whatsapp/services/monitor.service.ts | 31 +++++++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875d7ac4..2311edac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Fix in update settings that needed to restart after updated * Correction in the use of the api with mongodb * Adjustments to search endpoint for contacts, chats, messages and Status messages +* Now when deleting the instance, the data referring to it in mongodb is also deleted # 1.3.1 (2023-07-20 07:48) diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 8fdac88a..6aa43b70 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -18,6 +18,16 @@ import { Db } from 'mongodb'; import { initInstance } from '../whatsapp.module'; import { RedisCache } from '../../db/redis.client'; import { execSync } from 'child_process'; +import { dbserver } from '../../db/db.connect'; +import mongoose from 'mongoose'; +import { + AuthModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + WebhookModel, +} from '../models'; export class WAMonitoringService { constructor( @@ -45,6 +55,8 @@ export class WAMonitoringService { private dbInstance: Db; + private dbStore = dbserver; + private readonly logger = new Logger(WAMonitoringService.name); public readonly waInstances: Record = {}; @@ -218,11 +230,8 @@ export class WAMonitoringService { } public async cleaningStoreFiles(instanceName: string) { - this.logger.verbose('cleaning store files instance: ' + instanceName); - if (!this.db.ENABLED) { - const instance = this.waInstances[instanceName]; - + this.logger.verbose('cleaning store files instance: ' + instanceName); rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); @@ -233,7 +242,21 @@ export class WAMonitoringService { execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); + + return; } + + this.logger.verbose('cleaning store database instance: ' + instanceName); + + await AuthModel.deleteMany({ owner: instanceName }); + await ContactModel.deleteMany({ owner: instanceName }); + await MessageModel.deleteMany({ owner: instanceName }); + await MessageUpModel.deleteMany({ owner: instanceName }); + await AuthModel.deleteMany({ _id: instanceName }); + await WebhookModel.deleteMany({ _id: instanceName }); + await ChatwootModel.deleteMany({ _id: instanceName }); + + return; } public async loadInstance() { From d7f264c1c2265d145b0ca0542cd1984600974080 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 12:26:43 -0300 Subject: [PATCH 06/97] fix: It is now validated if the instance name contains uppercase and special characters --- CHANGELOG.md | 1 + src/whatsapp/controllers/instance.controller.ts | 6 ++++++ src/whatsapp/services/whatsapp.service.ts | 2 -- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2311edac..d9b5ac6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Correction in the use of the api with mongodb * Adjustments to search endpoint for contacts, chats, messages and Status messages * Now when deleting the instance, the data referring to it in mongodb is also deleted +* It is now validated if the instance name contains uppercase and special characters # 1.3.1 (2023-07-20 07:48) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index f0adb3a3..ef0c00aa 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -42,6 +42,12 @@ export class InstanceController { }: InstanceDto) { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException( + 'The instance name must be lowercase and without special characters', + ); + } + const mode = this.configService.get('AUTHENTICATION').INSTANCE.MODE; if (mode === 'container') { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 8035bbfb..549dd7d6 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1412,8 +1412,6 @@ export class WAStartupService { } if (Number(countryCode) === 52 || Number(countryCode) === 54) { - console.log('numero mexicano'); - const formattedMXARNumber = this.formatMXOrARNumber(number); if (formattedMXARNumber !== number) { From 091b920a222821b20290ad76650c49b7cbd62f99 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 12:40:58 -0300 Subject: [PATCH 07/97] fix: It is now validated if the instance name contains uppercase and special characters --- src/whatsapp/repository/repository.manager.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index aa0fbd65..6c2a3091 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -55,7 +55,6 @@ export class RepositoryBroker { const chatwootDir = join(storePath, 'chatwoot'); const tempDir = join(storePath, 'temp'); - // Check if directories exist, create them if not if (!fs.existsSync(authDir)) { this.logger.verbose('creating auth dir: ' + authDir); fs.mkdirSync(authDir, { recursive: true }); @@ -91,6 +90,21 @@ export class RepositoryBroker { } catch (error) { this.logger.error(error); } + } else { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + try { + } catch (error) { + this.logger.error(error); + } } } } From 19039aa281a3a192f833572f8b2355877f88cf73 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 13:12:55 -0300 Subject: [PATCH 08/97] fix: For compatibility reasons, container mode has been removed --- CHANGELOG.md | 1 + Docker/.env.example | 10 - src/config/env.config.ts | 19 +- src/dev-env.yml | 12 +- .../controllers/instance.controller.ts | 375 ++++++------------ src/whatsapp/services/monitor.service.ts | 4 - src/whatsapp/services/whatsapp.service.ts | 11 +- src/whatsapp/whatsapp.module.ts | 107 ----- 8 files changed, 121 insertions(+), 418 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9b5ac6b..2a864fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Adjustments to search endpoint for contacts, chats, messages and Status messages * Now when deleting the instance, the data referring to it in mongodb is also deleted * It is now validated if the instance name contains uppercase and special characters +* For compatibility reasons, container mode has been removed # 1.3.1 (2023-07-20 07:48) diff --git a/Docker/.env.example b/Docker/.env.example index 81eee8ca..a3782d4e 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -97,13 +97,3 @@ AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true # seconds - 3600s ===1h | zero (0) - never expires AUTHENTICATION_JWT_EXPIRIN_IN=0 AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG' -# Set the instance name and webhook url to create an instance in init the application -# With this option activated, you work with a url per webhook event, respecting the local url and the name of each event -# container or server -AUTHENTICATION_INSTANCE_MODE=server -# if you are using container mode, set the container name and the webhook url to default instance -AUTHENTICATION_INSTANCE_NAME=evolution -AUTHENTICATION_INSTANCE_WEBHOOK_URL='' -AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 -AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 -AUTHENTICATION_INSTANCE_CHATWOOT_URL='' diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7221f474..88b718de 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -94,20 +94,12 @@ export type EventsWebhook = { export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; -export type Instance = { - NAME: string; - WEBHOOK_URL: string; - MODE: string; - CHATWOOT_ACCOUNT_ID: string; - CHATWOOT_TOKEN: string; - CHATWOOT_URL: string; -}; + export type Auth = { API_KEY: ApiKey; EXPOSE_IN_FETCH_INSTANCES: boolean; JWT: Jwt; TYPE: 'jwt' | 'apikey'; - INSTANCE: Instance; }; export type DelInstance = number | boolean; @@ -276,15 +268,6 @@ export class ConfigService { : 3600, SECRET: process.env.AUTHENTICATION_JWT_SECRET, }, - INSTANCE: { - NAME: process.env.AUTHENTICATION_INSTANCE_NAME, - WEBHOOK_URL: process.env.AUTHENTICATION_INSTANCE_WEBHOOK_URL, - MODE: process.env.AUTHENTICATION_INSTANCE_MODE, - CHATWOOT_ACCOUNT_ID: - process.env.AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID || '', - CHATWOOT_TOKEN: process.env.AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN || '', - CHATWOOT_URL: process.env.AUTHENTICATION_INSTANCE_CHATWOOT_URL || '', - }, }, }; } diff --git a/src/dev-env.yml b/src/dev-env.yml index c0f907fc..41368ea4 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -136,14 +136,4 @@ AUTHENTICATION: # Set the secret key to encrypt and decrypt your token and its expiration time. JWT: EXPIRIN_IN: 0 # seconds - 3600s === 1h | zero (0) - never expires - SECRET: L=0YWt]b2w[WF>#>:&E` - # Set the instance name and webhook url to create an instance in init the application - INSTANCE: - # With this option activated, you work with a url per webhook event, respecting the local url and the name of each event - MODE: server # container or server - # if you are using container mode, set the container name and the webhook url to default instance - NAME: evolution - WEBHOOK_URL: - CHATWOOT_ACCOUNT_ID: 1 - CHATWOOT_TOKEN: 123456 - CHATWOOT_URL: + SECRET: L=0YWt]b2w[WF>#>:&E` \ No newline at end of file diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index ef0c00aa..75911848 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -48,273 +48,80 @@ export class InstanceController { ); } - const mode = this.configService.get('AUTHENTICATION').INSTANCE.MODE; + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); - if (mode === 'container') { - this.logger.verbose('container mode'); + this.logger.verbose('creating instance'); + const instance = new WAStartupService( + this.configService, + this.eventEmitter, + this.repository, + this.cache, + ); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); - if (Object.keys(this.waMonitor.waInstances).length > 0) { - throw new BadRequestException([ - 'Instance already created', - 'Only one instance can be created', - ]); + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); + + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); + + this.logger.verbose('hash: ' + hash + ' generated'); + + let getEvents: string[]; + + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); } - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); - - this.logger.verbose('creating instance'); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); - - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); - - this.logger.verbose('hash: ' + hash + ' generated'); - - let getEvents: string[]; - - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - this.logger.verbose('instance created'); - this.logger.verbose({ - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - events: getEvents, - }); - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - events: getEvents, - }; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - const urlServer = this.configService.get('SERVER').URL; - + this.logger.verbose('creating webhook'); try { - this.chatwootService.create(instance, { + this.webhookService.create(instance, { enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, + url: webhook, + events, + webhook_by_events, }); - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - ); + getEvents = (await this.webhookService.find(instance)).events; } catch (error) { this.logger.log(error); } + } - return { + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(); + await delay(2000); + getQrcode = instance.qrCode; + } + + this.logger.verbose('instance created'); + this.logger.verbose({ instance: { instanceName: instance.instanceName, status: 'created', }, hash, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; - } else { - this.logger.verbose('server mode'); - - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); - - this.logger.verbose('creating instance'); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); - - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); - - this.logger.verbose('hash: ' + hash + ' generated'); - - let getEvents: string[]; - - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } - - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; - - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(); - await delay(2000); - getQrcode = instance.qrCode; - } - - this.logger.verbose('instance created'); - this.logger.verbose({ - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - qrcode: getQrcode, - }); - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - qrcode: getQrcode, - }; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - ); - } catch (error) { - this.logger.log(error); - } + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }); return { instance: { @@ -325,17 +132,67 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, + qrcode: getQrcode, }; } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + ); + } catch (error) { + this.logger.log(error); + } + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, + }; } public async connectToWhatsapp({ instanceName }: InstanceDto) { diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 6aa43b70..6c31ad58 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -15,7 +15,6 @@ import { import { RepositoryBroker } from '../repository/repository.manager'; import { NotFoundException } from '../../exceptions'; import { Db } from 'mongodb'; -import { initInstance } from '../whatsapp.module'; import { RedisCache } from '../../db/redis.client'; import { execSync } from 'child_process'; import { dbserver } from '../../db/db.connect'; @@ -287,7 +286,6 @@ export class WAMonitoringService { keys.forEach(async (k) => await set(k.split(':')[1])); } else { this.logger.verbose('no instance keys found'); - initInstance(); } return; } @@ -303,7 +301,6 @@ export class WAMonitoringService { ); } else { this.logger.verbose('no collections found'); - initInstance(); } return; } @@ -324,7 +321,6 @@ export class WAMonitoringService { await set(dirent.name); } else { this.logger.verbose('no instance files found'); - initInstance(); } } } catch (error) { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 549dd7d6..3851f067 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -346,7 +346,6 @@ export class WAStartupService { const serverUrl = this.configService.get('SERVER').URL; const we = event.replace(/[\.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); - const instance = this.configService.get('AUTHENTICATION').INSTANCE; const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; @@ -355,7 +354,7 @@ export class WAStartupService { const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - if (local && instance.MODE !== 'container') { + if (local) { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { this.logger.verbose('Sending data to webhook local'); let baseURL; @@ -432,13 +431,7 @@ export class WAStartupService { globalURL = globalWebhook.URL; } - let localUrl; - - if (instance.MODE === 'container') { - localUrl = instance.WEBHOOK_URL; - } else { - localUrl = this.localWebhook.url; - } + const localUrl = this.localWebhook.url; if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { const logData = { diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index dbc80427..c91ee9c4 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -91,111 +91,4 @@ export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); export const groupController = new GroupController(waMonitor); -export async function initInstance() { - const instance = new WAStartupService(configService, eventEmitter, repository, cache); - - const mode = configService.get('AUTHENTICATION').INSTANCE.MODE; - - logger.verbose('Sending data webhook for event: ' + Events.APPLICATION_STARTUP); - instance.sendDataWebhook( - Events.APPLICATION_STARTUP, - { - message: 'Application startup', - mode, - }, - false, - ); - - if (mode === 'container') { - logger.verbose('Application startup in container mode'); - - const instanceName = configService.get('AUTHENTICATION').INSTANCE.NAME; - logger.verbose('Instance name: ' + instanceName); - - const instanceWebhook = - configService.get('AUTHENTICATION').INSTANCE.WEBHOOK_URL; - logger.verbose('Instance webhook: ' + instanceWebhook); - - // const chatwootAccountId = - // configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_ACCOUNT_ID; - // logger.verbose('Chatwoot account id: ' + chatwootAccountId); - - // const chatwootToken = - // configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_TOKEN; - // logger.verbose('Chatwoot token: ' + chatwootToken); - - // const chatwootUrl = configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_URL; - // logger.verbose('Chatwoot url: ' + chatwootUrl); - - instance.instanceName = instanceName; - - waMonitor.waInstances[instance.instanceName] = instance; - waMonitor.delInstanceTime(instance.instanceName); - - const hash = await authService.generateHash({ - instanceName: instance.instanceName, - token: configService.get('AUTHENTICATION').API_KEY.KEY, - }); - logger.verbose('Hash generated: ' + hash); - - if (instanceWebhook) { - logger.verbose('Creating webhook for instance: ' + instanceName); - try { - webhookService.create(instance, { enabled: true, url: instanceWebhook }); - logger.verbose('Webhook created'); - } catch (error) { - logger.log(error); - } - } - - // if (chatwootUrl && chatwootToken && chatwootAccountId) { - // logger.verbose('Creating chatwoot for instance: ' + instanceName); - // try { - // chatwootService.create(instance, { - // enabled: true, - // url: chatwootUrl, - // token: chatwootToken, - // account_id: chatwootAccountId, - // sign_msg: false, - // }); - // logger.verbose('Chatwoot created'); - // } catch (error) { - // logger.log(error); - // } - // } - - try { - const state = instance.connectionStatus?.state; - - switch (state) { - case 'close': - await instance.connectToWhatsapp(); - await delay(2000); - return instance.qrCode; - case 'connecting': - return instance.qrCode; - default: - return await this.connectionState({ instanceName }); - } - } catch (error) { - logger.log(error); - } - - const result = { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook: instanceWebhook, - }; - - logger.info(result); - - return result; - } - - return null; -} - logger.info('Module - ON'); From f847f38812fe4d5de3be8681557ebf2196bd6d25 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 15:13:49 -0300 Subject: [PATCH 09/97] fix: added docker-compose files example --- Docker/.env.example | 24 ++--- Dockerfile | 4 +- ...ompose.yaml => docker-compose.yaml.example | 2 + docker-compose.yaml.example.complete | 88 +++++++++++++++++++ 4 files changed, 104 insertions(+), 14 deletions(-) rename docker-compose.yaml => docker-compose.yaml.example (92%) create mode 100644 docker-compose.yaml.example.complete diff --git a/Docker/.env.example b/Docker/.env.example index a3782d4e..c3dbe505 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -1,13 +1,13 @@ # Server URL - Set your application url -SERVER_URL='http://localhost:8080' +SERVER_URL=http://localhost:8080 # Cors - * for all or set separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' -CORS_ORIGIN='*' -CORS_METHODS='POST,GET,PUT,DELETE' +CORS_ORIGIN=* +CORS_METHODS=POST,GET,PUT,DELETE CORS_CREDENTIALS=true # Determine the logs to be displayed -LOG_LEVEL='ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS' +LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS LOG_COLOR=true # Log Baileys - "fatal" | "error" | "warn" | "info" | "debug" | "trace" LOG_BAILEYS=error @@ -31,9 +31,9 @@ CLEAN_STORE_CONTACTS=true CLEAN_STORE_CHATS=true # Permanent data storage -DATABASE_ENABLED=false +DATABASE_ENABLED=true DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true -DATABASE_CONNECTION_DB_PREFIX_NAME=evolution +DATABASE_CONNECTION_DB_PREFIX_NAME=evdocker # Choose the data you want to save in the application's database or store DATABASE_SAVE_DATA_INSTANCE=false @@ -42,9 +42,9 @@ DATABASE_SAVE_MESSAGE_UPDATE=false DATABASE_SAVE_DATA_CONTACTS=false DATABASE_SAVE_DATA_CHATS=false -REDIS_ENABLED=false +REDIS_ENABLED=true REDIS_URI=redis://redis:6379 -REDIS_PREFIX_KEY=evolution +REDIS_PREFIX_KEY=evdocker # Global Webhook Settings # Each instance's Webhook URL and events will be requested at the time it is created @@ -77,7 +77,7 @@ WEBHOOK_EVENTS_CONNECTION_UPDATE=true WEBHOOK_EVENTS_NEW_JWT_TOKEN=false # Name that will be displayed on smartphone connection -CONFIG_SESSION_PHONE_CLIENT='Evolution API' +CONFIG_SESSION_PHONE_CLIENT=EvolutionAPI # Browser Name = chrome | firefox | edge | opera | safari CONFIG_SESSION_PHONE_NAME=chrome @@ -88,12 +88,12 @@ QRCODE_LIMIT=30 # We recommend using the apikey because it will allow you to use a custom token, # if you use jwt, a random token will be generated and may be expired and you will have to generate a new token # jwt or 'apikey' -AUTHENTICATION_TYPE='apikey' +AUTHENTICATION_TYPE=apikey ## Define a global apikey to access all instances. ### OBS: This key must be inserted in the request header to create an instance. -AUTHENTICATION_API_KEY='B6D711FCDE4D4FD5936544120E713976' +AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976 AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true ## Set the secret key to encrypt and decrypt your token and its expiration time # seconds - 3600s ===1h | zero (0) - never expires AUTHENTICATION_JWT_EXPIRIN_IN=0 -AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG' +AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`' diff --git a/Dockerfile b/Dockerfile index 088f6fa1..93fa60c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ COPY ./package.json . ENV DOCKER_ENV=true -ENV SERVER_URL='http://localhost:8080' +ENV SERVER_URL=http://localhost:8080 ENV CORS_ORIGIN=* ENV CORS_METHODS=POST,GET,PUT,DELETE @@ -77,7 +77,7 @@ ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false -ENV CONFIG_SESSION_PHONE_CLIENT='Evolution API' +ENV CONFIG_SESSION_PHONE_CLIENT=EvolutionAPI ENV CONFIG_SESSION_PHONE_NAME=chrome ENV QRCODE_LIMIT=30 diff --git a/docker-compose.yaml b/docker-compose.yaml.example similarity index 92% rename from docker-compose.yaml rename to docker-compose.yaml.example index c6d1bc73..93e04cc0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml.example @@ -10,6 +10,8 @@ services: volumes: - evolution_instances:/evolution/instances - evolution_store:/evolution/store + networks: + - evolution-net env_file: - ./Docker/.env command: ['node', './dist/src/main.js'] diff --git a/docker-compose.yaml.example.complete b/docker-compose.yaml.example.complete new file mode 100644 index 00000000..23f316c8 --- /dev/null +++ b/docker-compose.yaml.example.complete @@ -0,0 +1,88 @@ +version: '3.3' + +services: + api: + container_name: evolution_api + image: evolution/api:local + restart: always + ports: + - 8080:8080 + volumes: + - evolution_instances:/evolution/instances + - evolution_store:/evolution/store + networks: + - evolution-net + env_file: + - ./Docker/.env + command: ['node', './dist/src/main.js'] + expose: + - 8080 + + mongodb: + container_name: mongodb + image: mongo + restart: always + ports: + - 27017:27017 + environment: + - MONGO_INITDB_ROOT_USERNAME=root + - MONGO_INITDB_ROOT_PASSWORD=root + - PUID=1000 + - PGID=1000 + volumes: + - evolution_mongodb_data:/data/db + - evolution_mongodb_configdb:/data/configdb + networks: + - evolution-net + expose: + - 27017 + + mongo-express: + image: mongo-express + networks: + - evolution-net + environment: + ME_CONFIG_BASICAUTH_USERNAME: root + ME_CONFIG_BASICAUTH_PASSWORD: root + ME_CONFIG_MONGODB_SERVER: mongodb + ME_CONFIG_MONGODB_ADMINUSERNAME: root + ME_CONFIG_MONGODB_ADMINPASSWORD: root + ports: + - 8081:8081 + links: + - mongodb + + redis: + image: redis:latest + container_name: redis + command: > + redis-server + --port 6379 + --appendonly yes + volumes: + - evolution_redis:/data + networks: + - evolution-net + ports: + - 6379:6379 + + rebrow: + image: marian/rebrow + networks: + - evolution-net + ports: + - 5001:5001 + links: + - redis + +volumes: + evolution_instances: + evolution_store: + evolution_mongodb_data: + evolution_mongodb_configdb: + evolution_redis: + +networks: + evolution-net: + external: true + \ No newline at end of file From b2ccf965bb75e6933df5e870c7fd3e3d2b38f60a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 15:14:46 -0300 Subject: [PATCH 10/97] fix: added docker-compose files example --- .gitignore | 2 ++ CHANGELOG.md | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index e707b33e..69d60b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ lerna-debug.log* /docker-compose-data /docker-data +docker-compose.yaml + # Package /yarn.lock /package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a864fe5..5da5cdb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Now when deleting the instance, the data referring to it in mongodb is also deleted * It is now validated if the instance name contains uppercase and special characters * For compatibility reasons, container mode has been removed +* Added docker-compose files example # 1.3.1 (2023-07-20 07:48) From e851696430806654a547b2590b7684b8ad1f8ba0 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Fri, 21 Jul 2023 15:32:28 -0300 Subject: [PATCH 11/97] =?UTF-8?q?Adi=C3=A7=C3=A3o=20de=20Profile=20Route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validate/validate.schema.ts | 11 ++++++++ src/whatsapp/controllers/chat.controller.ts | 5 ++++ src/whatsapp/routers/chat.router.ts | 18 +++++++++++++ src/whatsapp/services/whatsapp.service.ts | 30 +++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2b9ce19a..2533e4d7 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -587,6 +587,17 @@ export const profilePictureSchema: JSONSchema7 = { }, }; +export const profileSchema: JSONSchema7 = { + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, +}; + export const messageValidateSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 0a176059..63c72092 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -47,6 +47,11 @@ export class ChatController { logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); } + + public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].profile(instanceName, data.number); + } public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 50ead521..ac7990c8 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -8,6 +8,7 @@ import { privacySettingsSchema, profileNameSchema, profilePictureSchema, + profileSchema, profileStatusSchema, readMessageSchema, whatsappNumberSchema, @@ -129,6 +130,23 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .get(this.routerPath('fetchProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: profileSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfile(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('findContacts'), ...guards, async (req, res) => { logger.verbose('request received in findContacts'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 65cb40bb..b2ab7127 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1436,6 +1436,36 @@ export class WAStartupService { }; } } + + public async profile(instanceName: string) { + const jid = this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const info = await waMonitor.instanceInfo(instanceName); + const business = await this.fetchBusinessProfile(jid); + + return { + wuid: jid, + name: info?.instance?.profileName, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + os: this.client?.authState?.creds?.platform, + isBusiness: typeof business !== 'undefined', + }; + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } private async sendMessageWithTyping( number: string, From 683fe4c3db06c6899cdb0b6bad235d91ea07612b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 16:00:58 -0300 Subject: [PATCH 12/97] fix: added docker-compose files example --- Docker/redis/docker-compose.yaml | 7 ------- docker-compose.yaml.example.complete | 9 --------- 2 files changed, 16 deletions(-) diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index 6409b851..b0bb5985 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -12,13 +12,6 @@ services: - evolution_redis:/data ports: - 6379:6379 - - rebrow: - image: marian/rebrow - ports: - - 5001:5001 - links: - - redis volumes: evolution_redis: diff --git a/docker-compose.yaml.example.complete b/docker-compose.yaml.example.complete index 23f316c8..4e453b51 100644 --- a/docker-compose.yaml.example.complete +++ b/docker-compose.yaml.example.complete @@ -66,15 +66,6 @@ services: ports: - 6379:6379 - rebrow: - image: marian/rebrow - networks: - - evolution-net - ports: - - 5001:5001 - links: - - redis - volumes: evolution_instances: evolution_store: From d0fa3b92f8eac6e76131ab96b23206e7b3bcc61a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 17:19:59 -0300 Subject: [PATCH 13/97] version: 1.3.2 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5da5cdb6..0c1be220 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.3.2 (homolog) +# 1.3.2 (2023-07-21 17:19) ### Fixed From 8f4d44a21212cf3d075b93c3cb2fae5a6acdaf88 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 20:37:58 -0300 Subject: [PATCH 14/97] Added connection functionality via pairing code --- CHANGELOG.md | 6 +++++ package.json | 2 +- src/validate/validate.schema.ts | 1 + .../controllers/instance.controller.ts | 22 +++++++++++++++++-- src/whatsapp/dto/instance.dto.ts | 1 + src/whatsapp/routers/instance.router.ts | 3 ++- src/whatsapp/services/whatsapp.service.ts | 5 +++++ src/whatsapp/types/wa.types.ts | 6 ++++- 8 files changed, 41 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1be220..8d56ec43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.0 (homolog) + +### Features + +* Added connection functionality via pairing code + # 1.3.2 (2023-07-21 17:19) ### Fixed diff --git a/package.json b/package.json index 7bc6f476..92745ead 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.3.2", + "version": "1.4.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2b9ce19a..ed7ab2e0 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -58,6 +58,7 @@ export const instanceNameSchema: JSONSchema7 = { }, }, qrcode: { type: 'boolean', enum: [true, false] }, + number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, token: { type: 'string' }, }, ...isNotEmpty('instanceName'), diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 75911848..b54858b2 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -34,6 +34,7 @@ export class InstanceController { webhook_by_events, events, qrcode, + number, token, chatwoot_account_id, chatwoot_token, @@ -102,10 +103,16 @@ export class InstanceController { if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { let getQrcode: wa.QrCode; + let getParingCode: string; if (qrcode) { this.logger.verbose('creating qrcode'); await instance.connectToWhatsapp(); + if (number) { + this.logger.verbose('creating number'); + await delay(5000); + getParingCode = await instance.client.requestPairingCode(number); + } await delay(2000); getQrcode = instance.qrCode; } @@ -120,6 +127,7 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, + pairingCode: getParingCode, qrcode: getQrcode, }); @@ -132,6 +140,7 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, + pairingCode: getParingCode, qrcode: getQrcode, }; } @@ -195,7 +204,7 @@ export class InstanceController { }; } - public async connectToWhatsapp({ instanceName }: InstanceDto) { + public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { try { this.logger.verbose( 'requested connectToWhatsapp from ' + instanceName + ' instance', @@ -210,8 +219,17 @@ export class InstanceController { case 'close': this.logger.verbose('connecting'); await instance.connectToWhatsapp(); + let pairingCode = null; + if (number) { + this.logger.verbose('creating pairing code'); + await delay(5000); + pairingCode = await instance.client.requestPairingCode(number); + } await delay(2000); - return instance.qrCode; + return { + pairingCode, + ...instance.qrCode, + }; case 'connecting': return instance.qrCode; default: diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index ce282e03..9e8a7ec3 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -4,6 +4,7 @@ export class InstanceDto { webhook_by_events?: boolean; events?: string[]; qrcode?: boolean; + number?: string; token?: string; chatwoot_account_id?: string; chatwoot_token?: string; diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index 850ffebd..a984a89c 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -60,7 +60,8 @@ export class InstanceRouter extends RouterBroker { request: req, schema: instanceNameSchema, ClassRef: InstanceDto, - execute: (instance) => instanceController.connectToWhatsapp(instance), + execute: (instance, data) => + instanceController.connectToWhatsapp(instance, data), }); return res.status(HttpStatus.OK).json(response); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3851f067..ab94d691 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -545,6 +545,11 @@ export class WAStartupService { return this.eventEmitter.emit('no.connection', this.instance.name); } + // pairing code + // await delay(5000); + // const code = await this.client.requestPairingCode('557499879409'); + // console.log(`Pairing code: ${code}`); + this.logger.verbose('Incrementing QR code count'); this.instance.qrcode.count++; diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 6869545f..fc71de31 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -25,7 +25,11 @@ export enum Events { } export declare namespace wa { - export type QrCode = { count?: number; base64?: string; code?: string }; + export type QrCode = { + count?: number; + base64?: string; + code?: string; + }; export type Instance = { qrcode?: QrCode; authState?: { state: AuthenticationState; saveCreds: () => void }; From b681e339444859ba94f6d3d9432642873bb54a07 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 20:38:10 -0300 Subject: [PATCH 15/97] Added connection functionality via pairing code --- src/whatsapp/routers/instance.router.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index a984a89c..850ffebd 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -60,8 +60,7 @@ export class InstanceRouter extends RouterBroker { request: req, schema: instanceNameSchema, ClassRef: InstanceDto, - execute: (instance, data) => - instanceController.connectToWhatsapp(instance, data), + execute: (instance) => instanceController.connectToWhatsapp(instance), }); return res.status(HttpStatus.OK).json(response); From 16ed5821e277a4128026bcf8137448dfbc5db9da Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 21 Jul 2023 20:58:37 -0300 Subject: [PATCH 16/97] Added connection functionality via pairing code --- .../controllers/instance.controller.ts | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index b54858b2..9b938ab0 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -103,7 +103,7 @@ export class InstanceController { if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { let getQrcode: wa.QrCode; - let getParingCode: string; + let getPairingCode: string; if (qrcode) { this.logger.verbose('creating qrcode'); @@ -111,14 +111,13 @@ export class InstanceController { if (number) { this.logger.verbose('creating number'); await delay(5000); - getParingCode = await instance.client.requestPairingCode(number); + getPairingCode = await instance.client.requestPairingCode(number); } await delay(2000); getQrcode = instance.qrCode; } - this.logger.verbose('instance created'); - this.logger.verbose({ + const result = { instance: { instanceName: instance.instanceName, status: 'created', @@ -127,22 +126,18 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, - pairingCode: getParingCode, - qrcode: getQrcode, - }); - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - pairingCode: getParingCode, - qrcode: getQrcode, }; + + if (getPairingCode) { + result['pairingCode'] = getPairingCode; + } else { + result['qrcode'] = getQrcode; + } + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; } if (!chatwoot_account_id) { @@ -225,11 +220,15 @@ export class InstanceController { await delay(5000); pairingCode = await instance.client.requestPairingCode(number); } + + if (pairingCode) { + return { + pairingCode, + }; + } + await delay(2000); - return { - pairingCode, - ...instance.qrCode, - }; + return instance.qrCode; case 'connecting': return instance.qrCode; default: From 1ec3ed32eeaa8cd668e0b28d2e5aa81fc2d4ff6a Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Sun, 23 Jul 2023 10:18:00 -0300 Subject: [PATCH 17/97] Melhorias --- src/whatsapp/controllers/chat.controller.ts | 2 +- src/whatsapp/dto/chat.dto.ts | 13 +++ src/whatsapp/routers/chat.router.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 92 +++++++++++++++------ 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 63c72092..2b500ad1 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -50,7 +50,7 @@ export class ChatController { public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].profile(instanceName, data.number); + return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number); } public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 07757194..5af66a5e 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -26,6 +26,19 @@ export class NumberDto { number: string; } +export class NumberBusiness { + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; +} + export class ProfileNameDto { name: string; } diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index ac7990c8..68ecd97e 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -130,7 +130,7 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) - .get(this.routerPath('fetchProfile'), ...guards, async (req, res) => { + .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { logger.verbose('request received in fetchProfile'); logger.verbose('request body: '); logger.verbose(req.body); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index b2ab7127..e99cee84 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -85,6 +85,7 @@ import { ArchiveChatDto, DeleteMessage, OnWhatsAppDto, + NumberBusiness, PrivacySettingDto, ReadMessageDto, WhatsAppNumberDto, @@ -1437,23 +1438,66 @@ export class WAStartupService { } } - public async profile(instanceName: string) { - const jid = this.client?.user?.id; - + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = (number) + ? this.createJid(number) + : this.client?.user?.id; this.logger.verbose('Getting profile with jid: ' + jid); try { this.logger.verbose('Getting profile info'); - const info = await waMonitor.instanceInfo(instanceName); const business = await this.fetchBusinessProfile(jid); - return { - wuid: jid, - name: info?.instance?.profileName, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - os: this.client?.authState?.creds?.platform, - isBusiness: typeof business !== 'undefined', - }; + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { this.logger.verbose('Profile not found'); return { @@ -2463,29 +2507,31 @@ export class WAStartupService { } } - public async fetchBusinessProfile(number: string) { + public async fetchBusinessProfile(number: string) : Promise { this.logger.verbose('Fetching business profile'); try { - let jid; - - if (!number) { - jid = this.instance.wuid; - } else { - jid = this.createJid(number); - } + const jid = (number) + ? this.createJid(number) + : this.instance.wuid; const profile = await this.client.getBusinessProfile(jid); this.logger.verbose('Trying to get business profile'); if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + return { - exists: false, - message: 'Business profile not found', + isBusiness: false, + message: 'Not is business profile', + ...info?.shift() }; } this.logger.verbose('Business profile fetched'); - return profile; + return { + isBusiness: true, + ...profile + }; } catch (error) { throw new InternalServerErrorException( 'Error updating profile name', From 90048afa9d1bb700380c3b28497241a8a386607a Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Sun, 23 Jul 2023 11:24:16 -0300 Subject: [PATCH 18/97] Add LinkPreview Option --- src/whatsapp/dto/sendMessage.dto.ts | 1 + src/whatsapp/services/whatsapp.service.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index 51a55cec..0a20674c 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -15,6 +15,7 @@ export class Options { presence?: WAPresence; quoted?: Quoted; mentions?: Mentions; + linkPreview?: boolean; } class OptionsMessage { options: Options; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3851f067..dd8fa3a5 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1480,6 +1480,8 @@ export class WAStartupService { this.logger.verbose('Sending presence update: paused'); } + let linkPreview = (options?.linkPreview != false) ? undefined : false; + let quoted: WAMessage; if (options?.quoted) { @@ -1573,6 +1575,7 @@ export class WAStartupService { { text: message['conversation'], mentions, + linkPreview: linkPreview, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); From 76d77ad76f54283fe47aa0376b89897055f73220 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 23 Jul 2023 22:05:21 -0300 Subject: [PATCH 19/97] =?UTF-8?q?Revert=20"Adi=C3=A7=C3=A3o=20de=20FetchPr?= =?UTF-8?q?ofile"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/validate/validate.schema.ts | 11 --- src/whatsapp/controllers/chat.controller.ts | 5 -- src/whatsapp/dto/chat.dto.ts | 13 --- src/whatsapp/routers/chat.router.ts | 18 ---- src/whatsapp/services/whatsapp.service.ts | 98 +++------------------ 5 files changed, 11 insertions(+), 134 deletions(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2533e4d7..2b9ce19a 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -587,17 +587,6 @@ export const profilePictureSchema: JSONSchema7 = { }, }; -export const profileSchema: JSONSchema7 = { - type: 'object', - properties: { - wuid: { type: 'string' }, - name: { type: 'string' }, - picture: { type: 'string' }, - status: { type: 'string' }, - isBusiness: { type: 'boolean' }, - }, -}; - export const messageValidateSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 2b500ad1..0a176059 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -47,11 +47,6 @@ export class ChatController { logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); } - - public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { - logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number); - } public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 5af66a5e..07757194 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -26,19 +26,6 @@ export class NumberDto { number: string; } -export class NumberBusiness { - wid?: string; - jid?: string; - exists?: boolean; - isBusiness: boolean; - name?: string; - message?: string; - description?: string; - email?: string; - website?: string[]; - address?: string; -} - export class ProfileNameDto { name: string; } diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 68ecd97e..50ead521 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -8,7 +8,6 @@ import { privacySettingsSchema, profileNameSchema, profilePictureSchema, - profileSchema, profileStatusSchema, readMessageSchema, whatsappNumberSchema, @@ -130,23 +129,6 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) - .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { - logger.verbose('request received in fetchProfile'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - - const response = await this.dataValidate({ - request: req, - schema: profileSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchProfile(instance, data), - }); - - return res.status(HttpStatus.OK).json(response); - }) .post(this.routerPath('findContacts'), ...guards, async (req, res) => { logger.verbose('request received in findContacts'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 1cd9035a..3851f067 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -85,7 +85,6 @@ import { ArchiveChatDto, DeleteMessage, OnWhatsAppDto, - NumberBusiness, PrivacySettingDto, ReadMessageDto, WhatsAppNumberDto, @@ -1444,79 +1443,6 @@ export class WAStartupService { }; } } - - public async getStatus(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } - - public async fetchProfile(instanceName: string, number?: string) { - const jid = (number) - ? this.createJid(number) - : this.client?.user?.id; - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - const business = await this.fetchBusinessProfile(jid); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(jid); - const status = await this.getStatus(jid); - - return { - wuid: jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } private async sendMessageWithTyping( number: string, @@ -2534,31 +2460,29 @@ export class WAStartupService { } } - public async fetchBusinessProfile(number: string) : Promise { + public async fetchBusinessProfile(number: string) { this.logger.verbose('Fetching business profile'); try { - const jid = (number) - ? this.createJid(number) - : this.instance.wuid; + let jid; + + if (!number) { + jid = this.instance.wuid; + } else { + jid = this.createJid(number); + } const profile = await this.client.getBusinessProfile(jid); this.logger.verbose('Trying to get business profile'); if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift() + exists: false, + message: 'Business profile not found', }; } this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile - }; + return profile; } catch (error) { throw new InternalServerErrorException( 'Error updating profile name', From 798eb90bed2873be514135512b75d14869777b09 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 23 Jul 2023 22:24:21 -0300 Subject: [PATCH 20/97] feat: Added fetch profile endpoint in chat controller and link preview option in send text message --- CHANGELOG.md | 5 + src/validate/validate.schema.ts | 11 +++ src/whatsapp/controllers/chat.controller.ts | 8 ++ .../controllers/instance.controller.ts | 57 ++++++----- src/whatsapp/dto/chat.dto.ts | 13 +++ src/whatsapp/routers/chat.router.ts | 18 ++++ src/whatsapp/services/whatsapp.service.ts | 99 ++++++++++++++++--- 7 files changed, 176 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d56ec43..cd9ec53d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ### Features * Added connection functionality via pairing code +* Added fetch profile endpoint in chat controller + +### Fixed + +* Added link preview option in send text message # 1.3.2 (2023-07-21 17:19) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index ed7ab2e0..30763fcd 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -588,6 +588,17 @@ export const profilePictureSchema: JSONSchema7 = { }, }; +export const profileSchema: JSONSchema7 = { + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, +}; + export const messageValidateSchema: JSONSchema7 = { $id: v4(), type: 'object', diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 0a176059..454ddabf 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -48,6 +48,14 @@ export class ChatController { return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); } + public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchProfile( + instanceName, + data.number, + ); + } + public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].fetchContacts(query); diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 9b938ab0..e7827f15 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -210,30 +210,41 @@ export class InstanceController { this.logger.verbose('state: ' + state); - switch (state) { - case 'close': - this.logger.verbose('connecting'); - await instance.connectToWhatsapp(); - let pairingCode = null; - if (number) { - this.logger.verbose('creating pairing code'); - await delay(5000); - pairingCode = await instance.client.requestPairingCode(number); - } - - if (pairingCode) { - return { - pairingCode, - }; - } - - await delay(2000); - return instance.qrCode; - case 'connecting': - return instance.qrCode; - default: - return await this.connectionState({ instanceName }); + if (state == 'open') { + return await this.connectionState({ instanceName }); } + + if (state == 'connecting') { + return instance.qrCode; + } + + if (state == 'close') { + this.logger.verbose('connecting'); + await instance.connectToWhatsapp(); + let pairingCode = null; + if (number) { + this.logger.verbose('creating pairing code'); + await delay(5000); + pairingCode = await instance.client.requestPairingCode(number); + } + + if (pairingCode) { + return { + pairingCode, + }; + } + + await delay(2000); + return instance.qrCode; + } + + return { + instance: { + instanceName: instanceName, + status: state, + }, + qrcode: instance?.qrCode, + }; } catch (error) { this.logger.error(error); } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 07757194..5af66a5e 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -26,6 +26,19 @@ export class NumberDto { number: string; } +export class NumberBusiness { + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; +} + export class ProfileNameDto { name: string; } diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 50ead521..49e64117 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -8,6 +8,7 @@ import { privacySettingsSchema, profileNameSchema, profilePictureSchema, + profileSchema, profileStatusSchema, readMessageSchema, whatsappNumberSchema, @@ -129,6 +130,23 @@ export class ChatRouter extends RouterBroker { return res.status(HttpStatus.OK).json(response); }) + .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: profileSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfile(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) .post(this.routerPath('findContacts'), ...guards, async (req, res) => { logger.verbose('request received in findContacts'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3f720dff..128183da 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -84,6 +84,7 @@ import { arrayUnique, isBase64, isURL } from 'class-validator'; import { ArchiveChatDto, DeleteMessage, + NumberBusiness, OnWhatsAppDto, PrivacySettingDto, ReadMessageDto, @@ -1449,6 +1450,78 @@ export class WAStartupService { } } + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const info = await waMonitor.instanceInfo(instanceName); + const business = await this.fetchBusinessProfile(jid); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + private async sendMessageWithTyping( number: string, message: T, @@ -1485,7 +1558,9 @@ export class WAStartupService { this.logger.verbose('Sending presence update: paused'); } - let linkPreview = (options?.linkPreview != false) ? undefined : false; + const linkPreview = options?.linkPreview != false ? undefined : false; + + console.log('linkPreview', linkPreview); let quoted: WAMessage; @@ -2468,29 +2543,29 @@ export class WAStartupService { } } - public async fetchBusinessProfile(number: string) { + public async fetchBusinessProfile(number: string): Promise { this.logger.verbose('Fetching business profile'); try { - let jid; - - if (!number) { - jid = this.instance.wuid; - } else { - jid = this.createJid(number); - } + const jid = number ? this.createJid(number) : this.instance.wuid; const profile = await this.client.getBusinessProfile(jid); this.logger.verbose('Trying to get business profile'); if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + return { - exists: false, - message: 'Business profile not found', + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), }; } this.logger.verbose('Business profile fetched'); - return profile; + return { + isBusiness: true, + ...profile, + }; } catch (error) { throw new InternalServerErrorException( 'Error updating profile name', From be699d24a1d31b17670293564b1c937d1255fc4b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Sun, 23 Jul 2023 22:26:38 -0300 Subject: [PATCH 21/97] feat: Added fetch profile endpoint in chat controller and link preview option in send text message --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd9ec53d..fed20ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ * Added link preview option in send text message +### Integrations + +- Chatwoot: v2.18.0 + # 1.3.2 (2023-07-21 17:19) ### Fixed @@ -21,12 +25,20 @@ * For compatibility reasons, container mode has been removed * Added docker-compose files example +### Integrations + +- Chatwoot: v2.18.0 + # 1.3.1 (2023-07-20 07:48) ### Fixed * Adjust in create store files +### Integrations + +- Chatwoot: v2.18.0 + # 1.3.0 (2023-07-19 11:33) ### Features From 73d9cd62a56d56c070c54ccf39134b6e63f9a6c5 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 09:42:29 -0300 Subject: [PATCH 22/97] feat: Created settings Controller --- CHANGELOG.md | 1 + src/validate/validate.schema.ts | 12 +++ .../controllers/settings.controller.ts | 29 +++++++ src/whatsapp/dto/settings.dto.ts | 5 ++ src/whatsapp/models/index.ts | 1 + src/whatsapp/models/settings.model.ts | 23 ++++++ src/whatsapp/repository/repository.manager.ts | 8 +- .../repository/settings.repository.ts | 75 +++++++++++++++++++ src/whatsapp/routers/index.router.ts | 4 +- src/whatsapp/routers/settings.router.ts | 52 +++++++++++++ src/whatsapp/services/settings.service.ts | 34 +++++++++ src/whatsapp/services/whatsapp.service.ts | 43 +++++++++++ src/whatsapp/types/wa.types.ts | 6 ++ src/whatsapp/whatsapp.module.ts | 10 +++ 14 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/whatsapp/controllers/settings.controller.ts create mode 100644 src/whatsapp/dto/settings.dto.ts create mode 100644 src/whatsapp/models/settings.model.ts create mode 100644 src/whatsapp/repository/settings.repository.ts create mode 100644 src/whatsapp/routers/settings.router.ts create mode 100644 src/whatsapp/services/settings.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fed20ae3..ea7401df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Added connection functionality via pairing code * Added fetch profile endpoint in chat controller +* Created settings controller ### Fixed diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 30763fcd..a2a8ef13 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -877,3 +877,15 @@ export const chatwootSchema: JSONSchema7 = { required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), }; + +export const settingsSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + reject_call: { type: 'boolean', enum: [true, false] }, + msg_call: { type: 'string' }, + groups_ignore: { type: 'boolean', enum: [true, false] }, + }, + required: ['reject_call'], + ...isNotEmpty('reject_call'), +}; diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts new file mode 100644 index 00000000..59031634 --- /dev/null +++ b/src/whatsapp/controllers/settings.controller.ts @@ -0,0 +1,29 @@ +import { isURL } from 'class-validator'; +import { BadRequestException } from '../../exceptions'; +import { InstanceDto } from '../dto/instance.dto'; +import { SettingsDto } from '../dto/settings.dto'; +import { SettingsService } from '../services/settings.service'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('SettingsController'); + +export class SettingsController { + constructor(private readonly settingsService: SettingsService) {} + + public async createSettings(instance: InstanceDto, data: SettingsDto) { + logger.verbose( + 'requested createSettings from ' + instance.instanceName + ' instance', + ); + + if (data.reject_call && data.msg_call.trim() == '') { + throw new BadRequestException('msg_call is required'); + } + + return this.settingsService.create(instance, data); + } + + public async findSettings(instance: InstanceDto) { + logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); + return this.settingsService.find(instance); + } +} diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts new file mode 100644 index 00000000..20a6cba0 --- /dev/null +++ b/src/whatsapp/dto/settings.dto.ts @@ -0,0 +1,5 @@ +export class SettingsDto { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; +} diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index e0b773f0..e6c6d8b4 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -4,3 +4,4 @@ export * from './message.model'; export * from './auth.model'; export * from './webhook.model'; export * from './chatwoot.model'; +export * from './settings.model'; diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts new file mode 100644 index 00000000..b5eb7fe7 --- /dev/null +++ b/src/whatsapp/models/settings.model.ts @@ -0,0 +1,23 @@ +import { Schema } from 'mongoose'; +import { dbserver } from '../../db/db.connect'; + +export class SettingsRaw { + _id?: string; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; +} + +const settingsSchema = new Schema({ + _id: { type: String, _id: true }, + reject_call: { type: Boolean, required: true }, + msg_call: { type: String, required: true }, + groups_ignore: { type: Boolean, required: true }, +}); + +export const SettingsModel = dbserver?.model( + SettingsRaw.name, + settingsSchema, + 'settings', +); +export type ISettingsModel = typeof SettingsModel; diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index 6c2a3091..dde636c7 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -5,10 +5,10 @@ import { MessageUpRepository } from './messageUp.repository'; import { MongoClient } from 'mongodb'; import { WebhookRepository } from './webhook.repository'; import { ChatwootRepository } from './chatwoot.repository'; +import { SettingsRepository } from './settings.repository'; import { AuthRepository } from './auth.repository'; import { Auth, ConfigService, Database } from '../../config/env.config'; -import { execSync } from 'child_process'; import { join } from 'path'; import fs from 'fs'; import { Logger } from '../../config/logger.config'; @@ -20,6 +20,7 @@ export class RepositoryBroker { public readonly messageUpdate: MessageUpRepository, public readonly webhook: WebhookRepository, public readonly chatwoot: ChatwootRepository, + public readonly settings: SettingsRepository, public readonly auth: AuthRepository, private configService: ConfigService, dbServer?: MongoClient, @@ -53,6 +54,7 @@ export class RepositoryBroker { const messageUpDir = join(storePath, 'message-up'); const webhookDir = join(storePath, 'webhook'); const chatwootDir = join(storePath, 'chatwoot'); + const settingsDir = join(storePath, 'settings'); const tempDir = join(storePath, 'temp'); if (!fs.existsSync(authDir)) { @@ -83,6 +85,10 @@ export class RepositoryBroker { this.logger.verbose('creating chatwoot dir: ' + chatwootDir); fs.mkdirSync(chatwootDir, { recursive: true }); } + if (!fs.existsSync(settingsDir)) { + this.logger.verbose('creating settings dir: ' + settingsDir); + fs.mkdirSync(settingsDir, { recursive: true }); + } if (!fs.existsSync(tempDir)) { this.logger.verbose('creating temp dir: ' + tempDir); fs.mkdirSync(tempDir, { recursive: true }); diff --git a/src/whatsapp/repository/settings.repository.ts b/src/whatsapp/repository/settings.repository.ts new file mode 100644 index 00000000..d253643d --- /dev/null +++ b/src/whatsapp/repository/settings.repository.ts @@ -0,0 +1,75 @@ +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ConfigService } from '../../config/env.config'; +import { join } from 'path'; +import { readFileSync } from 'fs'; +import { ISettingsModel, SettingsRaw } from '../models'; +import { Logger } from '../../config/logger.config'; + +export class SettingsRepository extends Repository { + constructor( + private readonly settingsModel: ISettingsModel, + private readonly configService: ConfigService, + ) { + super(configService); + } + + private readonly logger = new Logger('SettingsRepository'); + + public async create(data: SettingsRaw, instance: string): Promise { + try { + this.logger.verbose('creating settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving settings to db'); + const insert = await this.settingsModel.replaceOne( + { _id: instance }, + { ...data }, + { upsert: true }, + ); + + this.logger.verbose( + 'settings saved to db: ' + insert.modifiedCount + ' settings', + ); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving settings to store'); + + this.writeStore({ + path: join(this.storePath, 'settings'), + fileName: instance, + data, + }); + + this.logger.verbose( + 'settings saved to store in path: ' + + join(this.storePath, 'settings') + + '/' + + instance, + ); + + this.logger.verbose('settings created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding settings in db'); + return await this.settingsModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding settings in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'settings', instance + '.json'), { + encoding: 'utf-8', + }), + ) as SettingsRaw; + } catch (error) { + return {}; + } + } +} diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 5d8a2c05..4cf7befb 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -10,6 +10,7 @@ import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; import { ChatwootRouter } from './chatwoot.router'; import fs from 'fs'; +import { SettingsRouter } from './settings.router'; enum HttpStatus { OK = 200, @@ -44,6 +45,7 @@ router .use('/chat', new ChatRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) .use('/webhook', new WebhookRouter(...guards).router) - .use('/chatwoot', new ChatwootRouter(...guards).router); + .use('/chatwoot', new ChatwootRouter(...guards).router) + .use('/settings', new SettingsRouter(...guards).router); export { router, HttpStatus }; diff --git a/src/whatsapp/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts new file mode 100644 index 00000000..3ec3df83 --- /dev/null +++ b/src/whatsapp/routers/settings.router.ts @@ -0,0 +1,52 @@ +import { RequestHandler, Router } from 'express'; +import { instanceNameSchema, settingsSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { SettingsDto } from '../dto/settings.dto'; +import { settingsController } from '../whatsapp.module'; +import { SettingsService } from '../services/settings.service'; +import { HttpStatus } from './index.router'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('SettingsRouter'); + +export class SettingsRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setSettings'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: settingsSchema, + ClassRef: SettingsDto, + execute: (instance, data) => settingsController.createSettings(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findSettings'); + 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) => settingsController.findSettings(instance), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/services/settings.service.ts b/src/whatsapp/services/settings.service.ts new file mode 100644 index 00000000..9a82046a --- /dev/null +++ b/src/whatsapp/services/settings.service.ts @@ -0,0 +1,34 @@ +import { InstanceDto } from '../dto/instance.dto'; +import { SettingsDto } from '../dto/settings.dto'; +import { WAMonitoringService } from './monitor.service'; +import { Logger } from '../../config/logger.config'; + +export class SettingsService { + constructor(private readonly waMonitor: WAMonitoringService) {} + + private readonly logger = new Logger(SettingsService.name); + + public create(instance: InstanceDto, data: SettingsDto) { + this.logger.verbose('create settings: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setSettings(data); + + return { settings: { ...instance, settings: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find settings: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[ + instance.instanceName + ].findSettings(); + + if (Object.keys(result).length === 0) { + throw new Error('Settings not found'); + } + + return result; + } catch (error) { + return { reject_call: false, msg_call: '', groups_ignore: false }; + } + } +} diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 128183da..27019883 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -125,6 +125,7 @@ import { Log } from '../../config/env.config'; import ProxyAgent from 'proxy-agent'; import { ChatwootService } from './chatwoot.service'; import { waMonitor } from '../whatsapp.module'; +import { SettingsRaw } from '../models'; export class WAStartupService { constructor( @@ -143,6 +144,7 @@ export class WAStartupService { public client: WASocket; private readonly localWebhook: wa.LocalWebHook = {}; private readonly localChatwoot: wa.LocalChatwoot = {}; + private readonly localSettings: wa.LocalSettings = {}; private stateConnection: wa.StateConnection = { state: 'close' }; public readonly storePath = join(ROOT_DIR, 'store'); private readonly msgRetryCounterCache: CacheStore = new NodeCache(); @@ -341,6 +343,46 @@ export class WAStartupService { return data; } + private async loadSettings() { + this.logger.verbose('Loading settings'); + const data = await this.repository.settings.find(this.instanceName); + this.localSettings.reject_call = data?.reject_call; + this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); + + this.localSettings.msg_call = data?.msg_call; + this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); + + this.localSettings.groups_ignore = data?.groups_ignore; + this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + + this.logger.verbose('Settings loaded'); + } + + public async setSettings(data: SettingsRaw) { + this.logger.verbose('Setting settings'); + await this.repository.settings.create(data, this.instanceName); + this.logger.verbose(`Settings reject_call: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + Object.assign(this.localSettings, data); + this.logger.verbose('Settings set'); + } + + public async findSettings() { + this.logger.verbose('Finding settings'); + const data = await this.repository.settings.find(this.instanceName); + + if (!data) { + this.logger.verbose('Settings not found'); + throw new NotFoundException('Settings not found'); + } + + this.logger.verbose(`Settings url: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + return data; + } + public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; @@ -761,6 +803,7 @@ export class WAStartupService { try { this.loadWebhook(); this.loadChatwoot(); + this.loadSettings(); this.instance.authState = await this.defineAuthState(); diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index fc71de31..4b699d7e 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -55,6 +55,12 @@ export declare namespace wa { sign_msg?: boolean; }; + export type LocalSettings = { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + }; + export type StateConnection = { instance?: string; state?: WAConnectionState | 'refused'; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index c91ee9c4..9f2fed00 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -25,6 +25,7 @@ import { MessageUpModel, ChatwootModel, WebhookModel, + SettingsModel, } from './models'; import { dbserver } from '../db/db.connect'; import { WebhookRepository } from './repository/webhook.repository'; @@ -34,6 +35,9 @@ import { WAStartupService } from './services/whatsapp.service'; import { delay } from '@whiskeysockets/baileys'; import { Events } from './types/wa.types'; import { RedisCache } from '../db/redis.client'; +import { SettingsRepository } from './repository/settings.repository'; +import { SettingsService } from './services/settings.service'; +import { SettingsController } from './controllers/settings.controller'; const logger = new Logger('WA MODULE'); @@ -43,6 +47,7 @@ const contactRepository = new ContactRepository(ContactModel, configService); const messageUpdateRepository = new MessageUpRepository(MessageUpModel, configService); const webhookRepository = new WebhookRepository(WebhookModel, configService); const chatwootRepository = new ChatwootRepository(ChatwootModel, configService); +const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( @@ -52,6 +57,7 @@ export const repository = new RepositoryBroker( messageUpdateRepository, webhookRepository, chatwootRepository, + settingsRepository, authRepository, configService, dbserver?.getClient(), @@ -76,6 +82,10 @@ const chatwootService = new ChatwootService(waMonitor, configService); export const chatwootController = new ChatwootController(chatwootService, configService); +const settingsService = new SettingsService(waMonitor); + +export const settingsController = new SettingsController(settingsService); + export const instanceController = new InstanceController( waMonitor, configService, From 1fcbd4f9fd6f6f3024baf8bb7a3f3af7179dc56b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 10:18:16 -0300 Subject: [PATCH 23/97] feat: Added reject call and send text message when receiving a call and Added setting to ignore group messages --- CHANGELOG.md | 4 +- src/whatsapp/services/whatsapp.service.ts | 76 +++++++++++++++++------ 2 files changed, 61 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7401df..4fc020a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Added connection functionality via pairing code * Added fetch profile endpoint in chat controller * Created settings controller +* Added reject call and send text message when receiving a call +* Added setting to ignore group messages ### Fixed @@ -12,7 +14,7 @@ ### Integrations -- Chatwoot: v2.18.0 +- Chatwoot: v2.18.0 - v3.0.0 (Beta) # 1.3.2 (2023-07-21 17:19) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 27019883..d3bed078 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1086,6 +1086,7 @@ export class WAStartupService { type: MessageUpsertType; }, database: Database, + settings: SettingsRaw, ) => { this.logger.verbose('Event received: messages.upsert'); const received = messages[0]; @@ -1103,6 +1104,11 @@ export class WAStartupService { received.messageTimestamp = received.messageTimestamp?.toNumber(); } + if (settings.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + const messageRaw: MessageRaw = { key: received.key, pushName: received.pushName, @@ -1194,7 +1200,11 @@ export class WAStartupService { ); }, - 'messages.update': async (args: WAMessageUpdate[], database: Database) => { + 'messages.update': async ( + args: WAMessageUpdate[], + database: Database, + settings: SettingsRaw, + ) => { this.logger.verbose('Event received: messages.update'); const status: Record = { 0: 'ERROR', @@ -1205,6 +1215,10 @@ export class WAStartupService { 5: 'PLAYED', }; for await (const { key, update } of args) { + if (settings.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { this.logger.verbose('Message update is valid'); @@ -1304,9 +1318,28 @@ export class WAStartupService { private eventHandler() { this.logger.verbose('Initializing event handler'); - this.client.ev.process((events) => { + this.client.ev.process(async (events) => { if (!this.endSession) { const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); + + if (events.call) { + this.logger.verbose('Listening event: call'); + console.log('events.call', events.call); + const call = events.call[0]; + + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } + + if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + this.client.sendMessage(call.from, { + text: settings.msg_call, + }); + } + } if (events['connection.update']) { this.logger.verbose('Listening event: connection.update'); @@ -1327,37 +1360,44 @@ export class WAStartupService { if (events['messages.upsert']) { this.logger.verbose('Listening event: messages.upsert'); const payload = events['messages.upsert']; - this.messageHandle['messages.upsert'](payload, database); + this.messageHandle['messages.upsert'](payload, database, settings); } if (events['messages.update']) { this.logger.verbose('Listening event: messages.update'); const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, database); + this.messageHandle['messages.update'](payload, database, settings); } if (events['presence.update']) { this.logger.verbose('Listening event: presence.update'); const payload = events['presence.update']; + + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); } - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } } if (events['chats.upsert']) { From fd82aa143c421993f6e372aadc2b9b0fa49ce1f4 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 11:10:32 -0300 Subject: [PATCH 24/97] wip --- src/validate/validate.schema.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 56 +++++++++-------------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2b9ce19a..f3602582 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -123,7 +123,7 @@ const optionsSchema: JSONSchema7 = { const numberDefinition: JSONSchema7Definition = { type: 'string', - pattern: '^\\d+[\\.@\\w-]+', + // pattern: '^\\d+[\\.@\\w-]+', description: 'Invalid format', }; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3851f067..340cf5a6 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1380,7 +1380,7 @@ export class WAStartupService { private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); - + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); return number; @@ -1390,37 +1390,23 @@ export class WAStartupService { this.logger.verbose('Number already contains @broadcast'); return number; } - - const countryCode = number.substring(0, 2); - - if (Number(countryCode) === 55) { - const formattedBRNumber = this.formatBRNumber(number); - if (formattedBRNumber !== number) { - this.logger.verbose( - 'Jid created is whatsapp in format BR: ' + - `${formattedBRNumber}@s.whatsapp.net`, - ); - return `${formattedBRNumber}@s.whatsapp.net`; - } - } - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - const formattedMXARNumber = this.formatMXOrARNumber(number); - - if (formattedMXARNumber !== number) { - this.logger.verbose( - 'Jid created is whatsapp in format MXAR: ' + - `${formattedMXARNumber}@s.whatsapp.net`, - ); - return `${formattedMXARNumber}@s.whatsapp.net`; - } - } - - if (number.includes('-')) { + + number = number + ?.split(":")[0] + ?.split("@")[0] + ?.replace(' ', '') + ?.replace('+', '') + ?.replace('(', '') + ?.replace(')', ''); + + if (number.includes('-') && number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } - + + number = number.replace(/\D/g, ''); + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); return `${number}@s.whatsapp.net`; } @@ -1451,15 +1437,14 @@ export class WAStartupService { ) { this.logger.verbose('Sending message with typing'); - const jid = this.createJid(number); - const numberWA = await this.whatsappNumber({ numbers: [jid] }); + const numberWA = await this.whatsappNumber({ numbers: [number] }); const isWA = numberWA[0]; if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } - const sender = isJidGroup(jid) ? jid : isWA.jid; + const sender = isWA.jid; try { if (options?.delay) { @@ -1468,7 +1453,7 @@ export class WAStartupService { await this.client.presenceSubscribe(sender); this.logger.verbose('Subscribing to presence'); - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', jid); + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); this.logger.verbose( 'Sending presence update: ' + options?.presence ?? 'composing', ); @@ -1527,7 +1512,8 @@ export class WAStartupService { mentions = options.mentions.mentioned.map((mention) => { const jid = this.createJid(mention); if (isJidGroup(jid)) { - throw new BadRequestException('Mentions must be a number'); + return null; + // throw new BadRequestException('Mentions must be a number'); } return jid; }); @@ -2171,7 +2157,7 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { const jid = this.createJid(number); - // const jid = `${number}@s.whatsapp.net`; + if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); From 28c2c7285c2aada7c9a7b344e7737810a153cfb2 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 11:15:58 -0300 Subject: [PATCH 25/97] Gerar Wuid no SendContact --- src/validate/validate.schema.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 2b9ce19a..59cb2bd6 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -398,7 +398,7 @@ export const contactMessageSchema: JSONSchema7 = { email: { type: 'string' }, url: { type: 'string' }, }, - required: ['fullName', 'wuid', 'phoneNumber'], + required: ['fullName', 'phoneNumber'], ...isNotEmpty('fullName'), }, minItems: 1, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3851f067..b8e1eedb 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2125,6 +2125,11 @@ export class WAStartupService { result += `URL:${contact.url}\n`; } + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + From f35b62ed12fdd6a0c184f042eb800e61d8959e83 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 11:18:08 -0300 Subject: [PATCH 26/97] fix: Adjusts in createJid --- src/whatsapp/services/whatsapp.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index c56cddb1..2db6e9d4 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1469,7 +1469,7 @@ export class WAStartupService { private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); - + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); return number; @@ -1479,23 +1479,23 @@ export class WAStartupService { this.logger.verbose('Number already contains @broadcast'); return number; } - + number = number - ?.split(":")[0] - ?.split("@")[0] + ?.split(':')[0] + ?.split('@')[0] ?.replace(' ', '') ?.replace('+', '') ?.replace('(', '') ?.replace(')', ''); - + if (number.includes('-') && number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } - + number = number.replace(/\D/g, ''); - + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); return `${number}@s.whatsapp.net`; } @@ -2323,7 +2323,7 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { const jid = this.createJid(number); - + if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); From ffe15231702b471588e9fd50bf2aad7efb711b1e Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 11:42:06 -0300 Subject: [PATCH 27/97] Update whatsapp.service.ts --- src/whatsapp/services/whatsapp.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 340cf5a6..4e20fca8 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2156,7 +2156,7 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { - const jid = this.createJid(number); + let jid = this.createJid(number); if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); @@ -2165,6 +2165,7 @@ export class WAStartupService { onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); } else { + jid = (!jid.startsWith('+')) ? `+${jid}` : jid; const verify = await this.client.onWhatsApp(jid); const result = verify[0]; From 2f3d6f7e63b556d5dfcd2f2c69c57fa78c4a5570 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 11:48:10 -0300 Subject: [PATCH 28/97] fix: Fixed problem with fileSha256 appearing when sending a sticker in chatwoot --- CHANGELOG.md | 1 + src/validate/validate.schema.ts | 2 +- src/whatsapp/services/chatwoot.service.ts | 6 ++++++ src/whatsapp/services/whatsapp.service.ts | 7 ++++++- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc020a5..d9c76124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed * Added link preview option in send text message +* Fixed problem with fileSha256 appearing when sending a sticker in chatwoot ### Integrations diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index ff95074e..18a070ab 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -446,7 +446,7 @@ export const whatsappNumberSchema: JSONSchema7 = { uniqueItems: true, items: { type: 'string', - pattern: '^\\d+', + // pattern: '^\\d+', description: '"numbers" must be an array of numeric strings', }, }, diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index decb5822..0c022c22 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1173,6 +1173,10 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; + if (typeKey === 'stickerMessage') { + return null; + } + if (typeKey === 'contactMessage') { const vCardData = result.split('\n'); const contactInfo = {}; @@ -1295,6 +1299,8 @@ export class ChatwootService { this.logger.verbose('get conversation message'); const bodyMessage = await this.getConversationMessage(body.message); + console.log('bodyMessage', bodyMessage, body.message); + if (!bodyMessage && !isMedia) { this.logger.warn('no body message found'); return; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 445c9469..12d338a3 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1335,9 +1335,14 @@ export class WAStartupService { if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { this.logger.verbose('Sending message in call'); - this.client.sendMessage(call.from, { + const msg = await this.client.sendMessage(call.from, { text: settings.msg_call, }); + + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); } } From 95df402c4cc49adcf6a223520910818fb81958fd Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 11:57:15 -0300 Subject: [PATCH 29/97] wip --- src/validate/validate.schema.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index f3602582..50972dd0 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -445,7 +445,7 @@ export const whatsappNumberSchema: JSONSchema7 = { uniqueItems: true, items: { type: 'string', - pattern: '^\\d+', + // pattern: '^\\d+', description: '"numbers" must be an array of numeric strings', }, }, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 4e20fca8..a0914a68 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1392,14 +1392,14 @@ export class WAStartupService { } number = number - ?.split(":")[0] - ?.split("@")[0] - ?.replace(' ', '') - ?.replace('+', '') - ?.replace('(', '') - ?.replace(')', ''); + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(/\:/)[0] + .split('@')[0]; - if (number.includes('-') && number.length >= 18) { + if(number.includes('-') && number.length >= 24){ this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; @@ -1407,6 +1407,12 @@ export class WAStartupService { number = number.replace(/\D/g, ''); + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); return `${number}@s.whatsapp.net`; } From 1aa837d220e638c7f0a91d97fcee8f490c5d819a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 11:58:52 -0300 Subject: [PATCH 30/97] fix: Adjusts in chatwoot integration --- CHANGELOG.md | 2 ++ src/whatsapp/services/chatwoot.service.ts | 37 ++++++++++++++++------- src/whatsapp/services/whatsapp.service.ts | 2 +- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9c76124..f2d0d4ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ * Added link preview option in send text message * Fixed problem with fileSha256 appearing when sending a sticker in chatwoot +* Fixed issue where it was not possible to open a conversation when sent at first by me on my cell phone in chatwoot +* Now it only updates the contact name if it is the same as the phone number in chatwoot ### Integrations diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0c022c22..c077a983 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -428,10 +428,12 @@ export class ChatwootService { ); if (findParticipant) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } } else { await this.createContact( instance, @@ -454,13 +456,28 @@ export class ChatwootService { let contact: any; if (body.key.fromMe) { - contact = findContact; + if (findContact) { + contact = findContact; + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } } else { if (findContact) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = findContact; + } } else { contact = await this.createContact( instance, @@ -1299,8 +1316,6 @@ export class ChatwootService { this.logger.verbose('get conversation message'); const bodyMessage = await this.getConversationMessage(body.message); - console.log('bodyMessage', bodyMessage, body.message); - if (!bodyMessage && !isMedia) { this.logger.warn('no body message found'); return; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 12d338a3..5caaa9d9 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1093,7 +1093,7 @@ export class WAStartupService { if ( type !== 'notify' || - received.message?.protocolMessage || + // received.message?.protocolMessage || received.message?.pollUpdateMessage ) { this.logger.verbose('message rejected'); From c9b24ff612dbdcaa95cead2509fb30ed1ac5c2a6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 12:55:27 -0300 Subject: [PATCH 31/97] fix: adjusts for chatwoot v3 --- src/whatsapp/services/chatwoot.service.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c077a983..b28eed63 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1297,6 +1297,16 @@ export class ChatwootService { return; } + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); + + const isMedia = this.isMediaMessage(body.message); + + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; + } + this.logger.verbose('get conversation in chatwoot'); const getConversion = await this.createConversation(instance, body); @@ -1309,18 +1319,8 @@ export class ChatwootService { this.logger.verbose('message type: ' + messageType); - const isMedia = this.isMediaMessage(body.message); - this.logger.verbose('is media: ' + isMedia); - this.logger.verbose('get conversation message'); - const bodyMessage = await this.getConversationMessage(body.message); - - if (!bodyMessage && !isMedia) { - this.logger.warn('no body message found'); - return; - } - this.logger.verbose('check if is media'); if (isMedia) { this.logger.verbose('message is media'); From bcada5d553aa7c79f35fc825ea3dc9be2c4febca Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 13:03:40 -0300 Subject: [PATCH 32/97] fix: Now accepts all chatwoot inbox templates --- CHANGELOG.md | 1 + src/whatsapp/services/chatwoot.service.ts | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2d0d4ac..10f6b6bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Fixed problem with fileSha256 appearing when sending a sticker in chatwoot * Fixed issue where it was not possible to open a conversation when sent at first by me on my cell phone in chatwoot * Now it only updates the contact name if it is the same as the phone number in chatwoot +* Now accepts all chatwoot inbox templates ### Integrations diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index b28eed63..aa346f0a 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1111,11 +1111,7 @@ export class ChatwootService { } } - if ( - body.message_type === 'template' && - body.content_type === 'input_csat' && - body.event === 'message_created' - ) { + if (body.message_type === 'template' && body.event === 'message_created') { this.logger.verbose('check if is csat'); const data: SendTextDto = { From 7103a953056929c7a5ab215cdaccaadfb8d8b528 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 13:43:18 -0300 Subject: [PATCH 33/97] feat: Added connection with pairing code in chatwoot --- CHANGELOG.md | 1 + .../controllers/instance.controller.ts | 32 +++---------- src/whatsapp/dto/chatwoot.dto.ts | 1 + src/whatsapp/models/chatwoot.model.ts | 2 + src/whatsapp/services/chatwoot.service.ts | 27 +++++++++-- src/whatsapp/services/monitor.service.ts | 3 ++ src/whatsapp/services/whatsapp.service.ts | 47 +++++++++++++++---- src/whatsapp/types/wa.types.ts | 2 + 8 files changed, 76 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f6b6bb..7f8fd11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Created settings controller * Added reject call and send text message when receiving a call * Added setting to ignore group messages +* Added connection with pairing code in chatwoot ### Fixed diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index e7827f15..31db7918 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -103,16 +103,10 @@ export class InstanceController { if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { let getQrcode: wa.QrCode; - let getPairingCode: string; if (qrcode) { this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(); - if (number) { - this.logger.verbose('creating number'); - await delay(5000); - getPairingCode = await instance.client.requestPairingCode(number); - } + await instance.connectToWhatsapp(number); await delay(2000); getQrcode = instance.qrCode; } @@ -126,14 +120,9 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, + qrcode: getQrcode, }; - if (getPairingCode) { - result['pairingCode'] = getPairingCode; - } else { - result['qrcode'] = getQrcode; - } - this.logger.verbose('instance created'); this.logger.verbose(result); @@ -166,6 +155,7 @@ export class InstanceController { url: chatwoot_url, sign_msg: chatwoot_sign_msg || false, name_inbox: instance.instanceName, + number, }); this.chatwootService.initInstanceChatwoot( @@ -173,6 +163,7 @@ export class InstanceController { instance.instanceName, `${urlServer}/chatwoot/webhook/${instance.instanceName}`, qrcode, + number, ); } catch (error) { this.logger.log(error); @@ -193,6 +184,7 @@ export class InstanceController { token: chatwoot_token, url: chatwoot_url, sign_msg: chatwoot_sign_msg || false, + number, name_inbox: instance.instanceName, webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, }, @@ -220,19 +212,7 @@ export class InstanceController { if (state == 'close') { this.logger.verbose('connecting'); - await instance.connectToWhatsapp(); - let pairingCode = null; - if (number) { - this.logger.verbose('creating pairing code'); - await delay(5000); - pairingCode = await instance.client.requestPairingCode(number); - } - - if (pairingCode) { - return { - pairingCode, - }; - } + await instance.connectToWhatsapp(number); await delay(2000); return instance.qrCode; diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index e78b0676..a5026a46 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -5,4 +5,5 @@ export class ChatwootDto { url?: string; name_inbox?: string; sign_msg?: boolean; + number?: string; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index ca082309..54d9e051 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -9,6 +9,7 @@ export class ChatwootRaw { url?: string; name_inbox?: string; sign_msg?: boolean; + number?: string; } const chatwootSchema = new Schema({ @@ -19,6 +20,7 @@ const chatwootSchema = new Schema({ url: { type: String, required: true }, name_inbox: { type: String, required: true }, sign_msg: { type: Boolean, required: true }, + number: { type: String, required: true }, }); export const ChatwootModel = dbserver?.model( diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index aa346f0a..9fd3d6d6 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -13,6 +13,7 @@ import { SendAudioDto } from '../dto/sendMessage.dto'; import { SendMediaDto } from '../dto/sendMessage.dto'; import { ROOT_DIR } from '../../config/path.config'; import { ConfigService, HttpServer } from '../../config/env.config'; +import { delay } from '@whiskeysockets/baileys'; export class ChatwootService { private messageCacheFile: string; @@ -154,6 +155,7 @@ export class ChatwootService { inboxName: string, webhookUrl: string, qrcode: boolean, + number: string, ) { this.logger.verbose('init instance chatwoot: ' + instance.instanceName); @@ -243,11 +245,18 @@ export class ChatwootService { } this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = '/init'; + + if (number) { + contentMsg = `/init:${number}`; + } + const message = await client.messages.create({ accountId: this.provider.account_id, conversationId: conversation.id, data: { - content: '/init', + content: contentMsg, message_type: 'outgoing', }, }); @@ -953,13 +962,14 @@ export class ChatwootService { const command = messageReceived.replace('/', ''); - if (command === 'init' || command === 'iniciar') { + if (command.includes('init') || command.includes('iniciar')) { this.logger.verbose('command init found'); const state = waInstance?.connectionStatus?.state; if (state !== 'open') { this.logger.verbose('connect to whatsapp'); - await waInstance.connectToWhatsapp(); + const number = command.split(':')[1]; + await waInstance.connectToWhatsapp(number); } else { this.logger.verbose('whatsapp already connected'); await this.createBotMessage( @@ -1556,7 +1566,16 @@ export class ChatwootService { fileName, ); - const msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds:`; + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( + 0, + 4, + )}-${body.qrcode.pairingCode.substring(4, 8)}`; + } this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgQrCode, 'incoming'); diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 6c31ad58..7ffa81e1 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -25,6 +25,7 @@ import { ContactModel, MessageModel, MessageUpModel, + SettingsModel, WebhookModel, } from '../models'; @@ -241,6 +242,7 @@ export class WAMonitoringService { execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); + execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); return; } @@ -254,6 +256,7 @@ export class WAMonitoringService { await AuthModel.deleteMany({ _id: instanceName }); await WebhookModel.deleteMany({ _id: instanceName }); await ChatwootModel.deleteMany({ _id: instanceName }); + await SettingsModel.deleteMany({ _id: instanceName }); return; } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 7adaf641..9cb9b43f 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -152,6 +152,8 @@ export class WAStartupService { private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; + private phoneNumber: string; + private chatwootService = new ChatwootService(waMonitor, this.configService); public set instanceName(name: string) { @@ -241,6 +243,12 @@ export class WAStartupService { public get qrCode(): wa.QrCode { this.logger.verbose('Getting qrcode'); + if (this.instance.qrcode?.pairingCode) { + return { + pairingCode: this.instance.qrcode?.pairingCode, + }; + } + return { code: this.instance.qrcode?.code, base64: this.instance.qrcode?.base64, @@ -588,11 +596,6 @@ export class WAStartupService { return this.eventEmitter.emit('no.connection', this.instance.name); } - // pairing code - // await delay(5000); - // const code = await this.client.requestPairingCode('557499879409'); - // console.log(`Pairing code: ${code}`); - this.logger.verbose('Incrementing QR code count'); this.instance.qrcode.count++; @@ -603,6 +606,13 @@ export class WAStartupService { color: { light: '#ffffff', dark: '#198754' }, }; + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode( + this.phoneNumber, + ); + } + this.logger.verbose('Generating QR code'); qrcode.toDataURL(qr, optsQrcode, (error, base64) => { if (error) { @@ -614,7 +624,12 @@ export class WAStartupService { this.instance.qrcode.code = qr; this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { instance: this.instance.name, code: qr, base64 }, + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, }); if (this.localChatwoot.enabled) { @@ -622,7 +637,12 @@ export class WAStartupService { Events.QRCODE_UPDATED, { instanceName: this.instance.name }, { - qrcode: { instance: this.instance.name, code: qr, base64 }, + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, }, ); } @@ -631,7 +651,7 @@ export class WAStartupService { this.logger.verbose('Generating QR code in terminal'); qrcodeTerminal.generate(qr, { small: true }, (qrcode) => this.logger.log( - `\n{ instance: ${this.instance.name}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + qrcode, ), ); @@ -798,7 +818,7 @@ export class WAStartupService { return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); } - public async connectToWhatsapp(): Promise { + public async connectToWhatsapp(number?: string): Promise { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); @@ -872,6 +892,15 @@ export class WAStartupService { this.logger.verbose('Socket event handler initialized'); + this.phoneNumber = number; + + // if (number) { + // this.logger.verbose('creating pairing code'); + // await delay(5000); + // this.phoneNumber = number; + // this.instance.qrcode.pairingCode = await this.client.requestPairingCode(number); + // } + return this.client; } catch (error) { this.logger.error(error); diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 4b699d7e..d0c5f80c 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -27,11 +27,13 @@ export enum Events { export declare namespace wa { export type QrCode = { count?: number; + pairingCode?: string; base64?: string; code?: string; }; export type Instance = { qrcode?: QrCode; + pairingCode?: string; authState?: { state: AuthenticationState; saveCreds: () => void }; name?: string; wuid?: string; From fff420b652120af7919c01b9a4c55445da2bca9b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 15:05:03 -0300 Subject: [PATCH 34/97] fix: command to create new instances set to /new_instance:: --- CHANGELOG.md | 1 + .../controllers/chatwoot.controller.ts | 6 ++ src/whatsapp/services/chatwoot.service.ts | 56 ++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f8fd11e..57a8c4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * Fixed issue where it was not possible to open a conversation when sent at first by me on my cell phone in chatwoot * Now it only updates the contact name if it is the same as the phone number in chatwoot * Now accepts all chatwoot inbox templates +* Command to create new instances set to /new_instance:: ### Integrations diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index de0aef7a..13e8fcd4 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -94,4 +94,10 @@ export class ChatwootController { return chatwootService.receiveWebhook(instance, data); } + + public async newInstance(data: any) { + const chatwootService = new ChatwootService(waMonitor, this.configService); + + return chatwootService.newInstance(data); + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 9fd3d6d6..399261c2 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -13,7 +13,6 @@ import { SendAudioDto } from '../dto/sendMessage.dto'; import { SendMediaDto } from '../dto/sendMessage.dto'; import { ROOT_DIR } from '../../config/path.config'; import { ConfigService, HttpServer } from '../../config/env.config'; -import { delay } from '@whiskeysockets/baileys'; export class ChatwootService { private messageCacheFile: string; @@ -1017,7 +1016,7 @@ export class ChatwootService { await waInstance?.client?.ws?.close(); } - if (command.includes('#inbox_whatsapp')) { + if (command.includes('new_instance')) { const urlServer = this.configService.get('SERVER').URL; const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; @@ -1030,6 +1029,10 @@ export class ChatwootService { chatwoot_sign_msg: this.provider.sign_msg, }; + if (command.split(':')[2]) { + data['number'] = command.split(':')[2]; + } + const config = { method: 'post', maxBodyLength: Infinity, @@ -1585,4 +1588,53 @@ export class ChatwootService { this.logger.error(error); } } + + public async newInstance(data: any) { + try { + const instanceName = data.instanceName; + const qrcode = true; + const number = data.number; + const accountId = data.accountId; + const chatwootToken = data.token; + const chatwootUrl = data.url; + const signMsg = true; + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + console.log('data: ', data); + + const requestData = { + instanceName, + qrcode, + chatwoot_account_id: accountId, + chatwoot_token: chatwootToken, + chatwoot_url: chatwootUrl, + chatwoot_sign_msg: signMsg, + }; + + if (number) { + requestData['number'] = number; + } + + console.log('requestData: ', requestData); + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: requestData, + }; + + // await axios.request(config); + + return true; + } catch (error) { + this.logger.error(error); + return null; + } + } } From 4d00351db790b498f919f3083787a85ad7788ecb Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 15:05:26 -0300 Subject: [PATCH 35/97] fix: command to create new instances set to /new_instance:: --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a8c4cc..3cecc446 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * Created settings controller * Added reject call and send text message when receiving a call * Added setting to ignore group messages -* Added connection with pairing code in chatwoot +* Added connection with pairing code in chatwoot with command /init: ### Fixed From 45c11a5a8e9474a580557d8a8aa50f200d34646c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 15:12:13 -0300 Subject: [PATCH 36/97] fix: fix in chatwoot set, sign msg can now be disabled --- CHANGELOG.md | 1 + src/whatsapp/controllers/chatwoot.controller.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cecc446..7009fce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Now it only updates the contact name if it is the same as the phone number in chatwoot * Now accepts all chatwoot inbox templates * Command to create new instances set to /new_instance:: +* Fix in chatwoot set, sign msg can now be disabled ### Integrations diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 13e8fcd4..d5e5e841 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -33,7 +33,7 @@ export class ChatwootController { throw new BadRequestException('token is required'); } - if (!data.sign_msg) { + if (data.sign_msg !== true && data.sign_msg !== false) { throw new BadRequestException('sign_msg is required'); } } From 68d980795ad3fc57a4b902945be094934d9095cd Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 15:13:46 -0300 Subject: [PATCH 37/97] fix: fix in chatwoot set, sign msg can now be disabled --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7009fce9..ed6500be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ * Created settings controller * Added reject call and send text message when receiving a call * Added setting to ignore group messages -* Added connection with pairing code in chatwoot with command /init: +* Added connection with pairing code in chatwoot with command /init:{NUMBER} ### Fixed @@ -16,7 +16,7 @@ * Fixed issue where it was not possible to open a conversation when sent at first by me on my cell phone in chatwoot * Now it only updates the contact name if it is the same as the phone number in chatwoot * Now accepts all chatwoot inbox templates -* Command to create new instances set to /new_instance:: +* Command to create new instances set to /new_instance:{NAME}:{NUMBER} * Fix in chatwoot set, sign msg can now be disabled ### Integrations @@ -55,7 +55,7 @@ * Added messages.delete event * Added restart instance endpoint -* Created automation for creating instances in the chatwoot bot with the command '#inbox_whatsapp:' +* Created automation for creating instances in the chatwoot bot with the command '#inbox_whatsapp:{INSTANCE_NAME} * Change Baileys version to: 6.4.0 * Send contact in chatwoot * Send contact array in chatwoot From 8d91e7cb1d04abb2cf01e703a59222f04805fc5e Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 16:21:29 -0300 Subject: [PATCH 38/97] feat: Added encoding option in endpoint sendWhatsAppAudio --- CHANGELOG.md | 1 + .../controllers/instance.controller.ts | 2 +- src/whatsapp/dto/sendMessage.dto.ts | 1 + src/whatsapp/services/chatwoot.service.ts | 4 -- src/whatsapp/services/whatsapp.service.ts | 58 ++++++++++++------- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed6500be..beeb2888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Added reject call and send text message when receiving a call * Added setting to ignore group messages * Added connection with pairing code in chatwoot with command /init:{NUMBER} +* Added encoding option in endpoint sendWhatsAppAudio ### Fixed diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 31db7918..ef930c7b 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -107,7 +107,7 @@ export class InstanceController { if (qrcode) { this.logger.verbose('creating qrcode'); await instance.connectToWhatsapp(number); - await delay(2000); + await delay(3000); getQrcode = instance.qrCode; } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index 0a20674c..c2ddb3a2 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -16,6 +16,7 @@ export class Options { quoted?: Quoted; mentions?: Mentions; linkPreview?: boolean; + encoding?: boolean; } class OptionsMessage { options: Options; diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 399261c2..c77162ad 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1601,8 +1601,6 @@ export class ChatwootService { const urlServer = this.configService.get('SERVER').URL; const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - console.log('data: ', data); - const requestData = { instanceName, qrcode, @@ -1616,8 +1614,6 @@ export class ChatwootService { requestData['number'] = number; } - console.log('requestData: ', requestData); - const config = { method: 'post', maxBodyLength: Infinity, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 9cb9b43f..d475988a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1354,7 +1354,6 @@ export class WAStartupService { if (events.call) { this.logger.verbose('Listening event: call'); - console.log('events.call', events.call); const call = events.call[0]; if (settings?.reject_call && call.status == 'offer') { @@ -1662,8 +1661,6 @@ export class WAStartupService { const linkPreview = options?.linkPreview != false ? undefined : false; - console.log('linkPreview', linkPreview); - let quoted: WAMessage; if (options?.quoted) { @@ -2179,26 +2176,45 @@ export class WAStartupService { public async audioWhatsapp(data: SendAudioDto) { this.logger.verbose('Sending audio whatsapp'); - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; } + + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); } public async buttonMessage(data: SendButtonDto) { From 1c30728880874eac25f7373a1f8c702042b24682 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 17:05:18 -0300 Subject: [PATCH 39/97] version: 1.4.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index beeb2888..1bb71df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.0 (homolog) +# 1.4.0 (2023-07-24 17:03) ### Features From f7293255cf05d74c6c33ba887bc7774d926fcd5d Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 18:21:11 -0300 Subject: [PATCH 40/97] fix: Fixed reconnect with pairing code or qrcode --- CHANGELOG.md | 6 ++++++ src/whatsapp/controllers/instance.controller.ts | 4 ++-- src/whatsapp/services/whatsapp.service.ts | 10 +++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb71df6..2d62c451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.1 (homolog) + +### Fixed + +* Fixed reconnect with pairing code or qrcode + # 1.4.0 (2023-07-24 17:03) ### Features diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index ef930c7b..fa3b0812 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -107,7 +107,7 @@ export class InstanceController { if (qrcode) { this.logger.verbose('creating qrcode'); await instance.connectToWhatsapp(number); - await delay(3000); + await delay(5000); getQrcode = instance.qrCode; } @@ -214,7 +214,7 @@ export class InstanceController { this.logger.verbose('connecting'); await instance.connectToWhatsapp(number); - await delay(2000); + await delay(5000); return instance.qrCode; } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index d475988a..37ec3341 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -606,11 +606,14 @@ export class WAStartupService { color: { light: '#ffffff', dark: '#198754' }, }; + console.log(this.phoneNumber); if (this.phoneNumber) { await delay(2000); this.instance.qrcode.pairingCode = await this.client.requestPairingCode( this.phoneNumber, ); + } else { + this.instance.qrcode.pairingCode = null; } this.logger.verbose('Generating QR code'); @@ -894,13 +897,6 @@ export class WAStartupService { this.phoneNumber = number; - // if (number) { - // this.logger.verbose('creating pairing code'); - // await delay(5000); - // this.phoneNumber = number; - // this.instance.qrcode.pairingCode = await this.client.requestPairingCode(number); - // } - return this.client; } catch (error) { this.logger.error(error); From f9abd90cc9044961eb2078132f6c538433135614 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 18:28:01 -0300 Subject: [PATCH 41/97] fix: connection state --- CHANGELOG.md | 1 + src/validate/validate.schema.ts | 2 -- src/whatsapp/controllers/instance.controller.ts | 17 +++++++++++------ src/whatsapp/services/whatsapp.service.ts | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d62c451..b9672e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixed * Fixed reconnect with pairing code or qrcode +* Fixed problem in createJid # 1.4.0 (2023-07-24 17:03) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 18a070ab..aea723ca 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -124,7 +124,6 @@ const optionsSchema: JSONSchema7 = { const numberDefinition: JSONSchema7Definition = { type: 'string', - // pattern: '^\\d+[\\.@\\w-]+', description: 'Invalid format', }; @@ -446,7 +445,6 @@ export const whatsappNumberSchema: JSONSchema7 = { uniqueItems: true, items: { type: 'string', - // pattern: '^\\d+', description: '"numbers" must be an array of numeric strings', }, }, diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index fa3b0812..7322e194 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -245,7 +245,12 @@ export class InstanceController { public async connectionState({ instanceName }: InstanceDto) { this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); - return this.waMonitor.waInstances[instanceName]?.connectionStatus; + return { + instance: { + instanceName: instanceName, + state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, + }, + }; } public async fetchInstances({ instanceName }: InstanceDto) { @@ -260,9 +265,9 @@ export class InstanceController { public async logout({ instanceName }: InstanceDto) { this.logger.verbose('requested logout from ' + instanceName + ' instance'); - const stateConn = await this.connectionState({ instanceName }); + const { instance } = await this.connectionState({ instanceName }); - if (stateConn.state === 'close') { + if (instance.state === 'close') { throw new BadRequestException( 'The "' + instanceName + '" instance is not connected', ); @@ -285,15 +290,15 @@ export class InstanceController { public async deleteInstance({ instanceName }: InstanceDto) { this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); - const stateConn = await this.connectionState({ instanceName }); + const { instance } = await this.connectionState({ instanceName }); - if (stateConn.state === 'open') { + if (instance.state === 'open') { throw new BadRequestException( 'The "' + instanceName + '" instance needs to be disconnected', ); } try { - if (stateConn.state === 'connecting') { + if (instance.state === 'connecting') { this.logger.verbose('logging out instance: ' + instanceName); await this.logout({ instanceName }); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 37ec3341..6c7c96f0 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1517,7 +1517,7 @@ export class WAStartupService { .split(/\:/)[0] .split('@')[0]; - if (number.includes('-') && number.length >= 24) { + if (number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; From 8d1f2313ac002b179cd7ed79089f83a28bb2b326 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 18:28:39 -0300 Subject: [PATCH 42/97] version: 1.4.1 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9672e6d..7dd59c0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.1 (homolog) +# 1.4.1 (2023-07-24 18:28) ### Fixed diff --git a/package.json b/package.json index 92745ead..1145676d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.0", + "version": "1.4.1", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 0cc1f18a7eaafaa851194a0955f5bbf9637350bf Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 19:05:32 -0300 Subject: [PATCH 43/97] =?UTF-8?q?[BUG]=20Corre=C3=A7=C3=A3o=20de=20mencion?= =?UTF-8?q?ar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caso seja enviado everyOne como false sem passar nada no mentioned --- src/whatsapp/services/whatsapp.service.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 6c7c96f0..833637b2 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1686,20 +1686,13 @@ export class WAStartupService { if (options?.mentions) { this.logger.verbose('Mentions defined'); - if ( - !Array.isArray(options.mentions.mentioned) && - !options.mentions.everyOne - ) { - throw new BadRequestException('Mentions must be an array'); - } - - if (options.mentions.everyOne) { + if (options.mentions?.everyOne) { this.logger.verbose('Mentions everyone'); this.logger.verbose('Getting group metadata'); mentions = groupMetadata.participants.map((participant) => participant.id); this.logger.verbose('Getting group metadata for mentions'); - } else { + } else if(options.mentions?.mentioned?.length) { this.logger.verbose('Mentions manually defined'); mentions = options.mentions.mentioned.map((mention) => { const jid = this.createJid(mention); From ef4be6a612ff50465d1e48925eef14d578592b68 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 19:53:03 -0300 Subject: [PATCH 44/97] Start --- src/validate/validate.schema.ts | 1 + src/whatsapp/dto/group.dto.ts | 3 ++- src/whatsapp/services/whatsapp.service.ts | 11 ++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 18a070ab..761f01ba 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -669,6 +669,7 @@ export const createGroupSchema: JSONSchema7 = { subject: { type: 'string' }, description: { type: 'string' }, profilePicture: { type: 'string' }, + promoteParticipants: { type: 'boolean', enum: [true, false] }, participants: { type: 'array', minItems: 1, diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index bc36e27f..6dfdc45c 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -1,7 +1,8 @@ export class CreateGroupDto { subject: string; - description?: string; participants: string[]; + description?: string; + promoteParticipants?: boolean; } export class GroupPictureDto { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index d475988a..4f832852 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -2785,11 +2785,20 @@ export class WAStartupService { this.logger.verbose('Updating group description: ' + create.description); await this.client.groupUpdateDescription(id, create.description); } + + if (create?.promoteParticipants) { + this.logger.verbose('Prometing group participants: ' + create.description); + await this.updateGParticipant({ + groupJid: id, + action: "promote", + participants: participants + }); + } const group = await this.client.groupMetadata(id); this.logger.verbose('Getting group metadata'); - return { groupMetadata: group }; + return group; } catch (error) { this.logger.error(error); throw new InternalServerErrorException('Error creating group', error.toString()); From 58ed6f395fce5ba7a8d8c7cc15592e981204cc15 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Mon, 24 Jul 2023 19:59:09 -0300 Subject: [PATCH 45/97] =?UTF-8?q?Valida=C3=A7=C3=A3o=20de=20Grupo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/whatsapp/services/whatsapp.service.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 833637b2..a2767d16 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1517,14 +1517,22 @@ export class WAStartupService { .split(/\:/)[0] .split('@')[0]; + // Verificação de Grupos Antigos + if(number.includes('-') && number.length >= 24){ + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + number = number.replace(/\D/g, ''); + + // Verificação de Grupos Novos if (number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } - number = number.replace(/\D/g, ''); - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); return `${number}@s.whatsapp.net`; } From 84f3f072794d054d0e7097f3495f28569b6a1147 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:11:15 -0300 Subject: [PATCH 46/97] fix: Fixed validation is set settings --- src/whatsapp/controllers/settings.controller.ts | 4 ---- src/whatsapp/services/whatsapp.service.ts | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 59031634..f538abe6 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -15,10 +15,6 @@ export class SettingsController { 'requested createSettings from ' + instance.instanceName + ' instance', ); - if (data.reject_call && data.msg_call.trim() == '') { - throw new BadRequestException('msg_call is required'); - } - return this.settingsService.create(instance, data); } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 833637b2..0cfaf24c 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1692,7 +1692,7 @@ export class WAStartupService { this.logger.verbose('Getting group metadata'); mentions = groupMetadata.participants.map((participant) => participant.id); this.logger.verbose('Getting group metadata for mentions'); - } else if(options.mentions?.mentioned?.length) { + } else if (options.mentions?.mentioned?.length) { this.logger.verbose('Mentions manually defined'); mentions = options.mentions.mentioned.map((mention) => { const jid = this.createJid(mention); From c5824767c845013c83dceb735b2e13a0606985e1 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:11:49 -0300 Subject: [PATCH 47/97] fix: Fixed validation is set settings --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd59c0d..25812c6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.4.2 (homolog) + +### Fixed + +* Fixed validation is set settings +* Adjusts in group validations + # 1.4.1 (2023-07-24 18:28) ### Fixed From b77f22790bb5ea17133993f005d3613193698f3a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:13:18 -0300 Subject: [PATCH 48/97] fix: Fixed validation is set settings --- src/whatsapp/services/whatsapp.service.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index ade1e951..69e9562a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1517,16 +1517,14 @@ export class WAStartupService { .split(/\:/)[0] .split('@')[0]; - // Verificação de Grupos Antigos - if(number.includes('-') && number.length >= 24){ + if (number.includes('-') && number.length >= 24) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); return `${number}@g.us`; } - + number = number.replace(/\D/g, ''); - // Verificação de Grupos Novos if (number.length >= 18) { this.logger.verbose('Jid created is group: ' + `${number}@g.us`); number = number.replace(/[^\d-]/g, ''); @@ -2782,13 +2780,13 @@ export class WAStartupService { this.logger.verbose('Updating group description: ' + create.description); await this.client.groupUpdateDescription(id, create.description); } - + if (create?.promoteParticipants) { this.logger.verbose('Prometing group participants: ' + create.description); await this.updateGParticipant({ groupJid: id, - action: "promote", - participants: participants + action: 'promote', + participants: participants, }); } From f475391ba6b3c8af8d0fbdf02819b0a7bfacee80 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:32:15 -0300 Subject: [PATCH 49/97] fix: Ajusts in sticker message to chatwoot --- .../controllers/instance.controller.ts | 259 +++++++++--------- src/whatsapp/repository/repository.manager.ts | 5 + src/whatsapp/services/chatwoot.service.ts | 6 +- src/whatsapp/services/whatsapp.service.ts | 7 +- 4 files changed, 141 insertions(+), 136 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 7322e194..e9e690b3 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -41,77 +41,136 @@ export class InstanceController { chatwoot_url, chatwoot_sign_msg, }: InstanceDto) { - this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + try { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException( - 'The instance name must be lowercase and without special characters', - ); - } - - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); - - this.logger.verbose('creating instance'); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); - - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); - - this.logger.verbose('hash: ' + hash + ' generated'); - - let getEvents: string[]; - - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException( + 'The instance name must be lowercase and without special characters', + ); } - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); + + this.logger.verbose('creating instance'); + const instance = new WAStartupService( + this.configService, + this.eventEmitter, + this.repository, + this.cache, + ); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); + + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); + + this.logger.verbose('hash: ' + hash + ' generated'); + + let getEvents: string[]; + + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); + } + + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(number); + await delay(5000); + getQrcode = instance.qrCode; + } + + const result = { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }; + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + number, }); - getEvents = (await this.webhookService.find(instance)).events; + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + number, + ); } catch (error) { this.logger.log(error); } - } - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; - - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(number); - await delay(5000); - getQrcode = instance.qrCode; - } - - const result = { + return { instance: { instanceName: instance.instanceName, status: 'created', @@ -120,75 +179,21 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, - qrcode: getQrcode, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + number, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, }; - - this.logger.verbose('instance created'); - this.logger.verbose(result); - - return result; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - number, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - number, - ); } catch (error) { - this.logger.log(error); + console.log(error); + return { error: true, message: error.toString() }; } - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - number, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; } public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index dde636c7..d506cc46 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -102,7 +102,12 @@ export class RepositoryBroker { this.logger.verbose('creating store path: ' + storePath); const tempDir = join(storePath, 'temp'); + const chatwootDir = join(storePath, 'chatwoot'); + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } if (!fs.existsSync(tempDir)) { this.logger.verbose('creating temp dir: ' + tempDir); fs.mkdirSync(tempDir, { recursive: true }); diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c77162ad..6e55d145 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1179,7 +1179,7 @@ export class ChatwootService { videoMessage: msg.videoMessage?.caption, extendedTextMessage: msg.extendedTextMessage?.text, messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), + stickerMessage: undefined, documentMessage: msg.documentMessage?.caption, documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, @@ -1199,10 +1199,6 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; - if (typeKey === 'stickerMessage') { - return null; - } - if (typeKey === 'contactMessage') { const vCardData = result.split('\n'); const contactInfo = {}; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 69e9562a..ff6fdd71 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -116,16 +116,15 @@ import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-d import Long from 'long'; import { WebhookRaw } from '../models/webhook.model'; import { ChatwootRaw } from '../models/chatwoot.model'; +import { SettingsRaw } from '../models'; import { dbserver } from '../../db/db.connect'; import NodeCache from 'node-cache'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import sharp from 'sharp'; import { RedisCache } from '../../db/redis.client'; import { Log } from '../../config/env.config'; -import ProxyAgent from 'proxy-agent'; import { ChatwootService } from './chatwoot.service'; import { waMonitor } from '../whatsapp.module'; -import { SettingsRaw } from '../models'; export class WAStartupService { constructor( @@ -382,7 +381,7 @@ export class WAStartupService { if (!data) { this.logger.verbose('Settings not found'); - throw new NotFoundException('Settings not found'); + return null; } this.logger.verbose(`Settings url: ${data.reject_call}`); @@ -1129,7 +1128,7 @@ export class WAStartupService { received.messageTimestamp = received.messageTimestamp?.toNumber(); } - if (settings.groups_ignore && received.key.remoteJid.includes('@g.us')) { + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { this.logger.verbose('group ignored'); return; } From c76334a68aab4622d75abd905bbd59eaee8e2fa9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:32:29 -0300 Subject: [PATCH 50/97] fix: Ajusts in sticker message to chatwoot --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25812c6e..cddb23a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Fixed validation is set settings * Adjusts in group validations +* Ajusts in sticker message to chatwoot # 1.4.1 (2023-07-24 18:28) From 4d9ca4b4511ba997f28b4efa764b9079a180bcb4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 24 Jul 2023 20:52:34 -0300 Subject: [PATCH 51/97] version: 1.4.2 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cddb23a1..7429fddc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.2 (homolog) +# 1.4.2 (2023-07-24 20:52) ### Fixed diff --git a/package.json b/package.json index 1145676d..6489b86c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.1", + "version": "1.4.2", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From a12231a0aa003a01510cec3dc7db5d8e430fb523 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 09:57:28 -0300 Subject: [PATCH 52/97] fix: Adjusts in settings with options always_online, read_messages and read_status --- CHANGELOG.md | 6 +++++ src/validate/validate.schema.ts | 23 ++++++++++++++---- src/whatsapp/dto/chat.dto.ts | 2 +- src/whatsapp/dto/settings.dto.ts | 3 +++ src/whatsapp/models/settings.model.ts | 6 +++++ src/whatsapp/services/whatsapp.service.ts | 29 +++++++++++++++++++++-- src/whatsapp/types/wa.types.ts | 3 +++ 7 files changed, 65 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7429fddc..bc6da5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.3 (homolog) + +### Fixed + +* Adjusts in settings with options always_online, read_messages and read_status + # 1.4.2 (2023-07-24 20:52) ### Fixed diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 3c1f8d0a..318835b1 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -455,7 +455,7 @@ export const readMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', properties: { - readMessages: { + read_messages: { type: 'array', minItems: 1, uniqueItems: true, @@ -470,7 +470,7 @@ export const readMessageSchema: JSONSchema7 = { }, }, }, - required: ['readMessages'], + required: ['read_messages'], }; export const privacySettingsSchema: JSONSchema7 = { @@ -884,7 +884,22 @@ export const settingsSchema: JSONSchema7 = { reject_call: { type: 'boolean', enum: [true, false] }, msg_call: { type: 'string' }, groups_ignore: { type: 'boolean', enum: [true, false] }, + always_online: { type: 'boolean', enum: [true, false] }, + read_messages: { type: 'boolean', enum: [true, false] }, + read_status: { type: 'boolean', enum: [true, false] }, }, - required: ['reject_call'], - ...isNotEmpty('reject_call'), + required: [ + 'reject_call', + 'groups_ignore', + 'always_online', + 'read_messages', + 'read_status', + ], + ...isNotEmpty( + 'reject_call', + 'groups_ignore', + 'always_online', + 'read_messages', + 'read_status', + ), }; diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 5af66a5e..4681ef76 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -59,7 +59,7 @@ class Key { remoteJid: string; } export class ReadMessageDto { - readMessages: Key[]; + read_messages: Key[]; } class LastMessage { diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts index 20a6cba0..594ab3a4 100644 --- a/src/whatsapp/dto/settings.dto.ts +++ b/src/whatsapp/dto/settings.dto.ts @@ -2,4 +2,7 @@ export class SettingsDto { reject_call?: boolean; msg_call?: string; groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index b5eb7fe7..b6d2488d 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -6,6 +6,9 @@ export class SettingsRaw { reject_call?: boolean; msg_call?: string; groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } const settingsSchema = new Schema({ @@ -13,6 +16,9 @@ const settingsSchema = new Schema({ reject_call: { type: Boolean, required: true }, msg_call: { type: String, required: true }, groups_ignore: { type: Boolean, required: true }, + always_online: { type: Boolean, required: true }, + read_messages: { type: Boolean, required: true }, + read_status: { type: Boolean, required: true }, }); export const SettingsModel = dbserver?.model( diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index ff6fdd71..66a46aca 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -362,6 +362,15 @@ export class WAStartupService { this.localSettings.groups_ignore = data?.groups_ignore; this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + this.localSettings.always_online = data?.always_online; + this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); + + this.localSettings.read_messages = data?.read_messages; + this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); + + this.localSettings.read_status = data?.read_status; + this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); + this.logger.verbose('Settings loaded'); } @@ -371,8 +380,13 @@ export class WAStartupService { this.logger.verbose(`Settings reject_call: ${data.reject_call}`); this.logger.verbose(`Settings msg_call: ${data.msg_call}`); this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); Object.assign(this.localSettings, data); this.logger.verbose('Settings set'); + + this.client?.ws?.close(); } public async findSettings() { @@ -387,6 +401,9 @@ export class WAStartupService { this.logger.verbose(`Settings url: ${data.reject_call}`); this.logger.verbose(`Settings msg_call: ${data.msg_call}`); this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); return data; } @@ -847,6 +864,7 @@ export class WAStartupService { printQRInTerminal: false, browser, version, + markOnlineOnConnect: this.localSettings.always_online, connectTimeoutMs: 60_000, qrTimeout: 40_000, defaultQueryTimeoutMs: undefined, @@ -1143,6 +1161,14 @@ export class WAStartupService { source: getDevice(received.key.id), }; + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + this.logger.log(messageRaw); this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); @@ -2400,7 +2426,7 @@ export class WAStartupService { this.logger.verbose('Marking message as read'); try { const keys: proto.IMessageKey[] = []; - data.readMessages.forEach((read) => { + data.read_messages.forEach((read) => { if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { keys.push({ remoteJid: read.remoteJid, @@ -2640,7 +2666,6 @@ export class WAStartupService { await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); this.logger.verbose('Groups add privacy updated'); - // reinicia a instancia this.client?.ws?.close(); return { diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index d0c5f80c..9644b76e 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -61,6 +61,9 @@ export declare namespace wa { reject_call?: boolean; msg_call?: string; groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; }; export type StateConnection = { From 62e2a8a6e3a882e8350131aa53becc28ee862df7 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 10:23:18 -0300 Subject: [PATCH 53/97] fix: Fixed send webhook for event CALL --- CHANGELOG.md | 1 + Docker/.env.example | 1 + Dockerfile | 1 + src/config/env.config.ts | 2 ++ src/dev-env.yml | 1 + src/validate/validate.schema.ts | 2 ++ src/whatsapp/services/whatsapp.service.ts | 4 ++++ src/whatsapp/types/wa.types.ts | 1 + 8 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6da5f1..3d875a4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixed * Adjusts in settings with options always_online, read_messages and read_status +* Fixed send webhook for event CALL # 1.4.2 (2023-07-24 20:52) diff --git a/Docker/.env.example b/Docker/.env.example index c3dbe505..f4e8291d 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -73,6 +73,7 @@ WEBHOOK_EVENTS_GROUPS_UPSERT=true WEBHOOK_EVENTS_GROUPS_UPDATE=true WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true WEBHOOK_EVENTS_CONNECTION_UPDATE=true +WEBHOOK_EVENTS_CALL=true # This event fires every time a new token is requested via the refresh route WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/Dockerfile b/Dockerfile index 93fa60c4..0b3ac950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,6 +74,7 @@ ENV WEBHOOK_EVENTS_GROUPS_UPSERT=true ENV WEBHOOK_EVENTS_GROUPS_UPDATE=true ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true +ENV WEBHOOK_EVENTS_CALL=true ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 88b718de..78c90ec9 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -89,6 +89,7 @@ export type EventsWebhook = { GROUPS_UPSERT: boolean; GROUP_UPDATE: boolean; GROUP_PARTICIPANTS_UPDATE: boolean; + CALL: boolean; NEW_JWT_TOKEN: boolean; }; @@ -245,6 +246,7 @@ export class ConfigService { GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', }, }, diff --git a/src/dev-env.yml b/src/dev-env.yml index 41368ea4..b45d3201 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -110,6 +110,7 @@ WEBHOOK: GROUP_UPDATE: true GROUP_PARTICIPANTS_UPDATE: true CONNECTION_UPDATE: true + CALL: true # This event fires every time a new token is requested via the refresh route NEW_JWT_TOKEN: false diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 318835b1..62b7e41c 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -53,6 +53,7 @@ export const instanceNameSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'CALL', 'NEW_JWT_TOKEN', ], }, @@ -854,6 +855,7 @@ export const webhookSchema: JSONSchema7 = { 'GROUP_UPDATE', 'GROUP_PARTICIPANTS_UPDATE', 'CONNECTION_UPDATE', + 'CALL', 'NEW_JWT_TOKEN', ], }, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 66a46aca..8959197e 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1388,11 +1388,15 @@ export class WAStartupService { text: settings.msg_call, }); + this.logger.verbose('Sending data to event messages.upsert'); this.client.ev.emit('messages.upsert', { messages: [msg], type: 'notify', }); } + + this.logger.verbose('Sending data to webhook in event CALL'); + this.sendDataWebhook(Events.CALL, call); } if (events['connection.update']) { diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 9644b76e..80aede98 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -22,6 +22,7 @@ export enum Events { GROUPS_UPSERT = 'groups.upsert', GROUPS_UPDATE = 'groups.update', GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + CALL = 'call', } export declare namespace wa { From c314d00ccd98ae8bcc55b0873e2ccf1497fff88c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 10:42:34 -0300 Subject: [PATCH 54/97] fix: Create instance with settings --- .../controllers/instance.controller.ts | 24 +++++++++++++++++++ src/whatsapp/dto/instance.dto.ts | 12 +++++++--- src/whatsapp/whatsapp.module.ts | 1 + 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index e9e690b3..9be6ab5d 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -13,6 +13,7 @@ import { Logger } from '../../config/logger.config'; import { wa } from '../types/wa.types'; import { RedisCache } from '../../db/redis.client'; import { isURL } from 'class-validator'; +import { SettingsService } from '../services/settings.service'; export class InstanceController { constructor( @@ -23,6 +24,7 @@ export class InstanceController { private readonly authService: AuthService, private readonly webhookService: WebhookService, private readonly chatwootService: ChatwootService, + private readonly settingsService: SettingsService, private readonly cache: RedisCache, ) {} @@ -40,6 +42,12 @@ export class InstanceController { chatwoot_token, chatwoot_url, chatwoot_sign_msg, + reject_call, + msg_call, + groups_ignore, + always_online, + read_messages, + read_status, }: InstanceDto) { try { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -102,6 +110,20 @@ export class InstanceController { } } + this.logger.verbose('creating settings'); + const settings: wa.LocalSettings = { + reject_call: reject_call || false, + msg_call: msg_call || '', + groups_ignore: groups_ignore || false, + always_online: always_online || false, + read_messages: read_messages || false, + read_status: read_status || false, + }; + + this.logger.verbose('settings: ' + JSON.stringify(settings)); + + this.settingsService.create(instance, settings); + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { let getQrcode: wa.QrCode; @@ -121,6 +143,7 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, + settings, qrcode: getQrcode, }; @@ -179,6 +202,7 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, + settings, chatwoot: { enabled: true, account_id: chatwoot_account_id, diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 9e8a7ec3..ca88a729 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -1,11 +1,17 @@ export class InstanceDto { instanceName: string; - webhook?: string; - webhook_by_events?: boolean; - events?: string[]; qrcode?: boolean; number?: string; token?: string; + webhook?: string; + webhook_by_events?: boolean; + events?: string[]; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; chatwoot_account_id?: string; chatwoot_token?: string; chatwoot_url?: string; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 9f2fed00..b8f3b1ad 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -94,6 +94,7 @@ export const instanceController = new InstanceController( authService, webhookService, chatwootService, + settingsService, cache, ); export const viewsController = new ViewsController(waMonitor, configService); From fdee1df5b32ef9d867819b83ab2acd9051a360e3 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 10:42:45 -0300 Subject: [PATCH 55/97] fix: Create instance with settings --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d875a4a..7c9472f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Adjusts in settings with options always_online, read_messages and read_status * Fixed send webhook for event CALL +* Create instance with settings # 1.4.2 (2023-07-24 20:52) From 1cd7291068d8818aa6f3e8cfabe6ecfc6fc9fa19 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 10:51:28 -0300 Subject: [PATCH 56/97] version: 1.4.3 --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c9472f9..315ee04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.3 (homolog) +# 1.4.3 (2023-07-25 10:51) ### Fixed diff --git a/package.json b/package.json index 6489b86c..716aeec4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.2", + "version": "1.4.3", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 4c006970a24c798a701c3a2adc1e573e0b8956be Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 11:52:26 -0300 Subject: [PATCH 57/97] fix: Fixed chatwoot line wrap issue --- CHANGELOG.md | 6 ++++++ package.json | 2 +- src/whatsapp/services/chatwoot.service.ts | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 315ee04d..8eda7c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.4 (homolog) + +### Fixed + +* Fixed chatwoot line wrap issue + # 1.4.3 (2023-07-25 10:51) ### Fixed diff --git a/package.json b/package.json index 716aeec4..8cfd9f8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.3", + "version": "1.4.4", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 6e55d145..39873651 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1125,12 +1125,12 @@ export class ChatwootService { } if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is csat'); + this.logger.verbose('check if is template'); const data: SendTextDto = { number: chatId, textMessage: { - text: body.content, + text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), }, options: { delay: 1200, From 89f40d54d979b38d0583275b8fc20665d35bff20 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 12:41:54 -0300 Subject: [PATCH 58/97] fix: Solved receive location in chatwoot --- CHANGELOG.md | 1 + src/whatsapp/services/chatwoot.service.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eda7c55..4a2f5b55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixed * Fixed chatwoot line wrap issue +* Solved receive location in chatwoot # 1.4.3 (2023-07-25 10:51) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 39873651..5a981238 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -13,6 +13,7 @@ import { SendAudioDto } from '../dto/sendMessage.dto'; import { SendMediaDto } from '../dto/sendMessage.dto'; import { ROOT_DIR } from '../../config/path.config'; import { ConfigService, HttpServer } from '../../config/env.config'; +import { type } from 'os'; export class ChatwootService { private messageCacheFile: string; @@ -1186,6 +1187,10 @@ export class ChatwootService { audioMessage: msg.audioMessage?.caption, contactMessage: msg.contactMessage?.vcard, contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: + msg.locationMessage?.degreesLatitude + + ',' + + msg.locationMessage?.degreesLongitude, }; this.logger.verbose('type message: ' + types); @@ -1199,6 +1204,20 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; + if (typeKey === 'locationMessage') { + const [latitude, longitude] = result.split(','); + + const formattedLocation = `**Location:** + **latitude:** ${latitude} + **longitude:** ${longitude} + https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} + `; + + this.logger.verbose('message content: ' + formattedLocation); + + return formattedLocation; + } + if (typeKey === 'contactMessage') { const vCardData = result.split('\n'); const contactInfo = {}; From 14529f2c3580445a892a46dcdfdc25215e211d1a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 12:47:35 -0300 Subject: [PATCH 59/97] fix: When requesting the pairing code, it also brings the qr code --- CHANGELOG.md | 1 + src/whatsapp/services/whatsapp.service.ts | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2f5b55..bb0fd7bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Fixed chatwoot line wrap issue * Solved receive location in chatwoot +* When requesting the pairing code, it also brings the qr code # 1.4.3 (2023-07-25 10:51) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 8959197e..370d9b64 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -242,13 +242,9 @@ export class WAStartupService { public get qrCode(): wa.QrCode { this.logger.verbose('Getting qrcode'); - if (this.instance.qrcode?.pairingCode) { - return { - pairingCode: this.instance.qrcode?.pairingCode, - }; - } return { + pairingCode: this.instance.qrcode?.pairingCode, code: this.instance.qrcode?.code, base64: this.instance.qrcode?.base64, }; From f0d8c2d0954bb5a5f9bf7e3db943b7b53bc6dbf9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 13:19:15 -0300 Subject: [PATCH 60/97] fix: Solved receive location in chatwoot --- src/whatsapp/services/chatwoot.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 5a981238..c660f6d6 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1187,10 +1187,11 @@ export class ChatwootService { audioMessage: msg.audioMessage?.caption, contactMessage: msg.contactMessage?.vcard, contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: - msg.locationMessage?.degreesLatitude + - ',' + - msg.locationMessage?.degreesLongitude, + locationMessage: !msg.protocolMessage + ? msg.locationMessage?.degreesLatitude + + ',' + + msg.locationMessage?.degreesLongitude + : undefined, }; this.logger.verbose('type message: ' + types); From aef92240cc09ab7c74574c24d5ab916de89c0bb6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 15:20:21 -0300 Subject: [PATCH 61/97] fix: Option conversation_pending in chatwoot endpoint --- CHANGELOG.md | 2 + src/validate/validate.schema.ts | 21 ++++++++- .../controllers/chatwoot.controller.ts | 2 + .../controllers/instance.controller.ts | 24 ++++++++++ src/whatsapp/dto/chatwoot.dto.ts | 2 + src/whatsapp/dto/instance.dto.ts | 2 + src/whatsapp/models/chatwoot.model.ts | 2 + src/whatsapp/services/chatwoot.service.ts | 46 ++++++++++++++----- src/whatsapp/services/whatsapp.service.ts | 18 +++++++- src/whatsapp/types/wa.types.ts | 3 ++ 10 files changed, 107 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0fd7bb..3922d284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Fixed chatwoot line wrap issue * Solved receive location in chatwoot * When requesting the pairing code, it also brings the qr code +* Option reopen_conversation in chatwoot endpoint +* Option conversation_pending in chatwoot endpoint # 1.4.3 (2023-07-25 10:51) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 62b7e41c..d7e2ac1e 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -874,9 +874,26 @@ export const chatwootSchema: JSONSchema7 = { token: { type: 'string' }, url: { type: 'string' }, sign_msg: { type: 'boolean', enum: [true, false] }, + reopen_conversation: { type: 'boolean', enum: [true, false] }, + conversation_pending: { type: 'boolean', enum: [true, false] }, }, - required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], - ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), + required: [ + 'enabled', + 'account_id', + 'token', + 'url', + 'sign_msg', + 'reopen_conversation', + 'conversation_pending', + ], + ...isNotEmpty( + 'account_id', + 'token', + 'url', + 'sign_msg', + 'reopen_conversation', + 'conversation_pending', + ), }; export const settingsSchema: JSONSchema7 = { diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index d5e5e841..ad92e607 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -44,6 +44,8 @@ export class ChatwootController { data.token = ''; data.url = ''; data.sign_msg = false; + data.reopen_conversation = false; + data.conversation_pending = false; } data.name_inbox = instance.instanceName; diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 9be6ab5d..d9c351f7 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -42,6 +42,8 @@ export class InstanceController { chatwoot_token, chatwoot_url, chatwoot_sign_msg, + chatwoot_reopen_conversation, + chatwoot_conversation_pending, reject_call, msg_call, groups_ignore, @@ -169,6 +171,24 @@ export class InstanceController { throw new BadRequestException('Invalid "url" property in chatwoot'); } + if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + + if ( + chatwoot_reopen_conversation !== true && + chatwoot_reopen_conversation !== false + ) { + throw new BadRequestException('reopen_conversation is required'); + } + + if ( + chatwoot_conversation_pending !== true && + chatwoot_conversation_pending !== false + ) { + throw new BadRequestException('conversation_pending is required'); + } + const urlServer = this.configService.get('SERVER').URL; try { @@ -180,6 +200,8 @@ export class InstanceController { sign_msg: chatwoot_sign_msg || false, name_inbox: instance.instanceName, number, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, }); this.chatwootService.initInstanceChatwoot( @@ -209,6 +231,8 @@ export class InstanceController { token: chatwoot_token, url: chatwoot_url, sign_msg: chatwoot_sign_msg || false, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, number, name_inbox: instance.instanceName, webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index a5026a46..b270c869 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -6,4 +6,6 @@ export class ChatwootDto { name_inbox?: string; sign_msg?: boolean; number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index ca88a729..c317060f 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -16,4 +16,6 @@ export class InstanceDto { chatwoot_token?: string; chatwoot_url?: string; chatwoot_sign_msg?: boolean; + chatwoot_reopen_conversation?: boolean; + chatwoot_conversation_pending?: boolean; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index 54d9e051..bac226e9 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -10,6 +10,8 @@ export class ChatwootRaw { name_inbox?: string; sign_msg?: boolean; number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } const chatwootSchema = new Schema({ diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c660f6d6..facea536 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -231,12 +231,19 @@ export class ChatwootService { if (qrcode) { this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + console.log('this.provider', this.provider); const conversation = await client.conversations.create({ accountId: this.provider.account_id, - data: { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }, + data, }); if (!conversation) { @@ -521,11 +528,20 @@ export class ChatwootService { })) as any; if (contactConversations) { + let conversation: any; + if (this.provider.reopen_conversation) { + conversation = contactConversations.payload.find( + (conversation) => conversation.inbox_id == filterInbox.id, + ); + } else { + conversation = contactConversations.payload.find( + (conversation) => + conversation.status !== 'resolved' && + conversation.inbox_id == filterInbox.id, + ); + } this.logger.verbose('return conversation if exists'); - const conversation = contactConversations.payload.find( - (conversation) => - conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); + if (conversation) { this.logger.verbose('conversation found'); return conversation.id; @@ -533,12 +549,18 @@ export class ChatwootService { } this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + const conversation = await client.conversations.create({ accountId: this.provider.account_id, - data: { - contact_id: `${contactId}`, - inbox_id: `${filterInbox.id}`, - }, + data, }); if (!conversation) { diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 370d9b64..ccefcfa0 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -312,6 +312,19 @@ export class WAStartupService { this.localChatwoot.sign_msg = data?.sign_msg; this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + this.localChatwoot.number = data?.number; + this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); + + this.localChatwoot.reopen_conversation = data?.reopen_conversation; + this.logger.verbose( + `Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`, + ); + + this.localChatwoot.conversation_pending = data?.conversation_pending; + this.logger.verbose( + `Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`, + ); + this.logger.verbose('Chatwoot loaded'); } @@ -323,6 +336,8 @@ export class WAStartupService { this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); Object.assign(this.localChatwoot, data); this.logger.verbose('Chatwoot set'); @@ -342,6 +357,8 @@ export class WAStartupService { this.logger.verbose(`Chatwoot url: ${data.url}`); this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); return data; } @@ -618,7 +635,6 @@ export class WAStartupService { color: { light: '#ffffff', dark: '#198754' }, }; - console.log(this.phoneNumber); if (this.phoneNumber) { await delay(2000); this.instance.qrcode.pairingCode = await this.client.requestPairingCode( diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 80aede98..a0d514d8 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -56,6 +56,9 @@ export declare namespace wa { url?: string; name_inbox?: string; sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; }; export type LocalSettings = { From 6bb1abd7f0a186dd535cb11610fc860e078cb219 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 15:29:42 -0300 Subject: [PATCH 62/97] fix: Option conversation_pending in chatwoot endpoint --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3922d284..f3a30ac7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.4 (homolog) +# 1.4.4 (2023-07-25 15:24) ### Fixed From ecae077c6d50dc480934409efb00ee5f83a87389 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 25 Jul 2023 16:16:49 -0300 Subject: [PATCH 63/97] fix: Option conversation_pending in chatwoot endpoint --- src/whatsapp/services/chatwoot.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index facea536..045c6519 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1209,7 +1209,7 @@ export class ChatwootService { audioMessage: msg.audioMessage?.caption, contactMessage: msg.contactMessage?.vcard, contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: !msg.protocolMessage + locationMessage: msg.locationMessage ? msg.locationMessage?.degreesLatitude + ',' + msg.locationMessage?.degreesLongitude From dd0c1e20a723b835407efbb3b96005de5a5a7680 Mon Sep 17 00:00:00 2001 From: CodePhix Date: Tue, 25 Jul 2023 19:59:42 -0300 Subject: [PATCH 64/97] Update whatsapp.service.ts --- src/whatsapp/services/whatsapp.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index ccefcfa0..bd07f951 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1277,7 +1277,7 @@ export class WAStartupService { 5: 'PLAYED', }; for await (const { key, update } of args) { - if (settings.groups_ignore && key.remoteJid.includes('@g.us')) { + if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { this.logger.verbose('group ignored'); return; } From de0c9a1eff88aa1d22fad9ba9f8b8676caa4aa96 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 09:34:10 -0300 Subject: [PATCH 65/97] fix: fixed problems in localization template --- CHANGELOG.md | 6 ++++++ src/whatsapp/services/chatwoot.service.ts | 15 ++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a30ac7..88129b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.5 (2023-07-26 09:32) + +### Fixed + +* Fixed problems in localization template + # 1.4.4 (2023-07-25 15:24) ### Fixed diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 045c6519..90a0fa1a 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -240,7 +240,6 @@ export class ChatwootService { data['status'] = 'pending'; } - console.log('this.provider', this.provider); const conversation = await client.conversations.create({ accountId: this.provider.account_id, data, @@ -1209,11 +1208,8 @@ export class ChatwootService { audioMessage: msg.audioMessage?.caption, contactMessage: msg.contactMessage?.vcard, contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: msg.locationMessage - ? msg.locationMessage?.degreesLatitude + - ',' + - msg.locationMessage?.degreesLongitude - : undefined, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, }; this.logger.verbose('type message: ' + types); @@ -1227,8 +1223,9 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; - if (typeKey === 'locationMessage') { - const [latitude, longitude] = result.split(','); + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { + const latitude = result.degreesLatitude; + const longitude = result.degreesLongitude; const formattedLocation = `**Location:** **latitude:** ${latitude} @@ -1311,7 +1308,7 @@ export class ChatwootService { this.logger.verbose('get conversation message'); const types = this.getTypeMessage(msg); - += const messageContent = this.getMessageContent(types); this.logger.verbose('conversation message: ' + messageContent); From 3f41974a753285fd36fce7f0cdef3c4eb6cf044f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 09:38:22 -0300 Subject: [PATCH 66/97] fix: Fix mids going duplicated in chatwoot --- CHANGELOG.md | 3 ++- src/whatsapp/services/chatwoot.service.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88129b0e..cf5498ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ### Fixed -* Fixed problems in localization template +* Fixed problems in localization template in chatwoot +* Fix mids going duplicated in chatwoot # 1.4.4 (2023-07-25 15:24) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 90a0fa1a..2d470c37 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1308,7 +1308,7 @@ export class ChatwootService { this.logger.verbose('get conversation message'); const types = this.getTypeMessage(msg); -= + const messageContent = this.getMessageContent(types); this.logger.verbose('conversation message: ' + messageContent); diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index bd07f951..4c9bf62d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1147,7 +1147,7 @@ export class WAStartupService { if ( type !== 'notify' || - // received.message?.protocolMessage || + received.message?.protocolMessage || received.message?.pollUpdateMessage ) { this.logger.verbose('message rejected'); From 67e98456bb39bfc7e545c5b90bf76160f458dce6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 09:39:27 -0300 Subject: [PATCH 67/97] fix: Fix mids going duplicated in chatwoot --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8cfd9f8d..0ef463d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.4", + "version": "1.4.5", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From e151eb85f0d556d5fa21c526aba140f0b6d7947f Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 10:30:26 -0300 Subject: [PATCH 68/97] wip --- .eslintrc.js | 11 +- package.json | 13 +- src/config/env.config.ts | 2 +- src/config/logger.config.ts | 3 +- src/db/db.connect.ts | 1 + src/db/redis.client.ts | 3 +- src/main.ts | 12 +- src/utils/server-up.ts | 5 +- src/utils/use-multi-file-auth-state-db.ts | 13 +- .../use-multi-file-auth-state-redis-db.ts | 5 +- src/whatsapp/abstract/abstract.repository.ts | 1 + src/whatsapp/abstract/abstract.router.ts | 12 +- src/whatsapp/controllers/chat.controller.ts | 5 +- .../controllers/chatwoot.controller.ts | 13 +- src/whatsapp/controllers/group.controller.ts | 2 +- .../controllers/instance.controller.ts | 13 +- .../controllers/sendMessage.controller.ts | 6 +- .../controllers/settings.controller.ts | 3 +- src/whatsapp/controllers/views.controller.ts | 1 + .../controllers/webhook.controller.ts | 3 +- src/whatsapp/dto/chat.dto.ts | 2 +- src/whatsapp/guards/auth.guard.ts | 9 +- src/whatsapp/guards/instance.guard.ts | 3 +- src/whatsapp/models/auth.model.ts | 1 + src/whatsapp/models/chat.model.ts | 1 + src/whatsapp/models/chatwoot.model.ts | 1 + src/whatsapp/models/contact.model.ts | 1 + src/whatsapp/models/message.model.ts | 1 + src/whatsapp/models/settings.model.ts | 1 + src/whatsapp/models/webhook.model.ts | 1 + src/whatsapp/repository/auth.repository.ts | 11 +- src/whatsapp/repository/chat.repository.ts | 9 +- .../repository/chatwoot.repository.ts | 9 +- src/whatsapp/repository/contact.repository.ts | 5 +- src/whatsapp/repository/message.repository.ts | 9 +- .../repository/messageUp.repository.ts | 9 +- src/whatsapp/repository/repository.manager.ts | 44 +++--- .../repository/settings.repository.ts | 9 +- src/whatsapp/repository/webhook.repository.ts | 9 +- src/whatsapp/routers/chat.router.ts | 15 +- src/whatsapp/routers/chatwoot.router.ts | 15 +- src/whatsapp/routers/group.router.ts | 23 +-- src/whatsapp/routers/index.router.ts | 9 +- src/whatsapp/routers/instance.router.ts | 15 +- src/whatsapp/routers/sendMessage.router.ts | 5 +- src/whatsapp/routers/settings.router.ts | 5 +- src/whatsapp/routers/view.router.ts | 1 + src/whatsapp/routers/webhook.router.ts | 3 +- src/whatsapp/services/auth.service.ts | 19 +-- src/whatsapp/services/chatwoot.service.ts | 19 ++- src/whatsapp/services/monitor.service.ts | 27 ++-- src/whatsapp/services/settings.service.ts | 2 +- src/whatsapp/services/webhook.service.ts | 2 +- src/whatsapp/services/whatsapp.service.ts | 146 +++++++++--------- src/whatsapp/whatsapp.module.ts | 45 +++--- 55 files changed, 340 insertions(+), 273 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d3545e60..e1bcd14d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,10 +3,14 @@ module.exports = { parserOptions: { sourceType: 'CommonJS', }, - plugins: ['@typescript-eslint/eslint-plugin'], + plugins: [ + '@typescript-eslint', + 'simple-import-sort', + 'import' + ], extends: [ + 'eslint:recommended', 'plugin:@typescript-eslint/recommended', - 'plugin:prettier/recommended', 'plugin:prettier/recommended' ], globals: { @@ -27,6 +31,9 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-unused-vars': 'off', + 'import/first': 'error', + 'import/no-duplicates': 'error', + 'simple-import-sort/imports': 'error', '@typescript-eslint/ban-types': [ 'error', { diff --git a/package.json b/package.json index 1145676d..4d82309c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start": "ts-node --files --transpile-only ./src/main.ts", "start:prod": "bash start.sh", "dev:server": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts", - "test": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts" + "test": "clear && tsnd --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts", + "lint": "eslint --fix --ext .ts src" }, "repository": { "type": "git", @@ -84,12 +85,14 @@ "@types/qrcode": "^1.5.0", "@types/qrcode-terminal": "^0.12.0", "@types/uuid": "^8.3.4", - "@typescript-eslint/eslint-plugin": "^5.57.1", - "@typescript-eslint/parser": "^5.57.1", - "eslint": "^8.38.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.45.0", "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", "eslint-plugin-prettier": "^4.2.1", - "prettier": "^2.8.7", + "eslint-plugin-simple-import-sort": "^10.0.0", + "prettier": "^2.8.8", "ts-node-dev": "^2.0.0", "typescript": "^4.9.5" } diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 88b718de..0c99a12b 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -1,7 +1,7 @@ +import { isBooleanString } from 'class-validator'; import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; -import { isBooleanString } from 'class-validator'; export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index 26e4c38f..a5ca6a23 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -1,6 +1,7 @@ -import { configService, Log } from './env.config'; import dayjs from 'dayjs'; +import { configService, Log } from './env.config'; + const formatDateLog = (timestamp: number) => dayjs(timestamp) .toDate() diff --git a/src/db/db.connect.ts b/src/db/db.connect.ts index 59530b48..b11610c7 100644 --- a/src/db/db.connect.ts +++ b/src/db/db.connect.ts @@ -1,4 +1,5 @@ import mongoose from 'mongoose'; + import { configService, Database } from '../config/env.config'; import { Logger } from '../config/logger.config'; diff --git a/src/db/redis.client.ts b/src/db/redis.client.ts index 50e7efcb..a9cbfb0b 100644 --- a/src/db/redis.client.ts +++ b/src/db/redis.client.ts @@ -1,7 +1,8 @@ import { createClient, RedisClientType } from '@redis/client'; -import { Logger } from '../config/logger.config'; import { BufferJSON } from '@whiskeysockets/baileys'; + import { Redis } from '../config/env.config'; +import { Logger } from '../config/logger.config'; export class RedisCache { constructor() { diff --git a/src/main.ts b/src/main.ts index ac66e7b5..7184f921 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,18 @@ +import 'express-async-errors'; + +// import * as Sentry from '@sentry/node'; import compression from 'compression'; -import { configService, Cors, HttpServer } from './config/env.config'; import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; + +import { configService, Cors, HttpServer } from './config/env.config'; import { onUnexpectedError } from './config/error.config'; import { Logger } from './config/logger.config'; import { ROOT_DIR } from './config/path.config'; -import { waMonitor } from './whatsapp/whatsapp.module'; -import { HttpStatus, router } from './whatsapp/routers/index.router'; -import 'express-async-errors'; import { ServerUP } from './utils/server-up'; -import * as Sentry from '@sentry/node'; +import { HttpStatus, router } from './whatsapp/routers/index.router'; +import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { waMonitor.loadInstance(); diff --git a/src/utils/server-up.ts b/src/utils/server-up.ts index 9868efc0..e06caea7 100644 --- a/src/utils/server-up.ts +++ b/src/utils/server-up.ts @@ -1,8 +1,9 @@ import { Express } from 'express'; import { readFileSync } from 'fs'; -import { configService, SslConf } from '../config/env.config'; -import * as https from 'https'; import * as http from 'http'; +import * as https from 'https'; + +import { configService, SslConf } from '../config/env.config'; export class ServerUP { static #app: Express; diff --git a/src/utils/use-multi-file-auth-state-db.ts b/src/utils/use-multi-file-auth-state-db.ts index d0c518da..e26b7a7e 100644 --- a/src/utils/use-multi-file-auth-state-db.ts +++ b/src/utils/use-multi-file-auth-state-db.ts @@ -6,6 +6,7 @@ import { proto, SignalDataTypeMap, } from '@whiskeysockets/baileys'; + import { configService, Database } from '../config/env.config'; import { Logger } from '../config/logger.config'; import { dbserver } from '../db/db.connect'; @@ -29,7 +30,9 @@ export async function useMultiFileAuthStateDb( JSON.parse(JSON.stringify(data, BufferJSON.replacer)), { upsert: true }, ); - } catch {} + } catch (error) { + logger.error(error); + } }; const readData = async (key: string): Promise => { @@ -38,14 +41,18 @@ export async function useMultiFileAuthStateDb( const data = await collection.findOne({ _id: key }); const creds = JSON.stringify(data); return JSON.parse(creds, BufferJSON.reviver); - } catch {} + } catch (error) { + logger.error(error); + } }; const removeData = async (key: string) => { try { await client.connect(); return await collection.deleteOne({ _id: key }); - } catch {} + } catch (error) { + logger.error(error); + } }; const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); diff --git a/src/utils/use-multi-file-auth-state-redis-db.ts b/src/utils/use-multi-file-auth-state-redis-db.ts index c5450ddc..392d99ca 100644 --- a/src/utils/use-multi-file-auth-state-redis-db.ts +++ b/src/utils/use-multi-file-auth-state-redis-db.ts @@ -5,9 +5,10 @@ import { proto, SignalDataTypeMap, } from '@whiskeysockets/baileys'; -import { RedisCache } from '../db/redis.client'; -import { Logger } from '../config/logger.config'; + import { Redis } from '../config/env.config'; +import { Logger } from '../config/logger.config'; +import { RedisCache } from '../db/redis.client'; export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{ state: AuthenticationState; diff --git a/src/whatsapp/abstract/abstract.repository.ts b/src/whatsapp/abstract/abstract.repository.ts index a7215383..a0c4de6f 100644 --- a/src/whatsapp/abstract/abstract.repository.ts +++ b/src/whatsapp/abstract/abstract.repository.ts @@ -1,5 +1,6 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; + import { ConfigService, Database } from '../../config/env.config'; import { ROOT_DIR } from '../../config/path.config'; diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index cb224cd6..7a9dc427 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -1,11 +1,13 @@ -import { InstanceDto } from '../dto/instance.dto'; -import { JSONSchema7 } from 'json-schema'; -import { Request } from 'express'; -import { validate } from 'jsonschema'; -import { BadRequestException } from '../../exceptions'; import 'express-async-errors'; + +import { Request } from 'express'; +import { JSONSchema7 } from 'json-schema'; +import { validate } from 'jsonschema'; + import { Logger } from '../../config/logger.config'; +import { BadRequestException } from '../../exceptions'; import { GetParticipant, GroupInvite, GroupJid } from '../dto/group.dto'; +import { InstanceDto } from '../dto/instance.dto'; type DataValidate = { request: Request; diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 454ddabf..8217b908 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -1,7 +1,10 @@ import { proto } from '@whiskeysockets/baileys'; + +import { Logger } from '../../config/logger.config'; import { ArchiveChatDto, DeleteMessage, + getBase64FromMediaMessageDto, NumberDto, PrivacySettingDto, ProfileNameDto, @@ -9,14 +12,12 @@ import { ProfileStatusDto, ReadMessageDto, WhatsAppNumberDto, - getBase64FromMediaMessageDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; import { MessageQuery } from '../repository/message.repository'; import { MessageUpQuery } from '../repository/messageUp.repository'; import { WAMonitoringService } from '../services/monitor.service'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('ChatController'); diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index d5e5e841..892c6905 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -1,11 +1,12 @@ import { isURL } from 'class-validator'; -import { BadRequestException } from '../../exceptions'; -import { InstanceDto } from '../dto/instance.dto'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { ChatwootService } from '../services/chatwoot.service'; -import { Logger } from '../../config/logger.config'; -import { waMonitor } from '../whatsapp.module'; + import { ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { BadRequestException } from '../../exceptions'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootService } from '../services/chatwoot.service'; +import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index f4d381ce..56195f65 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -1,3 +1,4 @@ +import { Logger } from '../../config/logger.config'; import { CreateGroupDto, GetParticipant, @@ -13,7 +14,6 @@ import { } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; import { WAMonitoringService } from '../services/monitor.service'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('ChatController'); diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 7322e194..ab21986d 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -1,18 +1,19 @@ import { delay } from '@whiskeysockets/baileys'; +import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; + import { Auth, ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { RedisCache } from '../../db/redis.client'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.service'; -import { WAMonitoringService } from '../services/monitor.service'; -import { WAStartupService } from '../services/whatsapp.service'; -import { WebhookService } from '../services/webhook.service'; import { ChatwootService } from '../services/chatwoot.service'; -import { Logger } from '../../config/logger.config'; +import { WAMonitoringService } from '../services/monitor.service'; +import { WebhookService } from '../services/webhook.service'; +import { WAStartupService } from '../services/whatsapp.service'; import { wa } from '../types/wa.types'; -import { RedisCache } from '../../db/redis.client'; -import { isURL } from 'class-validator'; export class InstanceController { constructor( diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index fb942a9c..2f49ca50 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -1,4 +1,6 @@ import { isBase64, isURL } from 'class-validator'; + +import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { @@ -16,8 +18,6 @@ import { } from '../dto/sendMessage.dto'; import { WAMonitoringService } from '../services/monitor.service'; -import { Logger } from '../../config/logger.config'; - const logger = new Logger('MessageRouter'); export class SendMessageController { @@ -109,7 +109,7 @@ export class SendMessageController { public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { logger.verbose('requested sendReaction from ' + instanceName + ' instance'); - if (!data.reactionMessage.reaction.match(/[^\(\)\w\sà-ú"-\+]+/)) { + if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) { throw new BadRequestException('"reaction" must be an emoji'); } return await this.waMonitor.waInstances[instanceName].reactionMessage(data); diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 59031634..2c763b80 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -1,9 +1,10 @@ import { isURL } from 'class-validator'; + +import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; import { SettingsService } from '../services/settings.service'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('SettingsController'); diff --git a/src/whatsapp/controllers/views.controller.ts b/src/whatsapp/controllers/views.controller.ts index 3f54ef39..327a209b 100644 --- a/src/whatsapp/controllers/views.controller.ts +++ b/src/whatsapp/controllers/views.controller.ts @@ -1,4 +1,5 @@ import { Request, Response } from 'express'; + import { Auth, ConfigService } from '../../config/env.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index b5747b2e..281147db 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -1,9 +1,10 @@ import { isURL } from 'class-validator'; + +import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { WebhookService } from '../services/webhook.service'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('WebhookController'); diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 5af66a5e..2800c6ff 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,8 +1,8 @@ import { + proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue, - proto, } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { diff --git a/src/whatsapp/guards/auth.guard.ts b/src/whatsapp/guards/auth.guard.ts index 8607cab4..6ddc297e 100644 --- a/src/whatsapp/guards/auth.guard.ts +++ b/src/whatsapp/guards/auth.guard.ts @@ -1,12 +1,13 @@ import { isJWT } from 'class-validator'; import { NextFunction, Request, Response } from 'express'; import jwt from 'jsonwebtoken'; + +import { name } from '../../../package.json'; import { Auth, configService } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; -import { name } from '../../../package.json'; +import { ForbiddenException, UnauthorizedException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { JwtPayload } from '../services/auth.service'; -import { ForbiddenException, UnauthorizedException } from '../../exceptions'; import { repository } from '../whatsapp.module'; const logger = new Logger('GUARD'); @@ -86,7 +87,9 @@ async function apikey(req: Request, res: Response, next: NextFunction) { if (instanceKey.apikey === key) { return next(); } - } catch (error) {} + } catch (error) { + logger.error(error); + } throw new UnauthorizedException(); } diff --git a/src/whatsapp/guards/instance.guard.ts b/src/whatsapp/guards/instance.guard.ts index 1e79ff1d..964b99b6 100644 --- a/src/whatsapp/guards/instance.guard.ts +++ b/src/whatsapp/guards/instance.guard.ts @@ -1,6 +1,8 @@ import { NextFunction, Request, Response } from 'express'; import { existsSync } from 'fs'; import { join } from 'path'; + +import { configService, Database, Redis } from '../../config/env.config'; import { INSTANCE_DIR } from '../../config/path.config'; import { dbserver } from '../../db/db.connect'; import { @@ -10,7 +12,6 @@ import { } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { cache, waMonitor } from '../whatsapp.module'; -import { Database, Redis, configService } from '../../config/env.config'; async function getInstance(instanceName: string) { const db = configService.get('DATABASE'); diff --git a/src/whatsapp/models/auth.model.ts b/src/whatsapp/models/auth.model.ts index b5da800d..5c5b6a41 100644 --- a/src/whatsapp/models/auth.model.ts +++ b/src/whatsapp/models/auth.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class AuthRaw { diff --git a/src/whatsapp/models/chat.model.ts b/src/whatsapp/models/chat.model.ts index ebf7f217..20153603 100644 --- a/src/whatsapp/models/chat.model.ts +++ b/src/whatsapp/models/chat.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class ChatRaw { diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index 54d9e051..307565eb 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { diff --git a/src/whatsapp/models/contact.model.ts b/src/whatsapp/models/contact.model.ts index c15411fa..d9b51e1e 100644 --- a/src/whatsapp/models/contact.model.ts +++ b/src/whatsapp/models/contact.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class ContactRaw { diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index 4a684e53..c4f475ad 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; import { wa } from '../types/wa.types'; diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index b5eb7fe7..e70605e4 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class SettingsRaw { diff --git a/src/whatsapp/models/webhook.model.ts b/src/whatsapp/models/webhook.model.ts index 62ee38f4..fa91326c 100644 --- a/src/whatsapp/models/webhook.model.ts +++ b/src/whatsapp/models/webhook.model.ts @@ -1,4 +1,5 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class WebhookRaw { diff --git a/src/whatsapp/repository/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index 0d7e177f..9949cec3 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -1,10 +1,11 @@ -import { join } from 'path'; -import { Auth, ConfigService, Database } from '../../config/env.config'; -import { IInsert, Repository } from '../abstract/abstract.repository'; -import { IAuthModel, AuthRaw } from '../models'; import { readFileSync } from 'fs'; -import { AUTH_DIR } from '../../config/path.config'; +import { join } from 'path'; + +import { Auth, ConfigService, Database } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { AUTH_DIR } from '../../config/path.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { AuthRaw, IAuthModel } from '../models'; export class AuthRepository extends Repository { constructor( diff --git a/src/whatsapp/repository/chat.repository.ts b/src/whatsapp/repository/chat.repository.ts index 0f05760c..31e1764a 100644 --- a/src/whatsapp/repository/chat.repository.ts +++ b/src/whatsapp/repository/chat.repository.ts @@ -1,9 +1,10 @@ -import { join } from 'path'; -import { ConfigService, StoreConf } from '../../config/env.config'; -import { IInsert, Repository } from '../abstract/abstract.repository'; import { opendirSync, readFileSync, rmSync } from 'fs'; -import { ChatRaw, IChatModel } from '../models'; +import { join } from 'path'; + +import { ConfigService, StoreConf } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ChatRaw, IChatModel } from '../models'; export class ChatQuery { where: ChatRaw; diff --git a/src/whatsapp/repository/chatwoot.repository.ts b/src/whatsapp/repository/chatwoot.repository.ts index 3d24022a..bc722cd6 100644 --- a/src/whatsapp/repository/chatwoot.repository.ts +++ b/src/whatsapp/repository/chatwoot.repository.ts @@ -1,9 +1,10 @@ -import { IInsert, Repository } from '../abstract/abstract.repository'; -import { ConfigService } from '../../config/env.config'; -import { join } from 'path'; import { readFileSync } from 'fs'; -import { IChatwootModel, ChatwootRaw } from '../models'; +import { join } from 'path'; + +import { ConfigService } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ChatwootRaw, IChatwootModel } from '../models'; export class ChatwootRepository extends Repository { constructor( diff --git a/src/whatsapp/repository/contact.repository.ts b/src/whatsapp/repository/contact.repository.ts index 648b5bf4..6a16f2a1 100644 --- a/src/whatsapp/repository/contact.repository.ts +++ b/src/whatsapp/repository/contact.repository.ts @@ -1,9 +1,10 @@ import { opendirSync, readFileSync } from 'fs'; import { join } from 'path'; + import { ConfigService, StoreConf } from '../../config/env.config'; -import { ContactRaw, IContactModel } from '../models'; -import { IInsert, Repository } from '../abstract/abstract.repository'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ContactRaw, IContactModel } from '../models'; export class ContactQuery { where: ContactRaw; diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index dbfe01fc..d9ccea99 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -1,9 +1,10 @@ -import { ConfigService, StoreConf } from '../../config/env.config'; -import { join } from 'path'; -import { IMessageModel, MessageRaw } from '../models'; -import { IInsert, Repository } from '../abstract/abstract.repository'; import { opendirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +import { ConfigService, StoreConf } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { IMessageModel, MessageRaw } from '../models'; export class MessageQuery { where: MessageRaw; diff --git a/src/whatsapp/repository/messageUp.repository.ts b/src/whatsapp/repository/messageUp.repository.ts index 6a9f9cc4..fe2f623b 100644 --- a/src/whatsapp/repository/messageUp.repository.ts +++ b/src/whatsapp/repository/messageUp.repository.ts @@ -1,9 +1,10 @@ -import { ConfigService, StoreConf } from '../../config/env.config'; -import { IMessageUpModel, MessageUpdateRaw } from '../models'; -import { IInsert, Repository } from '../abstract/abstract.repository'; -import { join } from 'path'; import { opendirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +import { ConfigService, StoreConf } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { IMessageUpModel, MessageUpdateRaw } from '../models'; export class MessageUpQuery { where: MessageUpdateRaw; diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index dde636c7..eea039a9 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -1,17 +1,17 @@ -import { MessageRepository } from './message.repository'; -import { ChatRepository } from './chat.repository'; -import { ContactRepository } from './contact.repository'; -import { MessageUpRepository } from './messageUp.repository'; -import { MongoClient } from 'mongodb'; -import { WebhookRepository } from './webhook.repository'; -import { ChatwootRepository } from './chatwoot.repository'; -import { SettingsRepository } from './settings.repository'; - -import { AuthRepository } from './auth.repository'; -import { Auth, ConfigService, Database } from '../../config/env.config'; -import { join } from 'path'; import fs from 'fs'; +import { MongoClient } from 'mongodb'; +import { join } from 'path'; + +import { Auth, ConfigService, Database } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { AuthRepository } from './auth.repository'; +import { ChatRepository } from './chat.repository'; +import { ChatwootRepository } from './chatwoot.repository'; +import { ContactRepository } from './contact.repository'; +import { MessageRepository } from './message.repository'; +import { MessageUpRepository } from './messageUp.repository'; +import { SettingsRepository } from './settings.repository'; +import { WebhookRepository } from './webhook.repository'; export class RepositoryBroker { constructor( public readonly message: MessageRepository, @@ -97,17 +97,17 @@ export class RepositoryBroker { this.logger.error(error); } } else { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - - const tempDir = join(storePath, 'temp'); - - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } try { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } } catch (error) { this.logger.error(error); } diff --git a/src/whatsapp/repository/settings.repository.ts b/src/whatsapp/repository/settings.repository.ts index d253643d..704a3d7d 100644 --- a/src/whatsapp/repository/settings.repository.ts +++ b/src/whatsapp/repository/settings.repository.ts @@ -1,9 +1,10 @@ -import { IInsert, Repository } from '../abstract/abstract.repository'; -import { ConfigService } from '../../config/env.config'; -import { join } from 'path'; import { readFileSync } from 'fs'; -import { ISettingsModel, SettingsRaw } from '../models'; +import { join } from 'path'; + +import { ConfigService } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ISettingsModel, SettingsRaw } from '../models'; export class SettingsRepository extends Repository { constructor( diff --git a/src/whatsapp/repository/webhook.repository.ts b/src/whatsapp/repository/webhook.repository.ts index d9b34af1..dc6d9154 100644 --- a/src/whatsapp/repository/webhook.repository.ts +++ b/src/whatsapp/repository/webhook.repository.ts @@ -1,9 +1,10 @@ -import { IInsert, Repository } from '../abstract/abstract.repository'; -import { ConfigService } from '../../config/env.config'; -import { join } from 'path'; import { readFileSync } from 'fs'; -import { IWebhookModel, WebhookRaw } from '../models'; +import { join } from 'path'; + +import { ConfigService } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { IWebhookModel, WebhookRaw } from '../models'; export class WebhookRepository extends Repository { constructor( diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index 49e64117..ecc8ebb6 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -1,4 +1,7 @@ +import { proto } from '@whiskeysockets/baileys'; import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; import { archiveChatSchema, contactValidateSchema, @@ -13,9 +16,11 @@ import { readMessageSchema, whatsappNumberSchema, } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; import { ArchiveChatDto, DeleteMessage, + getBase64FromMediaMessageDto, NumberDto, PrivacySettingDto, ProfileNameDto, @@ -23,17 +28,13 @@ import { ProfileStatusDto, ReadMessageDto, WhatsAppNumberDto, - getBase64FromMediaMessageDto, } from '../dto/chat.dto'; +import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; import { MessageQuery } from '../repository/message.repository'; -import { chatController } from '../whatsapp.module'; -import { RouterBroker } from '../abstract/abstract.router'; -import { HttpStatus } from './index.router'; import { MessageUpQuery } from '../repository/messageUp.repository'; -import { proto } from '@whiskeysockets/baileys'; -import { InstanceDto } from '../dto/instance.dto'; -import { Logger } from '../../config/logger.config'; +import { chatController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; const logger = new Logger('ChatRouter'); diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index 3d87f137..74e47552 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -1,12 +1,13 @@ import { RequestHandler, Router } from 'express'; -import { instanceNameSchema, chatwootSchema } from '../../validate/validate.schema'; -import { RouterBroker } from '../abstract/abstract.router'; -import { InstanceDto } from '../dto/instance.dto'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { chatwootController } from '../whatsapp.module'; -import { ChatwootService } from '../services/chatwoot.service'; -import { HttpStatus } from './index.router'; + import { Logger } from '../../config/logger.config'; +import { chatwootSchema, instanceNameSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootService } from '../services/chatwoot.service'; +import { chatwootController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; const logger = new Logger('ChatwootRouter'); diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 4c1b3023..7d95ef10 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -1,34 +1,35 @@ import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; import { createGroupSchema, + getParticipantsSchema, + groupInviteSchema, groupJidSchema, - updateParticipantsSchema, - updateSettingsSchema, + groupSendInviteSchema, toggleEphemeralSchema, + updateGroupDescriptionSchema, updateGroupPictureSchema, updateGroupSubjectSchema, - updateGroupDescriptionSchema, - groupInviteSchema, - groupSendInviteSchema, - getParticipantsSchema, + updateParticipantsSchema, + updateSettingsSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { CreateGroupDto, + GetParticipant, + GroupDescriptionDto, GroupInvite, GroupJid, GroupPictureDto, + GroupSendInvite, GroupSubjectDto, - GroupDescriptionDto, + GroupToggleEphemeralDto, GroupUpdateParticipantDto, GroupUpdateSettingDto, - GroupToggleEphemeralDto, - GroupSendInvite, - GetParticipant, } from '../dto/group.dto'; import { groupController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('GroupRouter'); diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 4cf7befb..f58906b9 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -1,16 +1,17 @@ import { Router } from 'express'; +import fs from 'fs'; + import { Auth, configService } from '../../config/env.config'; -import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard'; import { authGuard } from '../guards/auth.guard'; +import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard'; import { ChatRouter } from './chat.router'; +import { ChatwootRouter } from './chatwoot.router'; import { GroupRouter } from './group.router'; import { InstanceRouter } from './instance.router'; import { MessageRouter } from './sendMessage.router'; +import { SettingsRouter } from './settings.router'; import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; -import { ChatwootRouter } from './chatwoot.router'; -import fs from 'fs'; -import { SettingsRouter } from './settings.router'; enum HttpStatus { OK = 200, diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index 850ffebd..e3dbd4a9 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -1,13 +1,14 @@ import { RequestHandler, Router } from 'express'; -import { instanceNameSchema, oldTokenSchema } from '../../validate/validate.schema'; -import { InstanceDto } from '../dto/instance.dto'; -import { instanceController } from '../whatsapp.module'; -import { RouterBroker } from '../abstract/abstract.router'; -import { HttpStatus } from './index.router'; -import { OldToken } from '../services/auth.service'; + import { Auth, ConfigService, Database } from '../../config/env.config'; -import { dbserver } from '../../db/db.connect'; import { Logger } from '../../config/logger.config'; +import { dbserver } from '../../db/db.connect'; +import { instanceNameSchema, oldTokenSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { OldToken } from '../services/auth.service'; +import { instanceController } from '../whatsapp.module'; +import { HttpStatus } from './index.router'; const logger = new Logger('InstanceRouter'); diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index f6f9c3eb..ae79d387 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -1,4 +1,6 @@ import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; import { audioMessageSchema, buttonMessageSchema, @@ -12,6 +14,7 @@ import { stickerMessageSchema, textMessageSchema, } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; import { SendAudioDto, SendButtonDto, @@ -26,9 +29,7 @@ import { SendTextDto, } from '../dto/sendMessage.dto'; import { sendMessageController } from '../whatsapp.module'; -import { RouterBroker } from '../abstract/abstract.router'; import { HttpStatus } from './index.router'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('MessageRouter'); diff --git a/src/whatsapp/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts index 3ec3df83..c45dd49b 100644 --- a/src/whatsapp/routers/settings.router.ts +++ b/src/whatsapp/routers/settings.router.ts @@ -1,12 +1,13 @@ import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; import { instanceNameSchema, settingsSchema } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; -import { settingsController } from '../whatsapp.module'; import { SettingsService } from '../services/settings.service'; +import { settingsController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('SettingsRouter'); diff --git a/src/whatsapp/routers/view.router.ts b/src/whatsapp/routers/view.router.ts index b5ddc008..c5e18129 100644 --- a/src/whatsapp/routers/view.router.ts +++ b/src/whatsapp/routers/view.router.ts @@ -1,4 +1,5 @@ import { RequestHandler, Router } from 'express'; + import { RouterBroker } from '../abstract/abstract.router'; import { viewsController } from '../whatsapp.module'; diff --git a/src/whatsapp/routers/webhook.router.ts b/src/whatsapp/routers/webhook.router.ts index c520d9d5..835d6014 100644 --- a/src/whatsapp/routers/webhook.router.ts +++ b/src/whatsapp/routers/webhook.router.ts @@ -1,11 +1,12 @@ import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../config/logger.config'; import { instanceNameSchema, webhookSchema } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { webhookController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('WebhookRouter'); diff --git a/src/whatsapp/services/auth.service.ts b/src/whatsapp/services/auth.service.ts index 88a84905..3854e148 100644 --- a/src/whatsapp/services/auth.service.ts +++ b/src/whatsapp/services/auth.service.ts @@ -1,14 +1,15 @@ -import { Auth, ConfigService, Webhook } from '../../config/env.config'; -import { InstanceDto } from '../dto/instance.dto'; -import { name as apiName } from '../../../package.json'; -import { verify, sign } from 'jsonwebtoken'; -import { Logger } from '../../config/logger.config'; -import { v4 } from 'uuid'; -import { isJWT } from 'class-validator'; -import { BadRequestException } from '../../exceptions'; import axios from 'axios'; -import { WAMonitoringService } from './monitor.service'; +import { isJWT } from 'class-validator'; +import { sign, verify } from 'jsonwebtoken'; +import { v4 } from 'uuid'; + +import { name as apiName } from '../../../package.json'; +import { Auth, ConfigService, Webhook } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { BadRequestException } from '../../exceptions'; +import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; +import { WAMonitoringService } from './monitor.service'; export type JwtPayload = { instanceName: string; diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c77162ad..11a2a78f 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,18 +1,17 @@ -import { InstanceDto } from '../dto/instance.dto'; -import path from 'path'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { WAMonitoringService } from './monitor.service'; -import { Logger } from '../../config/logger.config'; import ChatwootClient from '@figuro/chatwoot-sdk'; -import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import axios from 'axios'; import FormData from 'form-data'; -import { SendTextDto } from '../dto/sendMessage.dto'; +import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import mimeTypes from 'mime-types'; -import { SendAudioDto } from '../dto/sendMessage.dto'; -import { SendMediaDto } from '../dto/sendMessage.dto'; -import { ROOT_DIR } from '../../config/path.config'; +import path from 'path'; + import { ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { ROOT_DIR } from '../../config/path.config'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { WAMonitoringService } from './monitor.service'; export class ChatwootService { private messageCacheFile: string; diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 7ffa81e1..afeeb872 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -1,9 +1,10 @@ -import { opendirSync, readdirSync, rmSync } from 'fs'; -import { WAStartupService } from './whatsapp.service'; -import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; +import { execSync } from 'child_process'; import EventEmitter2 from 'eventemitter2'; +import { opendirSync, readdirSync, rmSync } from 'fs'; +import { Db } from 'mongodb'; +import mongoose from 'mongoose'; import { join } from 'path'; -import { Logger } from '../../config/logger.config'; + import { Auth, ConfigService, @@ -12,13 +13,11 @@ import { HttpServer, Redis, } from '../../config/env.config'; -import { RepositoryBroker } from '../repository/repository.manager'; -import { NotFoundException } from '../../exceptions'; -import { Db } from 'mongodb'; -import { RedisCache } from '../../db/redis.client'; -import { execSync } from 'child_process'; +import { Logger } from '../../config/logger.config'; +import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; import { dbserver } from '../../db/db.connect'; -import mongoose from 'mongoose'; +import { RedisCache } from '../../db/redis.client'; +import { NotFoundException } from '../../exceptions'; import { AuthModel, ChatwootModel, @@ -28,6 +27,8 @@ import { SettingsModel, WebhookModel, } from '../models'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { WAStartupService } from './whatsapp.service'; export class WAMonitoringService { constructor( @@ -183,7 +184,7 @@ export class WAMonitoringService { }); this.logger.verbose('instance files deleted: ' + name); }); - } else if (this.redis.ENABLED) { + // } else if (this.redis.ENABLED) { } else { const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); for await (const dirent of dir) { @@ -337,7 +338,9 @@ export class WAMonitoringService { try { this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); this.waInstances[instanceName] = undefined; - } catch {} + } catch (error) { + this.logger.error(error); + } try { this.logger.verbose('request cleaning up instance: ' + instanceName); diff --git a/src/whatsapp/services/settings.service.ts b/src/whatsapp/services/settings.service.ts index 9a82046a..f00138e1 100644 --- a/src/whatsapp/services/settings.service.ts +++ b/src/whatsapp/services/settings.service.ts @@ -1,7 +1,7 @@ +import { Logger } from '../../config/logger.config'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; import { WAMonitoringService } from './monitor.service'; -import { Logger } from '../../config/logger.config'; export class SettingsService { constructor(private readonly waMonitor: WAMonitoringService) {} diff --git a/src/whatsapp/services/webhook.service.ts b/src/whatsapp/services/webhook.service.ts index 2370e05b..1ffdce6f 100644 --- a/src/whatsapp/services/webhook.service.ts +++ b/src/whatsapp/services/webhook.service.ts @@ -1,7 +1,7 @@ +import { Logger } from '../../config/logger.config'; import { InstanceDto } from '../dto/instance.dto'; import { WebhookDto } from '../dto/webhook.dto'; import { WAMonitoringService } from './monitor.service'; -import { Logger } from '../../config/logger.config'; export class WebhookService { constructor(private readonly waMonitor: WAMonitoringService) {} diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 6c7c96f0..17c654da 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,9 +1,10 @@ +import ffmpegPath from '@ffmpeg-installer/ffmpeg'; +import { Boom } from '@hapi/boom'; import makeWASocket, { AnyMessageContent, BufferedEventData, BufferJSON, CacheStore, - makeCacheableSignalKeyStore, Chat, ConnectionState, Contact, @@ -12,11 +13,13 @@ import makeWASocket, { downloadMediaMessage, fetchLatestBaileysVersion, generateWAMessageFromContent, + getAggregateVotesInPollMessage, getContentType, getDevice, GroupMetadata, isJidGroup, isJidUser, + makeCacheableSignalKeyStore, MessageUpsertType, MiscMessageGenerationOptions, ParticipantAction, @@ -29,8 +32,24 @@ import makeWASocket, { WAMessage, WAMessageUpdate, WASocket, - getAggregateVotesInPollMessage, } from '@whiskeysockets/baileys'; +import axios from 'axios'; +import { exec, execSync } from 'child_process'; +import { arrayUnique, isBase64, isURL } from 'class-validator'; +import EventEmitter2 from 'eventemitter2'; +import fs, { existsSync, readFileSync } from 'fs'; +import Long from 'long'; +import NodeCache from 'node-cache'; +import { getMIMEType } from 'node-mime-types'; +import { release } from 'os'; +import { join } from 'path'; +import P from 'pino'; +import ProxyAgent from 'proxy-agent'; +import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; +import qrcodeTerminal from 'qrcode-terminal'; +import sharp from 'sharp'; +import { v4 } from 'uuid'; + import { Auth, CleanStoreConf, @@ -38,31 +57,45 @@ import { ConfigSessionPhone, Database, HttpServer, + Log, QrCode, Redis, Webhook, } from '../../config/env.config'; -import fs from 'fs'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; -import axios from 'axios'; -import { v4 } from 'uuid'; -import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; -import qrcodeTerminal from 'qrcode-terminal'; -import { Events, TypeMediaMessage, wa, MessageSubtype } from '../types/wa.types'; -import { Boom } from '@hapi/boom'; -import EventEmitter2 from 'eventemitter2'; -import { release } from 'os'; -import P from 'pino'; -import { execSync, exec } from 'child_process'; -import ffmpegPath from '@ffmpeg-installer/ffmpeg'; -import { RepositoryBroker } from '../repository/repository.manager'; -import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; -import { ContactRaw } from '../models/contact.model'; -import { ChatRaw } from '../models/chat.model'; -import { getMIMEType } from 'node-mime-types'; +import { dbserver } from '../../db/db.connect'; +import { RedisCache } from '../../db/redis.client'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '../../exceptions'; +import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; +import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; +import { + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberBusiness, + OnWhatsAppDto, + PrivacySettingDto, + ReadMessageDto, + WhatsAppNumberDto, +} from '../dto/chat.dto'; +import { + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, +} from '../dto/group.dto'; import { ContactMessage, MediaMessage, @@ -73,59 +106,26 @@ import { SendListDto, SendLocationDto, SendMediaDto, - SendReactionDto, - SendTextDto, SendPollDto, - SendStickerDto, + SendReactionDto, SendStatusDto, + SendStickerDto, + SendTextDto, StatusMessage, } from '../dto/sendMessage.dto'; -import { arrayUnique, isBase64, isURL } from 'class-validator'; -import { - ArchiveChatDto, - DeleteMessage, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - WhatsAppNumberDto, - getBase64FromMediaMessageDto, -} from '../dto/chat.dto'; -import { MessageQuery } from '../repository/message.repository'; -import { ContactQuery } from '../repository/contact.repository'; -import { - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from '../../exceptions'; -import { - CreateGroupDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, - GroupToggleEphemeralDto, - GroupSubjectDto, - GroupDescriptionDto, - GroupSendInvite, - GetParticipant, -} from '../dto/group.dto'; -import { MessageUpQuery } from '../repository/messageUp.repository'; -import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; -import Long from 'long'; -import { WebhookRaw } from '../models/webhook.model'; -import { ChatwootRaw } from '../models/chatwoot.model'; -import { dbserver } from '../../db/db.connect'; -import NodeCache from 'node-cache'; -import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; -import sharp from 'sharp'; -import { RedisCache } from '../../db/redis.client'; -import { Log } from '../../config/env.config'; -import ProxyAgent from 'proxy-agent'; -import { ChatwootService } from './chatwoot.service'; -import { waMonitor } from '../whatsapp.module'; import { SettingsRaw } from '../models'; +import { ChatRaw } from '../models/chat.model'; +import { ChatwootRaw } from '../models/chatwoot.model'; +import { ContactRaw } from '../models/contact.model'; +import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; +import { WebhookRaw } from '../models/webhook.model'; +import { ContactQuery } from '../repository/contact.repository'; +import { MessageQuery } from '../repository/message.repository'; +import { MessageUpQuery } from '../repository/messageUp.repository'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; +import { waMonitor } from '../whatsapp.module'; +import { ChatwootService } from './chatwoot.service'; export class WAStartupService { constructor( @@ -395,7 +395,7 @@ export class WAStartupService { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; const serverUrl = this.configService.get('SERVER').URL; - const we = event.replace(/[\.-]/gm, '_').toUpperCase(); + const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const expose = @@ -796,7 +796,9 @@ export class WAStartupService { ); } } - } catch (error) {} + } catch (error) { + this.logger.error(error); + } }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); } } @@ -1514,7 +1516,7 @@ export class WAStartupService { .replace(/\+/g, '') .replace(/\(/g, '') .replace(/\)/g, '') - .split(/\:/)[0] + .split(':')[0] .split('@')[0]; if (number.length >= 18) { diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 9f2fed00..30f43015 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -1,43 +1,44 @@ +import { delay } from '@whiskeysockets/baileys'; + import { Auth, configService } from '../config/env.config'; -import { Logger } from '../config/logger.config'; import { eventEmitter } from '../config/event.config'; -import { MessageRepository } from './repository/message.repository'; -import { WAMonitoringService } from './services/monitor.service'; -import { ChatRepository } from './repository/chat.repository'; -import { ContactRepository } from './repository/contact.repository'; -import { MessageUpRepository } from './repository/messageUp.repository'; +import { Logger } from '../config/logger.config'; +import { dbserver } from '../db/db.connect'; +import { RedisCache } from '../db/redis.client'; 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 { SendMessageController } from './controllers/sendMessage.controller'; -import { AuthService } from './services/auth.service'; -import { GroupController } from './controllers/group.controller'; +import { SettingsController } from './controllers/settings.controller'; import { ViewsController } from './controllers/views.controller'; -import { WebhookService } from './services/webhook.service'; import { WebhookController } from './controllers/webhook.controller'; -import { ChatwootService } from './services/chatwoot.service'; -import { ChatwootController } from './controllers/chatwoot.controller'; -import { RepositoryBroker } from './repository/repository.manager'; import { AuthModel, ChatModel, + ChatwootModel, ContactModel, MessageModel, MessageUpModel, - ChatwootModel, - WebhookModel, SettingsModel, + WebhookModel, } from './models'; -import { dbserver } from '../db/db.connect'; -import { WebhookRepository } from './repository/webhook.repository'; -import { ChatwootRepository } from './repository/chatwoot.repository'; import { AuthRepository } from './repository/auth.repository'; -import { WAStartupService } from './services/whatsapp.service'; -import { delay } from '@whiskeysockets/baileys'; -import { Events } from './types/wa.types'; -import { RedisCache } from '../db/redis.client'; +import { ChatRepository } from './repository/chat.repository'; +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 { RepositoryBroker } from './repository/repository.manager'; import { SettingsRepository } from './repository/settings.repository'; +import { WebhookRepository } from './repository/webhook.repository'; +import { AuthService } from './services/auth.service'; +import { ChatwootService } from './services/chatwoot.service'; +import { WAMonitoringService } from './services/monitor.service'; import { SettingsService } from './services/settings.service'; -import { SettingsController } from './controllers/settings.controller'; +import { WebhookService } from './services/webhook.service'; +import { WAStartupService } from './services/whatsapp.service'; +import { Events } from './types/wa.types'; const logger = new Logger('WA MODULE'); From 03f3020e9f1c426524af2814416f43dbb6f946ca Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 10:30:56 -0300 Subject: [PATCH 69/97] wip --- .eslintrc.js | 1 + src/whatsapp/models/index.ts | 6 +++--- src/whatsapp/routers/index.router.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e1bcd14d..9ee852a4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,7 @@ module.exports = { 'import/first': 'error', 'import/no-duplicates': 'error', 'simple-import-sort/imports': 'error', + 'simple-import-sort/exports': 'error', '@typescript-eslint/ban-types': [ 'error', { diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index e6c6d8b4..ee472de6 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -1,7 +1,7 @@ +export * from './auth.model'; export * from './chat.model'; +export * from './chatwoot.model'; export * from './contact.model'; export * from './message.model'; -export * from './auth.model'; -export * from './webhook.model'; -export * from './chatwoot.model'; export * from './settings.model'; +export * from './webhook.model'; diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index f58906b9..3cdcced7 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -49,4 +49,4 @@ router .use('/chatwoot', new ChatwootRouter(...guards).router) .use('/settings', new SettingsRouter(...guards).router); -export { router, HttpStatus }; +export { HttpStatus, router }; From 249aecbc0d0ba3071792df88eb5dab2df820bb16 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 10:58:13 -0300 Subject: [PATCH 70/97] wip --- .eslintrc.js | 2 +- .prettierrc.js | 11 +- src/config/env.config.ts | 410 +- src/config/error.config.ts | 28 +- src/config/event.config.ts | 6 +- src/config/logger.config.ts | 210 +- src/db/db.connect.ts | 26 +- src/db/redis.client.ts | 180 +- src/exceptions/400.exception.ts | 14 +- src/exceptions/401.exception.ts | 14 +- src/exceptions/403.exception.ts | 14 +- src/exceptions/404.exception.ts | 14 +- src/exceptions/500.exception.ts | 14 +- src/main.ts | 138 +- src/utils/server-up.ts | 34 +- src/utils/use-multi-file-auth-state-db.ts | 154 +- .../use-multi-file-auth-state-redis-db.ts | 133 +- src/validate/validate.schema.ts | 1404 ++-- src/whatsapp/abstract/abstract.repository.ts | 86 +- src/whatsapp/abstract/abstract.router.ts | 358 +- src/whatsapp/controllers/chat.controller.ts | 197 +- .../controllers/chatwoot.controller.ts | 147 +- src/whatsapp/controllers/group.controller.ts | 184 +- .../controllers/instance.controller.ts | 539 +- .../controllers/sendMessage.controller.ts | 184 +- .../controllers/settings.controller.ts | 28 +- src/whatsapp/controllers/views.controller.ts | 29 +- .../controllers/webhook.controller.ts | 34 +- src/whatsapp/dto/chat.dto.ts | 89 +- src/whatsapp/dto/chatwoot.dto.ts | 14 +- src/whatsapp/dto/group.dto.ts | 38 +- src/whatsapp/dto/instance.dto.ts | 22 +- src/whatsapp/dto/sendMessage.dto.ts | 144 +- src/whatsapp/dto/settings.dto.ts | 6 +- src/whatsapp/dto/webhook.dto.ts | 8 +- src/whatsapp/guards/auth.guard.ts | 124 +- src/whatsapp/guards/instance.guard.ts | 83 +- src/whatsapp/models/auth.model.ts | 12 +- src/whatsapp/models/chat.model.ts | 14 +- src/whatsapp/models/chatwoot.model.ts | 38 +- src/whatsapp/models/contact.model.ts | 20 +- src/whatsapp/models/message.model.ts | 94 +- src/whatsapp/models/settings.model.ts | 22 +- src/whatsapp/models/webhook.model.ts | 20 +- src/whatsapp/repository/auth.repository.ts | 109 +- src/whatsapp/repository/chat.repository.ts | 199 +- .../repository/chatwoot.repository.ts | 112 +- src/whatsapp/repository/contact.repository.ts | 332 +- src/whatsapp/repository/message.repository.ts | 265 +- .../repository/messageUp.repository.ts | 223 +- src/whatsapp/repository/repository.manager.ts | 194 +- .../repository/settings.repository.ts | 112 +- src/whatsapp/repository/webhook.repository.ts | 108 +- src/whatsapp/routers/chat.router.ts | 570 +- src/whatsapp/routers/chatwoot.router.ts | 94 +- src/whatsapp/routers/group.router.ts | 475 +- src/whatsapp/routers/index.router.ts | 46 +- src/whatsapp/routers/instance.router.ts | 316 +- src/whatsapp/routers/sendMessage.router.ts | 363 +- src/whatsapp/routers/settings.router.ts | 66 +- src/whatsapp/routers/view.router.ts | 14 +- src/whatsapp/routers/webhook.router.ts | 66 +- src/whatsapp/services/auth.service.ts | 307 +- src/whatsapp/services/chatwoot.service.ts | 3072 +++++---- src/whatsapp/services/monitor.service.ts | 651 +- src/whatsapp/services/settings.service.ts | 44 +- src/whatsapp/services/webhook.service.ts | 44 +- src/whatsapp/services/whatsapp.service.ts | 5628 ++++++++--------- src/whatsapp/types/wa.types.ts | 143 +- src/whatsapp/whatsapp.module.ts | 59 +- 70 files changed, 9184 insertions(+), 9768 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 9ee852a4..f805da92 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -30,7 +30,7 @@ module.exports = { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'error', 'import/first': 'error', 'import/no-duplicates': 'error', 'simple-import-sort/imports': 'error', diff --git a/.prettierrc.js b/.prettierrc.js index 067abea5..362adbcb 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -2,8 +2,11 @@ module.exports = { semi: true, trailingComma: 'all', singleQuote: true, - printWidth: 90, - tabWidth: 2, - bracketSameLine: true, - bracketSpacing: true + printWidth: 120, + arrowParens: 'always', + tabWidth: 4, + useTabs: false, + bracketSameLine: false, + bracketSpacing: true, + parser: 'typescript' } \ No newline at end of file diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 0c99a12b..737fff51 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -7,107 +7,99 @@ export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type Cors = { - ORIGIN: string[]; - METHODS: HttpMethods[]; - CREDENTIALS: boolean; + ORIGIN: string[]; + METHODS: HttpMethods[]; + CREDENTIALS: boolean; }; export type LogBaileys = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; -export type LogLevel = - | 'ERROR' - | 'WARN' - | 'DEBUG' - | 'INFO' - | 'LOG' - | 'VERBOSE' - | 'DARK' - | 'WEBHOOKS'; +export type LogLevel = 'ERROR' | 'WARN' | 'DEBUG' | 'INFO' | 'LOG' | 'VERBOSE' | 'DARK' | 'WEBHOOKS'; export type Log = { - LEVEL: LogLevel[]; - COLOR: boolean; - BAILEYS: LogBaileys; + LEVEL: LogLevel[]; + COLOR: boolean; + BAILEYS: LogBaileys; }; export type SaveData = { - INSTANCE: boolean; - NEW_MESSAGE: boolean; - MESSAGE_UPDATE: boolean; - CONTACTS: boolean; - CHATS: boolean; + INSTANCE: boolean; + NEW_MESSAGE: boolean; + MESSAGE_UPDATE: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type StoreConf = { - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type CleanStoreConf = { - CLEANING_INTERVAL: number; - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + CLEANING_INTERVAL: number; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type DBConnection = { - URI: string; - DB_PREFIX_NAME: string; + URI: string; + DB_PREFIX_NAME: string; }; export type Database = { - CONNECTION: DBConnection; - ENABLED: boolean; - SAVE_DATA: SaveData; + CONNECTION: DBConnection; + ENABLED: boolean; + SAVE_DATA: SaveData; }; export type Redis = { - ENABLED: boolean; - URI: string; - PREFIX_KEY: string; + ENABLED: boolean; + URI: string; + PREFIX_KEY: string; }; export type EventsWebhook = { - APPLICATION_STARTUP: boolean; - QRCODE_UPDATED: boolean; - MESSAGES_SET: boolean; - MESSAGES_UPSERT: boolean; - MESSAGES_UPDATE: boolean; - MESSAGES_DELETE: boolean; - SEND_MESSAGE: boolean; - CONTACTS_SET: boolean; - CONTACTS_UPDATE: boolean; - CONTACTS_UPSERT: boolean; - PRESENCE_UPDATE: boolean; - CHATS_SET: boolean; - CHATS_UPDATE: boolean; - CHATS_DELETE: boolean; - CHATS_UPSERT: boolean; - CONNECTION_UPDATE: boolean; - GROUPS_UPSERT: boolean; - GROUP_UPDATE: boolean; - GROUP_PARTICIPANTS_UPDATE: boolean; - NEW_JWT_TOKEN: boolean; + APPLICATION_STARTUP: boolean; + QRCODE_UPDATED: boolean; + MESSAGES_SET: boolean; + MESSAGES_UPSERT: boolean; + MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; + SEND_MESSAGE: boolean; + CONTACTS_SET: boolean; + CONTACTS_UPDATE: boolean; + CONTACTS_UPSERT: boolean; + PRESENCE_UPDATE: boolean; + CHATS_SET: boolean; + CHATS_UPDATE: boolean; + CHATS_DELETE: boolean; + CHATS_UPSERT: boolean; + CONNECTION_UPDATE: boolean; + GROUPS_UPSERT: boolean; + GROUP_UPDATE: boolean; + GROUP_PARTICIPANTS_UPDATE: boolean; + NEW_JWT_TOKEN: boolean; }; export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; export type Auth = { - API_KEY: ApiKey; - EXPOSE_IN_FETCH_INSTANCES: boolean; - JWT: Jwt; - TYPE: 'jwt' | 'apikey'; + API_KEY: ApiKey; + EXPOSE_IN_FETCH_INSTANCES: boolean; + JWT: Jwt; + TYPE: 'jwt' | 'apikey'; }; export type DelInstance = number | boolean; export type GlobalWebhook = { - URL: string; - ENABLED: boolean; - WEBHOOK_BY_EVENTS: boolean; + URL: string; + ENABLED: boolean; + WEBHOOK_BY_EVENTS: boolean; }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; @@ -116,161 +108,157 @@ export type QrCode = { LIMIT: number }; export type Production = boolean; export interface Env { - SERVER: HttpServer; - CORS: Cors; - SSL_CONF: SslConf; - STORE: StoreConf; - CLEAN_STORE: CleanStoreConf; - DATABASE: Database; - REDIS: Redis; - LOG: Log; - DEL_INSTANCE: DelInstance; - WEBHOOK: Webhook; - CONFIG_SESSION_PHONE: ConfigSessionPhone; - QRCODE: QrCode; - AUTHENTICATION: Auth; - PRODUCTION?: Production; + SERVER: HttpServer; + CORS: Cors; + SSL_CONF: SslConf; + STORE: StoreConf; + CLEAN_STORE: CleanStoreConf; + DATABASE: Database; + REDIS: Redis; + LOG: Log; + DEL_INSTANCE: DelInstance; + WEBHOOK: Webhook; + CONFIG_SESSION_PHONE: ConfigSessionPhone; + QRCODE: QrCode; + AUTHENTICATION: Auth; + PRODUCTION?: Production; } export type Key = keyof Env; export class ConfigService { - constructor() { - this.loadEnv(); - } - - private env: Env; - - public get(key: Key) { - return this.env[key] as T; - } - - private loadEnv() { - this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); - this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; - if (process.env?.DOCKER_ENV === 'true') { - this.env.SERVER.TYPE = 'http'; - this.env.SERVER.PORT = 8080; + constructor() { + this.loadEnv(); } - } - private envYaml(): Env { - return load( - readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' }), - ) as Env; - } + private env: Env; - private envProcess(): Env { - return { - SERVER: { - TYPE: process.env.SERVER_TYPE as 'http' | 'https', - PORT: Number.parseInt(process.env.SERVER_PORT), - URL: process.env.SERVER_URL, - }, - CORS: { - ORIGIN: process.env.CORS_ORIGIN.split(','), - METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], - CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', - }, - SSL_CONF: { - PRIVKEY: process.env?.SSL_CONF_PRIVKEY, - FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, - }, - STORE: { - MESSAGES: process.env?.STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.STORE_CONTACTS === 'true', - CHATS: process.env?.STORE_CHATS === 'true', - }, - CLEAN_STORE: { - CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) - ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) - : 7200, - MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', - CHATS: process.env?.CLEAN_STORE_CHATS === 'true', - }, - DATABASE: { - CONNECTION: { - URI: process.env.DATABASE_CONNECTION_URI, - DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, - }, - ENABLED: process.env?.DATABASE_ENABLED === 'true', - SAVE_DATA: { - INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', - NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', - MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', - CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', - CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', - }, - }, - REDIS: { - ENABLED: process.env?.REDIS_ENABLED === 'true', - URI: process.env.REDIS_URI, - PREFIX_KEY: process.env.REDIS_PREFIX_KEY, - }, - LOG: { - LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], - COLOR: process.env?.LOG_COLOR === 'true', - BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', - }, - DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) - ? process.env.DEL_INSTANCE === 'true' - : Number.parseInt(process.env.DEL_INSTANCE) || false, - WEBHOOK: { - GLOBAL: { - URL: process.env?.WEBHOOK_GLOBAL_URL, - ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', - WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', - }, - EVENTS: { - APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', - QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', - MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', - MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', - MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', - MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', - SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', - CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', - CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', - CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', - PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', - CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', - CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', - CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', - CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', - CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', - GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', - GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', - GROUP_PARTICIPANTS_UPDATE: - process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', - NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', - }, - }, - CONFIG_SESSION_PHONE: { - CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', - NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', - }, - QRCODE: { - LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, - }, - AUTHENTICATION: { - TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', - API_KEY: { - KEY: process.env.AUTHENTICATION_API_KEY, - }, - EXPOSE_IN_FETCH_INSTANCES: - process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', - JWT: { - EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) - ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) - : 3600, - SECRET: process.env.AUTHENTICATION_JWT_SECRET, - }, - }, - }; - } + public get(key: Key) { + return this.env[key] as T; + } + + private loadEnv() { + this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); + this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; + if (process.env?.DOCKER_ENV === 'true') { + this.env.SERVER.TYPE = 'http'; + this.env.SERVER.PORT = 8080; + } + } + + private envYaml(): Env { + return load(readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' })) as Env; + } + + private envProcess(): Env { + return { + SERVER: { + TYPE: process.env.SERVER_TYPE as 'http' | 'https', + PORT: Number.parseInt(process.env.SERVER_PORT), + URL: process.env.SERVER_URL, + }, + CORS: { + ORIGIN: process.env.CORS_ORIGIN.split(','), + METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], + CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', + }, + SSL_CONF: { + PRIVKEY: process.env?.SSL_CONF_PRIVKEY, + FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, + }, + STORE: { + MESSAGES: process.env?.STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.STORE_CONTACTS === 'true', + CHATS: process.env?.STORE_CHATS === 'true', + }, + CLEAN_STORE: { + CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) + ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) + : 7200, + MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', + CHATS: process.env?.CLEAN_STORE_CHATS === 'true', + }, + DATABASE: { + CONNECTION: { + URI: process.env.DATABASE_CONNECTION_URI, + DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, + }, + ENABLED: process.env?.DATABASE_ENABLED === 'true', + SAVE_DATA: { + INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', + NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', + MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', + CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', + CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', + }, + }, + REDIS: { + ENABLED: process.env?.REDIS_ENABLED === 'true', + URI: process.env.REDIS_URI, + PREFIX_KEY: process.env.REDIS_PREFIX_KEY, + }, + LOG: { + LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], + COLOR: process.env?.LOG_COLOR === 'true', + BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', + }, + DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) + ? process.env.DEL_INSTANCE === 'true' + : Number.parseInt(process.env.DEL_INSTANCE) || false, + WEBHOOK: { + GLOBAL: { + URL: process.env?.WEBHOOK_GLOBAL_URL, + ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', + WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', + }, + EVENTS: { + APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', + QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', + MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', + MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', + MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', + MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', + SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', + CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', + CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', + CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', + PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', + CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', + CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', + CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', + CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', + CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', + GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', + GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', + GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', + }, + }, + CONFIG_SESSION_PHONE: { + CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', + NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', + }, + QRCODE: { + LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, + }, + AUTHENTICATION: { + TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', + API_KEY: { + KEY: process.env.AUTHENTICATION_API_KEY, + }, + EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', + JWT: { + EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) + ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) + : 3600, + SECRET: process.env.AUTHENTICATION_JWT_SECRET, + }, + }, + }; + } } export const configService = new ConfigService(); diff --git a/src/config/error.config.ts b/src/config/error.config.ts index 6449d52e..999205c4 100644 --- a/src/config/error.config.ts +++ b/src/config/error.config.ts @@ -1,21 +1,21 @@ import { Logger } from './logger.config'; export function onUnexpectedError() { - process.on('uncaughtException', (error, origin) => { - const logger = new Logger('uncaughtException'); - logger.error({ - origin, - stderr: process.stderr.fd, - error, + process.on('uncaughtException', (error, origin) => { + const logger = new Logger('uncaughtException'); + logger.error({ + origin, + stderr: process.stderr.fd, + error, + }); }); - }); - process.on('unhandledRejection', (error, origin) => { - const logger = new Logger('unhandledRejection'); - logger.error({ - origin, - stderr: process.stderr.fd, - error, + process.on('unhandledRejection', (error, origin) => { + const logger = new Logger('unhandledRejection'); + logger.error({ + origin, + stderr: process.stderr.fd, + error, + }); }); - }); } diff --git a/src/config/event.config.ts b/src/config/event.config.ts index 8451ffdf..ec917110 100644 --- a/src/config/event.config.ts +++ b/src/config/event.config.ts @@ -1,7 +1,7 @@ import EventEmitter2 from 'eventemitter2'; export const eventEmitter = new EventEmitter2({ - delimiter: '.', - newListener: false, - ignoreErrors: false, + delimiter: '.', + newListener: false, + ignoreErrors: false, }); diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index a5ca6a23..9fe6bac2 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -3,135 +3,135 @@ import dayjs from 'dayjs'; import { configService, Log } from './env.config'; const formatDateLog = (timestamp: number) => - dayjs(timestamp) - .toDate() - .toString() - .replace(/\sGMT.+/, ''); + dayjs(timestamp) + .toDate() + .toString() + .replace(/\sGMT.+/, ''); enum Color { - LOG = '\x1b[32m', - INFO = '\x1b[34m', - WARN = '\x1b[33m', - ERROR = '\x1b[31m', - DEBUG = '\x1b[36m', - VERBOSE = '\x1b[37m', - DARK = '\x1b[30m', + LOG = '\x1b[32m', + INFO = '\x1b[34m', + WARN = '\x1b[33m', + ERROR = '\x1b[31m', + DEBUG = '\x1b[36m', + VERBOSE = '\x1b[37m', + DARK = '\x1b[30m', } enum Command { - RESET = '\x1b[0m', - BRIGHT = '\x1b[1m', - UNDERSCORE = '\x1b[4m', + RESET = '\x1b[0m', + BRIGHT = '\x1b[1m', + UNDERSCORE = '\x1b[4m', } enum Level { - LOG = Color.LOG + '%s' + Command.RESET, - DARK = Color.DARK + '%s' + Command.RESET, - INFO = Color.INFO + '%s' + Command.RESET, - WARN = Color.WARN + '%s' + Command.RESET, - ERROR = Color.ERROR + '%s' + Command.RESET, - DEBUG = Color.DEBUG + '%s' + Command.RESET, - VERBOSE = Color.VERBOSE + '%s' + Command.RESET, + LOG = Color.LOG + '%s' + Command.RESET, + DARK = Color.DARK + '%s' + Command.RESET, + INFO = Color.INFO + '%s' + Command.RESET, + WARN = Color.WARN + '%s' + Command.RESET, + ERROR = Color.ERROR + '%s' + Command.RESET, + DEBUG = Color.DEBUG + '%s' + Command.RESET, + VERBOSE = Color.VERBOSE + '%s' + Command.RESET, } enum Type { - LOG = 'LOG', - WARN = 'WARN', - INFO = 'INFO', - DARK = 'DARK', - ERROR = 'ERROR', - DEBUG = 'DEBUG', - VERBOSE = 'VERBOSE', + LOG = 'LOG', + WARN = 'WARN', + INFO = 'INFO', + DARK = 'DARK', + ERROR = 'ERROR', + DEBUG = 'DEBUG', + VERBOSE = 'VERBOSE', } enum Background { - LOG = '\x1b[42m', - INFO = '\x1b[44m', - WARN = '\x1b[43m', - DARK = '\x1b[40m', - ERROR = '\x1b[41m', - DEBUG = '\x1b[46m', - VERBOSE = '\x1b[47m', + LOG = '\x1b[42m', + INFO = '\x1b[44m', + WARN = '\x1b[43m', + DARK = '\x1b[40m', + ERROR = '\x1b[41m', + DEBUG = '\x1b[46m', + VERBOSE = '\x1b[47m', } export class Logger { - private readonly configService = configService; - constructor(private context = 'Logger') {} + private readonly configService = configService; + constructor(private context = 'Logger') {} - public setContext(value: string) { - this.context = value; - } - - private console(value: any, type: Type) { - const types: Type[] = []; - - this.configService.get('LOG').LEVEL.forEach((level) => types.push(Type[level])); - - const typeValue = typeof value; - if (types.includes(type)) { - if (configService.get('LOG').COLOR) { - console.log( - /*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type], - '[Evolution API]', - Command.BRIGHT + Color[type], - process.pid.toString(), - Command.RESET, - Command.BRIGHT + Color[type], - '-', - Command.BRIGHT + Color.VERBOSE, - `${formatDateLog(Date.now())} `, - Command.RESET, - Color[type] + Background[type] + Command.BRIGHT, - `${type} ` + Command.RESET, - Color.WARN + Command.BRIGHT, - `[${this.context}]` + Command.RESET, - Color[type] + Command.BRIGHT, - `[${typeValue}]` + Command.RESET, - Color[type], - typeValue !== 'object' ? value : '', - Command.RESET, - ); - typeValue === 'object' ? console.log(/*Level.DARK,*/ value, '\n') : ''; - } else { - console.log( - '[Evolution API]', - process.pid.toString(), - '-', - `${formatDateLog(Date.now())} `, - `${type} `, - `[${this.context}]`, - `[${typeValue}]`, - value, - ); - } + public setContext(value: string) { + this.context = value; } - } - public log(value: any) { - this.console(value, Type.LOG); - } + private console(value: any, type: Type) { + const types: Type[] = []; - public info(value: any) { - this.console(value, Type.INFO); - } + this.configService.get('LOG').LEVEL.forEach((level) => types.push(Type[level])); - public warn(value: any) { - this.console(value, Type.WARN); - } + const typeValue = typeof value; + if (types.includes(type)) { + if (configService.get('LOG').COLOR) { + console.log( + /*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type], + '[Evolution API]', + Command.BRIGHT + Color[type], + process.pid.toString(), + Command.RESET, + Command.BRIGHT + Color[type], + '-', + Command.BRIGHT + Color.VERBOSE, + `${formatDateLog(Date.now())} `, + Command.RESET, + Color[type] + Background[type] + Command.BRIGHT, + `${type} ` + Command.RESET, + Color.WARN + Command.BRIGHT, + `[${this.context}]` + Command.RESET, + Color[type] + Command.BRIGHT, + `[${typeValue}]` + Command.RESET, + Color[type], + typeValue !== 'object' ? value : '', + Command.RESET, + ); + typeValue === 'object' ? console.log(/*Level.DARK,*/ value, '\n') : ''; + } else { + console.log( + '[Evolution API]', + process.pid.toString(), + '-', + `${formatDateLog(Date.now())} `, + `${type} `, + `[${this.context}]`, + `[${typeValue}]`, + value, + ); + } + } + } - public error(value: any) { - this.console(value, Type.ERROR); - } + public log(value: any) { + this.console(value, Type.LOG); + } - public verbose(value: any) { - this.console(value, Type.VERBOSE); - } + public info(value: any) { + this.console(value, Type.INFO); + } - public debug(value: any) { - this.console(value, Type.DEBUG); - } + public warn(value: any) { + this.console(value, Type.WARN); + } - public dark(value: any) { - this.console(value, Type.DARK); - } + public error(value: any) { + this.console(value, Type.ERROR); + } + + public verbose(value: any) { + this.console(value, Type.VERBOSE); + } + + public debug(value: any) { + this.console(value, Type.DEBUG); + } + + public dark(value: any) { + this.console(value, Type.DARK); + } } diff --git a/src/db/db.connect.ts b/src/db/db.connect.ts index b11610c7..e7e965ac 100644 --- a/src/db/db.connect.ts +++ b/src/db/db.connect.ts @@ -7,19 +7,19 @@ const logger = new Logger('MongoDB'); const db = configService.get('DATABASE'); export const dbserver = (() => { - if (db.ENABLED) { - logger.verbose('connecting'); - const dbs = mongoose.createConnection(db.CONNECTION.URI, { - dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api', - }); - logger.verbose('connected in ' + db.CONNECTION.URI); - logger.info('ON - dbName: ' + dbs['$dbName']); + if (db.ENABLED) { + logger.verbose('connecting'); + const dbs = mongoose.createConnection(db.CONNECTION.URI, { + dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api', + }); + logger.verbose('connected in ' + db.CONNECTION.URI); + logger.info('ON - dbName: ' + dbs['$dbName']); - process.on('beforeExit', () => { - logger.verbose('instance destroyed'); - dbserver.destroy(true, (error) => logger.error(error)); - }); + process.on('beforeExit', () => { + logger.verbose('instance destroyed'); + dbserver.destroy(true, (error) => logger.error(error)); + }); - return dbs; - } + return dbs; + } })(); diff --git a/src/db/redis.client.ts b/src/db/redis.client.ts index a9cbfb0b..5f0604bc 100644 --- a/src/db/redis.client.ts +++ b/src/db/redis.client.ts @@ -5,113 +5,101 @@ import { Redis } from '../config/env.config'; import { Logger } from '../config/logger.config'; export class RedisCache { - constructor() { - this.logger.verbose('instance created'); - process.on('beforeExit', async () => { - this.logger.verbose('instance destroyed'); - if (this.statusConnection) { - this.logger.verbose('instance disconnect'); - await this.client.disconnect(); - } - }); - } - - private statusConnection = false; - private instanceName: string; - private redisEnv: Redis; - - public set reference(reference: string) { - this.logger.verbose('set reference: ' + reference); - this.instanceName = reference; - } - - public async connect(redisEnv: Redis) { - this.logger.verbose('connecting'); - this.client = createClient({ url: redisEnv.URI }); - this.logger.verbose('connected in ' + redisEnv.URI); - await this.client.connect(); - this.statusConnection = true; - this.redisEnv = redisEnv; - } - - private readonly logger = new Logger(RedisCache.name); - private client: RedisClientType; - - public async instanceKeys(): Promise { - try { - this.logger.verbose('instance keys: ' + this.redisEnv.PREFIX_KEY + ':*'); - return await this.client.sendCommand(['keys', this.redisEnv.PREFIX_KEY + ':*']); - } catch (error) { - this.logger.error(error); + constructor() { + this.logger.verbose('instance created'); + process.on('beforeExit', async () => { + this.logger.verbose('instance destroyed'); + if (this.statusConnection) { + this.logger.verbose('instance disconnect'); + await this.client.disconnect(); + } + }); } - } - public async keyExists(key?: string) { - if (key) { - this.logger.verbose('keyExists: ' + key); - return !!(await this.instanceKeys()).find((i) => i === key); + private statusConnection = false; + private instanceName: string; + private redisEnv: Redis; + + public set reference(reference: string) { + this.logger.verbose('set reference: ' + reference); + this.instanceName = reference; } - this.logger.verbose('keyExists: ' + this.instanceName); - return !!(await this.instanceKeys()).find((i) => i === this.instanceName); - } - public async writeData(field: string, data: any) { - try { - this.logger.verbose('writeData: ' + field); - const json = JSON.stringify(data, BufferJSON.replacer); - - return await this.client.hSet( - this.redisEnv.PREFIX_KEY + ':' + this.instanceName, - field, - json, - ); - } catch (error) { - this.logger.error(error); + public async connect(redisEnv: Redis) { + this.logger.verbose('connecting'); + this.client = createClient({ url: redisEnv.URI }); + this.logger.verbose('connected in ' + redisEnv.URI); + await this.client.connect(); + this.statusConnection = true; + this.redisEnv = redisEnv; } - } - public async readData(field: string) { - try { - this.logger.verbose('readData: ' + field); - const data = await this.client.hGet( - this.redisEnv.PREFIX_KEY + ':' + this.instanceName, - field, - ); + private readonly logger = new Logger(RedisCache.name); + private client: RedisClientType; - if (data) { - this.logger.verbose('readData: ' + field + ' success'); - return JSON.parse(data, BufferJSON.reviver); - } - - this.logger.verbose('readData: ' + field + ' not found'); - return null; - } catch (error) { - this.logger.error(error); + public async instanceKeys(): Promise { + try { + this.logger.verbose('instance keys: ' + this.redisEnv.PREFIX_KEY + ':*'); + return await this.client.sendCommand(['keys', this.redisEnv.PREFIX_KEY + ':*']); + } catch (error) { + this.logger.error(error); + } } - } - public async removeData(field: string) { - try { - this.logger.verbose('removeData: ' + field); - return await this.client.hDel( - this.redisEnv.PREFIX_KEY + ':' + this.instanceName, - field, - ); - } catch (error) { - this.logger.error(error); + public async keyExists(key?: string) { + if (key) { + this.logger.verbose('keyExists: ' + key); + return !!(await this.instanceKeys()).find((i) => i === key); + } + this.logger.verbose('keyExists: ' + this.instanceName); + return !!(await this.instanceKeys()).find((i) => i === this.instanceName); } - } - public async delAll(hash?: string) { - try { - this.logger.verbose('instance delAll: ' + hash); - const result = await this.client.del( - hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName, - ); + public async writeData(field: string, data: any) { + try { + this.logger.verbose('writeData: ' + field); + const json = JSON.stringify(data, BufferJSON.replacer); - return result; - } catch (error) { - this.logger.error(error); + return await this.client.hSet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field, json); + } catch (error) { + this.logger.error(error); + } + } + + public async readData(field: string) { + try { + this.logger.verbose('readData: ' + field); + const data = await this.client.hGet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); + + if (data) { + this.logger.verbose('readData: ' + field + ' success'); + return JSON.parse(data, BufferJSON.reviver); + } + + this.logger.verbose('readData: ' + field + ' not found'); + return null; + } catch (error) { + this.logger.error(error); + } + } + + public async removeData(field: string) { + try { + this.logger.verbose('removeData: ' + field); + return await this.client.hDel(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); + } catch (error) { + this.logger.error(error); + } + } + + public async delAll(hash?: string) { + try { + this.logger.verbose('instance delAll: ' + hash); + const result = await this.client.del(hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName); + + return result; + } catch (error) { + this.logger.error(error); + } } - } } diff --git a/src/exceptions/400.exception.ts b/src/exceptions/400.exception.ts index 833295c1..8b5c9c16 100644 --- a/src/exceptions/400.exception.ts +++ b/src/exceptions/400.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class BadRequestException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.BAD_REQUEST, - error: 'Bad Request', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.BAD_REQUEST, + error: 'Bad Request', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/401.exception.ts b/src/exceptions/401.exception.ts index 72734d4e..97724396 100644 --- a/src/exceptions/401.exception.ts +++ b/src/exceptions/401.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class UnauthorizedException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.UNAUTHORIZED, - error: 'Unauthorized', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.UNAUTHORIZED, + error: 'Unauthorized', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/403.exception.ts b/src/exceptions/403.exception.ts index f53ca9a5..9670fe25 100644 --- a/src/exceptions/403.exception.ts +++ b/src/exceptions/403.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class ForbiddenException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.FORBIDDEN, - error: 'Forbidden', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.FORBIDDEN, + error: 'Forbidden', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/404.exception.ts b/src/exceptions/404.exception.ts index 1119d1a1..44181fe5 100644 --- a/src/exceptions/404.exception.ts +++ b/src/exceptions/404.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class NotFoundException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.NOT_FOUND, - error: 'Not Found', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.NOT_FOUND, + error: 'Not Found', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/500.exception.ts b/src/exceptions/500.exception.ts index 2a41dfa5..88d70573 100644 --- a/src/exceptions/500.exception.ts +++ b/src/exceptions/500.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class InternalServerErrorException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.INTERNAL_SERVER_ERROR, - error: 'Internal Server Error', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Internal Server Error', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/main.ts b/src/main.ts index 7184f921..5d70661e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,96 +15,94 @@ import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { - waMonitor.loadInstance(); + waMonitor.loadInstance(); } function bootstrap() { - const logger = new Logger('SERVER'); - const app = express(); + const logger = new Logger('SERVER'); + const app = express(); - // Sentry.init({ - // dsn: '', - // integrations: [ - // // enable HTTP calls tracing - // new Sentry.Integrations.Http({ tracing: true }), - // // enable Express.js middleware tracing - // new Sentry.Integrations.Express({ app }), - // // Automatically instrument Node.js libraries and frameworks - // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), - // ], + // Sentry.init({ + // dsn: '', + // integrations: [ + // // enable HTTP calls tracing + // new Sentry.Integrations.Http({ tracing: true }), + // // enable Express.js middleware tracing + // new Sentry.Integrations.Express({ app }), + // // Automatically instrument Node.js libraries and frameworks + // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + // ], - // // Set tracesSampleRate to 1.0 to capture 100% - // // of transactions for performance monitoring. - // // We recommend adjusting this value in production - // tracesSampleRate: 1.0, - // }); + // // Set tracesSampleRate to 1.0 to capture 100% + // // of transactions for performance monitoring. + // // We recommend adjusting this value in production + // tracesSampleRate: 1.0, + // }); - // app.use(Sentry.Handlers.requestHandler()); + // app.use(Sentry.Handlers.requestHandler()); - // app.use(Sentry.Handlers.tracingHandler()); + // app.use(Sentry.Handlers.tracingHandler()); - app.use( - cors({ - origin(requestOrigin, callback) { - const { ORIGIN } = configService.get('CORS'); - !requestOrigin ? (requestOrigin = '*') : undefined; - if (ORIGIN.indexOf(requestOrigin) !== -1) { - return callback(null, true); - } - return callback(new Error('Not allowed by CORS')); - }, - methods: [...configService.get('CORS').METHODS], - credentials: configService.get('CORS').CREDENTIALS, - }), - urlencoded({ extended: true, limit: '136mb' }), - json({ limit: '136mb' }), - compression(), - ); + app.use( + cors({ + origin(requestOrigin, callback) { + const { ORIGIN } = configService.get('CORS'); + !requestOrigin ? (requestOrigin = '*') : undefined; + if (ORIGIN.indexOf(requestOrigin) !== -1) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, + methods: [...configService.get('CORS').METHODS], + credentials: configService.get('CORS').CREDENTIALS, + }), + urlencoded({ extended: true, limit: '136mb' }), + json({ limit: '136mb' }), + compression(), + ); - app.set('view engine', 'hbs'); - app.set('views', join(ROOT_DIR, 'views')); - app.use(express.static(join(ROOT_DIR, 'public'))); + app.set('view engine', 'hbs'); + app.set('views', join(ROOT_DIR, 'views')); + app.use(express.static(join(ROOT_DIR, 'public'))); - app.use('/', router); + app.use('/', router); - // app.use(Sentry.Handlers.errorHandler()); + // app.use(Sentry.Handlers.errorHandler()); - // app.use(function onError(err, req, res, next) { - // res.statusCode = 500; - // res.end(res.sentry + '\n'); - // }); + // app.use(function onError(err, req, res, next) { + // res.statusCode = 500; + // res.end(res.sentry + '\n'); + // }); - app.use( - (err: Error, req: Request, res: Response, next: NextFunction) => { - if (err) { - return res.status(err['status'] || 500).json(err); - } - }, - (req: Request, res: Response, next: NextFunction) => { - const { method, url } = req; + app.use( + (err: Error, req: Request, res: Response) => { + if (err) { + return res.status(err['status'] || 500).json(err); + } + }, + (req: Request, res: Response, next: NextFunction) => { + const { method, url } = req; - res.status(HttpStatus.NOT_FOUND).json({ - status: HttpStatus.NOT_FOUND, - message: `Cannot ${method.toUpperCase()} ${url}`, - error: 'Not Found', - }); + res.status(HttpStatus.NOT_FOUND).json({ + status: HttpStatus.NOT_FOUND, + message: `Cannot ${method.toUpperCase()} ${url}`, + error: 'Not Found', + }); - next(); - }, - ); + next(); + }, + ); - const httpServer = configService.get('SERVER'); + const httpServer = configService.get('SERVER'); - ServerUP.app = app; - const server = ServerUP[httpServer.TYPE]; + ServerUP.app = app; + const server = ServerUP[httpServer.TYPE]; - server.listen(httpServer.PORT, () => - logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT), - ); + server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); - initWA(); + initWA(); - onUnexpectedError(); + onUnexpectedError(); } bootstrap(); diff --git a/src/utils/server-up.ts b/src/utils/server-up.ts index e06caea7..a6205249 100644 --- a/src/utils/server-up.ts +++ b/src/utils/server-up.ts @@ -6,24 +6,24 @@ import * as https from 'https'; import { configService, SslConf } from '../config/env.config'; export class ServerUP { - static #app: Express; + static #app: Express; - static set app(e: Express) { - this.#app = e; - } + static set app(e: Express) { + this.#app = e; + } - static get https() { - const { FULLCHAIN, PRIVKEY } = configService.get('SSL_CONF'); - return https.createServer( - { - cert: readFileSync(FULLCHAIN), - key: readFileSync(PRIVKEY), - }, - ServerUP.#app, - ); - } + static get https() { + const { FULLCHAIN, PRIVKEY } = configService.get('SSL_CONF'); + return https.createServer( + { + cert: readFileSync(FULLCHAIN), + key: readFileSync(PRIVKEY), + }, + ServerUP.#app, + ); + } - static get http() { - return http.createServer(ServerUP.#app); - } + static get http() { + return http.createServer(ServerUP.#app); + } } diff --git a/src/utils/use-multi-file-auth-state-db.ts b/src/utils/use-multi-file-auth-state-db.ts index e26b7a7e..a372fbcf 100644 --- a/src/utils/use-multi-file-auth-state-db.ts +++ b/src/utils/use-multi-file-auth-state-db.ts @@ -1,10 +1,10 @@ import { - AuthenticationCreds, - AuthenticationState, - BufferJSON, - initAuthCreds, - proto, - SignalDataTypeMap, + AuthenticationCreds, + AuthenticationState, + BufferJSON, + initAuthCreds, + proto, + SignalDataTypeMap, } from '@whiskeysockets/baileys'; import { configService, Database } from '../config/env.config'; @@ -12,88 +12,86 @@ import { Logger } from '../config/logger.config'; import { dbserver } from '../db/db.connect'; export async function useMultiFileAuthStateDb( - coll: string, + coll: string, ): Promise<{ state: AuthenticationState; saveCreds: () => Promise }> { - const logger = new Logger(useMultiFileAuthStateDb.name); + const logger = new Logger(useMultiFileAuthStateDb.name); - const client = dbserver.getClient(); + const client = dbserver.getClient(); - const collection = client - .db(configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(coll); + const collection = client + .db(configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(coll); - const writeData = async (data: any, key: string): Promise => { - try { - await client.connect(); - return await collection.replaceOne( - { _id: key }, - JSON.parse(JSON.stringify(data, BufferJSON.replacer)), - { upsert: true }, - ); - } catch (error) { - logger.error(error); - } - }; + const writeData = async (data: any, key: string): Promise => { + try { + await client.connect(); + return await collection.replaceOne({ _id: key }, JSON.parse(JSON.stringify(data, BufferJSON.replacer)), { + upsert: true, + }); + } catch (error) { + logger.error(error); + } + }; - const readData = async (key: string): Promise => { - try { - await client.connect(); - const data = await collection.findOne({ _id: key }); - const creds = JSON.stringify(data); - return JSON.parse(creds, BufferJSON.reviver); - } catch (error) { - logger.error(error); - } - }; + const readData = async (key: string): Promise => { + try { + await client.connect(); + const data = await collection.findOne({ _id: key }); + const creds = JSON.stringify(data); + return JSON.parse(creds, BufferJSON.reviver); + } catch (error) { + logger.error(error); + } + }; - const removeData = async (key: string) => { - try { - await client.connect(); - return await collection.deleteOne({ _id: key }); - } catch (error) { - logger.error(error); - } - }; + const removeData = async (key: string) => { + try { + await client.connect(); + return await collection.deleteOne({ _id: key }); + } catch (error) { + logger.error(error); + } + }; - const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); + const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); - return { - state: { - creds, - keys: { - get: async (type, ids: string[]) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const data: { [_: string]: SignalDataTypeMap[type] } = {}; - await Promise.all( - ids.map(async (id) => { - let value = await readData(`${type}-${id}`); - if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.fromObject(value); - } + return { + state: { + creds, + keys: { + get: async (type, ids: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: { [_: string]: SignalDataTypeMap[type] } = {}; + await Promise.all( + ids.map(async (id) => { + let value = await readData(`${type}-${id}`); + if (type === 'app-state-sync-key' && value) { + value = proto.Message.AppStateSyncKeyData.fromObject(value); + } - data[id] = value; - }), - ); + data[id] = value; + }), + ); - return data; + return data; + }, + set: async (data: any) => { + const tasks: Promise[] = []; + for (const category in data) { + for (const id in data[category]) { + const value = data[category][id]; + const key = `${category}-${id}`; + tasks.push(value ? writeData(value, key) : removeData(key)); + } + } + + await Promise.all(tasks); + }, + }, }, - set: async (data: any) => { - const tasks: Promise[] = []; - for (const category in data) { - for (const id in data[category]) { - const value = data[category][id]; - const key = `${category}-${id}`; - tasks.push(value ? writeData(value, key) : removeData(key)); - } - } - - await Promise.all(tasks); + saveCreds: async () => { + return writeData(creds, 'creds'); }, - }, - }, - saveCreds: async () => { - return writeData(creds, 'creds'); - }, - }; + }; } diff --git a/src/utils/use-multi-file-auth-state-redis-db.ts b/src/utils/use-multi-file-auth-state-redis-db.ts index 392d99ca..f26d7d02 100644 --- a/src/utils/use-multi-file-auth-state-redis-db.ts +++ b/src/utils/use-multi-file-auth-state-redis-db.ts @@ -1,85 +1,84 @@ import { - AuthenticationCreds, - AuthenticationState, - initAuthCreds, - proto, - SignalDataTypeMap, + AuthenticationCreds, + AuthenticationState, + initAuthCreds, + proto, + SignalDataTypeMap, } from '@whiskeysockets/baileys'; -import { Redis } from '../config/env.config'; import { Logger } from '../config/logger.config'; import { RedisCache } from '../db/redis.client'; export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{ - state: AuthenticationState; - saveCreds: () => Promise; + state: AuthenticationState; + saveCreds: () => Promise; }> { - const logger = new Logger(useMultiFileAuthStateRedisDb.name); + const logger = new Logger(useMultiFileAuthStateRedisDb.name); - const writeData = async (data: any, key: string): Promise => { - try { - return await cache.writeData(key, data); - } catch (error) { - return logger.error({ localError: 'writeData', error }); - } - }; + const writeData = async (data: any, key: string): Promise => { + try { + return await cache.writeData(key, data); + } catch (error) { + return logger.error({ localError: 'writeData', error }); + } + }; - const readData = async (key: string): Promise => { - try { - return await cache.readData(key); - } catch (error) { - logger.error({ readData: 'writeData', error }); - return; - } - }; + const readData = async (key: string): Promise => { + try { + return await cache.readData(key); + } catch (error) { + logger.error({ readData: 'writeData', error }); + return; + } + }; - const removeData = async (key: string) => { - try { - return await cache.removeData(key); - } catch (error) { - logger.error({ readData: 'removeData', error }); - } - }; + const removeData = async (key: string) => { + try { + return await cache.removeData(key); + } catch (error) { + logger.error({ readData: 'removeData', error }); + } + }; - const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); + const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); - return { - state: { - creds, - keys: { - get: async (type, ids: string[]) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const data: { [_: string]: SignalDataTypeMap[type] } = {}; - await Promise.all( - ids.map(async (id) => { - let value = await readData(`${type}-${id}`); - if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.fromObject(value); - } + return { + state: { + creds, + keys: { + get: async (type, ids: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: { [_: string]: SignalDataTypeMap[type] } = {}; + await Promise.all( + ids.map(async (id) => { + let value = await readData(`${type}-${id}`); + if (type === 'app-state-sync-key' && value) { + value = proto.Message.AppStateSyncKeyData.fromObject(value); + } - data[id] = value; - }), - ); + data[id] = value; + }), + ); - return data; + return data; + }, + set: async (data: any) => { + const tasks: Promise[] = []; + for (const category in data) { + for (const id in data[category]) { + const value = data[category][id]; + const key = `${category}-${id}`; + tasks.push(value ? await writeData(value, key) : await removeData(key)); + } + } + + await Promise.all(tasks); + }, + }, }, - set: async (data: any) => { - const tasks: Promise[] = []; - for (const category in data) { - for (const id in data[category]) { - const value = data[category][id]; - const key = `${category}-${id}`; - tasks.push(value ? await writeData(value, key) : await removeData(key)); - } - } - - await Promise.all(tasks); + saveCreds: async () => { + return await writeData(creds, 'creds'); }, - }, - }, - saveCreds: async () => { - return await writeData(creds, 'creds'); - }, - }; + }; } diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index aea723ca..c83d6a61 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -2,888 +2,888 @@ import { JSONSchema7, JSONSchema7Definition } 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 }, - }; + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; }; // Instance Schema export const instanceNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - instanceName: { type: 'string' }, - webhook: { type: 'string' }, - webhook_by_events: { type: 'boolean' }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'NEW_JWT_TOKEN', - ], - }, + $id: v4(), + type: 'object', + properties: { + instanceName: { type: 'string' }, + webhook: { type: 'string' }, + webhook_by_events: { type: 'boolean' }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'NEW_JWT_TOKEN', + ], + }, + }, + qrcode: { type: 'boolean', enum: [true, false] }, + number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, + token: { type: 'string' }, }, - qrcode: { type: 'boolean', enum: [true, false] }, - number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, - token: { type: 'string' }, - }, - ...isNotEmpty('instanceName'), + ...isNotEmpty('instanceName'), }; export const oldTokenSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - oldToken: { type: 'string' }, - }, - required: ['oldToken'], - ...isNotEmpty('oldToken'), + $id: v4(), + type: 'object', + properties: { + oldToken: { type: 'string' }, + }, + required: ['oldToken'], + ...isNotEmpty('oldToken'), }; const quotedOptionsSchema: JSONSchema7 = { - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id'], - ...isNotEmpty('id'), + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id'], + ...isNotEmpty('id'), + }, + message: { type: 'object' }, }, - message: { type: 'object' }, - }, }; const mentionsOptionsSchema: JSONSchema7 = { - properties: { - everyOne: { type: 'boolean', enum: [true, false] }, - mentioned: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"mentioned" must be an array of numeric strings', - }, + properties: { + everyOne: { type: 'boolean', enum: [true, false] }, + mentioned: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"mentioned" must be an array of numeric strings', + }, + }, }, - }, }; // Send Message Schema const optionsSchema: JSONSchema7 = { - properties: { - delay: { - type: 'integer', - description: 'Enter a value in milliseconds', + properties: { + delay: { + type: 'integer', + description: 'Enter a value in milliseconds', + }, + presence: { + type: 'string', + enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], + }, + quoted: { ...quotedOptionsSchema }, + mentions: { ...mentionsOptionsSchema }, }, - presence: { - type: 'string', - enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], - }, - quoted: { ...quotedOptionsSchema }, - mentions: { ...mentionsOptionsSchema }, - }, }; const numberDefinition: JSONSchema7Definition = { - type: 'string', - description: 'Invalid format', + type: 'string', + description: 'Invalid format', }; export const textMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - textMessage: { - type: 'object', - properties: { - text: { type: 'string' }, - }, - required: ['text'], - ...isNotEmpty('text'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + textMessage: { + type: 'object', + properties: { + text: { type: 'string' }, + }, + required: ['text'], + ...isNotEmpty('text'), + }, }, - }, - required: ['textMessage', 'number'], + required: ['textMessage', 'number'], }; export const pollMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - pollMessage: { - type: 'object', - properties: { - name: { type: 'string' }, - selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, - values: { - type: 'array', - minItems: 2, - maxItems: 10, - uniqueItems: true, - items: { - type: 'string', - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + pollMessage: { + type: 'object', + properties: { + name: { type: 'string' }, + selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, + values: { + type: 'array', + minItems: 2, + maxItems: 10, + uniqueItems: true, + items: { + type: 'string', + }, + }, + }, + required: ['name', 'selectableCount', 'values'], + ...isNotEmpty('name', 'selectableCount', 'values'), }, - }, - required: ['name', 'selectableCount', 'values'], - ...isNotEmpty('name', 'selectableCount', 'values'), }, - }, - required: ['pollMessage', 'number'], + required: ['pollMessage', 'number'], }; export const statusMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - statusMessage: { - type: 'object', - properties: { - type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, - content: { type: 'string' }, - caption: { type: 'string' }, - backgroundColor: { type: 'string' }, - font: { type: 'integer', minimum: 0, maximum: 5 }, - statusJidList: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"statusJidList" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + statusMessage: { + type: 'object', + properties: { + type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, + content: { type: 'string' }, + caption: { type: 'string' }, + backgroundColor: { type: 'string' }, + font: { type: 'integer', minimum: 0, maximum: 5 }, + statusJidList: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"statusJidList" must be an array of numeric strings', + }, + }, + allContacts: { type: 'boolean', enum: [true, false] }, + }, + required: ['type', 'content'], + ...isNotEmpty('type', 'content'), }, - allContacts: { type: 'boolean', enum: [true, false] }, - }, - required: ['type', 'content'], - ...isNotEmpty('type', 'content'), }, - }, - required: ['statusMessage'], + required: ['statusMessage'], }; export const mediaMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - mediaMessage: { - type: 'object', - properties: { - mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, - media: { type: 'string' }, - fileName: { type: 'string' }, - caption: { type: 'string' }, - }, - required: ['mediatype', 'media'], - ...isNotEmpty('fileName', 'caption', 'media'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + mediaMessage: { + type: 'object', + properties: { + mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, + media: { type: 'string' }, + fileName: { type: 'string' }, + caption: { type: 'string' }, + }, + required: ['mediatype', 'media'], + ...isNotEmpty('fileName', 'caption', 'media'), + }, }, - }, - required: ['mediaMessage', 'number'], + required: ['mediaMessage', 'number'], }; export const stickerMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - stickerMessage: { - type: 'object', - properties: { - image: { type: 'string' }, - }, - required: ['image'], - ...isNotEmpty('image'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + stickerMessage: { + type: 'object', + properties: { + image: { type: 'string' }, + }, + required: ['image'], + ...isNotEmpty('image'), + }, }, - }, - required: ['stickerMessage', 'number'], + required: ['stickerMessage', 'number'], }; export const audioMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - audioMessage: { - type: 'object', - properties: { - audio: { type: 'string' }, - }, - required: ['audio'], - ...isNotEmpty('audio'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + audioMessage: { + type: 'object', + properties: { + audio: { type: 'string' }, + }, + required: ['audio'], + ...isNotEmpty('audio'), + }, }, - }, - required: ['audioMessage', 'number'], + required: ['audioMessage', 'number'], }; export const buttonMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - buttonMessage: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttons: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + buttonMessage: { type: 'object', properties: { - buttonText: { type: 'string' }, - buttonId: { type: 'string' }, + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttons: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + buttonText: { type: 'string' }, + buttonId: { type: 'string' }, + }, + required: ['buttonText', 'buttonId'], + ...isNotEmpty('buttonText', 'buttonId'), + }, + }, + mediaMessage: { + type: 'object', + properties: { + media: { type: 'string' }, + fileName: { type: 'string' }, + mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, + }, + required: ['media', 'mediatype'], + ...isNotEmpty('media', 'fileName'), + }, }, - required: ['buttonText', 'buttonId'], - ...isNotEmpty('buttonText', 'buttonId'), - }, + required: ['title', 'buttons'], + ...isNotEmpty('title', 'description'), }, - mediaMessage: { - type: 'object', - properties: { - media: { type: 'string' }, - fileName: { type: 'string' }, - mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, - }, - required: ['media', 'mediatype'], - ...isNotEmpty('media', 'fileName'), - }, - }, - required: ['title', 'buttons'], - ...isNotEmpty('title', 'description'), }, - }, - required: ['number', 'buttonMessage'], + required: ['number', 'buttonMessage'], }; export const locationMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - locationMessage: { - type: 'object', - properties: { - latitude: { type: 'number' }, - longitude: { type: 'number' }, - name: { type: 'string' }, - address: { type: 'string' }, - }, - required: ['latitude', 'longitude'], - ...isNotEmpty('name', 'addresss'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + locationMessage: { + type: 'object', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + name: { type: 'string' }, + address: { type: 'string' }, + }, + required: ['latitude', 'longitude'], + ...isNotEmpty('name', 'addresss'), + }, }, - }, - required: ['number', 'locationMessage'], + required: ['number', 'locationMessage'], }; export const listMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - listMessage: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttonText: { type: 'string' }, - sections: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + listMessage: { type: 'object', properties: { - title: { type: 'string' }, - rows: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - rowId: { type: 'string' }, - }, - required: ['title', 'description', 'rowId'], - ...isNotEmpty('title', 'description', 'rowId'), + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttonText: { type: 'string' }, + sections: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + rows: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + rowId: { type: 'string' }, + }, + required: ['title', 'description', 'rowId'], + ...isNotEmpty('title', 'description', 'rowId'), + }, + }, + }, + required: ['title', 'rows'], + ...isNotEmpty('title'), + }, }, - }, }, - required: ['title', 'rows'], - ...isNotEmpty('title'), - }, + required: ['title', 'description', 'buttonText', 'sections'], + ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - }, - required: ['title', 'description', 'buttonText', 'sections'], - ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - }, - required: ['number', 'listMessage'], + required: ['number', 'listMessage'], }; export const contactMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - contactMessage: { - type: 'array', - items: { - type: 'object', - properties: { - fullName: { type: 'string' }, - wuid: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"wuid" must be a numeric string', - }, - phoneNumber: { type: 'string', minLength: 10 }, - organization: { type: 'string' }, - email: { type: 'string' }, - url: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + contactMessage: { + type: 'array', + items: { + type: 'object', + properties: { + fullName: { type: 'string' }, + wuid: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"wuid" must be a numeric string', + }, + phoneNumber: { type: 'string', minLength: 10 }, + organization: { type: 'string' }, + email: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['fullName', 'phoneNumber'], + ...isNotEmpty('fullName'), + }, + minItems: 1, + uniqueItems: true, }, - required: ['fullName', 'phoneNumber'], - ...isNotEmpty('fullName'), - }, - minItems: 1, - uniqueItems: true, }, - }, - required: ['number', 'contactMessage'], + required: ['number', 'contactMessage'], }; export const reactionMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reactionMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'remoteJid', 'fromMe'], - ...isNotEmpty('id', 'remoteJid'), + $id: v4(), + type: 'object', + properties: { + reactionMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'remoteJid', 'fromMe'], + ...isNotEmpty('id', 'remoteJid'), + }, + reaction: { type: 'string' }, + }, + required: ['key', 'reaction'], + ...isNotEmpty('reaction'), }, - reaction: { type: 'string' }, - }, - required: ['key', 'reaction'], - ...isNotEmpty('reaction'), }, - }, - required: ['reactionMessage'], + required: ['reactionMessage'], }; // Chat Schema export const whatsappNumberSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - description: '"numbers" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + description: '"numbers" must be an array of numeric strings', + }, + }, }, - }, }; export const readMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - readMessages: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + readMessages: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, }, - }, - required: ['readMessages'], + required: ['readMessages'], }; export const privacySettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - privacySettings: { - type: 'object', - properties: { - readreceipts: { type: 'string', enum: ['all', 'none'] }, - profile: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], + $id: v4(), + type: 'object', + properties: { + privacySettings: { + type: 'object', + properties: { + readreceipts: { type: 'string', enum: ['all', 'none'] }, + profile: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + status: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + online: { type: 'string', enum: ['all', 'match_last_seen'] }, + last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, + groupadd: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + }, + required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], + ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - status: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - online: { type: 'string', enum: ['all', 'match_last_seen'] }, - last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, - groupadd: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - }, - required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], - ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - }, - required: ['privacySettings'], + required: ['privacySettings'], }; export const archiveChatSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - lastMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), + $id: v4(), + type: 'object', + properties: { + lastMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, + messageTimestamp: { type: 'integer', minLength: 1 }, + }, + required: ['key'], + ...isNotEmpty('messageTimestamp'), }, - messageTimestamp: { type: 'integer', minLength: 1 }, - }, - required: ['key'], - ...isNotEmpty('messageTimestamp'), + archive: { type: 'boolean', enum: [true, false] }, }, - archive: { type: 'boolean', enum: [true, false] }, - }, - required: ['lastMessage', 'archive'], + required: ['lastMessage', 'archive'], }; export const deleteMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - participant: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid', 'participant'), + $id: v4(), + type: 'object', + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + participant: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid', 'participant'), }; export const contactValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - pushName: { type: 'string', minLength: 1 }, - id: { type: 'string', minLength: 1 }, - }, - ...isNotEmpty('_id', 'id', 'pushName'), + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + pushName: { type: 'string', minLength: 1 }, + id: { type: 'string', minLength: 1 }, + }, + ...isNotEmpty('_id', 'id', 'pushName'), + }, }, - }, }; export const profileNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - name: { type: 'string' }, - }, - ...isNotEmpty('name'), + $id: v4(), + type: 'object', + properties: { + name: { type: 'string' }, + }, + ...isNotEmpty('name'), }; export const profileStatusSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - status: { type: 'string' }, - }, - ...isNotEmpty('status'), + $id: v4(), + type: 'object', + properties: { + status: { type: 'string' }, + }, + ...isNotEmpty('status'), }; export const profilePictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { type: 'string' }, - picture: { type: 'string' }, - }, + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + picture: { type: 'string' }, + }, }; export const profileSchema: JSONSchema7 = { - type: 'object', - properties: { - wuid: { type: 'string' }, - name: { type: 'string' }, - picture: { type: 'string' }, - status: { type: 'string' }, - isBusiness: { type: 'boolean' }, - }, + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, }; export const messageValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - key: { - type: 'object', - if: { - propertyNames: { - enum: ['fromMe', 'remoteJid', 'id'], - }, - }, - then: { + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', properties: { - remoteJid: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - id: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - fromMe: { type: 'boolean', enum: [true, false] }, + _id: { type: 'string', minLength: 1 }, + key: { + type: 'object', + if: { + propertyNames: { + enum: ['fromMe', 'remoteJid', 'id'], + }, + }, + then: { + properties: { + remoteJid: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + id: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + }, + }, + message: { type: 'object' }, }, - }, + ...isNotEmpty('_id'), }, - message: { type: 'object' }, - }, - ...isNotEmpty('_id'), + limit: { type: 'integer' }, }, - limit: { type: 'integer' }, - }, }; export const messageUpSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string' }, - remoteJid: { type: 'string' }, - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - participant: { type: 'string' }, - status: { - type: 'string', - enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string' }, + remoteJid: { type: 'string' }, + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + participant: { type: 'string' }, + status: { + type: 'string', + enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], + }, + }, + ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), }, - }, - ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), + limit: { type: 'integer' }, }, - limit: { type: 'integer' }, - }, }; // Group Schema export const createGroupSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - subject: { type: 'string' }, - description: { type: 'string' }, - profilePicture: { type: 'string' }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + subject: { type: 'string' }, + description: { type: 'string' }, + profilePicture: { type: 'string' }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, }, - }, - required: ['subject', 'participants'], - ...isNotEmpty('subject', 'description', 'profilePicture'), + required: ['subject', 'participants'], + ...isNotEmpty('subject', 'description', 'profilePicture'), }; export const groupJidSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, - }, - required: ['groupJid'], - ...isNotEmpty('groupJid'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, + }, + required: ['groupJid'], + ...isNotEmpty('groupJid'), }; export const getParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - getParticipants: { type: 'string', enum: ['true', 'false'] }, - }, - required: ['getParticipants'], - ...isNotEmpty('getParticipants'), + $id: v4(), + type: 'object', + properties: { + getParticipants: { type: 'string', enum: ['true', 'false'] }, + }, + required: ['getParticipants'], + ...isNotEmpty('getParticipants'), }; export const groupSendInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"numbers" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"numbers" must be an array of numeric strings', + }, + }, }, - }, - required: ['groupJid', 'description', 'numbers'], - ...isNotEmpty('groupJid', 'description', 'numbers'), + required: ['groupJid', 'description', 'numbers'], + ...isNotEmpty('groupJid', 'description', 'numbers'), }; export const groupInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, - }, - required: ['inviteCode'], - ...isNotEmpty('inviteCode'), + $id: v4(), + type: 'object', + properties: { + inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, + }, + required: ['inviteCode'], + ...isNotEmpty('inviteCode'), }; export const updateParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['add', 'remove', 'promote', 'demote'], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['add', 'remove', 'promote', 'demote'], + }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, - }, - required: ['groupJid', 'action', 'participants'], - ...isNotEmpty('groupJid', 'action'), + required: ['groupJid', 'action', 'participants'], + ...isNotEmpty('groupJid', 'action'), }; export const updateSettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], + }, }, - }, - required: ['groupJid', 'action'], - ...isNotEmpty('groupJid', 'action'), + required: ['groupJid', 'action'], + ...isNotEmpty('groupJid', 'action'), }; export const toggleEphemeralSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - expiration: { - type: 'number', - enum: [0, 86400, 604800, 7776000], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + expiration: { + type: 'number', + enum: [0, 86400, 604800, 7776000], + }, }, - }, - required: ['groupJid', 'expiration'], - ...isNotEmpty('groupJid', 'expiration'), + required: ['groupJid', 'expiration'], + ...isNotEmpty('groupJid', 'expiration'), }; export const updateGroupPictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - image: { type: 'string' }, - }, - required: ['groupJid', 'image'], - ...isNotEmpty('groupJid', 'image'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + image: { type: 'string' }, + }, + required: ['groupJid', 'image'], + ...isNotEmpty('groupJid', 'image'), }; export const updateGroupSubjectSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - subject: { type: 'string' }, - }, - required: ['groupJid', 'subject'], - ...isNotEmpty('groupJid', 'subject'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['groupJid', 'subject'], + ...isNotEmpty('groupJid', 'subject'), }; export const updateGroupDescriptionSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - }, - required: ['groupJid', 'description'], - ...isNotEmpty('groupJid', 'description'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['groupJid', 'description'], + ...isNotEmpty('groupJid', 'description'), }; // Webhook Schema export const webhookSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - url: { type: 'string' }, - enabled: { type: 'boolean', enum: [true, false] }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'NEW_JWT_TOKEN', - ], - }, + $id: v4(), + type: 'object', + properties: { + url: { type: 'string' }, + enabled: { type: 'boolean', enum: [true, false] }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'NEW_JWT_TOKEN', + ], + }, + }, }, - }, - required: ['url', 'enabled'], - ...isNotEmpty('url'), + required: ['url', 'enabled'], + ...isNotEmpty('url'), }; export const chatwootSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - enabled: { type: 'boolean', enum: [true, false] }, - account_id: { type: 'string' }, - token: { type: 'string' }, - url: { type: 'string' }, - sign_msg: { type: 'boolean', enum: [true, false] }, - }, - required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], - ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + account_id: { type: 'string' }, + token: { type: 'string' }, + url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, + }, + required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], + ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), }; export const settingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reject_call: { type: 'boolean', enum: [true, false] }, - msg_call: { type: 'string' }, - groups_ignore: { type: 'boolean', enum: [true, false] }, - }, - required: ['reject_call'], - ...isNotEmpty('reject_call'), + $id: v4(), + type: 'object', + properties: { + reject_call: { type: 'boolean', enum: [true, false] }, + msg_call: { type: 'string' }, + groups_ignore: { type: 'boolean', enum: [true, false] }, + }, + required: ['reject_call'], + ...isNotEmpty('reject_call'), }; diff --git a/src/whatsapp/abstract/abstract.repository.ts b/src/whatsapp/abstract/abstract.repository.ts index a0c4de6f..739489eb 100644 --- a/src/whatsapp/abstract/abstract.repository.ts +++ b/src/whatsapp/abstract/abstract.repository.ts @@ -7,59 +7,61 @@ import { ROOT_DIR } from '../../config/path.config'; export type IInsert = { insertCount: number }; export interface IRepository { - insert(data: any, instanceName: string, saveDb?: boolean): Promise; - update(data: any, instanceName: string, saveDb?: boolean): Promise; - find(query: any): Promise; - delete(query: any, force?: boolean): Promise; + insert(data: any, instanceName: string, saveDb?: boolean): Promise; + update(data: any, instanceName: string, saveDb?: boolean): Promise; + find(query: any): Promise; + delete(query: any, force?: boolean): Promise; - dbSettings: Database; - readonly storePath: string; + dbSettings: Database; + readonly storePath: string; } type WriteStore = { - path: string; - fileName: string; - data: U; + path: string; + fileName: string; + data: U; }; export abstract class Repository implements IRepository { - constructor(configService: ConfigService) { - this.dbSettings = configService.get('DATABASE'); - } - - dbSettings: Database; - readonly storePath = join(ROOT_DIR, 'store'); - - public writeStore = (create: WriteStore) => { - if (!existsSync(create.path)) { - mkdirSync(create.path, { recursive: true }); + constructor(configService: ConfigService) { + this.dbSettings = configService.get('DATABASE'); } - try { - writeFileSync( - join(create.path, create.fileName + '.json'), - JSON.stringify({ ...create.data }), - { encoding: 'utf-8' }, - ); - return { message: 'create - success' }; - } finally { - create.data = undefined; + dbSettings: Database; + readonly storePath = join(ROOT_DIR, 'store'); + + public writeStore = (create: WriteStore) => { + if (!existsSync(create.path)) { + mkdirSync(create.path, { recursive: true }); + } + try { + writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), { + encoding: 'utf-8', + }); + + return { message: 'create - success' }; + } finally { + create.data = undefined; + } + }; + + // eslint-disable-next-line + public insert(data: any, instanceName: string, saveDb = false): Promise { + throw new Error('Method not implemented.'); } - }; - public insert(data: any, instanceName: string, saveDb = false): Promise { - throw new Error('Method not implemented.'); - } + // eslint-disable-next-line + public update(data: any, instanceName: string, saveDb = false): Promise { + throw new Error('Method not implemented.'); + } - public update(data: any, instanceName: string, saveDb = false): Promise { - throw new Error('Method not implemented.'); - } + // eslint-disable-next-line + public find(query: any): Promise { + throw new Error('Method not implemented.'); + } - public find(query: any): Promise { - throw new Error('Method not implemented.'); - } - - delete(query: any, force?: boolean): Promise { - throw new Error('Method not implemented.'); - } + // eslint-disable-next-line + delete(query: any, force?: boolean): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 7a9dc427..86f05d6a 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -10,216 +10,214 @@ import { GetParticipant, GroupInvite, GroupJid } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; type DataValidate = { - request: Request; - schema: JSONSchema7; - ClassRef: any; - execute: (instance: InstanceDto, data: T) => Promise; + request: Request; + schema: JSONSchema7; + ClassRef: any; + execute: (instance: InstanceDto, data: T) => Promise; }; const logger = new Logger('Validate'); export abstract class RouterBroker { - constructor() {} - public routerPath(path: string, param = true) { - // const route = param ? '/:instanceName/' + path : '/' + path; - let route = '/' + path; - param ? (route += '/:instanceName') : null; + constructor() {} + public routerPath(path: string, param = true) { + // const route = param ? '/:instanceName/' + path : '/' + path; + let route = '/' + path; + param ? (route += '/:instanceName') : null; - return route; - } - - public async dataValidate(args: DataValidate) { - const { request, schema, ClassRef, execute } = args; - - const ref = new ClassRef(); - const body = request.body; - const instance = request.params as unknown as InstanceDto; - - if (request?.query && Object.keys(request.query).length > 0) { - Object.assign(instance, request.query); + return route; } - if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, body); - } + public async dataValidate(args: DataValidate) { + const { request, schema, ClassRef, execute } = args; - Object.assign(ref, body); + const ref = new ClassRef(); + const body = request.body; + const instance = request.params as unknown as InstanceDto; - const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); + if (request?.query && Object.keys(request.query).length > 0) { + Object.assign(instance, request.query); } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - return await execute(instance, ref); - } - - public async groupNoValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; - - const instance = request.params as unknown as InstanceDto; - - const ref = new ClassRef(); - - Object.assign(ref, request.body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); + if (request.originalUrl.includes('/instance/create')) { + Object.assign(instance, body); } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - return await execute(instance, ref); - } + Object.assign(ref, body); - public async groupValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; - const groupJid = request.query as unknown as GroupJid; - - if (!groupJid?.groupJid) { - throw new BadRequestException( - 'The group id needs to be informed in the query', - 'ex: "groupJid=120362@g.us"', - ); - } - - const instance = request.params as unknown as InstanceDto; - const body = request.body; - - const ref = new ClassRef(); - - Object.assign(body, groupJid); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); + + return await execute(instance, ref); } - return await execute(instance, ref); - } + public async groupNoValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; - public async inviteCodeValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + const instance = request.params as unknown as InstanceDto; - const inviteCode = request.query as unknown as GroupInvite; + const ref = new ClassRef(); - if (!inviteCode?.inviteCode) { - throw new BadRequestException( - 'The group invite code id needs to be informed in the query', - 'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)', - ); - } + Object.assign(ref, request.body); - const instance = request.params as unknown as InstanceDto; - const body = request.body; + const v = validate(ref, schema); - const ref = new ClassRef(); - - Object.assign(body, inviteCode); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); + + return await execute(instance, ref); } - return await execute(instance, ref); - } + public async groupValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; - public async getParticipantsValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + const groupJid = request.query as unknown as GroupJid; - const getParticipants = request.query as unknown as GetParticipant; - - if (!getParticipants?.getParticipants) { - throw new BadRequestException( - 'The getParticipants needs to be informed in the query', - ); - } - - const instance = request.params as unknown as InstanceDto; - const body = request.body; - - const ref = new ClassRef(); - - Object.assign(body, getParticipants); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); + if (!groupJid?.groupJid) { + throw new BadRequestException( + 'The group id needs to be informed in the query', + 'ex: "groupJid=120362@g.us"', + ); } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, groupJid); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); } - return await execute(instance, ref); - } + public async inviteCodeValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; + + const inviteCode = request.query as unknown as GroupInvite; + + if (!inviteCode?.inviteCode) { + throw new BadRequestException( + 'The group invite code id needs to be informed in the query', + 'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)', + ); + } + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, inviteCode); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); + } + + public async getParticipantsValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; + + const getParticipants = request.query as unknown as GetParticipant; + + if (!getParticipants?.getParticipants) { + throw new BadRequestException('The getParticipants needs to be informed in the query'); + } + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, getParticipants); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); + } } diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index 8217b908..c1eff50b 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -1,17 +1,15 @@ -import { proto } from '@whiskeysockets/baileys'; - import { Logger } from '../../config/logger.config'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberDto, - PrivacySettingDto, - ProfileNameDto, - ProfilePictureDto, - ProfileStatusDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberDto, + PrivacySettingDto, + ProfileNameDto, + ProfilePictureDto, + ProfileStatusDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -22,124 +20,95 @@ import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('ChatController'); export class ChatController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { - logger.verbose('requested whatsappNumber from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); - } + public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { + logger.verbose('requested whatsappNumber from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); + } - public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { - logger.verbose('requested readMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); - } + public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { + logger.verbose('requested readMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); + } - public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { - logger.verbose('requested archiveChat from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].archiveChat(data); - } + public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { + logger.verbose('requested archiveChat from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].archiveChat(data); + } - public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { - logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].deleteMessage(data); - } + public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { + logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].deleteMessage(data); + } - public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) { - logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); - } + public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); + } - public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { - logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchProfile( - instanceName, - data.number, - ); - } + public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number); + } - public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { - logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchContacts(query); - } + public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { + logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchContacts(query); + } - public async getBase64FromMediaMessage( - { instanceName }: InstanceDto, - data: getBase64FromMediaMessageDto, - ) { - logger.verbose( - 'requested getBase64FromMediaMessage from ' + instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data); - } + public async getBase64FromMediaMessage({ instanceName }: InstanceDto, data: getBase64FromMediaMessageDto) { + logger.verbose('requested getBase64FromMediaMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data); + } - public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) { - logger.verbose('requested fetchMessages from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchMessages(query); - } + public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) { + logger.verbose('requested fetchMessages from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchMessages(query); + } - public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) { - logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query); - } + public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) { + logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query); + } - public async fetchChats({ instanceName }: InstanceDto) { - logger.verbose('requested fetchChats from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchChats(); - } + public async fetchChats({ instanceName }: InstanceDto) { + logger.verbose('requested fetchChats from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchChats(); + } - public async fetchPrivacySettings({ instanceName }: InstanceDto) { - logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); - } + public async fetchPrivacySettings({ instanceName }: InstanceDto) { + logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); + } - public async updatePrivacySettings( - { instanceName }: InstanceDto, - data: PrivacySettingDto, - ) { - logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); - } + public async updatePrivacySettings({ instanceName }: InstanceDto, data: PrivacySettingDto) { + logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); + } - public async fetchBusinessProfile( - { instanceName }: InstanceDto, - data: ProfilePictureDto, - ) { - logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile( - data.number, - ); - } + public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) { + logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number); + } - public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { - logger.verbose('requested updateProfileName from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); - } + public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { + logger.verbose('requested updateProfileName from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); + } - public async updateProfileStatus( - { instanceName }: InstanceDto, - data: ProfileStatusDto, - ) { - logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfileStatus( - data.status, - ); - } + public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) { + logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status); + } - public async updateProfilePicture( - { instanceName }: InstanceDto, - data: ProfilePictureDto, - ) { - logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfilePicture( - data.picture, - ); - } + public async updateProfilePicture({ instanceName }: InstanceDto, data: ProfilePictureDto) { + logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfilePicture(data.picture); + } - public async removeProfilePicture( - { instanceName }: InstanceDto, - data: ProfilePictureDto, - ) { - logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); - } + public async removeProfilePicture({ instanceName }: InstanceDto) { + logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); + } } diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 892c6905..d4563485 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -11,94 +11,87 @@ import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor( - private readonly chatwootService: ChatwootService, - private readonly configService: ConfigService, - ) {} + constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} - public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - logger.verbose( - 'requested createChatwoot from ' + instance.instanceName + ' instance', - ); + public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { + logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); - if (data.enabled) { - if (!isURL(data.url, { require_tld: false })) { - throw new BadRequestException('url is not valid'); - } + if (data.enabled) { + if (!isURL(data.url, { require_tld: false })) { + throw new BadRequestException('url is not valid'); + } - if (!data.account_id) { - throw new BadRequestException('account_id is required'); - } + if (!data.account_id) { + throw new BadRequestException('account_id is required'); + } - if (!data.token) { - throw new BadRequestException('token is required'); - } + if (!data.token) { + throw new BadRequestException('token is required'); + } - if (data.sign_msg !== true && data.sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } + if (data.sign_msg !== true && data.sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + } + + if (!data.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + data.sign_msg = false; + } + + data.name_inbox = instance.instanceName; + + const result = this.chatwootService.create(instance, data); + + const urlServer = this.configService.get('SERVER').URL; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } - if (!data.enabled) { - logger.verbose('chatwoot disabled'); - data.account_id = ''; - data.token = ''; - data.url = ''; - data.sign_msg = false; + public async findChatwoot(instance: InstanceDto) { + logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); + const result = await this.chatwootService.find(instance); + + const urlServer = this.configService.get('SERVER').URL; + + if (Object.keys(result).length === 0) { + return { + enabled: false, + url: '', + account_id: '', + token: '', + sign_msg: false, + name_inbox: '', + webhook_url: '', + }; + } + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } - data.name_inbox = instance.instanceName; + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); + const chatwootService = new ChatwootService(waMonitor, this.configService); - const result = this.chatwootService.create(instance, data); - - const urlServer = this.configService.get('SERVER').URL; - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; - } - - public async findChatwoot(instance: InstanceDto) { - logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - const result = await this.chatwootService.find(instance); - - const urlServer = this.configService.get('SERVER').URL; - - if (Object.keys(result).length === 0) { - return { - enabled: false, - url: '', - account_id: '', - token: '', - sign_msg: false, - name_inbox: '', - webhook_url: '', - }; + return chatwootService.receiveWebhook(instance, data); } - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; + public async newInstance(data: any) { + const chatwootService = new ChatwootService(waMonitor, this.configService); - return response; - } - - public async receiveWebhook(instance: InstanceDto, data: any) { - logger.verbose( - 'requested receiveWebhook from ' + instance.instanceName + ' instance', - ); - const chatwootService = new ChatwootService(waMonitor, this.configService); - - return chatwootService.receiveWebhook(instance, data); - } - - public async newInstance(data: any) { - const chatwootService = new ChatwootService(waMonitor, this.configService); - - return chatwootService.newInstance(data); - } + return chatwootService.newInstance(data); + } } diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index 56195f65..86e78737 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -1,16 +1,16 @@ import { Logger } from '../../config/logger.config'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; import { WAMonitoringService } from '../services/monitor.service'; @@ -18,120 +18,80 @@ import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('ChatController'); export class GroupController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async createGroup(instance: InstanceDto, create: CreateGroupDto) { - logger.verbose('requested createGroup from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); - } + public async createGroup(instance: InstanceDto, create: CreateGroupDto) { + logger.verbose('requested createGroup from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); + } - public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { - logger.verbose( - 'requested updateGroupPicture from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture( - update, - ); - } + public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { + logger.verbose('requested updateGroupPicture from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update); + } - public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { - logger.verbose( - 'requested updateGroupSubject from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject( - update, - ); - } + public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { + logger.verbose('requested updateGroupSubject from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update); + } - public async updateGroupDescription( - instance: InstanceDto, - update: GroupDescriptionDto, - ) { - logger.verbose( - 'requested updateGroupDescription from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription( - update, - ); - } + public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) { + logger.verbose('requested updateGroupDescription from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update); + } - public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); - } + public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); + } - public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { - logger.verbose( - 'requested fetchAllGroups from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups( - getPaticipants, - ); - } + public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { + logger.verbose('requested fetchAllGroups from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants); + } - public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); - } + public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); + } - public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { - logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); - } + public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { + logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); + } - public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { - logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); - } + public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { + logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); + } - public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose( - 'requested revokeInviteCode from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode( - groupJid, - ); - } + public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); + } - public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose( - 'requested findParticipants from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].findParticipants( - groupJid, - ); - } + public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested findParticipants from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid); + } - public async updateGParticipate( - instance: InstanceDto, - update: GroupUpdateParticipantDto, - ) { - logger.verbose( - 'requested updateGParticipate from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant( - update, - ); - } + public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) { + logger.verbose('requested updateGParticipate from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update); + } - public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { - logger.verbose( - 'requested updateGSetting from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); - } + public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { + logger.verbose('requested updateGSetting from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); + } - public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { - logger.verbose( - 'requested toggleEphemeral from ' + instance.instanceName + ' instance', - ); - return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral( - update, - ); - } + public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { + logger.verbose('requested toggleEphemeral from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update); + } - public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); - } + public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); + } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index ab21986d..79449e29 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -2,7 +2,7 @@ import { delay } from '@whiskeysockets/baileys'; import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; -import { Auth, ConfigService, HttpServer } from '../../config/env.config'; +import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { RedisCache } from '../../db/redis.client'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; @@ -16,309 +16,294 @@ import { WAStartupService } from '../services/whatsapp.service'; import { wa } from '../types/wa.types'; export class InstanceController { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly eventEmitter: EventEmitter2, - private readonly authService: AuthService, - private readonly webhookService: WebhookService, - private readonly chatwootService: ChatwootService, - private readonly cache: RedisCache, - ) {} + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly eventEmitter: EventEmitter2, + private readonly authService: AuthService, + private readonly webhookService: WebhookService, + private readonly chatwootService: ChatwootService, + private readonly cache: RedisCache, + ) {} - private readonly logger = new Logger(InstanceController.name); + private readonly logger = new Logger(InstanceController.name); - public async createInstance({ - instanceName, - webhook, - webhook_by_events, - events, - qrcode, - number, - token, - chatwoot_account_id, - chatwoot_token, - chatwoot_url, - chatwoot_sign_msg, - }: InstanceDto) { - this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); - - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException( - 'The instance name must be lowercase and without special characters', - ); - } - - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); - - this.logger.verbose('creating instance'); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); - - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); - - this.logger.verbose('hash: ' + hash + ' generated'); - - let getEvents: string[]; - - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } - - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; - - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(number); - await delay(5000); - getQrcode = instance.qrCode; - } - - const result = { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, + public async createInstance({ + instanceName, webhook, webhook_by_events, - events: getEvents, - qrcode: getQrcode, - }; - - this.logger.verbose('instance created'); - this.logger.verbose(result); - - return result; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - number, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + events, qrcode, number, - ); - } catch (error) { - this.logger.log(error); + token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, + chatwoot_sign_msg, + }: InstanceDto) { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException('The instance name must be lowercase and without special characters'); + } + + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); + + this.logger.verbose('creating instance'); + const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); + + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); + + this.logger.verbose('hash: ' + hash + ' generated'); + + let getEvents: string[]; + + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); + } + + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(number); + await delay(5000); + getQrcode = instance.qrCode; + } + + const result = { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }; + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + number, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + number, + ); + } catch (error) { + this.logger.log(error); + } + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + number, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, + }; } - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - number, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; - } + public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { + try { + this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance'); - public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { - try { - this.logger.verbose( - 'requested connectToWhatsapp from ' + instanceName + ' instance', - ); + const instance = this.waMonitor.waInstances[instanceName]; + const state = instance?.connectionStatus?.state; - const instance = this.waMonitor.waInstances[instanceName]; - const state = instance?.connectionStatus?.state; + this.logger.verbose('state: ' + state); - this.logger.verbose('state: ' + state); + if (state == 'open') { + return await this.connectionState({ instanceName }); + } - if (state == 'open') { - return await this.connectionState({ instanceName }); - } + if (state == 'connecting') { + return instance.qrCode; + } - if (state == 'connecting') { - return instance.qrCode; - } + if (state == 'close') { + this.logger.verbose('connecting'); + await instance.connectToWhatsapp(number); - if (state == 'close') { - this.logger.verbose('connecting'); - await instance.connectToWhatsapp(number); + await delay(5000); + return instance.qrCode; + } - await delay(5000); - return instance.qrCode; - } - - return { - instance: { - instanceName: instanceName, - status: state, - }, - qrcode: instance?.qrCode, - }; - } catch (error) { - this.logger.error(error); - } - } - - public async restartInstance({ instanceName }: InstanceDto) { - try { - this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - - this.logger.verbose('logging out instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - - return { error: false, message: 'Instance restarted' }; - } catch (error) { - this.logger.error(error); - } - } - - public async connectionState({ instanceName }: InstanceDto) { - this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); - return { - instance: { - instanceName: instanceName, - state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, - }, - }; - } - - public async fetchInstances({ instanceName }: InstanceDto) { - this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); - if (instanceName) { - this.logger.verbose('instanceName: ' + instanceName); - return this.waMonitor.instanceInfo(instanceName); + return { + instance: { + instanceName: instanceName, + status: state, + }, + qrcode: instance?.qrCode, + }; + } catch (error) { + this.logger.error(error); + } } - return this.waMonitor.instanceInfo(); - } + public async restartInstance({ instanceName }: InstanceDto) { + try { + this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - public async logout({ instanceName }: InstanceDto) { - this.logger.verbose('requested logout from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - if (instance.state === 'close') { - throw new BadRequestException( - 'The "' + instanceName + '" instance is not connected', - ); + return { error: false, message: 'Instance restarted' }; + } catch (error) { + this.logger.error(error); + } } - try { - this.logger.verbose('logging out instance: ' + instanceName); - await this.waMonitor.waInstances[instanceName]?.client?.logout( - 'Log out instance: ' + instanceName, - ); - - this.logger.verbose('close connection instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - - return { error: false, message: 'Instance logged out' }; - } catch (error) { - throw new InternalServerErrorException(error.toString()); + public async connectionState({ instanceName }: InstanceDto) { + this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); + return { + instance: { + instanceName: instanceName, + state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, + }, + }; } - } - public async deleteInstance({ instanceName }: InstanceDto) { - this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); + public async fetchInstances({ instanceName }: InstanceDto) { + this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); + if (instanceName) { + this.logger.verbose('instanceName: ' + instanceName); + return this.waMonitor.instanceInfo(instanceName); + } - if (instance.state === 'open') { - throw new BadRequestException( - 'The "' + instanceName + '" instance needs to be disconnected', - ); + return this.waMonitor.instanceInfo(); } - try { - if (instance.state === 'connecting') { - this.logger.verbose('logging out instance: ' + instanceName); - await this.logout({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - return { error: false, message: 'Instance deleted' }; - } else { - this.logger.verbose('deleting instance: ' + instanceName); + public async logout({ instanceName }: InstanceDto) { + this.logger.verbose('requested logout from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { error: false, message: 'Instance deleted' }; - } - } catch (error) { - throw new BadRequestException(error.toString()); + if (instance.state === 'close') { + throw new BadRequestException('The "' + instanceName + '" instance is not connected'); + } + + try { + this.logger.verbose('logging out instance: ' + instanceName); + await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); + + this.logger.verbose('close connection instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + + return { error: false, message: 'Instance logged out' }; + } catch (error) { + throw new InternalServerErrorException(error.toString()); + } } - } - public async refreshToken(_: InstanceDto, oldToken: OldToken) { - this.logger.verbose('requested refreshToken'); - return await this.authService.refreshToken(oldToken); - } + public async deleteInstance({ instanceName }: InstanceDto) { + this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); + + if (instance.state === 'open') { + throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); + } + try { + if (instance.state === 'connecting') { + this.logger.verbose('logging out instance: ' + instanceName); + + await this.logout({ instanceName }); + delete this.waMonitor.waInstances[instanceName]; + return { error: false, message: 'Instance deleted' }; + } else { + this.logger.verbose('deleting instance: ' + instanceName); + + delete this.waMonitor.waInstances[instanceName]; + this.eventEmitter.emit('remove.instance', instanceName, 'inner'); + return { error: false, message: 'Instance deleted' }; + } + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async refreshToken(_: InstanceDto, oldToken: OldToken) { + this.logger.verbose('requested refreshToken'); + return await this.authService.refreshToken(oldToken); + } } diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index 2f49ca50..593b858e 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -4,124 +4,112 @@ import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, } from '../dto/sendMessage.dto'; import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('MessageRouter'); export class SendMessageController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { - logger.verbose('requested sendText from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].textMessage(data); - } - - public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { - logger.verbose('requested sendMedia from ' + instanceName + ' instance'); - - if ( - isBase64(data?.mediaMessage?.media) && - !data?.mediaMessage?.fileName && - data?.mediaMessage?.mediatype === 'document' - ) { - throw new BadRequestException('For base64 the file name must be informed.'); + public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { + logger.verbose('requested sendText from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].textMessage(data); } - logger.verbose( - 'isURL: ' + - isURL(data?.mediaMessage?.media) + - ', isBase64: ' + - isBase64(data?.mediaMessage?.media), - ); - if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) { - return await this.waMonitor.waInstances[instanceName].mediaMessage(data); + public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { + logger.verbose('requested sendMedia from ' + instanceName + ' instance'); + + if ( + isBase64(data?.mediaMessage?.media) && + !data?.mediaMessage?.fileName && + data?.mediaMessage?.mediatype === 'document' + ) { + throw new BadRequestException('For base64 the file name must be informed.'); + } + + logger.verbose( + 'isURL: ' + isURL(data?.mediaMessage?.media) + ', isBase64: ' + isBase64(data?.mediaMessage?.media), + ); + if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) { + return await this.waMonitor.waInstances[instanceName].mediaMessage(data); + } + throw new BadRequestException('Owned media must be a url or base64'); } - throw new BadRequestException('Owned media must be a url or base64'); - } - public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) { - logger.verbose('requested sendSticker from ' + instanceName + ' instance'); + public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) { + logger.verbose('requested sendSticker from ' + instanceName + ' instance'); - logger.verbose( - 'isURL: ' + - isURL(data?.stickerMessage?.image) + - ', isBase64: ' + - isBase64(data?.stickerMessage?.image), - ); - if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) { - return await this.waMonitor.waInstances[instanceName].mediaSticker(data); + logger.verbose( + 'isURL: ' + isURL(data?.stickerMessage?.image) + ', isBase64: ' + isBase64(data?.stickerMessage?.image), + ); + if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) { + return await this.waMonitor.waInstances[instanceName].mediaSticker(data); + } + throw new BadRequestException('Owned media must be a url or base64'); } - throw new BadRequestException('Owned media must be a url or base64'); - } - public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { - logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance'); + public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { + logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance'); - logger.verbose( - 'isURL: ' + - isURL(data?.audioMessage?.audio) + - ', isBase64: ' + - isBase64(data?.audioMessage?.audio), - ); - if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { - return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); + logger.verbose( + 'isURL: ' + isURL(data?.audioMessage?.audio) + ', isBase64: ' + isBase64(data?.audioMessage?.audio), + ); + if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { + return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); + } + throw new BadRequestException('Owned media must be a url or base64'); } - throw new BadRequestException('Owned media must be a url or base64'); - } - public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) { - logger.verbose('requested sendButtons from ' + instanceName + ' instance'); - if ( - isBase64(data.buttonMessage.mediaMessage?.media) && - !data.buttonMessage.mediaMessage?.fileName - ) { - throw new BadRequestException('For bse64 the file name must be informed.'); + public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) { + logger.verbose('requested sendButtons from ' + instanceName + ' instance'); + if (isBase64(data.buttonMessage.mediaMessage?.media) && !data.buttonMessage.mediaMessage?.fileName) { + throw new BadRequestException('For bse64 the file name must be informed.'); + } + return await this.waMonitor.waInstances[instanceName].buttonMessage(data); } - return await this.waMonitor.waInstances[instanceName].buttonMessage(data); - } - public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { - logger.verbose('requested sendLocation from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].locationMessage(data); - } - - public async sendList({ instanceName }: InstanceDto, data: SendListDto) { - logger.verbose('requested sendList from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].listMessage(data); - } - - public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { - logger.verbose('requested sendContact from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].contactMessage(data); - } - - public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { - logger.verbose('requested sendReaction from ' + instanceName + ' instance'); - if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) { - throw new BadRequestException('"reaction" must be an emoji'); + public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { + logger.verbose('requested sendLocation from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].locationMessage(data); } - return await this.waMonitor.waInstances[instanceName].reactionMessage(data); - } - public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { - logger.verbose('requested sendPoll from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].pollMessage(data); - } + public async sendList({ instanceName }: InstanceDto, data: SendListDto) { + logger.verbose('requested sendList from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].listMessage(data); + } - public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) { - logger.verbose('requested sendStatus from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].statusMessage(data); - } + public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { + logger.verbose('requested sendContact from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].contactMessage(data); + } + + public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { + logger.verbose('requested sendReaction from ' + instanceName + ' instance'); + if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) { + throw new BadRequestException('"reaction" must be an emoji'); + } + return await this.waMonitor.waInstances[instanceName].reactionMessage(data); + } + + public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { + logger.verbose('requested sendPoll from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].pollMessage(data); + } + + public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) { + logger.verbose('requested sendStatus from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].statusMessage(data); + } } diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 2c763b80..1d033783 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -1,5 +1,3 @@ -import { isURL } from 'class-validator'; - import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; @@ -9,22 +7,20 @@ import { SettingsService } from '../services/settings.service'; const logger = new Logger('SettingsController'); export class SettingsController { - constructor(private readonly settingsService: SettingsService) {} + constructor(private readonly settingsService: SettingsService) {} - public async createSettings(instance: InstanceDto, data: SettingsDto) { - logger.verbose( - 'requested createSettings from ' + instance.instanceName + ' instance', - ); + public async createSettings(instance: InstanceDto, data: SettingsDto) { + logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); - if (data.reject_call && data.msg_call.trim() == '') { - throw new BadRequestException('msg_call is required'); + if (data.reject_call && data.msg_call.trim() == '') { + throw new BadRequestException('msg_call is required'); + } + + return this.settingsService.create(instance, data); } - return this.settingsService.create(instance, data); - } - - public async findSettings(instance: InstanceDto) { - logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.find(instance); - } + public async findSettings(instance: InstanceDto) { + logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); + return this.settingsService.find(instance); + } } diff --git a/src/whatsapp/controllers/views.controller.ts b/src/whatsapp/controllers/views.controller.ts index 327a209b..269ea7de 100644 --- a/src/whatsapp/controllers/views.controller.ts +++ b/src/whatsapp/controllers/views.controller.ts @@ -7,23 +7,20 @@ import { HttpStatus } from '../routers/index.router'; import { WAMonitoringService } from '../services/monitor.service'; export class ViewsController { - constructor( - private readonly waMonit: WAMonitoringService, - private readonly configService: ConfigService, - ) {} + constructor(private readonly waMonit: WAMonitoringService, private readonly configService: ConfigService) {} - public async qrcode(request: Request, response: Response) { - try { - const param = request.params as unknown as InstanceDto; - const instance = this.waMonit.waInstances[param.instanceName]; - if (instance.connectionStatus.state === 'open') { - throw new BadRequestException('The instance is already connected'); - } - const type = this.configService.get('AUTHENTICATION').TYPE; + public async qrcode(request: Request, response: Response) { + try { + const param = request.params as unknown as InstanceDto; + const instance = this.waMonit.waInstances[param.instanceName]; + if (instance.connectionStatus.state === 'open') { + throw new BadRequestException('The instance is already connected'); + } + const type = this.configService.get('AUTHENTICATION').TYPE; - return response.status(HttpStatus.OK).render('qrcode', { type, ...param }); - } catch (error) { - console.log('ERROR: ', error); + return response.status(HttpStatus.OK).render('qrcode', { type, ...param }); + } catch (error) { + console.log('ERROR: ', error); + } } - } } diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index 281147db..073a22fc 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -9,26 +9,26 @@ import { WebhookService } from '../services/webhook.service'; const logger = new Logger('WebhookController'); export class WebhookController { - constructor(private readonly webhookService: WebhookService) {} + constructor(private readonly webhookService: WebhookService) {} - public async createWebhook(instance: InstanceDto, data: WebhookDto) { - logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); + public async createWebhook(instance: InstanceDto, data: WebhookDto) { + logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); - if (data.enabled && !isURL(data.url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property'); + if (data.enabled && !isURL(data.url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property'); + } + + if (!data.enabled) { + logger.verbose('webhook disabled'); + data.url = ''; + data.events = []; + } + + return this.webhookService.create(instance, data); } - if (!data.enabled) { - logger.verbose('webhook disabled'); - data.url = ''; - data.events = []; + public async findWebhook(instance: InstanceDto) { + logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); + return this.webhookService.find(instance); } - - return this.webhookService.create(instance, data); - } - - public async findWebhook(instance: InstanceDto) { - logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); - return this.webhookService.find(instance); - } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 2800c6ff..38f17bf0 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,93 +1,84 @@ -import { - proto, - WAPrivacyOnlineValue, - WAPrivacyValue, - WAReadReceiptsValue, -} from '@whiskeysockets/baileys'; +import { proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { - constructor( - public readonly jid: string, - public readonly exists: boolean, - public readonly name?: string, - ) {} + constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} } export class getBase64FromMediaMessageDto { - message: proto.WebMessageInfo; - convertToMp4?: boolean; + message: proto.WebMessageInfo; + convertToMp4?: boolean; } export class WhatsAppNumberDto { - numbers: string[]; + numbers: string[]; } export class NumberDto { - number: string; + number: string; } export class NumberBusiness { - wid?: string; - jid?: string; - exists?: boolean; - isBusiness: boolean; - name?: string; - message?: string; - description?: string; - email?: string; - website?: string[]; - address?: string; + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; } export class ProfileNameDto { - name: string; + name: string; } export class ProfileStatusDto { - status: string; + status: string; } export class ProfilePictureDto { - number?: string; - // url or base64 - picture?: string; + number?: string; + // url or base64 + picture?: string; } class Key { - id: string; - fromMe: boolean; - remoteJid: string; + id: string; + fromMe: boolean; + remoteJid: string; } export class ReadMessageDto { - readMessages: Key[]; + readMessages: Key[]; } class LastMessage { - key: Key; - messageTimestamp?: number; + key: Key; + messageTimestamp?: number; } export class ArchiveChatDto { - lastMessage: LastMessage; - archive: boolean; + lastMessage: LastMessage; + archive: boolean; } class PrivacySetting { - readreceipts: WAReadReceiptsValue; - profile: WAPrivacyValue; - status: WAPrivacyValue; - online: WAPrivacyOnlineValue; - last: WAPrivacyValue; - groupadd: WAPrivacyValue; + readreceipts: WAReadReceiptsValue; + profile: WAPrivacyValue; + status: WAPrivacyValue; + online: WAPrivacyOnlineValue; + last: WAPrivacyValue; + groupadd: WAPrivacyValue; } export class PrivacySettingDto { - privacySettings: PrivacySetting; + privacySettings: PrivacySetting; } export class DeleteMessage { - id: string; - fromMe: boolean; - remoteJid: string; - participant?: string; + id: string; + fromMe: boolean; + remoteJid: string; + participant?: string; } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index a5026a46..64a2f1b6 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -1,9 +1,9 @@ export class ChatwootDto { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; } diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index bc36e27f..ef47f9b8 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -1,51 +1,51 @@ export class CreateGroupDto { - subject: string; - description?: string; - participants: string[]; + subject: string; + description?: string; + participants: string[]; } export class GroupPictureDto { - groupJid: string; - image: string; + groupJid: string; + image: string; } export class GroupSubjectDto { - groupJid: string; - subject: string; + groupJid: string; + subject: string; } export class GroupDescriptionDto { - groupJid: string; - description: string; + groupJid: string; + description: string; } export class GroupJid { - groupJid: string; + groupJid: string; } export class GetParticipant { - getParticipants: string; + getParticipants: string; } export class GroupInvite { - inviteCode: string; + inviteCode: string; } export class GroupSendInvite { - groupJid: string; - description: string; - numbers: string[]; + groupJid: string; + description: string; + numbers: string[]; } export class GroupUpdateParticipantDto extends GroupJid { - action: 'add' | 'remove' | 'promote' | 'demote'; - participants: string[]; + action: 'add' | 'remove' | 'promote' | 'demote'; + participants: string[]; } export class GroupUpdateSettingDto extends GroupJid { - action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; + action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; } export class GroupToggleEphemeralDto extends GroupJid { - expiration: 0 | 86400 | 604800 | 7776000; + expiration: 0 | 86400 | 604800 | 7776000; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 9e8a7ec3..3fc780d1 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -1,13 +1,13 @@ export class InstanceDto { - instanceName: string; - webhook?: string; - webhook_by_events?: boolean; - events?: string[]; - qrcode?: boolean; - number?: string; - token?: string; - chatwoot_account_id?: string; - chatwoot_token?: string; - chatwoot_url?: string; - chatwoot_sign_msg?: boolean; + instanceName: string; + webhook?: string; + webhook_by_events?: boolean; + events?: string[]; + qrcode?: boolean; + number?: string; + token?: string; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; + chatwoot_sign_msg?: boolean; } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index c2ddb3a2..754d66e2 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -1,150 +1,150 @@ import { proto, WAPresence } from '@whiskeysockets/baileys'; export class Quoted { - key: proto.IMessageKey; - message: proto.IMessage; + key: proto.IMessageKey; + message: proto.IMessage; } export class Mentions { - everyOne: boolean; - mentioned: string[]; + everyOne: boolean; + mentioned: string[]; } export class Options { - delay?: number; - presence?: WAPresence; - quoted?: Quoted; - mentions?: Mentions; - linkPreview?: boolean; - encoding?: boolean; + delay?: number; + presence?: WAPresence; + quoted?: Quoted; + mentions?: Mentions; + linkPreview?: boolean; + encoding?: boolean; } class OptionsMessage { - options: Options; + options: Options; } export class Metadata extends OptionsMessage { - number: string; + number: string; } class TextMessage { - text: string; + text: string; } export class StatusMessage { - type: string; - content: string; - statusJidList?: string[]; - allContacts?: boolean; - caption?: string; - backgroundColor?: string; - font?: number; + type: string; + content: string; + statusJidList?: string[]; + allContacts?: boolean; + caption?: string; + backgroundColor?: string; + font?: number; } class PollMessage { - name: string; - selectableCount: number; - values: string[]; - messageSecret?: Uint8Array; + name: string; + selectableCount: number; + values: string[]; + messageSecret?: Uint8Array; } export class SendTextDto extends Metadata { - textMessage: TextMessage; + textMessage: TextMessage; } export class SendStatusDto extends Metadata { - statusMessage: StatusMessage; + statusMessage: StatusMessage; } export class SendPollDto extends Metadata { - pollMessage: PollMessage; + pollMessage: PollMessage; } export type MediaType = 'image' | 'document' | 'video' | 'audio'; export class MediaMessage { - mediatype: MediaType; - caption?: string; - // for document - fileName?: string; - // url or base64 - media: string; + mediatype: MediaType; + caption?: string; + // for document + fileName?: string; + // url or base64 + media: string; } export class SendMediaDto extends Metadata { - mediaMessage: MediaMessage; + mediaMessage: MediaMessage; } class Sticker { - image: string; + image: string; } export class SendStickerDto extends Metadata { - stickerMessage: Sticker; + stickerMessage: Sticker; } class Audio { - audio: string; + audio: string; } export class SendAudioDto extends Metadata { - audioMessage: Audio; + audioMessage: Audio; } class Button { - buttonText: string; - buttonId: string; + buttonText: string; + buttonId: string; } class ButtonMessage { - title: string; - description: string; - footerText?: string; - buttons: Button[]; - mediaMessage?: MediaMessage; + title: string; + description: string; + footerText?: string; + buttons: Button[]; + mediaMessage?: MediaMessage; } export class SendButtonDto extends Metadata { - buttonMessage: ButtonMessage; + buttonMessage: ButtonMessage; } class LocationMessage { - latitude: number; - longitude: number; - name?: string; - address?: string; + latitude: number; + longitude: number; + name?: string; + address?: string; } export class SendLocationDto extends Metadata { - locationMessage: LocationMessage; + locationMessage: LocationMessage; } class Row { - title: string; - description: string; - rowId: string; + title: string; + description: string; + rowId: string; } class Section { - title: string; - rows: Row[]; + title: string; + rows: Row[]; } class ListMessage { - title: string; - description: string; - footerText?: string; - buttonText: string; - sections: Section[]; + title: string; + description: string; + footerText?: string; + buttonText: string; + sections: Section[]; } export class SendListDto extends Metadata { - listMessage: ListMessage; + listMessage: ListMessage; } export class ContactMessage { - fullName: string; - wuid: string; - phoneNumber: string; - organization?: string; - email?: string; - url?: string; + fullName: string; + wuid: string; + phoneNumber: string; + organization?: string; + email?: string; + url?: string; } export class SendContactDto extends Metadata { - contactMessage: ContactMessage[]; + contactMessage: ContactMessage[]; } class ReactionMessage { - key: proto.IMessageKey; - reaction: string; + key: proto.IMessageKey; + reaction: string; } export class SendReactionDto { - reactionMessage: ReactionMessage; + reactionMessage: ReactionMessage; } diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts index 20a6cba0..870a24d9 100644 --- a/src/whatsapp/dto/settings.dto.ts +++ b/src/whatsapp/dto/settings.dto.ts @@ -1,5 +1,5 @@ export class SettingsDto { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; } diff --git a/src/whatsapp/dto/webhook.dto.ts b/src/whatsapp/dto/webhook.dto.ts index 5203884d..b41ec0e6 100644 --- a/src/whatsapp/dto/webhook.dto.ts +++ b/src/whatsapp/dto/webhook.dto.ts @@ -1,6 +1,6 @@ export class WebhookDto { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; } diff --git a/src/whatsapp/guards/auth.guard.ts b/src/whatsapp/guards/auth.guard.ts index 6ddc297e..f4d30fc1 100644 --- a/src/whatsapp/guards/auth.guard.ts +++ b/src/whatsapp/guards/auth.guard.ts @@ -13,85 +13,77 @@ import { repository } from '../whatsapp.module'; const logger = new Logger('GUARD'); async function jwtGuard(req: Request, res: Response, next: NextFunction) { - const key = req.get('apikey'); + const key = req.get('apikey'); - if (key && configService.get('AUTHENTICATION').API_KEY.KEY !== key) { - throw new UnauthorizedException(); - } - - if (configService.get('AUTHENTICATION').API_KEY.KEY === key) { - return next(); - } - - if ( - (req.originalUrl.includes('/instance/create') || - req.originalUrl.includes('/instance/fetchInstances')) && - !key - ) { - throw new ForbiddenException( - 'Missing global api key', - 'The global api key must be set', - ); - } - - const jwtOpts = configService.get('AUTHENTICATION').JWT; - try { - const [bearer, token] = req.get('authorization').split(' '); - - if (bearer.toLowerCase() !== 'bearer') { - throw new UnauthorizedException(); + if (key && configService.get('AUTHENTICATION').API_KEY.KEY !== key) { + throw new UnauthorizedException(); } - if (!isJWT(token)) { - throw new UnauthorizedException(); + if (configService.get('AUTHENTICATION').API_KEY.KEY === key) { + return next(); } - const param = req.params as unknown as InstanceDto; - const decode = jwt.verify(token, jwtOpts.SECRET, { - ignoreExpiration: jwtOpts.EXPIRIN_IN === 0, - }) as JwtPayload; - - if (param.instanceName !== decode.instanceName || name !== decode.apiName) { - throw new UnauthorizedException(); + if ( + (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && + !key + ) { + throw new ForbiddenException('Missing global api key', 'The global api key must be set'); } - return next(); - } catch (error) { - logger.error(error); - throw new UnauthorizedException(); - } + const jwtOpts = configService.get('AUTHENTICATION').JWT; + try { + const [bearer, token] = req.get('authorization').split(' '); + + if (bearer.toLowerCase() !== 'bearer') { + throw new UnauthorizedException(); + } + + if (!isJWT(token)) { + throw new UnauthorizedException(); + } + + const param = req.params as unknown as InstanceDto; + const decode = jwt.verify(token, jwtOpts.SECRET, { + ignoreExpiration: jwtOpts.EXPIRIN_IN === 0, + }) as JwtPayload; + + if (param.instanceName !== decode.instanceName || name !== decode.apiName) { + throw new UnauthorizedException(); + } + + return next(); + } catch (error) { + logger.error(error); + throw new UnauthorizedException(); + } } async function apikey(req: Request, res: Response, next: NextFunction) { - const env = configService.get('AUTHENTICATION').API_KEY; - const key = req.get('apikey'); + const env = configService.get('AUTHENTICATION').API_KEY; + const key = req.get('apikey'); - if (env.KEY === key) { - return next(); - } - - if ( - (req.originalUrl.includes('/instance/create') || - req.originalUrl.includes('/instance/fetchInstances')) && - !key - ) { - throw new ForbiddenException( - 'Missing global api key', - 'The global api key must be set', - ); - } - - try { - const param = req.params as unknown as InstanceDto; - const instanceKey = await repository.auth.find(param.instanceName); - if (instanceKey.apikey === key) { - return next(); + if (env.KEY === key) { + return next(); } - } catch (error) { - logger.error(error); - } - throw new UnauthorizedException(); + if ( + (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && + !key + ) { + throw new ForbiddenException('Missing global api key', 'The global api key must be set'); + } + + try { + const param = req.params as unknown as InstanceDto; + const instanceKey = await repository.auth.find(param.instanceName); + if (instanceKey.apikey === key) { + return next(); + } + } catch (error) { + logger.error(error); + } + + throw new UnauthorizedException(); } export const authGuard = { jwt: jwtGuard, apikey }; diff --git a/src/whatsapp/guards/instance.guard.ts b/src/whatsapp/guards/instance.guard.ts index 964b99b6..6d65c743 100644 --- a/src/whatsapp/guards/instance.guard.ts +++ b/src/whatsapp/guards/instance.guard.ts @@ -5,69 +5,60 @@ import { join } from 'path'; import { configService, Database, Redis } from '../../config/env.config'; import { INSTANCE_DIR } from '../../config/path.config'; import { dbserver } from '../../db/db.connect'; -import { - BadRequestException, - ForbiddenException, - NotFoundException, -} from '../../exceptions'; +import { BadRequestException, ForbiddenException, NotFoundException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { cache, waMonitor } from '../whatsapp.module'; async function getInstance(instanceName: string) { - const db = configService.get('DATABASE'); - const redisConf = configService.get('REDIS'); + const db = configService.get('DATABASE'); + const redisConf = configService.get('REDIS'); - const exists = !!waMonitor.waInstances[instanceName]; + const exists = !!waMonitor.waInstances[instanceName]; - if (redisConf.ENABLED) { - const keyExists = await cache.keyExists(); - return exists || keyExists; - } + if (redisConf.ENABLED) { + const keyExists = await cache.keyExists(); + return exists || keyExists; + } - if (db.ENABLED) { - const collection = dbserver - .getClient() - .db(db.CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(instanceName); - return exists || (await collection.find({}).toArray()).length > 0; - } + if (db.ENABLED) { + const collection = dbserver + .getClient() + .db(db.CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(instanceName); + return exists || (await collection.find({}).toArray()).length > 0; + } - return exists || existsSync(join(INSTANCE_DIR, instanceName)); + return exists || existsSync(join(INSTANCE_DIR, instanceName)); } export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) { - if ( - req.originalUrl.includes('/instance/create') || - req.originalUrl.includes('/instance/fetchInstances') - ) { - return next(); - } + if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) { + return next(); + } - const param = req.params as unknown as InstanceDto; - if (!param?.instanceName) { - throw new BadRequestException('"instanceName" not provided.'); - } + const param = req.params as unknown as InstanceDto; + if (!param?.instanceName) { + throw new BadRequestException('"instanceName" not provided.'); + } - if (!(await getInstance(param.instanceName))) { - throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); - } + if (!(await getInstance(param.instanceName))) { + throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); + } - next(); + next(); } export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) { - if (req.originalUrl.includes('/instance/create')) { - const instance = req.body as InstanceDto; - if (await getInstance(instance.instanceName)) { - throw new ForbiddenException( - `This name "${instance.instanceName}" is already in use.`, - ); + if (req.originalUrl.includes('/instance/create')) { + const instance = req.body as InstanceDto; + if (await getInstance(instance.instanceName)) { + throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); + } + + if (waMonitor.waInstances[instance.instanceName]) { + delete waMonitor.waInstances[instance.instanceName]; + } } - if (waMonitor.waInstances[instance.instanceName]) { - delete waMonitor.waInstances[instance.instanceName]; - } - } - - next(); + next(); } diff --git a/src/whatsapp/models/auth.model.ts b/src/whatsapp/models/auth.model.ts index 5c5b6a41..c9e48da1 100644 --- a/src/whatsapp/models/auth.model.ts +++ b/src/whatsapp/models/auth.model.ts @@ -3,15 +3,15 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class AuthRaw { - _id?: string; - jwt?: string; - apikey?: string; + _id?: string; + jwt?: string; + apikey?: string; } const authSchema = new Schema({ - _id: { type: String, _id: true }, - jwt: { type: String, minlength: 1 }, - apikey: { type: String, minlength: 1 }, + _id: { type: String, _id: true }, + jwt: { type: String, minlength: 1 }, + apikey: { type: String, minlength: 1 }, }); export const AuthModel = dbserver?.model(AuthRaw.name, authSchema, 'authentication'); diff --git a/src/whatsapp/models/chat.model.ts b/src/whatsapp/models/chat.model.ts index 20153603..d44a4673 100644 --- a/src/whatsapp/models/chat.model.ts +++ b/src/whatsapp/models/chat.model.ts @@ -3,16 +3,16 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ChatRaw { - _id?: string; - id?: string; - owner: string; - lastMsgTimestamp?: number; + _id?: string; + id?: string; + owner: string; + lastMsgTimestamp?: number; } const chatSchema = new Schema({ - _id: { type: String, _id: true }, - id: { type: String, required: true, minlength: 1 }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + id: { type: String, required: true, minlength: 1 }, + owner: { type: String, required: true, minlength: 1 }, }); export const ChatModel = dbserver?.model(ChatRaw.name, chatSchema, 'chats'); diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index 307565eb..31e28894 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -3,30 +3,26 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { - _id?: string; - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; + _id?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; } const chatwootSchema = new Schema({ - _id: { type: String, _id: true }, - enabled: { type: Boolean, required: true }, - account_id: { type: String, required: true }, - token: { type: String, required: true }, - url: { type: String, required: true }, - name_inbox: { type: String, required: true }, - sign_msg: { type: Boolean, required: true }, - number: { type: String, required: true }, + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + account_id: { type: String, required: true }, + token: { type: String, required: true }, + url: { type: String, required: true }, + name_inbox: { type: String, required: true }, + sign_msg: { type: Boolean, required: true }, + number: { type: String, required: true }, }); -export const ChatwootModel = dbserver?.model( - ChatwootRaw.name, - chatwootSchema, - 'chatwoot', -); +export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot'); export type IChatwootModel = typeof ChatwootModel; diff --git a/src/whatsapp/models/contact.model.ts b/src/whatsapp/models/contact.model.ts index d9b51e1e..84749945 100644 --- a/src/whatsapp/models/contact.model.ts +++ b/src/whatsapp/models/contact.model.ts @@ -3,19 +3,19 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ContactRaw { - _id?: string; - pushName?: string; - id?: string; - profilePictureUrl?: string; - owner: string; + _id?: string; + pushName?: string; + id?: string; + profilePictureUrl?: string; + owner: string; } const contactSchema = new Schema({ - _id: { type: String, _id: true }, - pushName: { type: String, minlength: 1 }, - id: { type: String, required: true, minlength: 1 }, - profilePictureUrl: { type: String, minlength: 1 }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + pushName: { type: String, minlength: 1 }, + id: { type: String, required: true, minlength: 1 }, + profilePictureUrl: { type: String, minlength: 1 }, + owner: { type: String, required: true, minlength: 1 }, }); export const ContactModel = dbserver?.model(ContactRaw.name, contactSchema, 'contacts'); diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index c4f475ad..bfa048cf 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -4,70 +4,66 @@ import { dbserver } from '../../db/db.connect'; import { wa } from '../types/wa.types'; class Key { - id?: string; - remoteJid?: string; - fromMe?: boolean; - participant?: string; + id?: string; + remoteJid?: string; + fromMe?: boolean; + participant?: string; } export class MessageRaw { - _id?: string; - key?: Key; - pushName?: string; - participant?: string; - message?: object; - messageType?: string; - messageTimestamp?: number | Long.Long; - owner: string; - source?: 'android' | 'web' | 'ios'; + _id?: string; + key?: Key; + pushName?: string; + participant?: string; + message?: object; + messageType?: string; + messageTimestamp?: number | Long.Long; + owner: string; + source?: 'android' | 'web' | 'ios'; } const messageSchema = new Schema({ - _id: { type: String, _id: true }, - key: { - id: { type: String, required: true, minlength: 1 }, - remoteJid: { type: String, required: true, minlength: 1 }, - fromMe: { type: Boolean, required: true }, - participant: { type: String, minlength: 1 }, - }, - pushName: { type: String }, - participant: { type: String }, - messageType: { type: String }, - message: { type: Object }, - source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, - messageTimestamp: { type: Number, required: true }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + key: { + id: { type: String, required: true, minlength: 1 }, + remoteJid: { type: String, required: true, minlength: 1 }, + fromMe: { type: Boolean, required: true }, + participant: { type: String, minlength: 1 }, + }, + pushName: { type: String }, + participant: { type: String }, + messageType: { type: String }, + message: { type: Object }, + source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, + messageTimestamp: { type: Number, required: true }, + owner: { type: String, required: true, minlength: 1 }, }); export const MessageModel = dbserver?.model(MessageRaw.name, messageSchema, 'messages'); export type IMessageModel = typeof MessageModel; export class MessageUpdateRaw { - _id?: string; - remoteJid?: string; - id?: string; - fromMe?: boolean; - participant?: string; - datetime?: number; - status?: wa.StatusMessage; - owner: string; - pollUpdates?: any; + _id?: string; + remoteJid?: string; + id?: string; + fromMe?: boolean; + participant?: string; + datetime?: number; + status?: wa.StatusMessage; + owner: string; + pollUpdates?: any; } const messageUpdateSchema = new Schema({ - _id: { type: String, _id: true }, - remoteJid: { type: String, required: true, min: 1 }, - id: { type: String, required: true, min: 1 }, - fromMe: { type: Boolean, required: true }, - participant: { type: String, min: 1 }, - datetime: { type: Number, required: true, min: 1 }, - status: { type: String, required: true }, - owner: { type: String, required: true, min: 1 }, + _id: { type: String, _id: true }, + remoteJid: { type: String, required: true, min: 1 }, + id: { type: String, required: true, min: 1 }, + fromMe: { type: Boolean, required: true }, + participant: { type: String, min: 1 }, + datetime: { type: Number, required: true, min: 1 }, + status: { type: String, required: true }, + owner: { type: String, required: true, min: 1 }, }); -export const MessageUpModel = dbserver?.model( - MessageUpdateRaw.name, - messageUpdateSchema, - 'messageUpdate', -); +export const MessageUpModel = dbserver?.model(MessageUpdateRaw.name, messageUpdateSchema, 'messageUpdate'); export type IMessageUpModel = typeof MessageUpModel; diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index e70605e4..283f44fd 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -3,22 +3,18 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class SettingsRaw { - _id?: string; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; + _id?: string; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; } const settingsSchema = new Schema({ - _id: { type: String, _id: true }, - reject_call: { type: Boolean, required: true }, - msg_call: { type: String, required: true }, - groups_ignore: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + reject_call: { type: Boolean, required: true }, + msg_call: { type: String, required: true }, + groups_ignore: { type: Boolean, required: true }, }); -export const SettingsModel = dbserver?.model( - SettingsRaw.name, - settingsSchema, - 'settings', -); +export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings'); export type ISettingsModel = typeof SettingsModel; diff --git a/src/whatsapp/models/webhook.model.ts b/src/whatsapp/models/webhook.model.ts index fa91326c..57d55a7b 100644 --- a/src/whatsapp/models/webhook.model.ts +++ b/src/whatsapp/models/webhook.model.ts @@ -3,19 +3,19 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class WebhookRaw { - _id?: string; - url?: string; - enabled?: boolean; - events?: string[]; - webhook_by_events?: boolean; + _id?: string; + url?: string; + enabled?: boolean; + events?: string[]; + webhook_by_events?: boolean; } const webhookSchema = new Schema({ - _id: { type: String, _id: true }, - url: { type: String, required: true }, - enabled: { type: Boolean, required: true }, - events: { type: [String], required: true }, - webhook_by_events: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + url: { type: String, required: true }, + enabled: { type: Boolean, required: true }, + events: { type: [String], required: true }, + webhook_by_events: { type: Boolean, required: true }, }); export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook'); diff --git a/src/whatsapp/repository/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index 9949cec3..b93ea86d 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -1,74 +1,65 @@ import { readFileSync } from 'fs'; import { join } from 'path'; -import { Auth, ConfigService, Database } from '../../config/env.config'; +import { Auth, ConfigService } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { AUTH_DIR } from '../../config/path.config'; import { IInsert, Repository } from '../abstract/abstract.repository'; import { AuthRaw, IAuthModel } from '../models'; export class AuthRepository extends Repository { - constructor( - private readonly authModel: IAuthModel, - readonly configService: ConfigService, - ) { - super(configService); - this.auth = configService.get('AUTHENTICATION'); - } - - private readonly auth: Auth; - private readonly logger = new Logger('AuthRepository'); - - public async create(data: AuthRaw, instance: string): Promise { - try { - this.logger.verbose('creating auth'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving auth to db'); - const insert = await this.authModel.replaceOne( - { _id: instance }, - { ...data }, - { upsert: true }, - ); - - this.logger.verbose('auth saved to db: ' + insert.modifiedCount + ' auth'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving auth to store'); - - this.writeStore({ - path: join(AUTH_DIR, this.auth.TYPE), - fileName: instance, - data, - }); - this.logger.verbose( - 'auth saved to store in path: ' + join(AUTH_DIR, this.auth.TYPE) + '/' + instance, - ); - - this.logger.verbose('auth created'); - return { insertCount: 1 }; - } catch (error) { - return { error } as any; + constructor(private readonly authModel: IAuthModel, readonly configService: ConfigService) { + super(configService); + this.auth = configService.get('AUTHENTICATION'); } - } - public async find(instance: string): Promise { - try { - this.logger.verbose('finding auth'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding auth in db'); - return await this.authModel.findOne({ _id: instance }); - } + private readonly auth: Auth; + private readonly logger = new Logger('AuthRepository'); - this.logger.verbose('finding auth in store'); + public async create(data: AuthRaw, instance: string): Promise { + try { + this.logger.verbose('creating auth'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving auth to db'); + const insert = await this.authModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); - return JSON.parse( - readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), { - encoding: 'utf-8', - }), - ) as AuthRaw; - } catch (error) { - return {}; + this.logger.verbose('auth saved to db: ' + insert.modifiedCount + ' auth'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving auth to store'); + + this.writeStore({ + path: join(AUTH_DIR, this.auth.TYPE), + fileName: instance, + data, + }); + this.logger.verbose('auth saved to store in path: ' + join(AUTH_DIR, this.auth.TYPE) + '/' + instance); + + this.logger.verbose('auth created'); + return { insertCount: 1 }; + } catch (error) { + return { error } as any; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding auth'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding auth in db'); + return await this.authModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding auth in store'); + + return JSON.parse( + readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), { + encoding: 'utf-8', + }), + ) as AuthRaw; + } catch (error) { + return {}; + } } - } } diff --git a/src/whatsapp/repository/chat.repository.ts b/src/whatsapp/repository/chat.repository.ts index 31e1764a..b3ff575a 100644 --- a/src/whatsapp/repository/chat.repository.ts +++ b/src/whatsapp/repository/chat.repository.ts @@ -7,122 +7,111 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ChatRaw, IChatModel } from '../models'; export class ChatQuery { - where: ChatRaw; + where: ChatRaw; } export class ChatRepository extends Repository { - constructor( - private readonly chatModel: IChatModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('ChatRepository'); - - public async insert( - data: ChatRaw[], - instanceName: string, - saveDb = false, - ): Promise { - this.logger.verbose('inserting chats'); - if (data.length === 0) { - this.logger.verbose('no chats to insert'); - return; + constructor(private readonly chatModel: IChatModel, private readonly configService: ConfigService) { + super(configService); } - try { - this.logger.verbose('saving chats to store'); - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving chats to db'); - const insert = await this.chatModel.insertMany([...data]); + private readonly logger = new Logger('ChatRepository'); - this.logger.verbose('chats saved to db: ' + insert.length + ' chats'); - return { insertCount: insert.length }; - } - - this.logger.verbose('saving chats to store'); - - const store = this.configService.get('STORE'); - - if (store.CHATS) { - this.logger.verbose('saving chats to store'); - data.forEach((chat) => { - this.writeStore({ - path: join(this.storePath, 'chats', instanceName), - fileName: chat.id, - data: chat, - }); - this.logger.verbose( - 'chats saved to store in path: ' + - join(this.storePath, 'chats', instanceName) + - '/' + - chat.id, - ); - }); - - this.logger.verbose('chats saved to store'); - return { insertCount: data.length }; - } - - this.logger.verbose('chats not saved to store'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } - } - - public async find(query: ChatQuery): Promise { - try { - this.logger.verbose('finding chats'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding chats in db'); - return await this.chatModel.find({ owner: query.where.owner }); - } - - this.logger.verbose('finding chats in store'); - - const chats: ChatRaw[] = []; - const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner)); - for await (const dirent of openDir) { - if (dirent.isFile()) { - chats.push( - JSON.parse( - readFileSync( - join(this.storePath, 'chats', query.where.owner, dirent.name), - { encoding: 'utf-8' }, - ), - ), - ); + public async insert(data: ChatRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting chats'); + if (data.length === 0) { + this.logger.verbose('no chats to insert'); + return; } - } - this.logger.verbose('chats found in store: ' + chats.length + ' chats'); - return chats; - } catch (error) { - return []; + try { + this.logger.verbose('saving chats to store'); + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving chats to db'); + const insert = await this.chatModel.insertMany([...data]); + + this.logger.verbose('chats saved to db: ' + insert.length + ' chats'); + return { insertCount: insert.length }; + } + + this.logger.verbose('saving chats to store'); + + const store = this.configService.get('STORE'); + + if (store.CHATS) { + this.logger.verbose('saving chats to store'); + data.forEach((chat) => { + this.writeStore({ + path: join(this.storePath, 'chats', instanceName), + fileName: chat.id, + data: chat, + }); + this.logger.verbose( + 'chats saved to store in path: ' + join(this.storePath, 'chats', instanceName) + '/' + chat.id, + ); + }); + + this.logger.verbose('chats saved to store'); + return { insertCount: data.length }; + } + + this.logger.verbose('chats not saved to store'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } } - } - public async delete(query: ChatQuery) { - try { - this.logger.verbose('deleting chats'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('deleting chats in db'); - return await this.chatModel.deleteOne({ ...query.where }); - } + public async find(query: ChatQuery): Promise { + try { + this.logger.verbose('finding chats'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chats in db'); + return await this.chatModel.find({ owner: query.where.owner }); + } - this.logger.verbose('deleting chats in store'); - rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), { - force: true, - recursive: true, - }); + this.logger.verbose('finding chats in store'); - return { deleted: { chatId: query.where.id } }; - } catch (error) { - return { error: error?.toString() }; + const chats: ChatRaw[] = []; + const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner)); + for await (const dirent of openDir) { + if (dirent.isFile()) { + chats.push( + JSON.parse( + readFileSync(join(this.storePath, 'chats', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + + this.logger.verbose('chats found in store: ' + chats.length + ' chats'); + return chats; + } catch (error) { + return []; + } + } + + public async delete(query: ChatQuery) { + try { + this.logger.verbose('deleting chats'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('deleting chats in db'); + return await this.chatModel.deleteOne({ ...query.where }); + } + + this.logger.verbose('deleting chats in store'); + rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), { + force: true, + recursive: true, + }); + + return { deleted: { chatId: query.where.id } }; + } catch (error) { + return { error: error?.toString() }; + } } - } } diff --git a/src/whatsapp/repository/chatwoot.repository.ts b/src/whatsapp/repository/chatwoot.repository.ts index bc722cd6..6cc3f631 100644 --- a/src/whatsapp/repository/chatwoot.repository.ts +++ b/src/whatsapp/repository/chatwoot.repository.ts @@ -7,70 +7,58 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ChatwootRaw, IChatwootModel } from '../models'; export class ChatwootRepository extends Repository { - constructor( - private readonly chatwootModel: IChatwootModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('ChatwootRepository'); - - public async create(data: ChatwootRaw, instance: string): Promise { - try { - this.logger.verbose('creating chatwoot'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving chatwoot to db'); - const insert = await this.chatwootModel.replaceOne( - { _id: instance }, - { ...data }, - { upsert: true }, - ); - - this.logger.verbose( - 'chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot', - ); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving chatwoot to store'); - - this.writeStore({ - path: join(this.storePath, 'chatwoot'), - fileName: instance, - data, - }); - - this.logger.verbose( - 'chatwoot saved to store in path: ' + - join(this.storePath, 'chatwoot') + - '/' + - instance, - ); - - this.logger.verbose('chatwoot created'); - return { insertCount: 1 }; - } catch (error) { - return error; + constructor(private readonly chatwootModel: IChatwootModel, private readonly configService: ConfigService) { + super(configService); } - } - public async find(instance: string): Promise { - try { - this.logger.verbose('finding chatwoot'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding chatwoot in db'); - return await this.chatwootModel.findOne({ _id: instance }); - } + private readonly logger = new Logger('ChatwootRepository'); - this.logger.verbose('finding chatwoot in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), { - encoding: 'utf-8', - }), - ) as ChatwootRaw; - } catch (error) { - return {}; + public async create(data: ChatwootRaw, instance: string): Promise { + try { + this.logger.verbose('creating chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving chatwoot to db'); + const insert = await this.chatwootModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving chatwoot to store'); + + this.writeStore({ + path: join(this.storePath, 'chatwoot'), + fileName: instance, + data, + }); + + this.logger.verbose( + 'chatwoot saved to store in path: ' + join(this.storePath, 'chatwoot') + '/' + instance, + ); + + this.logger.verbose('chatwoot created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chatwoot in db'); + return await this.chatwootModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding chatwoot in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), { + encoding: 'utf-8', + }), + ) as ChatwootRaw; + } catch (error) { + return {}; + } } - } } diff --git a/src/whatsapp/repository/contact.repository.ts b/src/whatsapp/repository/contact.repository.ts index 6a16f2a1..ed96ff56 100644 --- a/src/whatsapp/repository/contact.repository.ts +++ b/src/whatsapp/repository/contact.repository.ts @@ -7,189 +7,171 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ContactRaw, IContactModel } from '../models'; export class ContactQuery { - where: ContactRaw; + where: ContactRaw; } export class ContactRepository extends Repository { - constructor( - private readonly contactModel: IContactModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('ContactRepository'); - - public async insert( - data: ContactRaw[], - instanceName: string, - saveDb = false, - ): Promise { - this.logger.verbose('inserting contacts'); - - if (data.length === 0) { - this.logger.verbose('no contacts to insert'); - return; + constructor(private readonly contactModel: IContactModel, private readonly configService: ConfigService) { + super(configService); } - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving contacts to db'); + private readonly logger = new Logger('ContactRepository'); - const insert = await this.contactModel.insertMany([...data]); + public async insert(data: ContactRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting contacts'); - this.logger.verbose('contacts saved to db: ' + insert.length + ' contacts'); - return { insertCount: insert.length }; - } - - this.logger.verbose('saving contacts to store'); - - const store = this.configService.get('STORE'); - - if (store.CONTACTS) { - this.logger.verbose('saving contacts to store'); - data.forEach((contact) => { - this.writeStore({ - path: join(this.storePath, 'contacts', instanceName), - fileName: contact.id, - data: contact, - }); - this.logger.verbose( - 'contacts saved to store in path: ' + - join(this.storePath, 'contacts', instanceName) + - '/' + - contact.id, - ); - }); - - this.logger.verbose('contacts saved to store: ' + data.length + ' contacts'); - return { insertCount: data.length }; - } - - this.logger.verbose('contacts not saved'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } - } - - public async update( - data: ContactRaw[], - instanceName: string, - saveDb = false, - ): Promise { - try { - this.logger.verbose('updating contacts'); - - if (data.length === 0) { - this.logger.verbose('no contacts to update'); - return; - } - - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('updating contacts in db'); - - const contacts = data.map((contact) => { - return { - updateOne: { - filter: { id: contact.id }, - update: { ...contact }, - upsert: true, - }, - }; - }); - - const { nModified } = await this.contactModel.bulkWrite(contacts); - - this.logger.verbose('contacts updated in db: ' + nModified + ' contacts'); - return { insertCount: nModified }; - } - - this.logger.verbose('updating contacts in store'); - - const store = this.configService.get('STORE'); - - if (store.CONTACTS) { - this.logger.verbose('updating contacts in store'); - data.forEach((contact) => { - this.writeStore({ - path: join(this.storePath, 'contacts', instanceName), - fileName: contact.id, - data: contact, - }); - this.logger.verbose( - 'contacts updated in store in path: ' + - join(this.storePath, 'contacts', instanceName) + - '/' + - contact.id, - ); - }); - - this.logger.verbose('contacts updated in store: ' + data.length + ' contacts'); - - return { insertCount: data.length }; - } - - this.logger.verbose('contacts not updated'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } - } - - public async find(query: ContactQuery): Promise { - try { - this.logger.verbose('finding contacts'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding contacts in db'); - return await this.contactModel.find({ ...query.where }); - } - - this.logger.verbose('finding contacts in store'); - const contacts: ContactRaw[] = []; - if (query?.where?.id) { - this.logger.verbose('finding contacts in store by id'); - contacts.push( - JSON.parse( - readFileSync( - join( - this.storePath, - 'contacts', - query.where.owner, - query.where.id + '.json', - ), - { encoding: 'utf-8' }, - ), - ), - ); - } else { - this.logger.verbose('finding contacts in store by owner'); - - const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), { - encoding: 'utf-8', - }); - for await (const dirent of openDir) { - if (dirent.isFile()) { - contacts.push( - JSON.parse( - readFileSync( - join(this.storePath, 'contacts', query.where.owner, dirent.name), - { encoding: 'utf-8' }, - ), - ), - ); - } + if (data.length === 0) { + this.logger.verbose('no contacts to insert'); + return; } - } - this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); - return contacts; - } catch (error) { - return []; + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving contacts to db'); + + const insert = await this.contactModel.insertMany([...data]); + + this.logger.verbose('contacts saved to db: ' + insert.length + ' contacts'); + return { insertCount: insert.length }; + } + + this.logger.verbose('saving contacts to store'); + + const store = this.configService.get('STORE'); + + if (store.CONTACTS) { + this.logger.verbose('saving contacts to store'); + data.forEach((contact) => { + this.writeStore({ + path: join(this.storePath, 'contacts', instanceName), + fileName: contact.id, + data: contact, + }); + this.logger.verbose( + 'contacts saved to store in path: ' + + join(this.storePath, 'contacts', instanceName) + + '/' + + contact.id, + ); + }); + + this.logger.verbose('contacts saved to store: ' + data.length + ' contacts'); + return { insertCount: data.length }; + } + + this.logger.verbose('contacts not saved'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } + } + + public async update(data: ContactRaw[], instanceName: string, saveDb = false): Promise { + try { + this.logger.verbose('updating contacts'); + + if (data.length === 0) { + this.logger.verbose('no contacts to update'); + return; + } + + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating contacts in db'); + + const contacts = data.map((contact) => { + return { + updateOne: { + filter: { id: contact.id }, + update: { ...contact }, + upsert: true, + }, + }; + }); + + const { nModified } = await this.contactModel.bulkWrite(contacts); + + this.logger.verbose('contacts updated in db: ' + nModified + ' contacts'); + return { insertCount: nModified }; + } + + this.logger.verbose('updating contacts in store'); + + const store = this.configService.get('STORE'); + + if (store.CONTACTS) { + this.logger.verbose('updating contacts in store'); + data.forEach((contact) => { + this.writeStore({ + path: join(this.storePath, 'contacts', instanceName), + fileName: contact.id, + data: contact, + }); + this.logger.verbose( + 'contacts updated in store in path: ' + + join(this.storePath, 'contacts', instanceName) + + '/' + + contact.id, + ); + }); + + this.logger.verbose('contacts updated in store: ' + data.length + ' contacts'); + + return { insertCount: data.length }; + } + + this.logger.verbose('contacts not updated'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; + } + } + + public async find(query: ContactQuery): Promise { + try { + this.logger.verbose('finding contacts'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding contacts in db'); + return await this.contactModel.find({ ...query.where }); + } + + this.logger.verbose('finding contacts in store'); + const contacts: ContactRaw[] = []; + if (query?.where?.id) { + this.logger.verbose('finding contacts in store by id'); + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.where.owner, query.where.id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + } else { + this.logger.verbose('finding contacts in store by owner'); + + const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), { + encoding: 'utf-8', + }); + for await (const dirent of openDir) { + if (dirent.isFile()) { + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + } + + this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); + return contacts; + } catch (error) { + return []; + } } - } } diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index d9ccea99..efd57783 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -7,158 +7,145 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IMessageModel, MessageRaw } from '../models'; export class MessageQuery { - where: MessageRaw; - limit?: number; + where: MessageRaw; + limit?: number; } export class MessageRepository extends Repository { - constructor( - private readonly messageModel: IMessageModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('MessageRepository'); - - public async insert( - data: MessageRaw[], - instanceName: string, - saveDb = false, - ): Promise { - this.logger.verbose('inserting messages'); - - if (!Array.isArray(data) || data.length === 0) { - this.logger.verbose('no messages to insert'); - return; + constructor(private readonly messageModel: IMessageModel, private readonly configService: ConfigService) { + super(configService); } - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving messages to db'); - const cleanedData = data.map((obj) => { - const cleanedObj = { ...obj }; - if ('extendedTextMessage' in obj.message) { - const extendedTextMessage = obj.message.extendedTextMessage as { - contextInfo?: { - mentionedJid?: any; - }; - }; + private readonly logger = new Logger('MessageRepository'); - if (typeof extendedTextMessage === 'object' && extendedTextMessage !== null) { - if ('contextInfo' in extendedTextMessage) { - delete extendedTextMessage.contextInfo?.mentionedJid; - extendedTextMessage.contextInfo = {}; - } + public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting messages'); + + if (!Array.isArray(data) || data.length === 0) { + this.logger.verbose('no messages to insert'); + return; + } + + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving messages to db'); + const cleanedData = data.map((obj) => { + const cleanedObj = { ...obj }; + if ('extendedTextMessage' in obj.message) { + const extendedTextMessage = obj.message.extendedTextMessage as { + contextInfo?: { + mentionedJid?: any; + }; + }; + + if (typeof extendedTextMessage === 'object' && extendedTextMessage !== null) { + if ('contextInfo' in extendedTextMessage) { + delete extendedTextMessage.contextInfo?.mentionedJid; + extendedTextMessage.contextInfo = {}; + } + } + } + return cleanedObj; + }); + + const insert = await this.messageModel.insertMany([...cleanedData]); + + this.logger.verbose('messages saved to db: ' + insert.length + ' messages'); + return { insertCount: insert.length }; } - } - return cleanedObj; - }); - const insert = await this.messageModel.insertMany([...cleanedData]); + this.logger.verbose('saving messages to store'); - this.logger.verbose('messages saved to db: ' + insert.length + ' messages'); - return { insertCount: insert.length }; - } + const store = this.configService.get('STORE'); - this.logger.verbose('saving messages to store'); + if (store.MESSAGES) { + this.logger.verbose('saving messages to store'); - const store = this.configService.get('STORE'); + data.forEach((message) => { + this.writeStore({ + path: join(this.storePath, 'messages', instanceName), + fileName: message.key.id, + data: message, + }); + this.logger.verbose( + 'messages saved to store in path: ' + + join(this.storePath, 'messages', instanceName) + + '/' + + message.key.id, + ); + }); - if (store.MESSAGES) { - this.logger.verbose('saving messages to store'); + this.logger.verbose('messages saved to store: ' + data.length + ' messages'); + return { insertCount: data.length }; + } - data.forEach((message) => { - this.writeStore({ - path: join(this.storePath, 'messages', instanceName), - fileName: message.key.id, - data: message, - }); - this.logger.verbose( - 'messages saved to store in path: ' + - join(this.storePath, 'messages', instanceName) + - '/' + - message.key.id, - ); - }); - - this.logger.verbose('messages saved to store: ' + data.length + ' messages'); - return { insertCount: data.length }; - } - - this.logger.verbose('messages not saved to store'); - return { insertCount: 0 }; - } catch (error) { - console.log('ERROR: ', error); - return error; - } finally { - data = undefined; - } - } - - public async find(query: MessageQuery) { - try { - this.logger.verbose('finding messages'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding messages in db'); - if (query?.where?.key) { - for (const [k, v] of Object.entries(query.where.key)) { - query.where['key.' + k] = v; - } - delete query?.where?.key; + this.logger.verbose('messages not saved to store'); + return { insertCount: 0 }; + } catch (error) { + console.log('ERROR: ', error); + return error; + } finally { + data = undefined; + } + } + + public async find(query: MessageQuery) { + try { + this.logger.verbose('finding messages'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding messages in db'); + if (query?.where?.key) { + for (const [k, v] of Object.entries(query.where.key)) { + query.where['key.' + k] = v; + } + delete query?.where?.key; + } + + return await this.messageModel + .find({ ...query.where }) + .sort({ messageTimestamp: -1 }) + .limit(query?.limit ?? 0); + } + + this.logger.verbose('finding messages in store'); + const messages: MessageRaw[] = []; + if (query?.where?.key?.id) { + this.logger.verbose('finding messages in store by id'); + messages.push( + JSON.parse( + readFileSync( + join(this.storePath, 'messages', query.where.owner, query.where.key.id + '.json'), + { encoding: 'utf-8' }, + ), + ), + ); + } else { + this.logger.verbose('finding messages in store by owner'); + const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), { + encoding: 'utf-8', + }); + + for await (const dirent of openDir) { + if (dirent.isFile()) { + messages.push( + JSON.parse( + readFileSync(join(this.storePath, 'messages', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + } + + this.logger.verbose('messages found in store: ' + messages.length + ' messages'); + return messages + .sort((x, y) => { + return (y.messageTimestamp as number) - (x.messageTimestamp as number); + }) + .splice(0, query?.limit ?? messages.length); + } catch (error) { + return []; } - - return await this.messageModel - .find({ ...query.where }) - .sort({ messageTimestamp: -1 }) - .limit(query?.limit ?? 0); - } - - this.logger.verbose('finding messages in store'); - const messages: MessageRaw[] = []; - if (query?.where?.key?.id) { - this.logger.verbose('finding messages in store by id'); - messages.push( - JSON.parse( - readFileSync( - join( - this.storePath, - 'messages', - query.where.owner, - query.where.key.id + '.json', - ), - { encoding: 'utf-8' }, - ), - ), - ); - } else { - this.logger.verbose('finding messages in store by owner'); - const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), { - encoding: 'utf-8', - }); - - for await (const dirent of openDir) { - if (dirent.isFile()) { - messages.push( - JSON.parse( - readFileSync( - join(this.storePath, 'messages', query.where.owner, dirent.name), - { encoding: 'utf-8' }, - ), - ), - ); - } - } - } - - this.logger.verbose('messages found in store: ' + messages.length + ' messages'); - return messages - .sort((x, y) => { - return (y.messageTimestamp as number) - (x.messageTimestamp as number); - }) - .splice(0, query?.limit ?? messages.length); - } catch (error) { - return []; } - } } diff --git a/src/whatsapp/repository/messageUp.repository.ts b/src/whatsapp/repository/messageUp.repository.ts index fe2f623b..7a25467f 100644 --- a/src/whatsapp/repository/messageUp.repository.ts +++ b/src/whatsapp/repository/messageUp.repository.ts @@ -7,134 +7,117 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IMessageUpModel, MessageUpdateRaw } from '../models'; export class MessageUpQuery { - where: MessageUpdateRaw; - limit?: number; + where: MessageUpdateRaw; + limit?: number; } export class MessageUpRepository extends Repository { - constructor( - private readonly messageUpModel: IMessageUpModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('MessageUpRepository'); - - public async insert( - data: MessageUpdateRaw[], - instanceName: string, - saveDb?: boolean, - ): Promise { - this.logger.verbose('inserting message up'); - - if (data.length === 0) { - this.logger.verbose('no message up to insert'); - return; + constructor(private readonly messageUpModel: IMessageUpModel, private readonly configService: ConfigService) { + super(configService); } - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving message up to db'); - const insert = await this.messageUpModel.insertMany([...data]); + private readonly logger = new Logger('MessageUpRepository'); - this.logger.verbose('message up saved to db: ' + insert.length + ' message up'); - return { insertCount: insert.length }; - } + public async insert(data: MessageUpdateRaw[], instanceName: string, saveDb?: boolean): Promise { + this.logger.verbose('inserting message up'); - this.logger.verbose('saving message up to store'); - - const store = this.configService.get('STORE'); - - if (store.MESSAGE_UP) { - this.logger.verbose('saving message up to store'); - data.forEach((update) => { - this.writeStore({ - path: join(this.storePath, 'message-up', instanceName), - fileName: update.id, - data: update, - }); - this.logger.verbose( - 'message up saved to store in path: ' + - join(this.storePath, 'message-up', instanceName) + - '/' + - update.id, - ); - }); - - this.logger.verbose('message up saved to store: ' + data.length + ' message up'); - return { insertCount: data.length }; - } - - this.logger.verbose('message up not saved to store'); - return { insertCount: 0 }; - } catch (error) { - return error; - } - } - - public async find(query: MessageUpQuery) { - try { - this.logger.verbose('finding message up'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding message up in db'); - return await this.messageUpModel - .find({ ...query.where }) - .sort({ datetime: -1 }) - .limit(query?.limit ?? 0); - } - - this.logger.verbose('finding message up in store'); - - const messageUpdate: MessageUpdateRaw[] = []; - if (query?.where?.id) { - this.logger.verbose('finding message up in store by id'); - - messageUpdate.push( - JSON.parse( - readFileSync( - join( - this.storePath, - 'message-up', - query.where.owner, - query.where.id + '.json', - ), - { encoding: 'utf-8' }, - ), - ), - ); - } else { - this.logger.verbose('finding message up in store by owner'); - - const openDir = opendirSync( - join(this.storePath, 'message-up', query.where.owner), - { encoding: 'utf-8' }, - ); - - for await (const dirent of openDir) { - if (dirent.isFile()) { - messageUpdate.push( - JSON.parse( - readFileSync( - join(this.storePath, 'message-up', query.where.owner, dirent.name), - { encoding: 'utf-8' }, - ), - ), - ); - } + if (data.length === 0) { + this.logger.verbose('no message up to insert'); + return; } - } - this.logger.verbose( - 'message up found in store: ' + messageUpdate.length + ' message up', - ); - return messageUpdate - .sort((x, y) => { - return y.datetime - x.datetime; - }) - .splice(0, query?.limit ?? messageUpdate.length); - } catch (error) { - return []; + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving message up to db'); + const insert = await this.messageUpModel.insertMany([...data]); + + this.logger.verbose('message up saved to db: ' + insert.length + ' message up'); + return { insertCount: insert.length }; + } + + this.logger.verbose('saving message up to store'); + + const store = this.configService.get('STORE'); + + if (store.MESSAGE_UP) { + this.logger.verbose('saving message up to store'); + data.forEach((update) => { + this.writeStore({ + path: join(this.storePath, 'message-up', instanceName), + fileName: update.id, + data: update, + }); + this.logger.verbose( + 'message up saved to store in path: ' + + join(this.storePath, 'message-up', instanceName) + + '/' + + update.id, + ); + }); + + this.logger.verbose('message up saved to store: ' + data.length + ' message up'); + return { insertCount: data.length }; + } + + this.logger.verbose('message up not saved to store'); + return { insertCount: 0 }; + } catch (error) { + return error; + } + } + + public async find(query: MessageUpQuery) { + try { + this.logger.verbose('finding message up'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding message up in db'); + return await this.messageUpModel + .find({ ...query.where }) + .sort({ datetime: -1 }) + .limit(query?.limit ?? 0); + } + + this.logger.verbose('finding message up in store'); + + const messageUpdate: MessageUpdateRaw[] = []; + if (query?.where?.id) { + this.logger.verbose('finding message up in store by id'); + + messageUpdate.push( + JSON.parse( + readFileSync(join(this.storePath, 'message-up', query.where.owner, query.where.id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + } else { + this.logger.verbose('finding message up in store by owner'); + + const openDir = opendirSync(join(this.storePath, 'message-up', query.where.owner), { + encoding: 'utf-8', + }); + + for await (const dirent of openDir) { + if (dirent.isFile()) { + messageUpdate.push( + JSON.parse( + readFileSync(join(this.storePath, 'message-up', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + } + + this.logger.verbose('message up found in store: ' + messageUpdate.length + ' message up'); + return messageUpdate + .sort((x, y) => { + return y.datetime - x.datetime; + }) + .splice(0, query?.limit ?? messageUpdate.length); + } catch (error) { + return []; + } } - } } diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index eea039a9..57a206b8 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -13,104 +13,100 @@ import { MessageUpRepository } from './messageUp.repository'; import { SettingsRepository } from './settings.repository'; import { WebhookRepository } from './webhook.repository'; export class RepositoryBroker { - constructor( - public readonly message: MessageRepository, - public readonly chat: ChatRepository, - public readonly contact: ContactRepository, - public readonly messageUpdate: MessageUpRepository, - public readonly webhook: WebhookRepository, - public readonly chatwoot: ChatwootRepository, - public readonly settings: SettingsRepository, - public readonly auth: AuthRepository, - private configService: ConfigService, - dbServer?: MongoClient, - ) { - this.dbClient = dbServer; - this.__init_repo_without_db__(); - } - - private dbClient?: MongoClient; - private readonly logger = new Logger('RepositoryBroker'); - - public get dbServer() { - return this.dbClient; - } - - private __init_repo_without_db__() { - this.logger.verbose('initializing repository without db'); - if (!this.configService.get('DATABASE').ENABLED) { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - try { - const authDir = join( - storePath, - 'auth', - this.configService.get('AUTHENTICATION').TYPE, - ); - const chatsDir = join(storePath, 'chats'); - const contactsDir = join(storePath, 'contacts'); - const messagesDir = join(storePath, 'messages'); - const messageUpDir = join(storePath, 'message-up'); - const webhookDir = join(storePath, 'webhook'); - const chatwootDir = join(storePath, 'chatwoot'); - const settingsDir = join(storePath, 'settings'); - const tempDir = join(storePath, 'temp'); - - if (!fs.existsSync(authDir)) { - this.logger.verbose('creating auth dir: ' + authDir); - fs.mkdirSync(authDir, { recursive: true }); - } - if (!fs.existsSync(chatsDir)) { - this.logger.verbose('creating chats dir: ' + chatsDir); - fs.mkdirSync(chatsDir, { recursive: true }); - } - if (!fs.existsSync(contactsDir)) { - this.logger.verbose('creating contacts dir: ' + contactsDir); - fs.mkdirSync(contactsDir, { recursive: true }); - } - if (!fs.existsSync(messagesDir)) { - this.logger.verbose('creating messages dir: ' + messagesDir); - fs.mkdirSync(messagesDir, { recursive: true }); - } - if (!fs.existsSync(messageUpDir)) { - this.logger.verbose('creating message-up dir: ' + messageUpDir); - fs.mkdirSync(messageUpDir, { recursive: true }); - } - if (!fs.existsSync(webhookDir)) { - this.logger.verbose('creating webhook dir: ' + webhookDir); - fs.mkdirSync(webhookDir, { recursive: true }); - } - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(settingsDir)) { - this.logger.verbose('creating settings dir: ' + settingsDir); - fs.mkdirSync(settingsDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } - } else { - try { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - - const tempDir = join(storePath, 'temp'); - - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } + constructor( + public readonly message: MessageRepository, + public readonly chat: ChatRepository, + public readonly contact: ContactRepository, + public readonly messageUpdate: MessageUpRepository, + public readonly webhook: WebhookRepository, + public readonly chatwoot: ChatwootRepository, + public readonly settings: SettingsRepository, + public readonly auth: AuthRepository, + private configService: ConfigService, + dbServer?: MongoClient, + ) { + this.dbClient = dbServer; + this.__init_repo_without_db__(); + } + + private dbClient?: MongoClient; + private readonly logger = new Logger('RepositoryBroker'); + + public get dbServer() { + return this.dbClient; + } + + private __init_repo_without_db__() { + this.logger.verbose('initializing repository without db'); + if (!this.configService.get('DATABASE').ENABLED) { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + try { + const authDir = join(storePath, 'auth', this.configService.get('AUTHENTICATION').TYPE); + const chatsDir = join(storePath, 'chats'); + const contactsDir = join(storePath, 'contacts'); + const messagesDir = join(storePath, 'messages'); + const messageUpDir = join(storePath, 'message-up'); + const webhookDir = join(storePath, 'webhook'); + const chatwootDir = join(storePath, 'chatwoot'); + const settingsDir = join(storePath, 'settings'); + const tempDir = join(storePath, 'temp'); + + if (!fs.existsSync(authDir)) { + this.logger.verbose('creating auth dir: ' + authDir); + fs.mkdirSync(authDir, { recursive: true }); + } + if (!fs.existsSync(chatsDir)) { + this.logger.verbose('creating chats dir: ' + chatsDir); + fs.mkdirSync(chatsDir, { recursive: true }); + } + if (!fs.existsSync(contactsDir)) { + this.logger.verbose('creating contacts dir: ' + contactsDir); + fs.mkdirSync(contactsDir, { recursive: true }); + } + if (!fs.existsSync(messagesDir)) { + this.logger.verbose('creating messages dir: ' + messagesDir); + fs.mkdirSync(messagesDir, { recursive: true }); + } + if (!fs.existsSync(messageUpDir)) { + this.logger.verbose('creating message-up dir: ' + messageUpDir); + fs.mkdirSync(messageUpDir, { recursive: true }); + } + if (!fs.existsSync(webhookDir)) { + this.logger.verbose('creating webhook dir: ' + webhookDir); + fs.mkdirSync(webhookDir, { recursive: true }); + } + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(settingsDir)) { + this.logger.verbose('creating settings dir: ' + settingsDir); + fs.mkdirSync(settingsDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } else { + try { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } } - } } diff --git a/src/whatsapp/repository/settings.repository.ts b/src/whatsapp/repository/settings.repository.ts index 704a3d7d..96003d69 100644 --- a/src/whatsapp/repository/settings.repository.ts +++ b/src/whatsapp/repository/settings.repository.ts @@ -7,70 +7,58 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ISettingsModel, SettingsRaw } from '../models'; export class SettingsRepository extends Repository { - constructor( - private readonly settingsModel: ISettingsModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('SettingsRepository'); - - public async create(data: SettingsRaw, instance: string): Promise { - try { - this.logger.verbose('creating settings'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving settings to db'); - const insert = await this.settingsModel.replaceOne( - { _id: instance }, - { ...data }, - { upsert: true }, - ); - - this.logger.verbose( - 'settings saved to db: ' + insert.modifiedCount + ' settings', - ); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving settings to store'); - - this.writeStore({ - path: join(this.storePath, 'settings'), - fileName: instance, - data, - }); - - this.logger.verbose( - 'settings saved to store in path: ' + - join(this.storePath, 'settings') + - '/' + - instance, - ); - - this.logger.verbose('settings created'); - return { insertCount: 1 }; - } catch (error) { - return error; + constructor(private readonly settingsModel: ISettingsModel, private readonly configService: ConfigService) { + super(configService); } - } - public async find(instance: string): Promise { - try { - this.logger.verbose('finding settings'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding settings in db'); - return await this.settingsModel.findOne({ _id: instance }); - } + private readonly logger = new Logger('SettingsRepository'); - this.logger.verbose('finding settings in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'settings', instance + '.json'), { - encoding: 'utf-8', - }), - ) as SettingsRaw; - } catch (error) { - return {}; + public async create(data: SettingsRaw, instance: string): Promise { + try { + this.logger.verbose('creating settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving settings to db'); + const insert = await this.settingsModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('settings saved to db: ' + insert.modifiedCount + ' settings'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving settings to store'); + + this.writeStore({ + path: join(this.storePath, 'settings'), + fileName: instance, + data, + }); + + this.logger.verbose( + 'settings saved to store in path: ' + join(this.storePath, 'settings') + '/' + instance, + ); + + this.logger.verbose('settings created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding settings in db'); + return await this.settingsModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding settings in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'settings', instance + '.json'), { + encoding: 'utf-8', + }), + ) as SettingsRaw; + } catch (error) { + return {}; + } } - } } diff --git a/src/whatsapp/repository/webhook.repository.ts b/src/whatsapp/repository/webhook.repository.ts index dc6d9154..f3e9d12f 100644 --- a/src/whatsapp/repository/webhook.repository.ts +++ b/src/whatsapp/repository/webhook.repository.ts @@ -7,68 +7,56 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IWebhookModel, WebhookRaw } from '../models'; export class WebhookRepository extends Repository { - constructor( - private readonly webhookModel: IWebhookModel, - private readonly configService: ConfigService, - ) { - super(configService); - } - - private readonly logger = new Logger('WebhookRepository'); - - public async create(data: WebhookRaw, instance: string): Promise { - try { - this.logger.verbose('creating webhook'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving webhook to db'); - const insert = await this.webhookModel.replaceOne( - { _id: instance }, - { ...data }, - { upsert: true }, - ); - - this.logger.verbose('webhook saved to db: ' + insert.modifiedCount + ' webhook'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving webhook to store'); - - this.writeStore({ - path: join(this.storePath, 'webhook'), - fileName: instance, - data, - }); - - this.logger.verbose( - 'webhook saved to store in path: ' + - join(this.storePath, 'webhook') + - '/' + - instance, - ); - - this.logger.verbose('webhook created'); - return { insertCount: 1 }; - } catch (error) { - return error; + constructor(private readonly webhookModel: IWebhookModel, private readonly configService: ConfigService) { + super(configService); } - } - public async find(instance: string): Promise { - try { - this.logger.verbose('finding webhook'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding webhook in db'); - return await this.webhookModel.findOne({ _id: instance }); - } + private readonly logger = new Logger('WebhookRepository'); - this.logger.verbose('finding webhook in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'webhook', instance + '.json'), { - encoding: 'utf-8', - }), - ) as WebhookRaw; - } catch (error) { - return {}; + public async create(data: WebhookRaw, instance: string): Promise { + try { + this.logger.verbose('creating webhook'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving webhook to db'); + const insert = await this.webhookModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('webhook saved to db: ' + insert.modifiedCount + ' webhook'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving webhook to store'); + + this.writeStore({ + path: join(this.storePath, 'webhook'), + fileName: instance, + data, + }); + + this.logger.verbose('webhook saved to store in path: ' + join(this.storePath, 'webhook') + '/' + instance); + + this.logger.verbose('webhook created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding webhook'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding webhook in db'); + return await this.webhookModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding webhook in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'webhook', instance + '.json'), { + encoding: 'utf-8', + }), + ) as WebhookRaw; + } catch (error) { + return {}; + } } - } } diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index ecc8ebb6..ee7e972f 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -1,33 +1,32 @@ -import { proto } from '@whiskeysockets/baileys'; import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - archiveChatSchema, - contactValidateSchema, - deleteMessageSchema, - messageUpSchema, - messageValidateSchema, - privacySettingsSchema, - profileNameSchema, - profilePictureSchema, - profileSchema, - profileStatusSchema, - readMessageSchema, - whatsappNumberSchema, + archiveChatSchema, + contactValidateSchema, + deleteMessageSchema, + messageUpSchema, + messageValidateSchema, + privacySettingsSchema, + profileNameSchema, + profilePictureSchema, + profileSchema, + profileStatusSchema, + readMessageSchema, + whatsappNumberSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberDto, - PrivacySettingDto, - ProfileNameDto, - ProfilePictureDto, - ProfileStatusDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberDto, + PrivacySettingDto, + ProfileNameDto, + ProfilePictureDto, + ProfileStatusDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -39,326 +38,317 @@ import { HttpStatus } from './index.router'; const logger = new Logger('ChatRouter'); export class ChatRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { - logger.verbose('request received in whatsappNumbers'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { + logger.verbose('request received in whatsappNumbers'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: whatsappNumberSchema, - ClassRef: WhatsAppNumberDto, - execute: (instance, data) => chatController.whatsappNumber(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: whatsappNumberSchema, + ClassRef: WhatsAppNumberDto, + execute: (instance, data) => chatController.whatsappNumber(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { - logger.verbose('request received in markMessageAsRead'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { + logger.verbose('request received in markMessageAsRead'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: readMessageSchema, - ClassRef: ReadMessageDto, - execute: (instance, data) => chatController.readMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: readMessageSchema, + ClassRef: ReadMessageDto, + execute: (instance, data) => chatController.readMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('archiveChat'), ...guards, async (req, res) => { - logger.verbose('request received in archiveChat'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('archiveChat'), ...guards, async (req, res) => { + logger.verbose('request received in archiveChat'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: archiveChatSchema, - ClassRef: ArchiveChatDto, - execute: (instance, data) => chatController.archiveChat(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: archiveChatSchema, + ClassRef: ArchiveChatDto, + execute: (instance, data) => chatController.archiveChat(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .delete( - this.routerPath('deleteMessageForEveryone'), - ...guards, - async (req, res) => { - logger.verbose('request received in deleteMessageForEveryone'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .delete(this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => { + logger.verbose('request received in deleteMessageForEveryone'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: deleteMessageSchema, - ClassRef: DeleteMessage, - execute: (instance, data) => chatController.deleteMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: deleteMessageSchema, + ClassRef: DeleteMessage, + execute: (instance, data) => chatController.deleteMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }, - ) - .post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => { - logger.verbose('request received in fetchProfilePictureUrl'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfilePictureUrl'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchProfilePicture(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfilePicture(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { - logger.verbose('request received in fetchProfile'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchProfile(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfile(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('findContacts'), ...guards, async (req, res) => { - logger.verbose('request received in findContacts'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('findContacts'), ...guards, async (req, res) => { + logger.verbose('request received in findContacts'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: contactValidateSchema, - ClassRef: ContactQuery, - execute: (instance, data) => chatController.fetchContacts(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: contactValidateSchema, + ClassRef: ContactQuery, + execute: (instance, data) => chatController.fetchContacts(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => { - logger.verbose('request received in getBase64FromMediaMessage'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => { + logger.verbose('request received in getBase64FromMediaMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: getBase64FromMediaMessageDto, - execute: (instance, data) => - chatController.getBase64FromMediaMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: getBase64FromMediaMessageDto, + execute: (instance, data) => chatController.getBase64FromMediaMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('findMessages'), ...guards, async (req, res) => { - logger.verbose('request received in findMessages'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('findMessages'), ...guards, async (req, res) => { + logger.verbose('request received in findMessages'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: messageValidateSchema, - ClassRef: MessageQuery, - execute: (instance, data) => chatController.fetchMessages(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: messageValidateSchema, + ClassRef: MessageQuery, + execute: (instance, data) => chatController.fetchMessages(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => { - logger.verbose('request received in findStatusMessage'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => { + logger.verbose('request received in findStatusMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: messageUpSchema, - ClassRef: MessageUpQuery, - execute: (instance, data) => chatController.fetchStatusMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: messageUpSchema, + ClassRef: MessageUpQuery, + execute: (instance, data) => chatController.fetchStatusMessage(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('findChats'), ...guards, async (req, res) => { - logger.verbose('request received in findChats'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('findChats'), ...guards, async (req, res) => { + logger.verbose('request received in findChats'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => chatController.fetchChats(instance), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => chatController.fetchChats(instance), + }); - return res.status(HttpStatus.OK).json(response); - }) - // Profile routes - .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { - logger.verbose('request received in fetchPrivacySettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + // Profile routes + .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in fetchPrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => chatController.fetchPrivacySettings(instance), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => chatController.fetchPrivacySettings(instance), + }); - return res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => { - logger.verbose('request received in updatePrivacySettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in updatePrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: privacySettingsSchema, - ClassRef: PrivacySettingDto, - execute: (instance, data) => - chatController.updatePrivacySettings(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: privacySettingsSchema, + ClassRef: PrivacySettingDto, + execute: (instance, data) => chatController.updatePrivacySettings(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => { - logger.verbose('request received in fetchBusinessProfile'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchBusinessProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance, data) => - chatController.fetchBusinessProfile(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance, data) => chatController.fetchBusinessProfile(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfileName'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileName'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileNameSchema, - ClassRef: ProfileNameDto, - execute: (instance, data) => chatController.updateProfileName(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileNameSchema, + ClassRef: ProfileNameDto, + execute: (instance, data) => chatController.updateProfileName(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfileStatus'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileStatusSchema, - ClassRef: ProfileStatusDto, - execute: (instance, data) => chatController.updateProfileStatus(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileStatusSchema, + ClassRef: ProfileStatusDto, + execute: (instance, data) => chatController.updateProfileStatus(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfilePicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance, data) => - chatController.updateProfilePicture(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance, data) => chatController.updateProfilePicture(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => { - logger.verbose('request received in removeProfilePicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in removeProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance, data) => - chatController.removeProfilePicture(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance) => chatController.removeProfilePicture(instance), + }); - return res.status(HttpStatus.OK).json(response); - }); - } + return res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index 74e47552..6527099e 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -12,58 +12,58 @@ import { HttpStatus } from './index.router'; const logger = new Logger('ChatwootRouter'); export class ChatwootRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: chatwootSchema, - ClassRef: ChatwootDto, - execute: (instance, data) => chatwootController.createChatwoot(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: chatwootSchema, + ClassRef: ChatwootDto, + execute: (instance, data) => chatwootController.createChatwoot(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findChatwoot'); + 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) => chatwootController.findChatwoot(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => chatwootController.findChatwoot(instance), + }); - res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('webhook'), async (req, res) => { - logger.verbose('request received in findChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('webhook'), async (req, res) => { + logger.verbose('request received in findChatwoot'); + 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, data) => chatwootController.receiveWebhook(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance, data) => chatwootController.receiveWebhook(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 7d95ef10..33c22f9b 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -2,31 +2,31 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - createGroupSchema, - getParticipantsSchema, - groupInviteSchema, - groupJidSchema, - groupSendInviteSchema, - toggleEphemeralSchema, - updateGroupDescriptionSchema, - updateGroupPictureSchema, - updateGroupSubjectSchema, - updateParticipantsSchema, - updateSettingsSchema, + createGroupSchema, + getParticipantsSchema, + groupInviteSchema, + groupJidSchema, + groupSendInviteSchema, + toggleEphemeralSchema, + updateGroupDescriptionSchema, + updateGroupPictureSchema, + updateGroupSubjectSchema, + updateParticipantsSchema, + updateSettingsSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { groupController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -34,252 +34,251 @@ import { HttpStatus } from './index.router'; const logger = new Logger('GroupRouter'); export class GroupRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('create'), ...guards, async (req, res) => { - logger.verbose('request received in createGroup'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('create'), ...guards, async (req, res) => { + logger.verbose('request received in createGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: createGroupSchema, - ClassRef: CreateGroupDto, - execute: (instance, data) => groupController.createGroup(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: createGroupSchema, + ClassRef: CreateGroupDto, + execute: (instance, data) => groupController.createGroup(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupSubject'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupSubject'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupSubjectSchema, - ClassRef: GroupSubjectDto, - execute: (instance, data) => groupController.updateGroupSubject(instance, data), - }); + const response = await this.groupValidate({ + request: req, + schema: updateGroupSubjectSchema, + ClassRef: GroupSubjectDto, + execute: (instance, data) => groupController.updateGroupSubject(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupPicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupPicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupPictureSchema, - ClassRef: GroupPictureDto, - execute: (instance, data) => groupController.updateGroupPicture(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateGroupPictureSchema, + ClassRef: GroupPictureDto, + execute: (instance, data) => groupController.updateGroupPicture(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupDescription'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupDescription'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupDescriptionSchema, - ClassRef: GroupDescriptionDto, - execute: (instance, data) => - groupController.updateGroupDescription(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateGroupDescriptionSchema, + ClassRef: GroupDescriptionDto, + execute: (instance, data) => groupController.updateGroupDescription(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { - logger.verbose('request received in findGroupInfos'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { + logger.verbose('request received in findGroupInfos'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.findGroupInfo(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.findGroupInfo(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => { - logger.verbose('request received in fetchAllGroups'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => { + logger.verbose('request received in fetchAllGroups'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.getParticipantsValidate({ - request: req, - schema: getParticipantsSchema, - ClassRef: GetParticipant, - execute: (instance, data) => groupController.fetchAllGroups(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.getParticipantsValidate({ + request: req, + schema: getParticipantsSchema, + ClassRef: GetParticipant, + execute: (instance, data) => groupController.fetchAllGroups(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('participants'), ...guards, async (req, res) => { - logger.verbose('request received in participants'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('participants'), ...guards, async (req, res) => { + logger.verbose('request received in participants'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.findParticipants(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.findParticipants(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('inviteCode'), ...guards, async (req, res) => { - logger.verbose('request received in inviteCode'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('inviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in inviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.inviteCode(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.inviteCode(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('inviteInfo'), ...guards, async (req, res) => { - logger.verbose('request received in inviteInfo'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('inviteInfo'), ...guards, async (req, res) => { + logger.verbose('request received in inviteInfo'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.inviteCodeValidate({ - request: req, - schema: groupInviteSchema, - ClassRef: GroupInvite, - execute: (instance, data) => groupController.inviteInfo(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.inviteCodeValidate({ + request: req, + schema: groupInviteSchema, + ClassRef: GroupInvite, + execute: (instance, data) => groupController.inviteInfo(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { - logger.verbose('request received in sendInvite'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { + logger.verbose('request received in sendInvite'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupNoValidate({ - request: req, - schema: groupSendInviteSchema, - ClassRef: GroupSendInvite, - execute: (instance, data) => groupController.sendInvite(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupNoValidate({ + request: req, + schema: groupSendInviteSchema, + ClassRef: GroupSendInvite, + execute: (instance, data) => groupController.sendInvite(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { - logger.verbose('request received in revokeInviteCode'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in revokeInviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.revokeInviteCode(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.revokeInviteCode(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateParticipant'), ...guards, async (req, res) => { - logger.verbose('request received in updateParticipant'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateParticipant'), ...guards, async (req, res) => { + logger.verbose('request received in updateParticipant'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateParticipantsSchema, - ClassRef: GroupUpdateParticipantDto, - execute: (instance, data) => groupController.updateGParticipate(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateParticipantsSchema, + ClassRef: GroupUpdateParticipantDto, + execute: (instance, data) => groupController.updateGParticipate(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateSetting'), ...guards, async (req, res) => { - logger.verbose('request received in updateSetting'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateSetting'), ...guards, async (req, res) => { + logger.verbose('request received in updateSetting'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateSettingsSchema, - ClassRef: GroupUpdateSettingDto, - execute: (instance, data) => groupController.updateGSetting(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateSettingsSchema, + ClassRef: GroupUpdateSettingDto, + execute: (instance, data) => groupController.updateGSetting(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => { - logger.verbose('request received in toggleEphemeral'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => { + logger.verbose('request received in toggleEphemeral'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: toggleEphemeralSchema, - ClassRef: GroupToggleEphemeralDto, - execute: (instance, data) => groupController.toggleEphemeral(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: toggleEphemeralSchema, + ClassRef: GroupToggleEphemeralDto, + execute: (instance, data) => groupController.toggleEphemeral(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { - logger.verbose('request received in leaveGroup'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { + logger.verbose('request received in leaveGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: {}, - ClassRef: GroupJid, - execute: (instance, data) => groupController.leaveGroup(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: {}, + ClassRef: GroupJid, + execute: (instance, data) => groupController.leaveGroup(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 3cdcced7..941efeda 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -14,13 +14,13 @@ import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; enum HttpStatus { - OK = 200, - CREATED = 201, - NOT_FOUND = 404, - FORBIDDEN = 403, - BAD_REQUEST = 400, - UNAUTHORIZED = 401, - INTERNAL_SERVER_ERROR = 500, + OK = 200, + CREATED = 201, + NOT_FOUND = 404, + FORBIDDEN = 403, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + INTERNAL_SERVER_ERROR = 500, } const router = Router(); @@ -30,23 +30,19 @@ const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]]; const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); router - .get('/', (req, res) => { - res.status(HttpStatus.OK).json({ - status: HttpStatus.OK, - message: 'Welcome to the Evolution API, it is working!', - version: packageJson.version, - }); - }) - .use( - '/instance', - new InstanceRouter(configService, ...guards).router, - new ViewsRouter(instanceExistsGuard).router, - ) - .use('/message', new MessageRouter(...guards).router) - .use('/chat', new ChatRouter(...guards).router) - .use('/group', new GroupRouter(...guards).router) - .use('/webhook', new WebhookRouter(...guards).router) - .use('/chatwoot', new ChatwootRouter(...guards).router) - .use('/settings', new SettingsRouter(...guards).router); + .get('/', (req, res) => { + res.status(HttpStatus.OK).json({ + status: HttpStatus.OK, + message: 'Welcome to the Evolution API, it is working!', + version: packageJson.version, + }); + }) + .use('/instance', new InstanceRouter(configService, ...guards).router, new ViewsRouter(instanceExistsGuard).router) + .use('/message', new MessageRouter(...guards).router) + .use('/chat', new ChatRouter(...guards).router) + .use('/group', new GroupRouter(...guards).router) + .use('/webhook', new WebhookRouter(...guards).router) + .use('/chatwoot', new ChatwootRouter(...guards).router) + .use('/settings', new SettingsRouter(...guards).router); export { HttpStatus, router }; diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index e3dbd4a9..2c0f6a38 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -13,169 +13,165 @@ import { HttpStatus } from './index.router'; const logger = new Logger('InstanceRouter'); export class InstanceRouter extends RouterBroker { - constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { - super(); - const auth = configService.get('AUTHENTICATION'); - this.router - .post('/create', ...guards, async (req, res) => { - logger.verbose('request received in createInstance'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { + super(); + const auth = configService.get('AUTHENTICATION'); + this.router + .post('/create', ...guards, async (req, res) => { + logger.verbose('request received in createInstance'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: instanceNameSchema, - ClassRef: InstanceDto, - execute: (instance) => instanceController.createInstance(instance), + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => instanceController.createInstance(instance), + }); + + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('restart'), ...guards, async (req, res) => { + logger.verbose('request received in restartInstance'); + 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) => instanceController.restartInstance(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('connect'), ...guards, async (req, res) => { + logger.verbose('request received in connectInstance'); + 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) => instanceController.connectToWhatsapp(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('connectionState'), ...guards, async (req, res) => { + logger.verbose('request received in connectionState'); + 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) => instanceController.connectionState(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { + logger.verbose('request received in fetchInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => instanceController.fetchInstances(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('logout'), ...guards, async (req, res) => { + logger.verbose('request received in logoutInstances'); + 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) => instanceController.logout(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('delete'), ...guards, async (req, res) => { + logger.verbose('request received in deleteInstances'); + 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) => instanceController.deleteInstance(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }); + + if (auth.TYPE === 'jwt') { + this.router.put('/refreshToken', async (req, res) => { + logger.verbose('request received in refreshToken'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: oldTokenSchema, + ClassRef: OldToken, + execute: (_, data) => instanceController.refreshToken(_, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }); + } + + this.router.delete('/deleteDatabase', async (req, res) => { + logger.verbose('request received in deleteDatabase'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const db = this.configService.get('DATABASE'); + if (db.ENABLED) { + try { + await dbserver.dropDatabase(); + return res.status(HttpStatus.CREATED).json({ error: false, message: 'Database deleted' }); + } catch (error) { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message }); + } + } + + return res + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .json({ error: true, message: 'Database is not enabled' }); }); - - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('restart'), ...guards, async (req, res) => { - logger.verbose('request received in restartInstance'); - 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) => instanceController.restartInstance(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('connect'), ...guards, async (req, res) => { - logger.verbose('request received in connectInstance'); - 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) => instanceController.connectToWhatsapp(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('connectionState'), ...guards, async (req, res) => { - logger.verbose('request received in connectionState'); - 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) => instanceController.connectionState(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { - logger.verbose('request received in fetchInstances'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => instanceController.fetchInstances(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('logout'), ...guards, async (req, res) => { - logger.verbose('request received in logoutInstances'); - 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) => instanceController.logout(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('delete'), ...guards, async (req, res) => { - logger.verbose('request received in deleteInstances'); - 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) => instanceController.deleteInstance(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }); - - if (auth.TYPE === 'jwt') { - this.router.put('/refreshToken', async (req, res) => { - logger.verbose('request received in refreshToken'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: oldTokenSchema, - ClassRef: OldToken, - execute: (_, data) => instanceController.refreshToken(_, data), - }); - - return res.status(HttpStatus.CREATED).json(response); - }); } - this.router.delete('/deleteDatabase', async (req, res) => { - logger.verbose('request received in deleteDatabase'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const db = this.configService.get('DATABASE'); - if (db.ENABLED) { - try { - await dbserver.dropDatabase(); - return res - .status(HttpStatus.CREATED) - .json({ error: false, message: 'Database deleted' }); - } catch (error) { - return res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ error: true, message: error.message }); - } - } - - return res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ error: true, message: 'Database is not enabled' }); - }); - } - - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index ae79d387..4550ff8b 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -2,31 +2,31 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - audioMessageSchema, - buttonMessageSchema, - contactMessageSchema, - listMessageSchema, - locationMessageSchema, - mediaMessageSchema, - pollMessageSchema, - reactionMessageSchema, - statusMessageSchema, - stickerMessageSchema, - textMessageSchema, + audioMessageSchema, + buttonMessageSchema, + contactMessageSchema, + listMessageSchema, + locationMessageSchema, + mediaMessageSchema, + pollMessageSchema, + reactionMessageSchema, + statusMessageSchema, + stickerMessageSchema, + textMessageSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, } from '../dto/sendMessage.dto'; import { sendMessageController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -34,187 +34,186 @@ import { HttpStatus } from './index.router'; const logger = new Logger('MessageRouter'); export class MessageRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('sendText'), ...guards, async (req, res) => { - logger.verbose('request received in sendText'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('sendText'), ...guards, async (req, res) => { + logger.verbose('request received in sendText'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: textMessageSchema, - ClassRef: SendTextDto, - execute: (instance, data) => sendMessageController.sendText(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: textMessageSchema, + ClassRef: SendTextDto, + execute: (instance, data) => sendMessageController.sendText(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendMedia'), ...guards, async (req, res) => { - logger.verbose('request received in sendMedia'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendMedia'), ...guards, async (req, res) => { + logger.verbose('request received in sendMedia'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: mediaMessageSchema, - ClassRef: SendMediaDto, - execute: (instance, data) => sendMessageController.sendMedia(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: mediaMessageSchema, + ClassRef: SendMediaDto, + execute: (instance, data) => sendMessageController.sendMedia(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => { - logger.verbose('request received in sendWhatsAppAudio'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => { + logger.verbose('request received in sendWhatsAppAudio'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: audioMessageSchema, - ClassRef: SendMediaDto, - execute: (instance, data) => - sendMessageController.sendWhatsAppAudio(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: audioMessageSchema, + ClassRef: SendMediaDto, + execute: (instance, data) => sendMessageController.sendWhatsAppAudio(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendButtons'), ...guards, async (req, res) => { - logger.verbose('request received in sendButtons'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendButtons'), ...guards, async (req, res) => { + logger.verbose('request received in sendButtons'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: buttonMessageSchema, - ClassRef: SendButtonDto, - execute: (instance, data) => sendMessageController.sendButtons(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: buttonMessageSchema, + ClassRef: SendButtonDto, + execute: (instance, data) => sendMessageController.sendButtons(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendLocation'), ...guards, async (req, res) => { - logger.verbose('request received in sendLocation'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendLocation'), ...guards, async (req, res) => { + logger.verbose('request received in sendLocation'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: locationMessageSchema, - ClassRef: SendLocationDto, - execute: (instance, data) => sendMessageController.sendLocation(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: locationMessageSchema, + ClassRef: SendLocationDto, + execute: (instance, data) => sendMessageController.sendLocation(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendList'), ...guards, async (req, res) => { - logger.verbose('request received in sendList'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendList'), ...guards, async (req, res) => { + logger.verbose('request received in sendList'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: listMessageSchema, - ClassRef: SendListDto, - execute: (instance, data) => sendMessageController.sendList(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: listMessageSchema, + ClassRef: SendListDto, + execute: (instance, data) => sendMessageController.sendList(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendContact'), ...guards, async (req, res) => { - logger.verbose('request received in sendContact'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendContact'), ...guards, async (req, res) => { + logger.verbose('request received in sendContact'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: contactMessageSchema, - ClassRef: SendContactDto, - execute: (instance, data) => sendMessageController.sendContact(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: contactMessageSchema, + ClassRef: SendContactDto, + execute: (instance, data) => sendMessageController.sendContact(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendReaction'), ...guards, async (req, res) => { - logger.verbose('request received in sendReaction'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendReaction'), ...guards, async (req, res) => { + logger.verbose('request received in sendReaction'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: reactionMessageSchema, - ClassRef: SendReactionDto, - execute: (instance, data) => sendMessageController.sendReaction(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: reactionMessageSchema, + ClassRef: SendReactionDto, + execute: (instance, data) => sendMessageController.sendReaction(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendPoll'), ...guards, async (req, res) => { - logger.verbose('request received in sendPoll'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendPoll'), ...guards, async (req, res) => { + logger.verbose('request received in sendPoll'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: pollMessageSchema, - ClassRef: SendPollDto, - execute: (instance, data) => sendMessageController.sendPoll(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: pollMessageSchema, + ClassRef: SendPollDto, + execute: (instance, data) => sendMessageController.sendPoll(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendStatus'), ...guards, async (req, res) => { - logger.verbose('request received in sendStatus'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendStatus'), ...guards, async (req, res) => { + logger.verbose('request received in sendStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: statusMessageSchema, - ClassRef: SendStatusDto, - execute: (instance, data) => sendMessageController.sendStatus(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: statusMessageSchema, + ClassRef: SendStatusDto, + execute: (instance, data) => sendMessageController.sendStatus(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { - logger.verbose('request received in sendSticker'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { + logger.verbose('request received in sendSticker'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: stickerMessageSchema, - ClassRef: SendStickerDto, - execute: (instance, data) => sendMessageController.sendSticker(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: stickerMessageSchema, + ClassRef: SendStickerDto, + execute: (instance, data) => sendMessageController.sendSticker(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }); - } + return res.status(HttpStatus.CREATED).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts index c45dd49b..feb37cd0 100644 --- a/src/whatsapp/routers/settings.router.ts +++ b/src/whatsapp/routers/settings.router.ts @@ -12,42 +12,42 @@ import { HttpStatus } from './index.router'; const logger = new Logger('SettingsRouter'); export class SettingsRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setSettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setSettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: settingsSchema, - ClassRef: SettingsDto, - execute: (instance, data) => settingsController.createSettings(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: settingsSchema, + ClassRef: SettingsDto, + execute: (instance, data) => settingsController.createSettings(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findSettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findSettings'); + 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) => settingsController.findSettings(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => settingsController.findSettings(instance), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/view.router.ts b/src/whatsapp/routers/view.router.ts index c5e18129..297f0125 100644 --- a/src/whatsapp/routers/view.router.ts +++ b/src/whatsapp/routers/view.router.ts @@ -4,13 +4,13 @@ import { RouterBroker } from '../abstract/abstract.router'; import { viewsController } from '../whatsapp.module'; export class ViewsRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); + constructor(...guards: RequestHandler[]) { + super(); - this.router.get(this.routerPath('qrcode'), ...guards, (req, res) => { - return viewsController.qrcode(req, res); - }); - } + this.router.get(this.routerPath('qrcode'), ...guards, (req, res) => { + return viewsController.qrcode(req, res); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/webhook.router.ts b/src/whatsapp/routers/webhook.router.ts index 835d6014..ef95e5a2 100644 --- a/src/whatsapp/routers/webhook.router.ts +++ b/src/whatsapp/routers/webhook.router.ts @@ -11,42 +11,42 @@ import { HttpStatus } from './index.router'; const logger = new Logger('WebhookRouter'); export class WebhookRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setWebhook'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setWebhook'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: webhookSchema, - ClassRef: WebhookDto, - execute: (instance, data) => webhookController.createWebhook(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: webhookSchema, + ClassRef: WebhookDto, + execute: (instance, data) => webhookController.createWebhook(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findWebhook'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findWebhook'); + 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) => webhookController.findWebhook(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => webhookController.findWebhook(instance), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/services/auth.service.ts b/src/whatsapp/services/auth.service.ts index 3854e148..f5cd0c38 100644 --- a/src/whatsapp/services/auth.service.ts +++ b/src/whatsapp/services/auth.service.ts @@ -12,175 +12,166 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { WAMonitoringService } from './monitor.service'; export type JwtPayload = { - instanceName: string; - apiName: string; - jwt?: string; - apikey?: string; - tokenId: string; + instanceName: string; + apiName: string; + jwt?: string; + apikey?: string; + tokenId: string; }; export class OldToken { - oldToken: string; + oldToken: string; } export class AuthService { - constructor( - private readonly configService: ConfigService, - private readonly waMonitor: WAMonitoringService, - private readonly repository: RepositoryBroker, - ) {} + constructor( + private readonly configService: ConfigService, + private readonly waMonitor: WAMonitoringService, + private readonly repository: RepositoryBroker, + ) {} - private readonly logger = new Logger(AuthService.name); + private readonly logger = new Logger(AuthService.name); - private async jwt(instance: InstanceDto) { - const jwtOpts = this.configService.get('AUTHENTICATION').JWT; - const token = sign( - { - instanceName: instance.instanceName, - apiName, - tokenId: v4(), - }, - jwtOpts.SECRET, - { expiresIn: jwtOpts.EXPIRIN_IN, encoding: 'utf8', subject: 'g-t' }, - ); - - this.logger.verbose('JWT token created: ' + token); - - const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName); - - this.logger.verbose('JWT token saved in database'); - - if (auth['error']) { - this.logger.error({ - localError: AuthService.name + '.jwt', - error: auth['error'], - }); - throw new BadRequestException('Authentication error', auth['error']?.toString()); - } - - return { jwt: token }; - } - - private async apikey(instance: InstanceDto, token?: string) { - const apikey = token ? token : v4().toUpperCase(); - - this.logger.verbose( - token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey, - ); - - const auth = await this.repository.auth.create({ apikey }, instance.instanceName); - - this.logger.verbose('APIKEY saved in database'); - - if (auth['error']) { - this.logger.error({ - localError: AuthService.name + '.apikey', - error: auth['error'], - }); - throw new BadRequestException('Authentication error', auth['error']?.toString()); - } - - return { apikey }; - } - - public async checkDuplicateToken(token: string) { - const instances = await this.waMonitor.instanceInfo(); - - this.logger.verbose('checking duplicate token'); - - const instance = instances.find((instance) => instance.instance.apikey === token); - - if (instance) { - throw new BadRequestException('Token already exists'); - } - - this.logger.verbose('available token'); - - return true; - } - - public async generateHash(instance: InstanceDto, token?: string) { - const options = this.configService.get('AUTHENTICATION'); - - this.logger.verbose( - 'generating hash ' + options.TYPE + ' to instance: ' + instance.instanceName, - ); - - return (await this[options.TYPE](instance, token)) as - | { jwt: string } - | { apikey: string }; - } - - public async refreshToken({ oldToken }: OldToken) { - this.logger.verbose('refreshing token'); - - if (!isJWT(oldToken)) { - throw new BadRequestException('Invalid "oldToken"'); - } - - try { - const jwtOpts = this.configService.get('AUTHENTICATION').JWT; - - this.logger.verbose('checking oldToken'); - - const decode = verify(oldToken, jwtOpts.SECRET, { - ignoreExpiration: true, - }) as Pick; - - this.logger.verbose('checking token in database'); - - const tokenStore = await this.repository.auth.find(decode.instanceName); - - const decodeTokenStore = verify(tokenStore.jwt, jwtOpts.SECRET, { - ignoreExpiration: true, - }) as Pick; - - this.logger.verbose('checking tokenId'); - - if (decode.tokenId !== decodeTokenStore.tokenId) { - throw new BadRequestException('Invalid "oldToken"'); - } - - this.logger.verbose('generating new token'); - - const token = { - jwt: (await this.jwt({ instanceName: decode.instanceName })).jwt, - instanceName: decode.instanceName, - }; - - try { - this.logger.verbose('checking webhook'); - const webhook = await this.repository.webhook.find(decode.instanceName); - if ( - webhook?.enabled && - this.configService.get('WEBHOOK').EVENTS.NEW_JWT_TOKEN - ) { - this.logger.verbose('sending webhook'); - - const httpService = axios.create({ baseURL: webhook.url }); - await httpService.post( - '', + private async jwt(instance: InstanceDto) { + const jwtOpts = this.configService.get('AUTHENTICATION').JWT; + const token = sign( { - event: 'new.jwt', - instance: decode.instanceName, - data: token, + instanceName: instance.instanceName, + apiName, + tokenId: v4(), }, - { params: { owner: this.waMonitor.waInstances[decode.instanceName].wuid } }, - ); + jwtOpts.SECRET, + { expiresIn: jwtOpts.EXPIRIN_IN, encoding: 'utf8', subject: 'g-t' }, + ); + + this.logger.verbose('JWT token created: ' + token); + + const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName); + + this.logger.verbose('JWT token saved in database'); + + if (auth['error']) { + this.logger.error({ + localError: AuthService.name + '.jwt', + error: auth['error'], + }); + throw new BadRequestException('Authentication error', auth['error']?.toString()); } - } catch (error) { - this.logger.error(error); - } - this.logger.verbose('token refreshed'); - - return token; - } catch (error) { - this.logger.error({ - localError: AuthService.name + '.refreshToken', - error, - }); - throw new BadRequestException('Invalid "oldToken"'); + return { jwt: token }; + } + + private async apikey(instance: InstanceDto, token?: string) { + const apikey = token ? token : v4().toUpperCase(); + + this.logger.verbose(token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey); + + const auth = await this.repository.auth.create({ apikey }, instance.instanceName); + + this.logger.verbose('APIKEY saved in database'); + + if (auth['error']) { + this.logger.error({ + localError: AuthService.name + '.apikey', + error: auth['error'], + }); + throw new BadRequestException('Authentication error', auth['error']?.toString()); + } + + return { apikey }; + } + + public async checkDuplicateToken(token: string) { + const instances = await this.waMonitor.instanceInfo(); + + this.logger.verbose('checking duplicate token'); + + const instance = instances.find((instance) => instance.instance.apikey === token); + + if (instance) { + throw new BadRequestException('Token already exists'); + } + + this.logger.verbose('available token'); + + return true; + } + + public async generateHash(instance: InstanceDto, token?: string) { + const options = this.configService.get('AUTHENTICATION'); + + this.logger.verbose('generating hash ' + options.TYPE + ' to instance: ' + instance.instanceName); + + return (await this[options.TYPE](instance, token)) as { jwt: string } | { apikey: string }; + } + + public async refreshToken({ oldToken }: OldToken) { + this.logger.verbose('refreshing token'); + + if (!isJWT(oldToken)) { + throw new BadRequestException('Invalid "oldToken"'); + } + + try { + const jwtOpts = this.configService.get('AUTHENTICATION').JWT; + + this.logger.verbose('checking oldToken'); + + const decode = verify(oldToken, jwtOpts.SECRET, { + ignoreExpiration: true, + }) as Pick; + + this.logger.verbose('checking token in database'); + + const tokenStore = await this.repository.auth.find(decode.instanceName); + + const decodeTokenStore = verify(tokenStore.jwt, jwtOpts.SECRET, { + ignoreExpiration: true, + }) as Pick; + + this.logger.verbose('checking tokenId'); + + if (decode.tokenId !== decodeTokenStore.tokenId) { + throw new BadRequestException('Invalid "oldToken"'); + } + + this.logger.verbose('generating new token'); + + const token = { + jwt: (await this.jwt({ instanceName: decode.instanceName })).jwt, + instanceName: decode.instanceName, + }; + + try { + this.logger.verbose('checking webhook'); + const webhook = await this.repository.webhook.find(decode.instanceName); + if (webhook?.enabled && this.configService.get('WEBHOOK').EVENTS.NEW_JWT_TOKEN) { + this.logger.verbose('sending webhook'); + + const httpService = axios.create({ baseURL: webhook.url }); + await httpService.post( + '', + { + event: 'new.jwt', + instance: decode.instanceName, + data: token, + }, + { params: { owner: this.waMonitor.waInstances[decode.instanceName].wuid } }, + ); + } + } catch (error) { + this.logger.error(error); + } + + this.logger.verbose('token refreshed'); + + return token; + } catch (error) { + this.logger.error({ + localError: AuthService.name + '.refreshToken', + error, + }); + throw new BadRequestException('Invalid "oldToken"'); + } } - } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 11a2a78f..48368cb9 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -14,1622 +14,1536 @@ import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto' import { WAMonitoringService } from './monitor.service'; export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; + private messageCacheFile: string; + private messageCache: Set; - private readonly logger = new Logger(ChatwootService.name); + private readonly logger = new Logger(ChatwootService.name); - private provider: any; + private provider: any; - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - ) { - this.messageCache = new Set(); - } - - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); - } - } - - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); - } - - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); - } - - private async getProvider(instance: InstanceDto) { - this.logger.verbose('get provider to instance: ' + instance.instanceName); - try { - const provider = await this.waMonitor.waInstances[ - instance.instanceName - ].findChatwoot(); - - if (!provider) { - this.logger.warn('provider not found'); - return null; - } - - this.logger.verbose('provider found'); - - return provider; - } catch (error) { - this.logger.error('provider not found'); - return null; - } - } - - private async clientCw(instance: InstanceDto) { - this.logger.verbose('get client to instance: ' + instance.instanceName); - const provider = await this.getProvider(instance); - - if (!provider) { - this.logger.error('provider not found'); - return null; + constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { + this.messageCache = new Set(); } - this.logger.verbose('provider found'); - - this.provider = provider; - - this.logger.verbose('create client to instance: ' + instance.instanceName); - const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, - }); - - this.logger.verbose('client created'); - - return client; - } - - public create(instance: InstanceDto, data: ChatwootDto) { - this.logger.verbose('create chatwoot: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); - - this.logger.verbose('chatwoot created'); - return data; - } - - public async find(instance: InstanceDto): Promise { - this.logger.verbose('find chatwoot: ' + instance.instanceName); - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch (error) { - this.logger.error('chatwoot not found'); - return { enabled: null, url: '' }; - } - } - - public async getContact(instance: InstanceDto, id: number) { - this.logger.verbose('get contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await client.contact.getContactable({ - accountId: this.provider.account_id, - id, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact found'); - return contact; - } - - public async initInstanceChatwoot( - instance: InstanceDto, - inboxName: string, - webhookUrl: string, - qrcode: boolean, - number: string, - ) { - this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inbox in chatwoot'); - const findInbox: any = await client.inboxes.list({ - accountId: this.provider.account_id, - }); - - this.logger.verbose('check duplicate inbox'); - const checkDuplicate = findInbox.payload - .map((inbox) => inbox.name) - .includes(inboxName); - - let inboxId: number; - - if (!checkDuplicate) { - this.logger.verbose('create inbox in chatwoot'); - const data = { - type: 'api', - webhook_url: webhookUrl, - }; - - const inbox = await client.inboxes.create({ - accountId: this.provider.account_id, - data: { - name: inboxName, - channel: data as any, - }, - }); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } else { - this.logger.verbose('find inbox in chatwoot'); - const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } - - this.logger.verbose('find contact in chatwoot and create if not exists'); - const contact = - (await this.findContact(instance, '123456')) || - ((await this.createContact( - instance, - '123456', - inboxId, - false, - 'EvolutionAPI', - )) as any); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact.id || contact.payload.contact.id; - - if (qrcode) { - this.logger.verbose('create conversation in chatwoot'); - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data: { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('create message for init instance in chatwoot'); - - let contentMsg = '/init'; - - if (number) { - contentMsg = `/init:${number}`; - } - - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: contentMsg, - message_type: 'outgoing', - }, - }); - - if (!message) { - this.logger.warn('conversation not found'); - return null; - } - } - - this.logger.verbose('instance chatwoot initialized'); - return true; - } - - public async createContact( - instance: InstanceDto, - phoneNumber: string, - inboxId: number, - isGroup: boolean, - name?: string, - avatar_url?: string, - ) { - this.logger.verbose('create contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let data: any = {}; - if (!isGroup) { - this.logger.verbose('create contact in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - phone_number: `+${phoneNumber}`, - avatar_url: avatar_url, - }; - } else { - this.logger.verbose('create contact group in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: phoneNumber, - avatar_url: avatar_url, - }; - } - - this.logger.verbose('create contact in chatwoot'); - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact created'); - return contact; - } - - public async updateContact(instance: InstanceDto, id: number, data: any) { - this.logger.verbose('update contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('update contact in chatwoot'); - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); - - this.logger.verbose('contact updated'); - return contact; - } - - public async findContact(instance: InstanceDto, phoneNumber: string) { - this.logger.verbose('find contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let query: any; - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('format phone number'); - query = `+${phoneNumber}`; - } else { - this.logger.verbose('format group id'); - query = phoneNumber; - } - - this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); - } else { - this.logger.verbose('return group'); - return contact.payload.find((contact) => contact.identifier === query); - } - } - - public async createConversation(instance: InstanceDto, body: any) { - this.logger.verbose('create conversation to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const isGroup = body.key.remoteJid.includes('@g.us'); - - this.logger.verbose('is group: ' + isGroup); - - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - - this.logger.verbose('chat id: ' + chatId); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - if (isGroup) { - this.logger.verbose('get group name'); - const group = await this.waMonitor.waInstances[ - instance.instanceName - ].client.groupMetadata(chatId); - - nameContact = `${group.subject} (GROUP)`; - - this.logger.verbose('find or create participant in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[ - instance.instanceName - ].profilePicture(body.key.participant.split('@')[0]); - - const findParticipant = await this.findContact( - instance, - body.key.participant.split('@')[0], - ); - - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - ); + private loadMessageCache(): Set { + this.logger.verbose('load message cache'); + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); } - } - - this.logger.verbose('find or create contact in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[ - instance.instanceName - ].profilePicture(chatId); - - const findContact = await this.findContact(instance, chatId); - - let contact: any; - if (body.key.fromMe) { - if (findContact) { - contact = findContact; - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } else { - if (findContact) { - if (!findContact.name || findContact.name === chatId) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - contact = findContact; - } - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = - contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - this.logger.verbose('update contact name in chatwoot'); - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } - - this.logger.verbose('get contact conversations in chatwoot'); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; - - if (contactConversations) { - this.logger.verbose('return conversation if exists'); - const conversation = contactConversations.payload.find( - (conversation) => - conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - if (conversation) { - this.logger.verbose('conversation found'); - return conversation.id; - } - } - - this.logger.verbose('create conversation in chatwoot'); - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data: { - contact_id: `${contactId}`, - inbox_id: `${filterInbox.id}`, - }, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('conversation created'); - return conversation.id; - } catch (error) { - this.logger.error(error); - } - } - - public async getInbox(instance: InstanceDto) { - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; } - this.logger.verbose('find inboxes in chatwoot'); - const inbox = (await client.inboxes.list({ - accountId: this.provider.account_id, - })) as any; - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; + private saveMessageCache() { + this.logger.verbose('save message cache'); + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + this.logger.verbose('message cache saved'); } - this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find( - (inbox) => inbox.name === instance.instanceName, - ); - - if (!findByName) { - this.logger.warn('inbox not found'); - return null; + private clearMessageCache() { + this.logger.verbose('clear message cache'); + this.messageCache.clear(); + this.saveMessageCache(); } - this.logger.verbose('return inbox'); - return findByName; - } - - public async createMessage( - instance: InstanceDto, - conversationId: number, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('message created'); - - return message; - } - - public async createBotMessage( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create bot message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => - conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: content, - message_type: messageType, - attachments: attachments, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('bot message created'); - - return message; - } - - private async sendData( - conversationId: number, - file: string, - messageType: 'incoming' | 'outgoing' | undefined, - content?: string, - ) { - this.logger.verbose('send data to chatwoot'); - - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - unlinkSync(file); - } - } - - public async createBotQr( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - file?: string, - ) { - this.logger.verbose('create bot qr to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => - conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('send data to chatwoot'); - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - if (file) { - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - } - } - - public async sendAttachment( - waInstance: any, - number: string, - media: any, - caption?: string, - ) { - this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); - - try { - this.logger.verbose('get media type'); - const parts = media.split('/'); - - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); - - const mimeType = mimeTypes.lookup(fileName).toString(); - this.logger.verbose('mime type: ' + mimeType); - - let type = 'document'; - - switch (mimeType.split('/')[0]) { - case 'image': - type = 'image'; - break; - case 'video': - type = 'video'; - break; - case 'audio': - type = 'audio'; - break; - default: - type = 'document'; - break; - } - - this.logger.verbose('type: ' + type); - - if (type === 'audio') { - this.logger.verbose('send audio to instance: ' + waInstance.instanceName); - const data: SendAudioDto = { - number: number, - audioMessage: { - audio: media, - }, - options: { - delay: 1200, - presence: 'recording', - }, - }; - - await waInstance?.audioWhatsapp(data); - - this.logger.verbose('audio sent'); - return; - } - - this.logger.verbose('send media to instance: ' + waInstance.instanceName); - const data: SendMediaDto = { - number: number, - mediaMessage: { - mediatype: type as any, - fileName: fileName, - media: media, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - if (caption) { - this.logger.verbose('caption found'); - data.mediaMessage.caption = caption; - } - - await waInstance?.mediaMessage(data); - - this.logger.verbose('media sent'); - return; - } catch (error) { - this.logger.error(error); - } - } - - public async receiveWebhook(instance: InstanceDto, body: any) { - try { - this.logger.verbose( - 'receive webhook to chatwoot instance: ' + instance.instanceName, - ); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private) return { message: 'bot' }; - - this.logger.verbose('check if is group'); - const chatId = - body.conversation.meta.sender?.phone_number?.replace('+', '') || - body.conversation.meta.sender?.identifier; - const messageReceived = body.content; - const senderName = body?.sender?.name; - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (chatId === '123456' && body.message_type === 'outgoing') { - this.logger.verbose('check if is command'); - - const command = messageReceived.replace('/', ''); - - if (command.includes('init') || command.includes('iniciar')) { - this.logger.verbose('command init found'); - const state = waInstance?.connectionStatus?.state; - - if (state !== 'open') { - this.logger.verbose('connect to whatsapp'); - const number = command.split(':')[1]; - await waInstance.connectToWhatsapp(number); - } else { - this.logger.verbose('whatsapp already connected'); - await this.createBotMessage( - instance, - `🚨 ${body.inbox.name} instance is connected.`, - 'incoming', - ); - } - } - - if (command === 'status') { - this.logger.verbose('command status found'); - - const state = waInstance?.connectionStatus?.state; - - if (!state) { - this.logger.verbose('state not found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance not found.`, - 'incoming', - ); - } - - if (state) { - this.logger.verbose('state: ' + state + ' found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance status: *${state}*`, - 'incoming', - ); - } - } - - if (command === 'disconnect' || command === 'desconectar') { - this.logger.verbose('command disconnect found'); - - const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgLogout, 'incoming'); - - this.logger.verbose('disconnect to whatsapp'); - await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); - await waInstance?.client?.ws?.close(); - } - - if (command.includes('new_instance')) { - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - const data = { - instanceName: command.split(':')[1], - qrcode: true, - chatwoot_account_id: this.provider.account_id, - chatwoot_token: this.provider.token, - chatwoot_url: this.provider.url, - chatwoot_sign_msg: this.provider.sign_msg, - }; - - if (command.split(':')[2]) { - data['number'] = command.split(':')[2]; - } - - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: data, - }; - - await axios.request(config); - } - } - - if ( - body.message_type === 'outgoing' && - body?.conversation?.messages?.length && - chatId !== '123456' - ) { - this.logger.verbose('check if is group'); - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - this.logger.verbose('cache file path: ' + this.messageCacheFile); - - this.messageCache = this.loadMessageCache(); - this.logger.verbose('cache file loaded'); - this.logger.verbose(this.messageCache); - - this.logger.verbose('check if message is cached'); - if (this.messageCache.has(body.id.toString())) { - this.logger.verbose('message is cached'); - return { message: 'bot' }; - } - - this.logger.verbose('clear cache'); - this.clearMessageCache(); - - this.logger.verbose('Format message to send'); - let formatText: string; - if (senderName === null || senderName === undefined) { - formatText = messageReceived; - } else { - formatText = this.provider.sign_msg - ? `*${senderName}:*\n\n${messageReceived}` - : messageReceived; - } - - for (const message of body.conversation.messages) { - this.logger.verbose('check if message is media'); - if (message.attachments && message.attachments.length > 0) { - this.logger.verbose('message is media'); - for (const attachment of message.attachments) { - this.logger.verbose('send media to whatsapp'); - if (!messageReceived) { - this.logger.verbose('message do not have text'); - formatText = null; - } - - await this.sendAttachment( - waInstance, - chatId, - attachment.data_url, - formatText, - ); + private async getProvider(instance: InstanceDto) { + this.logger.verbose('get provider to instance: ' + instance.instanceName); + try { + const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + + if (!provider) { + this.logger.warn('provider not found'); + return null; } - } else { - this.logger.verbose('message is text'); - this.logger.verbose('send text to whatsapp'); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: formatText, - }, - options: { - delay: 1200, - presence: 'composing', - }, + this.logger.verbose('provider found'); + + return provider; + } catch (error) { + this.logger.error('provider not found'); + return null; + } + } + + private async clientCw(instance: InstanceDto) { + this.logger.verbose('get client to instance: ' + instance.instanceName); + const provider = await this.getProvider(instance); + + if (!provider) { + this.logger.error('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + this.provider = provider; + + this.logger.verbose('create client to instance: ' + instance.instanceName); + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + this.logger.verbose('client created'); + + return client; + } + + public create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + this.logger.verbose('chatwoot created'); + return data; + } + + public async find(instance: InstanceDto): Promise { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + try { + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + this.logger.error('chatwoot not found'); + return { enabled: null, url: '' }; + } + } + + public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose('get contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await client.contact.getContactable({ + accountId: this.provider.account_id, + id, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact found'); + return contact; + } + + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + number: string, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); + + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); + + let inboxId: number; + + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, }; - await waInstance?.textMessage(data); - } - } - } + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); - if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is csat'); - - const data: SendTextDto = { - number: chatId, - textMessage: { - text: body.content, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - this.logger.verbose('send text to whatsapp'); - - await waInstance?.textMessage(data); - } - - return { message: 'bot' }; - } catch (error) { - this.logger.error(error); - - return { message: 'bot' }; - } - } - - private isMediaMessage(message: any) { - this.logger.verbose('check if is media message'); - const media = [ - 'imageMessage', - 'documentMessage', - 'documentWithCaptionMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - ]; - - const messageKeys = Object.keys(message); - - const result = messageKeys.some((key) => media.includes(key)); - - this.logger.verbose('is media message: ' + result); - return result; - } - - private getTypeMessage(msg: any) { - this.logger.verbose('get type message'); - - const types = { - conversation: msg.conversation, - imageMessage: msg.imageMessage?.caption, - videoMessage: msg.videoMessage?.caption, - extendedTextMessage: msg.extendedTextMessage?.text, - messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), - documentMessage: msg.documentMessage?.caption, - documentWithCaptionMessage: - msg.documentWithCaptionMessage?.message?.documentMessage?.caption, - audioMessage: msg.audioMessage?.caption, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, - }; - - this.logger.verbose('type message: ' + types); - - return types; - } - - private getMessageContent(types: any) { - this.logger.verbose('get message content'); - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); - - const result = typeKey ? types[typeKey] : undefined; - - if (typeKey === 'stickerMessage') { - return null; - } - - if (typeKey === 'contactMessage') { - const vCardData = result.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = `**Contact:** - **name:** ${contactInfo['FN']}`; - - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); - - this.logger.verbose('message content: ' + formattedContact); - return formattedContact; - } - - if (typeKey === 'contactsArrayMessage') { - const formattedContacts = result.contacts.map((contact) => { - const vCardData = contact.vcard.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = `**Contact:** - **name:** ${contact.displayName}`; - - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); - - return formattedContact; - }); - - const formattedContactsArray = formattedContacts.join('\n\n'); - - this.logger.verbose('formatted contacts: ' + formattedContactsArray); - - return formattedContactsArray; - } - - this.logger.verbose('message content: ' + result); - - return result; - } - - private getConversationMessage(msg: any) { - this.logger.verbose('get conversation message'); - - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - this.logger.verbose('conversation message: ' + messageContent); - - return messageContent; - } - - public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { - this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (!waInstance) { - this.logger.warn('wa instance not found'); - return null; - } - - if (event === 'messages.upsert') { - this.logger.verbose('event messages.upsert'); - - if (body.key.remoteJid === 'status@broadcast') { - this.logger.verbose('status broadcast found'); - return; - } - - this.logger.verbose('get conversation message'); - const bodyMessage = await this.getConversationMessage(body.message); - - const isMedia = this.isMediaMessage(body.message); - - if (!bodyMessage && !isMedia) { - this.logger.warn('no body message found'); - return; - } - - this.logger.verbose('get conversation in chatwoot'); - const getConversion = await this.createConversation(instance, body); - - if (!getConversion) { - this.logger.warn('conversation not found'); - return; - } - - const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; - - this.logger.verbose('message type: ' + messageType); - - this.logger.verbose('is media: ' + isMedia); - - this.logger.verbose('check if is media'); - if (isMedia) { - this.logger.verbose('message is media'); - - this.logger.verbose('get base64 from media message'); - const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ - message: { - ...body, - }, - }); - - const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; - - const fileData = Buffer.from(downloadBase64.base64, 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - - this.logger.verbose('temp file name: ' + nameFile); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; + if (!inbox) { + this.logger.warn('inbox not found'); + return null; } - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData( - getConversion, - fileName, - messageType, - content, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData( - getConversion, - fileName, - messageType, - bodyMessage, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage( - instance, - getConversion, - content, - messageType, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; + inboxId = inbox.id; } else { - this.logger.verbose('message is not group'); + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage( - instance, - getConversion, - bodyMessage, - messageType, - ); + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; + inboxId = inbox.id; } - } - if (event === 'status.instance') { - this.logger.verbose('event status.instance'); - const data = body; - const inbox = await this.getInbox(instance); + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact(instance, '123456', inboxId, false, 'EvolutionAPI')) as any); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = '/init'; + + if (number) { + contentMsg = `/init:${number}`; + } + + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: contentMsg, + message_type: 'outgoing', + }, + }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } + } + + this.logger.verbose('instance chatwoot initialized'); + return true; + } + + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + isGroup: boolean, + name?: string, + avatar_url?: string, + ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let data: any = {}; + if (!isGroup) { + this.logger.verbose('create contact in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + avatar_url: avatar_url, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + avatar_url: avatar_url, + }; + } + + this.logger.verbose('create contact in chatwoot'); + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact created'); + return contact; + } + + public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose('update contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('update contact in chatwoot'); + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); + + this.logger.verbose('contact updated'); + return contact; + } + + public async findContact(instance: InstanceDto, phoneNumber: string) { + this.logger.verbose('find contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let query: any; + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('format phone number'); + query = `+${phoneNumber}`; + } else { + this.logger.verbose('format group id'); + query = phoneNumber; + } + + this.logger.verbose('find contact in chatwoot'); + const contact: any = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('return contact'); + return contact.payload.find((contact) => contact.phone_number === query); + } else { + this.logger.verbose('return group'); + return contact.payload.find((contact) => contact.identifier === query); + } + } + + public async createConversation(instance: InstanceDto, body: any) { + this.logger.verbose('create conversation to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const isGroup = body.key.remoteJid.includes('@g.us'); + + this.logger.verbose('is group: ' + isGroup); + + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; + + this.logger.verbose('chat id: ' + chatId); + + let nameContact: string; + + nameContact = !body.key.fromMe ? body.pushName : chatId; + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + if (isGroup) { + this.logger.verbose('get group name'); + const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); + + nameContact = `${group.subject} (GROUP)`; + + this.logger.verbose('find or create participant in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + body.key.participant.split('@')[0], + ); + + const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); + + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } + } else { + await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + ); + } + } + + this.logger.verbose('find or create contact in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); + + const findContact = await this.findContact(instance, chatId); + + let contact: any; + if (body.key.fromMe) { + if (findContact) { + contact = findContact; + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } else { + if (findContact) { + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = findContact; + } + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } + + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + this.logger.verbose('return conversation if exists'); + const conversation = contactConversations.payload.find( + (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } + + this.logger.verbose('create conversation in chatwoot'); + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: `${contactId}`, + inbox_id: `${filterInbox.id}`, + }, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; if (!inbox) { - this.logger.warn('inbox not found'); - return; + this.logger.warn('inbox not found'); + return null; } - const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + this.logger.verbose('find inbox by name'); + const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName); - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgStatus, 'incoming'); - } - - if (event === 'connection.update') { - this.logger.verbose('event connection.update'); - - if (body.status === 'open') { - const msgConnection = `🚀 Connection successfully established!`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgConnection, 'incoming'); + if (!findByName) { + this.logger.warn('inbox not found'); + return null; } - } - if (event === 'qrcode.updated') { - this.logger.verbose('event qrcode.updated'); - if (body.statusCode === 500) { - this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; - - this.logger.verbose('send message to chatwoot'); - return await this.createBotMessage(instance, erroQRcode, 'incoming'); - } else { - this.logger.verbose('qrcode success'); - const fileData = Buffer.from( - body?.qrcode.base64.replace('data:image/png;base64,', ''), - 'base64', - ); - - const fileName = `${path.join( - waInstance?.storePath, - 'temp', - `${`${instance}.png`}`, - )}`; - - this.logger.verbose('temp file name: ' + fileName); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr( - instance, - 'QRCode successfully generated!', - 'incoming', - fileName, - ); - - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; - - if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( - 0, - 4, - )}-${body.qrcode.pairingCode.substring(4, 8)}`; - } - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgQrCode, 'incoming'); - } - } - } catch (error) { - this.logger.error(error); + this.logger.verbose('return inbox'); + return findByName; } - } - public async newInstance(data: any) { - try { - const instanceName = data.instanceName; - const qrcode = true; - const number = data.number; - const accountId = data.accountId; - const chatwootToken = data.token; - const chatwootUrl = data.url; - const signMsg = true; - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create message to instance: ' + instance.instanceName); - const requestData = { - instanceName, - qrcode, - chatwoot_account_id: accountId, - chatwoot_token: chatwootToken, - chatwoot_url: chatwootUrl, - chatwoot_sign_msg: signMsg, - }; + const client = await this.clientCw(instance); - if (number) { - requestData['number'] = number; - } + if (!client) { + this.logger.warn('client not found'); + return null; + } - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: requestData, - }; + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + }, + }); - // await axios.request(config); + if (!message) { + this.logger.warn('message not found'); + return null; + } - return true; - } catch (error) { - this.logger.error(error); - return null; + this.logger.verbose('message created'); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create bot message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + ) { + this.logger.verbose('send data to chatwoot'); + + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + unlinkSync(file); + } + } + + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + this.logger.verbose('create bot qr to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('send data to chatwoot'); + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + if (file) { + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + } + } + + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { + this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + + try { + this.logger.verbose('get media type'); + const parts = media.split('/'); + + const fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); + + const mimeType = mimeTypes.lookup(fileName).toString(); + this.logger.verbose('mime type: ' + mimeType); + + let type = 'document'; + + switch (mimeType.split('/')[0]) { + case 'image': + type = 'image'; + break; + case 'video': + type = 'video'; + break; + case 'audio': + type = 'audio'; + break; + default: + type = 'document'; + break; + } + + this.logger.verbose('type: ' + type); + + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, + }; + + await waInstance?.audioWhatsapp(data); + + this.logger.verbose('audio sent'); + return; + } + + this.logger.verbose('send media to instance: ' + waInstance.instanceName); + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption) { + this.logger.verbose('caption found'); + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + this.logger.verbose('media sent'); + return; + } catch (error) { + this.logger.error(error); + } + } + + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('check if is bot'); + if (!body?.conversation || body.private) return { message: 'bot' }; + + this.logger.verbose('check if is group'); + const chatId = + body.conversation.meta.sender?.phone_number?.replace('+', '') || + body.conversation.meta.sender?.identifier; + const messageReceived = body.content; + const senderName = body?.sender?.name; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + this.logger.verbose('check if is command'); + + const command = messageReceived.replace('/', ''); + + if (command.includes('init') || command.includes('iniciar')) { + this.logger.verbose('command init found'); + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + this.logger.verbose('connect to whatsapp'); + const number = command.split(':')[1]; + await waInstance.connectToWhatsapp(number); + } else { + this.logger.verbose('whatsapp already connected'); + await this.createBotMessage( + instance, + `🚨 ${body.inbox.name} instance is connected.`, + 'incoming', + ); + } + } + + if (command === 'status') { + this.logger.verbose('command status found'); + + const state = waInstance?.connectionStatus?.state; + + if (!state) { + this.logger.verbose('state not found'); + await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance not found.`, 'incoming'); + } + + if (state) { + this.logger.verbose('state: ' + state + ' found'); + await this.createBotMessage( + instance, + `⚠️ ${body.inbox.name} instance status: *${state}*`, + 'incoming', + ); + } + } + + if (command === 'disconnect' || command === 'desconectar') { + this.logger.verbose('command disconnect found'); + + const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgLogout, 'incoming'); + + this.logger.verbose('disconnect to whatsapp'); + await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); + await waInstance?.client?.ws?.close(); + } + + if (command.includes('new_instance')) { + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const data = { + instanceName: command.split(':')[1], + qrcode: true, + chatwoot_account_id: this.provider.account_id, + chatwoot_token: this.provider.token, + chatwoot_url: this.provider.url, + chatwoot_sign_msg: this.provider.sign_msg, + }; + + if (command.split(':')[2]) { + data['number'] = command.split(':')[2]; + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: data, + }; + + await axios.request(config); + } + } + + if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { + this.logger.verbose('check if is group'); + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + this.logger.verbose('cache file path: ' + this.messageCacheFile); + + this.messageCache = this.loadMessageCache(); + this.logger.verbose('cache file loaded'); + this.logger.verbose(this.messageCache); + + this.logger.verbose('check if message is cached'); + if (this.messageCache.has(body.id.toString())) { + this.logger.verbose('message is cached'); + return { message: 'bot' }; + } + + this.logger.verbose('clear cache'); + this.clearMessageCache(); + + this.logger.verbose('Format message to send'); + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + formatText = this.provider.sign_msg ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; + } + + for (const message of body.conversation.messages) { + this.logger.verbose('check if message is media'); + if (message.attachments && message.attachments.length > 0) { + this.logger.verbose('message is media'); + for (const attachment of message.attachments) { + this.logger.verbose('send media to whatsapp'); + if (!messageReceived) { + this.logger.verbose('message do not have text'); + formatText = null; + } + + await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); + } + } else { + this.logger.verbose('message is text'); + + this.logger.verbose('send text to whatsapp'); + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + await waInstance?.textMessage(data); + } + } + } + + if (body.message_type === 'template' && body.event === 'message_created') { + this.logger.verbose('check if is csat'); + + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + this.logger.verbose('send text to whatsapp'); + + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + + const result = messageKeys.some((key) => media.includes(key)); + + this.logger.verbose('is media message: ' + result); + return result; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + }; + + this.logger.verbose('type message: ' + types); + + return types; + } + + private getMessageContent(types: any) { + this.logger.verbose('get message content'); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + const result = typeKey ? types[typeKey] : undefined; + + if (typeKey === 'stickerMessage') { + return null; + } + + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** + **name:** ${contactInfo['FN']}`; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); + + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } + + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** + **name:** ${contact.displayName}`; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); + + return formattedContact; + }); + + const formattedContactsArray = formattedContacts.join('\n\n'); + + this.logger.verbose('formatted contacts: ' + formattedContactsArray); + + return formattedContactsArray; + } + + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { + this.logger.verbose('get conversation message'); + + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + this.logger.verbose('conversation message: ' + messageContent); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } + + if (event === 'messages.upsert') { + this.logger.verbose('event messages.upsert'); + + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); + + const isMedia = this.isMediaMessage(body.message); + + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; + } + + this.logger.verbose('get conversation in chatwoot'); + const getConversion = await this.createConversation(instance, body); + + if (!getConversion) { + this.logger.warn('conversation not found'); + return; + } + + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + this.logger.verbose('message type: ' + messageType); + + this.logger.verbose('is media: ' + isMedia); + + this.logger.verbose('check if is media'); + if (isMedia) { + this.logger.verbose('message is media'); + + this.logger.verbose('get base64 from media message'); + const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ + message: { + ...body, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + + this.logger.verbose('temp file name: ' + nameFile); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, content); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, bodyMessage); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, content, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, bodyMessage, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + if (event === 'status.instance') { + this.logger.verbose('event status.instance'); + const data = body; + const inbox = await this.getInbox(instance); + + if (!inbox) { + this.logger.warn('inbox not found'); + return; + } + + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); + + if (body.status === 'open') { + const msgConnection = `🚀 Connection successfully established!`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'qrcode.updated') { + this.logger.verbose('event qrcode.updated'); + if (body.statusCode === 500) { + this.logger.verbose('qrcode error'); + const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; + + this.logger.verbose('send message to chatwoot'); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + this.logger.verbose('qrcode success'); + const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; + + this.logger.verbose('temp file name: ' + fileName); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('send qrcode to chatwoot'); + await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); + + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( + 0, + 4, + )}-${body.qrcode.pairingCode.substring(4, 8)}`; + } + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); + } + } + + public async newInstance(data: any) { + try { + const instanceName = data.instanceName; + const qrcode = true; + const number = data.number; + const accountId = data.accountId; + const chatwootToken = data.token; + const chatwootUrl = data.url; + const signMsg = true; + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const requestData = { + instanceName, + qrcode, + chatwoot_account_id: accountId, + chatwoot_token: chatwootToken, + chatwoot_url: chatwootUrl, + chatwoot_sign_msg: signMsg, + }; + + if (number) { + requestData['number'] = number; + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: requestData, + }; + + // await axios.request(config); + + return true; + } catch (error) { + this.logger.error(error); + return null; + } } - } } diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index afeeb872..1108d77c 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -5,380 +5,351 @@ import { Db } from 'mongodb'; import mongoose from 'mongoose'; import { join } from 'path'; -import { - Auth, - ConfigService, - Database, - DelInstance, - HttpServer, - Redis, -} from '../../config/env.config'; +import { Auth, ConfigService, Database, DelInstance, HttpServer, Redis } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, STORE_DIR } from '../../config/path.config'; import { dbserver } from '../../db/db.connect'; import { RedisCache } from '../../db/redis.client'; import { NotFoundException } from '../../exceptions'; import { - AuthModel, - ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, - SettingsModel, - WebhookModel, + AuthModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + SettingsModel, + WebhookModel, } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { WAStartupService } from './whatsapp.service'; export class WAMonitoringService { - constructor( - private readonly eventEmitter: EventEmitter2, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('instance created'); + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('instance created'); - this.removeInstance(); - this.noConnection(); - this.delInstanceFiles(); + this.removeInstance(); + this.noConnection(); + this.delInstanceFiles(); - Object.assign(this.db, configService.get('DATABASE')); - Object.assign(this.redis, configService.get('REDIS')); + Object.assign(this.db, configService.get('DATABASE')); + Object.assign(this.redis, configService.get('REDIS')); - this.dbInstance = this.db.ENABLED - ? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances') - : undefined; - } - - private readonly db: Partial = {}; - private readonly redis: Partial = {}; - - private dbInstance: Db; - - private dbStore = dbserver; - - private readonly logger = new Logger(WAMonitoringService.name); - public readonly waInstances: Record = {}; - - public delInstanceTime(instance: string) { - const time = this.configService.get('DEL_INSTANCE'); - if (typeof time === 'number' && time > 0) { - this.logger.verbose( - `Instance "${instance}" don't have connection, will be removed in ${time} minutes`, - ); - - setTimeout(async () => { - if (this.waInstances[instance]?.connectionStatus?.state !== 'open') { - if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') { - await this.waInstances[instance]?.client?.logout( - 'Log out instance: ' + instance, - ); - this.waInstances[instance]?.client?.ws?.close(); - this.waInstances[instance]?.client?.end(undefined); - delete this.waInstances[instance]; - } else { - delete this.waInstances[instance]; - this.eventEmitter.emit('remove.instance', instance, 'inner'); - } - } - }, 1000 * 60 * time); - } - } - - public async instanceInfo(instanceName?: string) { - this.logger.verbose('get instance info'); - if (instanceName && !this.waInstances[instanceName]) { - throw new NotFoundException(`Instance "${instanceName}" not found`); + this.dbInstance = this.db.ENABLED + ? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances') + : undefined; } - const instances: any[] = []; + private readonly db: Partial = {}; + private readonly redis: Partial = {}; - for await (const [key, value] of Object.entries(this.waInstances)) { - if (value) { - this.logger.verbose('get instance info: ' + key); - let chatwoot: any; + private dbInstance: Db; - const urlServer = this.configService.get('SERVER').URL; + private dbStore = dbserver; - const findChatwoot = await this.waInstances[key].findChatwoot(); + private readonly logger = new Logger(WAMonitoringService.name); + public readonly waInstances: Record = {}; - if (findChatwoot && findChatwoot.enabled) { - chatwoot = { - ...findChatwoot, - webhook_url: `${urlServer}/chatwoot/webhook/${key}`, - }; + public delInstanceTime(instance: string) { + const time = this.configService.get('DEL_INSTANCE'); + if (typeof time === 'number' && time > 0) { + this.logger.verbose(`Instance "${instance}" don't have connection, will be removed in ${time} minutes`); + + setTimeout(async () => { + if (this.waInstances[instance]?.connectionStatus?.state !== 'open') { + if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') { + await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance); + this.waInstances[instance]?.client?.ws?.close(); + this.waInstances[instance]?.client?.end(undefined); + delete this.waInstances[instance]; + } else { + delete this.waInstances[instance]; + this.eventEmitter.emit('remove.instance', instance, 'inner'); + } + } + }, 1000 * 60 * time); } - - if (value.connectionStatus.state === 'open') { - this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); - - const instanceData = { - instance: { - instanceName: key, - owner: value.wuid, - profileName: (await value.getProfileName()) || 'not loaded', - profilePictureUrl: value.profilePictureUrl, - profileStatus: (await value.getProfileStatus()) || '', - status: value.connectionStatus.state, - }, - }; - - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - instanceData.instance['serverUrl'] = - this.configService.get('SERVER').URL; - - instanceData.instance['apikey'] = ( - await this.repository.auth.find(key) - ).apikey; - - instanceData.instance['chatwoot'] = chatwoot; - } - - instances.push(instanceData); - } else { - this.logger.verbose( - 'instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state, - ); - - const instanceData = { - instance: { - instanceName: key, - status: value.connectionStatus.state, - }, - }; - - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - instanceData.instance['serverUrl'] = - this.configService.get('SERVER').URL; - - instanceData.instance['apikey'] = ( - await this.repository.auth.find(key) - ).apikey; - - instanceData.instance['chatwoot'] = chatwoot; - } - - instances.push(instanceData); - } - } } - this.logger.verbose('return instance info: ' + instances.length); + public async instanceInfo(instanceName?: string) { + this.logger.verbose('get instance info'); + if (instanceName && !this.waInstances[instanceName]) { + throw new NotFoundException(`Instance "${instanceName}" not found`); + } - return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; - } + const instances: any[] = []; - private delInstanceFiles() { - this.logger.verbose('cron to delete instance files started'); - setInterval(async () => { - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - const collections = await this.dbInstance.collections(); - collections.forEach(async (collection) => { - const name = collection.namespace.replace(/^[\w-]+./, ''); - await this.dbInstance.collection(name).deleteMany({ - $or: [ - { _id: { $regex: /^app.state.*/ } }, - { _id: { $regex: /^session-.*/ } }, - ], - }); - this.logger.verbose('instance files deleted: ' + name); - }); - // } else if (this.redis.ENABLED) { - } else { - const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); - for await (const dirent of dir) { - if (dirent.isDirectory()) { - const files = readdirSync(join(INSTANCE_DIR, dirent.name), { - encoding: 'utf-8', - }); - files.forEach(async (file) => { - if (file.match(/^app.state.*/) || file.match(/^session-.*/)) { - rmSync(join(INSTANCE_DIR, dirent.name, file), { - recursive: true, - force: true, + for await (const [key, value] of Object.entries(this.waInstances)) { + if (value) { + this.logger.verbose('get instance info: ' + key); + let chatwoot: any; + + const urlServer = this.configService.get('SERVER').URL; + + const findChatwoot = await this.waInstances[key].findChatwoot(); + + if (findChatwoot && findChatwoot.enabled) { + chatwoot = { + ...findChatwoot, + webhook_url: `${urlServer}/chatwoot/webhook/${key}`, + }; + } + + if (value.connectionStatus.state === 'open') { + this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); + + const instanceData = { + instance: { + instanceName: key, + owner: value.wuid, + profileName: (await value.getProfileName()) || 'not loaded', + profilePictureUrl: value.profilePictureUrl, + profileStatus: (await value.getProfileStatus()) || '', + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + + instanceData.instance['chatwoot'] = chatwoot; + } + + instances.push(instanceData); + } else { + this.logger.verbose('instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state); + + const instanceData = { + instance: { + instanceName: key, + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + + instanceData.instance['chatwoot'] = chatwoot; + } + + instances.push(instanceData); + } + } + } + + this.logger.verbose('return instance info: ' + instances.length); + + return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; + } + + private delInstanceFiles() { + this.logger.verbose('cron to delete instance files started'); + setInterval(async () => { + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + const collections = await this.dbInstance.collections(); + collections.forEach(async (collection) => { + const name = collection.namespace.replace(/^[\w-]+./, ''); + await this.dbInstance.collection(name).deleteMany({ + $or: [{ _id: { $regex: /^app.state.*/ } }, { _id: { $regex: /^session-.*/ } }], + }); + this.logger.verbose('instance files deleted: ' + name); }); - } - }); - this.logger.verbose('instance files deleted: ' + dirent.name); - } + // } else if (this.redis.ENABLED) { + } else { + const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); + for await (const dirent of dir) { + if (dirent.isDirectory()) { + const files = readdirSync(join(INSTANCE_DIR, dirent.name), { + encoding: 'utf-8', + }); + files.forEach(async (file) => { + if (file.match(/^app.state.*/) || file.match(/^session-.*/)) { + rmSync(join(INSTANCE_DIR, dirent.name, file), { + recursive: true, + force: true, + }); + } + }); + this.logger.verbose('instance files deleted: ' + dirent.name); + } + } + } + }, 3600 * 1000 * 2); + } + + public async cleaningUp(instanceName: string) { + this.logger.verbose('cleaning up instance: ' + instanceName); + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + this.logger.verbose('cleaning up instance in database: ' + instanceName); + await this.repository.dbServer.connect(); + const collections: any[] = await this.dbInstance.collections(); + if (collections.length > 0) { + await this.dbInstance.dropCollection(instanceName); + } + return; } - } - }, 3600 * 1000 * 2); - } - public async cleaningUp(instanceName: string) { - this.logger.verbose('cleaning up instance: ' + instanceName); - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - this.logger.verbose('cleaning up instance in database: ' + instanceName); - await this.repository.dbServer.connect(); - const collections: any[] = await this.dbInstance.collections(); - if (collections.length > 0) { - await this.dbInstance.dropCollection(instanceName); - } - return; - } - - if (this.redis.ENABLED) { - this.logger.verbose('cleaning up instance in redis: ' + instanceName); - this.cache.reference = instanceName; - await this.cache.delAll(); - return; - } - - this.logger.verbose('cleaning up instance in files: ' + instanceName); - rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); - } - - public async cleaningStoreFiles(instanceName: string) { - if (!this.db.ENABLED) { - this.logger.verbose('cleaning store files instance: ' + instanceName); - rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); - - execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'contacts', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'message-up', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'messages', instanceName)}`); - - execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); - execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); - execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); - execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); - - return; - } - - this.logger.verbose('cleaning store database instance: ' + instanceName); - - await AuthModel.deleteMany({ owner: instanceName }); - await ContactModel.deleteMany({ owner: instanceName }); - await MessageModel.deleteMany({ owner: instanceName }); - await MessageUpModel.deleteMany({ owner: instanceName }); - await AuthModel.deleteMany({ _id: instanceName }); - await WebhookModel.deleteMany({ _id: instanceName }); - await ChatwootModel.deleteMany({ _id: instanceName }); - await SettingsModel.deleteMany({ _id: instanceName }); - - return; - } - - public async loadInstance() { - this.logger.verbose('load instances'); - const set = async (name: string) => { - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = name; - this.logger.verbose('instance loaded: ' + name); - - await instance.connectToWhatsapp(); - this.logger.verbose('connectToWhatsapp: ' + name); - - this.waInstances[name] = instance; - }; - - try { - if (this.redis.ENABLED) { - this.logger.verbose('redis enabled'); - await this.cache.connect(this.redis as Redis); - const keys = await this.cache.instanceKeys(); - if (keys?.length > 0) { - this.logger.verbose('reading instance keys and setting instances'); - keys.forEach(async (k) => await set(k.split(':')[1])); - } else { - this.logger.verbose('no instance keys found'); + if (this.redis.ENABLED) { + this.logger.verbose('cleaning up instance in redis: ' + instanceName); + this.cache.reference = instanceName; + await this.cache.delAll(); + return; } + + this.logger.verbose('cleaning up instance in files: ' + instanceName); + rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + } + + public async cleaningStoreFiles(instanceName: string) { + if (!this.db.ENABLED) { + this.logger.verbose('cleaning store files instance: ' + instanceName); + rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + + execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'contacts', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'message-up', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'messages', instanceName)}`); + + execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); + execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); + + return; + } + + this.logger.verbose('cleaning store database instance: ' + instanceName); + + await AuthModel.deleteMany({ owner: instanceName }); + await ContactModel.deleteMany({ owner: instanceName }); + await MessageModel.deleteMany({ owner: instanceName }); + await MessageUpModel.deleteMany({ owner: instanceName }); + await AuthModel.deleteMany({ _id: instanceName }); + await WebhookModel.deleteMany({ _id: instanceName }); + await ChatwootModel.deleteMany({ _id: instanceName }); + await SettingsModel.deleteMany({ _id: instanceName }); + return; - } - - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - this.logger.verbose('database enabled'); - await this.repository.dbServer.connect(); - const collections: any[] = await this.dbInstance.collections(); - if (collections.length > 0) { - this.logger.verbose('reading collections and setting instances'); - collections.forEach( - async (coll) => await set(coll.namespace.replace(/^[\w-]+\./, '')), - ); - } else { - this.logger.verbose('no collections found'); - } - return; - } - - this.logger.verbose('store in files enabled'); - const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); - for await (const dirent of dir) { - if (dirent.isDirectory()) { - this.logger.verbose('reading instance files and setting instances'); - const files = readdirSync(join(INSTANCE_DIR, dirent.name), { - encoding: 'utf-8', - }); - if (files.length === 0) { - rmSync(join(INSTANCE_DIR, dirent.name), { recursive: true, force: true }); - break; - } - - await set(dirent.name); - } else { - this.logger.verbose('no instance files found'); - } - } - } catch (error) { - this.logger.error(error); } - } - private removeInstance() { - this.eventEmitter.on('remove.instance', async (instanceName: string) => { - this.logger.verbose('remove instance: ' + instanceName); - try { - this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); - this.waInstances[instanceName] = undefined; - } catch (error) { - this.logger.error(error); - } + public async loadInstance() { + this.logger.verbose('load instances'); + const set = async (name: string) => { + const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + instance.instanceName = name; + this.logger.verbose('instance loaded: ' + name); - try { - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - this.cleaningStoreFiles(instanceName); - } finally { - this.logger.warn(`Instance "${instanceName}" - REMOVED`); - } - }); - this.eventEmitter.on('logout.instance', async (instanceName: string) => { - this.logger.verbose('logout instance: ' + instanceName); - try { - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - } finally { - this.logger.warn(`Instance "${instanceName}" - LOGOUT`); - } - }); - } + await instance.connectToWhatsapp(); + this.logger.verbose('connectToWhatsapp: ' + name); - private noConnection() { - this.logger.verbose('checking instances without connection'); - this.eventEmitter.on('no.connection', async (instanceName) => { - try { - this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); - this.waInstances[instanceName] = undefined; + this.waInstances[name] = instance; + }; - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - } catch (error) { - this.logger.error({ - localError: 'noConnection', - warn: 'Error deleting instance from memory.', - error, + try { + if (this.redis.ENABLED) { + this.logger.verbose('redis enabled'); + await this.cache.connect(this.redis as Redis); + const keys = await this.cache.instanceKeys(); + if (keys?.length > 0) { + this.logger.verbose('reading instance keys and setting instances'); + keys.forEach(async (k) => await set(k.split(':')[1])); + } else { + this.logger.verbose('no instance keys found'); + } + return; + } + + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + this.logger.verbose('database enabled'); + await this.repository.dbServer.connect(); + const collections: any[] = await this.dbInstance.collections(); + if (collections.length > 0) { + this.logger.verbose('reading collections and setting instances'); + collections.forEach(async (coll) => await set(coll.namespace.replace(/^[\w-]+\./, ''))); + } else { + this.logger.verbose('no collections found'); + } + return; + } + + this.logger.verbose('store in files enabled'); + const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); + for await (const dirent of dir) { + if (dirent.isDirectory()) { + this.logger.verbose('reading instance files and setting instances'); + const files = readdirSync(join(INSTANCE_DIR, dirent.name), { + encoding: 'utf-8', + }); + if (files.length === 0) { + rmSync(join(INSTANCE_DIR, dirent.name), { recursive: true, force: true }); + break; + } + + await set(dirent.name); + } else { + this.logger.verbose('no instance files found'); + } + } + } catch (error) { + this.logger.error(error); + } + } + + private removeInstance() { + this.eventEmitter.on('remove.instance', async (instanceName: string) => { + this.logger.verbose('remove instance: ' + instanceName); + try { + this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); + this.waInstances[instanceName] = undefined; + } catch (error) { + this.logger.error(error); + } + + try { + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + this.cleaningStoreFiles(instanceName); + } finally { + this.logger.warn(`Instance "${instanceName}" - REMOVED`); + } }); - } finally { - this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`); - } - }); - } + this.eventEmitter.on('logout.instance', async (instanceName: string) => { + this.logger.verbose('logout instance: ' + instanceName); + try { + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + } finally { + this.logger.warn(`Instance "${instanceName}" - LOGOUT`); + } + }); + } + + private noConnection() { + this.logger.verbose('checking instances without connection'); + this.eventEmitter.on('no.connection', async (instanceName) => { + try { + this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); + this.waInstances[instanceName] = undefined; + + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + } catch (error) { + this.logger.error({ + localError: 'noConnection', + warn: 'Error deleting instance from memory.', + error, + }); + } finally { + this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`); + } + }); + } } diff --git a/src/whatsapp/services/settings.service.ts b/src/whatsapp/services/settings.service.ts index f00138e1..bbd5076f 100644 --- a/src/whatsapp/services/settings.service.ts +++ b/src/whatsapp/services/settings.service.ts @@ -4,31 +4,29 @@ import { SettingsDto } from '../dto/settings.dto'; import { WAMonitoringService } from './monitor.service'; export class SettingsService { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - private readonly logger = new Logger(SettingsService.name); + private readonly logger = new Logger(SettingsService.name); - public create(instance: InstanceDto, data: SettingsDto) { - this.logger.verbose('create settings: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setSettings(data); + public create(instance: InstanceDto, data: SettingsDto) { + this.logger.verbose('create settings: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setSettings(data); - return { settings: { ...instance, settings: data } }; - } - - public async find(instance: InstanceDto): Promise { - try { - this.logger.verbose('find settings: ' + instance.instanceName); - const result = await this.waMonitor.waInstances[ - instance.instanceName - ].findSettings(); - - if (Object.keys(result).length === 0) { - throw new Error('Settings not found'); - } - - return result; - } catch (error) { - return { reject_call: false, msg_call: '', groups_ignore: false }; + return { settings: { ...instance, settings: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find settings: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findSettings(); + + if (Object.keys(result).length === 0) { + throw new Error('Settings not found'); + } + + return result; + } catch (error) { + return { reject_call: false, msg_call: '', groups_ignore: false }; + } } - } } diff --git a/src/whatsapp/services/webhook.service.ts b/src/whatsapp/services/webhook.service.ts index 1ffdce6f..36233f1f 100644 --- a/src/whatsapp/services/webhook.service.ts +++ b/src/whatsapp/services/webhook.service.ts @@ -4,31 +4,29 @@ import { WebhookDto } from '../dto/webhook.dto'; import { WAMonitoringService } from './monitor.service'; export class WebhookService { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - private readonly logger = new Logger(WebhookService.name); + private readonly logger = new Logger(WebhookService.name); - public create(instance: InstanceDto, data: WebhookDto) { - this.logger.verbose('create webhook: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setWebhook(data); + public create(instance: InstanceDto, data: WebhookDto) { + this.logger.verbose('create webhook: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setWebhook(data); - return { webhook: { ...instance, webhook: data } }; - } - - public async find(instance: InstanceDto): Promise { - try { - this.logger.verbose('find webhook: ' + instance.instanceName); - const result = await this.waMonitor.waInstances[ - instance.instanceName - ].findWebhook(); - - if (Object.keys(result).length === 0) { - throw new Error('Webhook not found'); - } - - return result; - } catch (error) { - return { enabled: false, url: '', events: [], webhook_by_events: false }; + return { webhook: { ...instance, webhook: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find webhook: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findWebhook(); + + if (Object.keys(result).length === 0) { + throw new Error('Webhook not found'); + } + + return result; + } catch (error) { + return { enabled: false, url: '', events: [], webhook_by_events: false }; + } } - } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 17c654da..d1bae31e 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,37 +1,37 @@ import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import makeWASocket, { - AnyMessageContent, - BufferedEventData, - BufferJSON, - CacheStore, - Chat, - ConnectionState, - Contact, - delay, - DisconnectReason, - downloadMediaMessage, - fetchLatestBaileysVersion, - generateWAMessageFromContent, - getAggregateVotesInPollMessage, - getContentType, - getDevice, - GroupMetadata, - isJidGroup, - isJidUser, - makeCacheableSignalKeyStore, - MessageUpsertType, - MiscMessageGenerationOptions, - ParticipantAction, - prepareWAMessageMedia, - proto, - useMultiFileAuthState, - UserFacingSocketConfig, - WABrowserDescription, - WAMediaUpload, - WAMessage, - WAMessageUpdate, - WASocket, + AnyMessageContent, + BufferedEventData, + BufferJSON, + CacheStore, + Chat, + ConnectionState, + Contact, + delay, + DisconnectReason, + downloadMediaMessage, + fetchLatestBaileysVersion, + generateWAMessageFromContent, + getAggregateVotesInPollMessage, + getContentType, + getDevice, + GroupMetadata, + isJidGroup, + isJidUser, + makeCacheableSignalKeyStore, + MessageUpsertType, + MiscMessageGenerationOptions, + ParticipantAction, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + UserFacingSocketConfig, + WABrowserDescription, + WAMediaUpload, + WAMessage, + WAMessageUpdate, + WASocket, } from '@whiskeysockets/baileys'; import axios from 'axios'; import { exec, execSync } from 'child_process'; @@ -51,67 +51,63 @@ import sharp from 'sharp'; import { v4 } from 'uuid'; import { - Auth, - CleanStoreConf, - ConfigService, - ConfigSessionPhone, - Database, - HttpServer, - Log, - QrCode, - Redis, - Webhook, + Auth, + CleanStoreConf, + ConfigService, + ConfigSessionPhone, + Database, + HttpServer, + Log, + QrCode, + Redis, + Webhook, } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; import { dbserver } from '../../db/db.connect'; import { RedisCache } from '../../db/redis.client'; -import { - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from '../../exceptions'; +import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberBusiness, + OnWhatsAppDto, + PrivacySettingDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { - ContactMessage, - MediaMessage, - Options, - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, - StatusMessage, + ContactMessage, + MediaMessage, + Options, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, + StatusMessage, } from '../dto/sendMessage.dto'; import { SettingsRaw } from '../models'; import { ChatRaw } from '../models/chat.model'; @@ -128,2894 +124,2764 @@ import { waMonitor } from '../whatsapp.module'; import { ChatwootService } from './chatwoot.service'; export class WAStartupService { - constructor( - private readonly configService: ConfigService, - private readonly eventEmitter: EventEmitter2, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('WAStartupService initialized'); - this.cleanStore(); - this.instance.qrcode = { count: 0 }; - } - - private readonly logger = new Logger(WAStartupService.name); - private readonly instance: wa.Instance = {}; - public client: WASocket; - private readonly localWebhook: wa.LocalWebHook = {}; - private readonly localChatwoot: wa.LocalChatwoot = {}; - private readonly localSettings: wa.LocalSettings = {}; - private stateConnection: wa.StateConnection = { state: 'close' }; - public readonly storePath = join(ROOT_DIR, 'store'); - private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache(); - private endSession = false; - private logBaileys = this.configService.get('LOG').BAILEYS; - - private phoneNumber: string; - - private chatwootService = new ChatwootService(waMonitor, this.configService); - - public set instanceName(name: string) { - this.logger.verbose(`Initializing instance '${name}'`); - if (!name) { - this.logger.verbose('Instance name not found, generating random name with uuid'); - this.instance.name = v4(); - return; + constructor( + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('WAStartupService initialized'); + this.cleanStore(); + this.instance.qrcode = { count: 0 }; } - this.instance.name = name; - this.logger.verbose(`Instance '${this.instance.name}' initialized`); - this.logger.verbose('Sending instance status to webhook'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'created', - }); - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'created', - }, - ); - } - } + private readonly logger = new Logger(WAStartupService.name); + private readonly instance: wa.Instance = {}; + public client: WASocket; + private readonly localWebhook: wa.LocalWebHook = {}; + private readonly localChatwoot: wa.LocalChatwoot = {}; + private readonly localSettings: wa.LocalSettings = {}; + private stateConnection: wa.StateConnection = { state: 'close' }; + public readonly storePath = join(ROOT_DIR, 'store'); + private readonly msgRetryCounterCache: CacheStore = new NodeCache(); + private readonly userDevicesCache: CacheStore = new NodeCache(); + private endSession = false; + private logBaileys = this.configService.get('LOG').BAILEYS; - public get instanceName() { - this.logger.verbose('Getting instance name'); - return this.instance.name; - } + private phoneNumber: string; - public get wuid() { - this.logger.verbose('Getting remoteJid of instance'); - return this.instance.wuid; - } + private chatwootService = new ChatwootService(waMonitor, this.configService); - public async getProfileName() { - this.logger.verbose('Getting profile name'); - let profileName = this.client.user?.name ?? this.client.user?.verifiedName; - if (!profileName) { - this.logger.verbose('Profile name not found, trying to get from database'); - if (this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('Database enabled, trying to get from database'); - const collection = dbserver - .getClient() - .db( - this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + - '-instances', - ) - .collection(this.instanceName); - const data = await collection.findOne({ _id: 'creds' }); - if (data) { - this.logger.verbose('Profile name found in database'); - const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); - profileName = creds.me?.name || creds.me?.verifiedName; + public set instanceName(name: string) { + this.logger.verbose(`Initializing instance '${name}'`); + if (!name) { + this.logger.verbose('Instance name not found, generating random name with uuid'); + this.instance.name = v4(); + return; } - } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { - this.logger.verbose('Profile name found in file'); - const creds = JSON.parse( - readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { - encoding: 'utf-8', - }), - ); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } - - this.logger.verbose(`Profile name: ${profileName}`); - return profileName; - } - - public async getProfileStatus() { - this.logger.verbose('Getting profile status'); - const status = await this.client.fetchStatus(this.instance.wuid); - - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; - } - - public get profilePictureUrl() { - this.logger.verbose('Getting profile picture url'); - return this.instance.profilePictureUrl; - } - - public get qrCode(): wa.QrCode { - this.logger.verbose('Getting qrcode'); - if (this.instance.qrcode?.pairingCode) { - return { - pairingCode: this.instance.qrcode?.pairingCode, - }; - } - - return { - code: this.instance.qrcode?.code, - base64: this.instance.qrcode?.base64, - }; - } - - private async loadWebhook() { - this.logger.verbose('Loading webhook'); - const data = await this.repository.webhook.find(this.instanceName); - this.localWebhook.url = data?.url; - this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); - - this.localWebhook.enabled = data?.enabled; - this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); - - this.localWebhook.events = data?.events; - this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); - - this.localWebhook.webhook_by_events = data?.webhook_by_events; - this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); - - this.logger.verbose('Webhook loaded'); - } - - public async setWebhook(data: WebhookRaw) { - this.logger.verbose('Setting webhook'); - await this.repository.webhook.create(data, this.instanceName); - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - Object.assign(this.localWebhook, data); - this.logger.verbose('Webhook set'); - } - - public async findWebhook() { - this.logger.verbose('Finding webhook'); - const data = await this.repository.webhook.find(this.instanceName); - - if (!data) { - this.logger.verbose('Webhook not found'); - throw new NotFoundException('Webhook not found'); - } - - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - return data; - } - - private async loadChatwoot() { - this.logger.verbose('Loading chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - this.localChatwoot.enabled = data?.enabled; - this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); - - this.localChatwoot.account_id = data?.account_id; - this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); - - this.localChatwoot.token = data?.token; - this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); - - this.localChatwoot.url = data?.url; - this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); - - this.localChatwoot.name_inbox = data?.name_inbox; - this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); - - this.localChatwoot.sign_msg = data?.sign_msg; - this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); - - this.logger.verbose('Chatwoot loaded'); - } - - public async setChatwoot(data: ChatwootRaw) { - this.logger.verbose('Setting chatwoot'); - await this.repository.chatwoot.create(data, this.instanceName); - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - - Object.assign(this.localChatwoot, data); - this.logger.verbose('Chatwoot set'); - } - - public async findChatwoot() { - this.logger.verbose('Finding chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - - if (!data) { - this.logger.verbose('Chatwoot not found'); - return null; - } - - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - - return data; - } - - private async loadSettings() { - this.logger.verbose('Loading settings'); - const data = await this.repository.settings.find(this.instanceName); - this.localSettings.reject_call = data?.reject_call; - this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); - - this.localSettings.msg_call = data?.msg_call; - this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); - - this.localSettings.groups_ignore = data?.groups_ignore; - this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); - - this.logger.verbose('Settings loaded'); - } - - public async setSettings(data: SettingsRaw) { - this.logger.verbose('Setting settings'); - await this.repository.settings.create(data, this.instanceName); - this.logger.verbose(`Settings reject_call: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - Object.assign(this.localSettings, data); - this.logger.verbose('Settings set'); - } - - public async findSettings() { - this.logger.verbose('Finding settings'); - const data = await this.repository.settings.find(this.instanceName); - - if (!data) { - this.logger.verbose('Settings not found'); - throw new NotFoundException('Settings not found'); - } - - this.logger.verbose(`Settings url: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - return data; - } - - public async sendDataWebhook(event: Events, data: T, local = true) { - const webhookGlobal = this.configService.get('WEBHOOK'); - const webhookLocal = this.localWebhook.events; - const serverUrl = this.configService.get('SERVER').URL; - const we = event.replace(/[.-]/gm, '_').toUpperCase(); - const transformedWe = we.replace(/_/gm, '-').toLowerCase(); - - const expose = - this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; - const tokenStore = await this.repository.auth.find(this.instanceName); - const instanceApikey = tokenStore?.apikey || 'Apikey not found'; - - const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - if (local) { - if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { - this.logger.verbose('Sending data to webhook local'); - let baseURL; - - if (this.localWebhook.webhook_by_events) { - baseURL = `${this.localWebhook.url}/${transformedWe}`; - } else { - baseURL = this.localWebhook.url; - } - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-local', - url: baseURL, - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - apikey: (expose && instanceApikey) || null, - }; - - if (expose && instanceApikey) { - logData['apikey'] = instanceApikey; - } - - this.logger.log(logData); - } - - try { - if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { - const httpService = axios.create({ baseURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - }; - - if (expose && instanceApikey) { - postData['apikey'] = instanceApikey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-local', - 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]) { - this.logger.verbose('Sending data to webhook global'); - const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; - - let globalURL; - - if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { - globalURL = `${globalWebhook.URL}/${transformedWe}`; - } else { - globalURL = globalWebhook.URL; - } - - const localUrl = this.localWebhook.url; - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-global', - url: globalURL, - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - logData['apikey'] = globalApiKey; - } - - this.logger.log(logData); - } - - try { - if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { - const httpService = axios.create({ baseURL: globalURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - postData['apikey'] = globalApiKey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-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, - }); - } - } - } - } - - private async connectionUpdate({ - qr, - connection, - lastDisconnect, - }: Partial) { - this.logger.verbose('Connection update'); - if (qr) { - this.logger.verbose('QR code found'); - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { - this.logger.verbose('QR code limit reached'); - - this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); - this.sendDataWebhook(Events.QRCODE_UPDATED, { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }, - ); - } - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - state: 'refused', - statusReason: DisconnectReason.connectionClosed, - }); - - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.instance.name = name; + this.logger.verbose(`Instance '${this.instance.name}' initialized`); + this.logger.verbose('Sending instance status to webhook'); this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('endSession defined as true'); - this.endSession = true; - - this.logger.verbose('Emmiting event logout.instance'); - return this.eventEmitter.emit('no.connection', this.instance.name); - } - - this.logger.verbose('Incrementing QR code count'); - this.instance.qrcode.count++; - - const optsQrcode: QRCodeToDataURLOptions = { - margin: 3, - scale: 4, - errorCorrectionLevel: 'H', - color: { light: '#ffffff', dark: '#198754' }, - }; - - console.log(this.phoneNumber); - if (this.phoneNumber) { - await delay(2000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode( - this.phoneNumber, - ); - } else { - this.instance.qrcode.pairingCode = null; - } - - this.logger.verbose('Generating QR code'); - qrcode.toDataURL(qr, optsQrcode, (error, base64) => { - if (error) { - this.logger.error('Qrcode generate failed:' + error.toString()); - return; - } - - this.instance.qrcode.base64 = base64; - this.instance.qrcode.code = qr; - - this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, + status: 'created', }); if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - qrcode: { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'created', + }, + ); + } + } + + public get instanceName() { + this.logger.verbose('Getting instance name'); + return this.instance.name; + } + + public get wuid() { + this.logger.verbose('Getting remoteJid of instance'); + return this.instance.wuid; + } + + public async getProfileName() { + this.logger.verbose('Getting profile name'); + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; + if (!profileName) { + this.logger.verbose('Profile name not found, trying to get from database'); + if (this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('Database enabled, trying to get from database'); + const collection = dbserver + .getClient() + .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(this.instanceName); + const data = await collection.findOne({ _id: 'creds' }); + if (data) { + this.logger.verbose('Profile name found in database'); + const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { + this.logger.verbose('Profile name found in file'); + const creds = JSON.parse( + readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { + encoding: 'utf-8', + }), + ); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } + + this.logger.verbose(`Profile name: ${profileName}`); + return profileName; + } + + public async getProfileStatus() { + this.logger.verbose('Getting profile status'); + const status = await this.client.fetchStatus(this.instance.wuid); + + this.logger.verbose(`Profile status: ${status.status}`); + return status.status; + } + + public get profilePictureUrl() { + this.logger.verbose('Getting profile picture url'); + return this.instance.profilePictureUrl; + } + + public get qrCode(): wa.QrCode { + this.logger.verbose('Getting qrcode'); + if (this.instance.qrcode?.pairingCode) { + return { + pairingCode: this.instance.qrcode?.pairingCode, + }; + } + + return { + code: this.instance.qrcode?.code, + base64: this.instance.qrcode?.base64, + }; + } + + private async loadWebhook() { + this.logger.verbose('Loading webhook'); + const data = await this.repository.webhook.find(this.instanceName); + this.localWebhook.url = data?.url; + this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); + + this.localWebhook.enabled = data?.enabled; + this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); + + this.localWebhook.events = data?.events; + this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); + + this.localWebhook.webhook_by_events = data?.webhook_by_events; + this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); + + this.logger.verbose('Webhook loaded'); + } + + public async setWebhook(data: WebhookRaw) { + this.logger.verbose('Setting webhook'); + await this.repository.webhook.create(data, this.instanceName); + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + Object.assign(this.localWebhook, data); + this.logger.verbose('Webhook set'); + } + + public async findWebhook() { + this.logger.verbose('Finding webhook'); + const data = await this.repository.webhook.find(this.instanceName); + + if (!data) { + this.logger.verbose('Webhook not found'); + throw new NotFoundException('Webhook not found'); + } + + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + return data; + } + + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.localChatwoot.name_inbox = data?.name_inbox; + this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); + + this.localChatwoot.sign_msg = data?.sign_msg; + this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + + this.logger.verbose('Chatwoot loaded'); + } + + public async setChatwoot(data: ChatwootRaw) { + this.logger.verbose('Setting chatwoot'); + await this.repository.chatwoot.create(data, this.instanceName); + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + + Object.assign(this.localChatwoot, data); + this.logger.verbose('Chatwoot set'); + } + + public async findChatwoot() { + this.logger.verbose('Finding chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + + if (!data) { + this.logger.verbose('Chatwoot not found'); + return null; + } + + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + + return data; + } + + private async loadSettings() { + this.logger.verbose('Loading settings'); + const data = await this.repository.settings.find(this.instanceName); + this.localSettings.reject_call = data?.reject_call; + this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); + + this.localSettings.msg_call = data?.msg_call; + this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); + + this.localSettings.groups_ignore = data?.groups_ignore; + this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + + this.logger.verbose('Settings loaded'); + } + + public async setSettings(data: SettingsRaw) { + this.logger.verbose('Setting settings'); + await this.repository.settings.create(data, this.instanceName); + this.logger.verbose(`Settings reject_call: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + Object.assign(this.localSettings, data); + this.logger.verbose('Settings set'); + } + + public async findSettings() { + this.logger.verbose('Finding settings'); + const data = await this.repository.settings.find(this.instanceName); + + if (!data) { + this.logger.verbose('Settings not found'); + throw new NotFoundException('Settings not found'); + } + + this.logger.verbose(`Settings url: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + return data; + } + + public async sendDataWebhook(event: Events, data: T, local = true) { + const webhookGlobal = this.configService.get('WEBHOOK'); + const webhookLocal = this.localWebhook.events; + const serverUrl = this.configService.get('SERVER').URL; + const we = event.replace(/[.-]/gm, '_').toUpperCase(); + const transformedWe = we.replace(/_/gm, '-').toLowerCase(); + + const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; + const tokenStore = await this.repository.auth.find(this.instanceName); + const instanceApikey = tokenStore?.apikey || 'Apikey not found'; + + const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + if (local) { + if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { + this.logger.verbose('Sending data to webhook local'); + let baseURL; + + if (this.localWebhook.webhook_by_events) { + baseURL = `${this.localWebhook.url}/${transformedWe}`; + } else { + baseURL = this.localWebhook.url; + } + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-local', + url: baseURL, + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + + try { + if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { + const httpService = axios.create({ baseURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + }; + + if (expose && instanceApikey) { + postData['apikey'] = instanceApikey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-local', + 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]) { + this.logger.verbose('Sending data to webhook global'); + const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; + + let globalURL; + + if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { + globalURL = `${globalWebhook.URL}/${transformedWe}`; + } else { + globalURL = globalWebhook.URL; + } + + const localUrl = this.localWebhook.url; + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-global', + url: globalURL, + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + logData['apikey'] = globalApiKey; + } + + this.logger.log(logData); + } + + try { + if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { + const httpService = axios.create({ baseURL: globalURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + postData['apikey'] = globalApiKey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-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, + }); + } + } + } + } + + private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + this.logger.verbose('Connection update'); + if (qr) { + this.logger.verbose('QR code found'); + if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + this.logger.verbose('QR code limit reached'); + + this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); + } + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + state: 'refused', + statusReason: DisconnectReason.connectionClosed, + }); + + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + + this.logger.verbose('endSession defined as true'); + this.endSession = true; + + this.logger.verbose('Emmiting event logout.instance'); + return this.eventEmitter.emit('no.connection', this.instance.name); + } + + this.logger.verbose('Incrementing QR code count'); + this.instance.qrcode.count++; + + const optsQrcode: QRCodeToDataURLOptions = { + margin: 3, + scale: 4, + errorCorrectionLevel: 'H', + color: { light: '#ffffff', dark: '#198754' }, + }; + + console.log(this.phoneNumber); + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); + } else { + this.instance.qrcode.pairingCode = null; + } + + this.logger.verbose('Generating QR code'); + qrcode.toDataURL(qr, optsQrcode, (error, base64) => { + if (error) { + this.logger.error('Qrcode generate failed:' + error.toString()); + return; + } + + this.instance.qrcode.base64 = base64; + this.instance.qrcode.code = qr; + + this.sendDataWebhook(Events.QRCODE_UPDATED, { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }, + ); + } + }); + + this.logger.verbose('Generating QR code in terminal'); + qrcodeTerminal.generate(qr, { small: true }, (qrcode) => + this.logger.log( + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + qrcode, + ), + ); + } + + if (connection) { + this.logger.verbose('Connection found'); + this.stateConnection = { + state: connection, + statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + }; + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }, - ); - } - }); - - this.logger.verbose('Generating QR code in terminal'); - qrcodeTerminal.generate(qr, { small: true }, (qrcode) => - this.logger.log( - `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), - ); - } - - if (connection) { - this.logger.verbose('Connection found'); - this.stateConnection = { - state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, - }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - ...this.stateConnection, - }); - } - - if (connection === 'close') { - this.logger.verbose('Connection closed'); - const shouldReconnect = - (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - this.logger.verbose('Reconnecting to whatsapp'); - await this.connectToWhatsapp(); - } else { - this.logger.verbose('Do not reconnect to whatsapp'); - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); + ...this.stateConnection, + }); } - this.logger.verbose('Emittin event logout.instance'); - this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); - this.client?.ws?.close(); - this.client.end(new Error('Close connection')); - this.logger.verbose('Connection closed'); - } - } + if (connection === 'close') { + this.logger.verbose('Connection closed'); + const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + this.logger.verbose('Reconnecting to whatsapp'); + await this.connectToWhatsapp(); + } else { + this.logger.verbose('Do not reconnect to whatsapp'); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); - if (connection === 'open') { - this.logger.verbose('Connection opened'); - this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); - this.instance.profilePictureUrl = ( - await this.profilePicture(this.instance.wuid) - ).profilePictureUrl; - this.logger.info( - ` + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + + this.logger.verbose('Emittin event logout.instance'); + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.client?.ws?.close(); + this.client.end(new Error('Close connection')); + this.logger.verbose('Connection closed'); + } + } + + if (connection === 'open') { + this.logger.verbose('Connection opened'); + this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); + this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; + this.logger.info( + ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ └──────────────────────────────┘`.replace(/^ +/gm, ' '), - ); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.CONNECTION_UPDATE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'open', - }, - ); - } - } - } - - private async getMessage(key: proto.IMessageKey, full = false) { - this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); - try { - const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.name, key: { id: key.id } }, - })) as unknown as proto.IWebMessageInfo[]; - if (full) { - this.logger.verbose('Returning full message'); - return webMessageInfo[0]; - } - if (webMessageInfo[0].message?.pollCreationMessage) { - this.logger.verbose('Returning poll message'); - const messageSecretBase64 = - webMessageInfo[0].message?.messageContextInfo?.messageSecret; - - if (typeof messageSecretBase64 === 'string') { - const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - - const msg = { - messageContextInfo: { - messageSecret, - }, - pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, - }; - - return msg; - } - } - - this.logger.verbose('Returning message'); - return webMessageInfo[0].message; - } catch (error) { - return { conversation: '' }; - } - } - - private cleanStore() { - this.logger.verbose('Cronjob to clean store initialized'); - const cleanStore = this.configService.get('CLEAN_STORE'); - const database = this.configService.get('DATABASE'); - if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { - this.logger.verbose('Cronjob to clean store enabled'); - setInterval(() => { - try { - for (const [key, value] of Object.entries(cleanStore)) { - if (value === true) { - execSync( - `rm -rf ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - this.logger.verbose( - `Cleaned ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - } - } - } catch (error) { - this.logger.error(error); - } - }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); - } - } - - private async defineAuthState() { - this.logger.verbose('Defining auth state'); - const db = this.configService.get('DATABASE'); - const redis = this.configService.get('REDIS'); - - if (redis?.ENABLED) { - this.logger.verbose('Redis enabled'); - this.cache.reference = this.instance.name; - return await useMultiFileAuthStateRedisDb(this.cache); - } - - if (db.SAVE_DATA.INSTANCE && db.ENABLED) { - this.logger.verbose('Database enabled'); - return await useMultiFileAuthStateDb(this.instance.name); - } - - this.logger.verbose('Store file enabled'); - return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); - } - - public async connectToWhatsapp(number?: string): Promise { - this.logger.verbose('Connecting to whatsapp'); - try { - this.loadWebhook(); - this.loadChatwoot(); - this.loadSettings(); - - this.instance.authState = await this.defineAuthState(); - - const { version } = await fetchLatestBaileysVersion(); - this.logger.verbose('Baileys version: ' + version); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); - - const socketConfig: UserFacingSocketConfig = { - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore( - this.instance.authState.state.keys, - P({ level: 'error' }), - ), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser, - version, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => - (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: true, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!( - message.buttonsMessage || - message.listMessage || - message.templateMessage - ); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; - } - - return message; - }, - }; - - this.endSession = false; - - this.logger.verbose('Creating socket'); - - this.client = makeWASocket(socketConfig); - - this.logger.verbose('Socket created'); - - this.eventHandler(); - - this.logger.verbose('Socket event handler initialized'); - - this.phoneNumber = number; - - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } - - private readonly chatHandle = { - 'chats.upsert': async (chats: Chat[], database: Database) => { - this.logger.verbose('Event received: chats.upsert'); - - this.logger.verbose('Finding chats in database'); - const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if chats exists in database to insert'); - const chatsRaw: ChatRaw[] = []; - for await (const chat of chats) { - if (chatsRepository.find((cr) => cr.id === chat.id)) { - continue; - } - - chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); - } - - this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert( - chatsRaw, - this.instance.name, - database.SAVE_DATA.CHATS, - ); - }, - - 'chats.update': async ( - chats: Partial< - proto.IConversation & { - lastMessageRecvTimestamp?: number; - } & { - conditional: (bufferedData: BufferedEventData) => boolean; - } - >[], - ) => { - this.logger.verbose('Event received: chats.update'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { id: chat.id, owner: this.instance.wuid }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); - }, - - 'chats.delete': async (chats: string[]) => { - this.logger.verbose('Event received: chats.delete'); - - this.logger.verbose('Deleting chats in database'); - chats.forEach( - async (chat) => - await this.repository.chat.delete({ - where: { owner: this.instance.name, id: chat }, - }), - ); - - this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); - }, - }; - - private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[], database: Database) => { - this.logger.verbose('Event received: contacts.upsert'); - - this.logger.verbose('Finding contacts in database'); - const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - if (contactsRepository.find((cr) => cr.id === contact.id)) { - continue; - } - - contactsRaw.push({ - id: contact.id, - pushName: contact?.name || contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); - - this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert( - contactsRaw, - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - - 'contacts.update': async (contacts: Partial[], database: Database) => { - this.logger.verbose('Event received: contacts.update'); - - this.logger.verbose('Verifying if contacts exists in database to update'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - contactsRaw.push({ - id: contact.id, - pushName: contact?.name ?? contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - - this.logger.verbose('Updating contacts in database'); - await this.repository.contact.update( - contactsRaw, - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - }; - - private readonly messageHandle = { - 'messaging-history.set': async ( - { - messages, - chats, - isLatest, - }: { - chats: Chat[]; - contacts: Contact[]; - messages: proto.IWebMessageInfo[]; - isLatest: boolean; - }, - database: Database, - ) => { - this.logger.verbose('Event received: messaging-history.set'); - if (isLatest) { - this.logger.verbose('isLatest defined as true'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { - id: chat.id, - owner: this.instance.name, - lastMsgTimestamp: chat.lastMessageRecvTimestamp, - }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_SET'); - await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert( - chatsRaw, - this.instance.name, - database.SAVE_DATA.CHATS, - ); - } - - const messagesRaw: MessageRaw[] = []; - const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.name }, - }); - for await (const [, m] of Object.entries(messages)) { - if (!m.message) { - continue; - } - if ( - messagesRepository.find( - (mr) => mr.owner === this.instance.name && mr.key.id === m.key.id, - ) - ) { - continue; - } - - if (Long.isLong(m?.messageTimestamp)) { - m.messageTimestamp = m.messageTimestamp?.toNumber(); - } - - messagesRaw.push({ - key: m.key, - pushName: m.pushName, - participant: m.participant, - message: { ...m.message }, - messageType: getContentType(m.message), - messageTimestamp: m.messageTimestamp as number, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); - - messages = undefined; - }, - - 'messages.upsert': async ( - { - messages, - type, - }: { - messages: proto.IWebMessageInfo[]; - type: MessageUpsertType; - }, - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.upsert'); - const received = messages[0]; - - if ( - type !== 'notify' || - // received.message?.protocolMessage || - received.message?.pollUpdateMessage - ) { - this.logger.verbose('message rejected'); - return; - } - - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } - - if (settings.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - - const messageRaw: MessageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - database.SAVE_DATA.NEW_MESSAGE, - ); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) - .profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) - .profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update( - [contactRaw], - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - await this.repository.contact.insert( - [contactRaw], - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - - 'messages.update': async ( - args: WAMessageUpdate[], - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.update'); - const status: Record = { - 0: 'ERROR', - 1: 'PENDING', - 2: 'SERVER_ACK', - 3: 'DELIVERY_ACK', - 4: 'READ', - 5: 'PLAYED', - }; - for await (const { key, update } of args) { - if (settings.groups_ignore && key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { - this.logger.verbose('Message update is valid'); - - let pollUpdates: any; - if (update.pollUpdates) { - this.logger.verbose('Poll update found'); - - this.logger.verbose('Getting poll message'); - const pollCreation = await this.getMessage(key); - this.logger.verbose(pollCreation); - - if (pollCreation) { - this.logger.verbose('Getting aggregate votes in poll message'); - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); - } - } - - if (status[update.status] === 'READ' && !key.fromMe) return; - - if (update.message === null && update.status === undefined) { - this.logger.verbose('Message deleted'); - - this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - await this.sendDataWebhook(Events.MESSAGES_DELETE, key); - - const message: MessageUpdateRaw = { - ...key, - status: 'DELETED', - datetime: Date.now(), - owner: this.instance.name, - }; - - this.logger.verbose(message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, ); - return; - } - const message: MessageUpdateRaw = { - ...key, - status: status[update.status], - datetime: Date.now(), - owner: this.instance.name, - pollUpdates, - }; - - this.logger.verbose(message); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } } - } - }, - }; - - private readonly groupHandler = { - 'groups.upsert': (groupMetadata: GroupMetadata[]) => { - this.logger.verbose('Event received: groups.upsert'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); - this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); - }, - - 'groups.update': (groupMetadataUpdate: Partial[]) => { - this.logger.verbose('Event received: groups.update'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); - this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); - }, - - 'group-participants.update': (participantsUpdate: { - id: string; - participants: string[]; - action: ParticipantAction; - }) => { - this.logger.verbose('Event received: group-participants.update'); - - this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - }, - }; - - private eventHandler() { - this.logger.verbose('Initializing event handler'); - this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - this.logger.verbose('Listening event: call'); - const call = events.call[0]; - - if (settings?.reject_call && call.status == 'offer') { - this.logger.verbose('Rejecting call'); - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { - this.logger.verbose('Sending message in call'); - const msg = await this.client.sendMessage(call.from, { - text: settings.msg_call, - }); - - this.client.ev.emit('messages.upsert', { - messages: [msg], - type: 'notify', - }); - } - } - - if (events['connection.update']) { - this.logger.verbose('Listening event: connection.update'); - this.connectionUpdate(events['connection.update']); - } - - if (events['creds.update']) { - this.logger.verbose('Listening event: creds.update'); - this.instance.authState.saveCreds(); - } - - if (events['messaging-history.set']) { - this.logger.verbose('Listening event: messaging-history.set'); - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload, database); - } - - if (events['messages.upsert']) { - this.logger.verbose('Listening event: messages.upsert'); - const payload = events['messages.upsert']; - this.messageHandle['messages.upsert'](payload, database, settings); - } - - if (events['messages.update']) { - this.logger.verbose('Listening event: messages.update'); - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, database, settings); - } - - if (events['presence.update']) { - this.logger.verbose('Listening event: presence.update'); - const payload = events['presence.update']; - - if (settings.groups_ignore && payload.id.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } - - if (!settings?.groups_ignore) { - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } - - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } - - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); - } - } - - if (events['chats.upsert']) { - this.logger.verbose('Listening event: chats.upsert'); - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload, database); - } - - if (events['chats.update']) { - this.logger.verbose('Listening event: chats.update'); - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } - - if (events['chats.delete']) { - this.logger.verbose('Listening event: chats.delete'); - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } - - if (events['contacts.upsert']) { - this.logger.verbose('Listening event: contacts.upsert'); - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload, database); - } - - if (events['contacts.update']) { - this.logger.verbose('Listening event: contacts.update'); - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload, database); - } - } - }); - } - - // Check if the number is MX or AR - private formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; - } - return jid; - } - - // Check if the number is br - private formatBRNumber(jid: string) { - const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); - if (regexp.test(jid)) { - const match = regexp.exec(jid); - if (match && match[1] === '55') { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 31) { - return match[0]; - } - return match[1] + match[2] + match[3]; - } - return jid; - } else { - return jid; - } - } - - private createJid(number: string): string { - this.logger.verbose('Creating jid with number: ' + number); - - if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); - return number; } - if (number.includes('@broadcast')) { - this.logger.verbose('Number already contains @broadcast'); - return number; - } - - number = number - ?.replace(/\s/g, '') - .replace(/\+/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .split(':')[0] - .split('@')[0]; - - if (number.length >= 18) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - number = number.replace(/\D/g, ''); - - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); - return `${number}@s.whatsapp.net`; - } - - public async profilePicture(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile picture with jid: ' + jid); - try { - this.logger.verbose('Getting profile picture url'); - return { - wuid: jid, - profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), - }; - } catch (error) { - this.logger.verbose('Profile picture not found'); - return { - wuid: jid, - profilePictureUrl: null, - }; - } - } - - public async getStatus(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } - - public async fetchProfile(instanceName: string, number?: string) { - const jid = number ? this.createJid(number) : this.client?.user?.id; - - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - const info = await waMonitor.instanceInfo(instanceName); - const business = await this.fetchBusinessProfile(jid); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(jid); - const status = await this.getStatus(jid); - - return { - wuid: jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } - - private async sendMessageWithTyping( - number: string, - message: T, - options?: Options, - ) { - this.logger.verbose('Sending message with typing'); - - const numberWA = await this.whatsappNumber({ numbers: [number] }); - const isWA = numberWA[0]; - - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - try { - if (options?.delay) { - this.logger.verbose('Delaying message'); - - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); - this.logger.verbose( - 'Sending presence update: ' + options?.presence ?? 'composing', - ); - - await delay(options.delay); - this.logger.verbose('Set delay: ' + options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } - - const linkPreview = options?.linkPreview != false ? undefined : false; - - let quoted: WAMessage; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.message - ? m - : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - this.logger.verbose('Quoted message'); - } - - let mentions: string[]; - if (isJidGroup(sender)) { + private async getMessage(key: proto.IMessageKey, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); try { - const groupMetadata = await this.client.groupMetadata(sender); + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as proto.IWebMessageInfo[]; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; + } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; - if (!groupMetadata) { - throw new NotFoundException('Group not found'); - } + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - if (options?.mentions) { - this.logger.verbose('Mentions defined'); + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } + } + + private cleanStore() { + this.logger.verbose('Cronjob to clean store initialized'); + const cleanStore = this.configService.get('CLEAN_STORE'); + const database = this.configService.get('DATABASE'); + if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { + this.logger.verbose('Cronjob to clean store enabled'); + setInterval(() => { + try { + for (const [key, value] of Object.entries(cleanStore)) { + if (value === true) { + execSync( + `rm -rf ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); + this.logger.verbose( + `Cleaned ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); + } + } + } catch (error) { + this.logger.error(error); + } + }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); + } + } + + private async defineAuthState() { + this.logger.verbose('Defining auth state'); + const db = this.configService.get('DATABASE'); + const redis = this.configService.get('REDIS'); + + if (redis?.ENABLED) { + this.logger.verbose('Redis enabled'); + this.cache.reference = this.instance.name; + return await useMultiFileAuthStateRedisDb(this.cache); + } + + if (db.SAVE_DATA.INSTANCE && db.ENABLED) { + this.logger.verbose('Database enabled'); + return await useMultiFileAuthStateDb(this.instance.name); + } + + this.logger.verbose('Store file enabled'); + return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + } + + public async connectToWhatsapp(number?: string): Promise { + this.logger.verbose('Connecting to whatsapp'); + try { + this.loadWebhook(); + this.loadChatwoot(); + this.loadSettings(); + + this.instance.authState = await this.defineAuthState(); + + const { version } = await fetchLatestBaileysVersion(); + this.logger.verbose('Baileys version: ' + version); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + this.logger.verbose('Browser: ' + JSON.stringify(browser)); + + const socketConfig: UserFacingSocketConfig = { + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser, + version, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: true, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; + }, + }; + + this.endSession = false; + + this.logger.verbose('Creating socket'); + + this.client = makeWASocket(socketConfig); + + this.logger.verbose('Socket created'); + + this.eventHandler(); + + this.logger.verbose('Socket event handler initialized'); + + this.phoneNumber = number; + + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } + } + + private readonly chatHandle = { + 'chats.upsert': async (chats: Chat[], database: Database) => { + this.logger.verbose('Event received: chats.upsert'); + + this.logger.verbose('Finding chats in database'); + const chatsRepository = await this.repository.chat.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if chats exists in database to insert'); + const chatsRaw: ChatRaw[] = []; + for await (const chat of chats) { + if (chatsRepository.find((cr) => cr.id === chat.id)) { + continue; + } + + chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); + } + + this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); + await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + }, + + 'chats.update': async ( + chats: Partial< + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { + conditional: (bufferedData: BufferedEventData) => boolean; + } + >[], + ) => { + this.logger.verbose('Event received: chats.update'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { id: chat.id, owner: this.instance.wuid }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); + await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + }, + + 'chats.delete': async (chats: string[]) => { + this.logger.verbose('Event received: chats.delete'); + + this.logger.verbose('Deleting chats in database'); + chats.forEach( + async (chat) => + await this.repository.chat.delete({ + where: { owner: this.instance.name, id: chat }, + }), + ); + + this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); + await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + }, + }; + + private readonly contactHandle = { + 'contacts.upsert': async (contacts: Contact[], database: Database) => { + this.logger.verbose('Event received: contacts.upsert'); + + this.logger.verbose('Finding contacts in database'); + const contactsRepository = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if contacts exists in database to insert'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + if (contactsRepository.find((cr) => cr.id === contact.id)) { + continue; + } + + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Inserting contacts in database'); + await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + + 'contacts.update': async (contacts: Partial[], database: Database) => { + this.logger.verbose('Event received: contacts.update'); + + this.logger.verbose('Verifying if contacts exists in database to update'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name ?? contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + }; + + private readonly messageHandle = { + 'messaging-history.set': async ( + { + messages, + chats, + isLatest, + }: { + chats: Chat[]; + contacts: Contact[]; + messages: proto.IWebMessageInfo[]; + isLatest: boolean; + }, + database: Database, + ) => { + this.logger.verbose('Event received: messaging-history.set'); + if (isLatest) { + this.logger.verbose('isLatest defined as true'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { + id: chat.id, + owner: this.instance.name, + lastMsgTimestamp: chat.lastMessageRecvTimestamp, + }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_SET'); + await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + } + + const messagesRaw: MessageRaw[] = []; + const messagesRepository = await this.repository.message.find({ + where: { owner: this.instance.name }, + }); + for await (const [, m] of Object.entries(messages)) { + if (!m.message) { + continue; + } + if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { + continue; + } + + if (Long.isLong(m?.messageTimestamp)) { + m.messageTimestamp = m.messageTimestamp?.toNumber(); + } + + messagesRaw.push({ + key: m.key, + pushName: m.pushName, + participant: m.participant, + message: { ...m.message }, + messageType: getContentType(m.message), + messageTimestamp: m.messageTimestamp as number, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); + + messages = undefined; + }, + + 'messages.upsert': async ( + { + messages, + type, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + }, + database: Database, + settings: SettingsRaw, + ) => { + this.logger.verbose('Event received: messages.upsert'); + const received = messages[0]; if ( - !Array.isArray(options.mentions.mentioned) && - !options.mentions.everyOne + type !== 'notify' || + // received.message?.protocolMessage || + received.message?.pollUpdateMessage ) { - throw new BadRequestException('Mentions must be an array'); + this.logger.verbose('message rejected'); + return; } - if (options.mentions.everyOne) { - this.logger.verbose('Mentions everyone'); - - this.logger.verbose('Getting group metadata'); - mentions = groupMetadata.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - return null; - // throw new BadRequestException('Mentions must be a number'); - } - return jid; - }); + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); } - } - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - const messageSent = await (async () => { - const option = { - quoted, - }; - - if ( - !message['audio'] && - !message['poll'] && - !message['sticker'] && - !message['conversation'] && - sender !== 'status@broadcast' - ) { - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } - } - - if (message['conversation']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - text: message['conversation'], - mentions, - linkPreview: linkPreview, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (sender === 'status@broadcast') { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message['status'].content as unknown as AnyMessageContent, - { - backgroundColor: message['status'].option.backgroundColor, - font: message['status'].option.font, - statusJidList: message['status'].option.statusJidList, - } as unknown as MiscMessageGenerationOptions, - ); - } - - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - })(); - - const messageRaw: MessageRaw = { - key: messageSent.key, - pushName: messageSent.pushName, - message: { ...messageSent.message }, - messageType: getContentType(messageSent.message), - messageTimestamp: messageSent.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(messageSent.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - // if (this.localChatwoot.enabled) { - // this.chatwootService.eventWhatsapp( - // Events.SEND_MESSAGE, - // { instanceName: this.instance.name }, - // messageRaw, - // ); - // } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, - ); - - return messageSent; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Instance Controller - public get connectionStatus() { - this.logger.verbose('Getting connection status'); - return this.stateConnection; - } - - // Send Message Controller - public async textMessage(data: SendTextDto) { - this.logger.verbose('Sending text message'); - return await this.sendMessageWithTyping( - data.number, - { - conversation: data.textMessage.text, - }, - data?.options, - ); - } - - public async pollMessage(data: SendPollDto) { - this.logger.verbose('Sending poll message'); - return await this.sendMessageWithTyping( - data.number, - { - poll: { - name: data.pollMessage.name, - selectableCount: data.pollMessage.selectableCount, - values: data.pollMessage.values, - }, - }, - data?.options, - ); - } - - private async formatStatusMessage(status: StatusMessage) { - this.logger.verbose('Formatting status message'); - - if (!status.type) { - throw new BadRequestException('Type is required'); - } - - if (!status.content) { - throw new BadRequestException('Content is required'); - } - - if (status.allContacts) { - this.logger.verbose('All contacts defined as true'); - - this.logger.verbose('Getting contacts from database'); - const contacts = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - if (!contacts.length) { - throw new BadRequestException('Contacts not found'); - } - - this.logger.verbose('Getting contacts with push name'); - status.statusJidList = contacts - .filter((contact) => contact.pushName) - .map((contact) => contact.id); - - this.logger.verbose(status.statusJidList); - } - - if (!status.statusJidList?.length && !status.allContacts) { - throw new BadRequestException('StatusJidList is required'); - } - - if (status.type === 'text') { - this.logger.verbose('Type defined as text'); - - if (!status.backgroundColor) { - throw new BadRequestException('Background color is required'); - } - - if (!status.font) { - throw new BadRequestException('Font is required'); - } - - return { - content: { - text: status.content, - }, - option: { - backgroundColor: status.backgroundColor, - font: status.font, - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'image') { - this.logger.verbose('Type defined as image'); - - return { - content: { - image: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'video') { - this.logger.verbose('Type defined as video'); - - return { - content: { - video: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'audio') { - this.logger.verbose('Type defined as audio'); - - this.logger.verbose('Processing audio'); - const convert = await this.processAudio(status.content, 'status@broadcast'); - if (typeof convert === 'string') { - this.logger.verbose('Audio processed'); - const audio = fs.readFileSync(convert).toString('base64'); - - const result = { - content: { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - option: { - statusJidList: status.statusJidList, - }, - }; - - fs.unlinkSync(convert); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - throw new BadRequestException('Type not found'); - } - - public async statusMessage(data: SendStatusDto) { - this.logger.verbose('Sending status message'); - const status = await this.formatStatusMessage(data.statusMessage); - - return await this.sendMessageWithTyping('status@broadcast', { - status, - }); - } - - private async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - this.logger.verbose('Preparing media message'); - const prepareMedia = await prepareWAMessageMedia( - { - [mediaMessage.mediatype]: isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'), - } as any, - { upload: this.client.waUploadToServer }, - ); - - const mediaType = mediaMessage.mediatype + 'Message'; - this.logger.verbose('Media type: ' + mediaType); - - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - this.logger.verbose( - 'If media type is document and file name is not defined then', - ); - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - this.logger.verbose('File name: ' + mediaMessage.fileName); - } - - let mimetype: string; - - if (isURL(mediaMessage.media)) { - mimetype = getMIMEType(mediaMessage.media); - } else { - mimetype = getMIMEType(mediaMessage.fileName); - } - - this.logger.verbose('Mimetype: ' + mimetype); - - prepareMedia[mediaType].caption = mediaMessage?.caption; - prepareMedia[mediaType].mimetype = mimetype; - prepareMedia[mediaType].fileName = mediaMessage.fileName; - - if (mediaMessage.mediatype === 'video') { - this.logger.verbose('Is media type video then set gif playback as false'); - prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( - readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), - ); - prepareMedia[mediaType].gifPlayback = false; - } - - this.logger.verbose('Generating wa message from content'); - return generateWAMessageFromContent( - '', - { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, - ); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } - } - - private async convertToWebP(image: string, number: string) { - try { - this.logger.verbose('Converting image to WebP to sticker'); - - let imagePath: string; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to image name: ' + hash); - - const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; - this.logger.verbose('Output path: ' + outputPath); - - if (isBase64(image)) { - this.logger.verbose('Image is base64'); - - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); - const imageBuffer = Buffer.from(base64Data, 'base64'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } else { - this.logger.verbose('Image is url'); - - const timestamp = new Date().getTime(); - const url = `${image}?timestamp=${timestamp}`; - this.logger.verbose('including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting image from url'); - - const imageBuffer = Buffer.from(response.data, 'binary'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } - - await sharp(imagePath).webp().toFile(outputPath); - this.logger.verbose('Image converted to WebP'); - - fs.unlinkSync(imagePath); - this.logger.verbose('Temp image deleted'); - - return outputPath; - } catch (error) { - console.error('Erro ao converter a imagem para WebP:', error); - } - } - - public async mediaSticker(data: SendStickerDto) { - this.logger.verbose('Sending media sticker'); - const convert = await this.convertToWebP(data.stickerMessage.image, data.number); - const result = await this.sendMessageWithTyping( - data.number, - { - sticker: { url: convert }, - }, - data?.options, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted image deleted'); - - return result; - } - - public async mediaMessage(data: SendMediaDto) { - this.logger.verbose('Sending media message'); - const generate = await this.prepareMediaMessage(data.mediaMessage); - - return await this.sendMessageWithTyping( - data.number, - { ...generate.message }, - data?.options, - ); - } - - private async processAudio(audio: string, number: string) { - this.logger.verbose('Processing audio'); - let tempAudioPath: string; - let outputAudio: string; - - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to audio name: ' + hash); - - if (isURL(audio)) { - this.logger.verbose('Audio is url'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const timestamp = new Date().getTime(); - const url = `${audio}?timestamp=${timestamp}`; - - this.logger.verbose('Including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting audio from url'); - - fs.writeFileSync(tempAudioPath, response.data); - } else { - this.logger.verbose('Audio is base64'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const audioBuffer = Buffer.from(audio, 'base64'); - fs.writeFileSync(tempAudioPath, audioBuffer); - this.logger.verbose('Temp audio created'); - } - - this.logger.verbose('Converting audio to mp4'); - return new Promise((resolve, reject) => { - exec( - `${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, - (error, _stdout, _stderr) => { - fs.unlinkSync(tempAudioPath); - this.logger.verbose('Temp audio deleted'); - - if (error) reject(error); - - this.logger.verbose('Audio converted to mp4'); - resolve(outputAudio); - }, - ); - }); - } - - public async audioWhatsapp(data: SendAudioDto) { - this.logger.verbose('Sending audio whatsapp'); - - if (!data.options?.encoding && data.options?.encoding !== false) { - data.options.encoding = true; - } - - if (data.options?.encoding) { - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - return await this.sendMessageWithTyping( - data.number, - { - audio: isURL(data.audioMessage.audio) - ? { url: data.audioMessage.audio } - : Buffer.from(data.audioMessage.audio, 'base64'), - ptt: true, - mimetype: 'audio/ogg; codecs=opus', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - } - - public async buttonMessage(data: SendButtonDto) { - this.logger.verbose('Sending button message'); - const embeddedMedia: any = {}; - let mediatype = 'TEXT'; - - if (data.buttonMessage?.mediaMessage) { - mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; - embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; - const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); - embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; - embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; - } - - const btnItems = { - text: data.buttonMessage.buttons.map((btn) => btn.buttonText), - ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), - }; - - if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { - throw new BadRequestException( - 'Button texts cannot be repeated', - 'Button IDs cannot be repeated.', - ); - } - - return await this.sendMessageWithTyping( - data.number, - { - buttonsMessage: { - text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, - contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, - footerText: data.buttonMessage?.footerText, - buttons: data.buttonMessage.buttons.map((button) => { - return { - buttonText: { - displayText: button.buttonText, - }, - buttonId: button.buttonId, - type: 1, + if (settings.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + + const messageRaw: MessageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), }; - }), - headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], - [embeddedMedia?.mediaKey]: embeddedMedia?.message, + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); }, - }, - data?.options, - ); - } - public async locationMessage(data: SendLocationDto) { - this.logger.verbose('Sending location message'); - return await this.sendMessageWithTyping( - data.number, - { - locationMessage: { - degreesLatitude: data.locationMessage.latitude, - degreesLongitude: data.locationMessage.longitude, - name: data.locationMessage?.name, - address: data.locationMessage?.address, + 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { + this.logger.verbose('Event received: messages.update'); + const status: Record = { + 0: 'ERROR', + 1: 'PENDING', + 2: 'SERVER_ACK', + 3: 'DELIVERY_ACK', + 4: 'READ', + 5: 'PLAYED', + }; + for await (const { key, update } of args) { + if (settings.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + let pollUpdates: any; + if (update.pollUpdates) { + this.logger.verbose('Poll update found'); + + this.logger.verbose('Getting poll message'); + const pollCreation = await this.getMessage(key); + this.logger.verbose(pollCreation); + + if (pollCreation) { + this.logger.verbose('Getting aggregate votes in poll message'); + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } + } + + if (status[update.status] === 'READ' && !key.fromMe) return; + + if (update.message === null && update.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: status[update.status], + datetime: Date.now(), + owner: this.instance.name, + pollUpdates, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + } + } }, - }, - data?.options, - ); - } - - public async listMessage(data: SendListDto) { - this.logger.verbose('Sending list message'); - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.listMessage.title, - description: data.listMessage.description, - buttonText: data.listMessage?.buttonText, - footerText: data.listMessage?.footerText, - sections: data.listMessage.sections, - listType: 1, - }, - }, - data?.options, - ); - } - - public async contactMessage(data: SendContactDto) { - this.logger.verbose('Sending contact message'); - const message: proto.IMessage = {}; - - const vcard = (contact: ContactMessage) => { - this.logger.verbose('Creating vcard'); - let result = - 'BEGIN:VCARD\n' + - 'VERSION:3.0\n' + - `N:${contact.fullName}\n` + - `FN:${contact.fullName}\n`; - - if (contact.organization) { - this.logger.verbose('Organization defined'); - result += `ORG:${contact.organization};\n`; - } - - if (contact.email) { - this.logger.verbose('Email defined'); - result += `EMAIL:${contact.email}\n`; - } - - if (contact.url) { - this.logger.verbose('Url defined'); - result += `URL:${contact.url}\n`; - } - - if (!contact.wuid) { - this.logger.verbose('Wuid defined'); - contact.wuid = this.createJid(contact.phoneNumber); - } - - result += - `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + - 'item1.X-ABLabel:Celular\n' + - 'END:VCARD'; - - this.logger.verbose('Vcard created'); - return result; }; - if (data.contactMessage.length === 1) { - message.contactMessage = { - displayName: data.contactMessage[0].fullName, - vcard: vcard(data.contactMessage[0]), - }; - } else { - message.contactsArrayMessage = { - displayName: `${data.contactMessage.length} contacts`, - contacts: data.contactMessage.map((contact) => { - return { - displayName: contact.fullName, - vcard: vcard(contact), - }; - }), - }; + private readonly groupHandler = { + 'groups.upsert': (groupMetadata: GroupMetadata[]) => { + this.logger.verbose('Event received: groups.upsert'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); + this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); + }, + + 'groups.update': (groupMetadataUpdate: Partial[]) => { + this.logger.verbose('Event received: groups.update'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); + this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); + }, + + 'group-participants.update': (participantsUpdate: { + id: string; + participants: string[]; + action: ParticipantAction; + }) => { + this.logger.verbose('Event received: group-participants.update'); + + this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); + }, + }; + + private eventHandler() { + this.logger.verbose('Initializing event handler'); + this.client.ev.process(async (events) => { + if (!this.endSession) { + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); + + if (events.call) { + this.logger.verbose('Listening event: call'); + const call = events.call[0]; + + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } + + if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + const msg = await this.client.sendMessage(call.from, { + text: settings.msg_call, + }); + + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); + } + } + + if (events['connection.update']) { + this.logger.verbose('Listening event: connection.update'); + this.connectionUpdate(events['connection.update']); + } + + if (events['creds.update']) { + this.logger.verbose('Listening event: creds.update'); + this.instance.authState.saveCreds(); + } + + if (events['messaging-history.set']) { + this.logger.verbose('Listening event: messaging-history.set'); + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload, database); + } + + if (events['messages.upsert']) { + this.logger.verbose('Listening event: messages.upsert'); + const payload = events['messages.upsert']; + this.messageHandle['messages.upsert'](payload, database, settings); + } + + if (events['messages.update']) { + this.logger.verbose('Listening event: messages.update'); + const payload = events['messages.update']; + this.messageHandle['messages.update'](payload, database, settings); + } + + if (events['presence.update']) { + this.logger.verbose('Listening event: presence.update'); + const payload = events['presence.update']; + + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } + + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } + + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } + + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } + + if (events['chats.upsert']) { + this.logger.verbose('Listening event: chats.upsert'); + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload, database); + } + + if (events['chats.update']) { + this.logger.verbose('Listening event: chats.update'); + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } + + if (events['chats.delete']) { + this.logger.verbose('Listening event: chats.delete'); + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } + + if (events['contacts.upsert']) { + this.logger.verbose('Listening event: contacts.upsert'); + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload, database); + } + + if (events['contacts.update']) { + this.logger.verbose('Listening event: contacts.update'); + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload, database); + } + } + }); } - return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); - } + // Check if the number is MX or AR + private formatMXOrARNumber(jid: string): string { + const countryCode = jid.substring(0, 2); - public async reactionMessage(data: SendReactionDto) { - this.logger.verbose('Sending reaction message'); - return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { - reactionMessage: { - key: data.reactionMessage.key, - text: data.reactionMessage.reaction, - }, - }); - } + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + if (jid.length === 13) { + const number = countryCode + jid.substring(3); + return number; + } - // Chat Controller - public async whatsappNumber(data: WhatsAppNumberDto) { - this.logger.verbose('Getting whatsapp number'); + return jid; + } + return jid; + } - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); - - if (isJidGroup(jid)) { - const group = await this.findGroup({ groupJid: jid }, 'inner'); - - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); + // Check if the number is br + private formatBRNumber(jid: string) { + const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); + if (regexp.test(jid)) { + const match = regexp.exec(jid); + if (match && match[1] === '55') { + const joker = Number.parseInt(match[3][0]); + const ddd = Number.parseInt(match[2]); + if (joker < 7 || ddd < 31) { + return match[0]; + } + return match[1] + match[2] + match[3]; + } + return jid; } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); + return jid; } - } } - return onWhatsapp; - } + private createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); - public async markMessageAsRead(data: ReadMessageDto) { - this.logger.verbose('Marking message as read'); - try { - const keys: proto.IMessageKey[] = []; - data.readMessages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { - keys.push({ - remoteJid: read.remoteJid, - fromMe: read.fromMe, - id: read.id, - }); + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); + return number; } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); - } - } - public async archiveChat(data: ArchiveChatDto) { - this.logger.verbose('Archiving chat'); - try { - data.lastMessage.messageTimestamp = - data.lastMessage?.messageTimestamp ?? Date.now(); - await this.client.chatModify( - { - archive: data.archive, - lastMessages: [data.lastMessage], - }, - data.lastMessage.key.remoteJid, - ); - - return { - chatId: data.lastMessage.key.remoteJid, - archived: true, - }; - } catch (error) { - throw new InternalServerErrorException({ - archived: false, - message: [ - 'An error occurred while archiving the chat. Open a calling.', - error.toString(), - ], - }); - } - } - - public async deleteMessage(del: DeleteMessage) { - this.logger.verbose('Deleting message'); - try { - return await this.client.sendMessage(del.remoteJid, { delete: del }); - } catch (error) { - throw new InternalServerErrorException( - 'Error while deleting message for everyone', - error?.toString(), - ); - } - } - - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { - this.logger.verbose('Getting base64 from media message'); - try { - const m = data?.message; - const convertToMp4 = data?.convertToMp4 ?? false; - - const msg = m?.message - ? m - : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - for (const subtype of MessageSubtype) { - if (msg.message[subtype]) { - msg.message = msg.message[subtype].message; + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; } - } - let mediaMessage: any; - let mediaType: string; + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(':')[0] + .split('@')[0]; - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; } - } - if (!mediaMessage) { - throw 'The message is not of the media type'; - } + number = number.replace(/\D/g, ''); - if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message = JSON.parse(JSON.stringify(msg.message)); - } + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } - this.logger.verbose('Downloading media message'); - const buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }), - reuploadRequest: this.client.updateMediaMessage, - }, - ); - const typeMessage = getContentType(msg.message); + public async profilePicture(number: string) { + const jid = this.createJid(number); - if (convertToMp4 && typeMessage === 'audioMessage') { - this.logger.verbose('Converting audio to mp4'); - const number = msg.key.remoteJid.split('@')[0]; - const convert = await this.processAudio(buffer.toString('base64'), number); + this.logger.verbose('Getting profile picture with jid: ' + jid); + try { + this.logger.verbose('Getting profile picture url'); + return { + wuid: jid, + profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), + }; + } catch (error) { + this.logger.verbose('Profile picture not found'); + return { + wuid: jid, + profilePictureUrl: null, + }; + } + } - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - this.logger.verbose('Audio converted to mp4'); + public async getStatus(number: string) { + const jid = this.createJid(number); - const result = { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const info = await waMonitor.instanceInfo(instanceName); + const business = await this.fetchBusinessProfile(jid); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + + private async sendMessageWithTyping(number: string, message: T, options?: Options) { + this.logger.verbose('Sending message with typing'); + + const numberWA = await this.whatsappNumber({ numbers: [number] }); + const isWA = numberWA[0]; + + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + try { + if (options?.delay) { + this.logger.verbose('Delaying message'); + + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); + + await delay(options.delay); + this.logger.verbose('Set delay: ' + options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } + + const linkPreview = options?.linkPreview != false ? undefined : false; + + let quoted: WAMessage; + + if (options?.quoted) { + const m = options?.quoted; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let mentions: string[]; + if (isJidGroup(sender)) { + try { + const groupMetadata = await this.client.groupMetadata(sender); + + if (!groupMetadata) { + throw new NotFoundException('Group not found'); + } + + if (options?.mentions) { + this.logger.verbose('Mentions defined'); + + if (!Array.isArray(options.mentions.mentioned) && !options.mentions.everyOne) { + throw new BadRequestException('Mentions must be an array'); + } + + if (options.mentions.everyOne) { + this.logger.verbose('Mentions everyone'); + + this.logger.verbose('Getting group metadata'); + mentions = groupMetadata.participants.map((participant) => participant.id); + this.logger.verbose('Getting group metadata for mentions'); + } else { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + return null; + // throw new BadRequestException('Mentions must be a number'); + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); + } + } + + const messageSent = await (async () => { + const option = { + quoted, + }; + + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + !message['conversation'] && + sender !== 'status@broadcast' + ) { + if (!message['audio']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, + }, + mentions, + }, + option as unknown as MiscMessageGenerationOptions, + ); + } + } + + if (message['conversation']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + text: message['conversation'], + mentions, + linkPreview: linkPreview, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + + if (sender === 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, + { + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); + } + + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + })(); + + const messageRaw: MessageRaw = { + key: messageSent.key, + pushName: messageSent.pushName, + message: { ...messageSent.message }, + messageType: getContentType(messageSent.message), + messageTimestamp: messageSent.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + // if (this.localChatwoot.enabled) { + // this.chatwootService.eventWhatsapp( + // Events.SEND_MESSAGE, + // { instanceName: this.instance.name }, + // messageRaw, + // ); + // } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + return messageSent; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Instance Controller + public get connectionStatus() { + this.logger.verbose('Getting connection status'); + return this.stateConnection; + } + + // Send Message Controller + public async textMessage(data: SendTextDto) { + this.logger.verbose('Sending text message'); + return await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, }, - mimetype: 'audio/mp4', - base64: Buffer.from(audio, 'base64').toString('base64'), - }; + data?.options, + ); + } - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); + public async pollMessage(data: SendPollDto) { + this.logger.verbose('Sending poll message'); + return await this.sendMessageWithTyping( + data.number, + { + poll: { + name: data.pollMessage.name, + selectableCount: data.pollMessage.selectableCount, + values: data.pollMessage.values, + }, + }, + data?.options, + ); + } - this.logger.verbose('Media message downloaded'); - return result; + private async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); } - } - this.logger.verbose('Media message downloaded'); - return { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: mediaMessage['mimetype'], - base64: buffer.toString('base64'), - }; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - public async fetchContacts(query: ContactQuery) { - this.logger.verbose('Fetching contacts'); - if (query?.where) { - query.where.owner = this.instance.name; - if (query.where?.id) { - query.where.id = this.createJid(query.where.id); - } - } else { - query = { - where: { - owner: this.instance.name, - }, - }; - } - return await this.repository.contact.find(query); - } - - public async fetchMessages(query: MessageQuery) { - this.logger.verbose('Fetching messages'); - if (query?.where) { - if (query.where?.key?.remoteJid) { - query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.message.find(query); - } - - public async fetchStatusMessage(query: MessageUpQuery) { - this.logger.verbose('Fetching status messages'); - if (query?.where) { - if (query.where?.remoteJid) { - query.where.remoteJid = this.createJid(query.where.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.messageUpdate.find(query); - } - - public async fetchChats() { - this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.name } }); - } - - public async fetchPrivacySettings() { - this.logger.verbose('Fetching privacy settings'); - return await this.client.fetchPrivacySettings(); - } - - public async updatePrivacySettings(settings: PrivacySettingDto) { - this.logger.verbose('Updating privacy settings'); - try { - await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); - this.logger.verbose('Read receipts privacy updated'); - - await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); - this.logger.verbose('Profile picture privacy updated'); - - await this.client.updateStatusPrivacy(settings.privacySettings.status); - this.logger.verbose('Status privacy updated'); - - await this.client.updateOnlinePrivacy(settings.privacySettings.online); - this.logger.verbose('Online privacy updated'); - - await this.client.updateLastSeenPrivacy(settings.privacySettings.last); - this.logger.verbose('Last seen privacy updated'); - - await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); - this.logger.verbose('Groups add privacy updated'); - - // reinicia a instancia - this.client?.ws?.close(); - - return { - update: 'success', - data: { - readreceipts: settings.privacySettings.readreceipts, - profile: settings.privacySettings.profile, - status: settings.privacySettings.status, - online: settings.privacySettings.online, - last: settings.privacySettings.last, - groupadd: settings.privacySettings.groupadd, - }, - }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating privacy settings', - error.toString(), - ); - } - } - - public async fetchBusinessProfile(number: string): Promise { - this.logger.verbose('Fetching business profile'); - try { - const jid = number ? this.createJid(number) : this.instance.wuid; - - const profile = await this.client.getBusinessProfile(jid); - this.logger.verbose('Trying to get business profile'); - - if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); - - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift(), - }; - } - - this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile, - }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile name', - error.toString(), - ); - } - } - - public async updateProfileName(name: string) { - this.logger.verbose('Updating profile name to ' + name); - try { - await this.client.updateProfileName(name); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile name', - error.toString(), - ); - } - } - - public async updateProfileStatus(status: string) { - this.logger.verbose('Updating profile status to: ' + status); - try { - await this.client.updateProfileStatus(status); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile status', - error.toString(), - ); - } - } - - public async updateProfilePicture(picture: string) { - this.logger.verbose('Updating profile picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(this.instance.wuid, pic); - this.logger.verbose('Profile picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile picture', - error.toString(), - ); - } - } - - public async removeProfilePicture() { - this.logger.verbose('Removing profile picture'); - try { - await this.client.removeProfilePicture(this.instance.wuid); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error removing profile picture', - error.toString(), - ); - } - } - - // Group - public async createGroup(create: CreateGroupDto) { - this.logger.verbose('Creating group: ' + create.subject); - try { - const participants = create.participants.map((p) => this.createJid(p)); - const { id } = await this.client.groupCreate(create.subject, participants); - this.logger.verbose('Group created: ' + id); - - if (create?.description) { - this.logger.verbose('Updating group description: ' + create.description); - await this.client.groupUpdateDescription(id, create.description); - } - - const group = await this.client.groupMetadata(id); - this.logger.verbose('Getting group metadata'); - - return { groupMetadata: group }; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); - } - } - - public async updateGroupPicture(picture: GroupPictureDto) { - this.logger.verbose('Updating group picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture.image)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture.image}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture.image)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture.image, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(picture.groupJid, pic); - this.logger.verbose('Group picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error update group picture', - error.toString(), - ); - } - } - - public async updateGroupSubject(data: GroupSubjectDto) { - this.logger.verbose('Updating group subject to: ' + data.subject); - try { - await this.client.groupUpdateSubject(data.groupJid, data.subject); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating group subject', - error.toString(), - ); - } - } - - public async updateGroupDescription(data: GroupDescriptionDto) { - this.logger.verbose('Updating group description to: ' + data.description); - try { - await this.client.groupUpdateDescription(data.groupJid, data.description); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating group description', - error.toString(), - ); - } - } - - public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { - this.logger.verbose('Fetching group'); - try { - return await this.client.groupMetadata(id.groupJid); - } catch (error) { - if (reply === 'inner') { - return; - } - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async fetchAllGroups(getParticipants: GetParticipant) { - this.logger.verbose('Fetching all groups'); - try { - const fetch = Object.values(await this.client.groupFetchAllParticipating()); - - const groups = fetch.map((group) => { - const result = { - id: group.id, - subject: group.subject, - subjectOwner: group.subjectOwner, - subjectTime: group.subjectTime, - size: group.size, - creation: group.creation, - owner: group.owner, - desc: group.desc, - descId: group.descId, - restrict: group.restrict, - announce: group.announce, - }; - - if (getParticipants.getParticipants == 'true') { - result['participants'] = group.participants; + if (!status.content) { + throw new BadRequestException('Content is required'); } + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); + + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); + } + + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); + } + + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); + + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } + + if (!status.font) { + throw new BadRequestException('Font is required'); + } + + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); + + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); + + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); + + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); + + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; + + fs.unlinkSync(convert); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + const status = await this.formatStatusMessage(data.statusMessage); + + return await this.sendMessageWithTyping('status@broadcast', { + status, + }); + } + + private async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + const prepareMedia = await prepareWAMessageMedia( + { + [mediaMessage.mediatype]: isURL(mediaMessage.media) + ? { url: mediaMessage.media } + : Buffer.from(mediaMessage.media, 'base64'), + } as any, + { upload: this.client.waUploadToServer }, + ); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose('If media type is document and file name is not defined then'); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + let mimetype: string; + + if (isURL(mediaMessage.media)) { + mimetype = getMIMEType(mediaMessage.media); + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } + + this.logger.verbose('Mimetype: ' + mimetype); + + prepareMedia[mediaType].caption = mediaMessage?.caption; + prepareMedia[mediaType].mimetype = mimetype; + prepareMedia[mediaType].fileName = mediaMessage.fileName; + + if (mediaMessage.mediatype === 'video') { + this.logger.verbose('Is media type video then set gif playback as false'); + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), + ); + prepareMedia[mediaType].gifPlayback = false; + } + + this.logger.verbose('Generating wa message from content'); + return generateWAMessageFromContent( + '', + { [mediaType]: { ...prepareMedia[mediaType] } }, + { userJid: this.instance.wuid }, + ); + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + private async convertToWebP(image: string, number: string) { + try { + this.logger.verbose('Converting image to WebP to sticker'); + + let imagePath: string; + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to image name: ' + hash); + + const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; + this.logger.verbose('Output path: ' + outputPath); + + if (isBase64(image)) { + this.logger.verbose('Image is base64'); + + const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const imageBuffer = Buffer.from(base64Data, 'base64'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } else { + this.logger.verbose('Image is url'); + + const timestamp = new Date().getTime(); + const url = `${image}?timestamp=${timestamp}`; + this.logger.verbose('including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting image from url'); + + const imageBuffer = Buffer.from(response.data, 'binary'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } + + await sharp(imagePath).webp().toFile(outputPath); + this.logger.verbose('Image converted to WebP'); + + fs.unlinkSync(imagePath); + this.logger.verbose('Temp image deleted'); + + return outputPath; + } catch (error) { + console.error('Erro ao converter a imagem para WebP:', error); + } + } + + public async mediaSticker(data: SendStickerDto) { + this.logger.verbose('Sending media sticker'); + const convert = await this.convertToWebP(data.stickerMessage.image, data.number); + const result = await this.sendMessageWithTyping( + data.number, + { + sticker: { url: convert }, + }, + data?.options, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted image deleted'); + return result; - }); - - return groups; - } catch (error) { - throw new NotFoundException('Error fetching group', error.toString()); } - } - public async inviteCode(id: GroupJid) { - this.logger.verbose('Fetching invite code for group: ' + id.groupJid); - try { - const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { - throw new NotFoundException('No invite code', error.toString()); + public async mediaMessage(data: SendMediaDto) { + this.logger.verbose('Sending media message'); + const generate = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options); } - } - public async inviteInfo(id: GroupInvite) { - this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); - try { - return await this.client.groupGetInviteInfo(id.inviteCode); - } catch (error) { - throw new NotFoundException('No invite info', id.inviteCode); + private async processAudio(audio: string, number: string) { + this.logger.verbose('Processing audio'); + let tempAudioPath: string; + let outputAudio: string; + + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to audio name: ' + hash); + + if (isURL(audio)) { + this.logger.verbose('Audio is url'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const timestamp = new Date().getTime(); + const url = `${audio}?timestamp=${timestamp}`; + + this.logger.verbose('Including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting audio from url'); + + fs.writeFileSync(tempAudioPath, response.data); + } else { + this.logger.verbose('Audio is base64'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const audioBuffer = Buffer.from(audio, 'base64'); + fs.writeFileSync(tempAudioPath, audioBuffer); + this.logger.verbose('Temp audio created'); + } + + this.logger.verbose('Converting audio to mp4'); + return new Promise((resolve, reject) => { + exec( + `${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, + (error, _stdout, _stderr) => { + fs.unlinkSync(tempAudioPath); + this.logger.verbose('Temp audio deleted'); + + if (error) reject(error); + + this.logger.verbose('Audio converted to mp4'); + resolve(outputAudio); + }, + ); + }); } - } - public async sendInvite(id: GroupSendInvite) { - this.logger.verbose('Sending invite for group: ' + id.groupJid); - try { - const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); - this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); + public async audioWhatsapp(data: SendAudioDto) { + this.logger.verbose('Sending audio whatsapp'); - const inviteUrl = inviteCode.inviteUrl; - this.logger.verbose('Invite url: ' + inviteUrl); + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; + } - const numbers = id.numbers.map((number) => this.createJid(number)); - const description = id.description ?? ''; + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); - const msg = `${description}\n\n${inviteUrl}`; + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); - const message = { - conversation: msg, - }; + return result; + } else { + throw new InternalServerErrorException(convert); + } + } - for await (const number of numbers) { - await this.sendMessageWithTyping(number, message); - } - - this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - - return { send: true, inviteUrl }; - } catch (error) { - throw new NotFoundException('No send invite'); + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); } - } - public async revokeInviteCode(id: GroupJid) { - this.logger.verbose('Revoking invite code for group: ' + id.groupJid); - try { - const inviteCode = await this.client.groupRevokeInvite(id.groupJid); - return { revoked: true, inviteCode }; - } catch (error) { - throw new NotFoundException('Revoke error', error.toString()); - } - } + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; - public async findParticipants(id: GroupJid) { - this.logger.verbose('Fetching participants for group: ' + id.groupJid); - try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; - return { participants }; - } catch (error) { - throw new NotFoundException('No participants', error.toString()); - } - } + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + } - public async updateGParticipant(update: GroupUpdateParticipantDto) { - this.logger.verbose('Updating participants'); - try { - const participants = update.participants.map((p) => this.createJid(p)); - const updateParticipants = await this.client.groupParticipantsUpdate( - update.groupJid, - participants, - update.action, - ); - return { updateParticipants: updateParticipants }; - } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); - } - } + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; - public async updateGSetting(update: GroupUpdateSettingDto) { - this.logger.verbose('Updating setting for group: ' + update.groupJid); - try { - const updateSetting = await this.client.groupSettingUpdate( - update.groupJid, - update.action, - ); - return { updateSetting: updateSetting }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } + if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { + throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); + } - public async toggleEphemeral(update: GroupToggleEphemeralDto) { - this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); - try { - const toggleEphemeral = await this.client.groupToggleEphemeral( - update.groupJid, - update.expiration, - ); - return { success: true }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); + return await this.sendMessageWithTyping( + data.number, + { + buttonsMessage: { + text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, + contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, + footerText: data.buttonMessage?.footerText, + buttons: data.buttonMessage.buttons.map((button) => { + return { + buttonText: { + displayText: button.buttonText, + }, + buttonId: button.buttonId, + type: 1, + }; + }), + headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + }, + data?.options, + ); } - } - public async leaveGroup(id: GroupJid) { - this.logger.verbose('Leaving group: ' + id.groupJid); - try { - await this.client.groupLeave(id.groupJid); - return { groupJid: id.groupJid, leave: true }; - } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } + + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + return await this.sendMessageWithTyping( + data.number, + { + listMessage: { + title: data.listMessage.title, + description: data.listMessage.description, + buttonText: data.listMessage?.buttonText, + footerText: data.listMessage?.footerText, + sections: data.listMessage.sections, + listType: 1, + }, + }, + data?.options, + ); + } + + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + const message: proto.IMessage = {}; + + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } + + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += + `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + // Chat Controller + public async whatsappNumber(data: WhatsAppNumberDto) { + this.logger.verbose('Getting whatsapp number'); + + const onWhatsapp: OnWhatsAppDto[] = []; + for await (const number of data.numbers) { + let jid = this.createJid(number); + + if (isJidGroup(jid)) { + const group = await this.findGroup({ groupJid: jid }, 'inner'); + + if (!group) throw new BadRequestException('Group not found'); + + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else { + jid = !jid.startsWith('+') ? `+${jid}` : jid; + const verify = await this.client.onWhatsApp(jid); + + const result = verify[0]; + + if (!result) { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); + } else { + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); + } + } + } + + return onWhatsapp; + } + + public async markMessageAsRead(data: ReadMessageDto) { + this.logger.verbose('Marking message as read'); + try { + const keys: proto.IMessageKey[] = []; + data.readMessages.forEach((read) => { + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); + } + }); + await this.client.readMessages(keys); + return { message: 'Read messages', read: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Read messages fail', error.toString()); + } + } + + public async archiveChat(data: ArchiveChatDto) { + this.logger.verbose('Archiving chat'); + try { + data.lastMessage.messageTimestamp = data.lastMessage?.messageTimestamp ?? Date.now(); + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [data.lastMessage], + }, + data.lastMessage.key.remoteJid, + ); + + return { + chatId: data.lastMessage.key.remoteJid, + archived: true, + }; + } catch (error) { + throw new InternalServerErrorException({ + archived: false, + message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], + }); + } + } + + public async deleteMessage(del: DeleteMessage) { + this.logger.verbose('Deleting message'); + try { + return await this.client.sendMessage(del.remoteJid, { delete: del }); + } catch (error) { + throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); + } + } + + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { + this.logger.verbose('Getting base64 from media message'); + try { + const m = data?.message; + const convertToMp4 = data?.convertToMp4 ?? false; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + for (const subtype of MessageSubtype) { + if (msg.message[subtype]) { + msg.message = msg.message[subtype].message; + } + } + + let mediaMessage: any; + let mediaType: string; + + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; + } + } + + if (!mediaMessage) { + throw 'The message is not of the media type'; + } + + if (typeof mediaMessage['mediaKey'] === 'object') { + msg.message = JSON.parse(JSON.stringify(msg.message)); + } + + this.logger.verbose('Downloading media message'); + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }), + reuploadRequest: this.client.updateMediaMessage, + }, + ); + const typeMessage = getContentType(msg.message); + + if (convertToMp4 && typeMessage === 'audioMessage') { + this.logger.verbose('Converting audio to mp4'); + const number = msg.key.remoteJid.split('@')[0]; + const convert = await this.processAudio(buffer.toString('base64'), number); + + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + this.logger.verbose('Audio converted to mp4'); + + const result = { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: 'audio/mp4', + base64: Buffer.from(audio, 'base64').toString('base64'), + }; + + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); + + this.logger.verbose('Media message downloaded'); + return result; + } + } + + this.logger.verbose('Media message downloaded'); + return { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: mediaMessage['mimetype'], + base64: buffer.toString('base64'), + }; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + public async fetchContacts(query: ContactQuery) { + this.logger.verbose('Fetching contacts'); + if (query?.where) { + query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } + } else { + query = { + where: { + owner: this.instance.name, + }, + }; + } + return await this.repository.contact.find(query); + } + + public async fetchMessages(query: MessageQuery) { + this.logger.verbose('Fetching messages'); + if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.message.find(query); + } + + public async fetchStatusMessage(query: MessageUpQuery) { + this.logger.verbose('Fetching status messages'); + if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.messageUpdate.find(query); + } + + public async fetchChats() { + this.logger.verbose('Fetching chats'); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); + } + + public async fetchPrivacySettings() { + this.logger.verbose('Fetching privacy settings'); + return await this.client.fetchPrivacySettings(); + } + + public async updatePrivacySettings(settings: PrivacySettingDto) { + this.logger.verbose('Updating privacy settings'); + try { + await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); + this.logger.verbose('Read receipts privacy updated'); + + await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); + this.logger.verbose('Profile picture privacy updated'); + + await this.client.updateStatusPrivacy(settings.privacySettings.status); + this.logger.verbose('Status privacy updated'); + + await this.client.updateOnlinePrivacy(settings.privacySettings.online); + this.logger.verbose('Online privacy updated'); + + await this.client.updateLastSeenPrivacy(settings.privacySettings.last); + this.logger.verbose('Last seen privacy updated'); + + await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); + this.logger.verbose('Groups add privacy updated'); + + // reinicia a instancia + this.client?.ws?.close(); + + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating privacy settings', error.toString()); + } + } + + public async fetchBusinessProfile(number: string): Promise { + this.logger.verbose('Fetching business profile'); + try { + const jid = number ? this.createJid(number) : this.instance.wuid; + + const profile = await this.client.getBusinessProfile(jid); + this.logger.verbose('Trying to get business profile'); + + if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; + } + + this.logger.verbose('Business profile fetched'); + return { + isBusiness: true, + ...profile, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileName(name: string) { + this.logger.verbose('Updating profile name to ' + name); + try { + await this.client.updateProfileName(name); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileStatus(status: string) { + this.logger.verbose('Updating profile status to: ' + status); + try { + await this.client.updateProfileStatus(status); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile status', error.toString()); + } + } + + public async updateProfilePicture(picture: string) { + this.logger.verbose('Updating profile picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(this.instance.wuid, pic); + this.logger.verbose('Profile picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile picture', error.toString()); + } + } + + public async removeProfilePicture() { + this.logger.verbose('Removing profile picture'); + try { + await this.client.removeProfilePicture(this.instance.wuid); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error removing profile picture', error.toString()); + } + } + + // Group + public async createGroup(create: CreateGroupDto) { + this.logger.verbose('Creating group: ' + create.subject); + try { + const participants = create.participants.map((p) => this.createJid(p)); + const { id } = await this.client.groupCreate(create.subject, participants); + this.logger.verbose('Group created: ' + id); + + if (create?.description) { + this.logger.verbose('Updating group description: ' + create.description); + await this.client.groupUpdateDescription(id, create.description); + } + + const group = await this.client.groupMetadata(id); + this.logger.verbose('Getting group metadata'); + + return { groupMetadata: group }; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException('Error creating group', error.toString()); + } + } + + public async updateGroupPicture(picture: GroupPictureDto) { + this.logger.verbose('Updating group picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture.image)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture.image}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture.image)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture.image, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(picture.groupJid, pic); + this.logger.verbose('Group picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error update group picture', error.toString()); + } + } + + public async updateGroupSubject(data: GroupSubjectDto) { + this.logger.verbose('Updating group subject to: ' + data.subject); + try { + await this.client.groupUpdateSubject(data.groupJid, data.subject); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group subject', error.toString()); + } + } + + public async updateGroupDescription(data: GroupDescriptionDto) { + this.logger.verbose('Updating group description to: ' + data.description); + try { + await this.client.groupUpdateDescription(data.groupJid, data.description); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group description', error.toString()); + } + } + + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { + this.logger.verbose('Fetching group'); + try { + return await this.client.groupMetadata(id.groupJid); + } catch (error) { + if (reply === 'inner') { + return; + } + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async fetchAllGroups(getParticipants: GetParticipant) { + this.logger.verbose('Fetching all groups'); + try { + const fetch = Object.values(await this.client.groupFetchAllParticipating()); + + const groups = fetch.map((group) => { + const result = { + id: group.id, + subject: group.subject, + subjectOwner: group.subjectOwner, + subjectTime: group.subjectTime, + size: group.size, + creation: group.creation, + owner: group.owner, + desc: group.desc, + descId: group.descId, + restrict: group.restrict, + announce: group.announce, + }; + + if (getParticipants.getParticipants == 'true') { + result['participants'] = group.participants; + } + + return result; + }); + + return groups; + } catch (error) { + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async inviteCode(id: GroupJid) { + this.logger.verbose('Fetching invite code for group: ' + id.groupJid); + try { + const code = await this.client.groupInviteCode(id.groupJid); + return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + } catch (error) { + throw new NotFoundException('No invite code', error.toString()); + } + } + + public async inviteInfo(id: GroupInvite) { + this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); + try { + return await this.client.groupGetInviteInfo(id.inviteCode); + } catch (error) { + throw new NotFoundException('No invite info', id.inviteCode); + } + } + + public async sendInvite(id: GroupSendInvite) { + this.logger.verbose('Sending invite for group: ' + id.groupJid); + try { + const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); + this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); + + const inviteUrl = inviteCode.inviteUrl; + this.logger.verbose('Invite url: ' + inviteUrl); + + const numbers = id.numbers.map((number) => this.createJid(number)); + const description = id.description ?? ''; + + const msg = `${description}\n\n${inviteUrl}`; + + const message = { + conversation: msg, + }; + + for await (const number of numbers) { + await this.sendMessageWithTyping(number, message); + } + + this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); + + return { send: true, inviteUrl }; + } catch (error) { + throw new NotFoundException('No send invite'); + } + } + + public async revokeInviteCode(id: GroupJid) { + this.logger.verbose('Revoking invite code for group: ' + id.groupJid); + try { + const inviteCode = await this.client.groupRevokeInvite(id.groupJid); + return { revoked: true, inviteCode }; + } catch (error) { + throw new NotFoundException('Revoke error', error.toString()); + } + } + + public async findParticipants(id: GroupJid) { + this.logger.verbose('Fetching participants for group: ' + id.groupJid); + try { + const participants = (await this.client.groupMetadata(id.groupJid)).participants; + return { participants }; + } catch (error) { + throw new NotFoundException('No participants', error.toString()); + } + } + + public async updateGParticipant(update: GroupUpdateParticipantDto) { + this.logger.verbose('Updating participants'); + try { + const participants = update.participants.map((p) => this.createJid(p)); + const updateParticipants = await this.client.groupParticipantsUpdate( + update.groupJid, + participants, + update.action, + ); + return { updateParticipants: updateParticipants }; + } catch (error) { + throw new BadRequestException('Error updating participants', error.toString()); + } + } + + public async updateGSetting(update: GroupUpdateSettingDto) { + this.logger.verbose('Updating setting for group: ' + update.groupJid); + try { + const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); + return { updateSetting: updateSetting }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async toggleEphemeral(update: GroupToggleEphemeralDto) { + this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); + try { + const toggleEphemeral = await this.client.groupToggleEphemeral(update.groupJid, update.expiration); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async leaveGroup(id: GroupJid) { + this.logger.verbose('Leaving group: ' + id.groupJid); + try { + await this.client.groupLeave(id.groupJid); + return { groupJid: id.groupJid, leave: true }; + } catch (error) { + throw new BadRequestException('Unable to leave the group', error.toString()); + } } - } } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index d0c5f80c..7e4b8352 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -2,94 +2,81 @@ import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'; export enum Events { - APPLICATION_STARTUP = 'application.startup', - QRCODE_UPDATED = 'qrcode.updated', - CONNECTION_UPDATE = 'connection.update', - STATUS_INSTANCE = 'status.instance', - MESSAGES_SET = 'messages.set', - MESSAGES_UPSERT = 'messages.upsert', - MESSAGES_UPDATE = 'messages.update', - MESSAGES_DELETE = 'messages.delete', - SEND_MESSAGE = 'send.message', - CONTACTS_SET = 'contacts.set', - CONTACTS_UPSERT = 'contacts.upsert', - CONTACTS_UPDATE = 'contacts.update', - PRESENCE_UPDATE = 'presence.update', - CHATS_SET = 'chats.set', - CHATS_UPDATE = 'chats.update', - CHATS_UPSERT = 'chats.upsert', - CHATS_DELETE = 'chats.delete', - GROUPS_UPSERT = 'groups.upsert', - GROUPS_UPDATE = 'groups.update', - GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + APPLICATION_STARTUP = 'application.startup', + QRCODE_UPDATED = 'qrcode.updated', + CONNECTION_UPDATE = 'connection.update', + STATUS_INSTANCE = 'status.instance', + MESSAGES_SET = 'messages.set', + MESSAGES_UPSERT = 'messages.upsert', + MESSAGES_UPDATE = 'messages.update', + MESSAGES_DELETE = 'messages.delete', + SEND_MESSAGE = 'send.message', + CONTACTS_SET = 'contacts.set', + CONTACTS_UPSERT = 'contacts.upsert', + CONTACTS_UPDATE = 'contacts.update', + PRESENCE_UPDATE = 'presence.update', + CHATS_SET = 'chats.set', + CHATS_UPDATE = 'chats.update', + CHATS_UPSERT = 'chats.upsert', + CHATS_DELETE = 'chats.delete', + GROUPS_UPSERT = 'groups.upsert', + GROUPS_UPDATE = 'groups.update', + GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', } export declare namespace wa { - export type QrCode = { - count?: number; - pairingCode?: string; - base64?: string; - code?: string; - }; - export type Instance = { - qrcode?: QrCode; - pairingCode?: string; - authState?: { state: AuthenticationState; saveCreds: () => void }; - name?: string; - wuid?: string; - profileName?: string; - profilePictureUrl?: string; - }; + export type QrCode = { + count?: number; + pairingCode?: string; + base64?: string; + code?: string; + }; + export type Instance = { + qrcode?: QrCode; + pairingCode?: string; + authState?: { state: AuthenticationState; saveCreds: () => void }; + name?: string; + wuid?: string; + profileName?: string; + profilePictureUrl?: string; + }; - export type LocalWebHook = { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; - }; + export type LocalWebHook = { + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; + }; - export type LocalChatwoot = { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + }; - export type LocalSettings = { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - }; + export type LocalSettings = { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + }; - export type StateConnection = { - instance?: string; - state?: WAConnectionState | 'refused'; - statusReason?: number; - }; + export type StateConnection = { + instance?: string; + state?: WAConnectionState | 'refused'; + statusReason?: number; + }; - export type StatusMessage = - | 'ERROR' - | 'PENDING' - | 'SERVER_ACK' - | 'DELIVERY_ACK' - | 'READ' - | 'DELETED' - | 'PLAYED'; + export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; } -export const TypeMediaMessage = [ - 'imageMessage', - 'documentMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', -]; +export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage']; export const MessageSubtype = [ - 'ephemeralMessage', - 'documentWithCaptionMessage', - 'viewOnceMessage', - 'viewOnceMessageV2', + 'ephemeralMessage', + 'documentWithCaptionMessage', + 'viewOnceMessage', + 'viewOnceMessageV2', ]; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 30f43015..e783d490 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -14,14 +14,14 @@ import { SettingsController } from './controllers/settings.controller'; import { ViewsController } from './controllers/views.controller'; import { WebhookController } from './controllers/webhook.controller'; import { - AuthModel, - ChatModel, - ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, - SettingsModel, - WebhookModel, + AuthModel, + ChatModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + SettingsModel, + WebhookModel, } from './models'; import { AuthRepository } from './repository/auth.repository'; import { ChatRepository } from './repository/chat.repository'; @@ -52,26 +52,21 @@ const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( - messageRepository, - chatRepository, - contactRepository, - messageUpdateRepository, - webhookRepository, - chatwootRepository, - settingsRepository, - authRepository, - configService, - dbserver?.getClient(), + messageRepository, + chatRepository, + contactRepository, + messageUpdateRepository, + webhookRepository, + chatwootRepository, + settingsRepository, + authRepository, + configService, + dbserver?.getClient(), ); export const cache = new RedisCache(); -export const waMonitor = new WAMonitoringService( - eventEmitter, - configService, - repository, - cache, -); +export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache); const authService = new AuthService(configService, waMonitor, repository); @@ -88,14 +83,14 @@ const settingsService = new SettingsService(waMonitor); export const settingsController = new SettingsController(settingsService); export const instanceController = new InstanceController( - waMonitor, - configService, - repository, - eventEmitter, - authService, - webhookService, - chatwootService, - cache, + waMonitor, + configService, + repository, + eventEmitter, + authService, + webhookService, + chatwootService, + cache, ); export const viewsController = new ViewsController(waMonitor, configService); export const sendMessageController = new SendMessageController(waMonitor); From 5482601b41c650df84f0a7abde461832241372d8 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 10:59:15 -0300 Subject: [PATCH 71/97] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit fb6e58b3c4257a23b0b46a29231099ceb172f60b Merge: 1d3d557 67e9845 Author: Davidson Gomes Date: Wed Jul 26 09:39:37 2023 -0300 Merge branch 'release/1.4.5' commit 67e98456bb39bfc7e545c5b90bf76160f458dce6 Author: Davidson Gomes Date: Wed Jul 26 09:39:27 2023 -0300 fix: Fix mids going duplicated in chatwoot commit 3e47420534e4fdfa3a6898b3c6c1e5ea4f4df5c3 Merge: 3f41974 1d3d557 Author: Davidson Gomes Date: Wed Jul 26 09:38:44 2023 -0300 Merge tag '1.4.5' into develop * Fixed problems in localization template in chatwoot * Fix mids going duplicated in chatwoot commit 1d3d557c439631bbee14808e83516cbb67f95992 Merge: 6b926dc 3f41974 Author: Davidson Gomes Date: Wed Jul 26 09:38:36 2023 -0300 Merge branch 'release/1.4.5' commit 3f41974a753285fd36fce7f0cdef3c4eb6cf044f Author: Davidson Gomes Date: Wed Jul 26 09:38:22 2023 -0300 fix: Fix mids going duplicated in chatwoot commit 3e3c7397a5a7efbb1f68f6b91f6202ae227a0755 Merge: de0c9a1 dcb5170 Author: Davidson Gomes Date: Wed Jul 26 09:36:10 2023 -0300 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop commit dcb51702e75e76ea901b198f14d28e021de9d62e Merge: 4769d75 dd0c1e2 Author: Davidson Gomes Date: Wed Jul 26 09:36:01 2023 -0300 Merge pull request #30 from codephix/patch-1 Update whatsapp.service.ts commit de0c9a1eff88aa1d22fad9ba9f8b8676caa4aa96 Author: Davidson Gomes Date: Wed Jul 26 09:34:10 2023 -0300 fix: fixed problems in localization template commit dd0c1e20a723b835407efbb3b96005de5a5a7680 Author: CodePhix Date: Tue Jul 25 19:59:42 2023 -0300 Update whatsapp.service.ts commit 4769d75dc3ea8f6508b80bbb7e0df86f5e1d4786 Merge: ecae077 6b926dc Author: Davidson Gomes Date: Tue Jul 25 16:20:04 2023 -0300 Merge tag '1.4.4' into develop * Fixed chatwoot line wrap issue * Solved receive location in chatwoot * When requesting the pairing code, it also brings the qr code * Option reopen_conversation in chatwoot endpoint * Option conversation_pending in chatwoot endpoint commit 6b926dc697c4efa8a387f94b4a449149068378c3 Merge: 2b6dbfd ecae077 Author: Davidson Gomes Date: Tue Jul 25 16:19:54 2023 -0300 Merge branch 'release/1.4.4' commit ecae077c6d50dc480934409efb00ee5f83a87389 Author: Davidson Gomes Date: Tue Jul 25 16:16:49 2023 -0300 fix: Option conversation_pending in chatwoot endpoint commit 78ab1bed359807d46d5728df54001612150c0832 Merge: 6bb1abd 2b6dbfd Author: Davidson Gomes Date: Tue Jul 25 15:31:16 2023 -0300 Merge tag '1.4.4' into develop * Fixed chatwoot line wrap issue * Solved receive location in chatwoot * When requesting the pairing code, it also brings the qr code * Option reopen_conversation in chatwoot endpoint * Option conversation_pending in chatwoot endpoint commit 2b6dbfde6bd7d52ef625a9ea95ff946de8568b03 Merge: 82b1567 6bb1abd Author: Davidson Gomes Date: Tue Jul 25 15:31:06 2023 -0300 Merge branch 'release/1.4.4' commit 6bb1abd7f0a186dd535cb11610fc860e078cb219 Author: Davidson Gomes Date: Tue Jul 25 15:29:42 2023 -0300 fix: Option conversation_pending in chatwoot endpoint commit aef92240cc09ab7c74574c24d5ab916de89c0bb6 Author: Davidson Gomes Date: Tue Jul 25 15:20:21 2023 -0300 fix: Option conversation_pending in chatwoot endpoint commit f0d8c2d0954bb5a5f9bf7e3db943b7b53bc6dbf9 Author: Davidson Gomes Date: Tue Jul 25 13:19:15 2023 -0300 fix: Solved receive location in chatwoot commit 14529f2c3580445a892a46dcdfdc25215e211d1a Author: Davidson Gomes Date: Tue Jul 25 12:47:35 2023 -0300 fix: When requesting the pairing code, it also brings the qr code commit 89f40d54d979b38d0583275b8fc20665d35bff20 Author: Davidson Gomes Date: Tue Jul 25 12:41:54 2023 -0300 fix: Solved receive location in chatwoot commit 4c006970a24c798a701c3a2adc1e573e0b8956be Author: Davidson Gomes Date: Tue Jul 25 11:52:26 2023 -0300 fix: Fixed chatwoot line wrap issue commit de676041dfe58340fd526a5d288efc75bbb9a83d Merge: 1cd7291 82b1567 Author: Davidson Gomes Date: Tue Jul 25 10:51:53 2023 -0300 Merge tag '1.4.3' into develop * Adjusts in settings with options always_online, read_messages and read_status * Fixed send webhook for event CALL * Create instance with settings commit 82b1567ae51acfd5af4940b5827a2f99dcb7ca7e Merge: f95f312 1cd7291 Author: Davidson Gomes Date: Tue Jul 25 10:51:42 2023 -0300 Merge branch 'release/1.4.3' commit 1cd7291068d8818aa6f3e8cfabe6ecfc6fc9fa19 Author: Davidson Gomes Date: Tue Jul 25 10:51:28 2023 -0300 version: 1.4.3 commit fdee1df5b32ef9d867819b83ab2acd9051a360e3 Author: Davidson Gomes Date: Tue Jul 25 10:42:45 2023 -0300 fix: Create instance with settings commit c314d00ccd98ae8bcc55b0873e2ccf1497fff88c Author: Davidson Gomes Date: Tue Jul 25 10:42:34 2023 -0300 fix: Create instance with settings commit 62e2a8a6e3a882e8350131aa53becc28ee862df7 Author: Davidson Gomes Date: Tue Jul 25 10:23:18 2023 -0300 fix: Fixed send webhook for event CALL commit a12231a0aa003a01510cec3dc7db5d8e430fb523 Author: Davidson Gomes Date: Tue Jul 25 09:57:28 2023 -0300 fix: Adjusts in settings with options always_online, read_messages and read_status commit 183efd427a86b4ef237504a3ecebd134b285bfe4 Merge: 4d9ca4b f95f312 Author: Davidson Gomes Date: Mon Jul 24 20:52:58 2023 -0300 Merge tag '1.4.2' into develop * Fixed validation is set settings * Adjusts in group validations * Ajusts in sticker message to chatwoot commit f95f3126c3adad5b1bd8b35e119987eca95b06e4 Merge: cc91f2e 4d9ca4b Author: Davidson Gomes Date: Mon Jul 24 20:52:46 2023 -0300 Merge branch 'release/1.4.2' commit 4d9ca4b4511ba997f28b4efa764b9079a180bcb4 Author: Davidson Gomes Date: Mon Jul 24 20:52:34 2023 -0300 version: 1.4.2 commit c76334a68aab4622d75abd905bbd59eaee8e2fa9 Author: Davidson Gomes Date: Mon Jul 24 20:32:29 2023 -0300 fix: Ajusts in sticker message to chatwoot commit f475391ba6b3c8af8d0fbdf02819b0a7bfacee80 Author: Davidson Gomes Date: Mon Jul 24 20:32:15 2023 -0300 fix: Ajusts in sticker message to chatwoot commit b77f22790bb5ea17133993f005d3613193698f3a Author: Davidson Gomes Date: Mon Jul 24 20:13:18 2023 -0300 fix: Fixed validation is set settings commit 757a578c6ec362c77564daf6ff0262fc73c7f8f6 Merge: c582476 f8e1892 Author: Davidson Gomes Date: Mon Jul 24 20:12:51 2023 -0300 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop commit c5824767c845013c83dceb735b2e13a0606985e1 Author: Davidson Gomes Date: Mon Jul 24 20:11:49 2023 -0300 fix: Fixed validation is set settings commit 84f3f072794d054d0e7097f3495f28569b6a1147 Author: Davidson Gomes Date: Mon Jul 24 20:11:15 2023 -0300 fix: Fixed validation is set settings commit f8e1892eee3cd186e45cb5fd4fd79bf9b81cc20d Merge: 036a8ed 58ed6f3 Author: Davidson Gomes Date: Mon Jul 24 20:08:24 2023 -0300 fix: group validation Group validation commit 036a8edca0c3ebf00cbf1fcbccaca41c85b7b7ff Merge: 3d8e6f4 ef4be6a Author: Davidson Gomes Date: Mon Jul 24 20:07:05 2023 -0300 fix: Promote All Participants in Create Group Promote All Participants in Create Group commit 58ed6f395fce5ba7a8d8c7cc15592e981204cc15 Author: Alan Mosko Date: Mon Jul 24 19:59:09 2023 -0300 Validação de Grupo commit ef4be6a612ff50465d1e48925eef14d578592b68 Author: Alan Mosko Date: Mon Jul 24 19:53:03 2023 -0300 Start commit 3d8e6f439445334b060a0ce7ab4120601bb6f277 Merge: 8b6e577 0cc1f18 Author: Davidson Gomes Date: Mon Jul 24 19:08:39 2023 -0300 fix: Fixed mentions Fixed mentions commit 0cc1f18a7eaafaa851194a0955f5bbf9637350bf Author: Alan Mosko Date: Mon Jul 24 19:05:32 2023 -0300 [BUG] Correção de mencionar Caso seja enviado everyOne como false sem passar nada no mentioned commit 8b6e577b8ff94b28aea06e2b8f81fa38f6665fac Merge: f9abd90 cc91f2e Author: Davidson Gomes Date: Mon Jul 24 18:28:57 2023 -0300 Merge tag '1.4.1' into develop * Fixed reconnect with pairing code or qrcode * Fixed problem in createJid --- CHANGELOG.md | 33 + Docker/.env.example | 1 + Dockerfile | 1 + package.json | 2 +- src/config/env.config.ts | 408 +- src/dev-env.yml | 1 + src/validate/validate.schema.ts | 1441 +++-- .../controllers/chatwoot.controller.ts | 156 +- .../controllers/instance.controller.ts | 597 +- .../controllers/settings.controller.ts | 27 +- src/whatsapp/dto/chat.dto.ts | 89 +- src/whatsapp/dto/chatwoot.dto.ts | 16 +- src/whatsapp/dto/group.dto.ts | 39 +- src/whatsapp/dto/instance.dto.ts | 30 +- src/whatsapp/dto/settings.dto.ts | 9 +- src/whatsapp/models/chatwoot.model.ts | 41 +- src/whatsapp/models/settings.model.ts | 29 +- src/whatsapp/repository/repository.manager.ts | 209 +- src/whatsapp/services/chatwoot.service.ts | 2904 +++++---- src/whatsapp/services/whatsapp.service.ts | 5640 +++++++++-------- src/whatsapp/types/wa.types.ts | 150 +- src/whatsapp/whatsapp.module.ts | 107 +- 22 files changed, 6231 insertions(+), 5699 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd59c0d..cf5498ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ +# 1.4.5 (2023-07-26 09:32) + +### Fixed + +* Fixed problems in localization template in chatwoot +* Fix mids going duplicated in chatwoot + +# 1.4.4 (2023-07-25 15:24) + +### Fixed + +* Fixed chatwoot line wrap issue +* Solved receive location in chatwoot +* When requesting the pairing code, it also brings the qr code +* Option reopen_conversation in chatwoot endpoint +* Option conversation_pending in chatwoot endpoint + +# 1.4.3 (2023-07-25 10:51) + +### Fixed + +* Adjusts in settings with options always_online, read_messages and read_status +* Fixed send webhook for event CALL +* Create instance with settings + +# 1.4.2 (2023-07-24 20:52) + +### Fixed + +* Fixed validation is set settings +* Adjusts in group validations +* Ajusts in sticker message to chatwoot + # 1.4.1 (2023-07-24 18:28) ### Fixed diff --git a/Docker/.env.example b/Docker/.env.example index c3dbe505..f4e8291d 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -73,6 +73,7 @@ WEBHOOK_EVENTS_GROUPS_UPSERT=true WEBHOOK_EVENTS_GROUPS_UPDATE=true WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true WEBHOOK_EVENTS_CONNECTION_UPDATE=true +WEBHOOK_EVENTS_CALL=true # This event fires every time a new token is requested via the refresh route WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/Dockerfile b/Dockerfile index 93fa60c4..0b3ac950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -74,6 +74,7 @@ ENV WEBHOOK_EVENTS_GROUPS_UPSERT=true ENV WEBHOOK_EVENTS_GROUPS_UPDATE=true ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true +ENV WEBHOOK_EVENTS_CALL=true ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false diff --git a/package.json b/package.json index 4d82309c..0e0407a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.1", + "version": "1.4.5", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 737fff51..78c90ec9 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -1,105 +1,114 @@ -import { isBooleanString } from 'class-validator'; import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; +import { isBooleanString } from 'class-validator'; export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type Cors = { - ORIGIN: string[]; - METHODS: HttpMethods[]; - CREDENTIALS: boolean; + ORIGIN: string[]; + METHODS: HttpMethods[]; + CREDENTIALS: boolean; }; export type LogBaileys = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; -export type LogLevel = 'ERROR' | 'WARN' | 'DEBUG' | 'INFO' | 'LOG' | 'VERBOSE' | 'DARK' | 'WEBHOOKS'; +export type LogLevel = + | 'ERROR' + | 'WARN' + | 'DEBUG' + | 'INFO' + | 'LOG' + | 'VERBOSE' + | 'DARK' + | 'WEBHOOKS'; export type Log = { - LEVEL: LogLevel[]; - COLOR: boolean; - BAILEYS: LogBaileys; + LEVEL: LogLevel[]; + COLOR: boolean; + BAILEYS: LogBaileys; }; export type SaveData = { - INSTANCE: boolean; - NEW_MESSAGE: boolean; - MESSAGE_UPDATE: boolean; - CONTACTS: boolean; - CHATS: boolean; + INSTANCE: boolean; + NEW_MESSAGE: boolean; + MESSAGE_UPDATE: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type StoreConf = { - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type CleanStoreConf = { - CLEANING_INTERVAL: number; - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + CLEANING_INTERVAL: number; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type DBConnection = { - URI: string; - DB_PREFIX_NAME: string; + URI: string; + DB_PREFIX_NAME: string; }; export type Database = { - CONNECTION: DBConnection; - ENABLED: boolean; - SAVE_DATA: SaveData; + CONNECTION: DBConnection; + ENABLED: boolean; + SAVE_DATA: SaveData; }; export type Redis = { - ENABLED: boolean; - URI: string; - PREFIX_KEY: string; + ENABLED: boolean; + URI: string; + PREFIX_KEY: string; }; export type EventsWebhook = { - APPLICATION_STARTUP: boolean; - QRCODE_UPDATED: boolean; - MESSAGES_SET: boolean; - MESSAGES_UPSERT: boolean; - MESSAGES_UPDATE: boolean; - MESSAGES_DELETE: boolean; - SEND_MESSAGE: boolean; - CONTACTS_SET: boolean; - CONTACTS_UPDATE: boolean; - CONTACTS_UPSERT: boolean; - PRESENCE_UPDATE: boolean; - CHATS_SET: boolean; - CHATS_UPDATE: boolean; - CHATS_DELETE: boolean; - CHATS_UPSERT: boolean; - CONNECTION_UPDATE: boolean; - GROUPS_UPSERT: boolean; - GROUP_UPDATE: boolean; - GROUP_PARTICIPANTS_UPDATE: boolean; - NEW_JWT_TOKEN: boolean; + APPLICATION_STARTUP: boolean; + QRCODE_UPDATED: boolean; + MESSAGES_SET: boolean; + MESSAGES_UPSERT: boolean; + MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; + SEND_MESSAGE: boolean; + CONTACTS_SET: boolean; + CONTACTS_UPDATE: boolean; + CONTACTS_UPSERT: boolean; + PRESENCE_UPDATE: boolean; + CHATS_SET: boolean; + CHATS_UPDATE: boolean; + CHATS_DELETE: boolean; + CHATS_UPSERT: boolean; + CONNECTION_UPDATE: boolean; + GROUPS_UPSERT: boolean; + GROUP_UPDATE: boolean; + GROUP_PARTICIPANTS_UPDATE: boolean; + CALL: boolean; + NEW_JWT_TOKEN: boolean; }; export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; export type Auth = { - API_KEY: ApiKey; - EXPOSE_IN_FETCH_INSTANCES: boolean; - JWT: Jwt; - TYPE: 'jwt' | 'apikey'; + API_KEY: ApiKey; + EXPOSE_IN_FETCH_INSTANCES: boolean; + JWT: Jwt; + TYPE: 'jwt' | 'apikey'; }; export type DelInstance = number | boolean; export type GlobalWebhook = { - URL: string; - ENABLED: boolean; - WEBHOOK_BY_EVENTS: boolean; + URL: string; + ENABLED: boolean; + WEBHOOK_BY_EVENTS: boolean; }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; @@ -108,157 +117,162 @@ export type QrCode = { LIMIT: number }; export type Production = boolean; export interface Env { - SERVER: HttpServer; - CORS: Cors; - SSL_CONF: SslConf; - STORE: StoreConf; - CLEAN_STORE: CleanStoreConf; - DATABASE: Database; - REDIS: Redis; - LOG: Log; - DEL_INSTANCE: DelInstance; - WEBHOOK: Webhook; - CONFIG_SESSION_PHONE: ConfigSessionPhone; - QRCODE: QrCode; - AUTHENTICATION: Auth; - PRODUCTION?: Production; + SERVER: HttpServer; + CORS: Cors; + SSL_CONF: SslConf; + STORE: StoreConf; + CLEAN_STORE: CleanStoreConf; + DATABASE: Database; + REDIS: Redis; + LOG: Log; + DEL_INSTANCE: DelInstance; + WEBHOOK: Webhook; + CONFIG_SESSION_PHONE: ConfigSessionPhone; + QRCODE: QrCode; + AUTHENTICATION: Auth; + PRODUCTION?: Production; } export type Key = keyof Env; export class ConfigService { - constructor() { - this.loadEnv(); - } + constructor() { + this.loadEnv(); + } - private env: Env; + private env: Env; - public get(key: Key) { - return this.env[key] as T; - } + public get(key: Key) { + return this.env[key] as T; + } - private loadEnv() { - this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); - this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; - if (process.env?.DOCKER_ENV === 'true') { - this.env.SERVER.TYPE = 'http'; - this.env.SERVER.PORT = 8080; - } + private loadEnv() { + this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); + this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; + if (process.env?.DOCKER_ENV === 'true') { + this.env.SERVER.TYPE = 'http'; + this.env.SERVER.PORT = 8080; } + } - private envYaml(): Env { - return load(readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' })) as Env; - } + private envYaml(): Env { + return load( + readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' }), + ) as Env; + } - private envProcess(): Env { - return { - SERVER: { - TYPE: process.env.SERVER_TYPE as 'http' | 'https', - PORT: Number.parseInt(process.env.SERVER_PORT), - URL: process.env.SERVER_URL, - }, - CORS: { - ORIGIN: process.env.CORS_ORIGIN.split(','), - METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], - CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', - }, - SSL_CONF: { - PRIVKEY: process.env?.SSL_CONF_PRIVKEY, - FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, - }, - STORE: { - MESSAGES: process.env?.STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.STORE_CONTACTS === 'true', - CHATS: process.env?.STORE_CHATS === 'true', - }, - CLEAN_STORE: { - CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) - ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) - : 7200, - MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', - CHATS: process.env?.CLEAN_STORE_CHATS === 'true', - }, - DATABASE: { - CONNECTION: { - URI: process.env.DATABASE_CONNECTION_URI, - DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, - }, - ENABLED: process.env?.DATABASE_ENABLED === 'true', - SAVE_DATA: { - INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', - NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', - MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', - CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', - CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', - }, - }, - REDIS: { - ENABLED: process.env?.REDIS_ENABLED === 'true', - URI: process.env.REDIS_URI, - PREFIX_KEY: process.env.REDIS_PREFIX_KEY, - }, - LOG: { - LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], - COLOR: process.env?.LOG_COLOR === 'true', - BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', - }, - DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) - ? process.env.DEL_INSTANCE === 'true' - : Number.parseInt(process.env.DEL_INSTANCE) || false, - WEBHOOK: { - GLOBAL: { - URL: process.env?.WEBHOOK_GLOBAL_URL, - ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', - WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', - }, - EVENTS: { - APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', - QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', - MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', - MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', - MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', - MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', - SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', - CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', - CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', - CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', - PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', - CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', - CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', - CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', - CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', - CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', - GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', - GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', - GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', - NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', - }, - }, - CONFIG_SESSION_PHONE: { - CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', - NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', - }, - QRCODE: { - LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, - }, - AUTHENTICATION: { - TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', - API_KEY: { - KEY: process.env.AUTHENTICATION_API_KEY, - }, - EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', - JWT: { - EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) - ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) - : 3600, - SECRET: process.env.AUTHENTICATION_JWT_SECRET, - }, - }, - }; - } + private envProcess(): Env { + return { + SERVER: { + TYPE: process.env.SERVER_TYPE as 'http' | 'https', + PORT: Number.parseInt(process.env.SERVER_PORT), + URL: process.env.SERVER_URL, + }, + CORS: { + ORIGIN: process.env.CORS_ORIGIN.split(','), + METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], + CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', + }, + SSL_CONF: { + PRIVKEY: process.env?.SSL_CONF_PRIVKEY, + FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, + }, + STORE: { + MESSAGES: process.env?.STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.STORE_CONTACTS === 'true', + CHATS: process.env?.STORE_CHATS === 'true', + }, + CLEAN_STORE: { + CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) + ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) + : 7200, + MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', + CHATS: process.env?.CLEAN_STORE_CHATS === 'true', + }, + DATABASE: { + CONNECTION: { + URI: process.env.DATABASE_CONNECTION_URI, + DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, + }, + ENABLED: process.env?.DATABASE_ENABLED === 'true', + SAVE_DATA: { + INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', + NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', + MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', + CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', + CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', + }, + }, + REDIS: { + ENABLED: process.env?.REDIS_ENABLED === 'true', + URI: process.env.REDIS_URI, + PREFIX_KEY: process.env.REDIS_PREFIX_KEY, + }, + LOG: { + LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], + COLOR: process.env?.LOG_COLOR === 'true', + BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', + }, + DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) + ? process.env.DEL_INSTANCE === 'true' + : Number.parseInt(process.env.DEL_INSTANCE) || false, + WEBHOOK: { + GLOBAL: { + URL: process.env?.WEBHOOK_GLOBAL_URL, + ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', + WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', + }, + EVENTS: { + APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', + QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', + MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', + MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', + MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', + MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', + SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', + CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', + CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', + CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', + PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', + CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', + CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', + CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', + CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', + CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', + GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', + GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', + GROUP_PARTICIPANTS_UPDATE: + process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', + NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', + }, + }, + CONFIG_SESSION_PHONE: { + CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', + NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', + }, + QRCODE: { + LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, + }, + AUTHENTICATION: { + TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', + API_KEY: { + KEY: process.env.AUTHENTICATION_API_KEY, + }, + EXPOSE_IN_FETCH_INSTANCES: + process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', + JWT: { + EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) + ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) + : 3600, + SECRET: process.env.AUTHENTICATION_JWT_SECRET, + }, + }, + }; + } } export const configService = new ConfigService(); diff --git a/src/dev-env.yml b/src/dev-env.yml index 41368ea4..b45d3201 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -110,6 +110,7 @@ WEBHOOK: GROUP_UPDATE: true GROUP_PARTICIPANTS_UPDATE: true CONNECTION_UPDATE: true + CALL: true # This event fires every time a new token is requested via the refresh route NEW_JWT_TOKEN: false diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index c83d6a61..d7e2ac1e 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -2,888 +2,923 @@ import { JSONSchema7, JSONSchema7Definition } 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 }, - }; + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; }; // Instance Schema export const instanceNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - instanceName: { type: 'string' }, - webhook: { type: 'string' }, - webhook_by_events: { type: 'boolean' }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'NEW_JWT_TOKEN', - ], - }, - }, - qrcode: { type: 'boolean', enum: [true, false] }, - number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, - token: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + instanceName: { type: 'string' }, + webhook: { type: 'string' }, + webhook_by_events: { type: 'boolean' }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, }, - ...isNotEmpty('instanceName'), + qrcode: { type: 'boolean', enum: [true, false] }, + number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, + token: { type: 'string' }, + }, + ...isNotEmpty('instanceName'), }; export const oldTokenSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - oldToken: { type: 'string' }, - }, - required: ['oldToken'], - ...isNotEmpty('oldToken'), + $id: v4(), + type: 'object', + properties: { + oldToken: { type: 'string' }, + }, + required: ['oldToken'], + ...isNotEmpty('oldToken'), }; const quotedOptionsSchema: JSONSchema7 = { - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id'], - ...isNotEmpty('id'), - }, - message: { type: 'object' }, + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id'], + ...isNotEmpty('id'), }, + message: { type: 'object' }, + }, }; const mentionsOptionsSchema: JSONSchema7 = { - properties: { - everyOne: { type: 'boolean', enum: [true, false] }, - mentioned: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"mentioned" must be an array of numeric strings', - }, - }, + properties: { + everyOne: { type: 'boolean', enum: [true, false] }, + mentioned: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"mentioned" must be an array of numeric strings', + }, }, + }, }; // Send Message Schema const optionsSchema: JSONSchema7 = { - properties: { - delay: { - type: 'integer', - description: 'Enter a value in milliseconds', - }, - presence: { - type: 'string', - enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], - }, - quoted: { ...quotedOptionsSchema }, - mentions: { ...mentionsOptionsSchema }, + properties: { + delay: { + type: 'integer', + description: 'Enter a value in milliseconds', }, + presence: { + type: 'string', + enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], + }, + quoted: { ...quotedOptionsSchema }, + mentions: { ...mentionsOptionsSchema }, + }, }; const numberDefinition: JSONSchema7Definition = { - type: 'string', - description: 'Invalid format', + type: 'string', + description: 'Invalid format', }; export const textMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - textMessage: { - type: 'object', - properties: { - text: { type: 'string' }, - }, - required: ['text'], - ...isNotEmpty('text'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + textMessage: { + type: 'object', + properties: { + text: { type: 'string' }, + }, + required: ['text'], + ...isNotEmpty('text'), }, - required: ['textMessage', 'number'], + }, + required: ['textMessage', 'number'], }; export const pollMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - pollMessage: { - type: 'object', - properties: { - name: { type: 'string' }, - selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, - values: { - type: 'array', - minItems: 2, - maxItems: 10, - uniqueItems: true, - items: { - type: 'string', - }, - }, - }, - required: ['name', 'selectableCount', 'values'], - ...isNotEmpty('name', 'selectableCount', 'values'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + pollMessage: { + type: 'object', + properties: { + name: { type: 'string' }, + selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, + values: { + type: 'array', + minItems: 2, + maxItems: 10, + uniqueItems: true, + items: { + type: 'string', + }, }, + }, + required: ['name', 'selectableCount', 'values'], + ...isNotEmpty('name', 'selectableCount', 'values'), }, - required: ['pollMessage', 'number'], + }, + required: ['pollMessage', 'number'], }; export const statusMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - statusMessage: { - type: 'object', - properties: { - type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, - content: { type: 'string' }, - caption: { type: 'string' }, - backgroundColor: { type: 'string' }, - font: { type: 'integer', minimum: 0, maximum: 5 }, - statusJidList: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"statusJidList" must be an array of numeric strings', - }, - }, - allContacts: { type: 'boolean', enum: [true, false] }, - }, - required: ['type', 'content'], - ...isNotEmpty('type', 'content'), + $id: v4(), + type: 'object', + properties: { + statusMessage: { + type: 'object', + properties: { + type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, + content: { type: 'string' }, + caption: { type: 'string' }, + backgroundColor: { type: 'string' }, + font: { type: 'integer', minimum: 0, maximum: 5 }, + statusJidList: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"statusJidList" must be an array of numeric strings', + }, }, + allContacts: { type: 'boolean', enum: [true, false] }, + }, + required: ['type', 'content'], + ...isNotEmpty('type', 'content'), }, - required: ['statusMessage'], + }, + required: ['statusMessage'], }; export const mediaMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - mediaMessage: { - type: 'object', - properties: { - mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, - media: { type: 'string' }, - fileName: { type: 'string' }, - caption: { type: 'string' }, - }, - required: ['mediatype', 'media'], - ...isNotEmpty('fileName', 'caption', 'media'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + mediaMessage: { + type: 'object', + properties: { + mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, + media: { type: 'string' }, + fileName: { type: 'string' }, + caption: { type: 'string' }, + }, + required: ['mediatype', 'media'], + ...isNotEmpty('fileName', 'caption', 'media'), }, - required: ['mediaMessage', 'number'], + }, + required: ['mediaMessage', 'number'], }; export const stickerMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - stickerMessage: { - type: 'object', - properties: { - image: { type: 'string' }, - }, - required: ['image'], - ...isNotEmpty('image'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + stickerMessage: { + type: 'object', + properties: { + image: { type: 'string' }, + }, + required: ['image'], + ...isNotEmpty('image'), }, - required: ['stickerMessage', 'number'], + }, + required: ['stickerMessage', 'number'], }; export const audioMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - audioMessage: { - type: 'object', - properties: { - audio: { type: 'string' }, - }, - required: ['audio'], - ...isNotEmpty('audio'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + audioMessage: { + type: 'object', + properties: { + audio: { type: 'string' }, + }, + required: ['audio'], + ...isNotEmpty('audio'), }, - required: ['audioMessage', 'number'], + }, + required: ['audioMessage', 'number'], }; export const buttonMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - buttonMessage: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + buttonMessage: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttons: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'object', properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttons: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - buttonText: { type: 'string' }, - buttonId: { type: 'string' }, - }, - required: ['buttonText', 'buttonId'], - ...isNotEmpty('buttonText', 'buttonId'), - }, - }, - mediaMessage: { - type: 'object', - properties: { - media: { type: 'string' }, - fileName: { type: 'string' }, - mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, - }, - required: ['media', 'mediatype'], - ...isNotEmpty('media', 'fileName'), - }, + buttonText: { type: 'string' }, + buttonId: { type: 'string' }, }, - required: ['title', 'buttons'], - ...isNotEmpty('title', 'description'), + required: ['buttonText', 'buttonId'], + ...isNotEmpty('buttonText', 'buttonId'), + }, }, + mediaMessage: { + type: 'object', + properties: { + media: { type: 'string' }, + fileName: { type: 'string' }, + mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, + }, + required: ['media', 'mediatype'], + ...isNotEmpty('media', 'fileName'), + }, + }, + required: ['title', 'buttons'], + ...isNotEmpty('title', 'description'), }, - required: ['number', 'buttonMessage'], + }, + required: ['number', 'buttonMessage'], }; export const locationMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - locationMessage: { - type: 'object', - properties: { - latitude: { type: 'number' }, - longitude: { type: 'number' }, - name: { type: 'string' }, - address: { type: 'string' }, - }, - required: ['latitude', 'longitude'], - ...isNotEmpty('name', 'addresss'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + locationMessage: { + type: 'object', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + name: { type: 'string' }, + address: { type: 'string' }, + }, + required: ['latitude', 'longitude'], + ...isNotEmpty('name', 'addresss'), }, - required: ['number', 'locationMessage'], + }, + required: ['number', 'locationMessage'], }; export const listMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - listMessage: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + listMessage: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttonText: { type: 'string' }, + sections: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'object', properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttonText: { type: 'string' }, - sections: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - rows: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - rowId: { type: 'string' }, - }, - required: ['title', 'description', 'rowId'], - ...isNotEmpty('title', 'description', 'rowId'), - }, - }, - }, - required: ['title', 'rows'], - ...isNotEmpty('title'), - }, + title: { type: 'string' }, + rows: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + rowId: { type: 'string' }, + }, + required: ['title', 'description', 'rowId'], + ...isNotEmpty('title', 'description', 'rowId'), }, + }, }, - required: ['title', 'description', 'buttonText', 'sections'], - ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), + required: ['title', 'rows'], + ...isNotEmpty('title'), + }, }, + }, + required: ['title', 'description', 'buttonText', 'sections'], + ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - required: ['number', 'listMessage'], + }, + required: ['number', 'listMessage'], }; export const contactMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - contactMessage: { - type: 'array', - items: { - type: 'object', - properties: { - fullName: { type: 'string' }, - wuid: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"wuid" must be a numeric string', - }, - phoneNumber: { type: 'string', minLength: 10 }, - organization: { type: 'string' }, - email: { type: 'string' }, - url: { type: 'string' }, - }, - required: ['fullName', 'phoneNumber'], - ...isNotEmpty('fullName'), - }, - minItems: 1, - uniqueItems: true, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + contactMessage: { + type: 'array', + items: { + type: 'object', + properties: { + fullName: { type: 'string' }, + wuid: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"wuid" must be a numeric string', + }, + phoneNumber: { type: 'string', minLength: 10 }, + organization: { type: 'string' }, + email: { type: 'string' }, + url: { type: 'string' }, }, + required: ['fullName', 'phoneNumber'], + ...isNotEmpty('fullName'), + }, + minItems: 1, + uniqueItems: true, }, - required: ['number', 'contactMessage'], + }, + required: ['number', 'contactMessage'], }; export const reactionMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reactionMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'remoteJid', 'fromMe'], - ...isNotEmpty('id', 'remoteJid'), - }, - reaction: { type: 'string' }, - }, - required: ['key', 'reaction'], - ...isNotEmpty('reaction'), + $id: v4(), + type: 'object', + properties: { + reactionMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'remoteJid', 'fromMe'], + ...isNotEmpty('id', 'remoteJid'), }, + reaction: { type: 'string' }, + }, + required: ['key', 'reaction'], + ...isNotEmpty('reaction'), }, - required: ['reactionMessage'], + }, + required: ['reactionMessage'], }; // Chat Schema export const whatsappNumberSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - description: '"numbers" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + description: '"numbers" must be an array of numeric strings', + }, }, + }, }; export const readMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - readMessages: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, + $id: v4(), + type: 'object', + properties: { + read_messages: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, }, - required: ['readMessages'], + }, + required: ['read_messages'], }; export const privacySettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - privacySettings: { - type: 'object', - properties: { - readreceipts: { type: 'string', enum: ['all', 'none'] }, - profile: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - status: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - online: { type: 'string', enum: ['all', 'match_last_seen'] }, - last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, - groupadd: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - }, - required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], - ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), + $id: v4(), + type: 'object', + properties: { + privacySettings: { + type: 'object', + properties: { + readreceipts: { type: 'string', enum: ['all', 'none'] }, + profile: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], }, + status: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + online: { type: 'string', enum: ['all', 'match_last_seen'] }, + last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, + groupadd: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + }, + required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], + ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - required: ['privacySettings'], + }, + required: ['privacySettings'], }; export const archiveChatSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - lastMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, - messageTimestamp: { type: 'integer', minLength: 1 }, - }, - required: ['key'], - ...isNotEmpty('messageTimestamp'), + $id: v4(), + type: 'object', + properties: { + lastMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), }, - archive: { type: 'boolean', enum: [true, false] }, + messageTimestamp: { type: 'integer', minLength: 1 }, + }, + required: ['key'], + ...isNotEmpty('messageTimestamp'), }, - required: ['lastMessage', 'archive'], + archive: { type: 'boolean', enum: [true, false] }, + }, + required: ['lastMessage', 'archive'], }; export const deleteMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - participant: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid', 'participant'), + $id: v4(), + type: 'object', + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + participant: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid', 'participant'), }; export const contactValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - pushName: { type: 'string', minLength: 1 }, - id: { type: 'string', minLength: 1 }, - }, - ...isNotEmpty('_id', 'id', 'pushName'), - }, + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + pushName: { type: 'string', minLength: 1 }, + id: { type: 'string', minLength: 1 }, + }, + ...isNotEmpty('_id', 'id', 'pushName'), }, + }, }; export const profileNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - name: { type: 'string' }, - }, - ...isNotEmpty('name'), + $id: v4(), + type: 'object', + properties: { + name: { type: 'string' }, + }, + ...isNotEmpty('name'), }; export const profileStatusSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - status: { type: 'string' }, - }, - ...isNotEmpty('status'), + $id: v4(), + type: 'object', + properties: { + status: { type: 'string' }, + }, + ...isNotEmpty('status'), }; export const profilePictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { type: 'string' }, - picture: { type: 'string' }, - }, + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + picture: { type: 'string' }, + }, }; export const profileSchema: JSONSchema7 = { - type: 'object', - properties: { - wuid: { type: 'string' }, - name: { type: 'string' }, - picture: { type: 'string' }, - status: { type: 'string' }, - isBusiness: { type: 'boolean' }, - }, + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, }; export const messageValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - key: { - type: 'object', - if: { - propertyNames: { - enum: ['fromMe', 'remoteJid', 'id'], - }, - }, - then: { - properties: { - remoteJid: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - id: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - }, - }, - message: { type: 'object' }, + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + key: { + type: 'object', + if: { + propertyNames: { + enum: ['fromMe', 'remoteJid', 'id'], }, - ...isNotEmpty('_id'), + }, + then: { + properties: { + remoteJid: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + id: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + }, }, - limit: { type: 'integer' }, + message: { type: 'object' }, + }, + ...isNotEmpty('_id'), }, + limit: { type: 'integer' }, + }, }; export const messageUpSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string' }, - remoteJid: { type: 'string' }, - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - participant: { type: 'string' }, - status: { - type: 'string', - enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], - }, - }, - ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string' }, + remoteJid: { type: 'string' }, + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + participant: { type: 'string' }, + status: { + type: 'string', + enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], }, - limit: { type: 'integer' }, + }, + ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), }, + limit: { type: 'integer' }, + }, }; // Group Schema export const createGroupSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - subject: { type: 'string' }, - description: { type: 'string' }, - profilePicture: { type: 'string' }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + subject: { type: 'string' }, + description: { type: 'string' }, + profilePicture: { type: 'string' }, + promoteParticipants: { type: 'boolean', enum: [true, false] }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, }, - required: ['subject', 'participants'], - ...isNotEmpty('subject', 'description', 'profilePicture'), + }, + required: ['subject', 'participants'], + ...isNotEmpty('subject', 'description', 'profilePicture'), }; export const groupJidSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, - }, - required: ['groupJid'], - ...isNotEmpty('groupJid'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, + }, + required: ['groupJid'], + ...isNotEmpty('groupJid'), }; export const getParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - getParticipants: { type: 'string', enum: ['true', 'false'] }, - }, - required: ['getParticipants'], - ...isNotEmpty('getParticipants'), + $id: v4(), + type: 'object', + properties: { + getParticipants: { type: 'string', enum: ['true', 'false'] }, + }, + required: ['getParticipants'], + ...isNotEmpty('getParticipants'), }; export const groupSendInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"numbers" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"numbers" must be an array of numeric strings', + }, }, - required: ['groupJid', 'description', 'numbers'], - ...isNotEmpty('groupJid', 'description', 'numbers'), + }, + required: ['groupJid', 'description', 'numbers'], + ...isNotEmpty('groupJid', 'description', 'numbers'), }; export const groupInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, - }, - required: ['inviteCode'], - ...isNotEmpty('inviteCode'), + $id: v4(), + type: 'object', + properties: { + inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, + }, + required: ['inviteCode'], + ...isNotEmpty('inviteCode'), }; export const updateParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['add', 'remove', 'promote', 'demote'], - }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['add', 'remove', 'promote', 'demote'], }, - required: ['groupJid', 'action', 'participants'], - ...isNotEmpty('groupJid', 'action'), + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, + }, + required: ['groupJid', 'action', 'participants'], + ...isNotEmpty('groupJid', 'action'), }; export const updateSettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], }, - required: ['groupJid', 'action'], - ...isNotEmpty('groupJid', 'action'), + }, + required: ['groupJid', 'action'], + ...isNotEmpty('groupJid', 'action'), }; export const toggleEphemeralSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - expiration: { - type: 'number', - enum: [0, 86400, 604800, 7776000], - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + expiration: { + type: 'number', + enum: [0, 86400, 604800, 7776000], }, - required: ['groupJid', 'expiration'], - ...isNotEmpty('groupJid', 'expiration'), + }, + required: ['groupJid', 'expiration'], + ...isNotEmpty('groupJid', 'expiration'), }; export const updateGroupPictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - image: { type: 'string' }, - }, - required: ['groupJid', 'image'], - ...isNotEmpty('groupJid', 'image'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + image: { type: 'string' }, + }, + required: ['groupJid', 'image'], + ...isNotEmpty('groupJid', 'image'), }; export const updateGroupSubjectSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - subject: { type: 'string' }, - }, - required: ['groupJid', 'subject'], - ...isNotEmpty('groupJid', 'subject'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['groupJid', 'subject'], + ...isNotEmpty('groupJid', 'subject'), }; export const updateGroupDescriptionSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - }, - required: ['groupJid', 'description'], - ...isNotEmpty('groupJid', 'description'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['groupJid', 'description'], + ...isNotEmpty('groupJid', 'description'), }; // Webhook Schema export const webhookSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - url: { type: 'string' }, - enabled: { type: 'boolean', enum: [true, false] }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'NEW_JWT_TOKEN', - ], - }, - }, + $id: v4(), + type: 'object', + properties: { + url: { type: 'string' }, + enabled: { type: 'boolean', enum: [true, false] }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, }, - required: ['url', 'enabled'], - ...isNotEmpty('url'), + }, + required: ['url', 'enabled'], + ...isNotEmpty('url'), }; export const chatwootSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - enabled: { type: 'boolean', enum: [true, false] }, - account_id: { type: 'string' }, - token: { type: 'string' }, - url: { type: 'string' }, - sign_msg: { type: 'boolean', enum: [true, false] }, - }, - required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], - ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + account_id: { type: 'string' }, + token: { type: 'string' }, + url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, + reopen_conversation: { type: 'boolean', enum: [true, false] }, + conversation_pending: { type: 'boolean', enum: [true, false] }, + }, + required: [ + 'enabled', + 'account_id', + 'token', + 'url', + 'sign_msg', + 'reopen_conversation', + 'conversation_pending', + ], + ...isNotEmpty( + 'account_id', + 'token', + 'url', + 'sign_msg', + 'reopen_conversation', + 'conversation_pending', + ), }; export const settingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reject_call: { type: 'boolean', enum: [true, false] }, - msg_call: { type: 'string' }, - groups_ignore: { type: 'boolean', enum: [true, false] }, - }, - required: ['reject_call'], - ...isNotEmpty('reject_call'), + $id: v4(), + type: 'object', + properties: { + reject_call: { type: 'boolean', enum: [true, false] }, + msg_call: { type: 'string' }, + groups_ignore: { type: 'boolean', enum: [true, false] }, + always_online: { type: 'boolean', enum: [true, false] }, + read_messages: { type: 'boolean', enum: [true, false] }, + read_status: { type: 'boolean', enum: [true, false] }, + }, + required: [ + 'reject_call', + 'groups_ignore', + 'always_online', + 'read_messages', + 'read_status', + ], + ...isNotEmpty( + 'reject_call', + 'groups_ignore', + 'always_online', + 'read_messages', + 'read_status', + ), }; diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index d4563485..ad92e607 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -1,97 +1,105 @@ import { isURL } from 'class-validator'; - -import { ConfigService, HttpServer } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; -import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootDto } from '../dto/chatwoot.dto'; import { ChatwootService } from '../services/chatwoot.service'; +import { Logger } from '../../config/logger.config'; import { waMonitor } from '../whatsapp.module'; +import { ConfigService, HttpServer } from '../../config/env.config'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} + constructor( + private readonly chatwootService: ChatwootService, + private readonly configService: ConfigService, + ) {} - public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); + public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { + logger.verbose( + 'requested createChatwoot from ' + instance.instanceName + ' instance', + ); - if (data.enabled) { - if (!isURL(data.url, { require_tld: false })) { - throw new BadRequestException('url is not valid'); - } + if (data.enabled) { + if (!isURL(data.url, { require_tld: false })) { + throw new BadRequestException('url is not valid'); + } - if (!data.account_id) { - throw new BadRequestException('account_id is required'); - } + if (!data.account_id) { + throw new BadRequestException('account_id is required'); + } - if (!data.token) { - throw new BadRequestException('token is required'); - } + if (!data.token) { + throw new BadRequestException('token is required'); + } - if (data.sign_msg !== true && data.sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } - } - - if (!data.enabled) { - logger.verbose('chatwoot disabled'); - data.account_id = ''; - data.token = ''; - data.url = ''; - data.sign_msg = false; - } - - data.name_inbox = instance.instanceName; - - const result = this.chatwootService.create(instance, data); - - const urlServer = this.configService.get('SERVER').URL; - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; + if (data.sign_msg !== true && data.sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } } - public async findChatwoot(instance: InstanceDto) { - logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - const result = await this.chatwootService.find(instance); - - const urlServer = this.configService.get('SERVER').URL; - - if (Object.keys(result).length === 0) { - return { - enabled: false, - url: '', - account_id: '', - token: '', - sign_msg: false, - name_inbox: '', - webhook_url: '', - }; - } - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; + if (!data.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + data.sign_msg = false; + data.reopen_conversation = false; + data.conversation_pending = false; } - public async receiveWebhook(instance: InstanceDto, data: any) { - logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = new ChatwootService(waMonitor, this.configService); + data.name_inbox = instance.instanceName; - return chatwootService.receiveWebhook(instance, data); + const result = this.chatwootService.create(instance, data); + + const urlServer = this.configService.get('SERVER').URL; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; + } + + public async findChatwoot(instance: InstanceDto) { + logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); + const result = await this.chatwootService.find(instance); + + const urlServer = this.configService.get('SERVER').URL; + + if (Object.keys(result).length === 0) { + return { + enabled: false, + url: '', + account_id: '', + token: '', + sign_msg: false, + name_inbox: '', + webhook_url: '', + }; } - public async newInstance(data: any) { - const chatwootService = new ChatwootService(waMonitor, this.configService); + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; - return chatwootService.newInstance(data); - } + return response; + } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose( + 'requested receiveWebhook from ' + instance.instanceName + ' instance', + ); + const chatwootService = new ChatwootService(waMonitor, this.configService); + + return chatwootService.receiveWebhook(instance, data); + } + + public async newInstance(data: any) { + const chatwootService = new ChatwootService(waMonitor, this.configService); + + return chatwootService.newInstance(data); + } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 79449e29..d9c351f7 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -1,309 +1,376 @@ import { delay } from '@whiskeysockets/baileys'; -import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; - -import { ConfigService, HttpServer } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; -import { RedisCache } from '../../db/redis.client'; +import { Auth, ConfigService, HttpServer } from '../../config/env.config'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.service'; -import { ChatwootService } from '../services/chatwoot.service'; import { WAMonitoringService } from '../services/monitor.service'; -import { WebhookService } from '../services/webhook.service'; import { WAStartupService } from '../services/whatsapp.service'; +import { WebhookService } from '../services/webhook.service'; +import { ChatwootService } from '../services/chatwoot.service'; +import { Logger } from '../../config/logger.config'; import { wa } from '../types/wa.types'; +import { RedisCache } from '../../db/redis.client'; +import { isURL } from 'class-validator'; +import { SettingsService } from '../services/settings.service'; export class InstanceController { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly eventEmitter: EventEmitter2, - private readonly authService: AuthService, - private readonly webhookService: WebhookService, - private readonly chatwootService: ChatwootService, - private readonly cache: RedisCache, - ) {} + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly eventEmitter: EventEmitter2, + private readonly authService: AuthService, + private readonly webhookService: WebhookService, + private readonly chatwootService: ChatwootService, + private readonly settingsService: SettingsService, + private readonly cache: RedisCache, + ) {} - private readonly logger = new Logger(InstanceController.name); + private readonly logger = new Logger(InstanceController.name); - public async createInstance({ - instanceName, + public async createInstance({ + instanceName, + webhook, + webhook_by_events, + events, + qrcode, + number, + token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, + chatwoot_sign_msg, + chatwoot_reopen_conversation, + chatwoot_conversation_pending, + reject_call, + msg_call, + groups_ignore, + always_online, + read_messages, + read_status, + }: InstanceDto) { + try { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException( + 'The instance name must be lowercase and without special characters', + ); + } + + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); + + this.logger.verbose('creating instance'); + const instance = new WAStartupService( + this.configService, + this.eventEmitter, + this.repository, + this.cache, + ); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); + + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); + + this.logger.verbose('hash: ' + hash + ' generated'); + + let getEvents: string[]; + + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); + } + + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + + this.logger.verbose('creating settings'); + const settings: wa.LocalSettings = { + reject_call: reject_call || false, + msg_call: msg_call || '', + groups_ignore: groups_ignore || false, + always_online: always_online || false, + read_messages: read_messages || false, + read_status: read_status || false, + }; + + this.logger.verbose('settings: ' + JSON.stringify(settings)); + + this.settingsService.create(instance, settings); + + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(number); + await delay(5000); + getQrcode = instance.qrCode; + } + + const result = { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + settings, + qrcode: getQrcode, + }; + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + + if ( + chatwoot_reopen_conversation !== true && + chatwoot_reopen_conversation !== false + ) { + throw new BadRequestException('reopen_conversation is required'); + } + + if ( + chatwoot_conversation_pending !== true && + chatwoot_conversation_pending !== false + ) { + throw new BadRequestException('conversation_pending is required'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + number, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + number, + ); + } catch (error) { + this.logger.log(error); + } + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, webhook, webhook_by_events, - events, - qrcode, - number, - token, - chatwoot_account_id, - chatwoot_token, - chatwoot_url, - chatwoot_sign_msg, - }: InstanceDto) { - this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + events: getEvents, + settings, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + number, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, + }; + } catch (error) { + console.log(error); + return { error: true, message: error.toString() }; + } + } - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException('The instance name must be lowercase and without special characters'); - } + public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { + try { + this.logger.verbose( + 'requested connectToWhatsapp from ' + instanceName + ' instance', + ); - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); + const instance = this.waMonitor.waInstances[instanceName]; + const state = instance?.connectionStatus?.state; - this.logger.verbose('creating instance'); - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); + this.logger.verbose('state: ' + state); - this.logger.verbose('instance: ' + instance.instanceName + ' created'); + if (state == 'open') { + return await this.connectionState({ instanceName }); + } - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); + if (state == 'connecting') { + return instance.qrCode; + } - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); + if (state == 'close') { + this.logger.verbose('connecting'); + await instance.connectToWhatsapp(number); - this.logger.verbose('hash: ' + hash + ' generated'); + await delay(5000); + return instance.qrCode; + } - let getEvents: string[]; + return { + instance: { + instanceName: instanceName, + status: state, + }, + qrcode: instance?.qrCode, + }; + } catch (error) { + this.logger.error(error); + } + } - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } + public async restartInstance({ instanceName }: InstanceDto) { + try { + this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } + return { error: false, message: 'Instance restarted' }; + } catch (error) { + this.logger.error(error); + } + } - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; + public async connectionState({ instanceName }: InstanceDto) { + this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); + return { + instance: { + instanceName: instanceName, + state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, + }, + }; + } - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(number); - await delay(5000); - getQrcode = instance.qrCode; - } - - const result = { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - qrcode: getQrcode, - }; - - this.logger.verbose('instance created'); - this.logger.verbose(result); - - return result; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - number, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - number, - ); - } catch (error) { - this.logger.log(error); - } - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - number, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; + public async fetchInstances({ instanceName }: InstanceDto) { + this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); + if (instanceName) { + this.logger.verbose('instanceName: ' + instanceName); + return this.waMonitor.instanceInfo(instanceName); } - public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { - try { - this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance'); + return this.waMonitor.instanceInfo(); + } - const instance = this.waMonitor.waInstances[instanceName]; - const state = instance?.connectionStatus?.state; + public async logout({ instanceName }: InstanceDto) { + this.logger.verbose('requested logout from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); - this.logger.verbose('state: ' + state); - - if (state == 'open') { - return await this.connectionState({ instanceName }); - } - - if (state == 'connecting') { - return instance.qrCode; - } - - if (state == 'close') { - this.logger.verbose('connecting'); - await instance.connectToWhatsapp(number); - - await delay(5000); - return instance.qrCode; - } - - return { - instance: { - instanceName: instanceName, - status: state, - }, - qrcode: instance?.qrCode, - }; - } catch (error) { - this.logger.error(error); - } + if (instance.state === 'close') { + throw new BadRequestException( + 'The "' + instanceName + '" instance is not connected', + ); } - public async restartInstance({ instanceName }: InstanceDto) { - try { - this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); + try { + this.logger.verbose('logging out instance: ' + instanceName); + await this.waMonitor.waInstances[instanceName]?.client?.logout( + 'Log out instance: ' + instanceName, + ); - this.logger.verbose('logging out instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + this.logger.verbose('close connection instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - return { error: false, message: 'Instance restarted' }; - } catch (error) { - this.logger.error(error); - } + return { error: false, message: 'Instance logged out' }; + } catch (error) { + throw new InternalServerErrorException(error.toString()); } + } - public async connectionState({ instanceName }: InstanceDto) { - this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); - return { - instance: { - instanceName: instanceName, - state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, - }, - }; + public async deleteInstance({ instanceName }: InstanceDto) { + this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); + + if (instance.state === 'open') { + throw new BadRequestException( + 'The "' + instanceName + '" instance needs to be disconnected', + ); } + try { + if (instance.state === 'connecting') { + this.logger.verbose('logging out instance: ' + instanceName); - public async fetchInstances({ instanceName }: InstanceDto) { - this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); - if (instanceName) { - this.logger.verbose('instanceName: ' + instanceName); - return this.waMonitor.instanceInfo(instanceName); - } + await this.logout({ instanceName }); + delete this.waMonitor.waInstances[instanceName]; + return { error: false, message: 'Instance deleted' }; + } else { + this.logger.verbose('deleting instance: ' + instanceName); - return this.waMonitor.instanceInfo(); + delete this.waMonitor.waInstances[instanceName]; + this.eventEmitter.emit('remove.instance', instanceName, 'inner'); + return { error: false, message: 'Instance deleted' }; + } + } catch (error) { + throw new BadRequestException(error.toString()); } + } - public async logout({ instanceName }: InstanceDto) { - this.logger.verbose('requested logout from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); - - if (instance.state === 'close') { - throw new BadRequestException('The "' + instanceName + '" instance is not connected'); - } - - try { - this.logger.verbose('logging out instance: ' + instanceName); - await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); - - this.logger.verbose('close connection instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - - return { error: false, message: 'Instance logged out' }; - } catch (error) { - throw new InternalServerErrorException(error.toString()); - } - } - - public async deleteInstance({ instanceName }: InstanceDto) { - this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); - - if (instance.state === 'open') { - throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); - } - try { - if (instance.state === 'connecting') { - this.logger.verbose('logging out instance: ' + instanceName); - - await this.logout({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - return { error: false, message: 'Instance deleted' }; - } else { - this.logger.verbose('deleting instance: ' + instanceName); - - delete this.waMonitor.waInstances[instanceName]; - this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { error: false, message: 'Instance deleted' }; - } - } catch (error) { - throw new BadRequestException(error.toString()); - } - } - - public async refreshToken(_: InstanceDto, oldToken: OldToken) { - this.logger.verbose('requested refreshToken'); - return await this.authService.refreshToken(oldToken); - } + public async refreshToken(_: InstanceDto, oldToken: OldToken) { + this.logger.verbose('requested refreshToken'); + return await this.authService.refreshToken(oldToken); + } } diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 1d033783..f538abe6 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -1,26 +1,25 @@ -import { Logger } from '../../config/logger.config'; +import { isURL } from 'class-validator'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; import { SettingsService } from '../services/settings.service'; +import { Logger } from '../../config/logger.config'; const logger = new Logger('SettingsController'); export class SettingsController { - constructor(private readonly settingsService: SettingsService) {} + constructor(private readonly settingsService: SettingsService) {} - public async createSettings(instance: InstanceDto, data: SettingsDto) { - logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); + public async createSettings(instance: InstanceDto, data: SettingsDto) { + logger.verbose( + 'requested createSettings from ' + instance.instanceName + ' instance', + ); - if (data.reject_call && data.msg_call.trim() == '') { - throw new BadRequestException('msg_call is required'); - } + return this.settingsService.create(instance, data); + } - return this.settingsService.create(instance, data); - } - - public async findSettings(instance: InstanceDto) { - logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.find(instance); - } + public async findSettings(instance: InstanceDto) { + logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); + return this.settingsService.find(instance); + } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 38f17bf0..4681ef76 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,84 +1,93 @@ -import { proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; +import { + WAPrivacyOnlineValue, + WAPrivacyValue, + WAReadReceiptsValue, + proto, +} from '@whiskeysockets/baileys'; export class OnWhatsAppDto { - constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} + constructor( + public readonly jid: string, + public readonly exists: boolean, + public readonly name?: string, + ) {} } export class getBase64FromMediaMessageDto { - message: proto.WebMessageInfo; - convertToMp4?: boolean; + message: proto.WebMessageInfo; + convertToMp4?: boolean; } export class WhatsAppNumberDto { - numbers: string[]; + numbers: string[]; } export class NumberDto { - number: string; + number: string; } export class NumberBusiness { - wid?: string; - jid?: string; - exists?: boolean; - isBusiness: boolean; - name?: string; - message?: string; - description?: string; - email?: string; - website?: string[]; - address?: string; + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; } export class ProfileNameDto { - name: string; + name: string; } export class ProfileStatusDto { - status: string; + status: string; } export class ProfilePictureDto { - number?: string; - // url or base64 - picture?: string; + number?: string; + // url or base64 + picture?: string; } class Key { - id: string; - fromMe: boolean; - remoteJid: string; + id: string; + fromMe: boolean; + remoteJid: string; } export class ReadMessageDto { - readMessages: Key[]; + read_messages: Key[]; } class LastMessage { - key: Key; - messageTimestamp?: number; + key: Key; + messageTimestamp?: number; } export class ArchiveChatDto { - lastMessage: LastMessage; - archive: boolean; + lastMessage: LastMessage; + archive: boolean; } class PrivacySetting { - readreceipts: WAReadReceiptsValue; - profile: WAPrivacyValue; - status: WAPrivacyValue; - online: WAPrivacyOnlineValue; - last: WAPrivacyValue; - groupadd: WAPrivacyValue; + readreceipts: WAReadReceiptsValue; + profile: WAPrivacyValue; + status: WAPrivacyValue; + online: WAPrivacyOnlineValue; + last: WAPrivacyValue; + groupadd: WAPrivacyValue; } export class PrivacySettingDto { - privacySettings: PrivacySetting; + privacySettings: PrivacySetting; } export class DeleteMessage { - id: string; - fromMe: boolean; - remoteJid: string; - participant?: string; + id: string; + fromMe: boolean; + remoteJid: string; + participant?: string; } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index 64a2f1b6..b270c869 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -1,9 +1,11 @@ export class ChatwootDto { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index ef47f9b8..6dfdc45c 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -1,51 +1,52 @@ export class CreateGroupDto { - subject: string; - description?: string; - participants: string[]; + subject: string; + participants: string[]; + description?: string; + promoteParticipants?: boolean; } export class GroupPictureDto { - groupJid: string; - image: string; + groupJid: string; + image: string; } export class GroupSubjectDto { - groupJid: string; - subject: string; + groupJid: string; + subject: string; } export class GroupDescriptionDto { - groupJid: string; - description: string; + groupJid: string; + description: string; } export class GroupJid { - groupJid: string; + groupJid: string; } export class GetParticipant { - getParticipants: string; + getParticipants: string; } export class GroupInvite { - inviteCode: string; + inviteCode: string; } export class GroupSendInvite { - groupJid: string; - description: string; - numbers: string[]; + groupJid: string; + description: string; + numbers: string[]; } export class GroupUpdateParticipantDto extends GroupJid { - action: 'add' | 'remove' | 'promote' | 'demote'; - participants: string[]; + action: 'add' | 'remove' | 'promote' | 'demote'; + participants: string[]; } export class GroupUpdateSettingDto extends GroupJid { - action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; + action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; } export class GroupToggleEphemeralDto extends GroupJid { - expiration: 0 | 86400 | 604800 | 7776000; + expiration: 0 | 86400 | 604800 | 7776000; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 3fc780d1..c317060f 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -1,13 +1,21 @@ export class InstanceDto { - instanceName: string; - webhook?: string; - webhook_by_events?: boolean; - events?: string[]; - qrcode?: boolean; - number?: string; - token?: string; - chatwoot_account_id?: string; - chatwoot_token?: string; - chatwoot_url?: string; - chatwoot_sign_msg?: boolean; + instanceName: string; + qrcode?: boolean; + number?: string; + token?: string; + webhook?: string; + webhook_by_events?: boolean; + events?: string[]; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; + chatwoot_sign_msg?: boolean; + chatwoot_reopen_conversation?: boolean; + chatwoot_conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts index 870a24d9..594ab3a4 100644 --- a/src/whatsapp/dto/settings.dto.ts +++ b/src/whatsapp/dto/settings.dto.ts @@ -1,5 +1,8 @@ export class SettingsDto { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index 31e28894..bac226e9 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -1,28 +1,33 @@ import { Schema } from 'mongoose'; - import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { - _id?: string; - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; + _id?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } const chatwootSchema = new Schema({ - _id: { type: String, _id: true }, - enabled: { type: Boolean, required: true }, - account_id: { type: String, required: true }, - token: { type: String, required: true }, - url: { type: String, required: true }, - name_inbox: { type: String, required: true }, - sign_msg: { type: Boolean, required: true }, - number: { type: String, required: true }, + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + account_id: { type: String, required: true }, + token: { type: String, required: true }, + url: { type: String, required: true }, + name_inbox: { type: String, required: true }, + sign_msg: { type: Boolean, required: true }, + number: { type: String, required: true }, }); -export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot'); +export const ChatwootModel = dbserver?.model( + ChatwootRaw.name, + chatwootSchema, + 'chatwoot', +); export type IChatwootModel = typeof ChatwootModel; diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index 283f44fd..b6d2488d 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -1,20 +1,29 @@ import { Schema } from 'mongoose'; - import { dbserver } from '../../db/db.connect'; export class SettingsRaw { - _id?: string; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; + _id?: string; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } const settingsSchema = new Schema({ - _id: { type: String, _id: true }, - reject_call: { type: Boolean, required: true }, - msg_call: { type: String, required: true }, - groups_ignore: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + reject_call: { type: Boolean, required: true }, + msg_call: { type: String, required: true }, + groups_ignore: { type: Boolean, required: true }, + always_online: { type: Boolean, required: true }, + read_messages: { type: Boolean, required: true }, + read_status: { type: Boolean, required: true }, }); -export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings'); +export const SettingsModel = dbserver?.model( + SettingsRaw.name, + settingsSchema, + 'settings', +); export type ISettingsModel = typeof SettingsModel; diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index 57a206b8..d506cc46 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -1,112 +1,121 @@ -import fs from 'fs'; -import { MongoClient } from 'mongodb'; -import { join } from 'path'; - -import { Auth, ConfigService, Database } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; -import { AuthRepository } from './auth.repository'; -import { ChatRepository } from './chat.repository'; -import { ChatwootRepository } from './chatwoot.repository'; -import { ContactRepository } from './contact.repository'; import { MessageRepository } from './message.repository'; +import { ChatRepository } from './chat.repository'; +import { ContactRepository } from './contact.repository'; import { MessageUpRepository } from './messageUp.repository'; -import { SettingsRepository } from './settings.repository'; +import { MongoClient } from 'mongodb'; import { WebhookRepository } from './webhook.repository'; +import { ChatwootRepository } from './chatwoot.repository'; +import { SettingsRepository } from './settings.repository'; + +import { AuthRepository } from './auth.repository'; +import { Auth, ConfigService, Database } from '../../config/env.config'; +import { join } from 'path'; +import fs from 'fs'; +import { Logger } from '../../config/logger.config'; export class RepositoryBroker { - constructor( - public readonly message: MessageRepository, - public readonly chat: ChatRepository, - public readonly contact: ContactRepository, - public readonly messageUpdate: MessageUpRepository, - public readonly webhook: WebhookRepository, - public readonly chatwoot: ChatwootRepository, - public readonly settings: SettingsRepository, - public readonly auth: AuthRepository, - private configService: ConfigService, - dbServer?: MongoClient, - ) { - this.dbClient = dbServer; - this.__init_repo_without_db__(); - } + constructor( + public readonly message: MessageRepository, + public readonly chat: ChatRepository, + public readonly contact: ContactRepository, + public readonly messageUpdate: MessageUpRepository, + public readonly webhook: WebhookRepository, + public readonly chatwoot: ChatwootRepository, + public readonly settings: SettingsRepository, + public readonly auth: AuthRepository, + private configService: ConfigService, + dbServer?: MongoClient, + ) { + this.dbClient = dbServer; + this.__init_repo_without_db__(); + } - private dbClient?: MongoClient; - private readonly logger = new Logger('RepositoryBroker'); + private dbClient?: MongoClient; + private readonly logger = new Logger('RepositoryBroker'); - public get dbServer() { - return this.dbClient; - } + public get dbServer() { + return this.dbClient; + } - private __init_repo_without_db__() { - this.logger.verbose('initializing repository without db'); - if (!this.configService.get('DATABASE').ENABLED) { - const storePath = join(process.cwd(), 'store'); + private __init_repo_without_db__() { + this.logger.verbose('initializing repository without db'); + if (!this.configService.get('DATABASE').ENABLED) { + const storePath = join(process.cwd(), 'store'); - this.logger.verbose('creating store path: ' + storePath); - try { - const authDir = join(storePath, 'auth', this.configService.get('AUTHENTICATION').TYPE); - const chatsDir = join(storePath, 'chats'); - const contactsDir = join(storePath, 'contacts'); - const messagesDir = join(storePath, 'messages'); - const messageUpDir = join(storePath, 'message-up'); - const webhookDir = join(storePath, 'webhook'); - const chatwootDir = join(storePath, 'chatwoot'); - const settingsDir = join(storePath, 'settings'); - const tempDir = join(storePath, 'temp'); + this.logger.verbose('creating store path: ' + storePath); + try { + const authDir = join( + storePath, + 'auth', + this.configService.get('AUTHENTICATION').TYPE, + ); + const chatsDir = join(storePath, 'chats'); + const contactsDir = join(storePath, 'contacts'); + const messagesDir = join(storePath, 'messages'); + const messageUpDir = join(storePath, 'message-up'); + const webhookDir = join(storePath, 'webhook'); + const chatwootDir = join(storePath, 'chatwoot'); + const settingsDir = join(storePath, 'settings'); + const tempDir = join(storePath, 'temp'); - if (!fs.existsSync(authDir)) { - this.logger.verbose('creating auth dir: ' + authDir); - fs.mkdirSync(authDir, { recursive: true }); - } - if (!fs.existsSync(chatsDir)) { - this.logger.verbose('creating chats dir: ' + chatsDir); - fs.mkdirSync(chatsDir, { recursive: true }); - } - if (!fs.existsSync(contactsDir)) { - this.logger.verbose('creating contacts dir: ' + contactsDir); - fs.mkdirSync(contactsDir, { recursive: true }); - } - if (!fs.existsSync(messagesDir)) { - this.logger.verbose('creating messages dir: ' + messagesDir); - fs.mkdirSync(messagesDir, { recursive: true }); - } - if (!fs.existsSync(messageUpDir)) { - this.logger.verbose('creating message-up dir: ' + messageUpDir); - fs.mkdirSync(messageUpDir, { recursive: true }); - } - if (!fs.existsSync(webhookDir)) { - this.logger.verbose('creating webhook dir: ' + webhookDir); - fs.mkdirSync(webhookDir, { recursive: true }); - } - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(settingsDir)) { - this.logger.verbose('creating settings dir: ' + settingsDir); - fs.mkdirSync(settingsDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } - } else { - try { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - - const tempDir = join(storePath, 'temp'); - - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } + if (!fs.existsSync(authDir)) { + this.logger.verbose('creating auth dir: ' + authDir); + fs.mkdirSync(authDir, { recursive: true }); } + if (!fs.existsSync(chatsDir)) { + this.logger.verbose('creating chats dir: ' + chatsDir); + fs.mkdirSync(chatsDir, { recursive: true }); + } + if (!fs.existsSync(contactsDir)) { + this.logger.verbose('creating contacts dir: ' + contactsDir); + fs.mkdirSync(contactsDir, { recursive: true }); + } + if (!fs.existsSync(messagesDir)) { + this.logger.verbose('creating messages dir: ' + messagesDir); + fs.mkdirSync(messagesDir, { recursive: true }); + } + if (!fs.existsSync(messageUpDir)) { + this.logger.verbose('creating message-up dir: ' + messageUpDir); + fs.mkdirSync(messageUpDir, { recursive: true }); + } + if (!fs.existsSync(webhookDir)) { + this.logger.verbose('creating webhook dir: ' + webhookDir); + fs.mkdirSync(webhookDir, { recursive: true }); + } + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(settingsDir)) { + this.logger.verbose('creating settings dir: ' + settingsDir); + fs.mkdirSync(settingsDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } else { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + const chatwootDir = join(storePath, 'chatwoot'); + + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + try { + } catch (error) { + this.logger.error(error); + } } + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 48368cb9..2d470c37 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,1549 +1,1671 @@ +import { InstanceDto } from '../dto/instance.dto'; +import path from 'path'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { WAMonitoringService } from './monitor.service'; +import { Logger } from '../../config/logger.config'; import ChatwootClient from '@figuro/chatwoot-sdk'; +import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import axios from 'axios'; import FormData from 'form-data'; -import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { SendTextDto } from '../dto/sendMessage.dto'; import mimeTypes from 'mime-types'; -import path from 'path'; - -import { ConfigService, HttpServer } from '../../config/env.config'; -import { Logger } from '../../config/logger.config'; +import { SendAudioDto } from '../dto/sendMessage.dto'; +import { SendMediaDto } from '../dto/sendMessage.dto'; import { ROOT_DIR } from '../../config/path.config'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { InstanceDto } from '../dto/instance.dto'; -import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; -import { WAMonitoringService } from './monitor.service'; +import { ConfigService, HttpServer } from '../../config/env.config'; +import { type } from 'os'; export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; + private messageCacheFile: string; + private messageCache: Set; - private readonly logger = new Logger(ChatwootService.name); + private readonly logger = new Logger(ChatwootService.name); - private provider: any; + private provider: any; - constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { - this.messageCache = new Set(); + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + ) { + this.messageCache = new Set(); + } + + private loadMessageCache(): Set { + this.logger.verbose('load message cache'); + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); + } + } + + private saveMessageCache() { + this.logger.verbose('save message cache'); + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + this.logger.verbose('message cache saved'); + } + + private clearMessageCache() { + this.logger.verbose('clear message cache'); + this.messageCache.clear(); + this.saveMessageCache(); + } + + private async getProvider(instance: InstanceDto) { + this.logger.verbose('get provider to instance: ' + instance.instanceName); + try { + const provider = await this.waMonitor.waInstances[ + instance.instanceName + ].findChatwoot(); + + if (!provider) { + this.logger.warn('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + return provider; + } catch (error) { + this.logger.error('provider not found'); + return null; + } + } + + private async clientCw(instance: InstanceDto) { + this.logger.verbose('get client to instance: ' + instance.instanceName); + const provider = await this.getProvider(instance); + + if (!provider) { + this.logger.error('provider not found'); + return null; } - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); - } + this.logger.verbose('provider found'); + + this.provider = provider; + + this.logger.verbose('create client to instance: ' + instance.instanceName); + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + this.logger.verbose('client created'); + + return client; + } + + public create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + this.logger.verbose('chatwoot created'); + return data; + } + + public async find(instance: InstanceDto): Promise { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + try { + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + this.logger.error('chatwoot not found'); + return { enabled: null, url: '' }; + } + } + + public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose('get contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; } - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); + if (!id) { + this.logger.warn('id is required'); + return null; } - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); + this.logger.verbose('find contact in chatwoot'); + const contact = await client.contact.getContactable({ + accountId: this.provider.account_id, + id, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; } - private async getProvider(instance: InstanceDto) { - this.logger.verbose('get provider to instance: ' + instance.instanceName); - try { - const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + this.logger.verbose('contact found'); + return contact; + } - if (!provider) { - this.logger.warn('provider not found'); - return null; - } + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + number: string, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - this.logger.verbose('provider found'); + const client = await this.clientCw(instance); - return provider; - } catch (error) { - this.logger.error('provider not found'); - return null; - } + if (!client) { + this.logger.warn('client not found'); + return null; } - private async clientCw(instance: InstanceDto) { - this.logger.verbose('get client to instance: ' + instance.instanceName); - const provider = await this.getProvider(instance); + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); - if (!provider) { - this.logger.error('provider not found'); - return null; - } + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload + .map((inbox) => inbox.name) + .includes(inboxName); - this.logger.verbose('provider found'); + let inboxId: number; - this.provider = provider; + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, + }; - this.logger.verbose('create client to instance: ' + instance.instanceName); - const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, - }); + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); - this.logger.verbose('client created'); + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } - return client; + inboxId = inbox.id; + } else { + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; } - public create(instance: InstanceDto, data: ChatwootDto) { - this.logger.verbose('create chatwoot: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact( + instance, + '123456', + inboxId, + false, + 'EvolutionAPI', + )) as any); - this.logger.verbose('chatwoot created'); - return data; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async find(instance: InstanceDto): Promise { - this.logger.verbose('find chatwoot: ' + instance.instanceName); - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch (error) { - this.logger.error('chatwoot not found'); - return { enabled: null, url: '' }; - } + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = '/init'; + + if (number) { + contentMsg = `/init:${number}`; + } + + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: contentMsg, + message_type: 'outgoing', + }, + }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } } - public async getContact(instance: InstanceDto, id: number) { - this.logger.verbose('get contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + this.logger.verbose('instance chatwoot initialized'); + return true; + } - if (!client) { - this.logger.warn('client not found'); - return null; - } + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + isGroup: boolean, + name?: string, + avatar_url?: string, + ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); - if (!id) { - this.logger.warn('id is required'); - return null; - } + const client = await this.clientCw(instance); - this.logger.verbose('find contact in chatwoot'); - const contact = await client.contact.getContactable({ - accountId: this.provider.account_id, - id, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact found'); - return contact; + if (!client) { + this.logger.warn('client not found'); + return null; } - public async initInstanceChatwoot( - instance: InstanceDto, - inboxName: string, - webhookUrl: string, - qrcode: boolean, - number: string, - ) { - this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inbox in chatwoot'); - const findInbox: any = await client.inboxes.list({ - accountId: this.provider.account_id, - }); - - this.logger.verbose('check duplicate inbox'); - const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); - - let inboxId: number; - - if (!checkDuplicate) { - this.logger.verbose('create inbox in chatwoot'); - const data = { - type: 'api', - webhook_url: webhookUrl, - }; - - const inbox = await client.inboxes.create({ - accountId: this.provider.account_id, - data: { - name: inboxName, - channel: data as any, - }, - }); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } else { - this.logger.verbose('find inbox in chatwoot'); - const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } - - this.logger.verbose('find contact in chatwoot and create if not exists'); - const contact = - (await this.findContact(instance, '123456')) || - ((await this.createContact(instance, '123456', inboxId, false, 'EvolutionAPI')) as any); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact.id || contact.payload.contact.id; - - if (qrcode) { - this.logger.verbose('create conversation in chatwoot'); - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data: { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('create message for init instance in chatwoot'); - - let contentMsg = '/init'; - - if (number) { - contentMsg = `/init:${number}`; - } - - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: contentMsg, - message_type: 'outgoing', - }, - }); - - if (!message) { - this.logger.warn('conversation not found'); - return null; - } - } - - this.logger.verbose('instance chatwoot initialized'); - return true; + let data: any = {}; + if (!isGroup) { + this.logger.verbose('create contact in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + avatar_url: avatar_url, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + avatar_url: avatar_url, + }; } - public async createContact( - instance: InstanceDto, - phoneNumber: string, - inboxId: number, - isGroup: boolean, - name?: string, - avatar_url?: string, - ) { - this.logger.verbose('create contact to instance: ' + instance.instanceName); + this.logger.verbose('create contact in chatwoot'); + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, + }); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let data: any = {}; - if (!isGroup) { - this.logger.verbose('create contact in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - phone_number: `+${phoneNumber}`, - avatar_url: avatar_url, - }; - } else { - this.logger.verbose('create contact group in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: phoneNumber, - avatar_url: avatar_url, - }; - } - - this.logger.verbose('create contact in chatwoot'); - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact created'); - return contact; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async updateContact(instance: InstanceDto, id: number, data: any) { - this.logger.verbose('update contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + this.logger.verbose('contact created'); + return contact; + } - if (!client) { - this.logger.warn('client not found'); - return null; - } + public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose('update contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('update contact in chatwoot'); - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); - - this.logger.verbose('contact updated'); - return contact; + if (!client) { + this.logger.warn('client not found'); + return null; } - public async findContact(instance: InstanceDto, phoneNumber: string) { - this.logger.verbose('find contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let query: any; - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('format phone number'); - query = `+${phoneNumber}`; - } else { - this.logger.verbose('format group id'); - query = phoneNumber; - } - - this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); - } else { - this.logger.verbose('return group'); - return contact.payload.find((contact) => contact.identifier === query); - } + if (!id) { + this.logger.warn('id is required'); + return null; } - public async createConversation(instance: InstanceDto, body: any) { - this.logger.verbose('create conversation to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); + this.logger.verbose('update contact in chatwoot'); + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); - if (!client) { - this.logger.warn('client not found'); - return null; - } + this.logger.verbose('contact updated'); + return contact; + } - const isGroup = body.key.remoteJid.includes('@g.us'); + public async findContact(instance: InstanceDto, phoneNumber: string) { + this.logger.verbose('find contact to instance: ' + instance.instanceName); - this.logger.verbose('is group: ' + isGroup); + const client = await this.clientCw(instance); - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - - this.logger.verbose('chat id: ' + chatId); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - if (isGroup) { - this.logger.verbose('get group name'); - const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); - - nameContact = `${group.subject} (GROUP)`; - - this.logger.verbose('find or create participant in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( - body.key.participant.split('@')[0], - ); - - const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); - - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - ); - } - } - - this.logger.verbose('find or create contact in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); - - const findContact = await this.findContact(instance, chatId); - - let contact: any; - if (body.key.fromMe) { - if (findContact) { - contact = findContact; - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } else { - if (findContact) { - if (!findContact.name || findContact.name === chatId) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - contact = findContact; - } - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - this.logger.verbose('update contact name in chatwoot'); - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } - - this.logger.verbose('get contact conversations in chatwoot'); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; - - if (contactConversations) { - this.logger.verbose('return conversation if exists'); - const conversation = contactConversations.payload.find( - (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - if (conversation) { - this.logger.verbose('conversation found'); - return conversation.id; - } - } - - this.logger.verbose('create conversation in chatwoot'); - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data: { - contact_id: `${contactId}`, - inbox_id: `${filterInbox.id}`, - }, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('conversation created'); - return conversation.id; - } catch (error) { - this.logger.error(error); - } + if (!client) { + this.logger.warn('client not found'); + return null; } - public async getInbox(instance: InstanceDto) { - this.logger.verbose('get inbox to instance: ' + instance.instanceName); + let query: any; - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inboxes in chatwoot'); - const inbox = (await client.inboxes.list({ - accountId: this.provider.account_id, - })) as any; - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName); - - if (!findByName) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('return inbox'); - return findByName; + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('format phone number'); + query = `+${phoneNumber}`; + } else { + this.logger.verbose('format group id'); + query = phoneNumber; } - public async createMessage( - instance: InstanceDto, - conversationId: number, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create message to instance: ' + instance.instanceName); + this.logger.verbose('find contact in chatwoot'); + const contact: any = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('message created'); - - return message; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async createBotMessage( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create bot message to instance: ' + instance.instanceName); + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('return contact'); + return contact.payload.find((contact) => contact.phone_number === query); + } else { + this.logger.verbose('return group'); + return contact.payload.find((contact) => contact.identifier === query); + } + } - const client = await this.clientCw(instance); + public async createConversation(instance: InstanceDto, body: any) { + this.logger.verbose('create conversation to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); - if (!client) { - this.logger.warn('client not found'); - return null; - } + if (!client) { + this.logger.warn('client not found'); + return null; + } - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); + const isGroup = body.key.remoteJid.includes('@g.us'); - if (!contact) { - this.logger.warn('contact not found'); - return null; - } + this.logger.verbose('is group: ' + isGroup); - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } + this.logger.verbose('chat id: ' + chatId); - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); + let nameContact: string; - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } + nameContact = !body.key.fromMe ? body.pushName : chatId; - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + if (isGroup) { + this.logger.verbose('get group name'); + const group = await this.waMonitor.waInstances[ + instance.instanceName + ].client.groupMetadata(chatId); + + nameContact = `${group.subject} (GROUP)`; + + this.logger.verbose('find or create participant in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[ + instance.instanceName + ].profilePicture(body.key.participant.split('@')[0]); + + const findParticipant = await this.findContact( + instance, + body.key.participant.split('@')[0], ); - if (!conversation) { - this.logger.warn('conversation not found'); - return; + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } + } else { + await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + ); } + } - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: content, - message_type: messageType, - attachments: attachments, - }, + this.logger.verbose('find or create contact in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[ + instance.instanceName + ].profilePicture(chatId); + + const findContact = await this.findContact(instance, chatId); + + let contact: any; + if (body.key.fromMe) { + if (findContact) { + contact = findContact; + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } else { + if (findContact) { + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = findContact; + } + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = + contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, }); + } - if (!message) { - this.logger.warn('message not found'); - return null; + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + let conversation: any; + if (this.provider.reopen_conversation) { + conversation = contactConversations.payload.find( + (conversation) => conversation.inbox_id == filterInbox.id, + ); + } else { + conversation = contactConversations.payload.find( + (conversation) => + conversation.status !== 'resolved' && + conversation.inbox_id == filterInbox.id, + ); } + this.logger.verbose('return conversation if exists'); - this.logger.verbose('bot message created'); + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } - return message; + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; } - private async sendData( - conversationId: number, - file: string, - messageType: 'incoming' | 'outgoing' | undefined, - content?: string, - ) { - this.logger.verbose('send data to chatwoot'); + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; - const data = new FormData(); + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } + this.logger.verbose('find inbox by name'); + const findByName = inbox.payload.find( + (inbox) => inbox.name === instance.instanceName, + ); - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); + if (!findByName) { + this.logger.warn('inbox not found'); + return null; + } - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); + this.logger.verbose('return inbox'); + return findByName; + } - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('message created'); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create bot message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => + conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + ) { + this.logger.verbose('send data to chatwoot'); + + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + unlinkSync(file); + } + } + + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + this.logger.verbose('create bot qr to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => + conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('send data to chatwoot'); + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + if (file) { + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + } + } + + public async sendAttachment( + waInstance: any, + number: string, + media: any, + caption?: string, + ) { + this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + + try { + this.logger.verbose('get media type'); + const parts = media.split('/'); + + const fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); + + const mimeType = mimeTypes.lookup(fileName).toString(); + this.logger.verbose('mime type: ' + mimeType); + + let type = 'document'; + + switch (mimeType.split('/')[0]) { + case 'image': + type = 'image'; + break; + case 'video': + type = 'video'; + break; + case 'audio': + type = 'audio'; + break; + default: + type = 'document'; + break; + } + + this.logger.verbose('type: ' + type); + + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, }; - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); + await waInstance?.audioWhatsapp(data); - this.logger.verbose('remove temp file'); - unlinkSync(file); + this.logger.verbose('audio sent'); + return; + } - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - unlinkSync(file); - } + this.logger.verbose('send media to instance: ' + waInstance.instanceName); + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption) { + this.logger.verbose('caption found'); + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + this.logger.verbose('media sent'); + return; + } catch (error) { + this.logger.error(error); } + } - public async createBotQr( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - file?: string, - ) { - this.logger.verbose('create bot qr to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + this.logger.verbose( + 'receive webhook to chatwoot instance: ' + instance.instanceName, + ); + const client = await this.clientCw(instance); - if (!client) { - this.logger.warn('client not found'); - return null; + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('check if is bot'); + if (!body?.conversation || body.private) return { message: 'bot' }; + + this.logger.verbose('check if is group'); + const chatId = + body.conversation.meta.sender?.phone_number?.replace('+', '') || + body.conversation.meta.sender?.identifier; + const messageReceived = body.content; + const senderName = body?.sender?.name; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + this.logger.verbose('check if is command'); + + const command = messageReceived.replace('/', ''); + + if (command.includes('init') || command.includes('iniciar')) { + this.logger.verbose('command init found'); + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + this.logger.verbose('connect to whatsapp'); + const number = command.split(':')[1]; + await waInstance.connectToWhatsapp(number); + } else { + this.logger.verbose('whatsapp already connected'); + await this.createBotMessage( + instance, + `🚨 ${body.inbox.name} instance is connected.`, + 'incoming', + ); + } } - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); + if (command === 'status') { + this.logger.verbose('command status found'); - if (!contact) { - this.logger.warn('contact not found'); - return null; + const state = waInstance?.connectionStatus?.state; + + if (!state) { + this.logger.verbose('state not found'); + await this.createBotMessage( + instance, + `⚠️ ${body.inbox.name} instance not found.`, + 'incoming', + ); + } + + if (state) { + this.logger.verbose('state: ' + state + ' found'); + await this.createBotMessage( + instance, + `⚠️ ${body.inbox.name} instance status: *${state}*`, + 'incoming', + ); + } } - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); + if (command === 'disconnect' || command === 'desconectar') { + this.logger.verbose('command disconnect found'); - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; + const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgLogout, 'incoming'); + + this.logger.verbose('disconnect to whatsapp'); + await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); + await waInstance?.client?.ws?.close(); } - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); + if (command.includes('new_instance')) { + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; + const data = { + instanceName: command.split(':')[1], + qrcode: true, + chatwoot_account_id: this.provider.account_id, + chatwoot_token: this.provider.token, + chatwoot_url: this.provider.url, + chatwoot_sign_msg: this.provider.sign_msg, + }; + + if (command.split(':')[2]) { + data['number'] = command.split(':')[2]; + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: data, + }; + + await axios.request(config); } + } - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + if ( + body.message_type === 'outgoing' && + body?.conversation?.messages?.length && + chatId !== '123456' + ) { + this.logger.verbose('check if is group'); + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, ); + this.logger.verbose('cache file path: ' + this.messageCacheFile); - if (!conversation) { - this.logger.warn('conversation not found'); - return; + this.messageCache = this.loadMessageCache(); + this.logger.verbose('cache file loaded'); + this.logger.verbose(this.messageCache); + + this.logger.verbose('check if message is cached'); + if (this.messageCache.has(body.id.toString())) { + this.logger.verbose('message is cached'); + return { message: 'bot' }; } - this.logger.verbose('send data to chatwoot'); - const data = new FormData(); + this.logger.verbose('clear cache'); + this.clearMessageCache(); - if (content) { - this.logger.verbose('content found'); - data.append('content', content); + this.logger.verbose('Format message to send'); + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + formatText = this.provider.sign_msg + ? `*${senderName}:*\n\n${messageReceived}` + : messageReceived; } - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); + for (const message of body.conversation.messages) { + this.logger.verbose('check if message is media'); + if (message.attachments && message.attachments.length > 0) { + this.logger.verbose('message is media'); + for (const attachment of message.attachments) { + this.logger.verbose('send media to whatsapp'); + if (!messageReceived) { + this.logger.verbose('message do not have text'); + formatText = null; + } - if (file) { - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - } - } - - public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { - this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); - - try { - this.logger.verbose('get media type'); - const parts = media.split('/'); - - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); - - const mimeType = mimeTypes.lookup(fileName).toString(); - this.logger.verbose('mime type: ' + mimeType); - - let type = 'document'; - - switch (mimeType.split('/')[0]) { - case 'image': - type = 'image'; - break; - case 'video': - type = 'video'; - break; - case 'audio': - type = 'audio'; - break; - default: - type = 'document'; - break; + await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + ); } + } else { + this.logger.verbose('message is text'); - this.logger.verbose('type: ' + type); - - if (type === 'audio') { - this.logger.verbose('send audio to instance: ' + waInstance.instanceName); - const data: SendAudioDto = { - number: number, - audioMessage: { - audio: media, - }, - options: { - delay: 1200, - presence: 'recording', - }, - }; - - await waInstance?.audioWhatsapp(data); - - this.logger.verbose('audio sent'); - return; - } - - this.logger.verbose('send media to instance: ' + waInstance.instanceName); - const data: SendMediaDto = { - number: number, - mediaMessage: { - mediatype: type as any, - fileName: fileName, - media: media, - }, - options: { - delay: 1200, - presence: 'composing', - }, + this.logger.verbose('send text to whatsapp'); + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, }; - if (caption) { - this.logger.verbose('caption found'); - data.mediaMessage.caption = caption; - } - - await waInstance?.mediaMessage(data); - - this.logger.verbose('media sent'); - return; - } catch (error) { - this.logger.error(error); + await waInstance?.textMessage(data); + } } - } + } - public async receiveWebhook(instance: InstanceDto, body: any) { - try { - this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + if (body.message_type === 'template' && body.event === 'message_created') { + this.logger.verbose('check if is template'); - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private) return { message: 'bot' }; - - this.logger.verbose('check if is group'); - const chatId = - body.conversation.meta.sender?.phone_number?.replace('+', '') || - body.conversation.meta.sender?.identifier; - const messageReceived = body.content; - const senderName = body?.sender?.name; - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (chatId === '123456' && body.message_type === 'outgoing') { - this.logger.verbose('check if is command'); - - const command = messageReceived.replace('/', ''); - - if (command.includes('init') || command.includes('iniciar')) { - this.logger.verbose('command init found'); - const state = waInstance?.connectionStatus?.state; - - if (state !== 'open') { - this.logger.verbose('connect to whatsapp'); - const number = command.split(':')[1]; - await waInstance.connectToWhatsapp(number); - } else { - this.logger.verbose('whatsapp already connected'); - await this.createBotMessage( - instance, - `🚨 ${body.inbox.name} instance is connected.`, - 'incoming', - ); - } - } - - if (command === 'status') { - this.logger.verbose('command status found'); - - const state = waInstance?.connectionStatus?.state; - - if (!state) { - this.logger.verbose('state not found'); - await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance not found.`, 'incoming'); - } - - if (state) { - this.logger.verbose('state: ' + state + ' found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance status: *${state}*`, - 'incoming', - ); - } - } - - if (command === 'disconnect' || command === 'desconectar') { - this.logger.verbose('command disconnect found'); - - const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgLogout, 'incoming'); - - this.logger.verbose('disconnect to whatsapp'); - await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); - await waInstance?.client?.ws?.close(); - } - - if (command.includes('new_instance')) { - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - const data = { - instanceName: command.split(':')[1], - qrcode: true, - chatwoot_account_id: this.provider.account_id, - chatwoot_token: this.provider.token, - chatwoot_url: this.provider.url, - chatwoot_sign_msg: this.provider.sign_msg, - }; - - if (command.split(':')[2]) { - data['number'] = command.split(':')[2]; - } - - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: data, - }; - - await axios.request(config); - } - } - - if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { - this.logger.verbose('check if is group'); - - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - this.logger.verbose('cache file path: ' + this.messageCacheFile); - - this.messageCache = this.loadMessageCache(); - this.logger.verbose('cache file loaded'); - this.logger.verbose(this.messageCache); - - this.logger.verbose('check if message is cached'); - if (this.messageCache.has(body.id.toString())) { - this.logger.verbose('message is cached'); - return { message: 'bot' }; - } - - this.logger.verbose('clear cache'); - this.clearMessageCache(); - - this.logger.verbose('Format message to send'); - let formatText: string; - if (senderName === null || senderName === undefined) { - formatText = messageReceived; - } else { - formatText = this.provider.sign_msg ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; - } - - for (const message of body.conversation.messages) { - this.logger.verbose('check if message is media'); - if (message.attachments && message.attachments.length > 0) { - this.logger.verbose('message is media'); - for (const attachment of message.attachments) { - this.logger.verbose('send media to whatsapp'); - if (!messageReceived) { - this.logger.verbose('message do not have text'); - formatText = null; - } - - await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); - } - } else { - this.logger.verbose('message is text'); - - this.logger.verbose('send text to whatsapp'); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: formatText, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - await waInstance?.textMessage(data); - } - } - } - - if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is csat'); - - const data: SendTextDto = { - number: chatId, - textMessage: { - text: body.content, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - this.logger.verbose('send text to whatsapp'); - - await waInstance?.textMessage(data); - } - - return { message: 'bot' }; - } catch (error) { - this.logger.error(error); - - return { message: 'bot' }; - } - } - - private isMediaMessage(message: any) { - this.logger.verbose('check if is media message'); - const media = [ - 'imageMessage', - 'documentMessage', - 'documentWithCaptionMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - ]; - - const messageKeys = Object.keys(message); - - const result = messageKeys.some((key) => media.includes(key)); - - this.logger.verbose('is media message: ' + result); - return result; - } - - private getTypeMessage(msg: any) { - this.logger.verbose('get type message'); - - const types = { - conversation: msg.conversation, - imageMessage: msg.imageMessage?.caption, - videoMessage: msg.videoMessage?.caption, - extendedTextMessage: msg.extendedTextMessage?.text, - messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), - documentMessage: msg.documentMessage?.caption, - documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, - audioMessage: msg.audioMessage?.caption, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), + }, + options: { + delay: 1200, + presence: 'composing', + }, }; - this.logger.verbose('type message: ' + types); + this.logger.verbose('send text to whatsapp'); - return types; + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + + const result = messageKeys.some((key) => media.includes(key)); + + this.logger.verbose('is media message: ' + result); + return result; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: undefined, + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: + msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, + }; + + this.logger.verbose('type message: ' + types); + + return types; + } + + private getMessageContent(types: any) { + this.logger.verbose('get message content'); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + const result = typeKey ? types[typeKey] : undefined; + + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { + const latitude = result.degreesLatitude; + const longitude = result.degreesLongitude; + + const formattedLocation = `**Location:** + **latitude:** ${latitude} + **longitude:** ${longitude} + https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} + `; + + this.logger.verbose('message content: ' + formattedLocation); + + return formattedLocation; } - private getMessageContent(types: any) { - this.logger.verbose('get message content'); - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; - const result = typeKey ? types[typeKey] : undefined; - - if (typeKey === 'stickerMessage') { - return null; + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; } + }); - if (typeKey === 'contactMessage') { - const vCardData = result.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = `**Contact:** + let formattedContact = `**Contact:** **name:** ${contactInfo['FN']}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); - - this.logger.verbose('message content: ' + formattedContact); - return formattedContact; + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; } + }); - if (typeKey === 'contactsArrayMessage') { - const formattedContacts = result.contacts.map((contact) => { - const vCardData = contact.vcard.split('\n'); - const contactInfo = {}; + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; - let formattedContact = `**Contact:** + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** **name:** ${contact.displayName}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); - return formattedContact; - }); + return formattedContact; + }); - const formattedContactsArray = formattedContacts.join('\n\n'); + const formattedContactsArray = formattedContacts.join('\n\n'); - this.logger.verbose('formatted contacts: ' + formattedContactsArray); + this.logger.verbose('formatted contacts: ' + formattedContactsArray); - return formattedContactsArray; - } - - this.logger.verbose('message content: ' + result); - - return result; + return formattedContactsArray; } - private getConversationMessage(msg: any) { + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { + this.logger.verbose('get conversation message'); + + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + this.logger.verbose('conversation message: ' + messageContent); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } + + if (event === 'messages.upsert') { + this.logger.verbose('event messages.upsert'); + + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); - const types = this.getTypeMessage(msg); + const isMedia = this.isMediaMessage(body.message); - const messageContent = this.getMessageContent(types); - - this.logger.verbose('conversation message: ' + messageContent); - - return messageContent; - } - - public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { - this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (!waInstance) { - this.logger.warn('wa instance not found'); - return null; - } - - if (event === 'messages.upsert') { - this.logger.verbose('event messages.upsert'); - - if (body.key.remoteJid === 'status@broadcast') { - this.logger.verbose('status broadcast found'); - return; - } - - this.logger.verbose('get conversation message'); - const bodyMessage = await this.getConversationMessage(body.message); - - const isMedia = this.isMediaMessage(body.message); - - if (!bodyMessage && !isMedia) { - this.logger.warn('no body message found'); - return; - } - - this.logger.verbose('get conversation in chatwoot'); - const getConversion = await this.createConversation(instance, body); - - if (!getConversion) { - this.logger.warn('conversation not found'); - return; - } - - const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; - - this.logger.verbose('message type: ' + messageType); - - this.logger.verbose('is media: ' + isMedia); - - this.logger.verbose('check if is media'); - if (isMedia) { - this.logger.verbose('message is media'); - - this.logger.verbose('get base64 from media message'); - const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ - message: { - ...body, - }, - }); - - const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; - - const fileData = Buffer.from(downloadBase64.base64, 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - - this.logger.verbose('temp file name: ' + nameFile); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversion, fileName, messageType, content); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversion, fileName, messageType, bodyMessage); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversion, content, messageType); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversion, bodyMessage, messageType); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - if (event === 'status.instance') { - this.logger.verbose('event status.instance'); - const data = body; - const inbox = await this.getInbox(instance); - - if (!inbox) { - this.logger.warn('inbox not found'); - return; - } - - const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgStatus, 'incoming'); - } - - if (event === 'connection.update') { - this.logger.verbose('event connection.update'); - - if (body.status === 'open') { - const msgConnection = `🚀 Connection successfully established!`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - } - } - - if (event === 'qrcode.updated') { - this.logger.verbose('event qrcode.updated'); - if (body.statusCode === 500) { - this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; - - this.logger.verbose('send message to chatwoot'); - return await this.createBotMessage(instance, erroQRcode, 'incoming'); - } else { - this.logger.verbose('qrcode success'); - const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; - - this.logger.verbose('temp file name: ' + fileName); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); - - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; - - if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( - 0, - 4, - )}-${body.qrcode.pairingCode.substring(4, 8)}`; - } - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgQrCode, 'incoming'); - } - } - } catch (error) { - this.logger.error(error); + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; } - } - public async newInstance(data: any) { - try { - const instanceName = data.instanceName; - const qrcode = true; - const number = data.number; - const accountId = data.accountId; - const chatwootToken = data.token; - const chatwootUrl = data.url; - const signMsg = true; - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + this.logger.verbose('get conversation in chatwoot'); + const getConversion = await this.createConversation(instance, body); - const requestData = { - instanceName, - qrcode, - chatwoot_account_id: accountId, - chatwoot_token: chatwootToken, - chatwoot_url: chatwootUrl, - chatwoot_sign_msg: signMsg, - }; + if (!getConversion) { + this.logger.warn('conversation not found'); + return; + } - if (number) { - requestData['number'] = number; + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + this.logger.verbose('message type: ' + messageType); + + this.logger.verbose('is media: ' + isMedia); + + this.logger.verbose('check if is media'); + if (isMedia) { + this.logger.verbose('message is media'); + + this.logger.verbose('get base64 from media message'); + const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ + message: { + ...body, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + + this.logger.verbose('temp file name: ' + nameFile); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; } - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: requestData, - }; + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData( + getConversion, + fileName, + messageType, + content, + ); - // await axios.request(config); + if (!send) { + this.logger.warn('message not sent'); + return; + } - return true; - } catch (error) { - this.logger.error(error); - return null; + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData( + getConversion, + fileName, + messageType, + bodyMessage, + ); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } } + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage( + instance, + getConversion, + content, + messageType, + ); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage( + instance, + getConversion, + bodyMessage, + messageType, + ); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + if (event === 'status.instance') { + this.logger.verbose('event status.instance'); + const data = body; + const inbox = await this.getInbox(instance); + + if (!inbox) { + this.logger.warn('inbox not found'); + return; + } + + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); + + if (body.status === 'open') { + const msgConnection = `🚀 Connection successfully established!`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'qrcode.updated') { + this.logger.verbose('event qrcode.updated'); + if (body.statusCode === 500) { + this.logger.verbose('qrcode error'); + const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; + + this.logger.verbose('send message to chatwoot'); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + this.logger.verbose('qrcode success'); + const fileData = Buffer.from( + body?.qrcode.base64.replace('data:image/png;base64,', ''), + 'base64', + ); + + const fileName = `${path.join( + waInstance?.storePath, + 'temp', + `${`${instance}.png`}`, + )}`; + + this.logger.verbose('temp file name: ' + fileName); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('send qrcode to chatwoot'); + await this.createBotQr( + instance, + 'QRCode successfully generated!', + 'incoming', + fileName, + ); + + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( + 0, + 4, + )}-${body.qrcode.pairingCode.substring(4, 8)}`; + } + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); } + } + + public async newInstance(data: any) { + try { + const instanceName = data.instanceName; + const qrcode = true; + const number = data.number; + const accountId = data.accountId; + const chatwootToken = data.token; + const chatwootUrl = data.url; + const signMsg = true; + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const requestData = { + instanceName, + qrcode, + chatwoot_account_id: accountId, + chatwoot_token: chatwootToken, + chatwoot_url: chatwootUrl, + chatwoot_sign_msg: signMsg, + }; + + if (number) { + requestData['number'] = number; + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: requestData, + }; + + // await axios.request(config); + + return true; + } catch (error) { + this.logger.error(error); + return null; + } + } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index d1bae31e..4c9bf62d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,2887 +1,3067 @@ -import ffmpegPath from '@ffmpeg-installer/ffmpeg'; -import { Boom } from '@hapi/boom'; import makeWASocket, { - AnyMessageContent, - BufferedEventData, - BufferJSON, - CacheStore, - Chat, - ConnectionState, - Contact, - delay, - DisconnectReason, - downloadMediaMessage, - fetchLatestBaileysVersion, - generateWAMessageFromContent, - getAggregateVotesInPollMessage, - getContentType, - getDevice, - GroupMetadata, - isJidGroup, - isJidUser, - makeCacheableSignalKeyStore, - MessageUpsertType, - MiscMessageGenerationOptions, - ParticipantAction, - prepareWAMessageMedia, - proto, - useMultiFileAuthState, - UserFacingSocketConfig, - WABrowserDescription, - WAMediaUpload, - WAMessage, - WAMessageUpdate, - WASocket, + AnyMessageContent, + BufferedEventData, + BufferJSON, + CacheStore, + makeCacheableSignalKeyStore, + Chat, + ConnectionState, + Contact, + delay, + DisconnectReason, + downloadMediaMessage, + fetchLatestBaileysVersion, + generateWAMessageFromContent, + getContentType, + getDevice, + GroupMetadata, + isJidGroup, + isJidUser, + MessageUpsertType, + MiscMessageGenerationOptions, + ParticipantAction, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + UserFacingSocketConfig, + WABrowserDescription, + WAMediaUpload, + WAMessage, + WAMessageUpdate, + WASocket, + getAggregateVotesInPollMessage, } from '@whiskeysockets/baileys'; -import axios from 'axios'; -import { exec, execSync } from 'child_process'; -import { arrayUnique, isBase64, isURL } from 'class-validator'; -import EventEmitter2 from 'eventemitter2'; -import fs, { existsSync, readFileSync } from 'fs'; -import Long from 'long'; -import NodeCache from 'node-cache'; -import { getMIMEType } from 'node-mime-types'; -import { release } from 'os'; -import { join } from 'path'; -import P from 'pino'; -import ProxyAgent from 'proxy-agent'; -import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; -import qrcodeTerminal from 'qrcode-terminal'; -import sharp from 'sharp'; -import { v4 } from 'uuid'; - import { - Auth, - CleanStoreConf, - ConfigService, - ConfigSessionPhone, - Database, - HttpServer, - Log, - QrCode, - Redis, - Webhook, + Auth, + CleanStoreConf, + ConfigService, + ConfigSessionPhone, + Database, + HttpServer, + QrCode, + Redis, + Webhook, } from '../../config/env.config'; +import fs from 'fs'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; -import { dbserver } from '../../db/db.connect'; -import { RedisCache } from '../../db/redis.client'; -import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; -import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; -import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; -import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - WhatsAppNumberDto, -} from '../dto/chat.dto'; -import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, -} from '../dto/group.dto'; -import { - ContactMessage, - MediaMessage, - Options, - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, - StatusMessage, -} from '../dto/sendMessage.dto'; -import { SettingsRaw } from '../models'; -import { ChatRaw } from '../models/chat.model'; -import { ChatwootRaw } from '../models/chatwoot.model'; -import { ContactRaw } from '../models/contact.model'; -import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; -import { WebhookRaw } from '../models/webhook.model'; -import { ContactQuery } from '../repository/contact.repository'; -import { MessageQuery } from '../repository/message.repository'; -import { MessageUpQuery } from '../repository/messageUp.repository'; +import { existsSync, readFileSync } from 'fs'; +import { join } from 'path'; +import axios from 'axios'; +import { v4 } from 'uuid'; +import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; +import qrcodeTerminal from 'qrcode-terminal'; +import { Events, TypeMediaMessage, wa, MessageSubtype } from '../types/wa.types'; +import { Boom } from '@hapi/boom'; +import EventEmitter2 from 'eventemitter2'; +import { release } from 'os'; +import P from 'pino'; +import { execSync, exec } from 'child_process'; +import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { RepositoryBroker } from '../repository/repository.manager'; -import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; -import { waMonitor } from '../whatsapp.module'; +import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; +import { ContactRaw } from '../models/contact.model'; +import { ChatRaw } from '../models/chat.model'; +import { getMIMEType } from 'node-mime-types'; +import { + ContactMessage, + MediaMessage, + Options, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendReactionDto, + SendTextDto, + SendPollDto, + SendStickerDto, + SendStatusDto, + StatusMessage, +} from '../dto/sendMessage.dto'; +import { arrayUnique, isBase64, isURL } from 'class-validator'; +import { + ArchiveChatDto, + DeleteMessage, + NumberBusiness, + OnWhatsAppDto, + PrivacySettingDto, + ReadMessageDto, + WhatsAppNumberDto, + getBase64FromMediaMessageDto, +} from '../dto/chat.dto'; +import { MessageQuery } from '../repository/message.repository'; +import { ContactQuery } from '../repository/contact.repository'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '../../exceptions'; +import { + CreateGroupDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, + GroupToggleEphemeralDto, + GroupSubjectDto, + GroupDescriptionDto, + GroupSendInvite, + GetParticipant, +} from '../dto/group.dto'; +import { MessageUpQuery } from '../repository/messageUp.repository'; +import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; +import Long from 'long'; +import { WebhookRaw } from '../models/webhook.model'; +import { ChatwootRaw } from '../models/chatwoot.model'; +import { SettingsRaw } from '../models'; +import { dbserver } from '../../db/db.connect'; +import NodeCache from 'node-cache'; +import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; +import sharp from 'sharp'; +import { RedisCache } from '../../db/redis.client'; +import { Log } from '../../config/env.config'; import { ChatwootService } from './chatwoot.service'; +import { waMonitor } from '../whatsapp.module'; export class WAStartupService { - constructor( - private readonly configService: ConfigService, - private readonly eventEmitter: EventEmitter2, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('WAStartupService initialized'); - this.cleanStore(); - this.instance.qrcode = { count: 0 }; + constructor( + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('WAStartupService initialized'); + this.cleanStore(); + this.instance.qrcode = { count: 0 }; + } + + private readonly logger = new Logger(WAStartupService.name); + private readonly instance: wa.Instance = {}; + public client: WASocket; + private readonly localWebhook: wa.LocalWebHook = {}; + private readonly localChatwoot: wa.LocalChatwoot = {}; + private readonly localSettings: wa.LocalSettings = {}; + private stateConnection: wa.StateConnection = { state: 'close' }; + public readonly storePath = join(ROOT_DIR, 'store'); + private readonly msgRetryCounterCache: CacheStore = new NodeCache(); + private readonly userDevicesCache: CacheStore = new NodeCache(); + private endSession = false; + private logBaileys = this.configService.get('LOG').BAILEYS; + + private phoneNumber: string; + + private chatwootService = new ChatwootService(waMonitor, this.configService); + + public set instanceName(name: string) { + this.logger.verbose(`Initializing instance '${name}'`); + if (!name) { + this.logger.verbose('Instance name not found, generating random name with uuid'); + this.instance.name = v4(); + return; + } + this.instance.name = name; + this.logger.verbose(`Instance '${this.instance.name}' initialized`); + this.logger.verbose('Sending instance status to webhook'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'created', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'created', + }, + ); + } + } + + public get instanceName() { + this.logger.verbose('Getting instance name'); + return this.instance.name; + } + + public get wuid() { + this.logger.verbose('Getting remoteJid of instance'); + return this.instance.wuid; + } + + public async getProfileName() { + this.logger.verbose('Getting profile name'); + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; + if (!profileName) { + this.logger.verbose('Profile name not found, trying to get from database'); + if (this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('Database enabled, trying to get from database'); + const collection = dbserver + .getClient() + .db( + this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + + '-instances', + ) + .collection(this.instanceName); + const data = await collection.findOne({ _id: 'creds' }); + if (data) { + this.logger.verbose('Profile name found in database'); + const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { + this.logger.verbose('Profile name found in file'); + const creds = JSON.parse( + readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { + encoding: 'utf-8', + }), + ); + profileName = creds.me?.name || creds.me?.verifiedName; + } } - private readonly logger = new Logger(WAStartupService.name); - private readonly instance: wa.Instance = {}; - public client: WASocket; - private readonly localWebhook: wa.LocalWebHook = {}; - private readonly localChatwoot: wa.LocalChatwoot = {}; - private readonly localSettings: wa.LocalSettings = {}; - private stateConnection: wa.StateConnection = { state: 'close' }; - public readonly storePath = join(ROOT_DIR, 'store'); - private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache(); - private endSession = false; - private logBaileys = this.configService.get('LOG').BAILEYS; + this.logger.verbose(`Profile name: ${profileName}`); + return profileName; + } - private phoneNumber: string; + public async getProfileStatus() { + this.logger.verbose('Getting profile status'); + const status = await this.client.fetchStatus(this.instance.wuid); - private chatwootService = new ChatwootService(waMonitor, this.configService); + this.logger.verbose(`Profile status: ${status.status}`); + return status.status; + } - public set instanceName(name: string) { - this.logger.verbose(`Initializing instance '${name}'`); - if (!name) { - this.logger.verbose('Instance name not found, generating random name with uuid'); - this.instance.name = v4(); - return; + public get profilePictureUrl() { + this.logger.verbose('Getting profile picture url'); + return this.instance.profilePictureUrl; + } + + public get qrCode(): wa.QrCode { + this.logger.verbose('Getting qrcode'); + + return { + pairingCode: this.instance.qrcode?.pairingCode, + code: this.instance.qrcode?.code, + base64: this.instance.qrcode?.base64, + }; + } + + private async loadWebhook() { + this.logger.verbose('Loading webhook'); + const data = await this.repository.webhook.find(this.instanceName); + this.localWebhook.url = data?.url; + this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); + + this.localWebhook.enabled = data?.enabled; + this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); + + this.localWebhook.events = data?.events; + this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); + + this.localWebhook.webhook_by_events = data?.webhook_by_events; + this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); + + this.logger.verbose('Webhook loaded'); + } + + public async setWebhook(data: WebhookRaw) { + this.logger.verbose('Setting webhook'); + await this.repository.webhook.create(data, this.instanceName); + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + Object.assign(this.localWebhook, data); + this.logger.verbose('Webhook set'); + } + + public async findWebhook() { + this.logger.verbose('Finding webhook'); + const data = await this.repository.webhook.find(this.instanceName); + + if (!data) { + this.logger.verbose('Webhook not found'); + throw new NotFoundException('Webhook not found'); + } + + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + return data; + } + + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.localChatwoot.name_inbox = data?.name_inbox; + this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); + + this.localChatwoot.sign_msg = data?.sign_msg; + this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + + this.localChatwoot.number = data?.number; + this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); + + this.localChatwoot.reopen_conversation = data?.reopen_conversation; + this.logger.verbose( + `Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`, + ); + + this.localChatwoot.conversation_pending = data?.conversation_pending; + this.logger.verbose( + `Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`, + ); + + this.logger.verbose('Chatwoot loaded'); + } + + public async setChatwoot(data: ChatwootRaw) { + this.logger.verbose('Setting chatwoot'); + await this.repository.chatwoot.create(data, this.instanceName); + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + Object.assign(this.localChatwoot, data); + this.logger.verbose('Chatwoot set'); + } + + public async findChatwoot() { + this.logger.verbose('Finding chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + + if (!data) { + this.logger.verbose('Chatwoot not found'); + return null; + } + + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + return data; + } + + private async loadSettings() { + this.logger.verbose('Loading settings'); + const data = await this.repository.settings.find(this.instanceName); + this.localSettings.reject_call = data?.reject_call; + this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); + + this.localSettings.msg_call = data?.msg_call; + this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); + + this.localSettings.groups_ignore = data?.groups_ignore; + this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + + this.localSettings.always_online = data?.always_online; + this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); + + this.localSettings.read_messages = data?.read_messages; + this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); + + this.localSettings.read_status = data?.read_status; + this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); + + this.logger.verbose('Settings loaded'); + } + + public async setSettings(data: SettingsRaw) { + this.logger.verbose('Setting settings'); + await this.repository.settings.create(data, this.instanceName); + this.logger.verbose(`Settings reject_call: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + Object.assign(this.localSettings, data); + this.logger.verbose('Settings set'); + + this.client?.ws?.close(); + } + + public async findSettings() { + this.logger.verbose('Finding settings'); + const data = await this.repository.settings.find(this.instanceName); + + if (!data) { + this.logger.verbose('Settings not found'); + return null; + } + + this.logger.verbose(`Settings url: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + return data; + } + + public async sendDataWebhook(event: Events, data: T, local = true) { + const webhookGlobal = this.configService.get('WEBHOOK'); + const webhookLocal = this.localWebhook.events; + const serverUrl = this.configService.get('SERVER').URL; + const we = event.replace(/[\.-]/gm, '_').toUpperCase(); + const transformedWe = we.replace(/_/gm, '-').toLowerCase(); + + const expose = + this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; + const tokenStore = await this.repository.auth.find(this.instanceName); + const instanceApikey = tokenStore?.apikey || 'Apikey not found'; + + const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + if (local) { + if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { + this.logger.verbose('Sending data to webhook local'); + let baseURL; + + if (this.localWebhook.webhook_by_events) { + baseURL = `${this.localWebhook.url}/${transformedWe}`; + } else { + baseURL = this.localWebhook.url; } - this.instance.name = name; - this.logger.verbose(`Instance '${this.instance.name}' initialized`); - this.logger.verbose('Sending instance status to webhook'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-local', + url: baseURL, + event, instance: this.instance.name, - status: 'created', + data, + destination: this.localWebhook.url, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + + try { + if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { + const httpService = axios.create({ baseURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + }; + + if (expose && instanceApikey) { + postData['apikey'] = instanceApikey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-local', + 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]) { + this.logger.verbose('Sending data to webhook global'); + const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; + + let globalURL; + + if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { + globalURL = `${globalWebhook.URL}/${transformedWe}`; + } else { + globalURL = globalWebhook.URL; + } + + const localUrl = this.localWebhook.url; + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-global', + url: globalURL, + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + logData['apikey'] = globalApiKey; + } + + this.logger.log(logData); + } + + try { + if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { + const httpService = axios.create({ baseURL: globalURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + postData['apikey'] = globalApiKey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-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, + }); + } + } + } + } + + private async connectionUpdate({ + qr, + connection, + lastDisconnect, + }: Partial) { + this.logger.verbose('Connection update'); + if (qr) { + this.logger.verbose('QR code found'); + if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + this.logger.verbose('QR code limit reached'); + + this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, }); if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'created', - }, - ); - } - } - - public get instanceName() { - this.logger.verbose('Getting instance name'); - return this.instance.name; - } - - public get wuid() { - this.logger.verbose('Getting remoteJid of instance'); - return this.instance.wuid; - } - - public async getProfileName() { - this.logger.verbose('Getting profile name'); - let profileName = this.client.user?.name ?? this.client.user?.verifiedName; - if (!profileName) { - this.logger.verbose('Profile name not found, trying to get from database'); - if (this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('Database enabled, trying to get from database'); - const collection = dbserver - .getClient() - .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(this.instanceName); - const data = await collection.findOne({ _id: 'creds' }); - if (data) { - this.logger.verbose('Profile name found in database'); - const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { - this.logger.verbose('Profile name found in file'); - const creds = JSON.parse( - readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { - encoding: 'utf-8', - }), - ); - profileName = creds.me?.name || creds.me?.verifiedName; - } + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); } - this.logger.verbose(`Profile name: ${profileName}`); - return profileName; - } + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + state: 'refused', + statusReason: DisconnectReason.connectionClosed, + }); - public async getProfileStatus() { - this.logger.verbose('Getting profile status'); - const status = await this.client.fetchStatus(this.instance.wuid); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; - } - - public get profilePictureUrl() { - this.logger.verbose('Getting profile picture url'); - return this.instance.profilePictureUrl; - } - - public get qrCode(): wa.QrCode { - this.logger.verbose('Getting qrcode'); - if (this.instance.qrcode?.pairingCode) { - return { - pairingCode: this.instance.qrcode?.pairingCode, - }; + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); } - return { - code: this.instance.qrcode?.code, - base64: this.instance.qrcode?.base64, - }; - } + this.logger.verbose('endSession defined as true'); + this.endSession = true; - private async loadWebhook() { - this.logger.verbose('Loading webhook'); - const data = await this.repository.webhook.find(this.instanceName); - this.localWebhook.url = data?.url; - this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); + this.logger.verbose('Emmiting event logout.instance'); + return this.eventEmitter.emit('no.connection', this.instance.name); + } - this.localWebhook.enabled = data?.enabled; - this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); + this.logger.verbose('Incrementing QR code count'); + this.instance.qrcode.count++; - this.localWebhook.events = data?.events; - this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); + const optsQrcode: QRCodeToDataURLOptions = { + margin: 3, + scale: 4, + errorCorrectionLevel: 'H', + color: { light: '#ffffff', dark: '#198754' }, + }; - this.localWebhook.webhook_by_events = data?.webhook_by_events; - this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode( + this.phoneNumber, + ); + } else { + this.instance.qrcode.pairingCode = null; + } - this.logger.verbose('Webhook loaded'); - } - - public async setWebhook(data: WebhookRaw) { - this.logger.verbose('Setting webhook'); - await this.repository.webhook.create(data, this.instanceName); - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - Object.assign(this.localWebhook, data); - this.logger.verbose('Webhook set'); - } - - public async findWebhook() { - this.logger.verbose('Finding webhook'); - const data = await this.repository.webhook.find(this.instanceName); - - if (!data) { - this.logger.verbose('Webhook not found'); - throw new NotFoundException('Webhook not found'); + this.logger.verbose('Generating QR code'); + qrcode.toDataURL(qr, optsQrcode, (error, base64) => { + if (error) { + this.logger.error('Qrcode generate failed:' + error.toString()); + return; } - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - return data; - } + this.instance.qrcode.base64 = base64; + this.instance.qrcode.code = qr; - private async loadChatwoot() { - this.logger.verbose('Loading chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - this.localChatwoot.enabled = data?.enabled; - this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }); - this.localChatwoot.account_id = data?.account_id; - this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); - - this.localChatwoot.token = data?.token; - this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); - - this.localChatwoot.url = data?.url; - this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); - - this.localChatwoot.name_inbox = data?.name_inbox; - this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); - - this.localChatwoot.sign_msg = data?.sign_msg; - this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); - - this.logger.verbose('Chatwoot loaded'); - } - - public async setChatwoot(data: ChatwootRaw) { - this.logger.verbose('Setting chatwoot'); - await this.repository.chatwoot.create(data, this.instanceName); - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - - Object.assign(this.localChatwoot, data); - this.logger.verbose('Chatwoot set'); - } - - public async findChatwoot() { - this.logger.verbose('Finding chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - - if (!data) { - this.logger.verbose('Chatwoot not found'); - return null; - } - - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - - return data; - } - - private async loadSettings() { - this.logger.verbose('Loading settings'); - const data = await this.repository.settings.find(this.instanceName); - this.localSettings.reject_call = data?.reject_call; - this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); - - this.localSettings.msg_call = data?.msg_call; - this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); - - this.localSettings.groups_ignore = data?.groups_ignore; - this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); - - this.logger.verbose('Settings loaded'); - } - - public async setSettings(data: SettingsRaw) { - this.logger.verbose('Setting settings'); - await this.repository.settings.create(data, this.instanceName); - this.logger.verbose(`Settings reject_call: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - Object.assign(this.localSettings, data); - this.logger.verbose('Settings set'); - } - - public async findSettings() { - this.logger.verbose('Finding settings'); - const data = await this.repository.settings.find(this.instanceName); - - if (!data) { - this.logger.verbose('Settings not found'); - throw new NotFoundException('Settings not found'); - } - - this.logger.verbose(`Settings url: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - return data; - } - - public async sendDataWebhook(event: Events, data: T, local = true) { - const webhookGlobal = this.configService.get('WEBHOOK'); - const webhookLocal = this.localWebhook.events; - const serverUrl = this.configService.get('SERVER').URL; - const we = event.replace(/[.-]/gm, '_').toUpperCase(); - const transformedWe = we.replace(/_/gm, '-').toLowerCase(); - - const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; - const tokenStore = await this.repository.auth.find(this.instanceName); - const instanceApikey = tokenStore?.apikey || 'Apikey not found'; - - const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - if (local) { - if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { - this.logger.verbose('Sending data to webhook local'); - let baseURL; - - if (this.localWebhook.webhook_by_events) { - baseURL = `${this.localWebhook.url}/${transformedWe}`; - } else { - baseURL = this.localWebhook.url; - } - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-local', - url: baseURL, - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - apikey: (expose && instanceApikey) || null, - }; - - if (expose && instanceApikey) { - logData['apikey'] = instanceApikey; - } - - this.logger.log(logData); - } - - try { - if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { - const httpService = axios.create({ baseURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - }; - - if (expose && instanceApikey) { - postData['apikey'] = instanceApikey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-local', - 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]) { - this.logger.verbose('Sending data to webhook global'); - const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; - - let globalURL; - - if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { - globalURL = `${globalWebhook.URL}/${transformedWe}`; - } else { - globalURL = globalWebhook.URL; - } - - const localUrl = this.localWebhook.url; - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-global', - url: globalURL, - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - logData['apikey'] = globalApiKey; - } - - this.logger.log(logData); - } - - try { - if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { - const httpService = axios.create({ baseURL: globalURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - postData['apikey'] = globalApiKey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-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, - }); - } - } - } - } - - private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { - this.logger.verbose('Connection update'); - if (qr) { - this.logger.verbose('QR code found'); - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { - this.logger.verbose('QR code limit reached'); - - this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); - this.sendDataWebhook(Events.QRCODE_UPDATED, { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }, - ); - } - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - state: 'refused', - statusReason: DisconnectReason.connectionClosed, - }); - - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('endSession defined as true'); - this.endSession = true; - - this.logger.verbose('Emmiting event logout.instance'); - return this.eventEmitter.emit('no.connection', this.instance.name); - } - - this.logger.verbose('Incrementing QR code count'); - this.instance.qrcode.count++; - - const optsQrcode: QRCodeToDataURLOptions = { - margin: 3, - scale: 4, - errorCorrectionLevel: 'H', - color: { light: '#ffffff', dark: '#198754' }, - }; - - console.log(this.phoneNumber); - if (this.phoneNumber) { - await delay(2000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); - } else { - this.instance.qrcode.pairingCode = null; - } - - this.logger.verbose('Generating QR code'); - qrcode.toDataURL(qr, optsQrcode, (error, base64) => { - if (error) { - this.logger.error('Qrcode generate failed:' + error.toString()); - return; - } - - this.instance.qrcode.base64 = base64; - this.instance.qrcode.code = qr; - - this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }, - ); - } - }); - - this.logger.verbose('Generating QR code in terminal'); - qrcodeTerminal.generate(qr, { small: true }, (qrcode) => - this.logger.log( - `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), - ); - } - - if (connection) { - this.logger.verbose('Connection found'); - this.stateConnection = { - state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, - }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { instance: this.instance.name, - ...this.stateConnection, - }); + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }, + ); + } + }); + + this.logger.verbose('Generating QR code in terminal'); + qrcodeTerminal.generate(qr, { small: true }, (qrcode) => + this.logger.log( + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + qrcode, + ), + ); + } + + if (connection) { + this.logger.verbose('Connection found'); + this.stateConnection = { + state: connection, + statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + }; + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); + } + + if (connection === 'close') { + this.logger.verbose('Connection closed'); + const shouldReconnect = + (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + this.logger.verbose('Reconnecting to whatsapp'); + await this.connectToWhatsapp(); + } else { + this.logger.verbose('Do not reconnect to whatsapp'); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); } - if (connection === 'close') { - this.logger.verbose('Connection closed'); - const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - this.logger.verbose('Reconnecting to whatsapp'); - await this.connectToWhatsapp(); - } else { - this.logger.verbose('Do not reconnect to whatsapp'); - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); + this.logger.verbose('Emittin event logout.instance'); + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.client?.ws?.close(); + this.client.end(new Error('Close connection')); + this.logger.verbose('Connection closed'); + } + } - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('Emittin event logout.instance'); - this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); - this.client?.ws?.close(); - this.client.end(new Error('Close connection')); - this.logger.verbose('Connection closed'); - } - } - - if (connection === 'open') { - this.logger.verbose('Connection opened'); - this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); - this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; - this.logger.info( - ` + if (connection === 'open') { + this.logger.verbose('Connection opened'); + this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); + this.instance.profilePictureUrl = ( + await this.profilePicture(this.instance.wuid) + ).profilePictureUrl; + this.logger.info( + ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ └──────────────────────────────┘`.replace(/^ +/gm, ' '), - ); + ); - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.CONNECTION_UPDATE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'open', - }, - ); - } - } + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } } + } - private async getMessage(key: proto.IMessageKey, full = false) { - this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + private async getMessage(key: proto.IMessageKey, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + try { + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as proto.IWebMessageInfo[]; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; + } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = + webMessageInfo[0].message?.messageContextInfo?.messageSecret; + + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); + + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } + } + + private cleanStore() { + this.logger.verbose('Cronjob to clean store initialized'); + const cleanStore = this.configService.get('CLEAN_STORE'); + const database = this.configService.get('DATABASE'); + if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { + this.logger.verbose('Cronjob to clean store enabled'); + setInterval(() => { try { - const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.name, key: { id: key.id } }, - })) as unknown as proto.IWebMessageInfo[]; - if (full) { - this.logger.verbose('Returning full message'); - return webMessageInfo[0]; + for (const [key, value] of Object.entries(cleanStore)) { + if (value === true) { + execSync( + `rm -rf ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); + this.logger.verbose( + `Cleaned ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); } - if (webMessageInfo[0].message?.pollCreationMessage) { - this.logger.verbose('Returning poll message'); - const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; + } + } catch (error) {} + }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); + } + } - if (typeof messageSecretBase64 === 'string') { - const messageSecret = Buffer.from(messageSecretBase64, 'base64'); + private async defineAuthState() { + this.logger.verbose('Defining auth state'); + const db = this.configService.get('DATABASE'); + const redis = this.configService.get('REDIS'); - const msg = { - messageContextInfo: { - messageSecret, - }, - pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, - }; - - return msg; - } - } - - this.logger.verbose('Returning message'); - return webMessageInfo[0].message; - } catch (error) { - return { conversation: '' }; - } + if (redis?.ENABLED) { + this.logger.verbose('Redis enabled'); + this.cache.reference = this.instance.name; + return await useMultiFileAuthStateRedisDb(this.cache); } - private cleanStore() { - this.logger.verbose('Cronjob to clean store initialized'); - const cleanStore = this.configService.get('CLEAN_STORE'); + if (db.SAVE_DATA.INSTANCE && db.ENABLED) { + this.logger.verbose('Database enabled'); + return await useMultiFileAuthStateDb(this.instance.name); + } + + this.logger.verbose('Store file enabled'); + return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + } + + public async connectToWhatsapp(number?: string): Promise { + this.logger.verbose('Connecting to whatsapp'); + try { + this.loadWebhook(); + this.loadChatwoot(); + this.loadSettings(); + + this.instance.authState = await this.defineAuthState(); + + const { version } = await fetchLatestBaileysVersion(); + this.logger.verbose('Baileys version: ' + version); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + this.logger.verbose('Browser: ' + JSON.stringify(browser)); + + const socketConfig: UserFacingSocketConfig = { + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore( + this.instance.authState.state.keys, + P({ level: 'error' }), + ), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser, + version, + markOnlineOnConnect: this.localSettings.always_online, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => + (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: true, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!( + message.buttonsMessage || + message.listMessage || + message.templateMessage + ); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; + }, + }; + + this.endSession = false; + + this.logger.verbose('Creating socket'); + + this.client = makeWASocket(socketConfig); + + this.logger.verbose('Socket created'); + + this.eventHandler(); + + this.logger.verbose('Socket event handler initialized'); + + this.phoneNumber = number; + + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } + } + + private readonly chatHandle = { + 'chats.upsert': async (chats: Chat[], database: Database) => { + this.logger.verbose('Event received: chats.upsert'); + + this.logger.verbose('Finding chats in database'); + const chatsRepository = await this.repository.chat.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if chats exists in database to insert'); + const chatsRaw: ChatRaw[] = []; + for await (const chat of chats) { + if (chatsRepository.find((cr) => cr.id === chat.id)) { + continue; + } + + chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); + } + + this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); + await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert( + chatsRaw, + this.instance.name, + database.SAVE_DATA.CHATS, + ); + }, + + 'chats.update': async ( + chats: Partial< + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { + conditional: (bufferedData: BufferedEventData) => boolean; + } + >[], + ) => { + this.logger.verbose('Event received: chats.update'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { id: chat.id, owner: this.instance.wuid }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); + await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + }, + + 'chats.delete': async (chats: string[]) => { + this.logger.verbose('Event received: chats.delete'); + + this.logger.verbose('Deleting chats in database'); + chats.forEach( + async (chat) => + await this.repository.chat.delete({ + where: { owner: this.instance.name, id: chat }, + }), + ); + + this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); + await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + }, + }; + + private readonly contactHandle = { + 'contacts.upsert': async (contacts: Contact[], database: Database) => { + this.logger.verbose('Event received: contacts.upsert'); + + this.logger.verbose('Finding contacts in database'); + const contactsRepository = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if contacts exists in database to insert'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + if (contactsRepository.find((cr) => cr.id === contact.id)) { + continue; + } + + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Inserting contacts in database'); + await this.repository.contact.insert( + contactsRaw, + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); + }, + + 'contacts.update': async (contacts: Partial[], database: Database) => { + this.logger.verbose('Event received: contacts.update'); + + this.logger.verbose('Verifying if contacts exists in database to update'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name ?? contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + await this.repository.contact.update( + contactsRaw, + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); + }, + }; + + private readonly messageHandle = { + 'messaging-history.set': async ( + { + messages, + chats, + isLatest, + }: { + chats: Chat[]; + contacts: Contact[]; + messages: proto.IWebMessageInfo[]; + isLatest: boolean; + }, + database: Database, + ) => { + this.logger.verbose('Event received: messaging-history.set'); + if (isLatest) { + this.logger.verbose('isLatest defined as true'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { + id: chat.id, + owner: this.instance.name, + lastMsgTimestamp: chat.lastMessageRecvTimestamp, + }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_SET'); + await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert( + chatsRaw, + this.instance.name, + database.SAVE_DATA.CHATS, + ); + } + + const messagesRaw: MessageRaw[] = []; + const messagesRepository = await this.repository.message.find({ + where: { owner: this.instance.name }, + }); + for await (const [, m] of Object.entries(messages)) { + if (!m.message) { + continue; + } + if ( + messagesRepository.find( + (mr) => mr.owner === this.instance.name && mr.key.id === m.key.id, + ) + ) { + continue; + } + + if (Long.isLong(m?.messageTimestamp)) { + m.messageTimestamp = m.messageTimestamp?.toNumber(); + } + + messagesRaw.push({ + key: m.key, + pushName: m.pushName, + participant: m.participant, + message: { ...m.message }, + messageType: getContentType(m.message), + messageTimestamp: m.messageTimestamp as number, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); + + messages = undefined; + }, + + 'messages.upsert': async ( + { + messages, + type, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + }, + database: Database, + settings: SettingsRaw, + ) => { + this.logger.verbose('Event received: messages.upsert'); + const received = messages[0]; + + if ( + type !== 'notify' || + received.message?.protocolMessage || + received.message?.pollUpdateMessage + ) { + this.logger.verbose('message rejected'); + return; + } + + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); + } + + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + + const messageRaw: MessageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + database.SAVE_DATA.NEW_MESSAGE, + ); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) + .profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) + .profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update( + [contactRaw], + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + await this.repository.contact.insert( + [contactRaw], + this.instance.name, + database.SAVE_DATA.CONTACTS, + ); + }, + + 'messages.update': async ( + args: WAMessageUpdate[], + database: Database, + settings: SettingsRaw, + ) => { + this.logger.verbose('Event received: messages.update'); + const status: Record = { + 0: 'ERROR', + 1: 'PENDING', + 2: 'SERVER_ACK', + 3: 'DELIVERY_ACK', + 4: 'READ', + 5: 'PLAYED', + }; + for await (const { key, update } of args) { + if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + let pollUpdates: any; + if (update.pollUpdates) { + this.logger.verbose('Poll update found'); + + this.logger.verbose('Getting poll message'); + const pollCreation = await this.getMessage(key); + this.logger.verbose(pollCreation); + + if (pollCreation) { + this.logger.verbose('Getting aggregate votes in poll message'); + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } + } + + if (status[update.status] === 'READ' && !key.fromMe) return; + + if (update.message === null && update.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: status[update.status], + datetime: Date.now(), + owner: this.instance.name, + pollUpdates, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + } + } + }, + }; + + private readonly groupHandler = { + 'groups.upsert': (groupMetadata: GroupMetadata[]) => { + this.logger.verbose('Event received: groups.upsert'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); + this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); + }, + + 'groups.update': (groupMetadataUpdate: Partial[]) => { + this.logger.verbose('Event received: groups.update'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); + this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); + }, + + 'group-participants.update': (participantsUpdate: { + id: string; + participants: string[]; + action: ParticipantAction; + }) => { + this.logger.verbose('Event received: group-participants.update'); + + this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); + }, + }; + + private eventHandler() { + this.logger.verbose('Initializing event handler'); + this.client.ev.process(async (events) => { + if (!this.endSession) { const database = this.configService.get('DATABASE'); - if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { - this.logger.verbose('Cronjob to clean store enabled'); - setInterval(() => { - try { - for (const [key, value] of Object.entries(cleanStore)) { - if (value === true) { - execSync( - `rm -rf ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - this.logger.verbose( - `Cleaned ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - } - } - } catch (error) { - this.logger.error(error); - } - }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); - } - } + const settings = await this.findSettings(); - private async defineAuthState() { - this.logger.verbose('Defining auth state'); - const db = this.configService.get('DATABASE'); - const redis = this.configService.get('REDIS'); + if (events.call) { + this.logger.verbose('Listening event: call'); + const call = events.call[0]; - if (redis?.ENABLED) { - this.logger.verbose('Redis enabled'); - this.cache.reference = this.instance.name; - return await useMultiFileAuthStateRedisDb(this.cache); - } + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } - if (db.SAVE_DATA.INSTANCE && db.ENABLED) { - this.logger.verbose('Database enabled'); - return await useMultiFileAuthStateDb(this.instance.name); - } - - this.logger.verbose('Store file enabled'); - return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); - } - - public async connectToWhatsapp(number?: string): Promise { - this.logger.verbose('Connecting to whatsapp'); - try { - this.loadWebhook(); - this.loadChatwoot(); - this.loadSettings(); - - this.instance.authState = await this.defineAuthState(); - - const { version } = await fetchLatestBaileysVersion(); - this.logger.verbose('Baileys version: ' + version); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); - - const socketConfig: UserFacingSocketConfig = { - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser, - version, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: true, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; - } - - return message; - }, - }; - - this.endSession = false; - - this.logger.verbose('Creating socket'); - - this.client = makeWASocket(socketConfig); - - this.logger.verbose('Socket created'); - - this.eventHandler(); - - this.logger.verbose('Socket event handler initialized'); - - this.phoneNumber = number; - - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } - - private readonly chatHandle = { - 'chats.upsert': async (chats: Chat[], database: Database) => { - this.logger.verbose('Event received: chats.upsert'); - - this.logger.verbose('Finding chats in database'); - const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.name }, + if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + const msg = await this.client.sendMessage(call.from, { + text: settings.msg_call, }); - this.logger.verbose('Verifying if chats exists in database to insert'); - const chatsRaw: ChatRaw[] = []; - for await (const chat of chats) { - if (chatsRepository.find((cr) => cr.id === chat.id)) { - continue; - } - - chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); - } - - this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - }, - - 'chats.update': async ( - chats: Partial< - proto.IConversation & { - lastMessageRecvTimestamp?: number; - } & { - conditional: (bufferedData: BufferedEventData) => boolean; - } - >[], - ) => { - this.logger.verbose('Event received: chats.update'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { id: chat.id, owner: this.instance.wuid }; + this.logger.verbose('Sending data to event messages.upsert'); + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', }); - - this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); - }, - - 'chats.delete': async (chats: string[]) => { - this.logger.verbose('Event received: chats.delete'); - - this.logger.verbose('Deleting chats in database'); - chats.forEach( - async (chat) => - await this.repository.chat.delete({ - where: { owner: this.instance.name, id: chat }, - }), - ); - - this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); - }, - }; - - private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[], database: Database) => { - this.logger.verbose('Event received: contacts.upsert'); - - this.logger.verbose('Finding contacts in database'); - const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - if (contactsRepository.find((cr) => cr.id === contact.id)) { - continue; - } - - contactsRaw.push({ - id: contact.id, - pushName: contact?.name || contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); - - this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - - 'contacts.update': async (contacts: Partial[], database: Database) => { - this.logger.verbose('Event received: contacts.update'); - - this.logger.verbose('Verifying if contacts exists in database to update'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - contactsRaw.push({ - id: contact.id, - pushName: contact?.name ?? contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - - this.logger.verbose('Updating contacts in database'); - await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - }; - - private readonly messageHandle = { - 'messaging-history.set': async ( - { - messages, - chats, - isLatest, - }: { - chats: Chat[]; - contacts: Contact[]; - messages: proto.IWebMessageInfo[]; - isLatest: boolean; - }, - database: Database, - ) => { - this.logger.verbose('Event received: messaging-history.set'); - if (isLatest) { - this.logger.verbose('isLatest defined as true'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { - id: chat.id, - owner: this.instance.name, - lastMsgTimestamp: chat.lastMessageRecvTimestamp, - }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_SET'); - await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - } - - const messagesRaw: MessageRaw[] = []; - const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.name }, - }); - for await (const [, m] of Object.entries(messages)) { - if (!m.message) { - continue; - } - if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { - continue; - } - - if (Long.isLong(m?.messageTimestamp)) { - m.messageTimestamp = m.messageTimestamp?.toNumber(); - } - - messagesRaw.push({ - key: m.key, - pushName: m.pushName, - participant: m.participant, - message: { ...m.message }, - messageType: getContentType(m.message), - messageTimestamp: m.messageTimestamp as number, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); - - messages = undefined; - }, - - 'messages.upsert': async ( - { - messages, - type, - }: { - messages: proto.IWebMessageInfo[]; - type: MessageUpsertType; - }, - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.upsert'); - const received = messages[0]; - - if ( - type !== 'notify' || - // received.message?.protocolMessage || - received.message?.pollUpdateMessage - ) { - this.logger.verbose('message rejected'); - return; - } - - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } - - if (settings.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - - const messageRaw: MessageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - }, - - 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { - this.logger.verbose('Event received: messages.update'); - const status: Record = { - 0: 'ERROR', - 1: 'PENDING', - 2: 'SERVER_ACK', - 3: 'DELIVERY_ACK', - 4: 'READ', - 5: 'PLAYED', - }; - for await (const { key, update } of args) { - if (settings.groups_ignore && key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { - this.logger.verbose('Message update is valid'); - - let pollUpdates: any; - if (update.pollUpdates) { - this.logger.verbose('Poll update found'); - - this.logger.verbose('Getting poll message'); - const pollCreation = await this.getMessage(key); - this.logger.verbose(pollCreation); - - if (pollCreation) { - this.logger.verbose('Getting aggregate votes in poll message'); - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); - } - } - - if (status[update.status] === 'READ' && !key.fromMe) return; - - if (update.message === null && update.status === undefined) { - this.logger.verbose('Message deleted'); - - this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - await this.sendDataWebhook(Events.MESSAGES_DELETE, key); - - const message: MessageUpdateRaw = { - ...key, - status: 'DELETED', - datetime: Date.now(), - owner: this.instance.name, - }; - - this.logger.verbose(message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - return; - } - - const message: MessageUpdateRaw = { - ...key, - status: status[update.status], - datetime: Date.now(), - owner: this.instance.name, - pollUpdates, - }; - - this.logger.verbose(message); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - } - } - }, - }; - - private readonly groupHandler = { - 'groups.upsert': (groupMetadata: GroupMetadata[]) => { - this.logger.verbose('Event received: groups.upsert'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); - this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); - }, - - 'groups.update': (groupMetadataUpdate: Partial[]) => { - this.logger.verbose('Event received: groups.update'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); - this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); - }, - - 'group-participants.update': (participantsUpdate: { - id: string; - participants: string[]; - action: ParticipantAction; - }) => { - this.logger.verbose('Event received: group-participants.update'); - - this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - }, - }; - - private eventHandler() { - this.logger.verbose('Initializing event handler'); - this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - this.logger.verbose('Listening event: call'); - const call = events.call[0]; - - if (settings?.reject_call && call.status == 'offer') { - this.logger.verbose('Rejecting call'); - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { - this.logger.verbose('Sending message in call'); - const msg = await this.client.sendMessage(call.from, { - text: settings.msg_call, - }); - - this.client.ev.emit('messages.upsert', { - messages: [msg], - type: 'notify', - }); - } - } - - if (events['connection.update']) { - this.logger.verbose('Listening event: connection.update'); - this.connectionUpdate(events['connection.update']); - } - - if (events['creds.update']) { - this.logger.verbose('Listening event: creds.update'); - this.instance.authState.saveCreds(); - } - - if (events['messaging-history.set']) { - this.logger.verbose('Listening event: messaging-history.set'); - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload, database); - } - - if (events['messages.upsert']) { - this.logger.verbose('Listening event: messages.upsert'); - const payload = events['messages.upsert']; - this.messageHandle['messages.upsert'](payload, database, settings); - } - - if (events['messages.update']) { - this.logger.verbose('Listening event: messages.update'); - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, database, settings); - } - - if (events['presence.update']) { - this.logger.verbose('Listening event: presence.update'); - const payload = events['presence.update']; - - if (settings.groups_ignore && payload.id.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } - - if (!settings?.groups_ignore) { - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } - - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } - - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); - } - } - - if (events['chats.upsert']) { - this.logger.verbose('Listening event: chats.upsert'); - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload, database); - } - - if (events['chats.update']) { - this.logger.verbose('Listening event: chats.update'); - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } - - if (events['chats.delete']) { - this.logger.verbose('Listening event: chats.delete'); - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } - - if (events['contacts.upsert']) { - this.logger.verbose('Listening event: contacts.upsert'); - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload, database); - } - - if (events['contacts.update']) { - this.logger.verbose('Listening event: contacts.update'); - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload, database); - } - } - }); + } + + this.logger.verbose('Sending data to webhook in event CALL'); + this.sendDataWebhook(Events.CALL, call); + } + + if (events['connection.update']) { + this.logger.verbose('Listening event: connection.update'); + this.connectionUpdate(events['connection.update']); + } + + if (events['creds.update']) { + this.logger.verbose('Listening event: creds.update'); + this.instance.authState.saveCreds(); + } + + if (events['messaging-history.set']) { + this.logger.verbose('Listening event: messaging-history.set'); + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload, database); + } + + if (events['messages.upsert']) { + this.logger.verbose('Listening event: messages.upsert'); + const payload = events['messages.upsert']; + this.messageHandle['messages.upsert'](payload, database, settings); + } + + if (events['messages.update']) { + this.logger.verbose('Listening event: messages.update'); + const payload = events['messages.update']; + this.messageHandle['messages.update'](payload, database, settings); + } + + if (events['presence.update']) { + this.logger.verbose('Listening event: presence.update'); + const payload = events['presence.update']; + + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } + + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } + + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } + + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } + + if (events['chats.upsert']) { + this.logger.verbose('Listening event: chats.upsert'); + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload, database); + } + + if (events['chats.update']) { + this.logger.verbose('Listening event: chats.update'); + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } + + if (events['chats.delete']) { + this.logger.verbose('Listening event: chats.delete'); + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } + + if (events['contacts.upsert']) { + this.logger.verbose('Listening event: contacts.upsert'); + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload, database); + } + + if (events['contacts.update']) { + this.logger.verbose('Listening event: contacts.update'); + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload, database); + } + } + }); + } + + // Check if the number is MX or AR + private formatMXOrARNumber(jid: string): string { + const countryCode = jid.substring(0, 2); + + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + if (jid.length === 13) { + const number = countryCode + jid.substring(3); + return number; + } + + return jid; + } + return jid; + } + + // Check if the number is br + private formatBRNumber(jid: string) { + const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); + if (regexp.test(jid)) { + const match = regexp.exec(jid); + if (match && match[1] === '55') { + const joker = Number.parseInt(match[3][0]); + const ddd = Number.parseInt(match[2]); + if (joker < 7 || ddd < 31) { + return match[0]; + } + return match[1] + match[2] + match[3]; + } + return jid; + } else { + return jid; + } + } + + private createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); + + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); + return number; } - // Check if the number is MX or AR - private formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; - } - return jid; + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; } - // Check if the number is br - private formatBRNumber(jid: string) { - const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); - if (regexp.test(jid)) { - const match = regexp.exec(jid); - if (match && match[1] === '55') { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 31) { - return match[0]; - } - return match[1] + match[2] + match[3]; - } - return jid; - } else { - return jid; - } + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(/\:/)[0] + .split('@')[0]; + + if (number.includes('-') && number.length >= 24) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; } - private createJid(number: string): string { - this.logger.verbose('Creating jid with number: ' + number); + number = number.replace(/\D/g, ''); - if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); - return number; - } - - if (number.includes('@broadcast')) { - this.logger.verbose('Number already contains @broadcast'); - return number; - } - - number = number - ?.replace(/\s/g, '') - .replace(/\+/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .split(':')[0] - .split('@')[0]; - - if (number.length >= 18) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - number = number.replace(/\D/g, ''); - - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); - return `${number}@s.whatsapp.net`; + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; } - public async profilePicture(number: string) { - const jid = this.createJid(number); + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } - this.logger.verbose('Getting profile picture with jid: ' + jid); - try { - this.logger.verbose('Getting profile picture url'); - return { - wuid: jid, - profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), - }; - } catch (error) { - this.logger.verbose('Profile picture not found'); - return { - wuid: jid, - profilePictureUrl: null, - }; - } + public async profilePicture(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile picture with jid: ' + jid); + try { + this.logger.verbose('Getting profile picture url'); + return { + wuid: jid, + profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), + }; + } catch (error) { + this.logger.verbose('Profile picture not found'); + return { + wuid: jid, + profilePictureUrl: null, + }; + } + } + + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const info = await waMonitor.instanceInfo(instanceName); + const business = await this.fetchBusinessProfile(jid); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + + private async sendMessageWithTyping( + number: string, + message: T, + options?: Options, + ) { + this.logger.verbose('Sending message with typing'); + + const numberWA = await this.whatsappNumber({ numbers: [number] }); + const isWA = numberWA[0]; + + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); } - public async getStatus(number: string) { - const jid = this.createJid(number); + const sender = isWA.jid; - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } + try { + if (options?.delay) { + this.logger.verbose('Delaying message'); - public async fetchProfile(instanceName: string, number?: string) { - const jid = number ? this.createJid(number) : this.client?.user?.id; + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - const info = await waMonitor.instanceInfo(instanceName); - const business = await this.fetchBusinessProfile(jid); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(jid); - const status = await this.getStatus(jid); - - return { - wuid: jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } - - private async sendMessageWithTyping(number: string, message: T, options?: Options) { - this.logger.verbose('Sending message with typing'); - - const numberWA = await this.whatsappNumber({ numbers: [number] }); - const isWA = numberWA[0]; - - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - try { - if (options?.delay) { - this.logger.verbose('Delaying message'); - - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); - this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); - - await delay(options.delay); - this.logger.verbose('Set delay: ' + options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } - - const linkPreview = options?.linkPreview != false ? undefined : false; - - let quoted: WAMessage; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - this.logger.verbose('Quoted message'); - } - - let mentions: string[]; - if (isJidGroup(sender)) { - try { - const groupMetadata = await this.client.groupMetadata(sender); - - if (!groupMetadata) { - throw new NotFoundException('Group not found'); - } - - if (options?.mentions) { - this.logger.verbose('Mentions defined'); - - if (!Array.isArray(options.mentions.mentioned) && !options.mentions.everyOne) { - throw new BadRequestException('Mentions must be an array'); - } - - if (options.mentions.everyOne) { - this.logger.verbose('Mentions everyone'); - - this.logger.verbose('Getting group metadata'); - mentions = groupMetadata.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - return null; - // throw new BadRequestException('Mentions must be a number'); - } - return jid; - }); - } - } - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - - const messageSent = await (async () => { - const option = { - quoted, - }; - - if ( - !message['audio'] && - !message['poll'] && - !message['sticker'] && - !message['conversation'] && - sender !== 'status@broadcast' - ) { - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } - } - - if (message['conversation']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - text: message['conversation'], - mentions, - linkPreview: linkPreview, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (sender === 'status@broadcast') { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message['status'].content as unknown as AnyMessageContent, - { - backgroundColor: message['status'].option.backgroundColor, - font: message['status'].option.font, - statusJidList: message['status'].option.statusJidList, - } as unknown as MiscMessageGenerationOptions, - ); - } - - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - })(); - - const messageRaw: MessageRaw = { - key: messageSent.key, - pushName: messageSent.pushName, - message: { ...messageSent.message }, - messageType: getContentType(messageSent.message), - messageTimestamp: messageSent.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(messageSent.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - // if (this.localChatwoot.enabled) { - // this.chatwootService.eventWhatsapp( - // Events.SEND_MESSAGE, - // { instanceName: this.instance.name }, - // messageRaw, - // ); - // } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, - ); - - return messageSent; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Instance Controller - public get connectionStatus() { - this.logger.verbose('Getting connection status'); - return this.stateConnection; - } - - // Send Message Controller - public async textMessage(data: SendTextDto) { - this.logger.verbose('Sending text message'); - return await this.sendMessageWithTyping( - data.number, - { - conversation: data.textMessage.text, - }, - data?.options, + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); + this.logger.verbose( + 'Sending presence update: ' + options?.presence ?? 'composing', ); - } - public async pollMessage(data: SendPollDto) { - this.logger.verbose('Sending poll message'); - return await this.sendMessageWithTyping( - data.number, - { - poll: { - name: data.pollMessage.name, - selectableCount: data.pollMessage.selectableCount, - values: data.pollMessage.values, + await delay(options.delay); + this.logger.verbose('Set delay: ' + options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } + + const linkPreview = options?.linkPreview != false ? undefined : false; + + let quoted: WAMessage; + + if (options?.quoted) { + const m = options?.quoted; + + const msg = m?.message + ? m + : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let mentions: string[]; + if (isJidGroup(sender)) { + try { + const groupMetadata = await this.client.groupMetadata(sender); + + if (!groupMetadata) { + throw new NotFoundException('Group not found'); + } + + if (options?.mentions) { + this.logger.verbose('Mentions defined'); + + if (options.mentions?.everyOne) { + this.logger.verbose('Mentions everyone'); + + this.logger.verbose('Getting group metadata'); + mentions = groupMetadata.participants.map((participant) => participant.id); + this.logger.verbose('Getting group metadata for mentions'); + } else if (options.mentions?.mentioned?.length) { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + return null; + // throw new BadRequestException('Mentions must be a number'); + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); + } + } + + const messageSent = await (async () => { + const option = { + quoted, + }; + + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + !message['conversation'] && + sender !== 'status@broadcast' + ) { + if (!message['audio']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, }, - }, - data?.options, + mentions, + }, + option as unknown as MiscMessageGenerationOptions, + ); + } + } + + if (message['conversation']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + text: message['conversation'], + mentions, + linkPreview: linkPreview, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + + if (sender === 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, + { + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); + } + + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, ); + })(); + + const messageRaw: MessageRaw = { + key: messageSent.key, + pushName: messageSent.pushName, + message: { ...messageSent.message }, + messageType: getContentType(messageSent.message), + messageTimestamp: messageSent.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + // if (this.localChatwoot.enabled) { + // this.chatwootService.eventWhatsapp( + // Events.SEND_MESSAGE, + // { instanceName: this.instance.name }, + // messageRaw, + // ); + // } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + return messageSent; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Instance Controller + public get connectionStatus() { + this.logger.verbose('Getting connection status'); + return this.stateConnection; + } + + // Send Message Controller + public async textMessage(data: SendTextDto) { + this.logger.verbose('Sending text message'); + return await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, + }, + data?.options, + ); + } + + public async pollMessage(data: SendPollDto) { + this.logger.verbose('Sending poll message'); + return await this.sendMessageWithTyping( + data.number, + { + poll: { + name: data.pollMessage.name, + selectableCount: data.pollMessage.selectableCount, + values: data.pollMessage.values, + }, + }, + data?.options, + ); + } + + private async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); } - private async formatStatusMessage(status: StatusMessage) { - this.logger.verbose('Formatting status message'); - - if (!status.type) { - throw new BadRequestException('Type is required'); - } - - if (!status.content) { - throw new BadRequestException('Content is required'); - } - - if (status.allContacts) { - this.logger.verbose('All contacts defined as true'); - - this.logger.verbose('Getting contacts from database'); - const contacts = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - if (!contacts.length) { - throw new BadRequestException('Contacts not found'); - } - - this.logger.verbose('Getting contacts with push name'); - status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); - - this.logger.verbose(status.statusJidList); - } - - if (!status.statusJidList?.length && !status.allContacts) { - throw new BadRequestException('StatusJidList is required'); - } - - if (status.type === 'text') { - this.logger.verbose('Type defined as text'); - - if (!status.backgroundColor) { - throw new BadRequestException('Background color is required'); - } - - if (!status.font) { - throw new BadRequestException('Font is required'); - } - - return { - content: { - text: status.content, - }, - option: { - backgroundColor: status.backgroundColor, - font: status.font, - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'image') { - this.logger.verbose('Type defined as image'); - - return { - content: { - image: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'video') { - this.logger.verbose('Type defined as video'); - - return { - content: { - video: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'audio') { - this.logger.verbose('Type defined as audio'); - - this.logger.verbose('Processing audio'); - const convert = await this.processAudio(status.content, 'status@broadcast'); - if (typeof convert === 'string') { - this.logger.verbose('Audio processed'); - const audio = fs.readFileSync(convert).toString('base64'); - - const result = { - content: { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - option: { - statusJidList: status.statusJidList, - }, - }; - - fs.unlinkSync(convert); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - throw new BadRequestException('Type not found'); + if (!status.content) { + throw new BadRequestException('Content is required'); } - public async statusMessage(data: SendStatusDto) { - this.logger.verbose('Sending status message'); - const status = await this.formatStatusMessage(data.statusMessage); + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); - return await this.sendMessageWithTyping('status@broadcast', { - status, - }); + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts + .filter((contact) => contact.pushName) + .map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); } - private async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - this.logger.verbose('Preparing media message'); - const prepareMedia = await prepareWAMessageMedia( - { - [mediaMessage.mediatype]: isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'), - } as any, - { upload: this.client.waUploadToServer }, - ); - - const mediaType = mediaMessage.mediatype + 'Message'; - this.logger.verbose('Media type: ' + mediaType); - - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - this.logger.verbose('If media type is document and file name is not defined then'); - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - this.logger.verbose('File name: ' + mediaMessage.fileName); - } - - let mimetype: string; - - if (isURL(mediaMessage.media)) { - mimetype = getMIMEType(mediaMessage.media); - } else { - mimetype = getMIMEType(mediaMessage.fileName); - } - - this.logger.verbose('Mimetype: ' + mimetype); - - prepareMedia[mediaType].caption = mediaMessage?.caption; - prepareMedia[mediaType].mimetype = mimetype; - prepareMedia[mediaType].fileName = mediaMessage.fileName; - - if (mediaMessage.mediatype === 'video') { - this.logger.verbose('Is media type video then set gif playback as false'); - prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( - readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), - ); - prepareMedia[mediaType].gifPlayback = false; - } - - this.logger.verbose('Generating wa message from content'); - return generateWAMessageFromContent( - '', - { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, - ); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); } - private async convertToWebP(image: string, number: string) { - try { - this.logger.verbose('Converting image to WebP to sticker'); + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); - let imagePath: string; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to image name: ' + hash); + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } - const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; - this.logger.verbose('Output path: ' + outputPath); + if (!status.font) { + throw new BadRequestException('Font is required'); + } - if (isBase64(image)) { - this.logger.verbose('Image is base64'); + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); - const imageBuffer = Buffer.from(base64Data, 'base64'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } else { - this.logger.verbose('Image is url'); + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); - const timestamp = new Date().getTime(); - const url = `${image}?timestamp=${timestamp}`; - this.logger.verbose('including timestamp in url: ' + url); + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting image from url'); + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; - const imageBuffer = Buffer.from(response.data, 'binary'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); + fs.unlinkSync(convert); - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } - - await sharp(imagePath).webp().toFile(outputPath); - this.logger.verbose('Image converted to WebP'); - - fs.unlinkSync(imagePath); - this.logger.verbose('Temp image deleted'); - - return outputPath; - } catch (error) { - console.error('Erro ao converter a imagem para WebP:', error); - } + return result; + } else { + throw new InternalServerErrorException(convert); + } } - public async mediaSticker(data: SendStickerDto) { - this.logger.verbose('Sending media sticker'); - const convert = await this.convertToWebP(data.stickerMessage.image, data.number); - const result = await this.sendMessageWithTyping( - data.number, - { - sticker: { url: convert }, - }, - data?.options, + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + const status = await this.formatStatusMessage(data.statusMessage); + + return await this.sendMessageWithTyping('status@broadcast', { + status, + }); + } + + private async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + const prepareMedia = await prepareWAMessageMedia( + { + [mediaMessage.mediatype]: isURL(mediaMessage.media) + ? { url: mediaMessage.media } + : Buffer.from(mediaMessage.media, 'base64'), + } as any, + { upload: this.client.waUploadToServer }, + ); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose( + 'If media type is document and file name is not defined then', + ); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + let mimetype: string; + + if (isURL(mediaMessage.media)) { + mimetype = getMIMEType(mediaMessage.media); + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } + + this.logger.verbose('Mimetype: ' + mimetype); + + prepareMedia[mediaType].caption = mediaMessage?.caption; + prepareMedia[mediaType].mimetype = mimetype; + prepareMedia[mediaType].fileName = mediaMessage.fileName; + + if (mediaMessage.mediatype === 'video') { + this.logger.verbose('Is media type video then set gif playback as false'); + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), + ); + prepareMedia[mediaType].gifPlayback = false; + } + + this.logger.verbose('Generating wa message from content'); + return generateWAMessageFromContent( + '', + { [mediaType]: { ...prepareMedia[mediaType] } }, + { userJid: this.instance.wuid }, + ); + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + private async convertToWebP(image: string, number: string) { + try { + this.logger.verbose('Converting image to WebP to sticker'); + + let imagePath: string; + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to image name: ' + hash); + + const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; + this.logger.verbose('Output path: ' + outputPath); + + if (isBase64(image)) { + this.logger.verbose('Image is base64'); + + const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const imageBuffer = Buffer.from(base64Data, 'base64'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } else { + this.logger.verbose('Image is url'); + + const timestamp = new Date().getTime(); + const url = `${image}?timestamp=${timestamp}`; + this.logger.verbose('including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting image from url'); + + const imageBuffer = Buffer.from(response.data, 'binary'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } + + await sharp(imagePath).webp().toFile(outputPath); + this.logger.verbose('Image converted to WebP'); + + fs.unlinkSync(imagePath); + this.logger.verbose('Temp image deleted'); + + return outputPath; + } catch (error) { + console.error('Erro ao converter a imagem para WebP:', error); + } + } + + public async mediaSticker(data: SendStickerDto) { + this.logger.verbose('Sending media sticker'); + const convert = await this.convertToWebP(data.stickerMessage.image, data.number); + const result = await this.sendMessageWithTyping( + data.number, + { + sticker: { url: convert }, + }, + data?.options, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted image deleted'); + + return result; + } + + public async mediaMessage(data: SendMediaDto) { + this.logger.verbose('Sending media message'); + const generate = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping( + data.number, + { ...generate.message }, + data?.options, + ); + } + + private async processAudio(audio: string, number: string) { + this.logger.verbose('Processing audio'); + let tempAudioPath: string; + let outputAudio: string; + + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to audio name: ' + hash); + + if (isURL(audio)) { + this.logger.verbose('Audio is url'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const timestamp = new Date().getTime(); + const url = `${audio}?timestamp=${timestamp}`; + + this.logger.verbose('Including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting audio from url'); + + fs.writeFileSync(tempAudioPath, response.data); + } else { + this.logger.verbose('Audio is base64'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const audioBuffer = Buffer.from(audio, 'base64'); + fs.writeFileSync(tempAudioPath, audioBuffer); + this.logger.verbose('Temp audio created'); + } + + this.logger.verbose('Converting audio to mp4'); + return new Promise((resolve, reject) => { + exec( + `${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, + (error, _stdout, _stderr) => { + fs.unlinkSync(tempAudioPath); + this.logger.verbose('Temp audio deleted'); + + if (error) reject(error); + + this.logger.verbose('Audio converted to mp4'); + resolve(outputAudio); + }, + ); + }); + } + + public async audioWhatsapp(data: SendAudioDto) { + this.logger.verbose('Sending audio whatsapp'); + + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; + } + + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, ); fs.unlinkSync(convert); - this.logger.verbose('Converted image deleted'); + this.logger.verbose('Converted audio deleted'); return result; + } else { + throw new InternalServerErrorException(convert); + } } - public async mediaMessage(data: SendMediaDto) { - this.logger.verbose('Sending media message'); - const generate = await this.prepareMediaMessage(data.mediaMessage); + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); + } - return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options); + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; + + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; } - private async processAudio(audio: string, number: string) { - this.logger.verbose('Processing audio'); - let tempAudioPath: string; - let outputAudio: string; + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to audio name: ' + hash); + if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { + throw new BadRequestException( + 'Button texts cannot be repeated', + 'Button IDs cannot be repeated.', + ); + } - if (isURL(audio)) { - this.logger.verbose('Audio is url'); + return await this.sendMessageWithTyping( + data.number, + { + buttonsMessage: { + text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, + contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, + footerText: data.buttonMessage?.footerText, + buttons: data.buttonMessage.buttons.map((button) => { + return { + buttonText: { + displayText: button.buttonText, + }, + buttonId: button.buttonId, + type: 1, + }; + }), + headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + }, + data?.options, + ); + } - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + return await this.sendMessageWithTyping( + data.number, + { + listMessage: { + title: data.listMessage.title, + description: data.listMessage.description, + buttonText: data.listMessage?.buttonText, + footerText: data.listMessage?.footerText, + sections: data.listMessage.sections, + listType: 1, + }, + }, + data?.options, + ); + } - const timestamp = new Date().getTime(); - const url = `${audio}?timestamp=${timestamp}`; + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + const message: proto.IMessage = {}; - this.logger.verbose('Including timestamp in url: ' + url); + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = + 'BEGIN:VCARD\n' + + 'VERSION:3.0\n' + + `N:${contact.fullName}\n` + + `FN:${contact.fullName}\n`; - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting audio from url'); + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } - fs.writeFileSync(tempAudioPath, response.data); + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += + `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + + 'item1.X-ABLabel:Celular\n' + + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + // Chat Controller + public async whatsappNumber(data: WhatsAppNumberDto) { + this.logger.verbose('Getting whatsapp number'); + + const onWhatsapp: OnWhatsAppDto[] = []; + for await (const number of data.numbers) { + let jid = this.createJid(number); + + if (isJidGroup(jid)) { + const group = await this.findGroup({ groupJid: jid }, 'inner'); + + if (!group) throw new BadRequestException('Group not found'); + + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else { + jid = !jid.startsWith('+') ? `+${jid}` : jid; + const verify = await this.client.onWhatsApp(jid); + + const result = verify[0]; + + if (!result) { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); } else { - this.logger.verbose('Audio is base64'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const audioBuffer = Buffer.from(audio, 'base64'); - fs.writeFileSync(tempAudioPath, audioBuffer); - this.logger.verbose('Temp audio created'); + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); } + } + } + return onWhatsapp; + } + + public async markMessageAsRead(data: ReadMessageDto) { + this.logger.verbose('Marking message as read'); + try { + const keys: proto.IMessageKey[] = []; + data.read_messages.forEach((read) => { + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); + } + }); + await this.client.readMessages(keys); + return { message: 'Read messages', read: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Read messages fail', error.toString()); + } + } + + public async archiveChat(data: ArchiveChatDto) { + this.logger.verbose('Archiving chat'); + try { + data.lastMessage.messageTimestamp = + data.lastMessage?.messageTimestamp ?? Date.now(); + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [data.lastMessage], + }, + data.lastMessage.key.remoteJid, + ); + + return { + chatId: data.lastMessage.key.remoteJid, + archived: true, + }; + } catch (error) { + throw new InternalServerErrorException({ + archived: false, + message: [ + 'An error occurred while archiving the chat. Open a calling.', + error.toString(), + ], + }); + } + } + + public async deleteMessage(del: DeleteMessage) { + this.logger.verbose('Deleting message'); + try { + return await this.client.sendMessage(del.remoteJid, { delete: del }); + } catch (error) { + throw new InternalServerErrorException( + 'Error while deleting message for everyone', + error?.toString(), + ); + } + } + + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { + this.logger.verbose('Getting base64 from media message'); + try { + const m = data?.message; + const convertToMp4 = data?.convertToMp4 ?? false; + + const msg = m?.message + ? m + : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + for (const subtype of MessageSubtype) { + if (msg.message[subtype]) { + msg.message = msg.message[subtype].message; + } + } + + let mediaMessage: any; + let mediaType: string; + + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; + } + } + + if (!mediaMessage) { + throw 'The message is not of the media type'; + } + + if (typeof mediaMessage['mediaKey'] === 'object') { + msg.message = JSON.parse(JSON.stringify(msg.message)); + } + + this.logger.verbose('Downloading media message'); + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }), + reuploadRequest: this.client.updateMediaMessage, + }, + ); + const typeMessage = getContentType(msg.message); + + if (convertToMp4 && typeMessage === 'audioMessage') { this.logger.verbose('Converting audio to mp4'); - return new Promise((resolve, reject) => { - exec( - `${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, - (error, _stdout, _stderr) => { - fs.unlinkSync(tempAudioPath); - this.logger.verbose('Temp audio deleted'); + const number = msg.key.remoteJid.split('@')[0]; + const convert = await this.processAudio(buffer.toString('base64'), number); - if (error) reject(error); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + this.logger.verbose('Audio converted to mp4'); - this.logger.verbose('Audio converted to mp4'); - resolve(outputAudio); - }, - ); - }); - } - - public async audioWhatsapp(data: SendAudioDto) { - this.logger.verbose('Sending audio whatsapp'); - - if (!data.options?.encoding && data.options?.encoding !== false) { - data.options.encoding = true; - } - - if (data.options?.encoding) { - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - return await this.sendMessageWithTyping( - data.number, - { - audio: isURL(data.audioMessage.audio) - ? { url: data.audioMessage.audio } - : Buffer.from(data.audioMessage.audio, 'base64'), - ptt: true, - mimetype: 'audio/ogg; codecs=opus', + const result = { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], }, - { presence: 'recording', delay: data?.options?.delay }, - ); - } + mimetype: 'audio/mp4', + base64: Buffer.from(audio, 'base64').toString('base64'), + }; - public async buttonMessage(data: SendButtonDto) { - this.logger.verbose('Sending button message'); - const embeddedMedia: any = {}; - let mediatype = 'TEXT'; + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); - if (data.buttonMessage?.mediaMessage) { - mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; - embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; - const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); - embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; - embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + this.logger.verbose('Media message downloaded'); + return result; } + } - const btnItems = { - text: data.buttonMessage.buttons.map((btn) => btn.buttonText), - ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + this.logger.verbose('Media message downloaded'); + return { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: mediaMessage['mimetype'], + base64: buffer.toString('base64'), + }; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + public async fetchContacts(query: ContactQuery) { + this.logger.verbose('Fetching contacts'); + if (query?.where) { + query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } + } else { + query = { + where: { + owner: this.instance.name, + }, + }; + } + return await this.repository.contact.find(query); + } + + public async fetchMessages(query: MessageQuery) { + this.logger.verbose('Fetching messages'); + if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.message.find(query); + } + + public async fetchStatusMessage(query: MessageUpQuery) { + this.logger.verbose('Fetching status messages'); + if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.messageUpdate.find(query); + } + + public async fetchChats() { + this.logger.verbose('Fetching chats'); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); + } + + public async fetchPrivacySettings() { + this.logger.verbose('Fetching privacy settings'); + return await this.client.fetchPrivacySettings(); + } + + public async updatePrivacySettings(settings: PrivacySettingDto) { + this.logger.verbose('Updating privacy settings'); + try { + await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); + this.logger.verbose('Read receipts privacy updated'); + + await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); + this.logger.verbose('Profile picture privacy updated'); + + await this.client.updateStatusPrivacy(settings.privacySettings.status); + this.logger.verbose('Status privacy updated'); + + await this.client.updateOnlinePrivacy(settings.privacySettings.online); + this.logger.verbose('Online privacy updated'); + + await this.client.updateLastSeenPrivacy(settings.privacySettings.last); + this.logger.verbose('Last seen privacy updated'); + + await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); + this.logger.verbose('Groups add privacy updated'); + + this.client?.ws?.close(); + + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating privacy settings', + error.toString(), + ); + } + } + + public async fetchBusinessProfile(number: string): Promise { + this.logger.verbose('Fetching business profile'); + try { + const jid = number ? this.createJid(number) : this.instance.wuid; + + const profile = await this.client.getBusinessProfile(jid); + this.logger.verbose('Trying to get business profile'); + + if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; + } + + this.logger.verbose('Business profile fetched'); + return { + isBusiness: true, + ...profile, + }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating profile name', + error.toString(), + ); + } + } + + public async updateProfileName(name: string) { + this.logger.verbose('Updating profile name to ' + name); + try { + await this.client.updateProfileName(name); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating profile name', + error.toString(), + ); + } + } + + public async updateProfileStatus(status: string) { + this.logger.verbose('Updating profile status to: ' + status); + try { + await this.client.updateProfileStatus(status); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating profile status', + error.toString(), + ); + } + } + + public async updateProfilePicture(picture: string) { + this.logger.verbose('Updating profile picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(this.instance.wuid, pic); + this.logger.verbose('Profile picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating profile picture', + error.toString(), + ); + } + } + + public async removeProfilePicture() { + this.logger.verbose('Removing profile picture'); + try { + await this.client.removeProfilePicture(this.instance.wuid); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error removing profile picture', + error.toString(), + ); + } + } + + // Group + public async createGroup(create: CreateGroupDto) { + this.logger.verbose('Creating group: ' + create.subject); + try { + const participants = create.participants.map((p) => this.createJid(p)); + const { id } = await this.client.groupCreate(create.subject, participants); + this.logger.verbose('Group created: ' + id); + + if (create?.description) { + this.logger.verbose('Updating group description: ' + create.description); + await this.client.groupUpdateDescription(id, create.description); + } + + if (create?.promoteParticipants) { + this.logger.verbose('Prometing group participants: ' + create.description); + await this.updateGParticipant({ + groupJid: id, + action: 'promote', + participants: participants, + }); + } + + const group = await this.client.groupMetadata(id); + this.logger.verbose('Getting group metadata'); + + return group; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException('Error creating group', error.toString()); + } + } + + public async updateGroupPicture(picture: GroupPictureDto) { + this.logger.verbose('Updating group picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture.image)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture.image}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture.image)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture.image, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(picture.groupJid, pic); + this.logger.verbose('Group picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error update group picture', + error.toString(), + ); + } + } + + public async updateGroupSubject(data: GroupSubjectDto) { + this.logger.verbose('Updating group subject to: ' + data.subject); + try { + await this.client.groupUpdateSubject(data.groupJid, data.subject); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating group subject', + error.toString(), + ); + } + } + + public async updateGroupDescription(data: GroupDescriptionDto) { + this.logger.verbose('Updating group description to: ' + data.description); + try { + await this.client.groupUpdateDescription(data.groupJid, data.description); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException( + 'Error updating group description', + error.toString(), + ); + } + } + + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { + this.logger.verbose('Fetching group'); + try { + return await this.client.groupMetadata(id.groupJid); + } catch (error) { + if (reply === 'inner') { + return; + } + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async fetchAllGroups(getParticipants: GetParticipant) { + this.logger.verbose('Fetching all groups'); + try { + const fetch = Object.values(await this.client.groupFetchAllParticipating()); + + const groups = fetch.map((group) => { + const result = { + id: group.id, + subject: group.subject, + subjectOwner: group.subjectOwner, + subjectTime: group.subjectTime, + size: group.size, + creation: group.creation, + owner: group.owner, + desc: group.desc, + descId: group.descId, + restrict: group.restrict, + announce: group.announce, }; - if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { - throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); + if (getParticipants.getParticipants == 'true') { + result['participants'] = group.participants; } - return await this.sendMessageWithTyping( - data.number, - { - buttonsMessage: { - text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, - contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, - footerText: data.buttonMessage?.footerText, - buttons: data.buttonMessage.buttons.map((button) => { - return { - buttonText: { - displayText: button.buttonText, - }, - buttonId: button.buttonId, - type: 1, - }; - }), - headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], - [embeddedMedia?.mediaKey]: embeddedMedia?.message, - }, - }, - data?.options, - ); + return result; + }); + + return groups; + } catch (error) { + throw new NotFoundException('Error fetching group', error.toString()); } + } - public async locationMessage(data: SendLocationDto) { - this.logger.verbose('Sending location message'); - return await this.sendMessageWithTyping( - data.number, - { - locationMessage: { - degreesLatitude: data.locationMessage.latitude, - degreesLongitude: data.locationMessage.longitude, - name: data.locationMessage?.name, - address: data.locationMessage?.address, - }, - }, - data?.options, - ); + public async inviteCode(id: GroupJid) { + this.logger.verbose('Fetching invite code for group: ' + id.groupJid); + try { + const code = await this.client.groupInviteCode(id.groupJid); + return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + } catch (error) { + throw new NotFoundException('No invite code', error.toString()); } + } - public async listMessage(data: SendListDto) { - this.logger.verbose('Sending list message'); - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.listMessage.title, - description: data.listMessage.description, - buttonText: data.listMessage?.buttonText, - footerText: data.listMessage?.footerText, - sections: data.listMessage.sections, - listType: 1, - }, - }, - data?.options, - ); + public async inviteInfo(id: GroupInvite) { + this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); + try { + return await this.client.groupGetInviteInfo(id.inviteCode); + } catch (error) { + throw new NotFoundException('No invite info', id.inviteCode); } + } - public async contactMessage(data: SendContactDto) { - this.logger.verbose('Sending contact message'); - const message: proto.IMessage = {}; + public async sendInvite(id: GroupSendInvite) { + this.logger.verbose('Sending invite for group: ' + id.groupJid); + try { + const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); + this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); - const vcard = (contact: ContactMessage) => { - this.logger.verbose('Creating vcard'); - let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + const inviteUrl = inviteCode.inviteUrl; + this.logger.verbose('Invite url: ' + inviteUrl); - if (contact.organization) { - this.logger.verbose('Organization defined'); - result += `ORG:${contact.organization};\n`; - } + const numbers = id.numbers.map((number) => this.createJid(number)); + const description = id.description ?? ''; - if (contact.email) { - this.logger.verbose('Email defined'); - result += `EMAIL:${contact.email}\n`; - } + const msg = `${description}\n\n${inviteUrl}`; - if (contact.url) { - this.logger.verbose('Url defined'); - result += `URL:${contact.url}\n`; - } + const message = { + conversation: msg, + }; - if (!contact.wuid) { - this.logger.verbose('Wuid defined'); - contact.wuid = this.createJid(contact.phoneNumber); - } + for await (const number of numbers) { + await this.sendMessageWithTyping(number, message); + } - result += - `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - this.logger.verbose('Vcard created'); - return result; - }; - - if (data.contactMessage.length === 1) { - message.contactMessage = { - displayName: data.contactMessage[0].fullName, - vcard: vcard(data.contactMessage[0]), - }; - } else { - message.contactsArrayMessage = { - displayName: `${data.contactMessage.length} contacts`, - contacts: data.contactMessage.map((contact) => { - return { - displayName: contact.fullName, - vcard: vcard(contact), - }; - }), - }; - } - - return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + return { send: true, inviteUrl }; + } catch (error) { + throw new NotFoundException('No send invite'); } + } - public async reactionMessage(data: SendReactionDto) { - this.logger.verbose('Sending reaction message'); - return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { - reactionMessage: { - key: data.reactionMessage.key, - text: data.reactionMessage.reaction, - }, - }); + public async revokeInviteCode(id: GroupJid) { + this.logger.verbose('Revoking invite code for group: ' + id.groupJid); + try { + const inviteCode = await this.client.groupRevokeInvite(id.groupJid); + return { revoked: true, inviteCode }; + } catch (error) { + throw new NotFoundException('Revoke error', error.toString()); } + } - // Chat Controller - public async whatsappNumber(data: WhatsAppNumberDto) { - this.logger.verbose('Getting whatsapp number'); - - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); - - if (isJidGroup(jid)) { - const group = await this.findGroup({ groupJid: jid }, 'inner'); - - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); - } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); - } - } - } - - return onWhatsapp; + public async findParticipants(id: GroupJid) { + this.logger.verbose('Fetching participants for group: ' + id.groupJid); + try { + const participants = (await this.client.groupMetadata(id.groupJid)).participants; + return { participants }; + } catch (error) { + throw new NotFoundException('No participants', error.toString()); } + } - public async markMessageAsRead(data: ReadMessageDto) { - this.logger.verbose('Marking message as read'); - try { - const keys: proto.IMessageKey[] = []; - data.readMessages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { - keys.push({ - remoteJid: read.remoteJid, - fromMe: read.fromMe, - id: read.id, - }); - } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); - } + public async updateGParticipant(update: GroupUpdateParticipantDto) { + this.logger.verbose('Updating participants'); + try { + const participants = update.participants.map((p) => this.createJid(p)); + const updateParticipants = await this.client.groupParticipantsUpdate( + update.groupJid, + participants, + update.action, + ); + return { updateParticipants: updateParticipants }; + } catch (error) { + throw new BadRequestException('Error updating participants', error.toString()); } + } - public async archiveChat(data: ArchiveChatDto) { - this.logger.verbose('Archiving chat'); - try { - data.lastMessage.messageTimestamp = data.lastMessage?.messageTimestamp ?? Date.now(); - await this.client.chatModify( - { - archive: data.archive, - lastMessages: [data.lastMessage], - }, - data.lastMessage.key.remoteJid, - ); - - return { - chatId: data.lastMessage.key.remoteJid, - archived: true, - }; - } catch (error) { - throw new InternalServerErrorException({ - archived: false, - message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], - }); - } + public async updateGSetting(update: GroupUpdateSettingDto) { + this.logger.verbose('Updating setting for group: ' + update.groupJid); + try { + const updateSetting = await this.client.groupSettingUpdate( + update.groupJid, + update.action, + ); + return { updateSetting: updateSetting }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); } + } - public async deleteMessage(del: DeleteMessage) { - this.logger.verbose('Deleting message'); - try { - return await this.client.sendMessage(del.remoteJid, { delete: del }); - } catch (error) { - throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); - } + public async toggleEphemeral(update: GroupToggleEphemeralDto) { + this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); + try { + const toggleEphemeral = await this.client.groupToggleEphemeral( + update.groupJid, + update.expiration, + ); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); } + } - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { - this.logger.verbose('Getting base64 from media message'); - try { - const m = data?.message; - const convertToMp4 = data?.convertToMp4 ?? false; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - for (const subtype of MessageSubtype) { - if (msg.message[subtype]) { - msg.message = msg.message[subtype].message; - } - } - - let mediaMessage: any; - let mediaType: string; - - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; - } - } - - if (!mediaMessage) { - throw 'The message is not of the media type'; - } - - if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message = JSON.parse(JSON.stringify(msg.message)); - } - - this.logger.verbose('Downloading media message'); - const buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }), - reuploadRequest: this.client.updateMediaMessage, - }, - ); - const typeMessage = getContentType(msg.message); - - if (convertToMp4 && typeMessage === 'audioMessage') { - this.logger.verbose('Converting audio to mp4'); - const number = msg.key.remoteJid.split('@')[0]; - const convert = await this.processAudio(buffer.toString('base64'), number); - - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - this.logger.verbose('Audio converted to mp4'); - - const result = { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: 'audio/mp4', - base64: Buffer.from(audio, 'base64').toString('base64'), - }; - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - this.logger.verbose('Media message downloaded'); - return result; - } - } - - this.logger.verbose('Media message downloaded'); - return { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: mediaMessage['mimetype'], - base64: buffer.toString('base64'), - }; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - public async fetchContacts(query: ContactQuery) { - this.logger.verbose('Fetching contacts'); - if (query?.where) { - query.where.owner = this.instance.name; - if (query.where?.id) { - query.where.id = this.createJid(query.where.id); - } - } else { - query = { - where: { - owner: this.instance.name, - }, - }; - } - return await this.repository.contact.find(query); - } - - public async fetchMessages(query: MessageQuery) { - this.logger.verbose('Fetching messages'); - if (query?.where) { - if (query.where?.key?.remoteJid) { - query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.message.find(query); - } - - public async fetchStatusMessage(query: MessageUpQuery) { - this.logger.verbose('Fetching status messages'); - if (query?.where) { - if (query.where?.remoteJid) { - query.where.remoteJid = this.createJid(query.where.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.messageUpdate.find(query); - } - - public async fetchChats() { - this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.name } }); - } - - public async fetchPrivacySettings() { - this.logger.verbose('Fetching privacy settings'); - return await this.client.fetchPrivacySettings(); - } - - public async updatePrivacySettings(settings: PrivacySettingDto) { - this.logger.verbose('Updating privacy settings'); - try { - await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); - this.logger.verbose('Read receipts privacy updated'); - - await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); - this.logger.verbose('Profile picture privacy updated'); - - await this.client.updateStatusPrivacy(settings.privacySettings.status); - this.logger.verbose('Status privacy updated'); - - await this.client.updateOnlinePrivacy(settings.privacySettings.online); - this.logger.verbose('Online privacy updated'); - - await this.client.updateLastSeenPrivacy(settings.privacySettings.last); - this.logger.verbose('Last seen privacy updated'); - - await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); - this.logger.verbose('Groups add privacy updated'); - - // reinicia a instancia - this.client?.ws?.close(); - - return { - update: 'success', - data: { - readreceipts: settings.privacySettings.readreceipts, - profile: settings.privacySettings.profile, - status: settings.privacySettings.status, - online: settings.privacySettings.online, - last: settings.privacySettings.last, - groupadd: settings.privacySettings.groupadd, - }, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating privacy settings', error.toString()); - } - } - - public async fetchBusinessProfile(number: string): Promise { - this.logger.verbose('Fetching business profile'); - try { - const jid = number ? this.createJid(number) : this.instance.wuid; - - const profile = await this.client.getBusinessProfile(jid); - this.logger.verbose('Trying to get business profile'); - - if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); - - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift(), - }; - } - - this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileName(name: string) { - this.logger.verbose('Updating profile name to ' + name); - try { - await this.client.updateProfileName(name); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileStatus(status: string) { - this.logger.verbose('Updating profile status to: ' + status); - try { - await this.client.updateProfileStatus(status); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile status', error.toString()); - } - } - - public async updateProfilePicture(picture: string) { - this.logger.verbose('Updating profile picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(this.instance.wuid, pic); - this.logger.verbose('Profile picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile picture', error.toString()); - } - } - - public async removeProfilePicture() { - this.logger.verbose('Removing profile picture'); - try { - await this.client.removeProfilePicture(this.instance.wuid); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error removing profile picture', error.toString()); - } - } - - // Group - public async createGroup(create: CreateGroupDto) { - this.logger.verbose('Creating group: ' + create.subject); - try { - const participants = create.participants.map((p) => this.createJid(p)); - const { id } = await this.client.groupCreate(create.subject, participants); - this.logger.verbose('Group created: ' + id); - - if (create?.description) { - this.logger.verbose('Updating group description: ' + create.description); - await this.client.groupUpdateDescription(id, create.description); - } - - const group = await this.client.groupMetadata(id); - this.logger.verbose('Getting group metadata'); - - return { groupMetadata: group }; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); - } - } - - public async updateGroupPicture(picture: GroupPictureDto) { - this.logger.verbose('Updating group picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture.image)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture.image}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture.image)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture.image, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(picture.groupJid, pic); - this.logger.verbose('Group picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error update group picture', error.toString()); - } - } - - public async updateGroupSubject(data: GroupSubjectDto) { - this.logger.verbose('Updating group subject to: ' + data.subject); - try { - await this.client.groupUpdateSubject(data.groupJid, data.subject); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group subject', error.toString()); - } - } - - public async updateGroupDescription(data: GroupDescriptionDto) { - this.logger.verbose('Updating group description to: ' + data.description); - try { - await this.client.groupUpdateDescription(data.groupJid, data.description); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group description', error.toString()); - } - } - - public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { - this.logger.verbose('Fetching group'); - try { - return await this.client.groupMetadata(id.groupJid); - } catch (error) { - if (reply === 'inner') { - return; - } - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async fetchAllGroups(getParticipants: GetParticipant) { - this.logger.verbose('Fetching all groups'); - try { - const fetch = Object.values(await this.client.groupFetchAllParticipating()); - - const groups = fetch.map((group) => { - const result = { - id: group.id, - subject: group.subject, - subjectOwner: group.subjectOwner, - subjectTime: group.subjectTime, - size: group.size, - creation: group.creation, - owner: group.owner, - desc: group.desc, - descId: group.descId, - restrict: group.restrict, - announce: group.announce, - }; - - if (getParticipants.getParticipants == 'true') { - result['participants'] = group.participants; - } - - return result; - }); - - return groups; - } catch (error) { - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async inviteCode(id: GroupJid) { - this.logger.verbose('Fetching invite code for group: ' + id.groupJid); - try { - const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { - throw new NotFoundException('No invite code', error.toString()); - } - } - - public async inviteInfo(id: GroupInvite) { - this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); - try { - return await this.client.groupGetInviteInfo(id.inviteCode); - } catch (error) { - throw new NotFoundException('No invite info', id.inviteCode); - } - } - - public async sendInvite(id: GroupSendInvite) { - this.logger.verbose('Sending invite for group: ' + id.groupJid); - try { - const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); - this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); - - const inviteUrl = inviteCode.inviteUrl; - this.logger.verbose('Invite url: ' + inviteUrl); - - const numbers = id.numbers.map((number) => this.createJid(number)); - const description = id.description ?? ''; - - const msg = `${description}\n\n${inviteUrl}`; - - const message = { - conversation: msg, - }; - - for await (const number of numbers) { - await this.sendMessageWithTyping(number, message); - } - - this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - - return { send: true, inviteUrl }; - } catch (error) { - throw new NotFoundException('No send invite'); - } - } - - public async revokeInviteCode(id: GroupJid) { - this.logger.verbose('Revoking invite code for group: ' + id.groupJid); - try { - const inviteCode = await this.client.groupRevokeInvite(id.groupJid); - return { revoked: true, inviteCode }; - } catch (error) { - throw new NotFoundException('Revoke error', error.toString()); - } - } - - public async findParticipants(id: GroupJid) { - this.logger.verbose('Fetching participants for group: ' + id.groupJid); - try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; - return { participants }; - } catch (error) { - throw new NotFoundException('No participants', error.toString()); - } - } - - public async updateGParticipant(update: GroupUpdateParticipantDto) { - this.logger.verbose('Updating participants'); - try { - const participants = update.participants.map((p) => this.createJid(p)); - const updateParticipants = await this.client.groupParticipantsUpdate( - update.groupJid, - participants, - update.action, - ); - return { updateParticipants: updateParticipants }; - } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); - } - } - - public async updateGSetting(update: GroupUpdateSettingDto) { - this.logger.verbose('Updating setting for group: ' + update.groupJid); - try { - const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); - return { updateSetting: updateSetting }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async toggleEphemeral(update: GroupToggleEphemeralDto) { - this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); - try { - const toggleEphemeral = await this.client.groupToggleEphemeral(update.groupJid, update.expiration); - return { success: true }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async leaveGroup(id: GroupJid) { - this.logger.verbose('Leaving group: ' + id.groupJid); - try { - await this.client.groupLeave(id.groupJid); - return { groupJid: id.groupJid, leave: true }; - } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); - } + public async leaveGroup(id: GroupJid) { + this.logger.verbose('Leaving group: ' + id.groupJid); + try { + await this.client.groupLeave(id.groupJid); + return { groupJid: id.groupJid, leave: true }; + } catch (error) { + throw new BadRequestException('Unable to leave the group', error.toString()); } + } } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 7e4b8352..a0d514d8 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -2,81 +2,101 @@ import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'; export enum Events { - APPLICATION_STARTUP = 'application.startup', - QRCODE_UPDATED = 'qrcode.updated', - CONNECTION_UPDATE = 'connection.update', - STATUS_INSTANCE = 'status.instance', - MESSAGES_SET = 'messages.set', - MESSAGES_UPSERT = 'messages.upsert', - MESSAGES_UPDATE = 'messages.update', - MESSAGES_DELETE = 'messages.delete', - SEND_MESSAGE = 'send.message', - CONTACTS_SET = 'contacts.set', - CONTACTS_UPSERT = 'contacts.upsert', - CONTACTS_UPDATE = 'contacts.update', - PRESENCE_UPDATE = 'presence.update', - CHATS_SET = 'chats.set', - CHATS_UPDATE = 'chats.update', - CHATS_UPSERT = 'chats.upsert', - CHATS_DELETE = 'chats.delete', - GROUPS_UPSERT = 'groups.upsert', - GROUPS_UPDATE = 'groups.update', - GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + APPLICATION_STARTUP = 'application.startup', + QRCODE_UPDATED = 'qrcode.updated', + CONNECTION_UPDATE = 'connection.update', + STATUS_INSTANCE = 'status.instance', + MESSAGES_SET = 'messages.set', + MESSAGES_UPSERT = 'messages.upsert', + MESSAGES_UPDATE = 'messages.update', + MESSAGES_DELETE = 'messages.delete', + SEND_MESSAGE = 'send.message', + CONTACTS_SET = 'contacts.set', + CONTACTS_UPSERT = 'contacts.upsert', + CONTACTS_UPDATE = 'contacts.update', + PRESENCE_UPDATE = 'presence.update', + CHATS_SET = 'chats.set', + CHATS_UPDATE = 'chats.update', + CHATS_UPSERT = 'chats.upsert', + CHATS_DELETE = 'chats.delete', + GROUPS_UPSERT = 'groups.upsert', + GROUPS_UPDATE = 'groups.update', + GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + CALL = 'call', } export declare namespace wa { - export type QrCode = { - count?: number; - pairingCode?: string; - base64?: string; - code?: string; - }; - export type Instance = { - qrcode?: QrCode; - pairingCode?: string; - authState?: { state: AuthenticationState; saveCreds: () => void }; - name?: string; - wuid?: string; - profileName?: string; - profilePictureUrl?: string; - }; + export type QrCode = { + count?: number; + pairingCode?: string; + base64?: string; + code?: string; + }; + export type Instance = { + qrcode?: QrCode; + pairingCode?: string; + authState?: { state: AuthenticationState; saveCreds: () => void }; + name?: string; + wuid?: string; + profileName?: string; + profilePictureUrl?: string; + }; - export type LocalWebHook = { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; - }; + export type LocalWebHook = { + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; + }; - export type LocalChatwoot = { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; + }; - export type LocalSettings = { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - }; + export type LocalSettings = { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + }; - export type StateConnection = { - instance?: string; - state?: WAConnectionState | 'refused'; - statusReason?: number; - }; + export type StateConnection = { + instance?: string; + state?: WAConnectionState | 'refused'; + statusReason?: number; + }; - export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; + export type StatusMessage = + | 'ERROR' + | 'PENDING' + | 'SERVER_ACK' + | 'DELIVERY_ACK' + | 'READ' + | 'DELETED' + | 'PLAYED'; } -export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage']; +export const TypeMediaMessage = [ + 'imageMessage', + 'documentMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', +]; export const MessageSubtype = [ - 'ephemeralMessage', - 'documentWithCaptionMessage', - 'viewOnceMessage', - 'viewOnceMessageV2', + 'ephemeralMessage', + 'documentWithCaptionMessage', + 'viewOnceMessage', + 'viewOnceMessageV2', ]; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index e783d490..b8f3b1ad 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -1,44 +1,43 @@ -import { delay } from '@whiskeysockets/baileys'; - import { Auth, configService } from '../config/env.config'; -import { eventEmitter } from '../config/event.config'; import { Logger } from '../config/logger.config'; -import { dbserver } from '../db/db.connect'; -import { RedisCache } from '../db/redis.client'; +import { eventEmitter } from '../config/event.config'; +import { MessageRepository } from './repository/message.repository'; +import { WAMonitoringService } from './services/monitor.service'; +import { ChatRepository } from './repository/chat.repository'; +import { ContactRepository } from './repository/contact.repository'; +import { MessageUpRepository } from './repository/messageUp.repository'; 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 { SendMessageController } from './controllers/sendMessage.controller'; -import { SettingsController } from './controllers/settings.controller'; -import { ViewsController } from './controllers/views.controller'; -import { WebhookController } from './controllers/webhook.controller'; -import { - AuthModel, - ChatModel, - ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, - SettingsModel, - WebhookModel, -} from './models'; -import { AuthRepository } from './repository/auth.repository'; -import { ChatRepository } from './repository/chat.repository'; -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 { RepositoryBroker } from './repository/repository.manager'; -import { SettingsRepository } from './repository/settings.repository'; -import { WebhookRepository } from './repository/webhook.repository'; import { AuthService } from './services/auth.service'; -import { ChatwootService } from './services/chatwoot.service'; -import { WAMonitoringService } from './services/monitor.service'; -import { SettingsService } from './services/settings.service'; +import { GroupController } from './controllers/group.controller'; +import { ViewsController } from './controllers/views.controller'; import { WebhookService } from './services/webhook.service'; +import { WebhookController } from './controllers/webhook.controller'; +import { ChatwootService } from './services/chatwoot.service'; +import { ChatwootController } from './controllers/chatwoot.controller'; +import { RepositoryBroker } from './repository/repository.manager'; +import { + AuthModel, + ChatModel, + ContactModel, + MessageModel, + MessageUpModel, + ChatwootModel, + WebhookModel, + SettingsModel, +} from './models'; +import { dbserver } from '../db/db.connect'; +import { WebhookRepository } from './repository/webhook.repository'; +import { ChatwootRepository } from './repository/chatwoot.repository'; +import { AuthRepository } from './repository/auth.repository'; import { WAStartupService } from './services/whatsapp.service'; +import { delay } from '@whiskeysockets/baileys'; import { Events } from './types/wa.types'; +import { RedisCache } from '../db/redis.client'; +import { SettingsRepository } from './repository/settings.repository'; +import { SettingsService } from './services/settings.service'; +import { SettingsController } from './controllers/settings.controller'; const logger = new Logger('WA MODULE'); @@ -52,21 +51,26 @@ const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( - messageRepository, - chatRepository, - contactRepository, - messageUpdateRepository, - webhookRepository, - chatwootRepository, - settingsRepository, - authRepository, - configService, - dbserver?.getClient(), + messageRepository, + chatRepository, + contactRepository, + messageUpdateRepository, + webhookRepository, + chatwootRepository, + settingsRepository, + authRepository, + configService, + dbserver?.getClient(), ); export const cache = new RedisCache(); -export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache); +export const waMonitor = new WAMonitoringService( + eventEmitter, + configService, + repository, + cache, +); const authService = new AuthService(configService, waMonitor, repository); @@ -83,14 +87,15 @@ const settingsService = new SettingsService(waMonitor); export const settingsController = new SettingsController(settingsService); export const instanceController = new InstanceController( - waMonitor, - configService, - repository, - eventEmitter, - authService, - webhookService, - chatwootService, - cache, + waMonitor, + configService, + repository, + eventEmitter, + authService, + webhookService, + chatwootService, + settingsService, + cache, ); export const viewsController = new ViewsController(waMonitor, configService); export const sendMessageController = new SendMessageController(waMonitor); From ddc75d710fc0a96d907e0511d9c82f12e74dbd0b Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 11:08:14 -0300 Subject: [PATCH 72/97] wip --- package.json | 1 + src/config/env.config.ts | 416 +- src/validate/validate.schema.ts | 1447 ++--- .../controllers/chatwoot.controller.ts | 164 +- .../controllers/instance.controller.ts | 646 +- .../controllers/settings.controller.ts | 27 +- src/whatsapp/dto/chat.dto.ts | 89 +- src/whatsapp/dto/chatwoot.dto.ts | 18 +- src/whatsapp/dto/group.dto.ts | 40 +- src/whatsapp/dto/instance.dto.ts | 38 +- src/whatsapp/dto/settings.dto.ts | 12 +- src/whatsapp/models/chatwoot.model.ts | 43 +- src/whatsapp/models/settings.model.ts | 35 +- src/whatsapp/repository/repository.manager.ts | 228 +- src/whatsapp/routers/chatwoot.router.ts | 2 +- src/whatsapp/routers/settings.router.ts | 2 +- src/whatsapp/services/chatwoot.service.ts | 3097 +++++---- src/whatsapp/services/monitor.service.ts | 1 - src/whatsapp/services/whatsapp.service.ts | 5709 ++++++++--------- src/whatsapp/types/wa.types.ts | 157 +- src/whatsapp/whatsapp.module.ts | 112 +- 21 files changed, 5975 insertions(+), 6309 deletions(-) diff --git a/package.json b/package.json index 0e0407a1..ac392703 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "@types/express": "^4.17.17", "@types/js-yaml": "^4.0.5", "@types/jsonwebtoken": "^8.5.9", + "@types/mime-types": "^2.1.1", "@types/node": "^18.15.11", "@types/qrcode": "^1.5.0", "@types/qrcode-terminal": "^0.12.0", diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 78c90ec9..4e5871e1 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -1,114 +1,106 @@ +import { isBooleanString } from 'class-validator'; import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; -import { isBooleanString } from 'class-validator'; export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type Cors = { - ORIGIN: string[]; - METHODS: HttpMethods[]; - CREDENTIALS: boolean; + ORIGIN: string[]; + METHODS: HttpMethods[]; + CREDENTIALS: boolean; }; export type LogBaileys = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; -export type LogLevel = - | 'ERROR' - | 'WARN' - | 'DEBUG' - | 'INFO' - | 'LOG' - | 'VERBOSE' - | 'DARK' - | 'WEBHOOKS'; +export type LogLevel = 'ERROR' | 'WARN' | 'DEBUG' | 'INFO' | 'LOG' | 'VERBOSE' | 'DARK' | 'WEBHOOKS'; export type Log = { - LEVEL: LogLevel[]; - COLOR: boolean; - BAILEYS: LogBaileys; + LEVEL: LogLevel[]; + COLOR: boolean; + BAILEYS: LogBaileys; }; export type SaveData = { - INSTANCE: boolean; - NEW_MESSAGE: boolean; - MESSAGE_UPDATE: boolean; - CONTACTS: boolean; - CHATS: boolean; + INSTANCE: boolean; + NEW_MESSAGE: boolean; + MESSAGE_UPDATE: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type StoreConf = { - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type CleanStoreConf = { - CLEANING_INTERVAL: number; - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + CLEANING_INTERVAL: number; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type DBConnection = { - URI: string; - DB_PREFIX_NAME: string; + URI: string; + DB_PREFIX_NAME: string; }; export type Database = { - CONNECTION: DBConnection; - ENABLED: boolean; - SAVE_DATA: SaveData; + CONNECTION: DBConnection; + ENABLED: boolean; + SAVE_DATA: SaveData; }; export type Redis = { - ENABLED: boolean; - URI: string; - PREFIX_KEY: string; + ENABLED: boolean; + URI: string; + PREFIX_KEY: string; }; export type EventsWebhook = { - APPLICATION_STARTUP: boolean; - QRCODE_UPDATED: boolean; - MESSAGES_SET: boolean; - MESSAGES_UPSERT: boolean; - MESSAGES_UPDATE: boolean; - MESSAGES_DELETE: boolean; - SEND_MESSAGE: boolean; - CONTACTS_SET: boolean; - CONTACTS_UPDATE: boolean; - CONTACTS_UPSERT: boolean; - PRESENCE_UPDATE: boolean; - CHATS_SET: boolean; - CHATS_UPDATE: boolean; - CHATS_DELETE: boolean; - CHATS_UPSERT: boolean; - CONNECTION_UPDATE: boolean; - GROUPS_UPSERT: boolean; - GROUP_UPDATE: boolean; - GROUP_PARTICIPANTS_UPDATE: boolean; - CALL: boolean; - NEW_JWT_TOKEN: boolean; + APPLICATION_STARTUP: boolean; + QRCODE_UPDATED: boolean; + MESSAGES_SET: boolean; + MESSAGES_UPSERT: boolean; + MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; + SEND_MESSAGE: boolean; + CONTACTS_SET: boolean; + CONTACTS_UPDATE: boolean; + CONTACTS_UPSERT: boolean; + PRESENCE_UPDATE: boolean; + CHATS_SET: boolean; + CHATS_UPDATE: boolean; + CHATS_DELETE: boolean; + CHATS_UPSERT: boolean; + CONNECTION_UPDATE: boolean; + GROUPS_UPSERT: boolean; + GROUP_UPDATE: boolean; + GROUP_PARTICIPANTS_UPDATE: boolean; + CALL: boolean; + NEW_JWT_TOKEN: boolean; }; export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; export type Auth = { - API_KEY: ApiKey; - EXPOSE_IN_FETCH_INSTANCES: boolean; - JWT: Jwt; - TYPE: 'jwt' | 'apikey'; + API_KEY: ApiKey; + EXPOSE_IN_FETCH_INSTANCES: boolean; + JWT: Jwt; + TYPE: 'jwt' | 'apikey'; }; export type DelInstance = number | boolean; export type GlobalWebhook = { - URL: string; - ENABLED: boolean; - WEBHOOK_BY_EVENTS: boolean; + URL: string; + ENABLED: boolean; + WEBHOOK_BY_EVENTS: boolean; }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; @@ -117,162 +109,158 @@ export type QrCode = { LIMIT: number }; export type Production = boolean; export interface Env { - SERVER: HttpServer; - CORS: Cors; - SSL_CONF: SslConf; - STORE: StoreConf; - CLEAN_STORE: CleanStoreConf; - DATABASE: Database; - REDIS: Redis; - LOG: Log; - DEL_INSTANCE: DelInstance; - WEBHOOK: Webhook; - CONFIG_SESSION_PHONE: ConfigSessionPhone; - QRCODE: QrCode; - AUTHENTICATION: Auth; - PRODUCTION?: Production; + SERVER: HttpServer; + CORS: Cors; + SSL_CONF: SslConf; + STORE: StoreConf; + CLEAN_STORE: CleanStoreConf; + DATABASE: Database; + REDIS: Redis; + LOG: Log; + DEL_INSTANCE: DelInstance; + WEBHOOK: Webhook; + CONFIG_SESSION_PHONE: ConfigSessionPhone; + QRCODE: QrCode; + AUTHENTICATION: Auth; + PRODUCTION?: Production; } export type Key = keyof Env; export class ConfigService { - constructor() { - this.loadEnv(); - } - - private env: Env; - - public get(key: Key) { - return this.env[key] as T; - } - - private loadEnv() { - this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); - this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; - if (process.env?.DOCKER_ENV === 'true') { - this.env.SERVER.TYPE = 'http'; - this.env.SERVER.PORT = 8080; + constructor() { + this.loadEnv(); } - } - private envYaml(): Env { - return load( - readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' }), - ) as Env; - } + private env: Env; - private envProcess(): Env { - return { - SERVER: { - TYPE: process.env.SERVER_TYPE as 'http' | 'https', - PORT: Number.parseInt(process.env.SERVER_PORT), - URL: process.env.SERVER_URL, - }, - CORS: { - ORIGIN: process.env.CORS_ORIGIN.split(','), - METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], - CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', - }, - SSL_CONF: { - PRIVKEY: process.env?.SSL_CONF_PRIVKEY, - FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, - }, - STORE: { - MESSAGES: process.env?.STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.STORE_CONTACTS === 'true', - CHATS: process.env?.STORE_CHATS === 'true', - }, - CLEAN_STORE: { - CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) - ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) - : 7200, - MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', - CHATS: process.env?.CLEAN_STORE_CHATS === 'true', - }, - DATABASE: { - CONNECTION: { - URI: process.env.DATABASE_CONNECTION_URI, - DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, - }, - ENABLED: process.env?.DATABASE_ENABLED === 'true', - SAVE_DATA: { - INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', - NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', - MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', - CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', - CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', - }, - }, - REDIS: { - ENABLED: process.env?.REDIS_ENABLED === 'true', - URI: process.env.REDIS_URI, - PREFIX_KEY: process.env.REDIS_PREFIX_KEY, - }, - LOG: { - LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], - COLOR: process.env?.LOG_COLOR === 'true', - BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', - }, - DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) - ? process.env.DEL_INSTANCE === 'true' - : Number.parseInt(process.env.DEL_INSTANCE) || false, - WEBHOOK: { - GLOBAL: { - URL: process.env?.WEBHOOK_GLOBAL_URL, - ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', - WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', - }, - EVENTS: { - APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', - QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', - MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', - MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', - MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', - MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', - SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', - CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', - CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', - CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', - PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', - CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', - CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', - CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', - CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', - CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', - GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', - GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', - GROUP_PARTICIPANTS_UPDATE: - process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', - CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', - NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', - }, - }, - CONFIG_SESSION_PHONE: { - CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', - NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', - }, - QRCODE: { - LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, - }, - AUTHENTICATION: { - TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', - API_KEY: { - KEY: process.env.AUTHENTICATION_API_KEY, - }, - EXPOSE_IN_FETCH_INSTANCES: - process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', - JWT: { - EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) - ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) - : 3600, - SECRET: process.env.AUTHENTICATION_JWT_SECRET, - }, - }, - }; - } + public get(key: Key) { + return this.env[key] as T; + } + + private loadEnv() { + this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); + this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; + if (process.env?.DOCKER_ENV === 'true') { + this.env.SERVER.TYPE = 'http'; + this.env.SERVER.PORT = 8080; + } + } + + private envYaml(): Env { + return load(readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' })) as Env; + } + + private envProcess(): Env { + return { + SERVER: { + TYPE: process.env.SERVER_TYPE as 'http' | 'https', + PORT: Number.parseInt(process.env.SERVER_PORT), + URL: process.env.SERVER_URL, + }, + CORS: { + ORIGIN: process.env.CORS_ORIGIN.split(','), + METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], + CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', + }, + SSL_CONF: { + PRIVKEY: process.env?.SSL_CONF_PRIVKEY, + FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, + }, + STORE: { + MESSAGES: process.env?.STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.STORE_CONTACTS === 'true', + CHATS: process.env?.STORE_CHATS === 'true', + }, + CLEAN_STORE: { + CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) + ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) + : 7200, + MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', + CHATS: process.env?.CLEAN_STORE_CHATS === 'true', + }, + DATABASE: { + CONNECTION: { + URI: process.env.DATABASE_CONNECTION_URI, + DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, + }, + ENABLED: process.env?.DATABASE_ENABLED === 'true', + SAVE_DATA: { + INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', + NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', + MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', + CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', + CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', + }, + }, + REDIS: { + ENABLED: process.env?.REDIS_ENABLED === 'true', + URI: process.env.REDIS_URI, + PREFIX_KEY: process.env.REDIS_PREFIX_KEY, + }, + LOG: { + LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], + COLOR: process.env?.LOG_COLOR === 'true', + BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', + }, + DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) + ? process.env.DEL_INSTANCE === 'true' + : Number.parseInt(process.env.DEL_INSTANCE) || false, + WEBHOOK: { + GLOBAL: { + URL: process.env?.WEBHOOK_GLOBAL_URL, + ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', + WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', + }, + EVENTS: { + APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', + QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', + MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', + MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', + MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', + MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', + SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', + CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', + CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', + CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', + PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', + CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', + CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', + CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', + CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', + CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', + GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', + GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', + GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', + NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', + }, + }, + CONFIG_SESSION_PHONE: { + CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', + NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', + }, + QRCODE: { + LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, + }, + AUTHENTICATION: { + TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', + API_KEY: { + KEY: process.env.AUTHENTICATION_API_KEY, + }, + EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', + JWT: { + EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) + ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) + : 3600, + SECRET: process.env.AUTHENTICATION_JWT_SECRET, + }, + }, + }; + } } export const configService = new ConfigService(); diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index d7e2ac1e..8e021116 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -2,923 +2,896 @@ import { JSONSchema7, JSONSchema7Definition } 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 }, - }; + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; }; // Instance Schema export const instanceNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - instanceName: { type: 'string' }, - webhook: { type: 'string' }, - webhook_by_events: { type: 'boolean' }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'CALL', - 'NEW_JWT_TOKEN', - ], - }, + $id: v4(), + type: 'object', + properties: { + instanceName: { type: 'string' }, + webhook: { type: 'string' }, + webhook_by_events: { type: 'boolean' }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, + }, + qrcode: { type: 'boolean', enum: [true, false] }, + number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, + token: { type: 'string' }, }, - qrcode: { type: 'boolean', enum: [true, false] }, - number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, - token: { type: 'string' }, - }, - ...isNotEmpty('instanceName'), + ...isNotEmpty('instanceName'), }; export const oldTokenSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - oldToken: { type: 'string' }, - }, - required: ['oldToken'], - ...isNotEmpty('oldToken'), + $id: v4(), + type: 'object', + properties: { + oldToken: { type: 'string' }, + }, + required: ['oldToken'], + ...isNotEmpty('oldToken'), }; const quotedOptionsSchema: JSONSchema7 = { - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id'], - ...isNotEmpty('id'), + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id'], + ...isNotEmpty('id'), + }, + message: { type: 'object' }, }, - message: { type: 'object' }, - }, }; const mentionsOptionsSchema: JSONSchema7 = { - properties: { - everyOne: { type: 'boolean', enum: [true, false] }, - mentioned: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"mentioned" must be an array of numeric strings', - }, + properties: { + everyOne: { type: 'boolean', enum: [true, false] }, + mentioned: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"mentioned" must be an array of numeric strings', + }, + }, }, - }, }; // Send Message Schema const optionsSchema: JSONSchema7 = { - properties: { - delay: { - type: 'integer', - description: 'Enter a value in milliseconds', + properties: { + delay: { + type: 'integer', + description: 'Enter a value in milliseconds', + }, + presence: { + type: 'string', + enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], + }, + quoted: { ...quotedOptionsSchema }, + mentions: { ...mentionsOptionsSchema }, }, - presence: { - type: 'string', - enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], - }, - quoted: { ...quotedOptionsSchema }, - mentions: { ...mentionsOptionsSchema }, - }, }; const numberDefinition: JSONSchema7Definition = { - type: 'string', - description: 'Invalid format', + type: 'string', + description: 'Invalid format', }; export const textMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - textMessage: { - type: 'object', - properties: { - text: { type: 'string' }, - }, - required: ['text'], - ...isNotEmpty('text'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + textMessage: { + type: 'object', + properties: { + text: { type: 'string' }, + }, + required: ['text'], + ...isNotEmpty('text'), + }, }, - }, - required: ['textMessage', 'number'], + required: ['textMessage', 'number'], }; export const pollMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - pollMessage: { - type: 'object', - properties: { - name: { type: 'string' }, - selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, - values: { - type: 'array', - minItems: 2, - maxItems: 10, - uniqueItems: true, - items: { - type: 'string', - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + pollMessage: { + type: 'object', + properties: { + name: { type: 'string' }, + selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, + values: { + type: 'array', + minItems: 2, + maxItems: 10, + uniqueItems: true, + items: { + type: 'string', + }, + }, + }, + required: ['name', 'selectableCount', 'values'], + ...isNotEmpty('name', 'selectableCount', 'values'), }, - }, - required: ['name', 'selectableCount', 'values'], - ...isNotEmpty('name', 'selectableCount', 'values'), }, - }, - required: ['pollMessage', 'number'], + required: ['pollMessage', 'number'], }; export const statusMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - statusMessage: { - type: 'object', - properties: { - type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, - content: { type: 'string' }, - caption: { type: 'string' }, - backgroundColor: { type: 'string' }, - font: { type: 'integer', minimum: 0, maximum: 5 }, - statusJidList: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"statusJidList" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + statusMessage: { + type: 'object', + properties: { + type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, + content: { type: 'string' }, + caption: { type: 'string' }, + backgroundColor: { type: 'string' }, + font: { type: 'integer', minimum: 0, maximum: 5 }, + statusJidList: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"statusJidList" must be an array of numeric strings', + }, + }, + allContacts: { type: 'boolean', enum: [true, false] }, + }, + required: ['type', 'content'], + ...isNotEmpty('type', 'content'), }, - allContacts: { type: 'boolean', enum: [true, false] }, - }, - required: ['type', 'content'], - ...isNotEmpty('type', 'content'), }, - }, - required: ['statusMessage'], + required: ['statusMessage'], }; export const mediaMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - mediaMessage: { - type: 'object', - properties: { - mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, - media: { type: 'string' }, - fileName: { type: 'string' }, - caption: { type: 'string' }, - }, - required: ['mediatype', 'media'], - ...isNotEmpty('fileName', 'caption', 'media'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + mediaMessage: { + type: 'object', + properties: { + mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, + media: { type: 'string' }, + fileName: { type: 'string' }, + caption: { type: 'string' }, + }, + required: ['mediatype', 'media'], + ...isNotEmpty('fileName', 'caption', 'media'), + }, }, - }, - required: ['mediaMessage', 'number'], + required: ['mediaMessage', 'number'], }; export const stickerMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - stickerMessage: { - type: 'object', - properties: { - image: { type: 'string' }, - }, - required: ['image'], - ...isNotEmpty('image'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + stickerMessage: { + type: 'object', + properties: { + image: { type: 'string' }, + }, + required: ['image'], + ...isNotEmpty('image'), + }, }, - }, - required: ['stickerMessage', 'number'], + required: ['stickerMessage', 'number'], }; export const audioMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - audioMessage: { - type: 'object', - properties: { - audio: { type: 'string' }, - }, - required: ['audio'], - ...isNotEmpty('audio'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + audioMessage: { + type: 'object', + properties: { + audio: { type: 'string' }, + }, + required: ['audio'], + ...isNotEmpty('audio'), + }, }, - }, - required: ['audioMessage', 'number'], + required: ['audioMessage', 'number'], }; export const buttonMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - buttonMessage: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttons: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + buttonMessage: { type: 'object', properties: { - buttonText: { type: 'string' }, - buttonId: { type: 'string' }, + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttons: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + buttonText: { type: 'string' }, + buttonId: { type: 'string' }, + }, + required: ['buttonText', 'buttonId'], + ...isNotEmpty('buttonText', 'buttonId'), + }, + }, + mediaMessage: { + type: 'object', + properties: { + media: { type: 'string' }, + fileName: { type: 'string' }, + mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, + }, + required: ['media', 'mediatype'], + ...isNotEmpty('media', 'fileName'), + }, }, - required: ['buttonText', 'buttonId'], - ...isNotEmpty('buttonText', 'buttonId'), - }, + required: ['title', 'buttons'], + ...isNotEmpty('title', 'description'), }, - mediaMessage: { - type: 'object', - properties: { - media: { type: 'string' }, - fileName: { type: 'string' }, - mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, - }, - required: ['media', 'mediatype'], - ...isNotEmpty('media', 'fileName'), - }, - }, - required: ['title', 'buttons'], - ...isNotEmpty('title', 'description'), }, - }, - required: ['number', 'buttonMessage'], + required: ['number', 'buttonMessage'], }; export const locationMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - locationMessage: { - type: 'object', - properties: { - latitude: { type: 'number' }, - longitude: { type: 'number' }, - name: { type: 'string' }, - address: { type: 'string' }, - }, - required: ['latitude', 'longitude'], - ...isNotEmpty('name', 'addresss'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + locationMessage: { + type: 'object', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + name: { type: 'string' }, + address: { type: 'string' }, + }, + required: ['latitude', 'longitude'], + ...isNotEmpty('name', 'addresss'), + }, }, - }, - required: ['number', 'locationMessage'], + required: ['number', 'locationMessage'], }; export const listMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - listMessage: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttonText: { type: 'string' }, - sections: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + listMessage: { type: 'object', properties: { - title: { type: 'string' }, - rows: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - rowId: { type: 'string' }, - }, - required: ['title', 'description', 'rowId'], - ...isNotEmpty('title', 'description', 'rowId'), + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttonText: { type: 'string' }, + sections: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + rows: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + rowId: { type: 'string' }, + }, + required: ['title', 'description', 'rowId'], + ...isNotEmpty('title', 'description', 'rowId'), + }, + }, + }, + required: ['title', 'rows'], + ...isNotEmpty('title'), + }, }, - }, }, - required: ['title', 'rows'], - ...isNotEmpty('title'), - }, + required: ['title', 'description', 'buttonText', 'sections'], + ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - }, - required: ['title', 'description', 'buttonText', 'sections'], - ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - }, - required: ['number', 'listMessage'], + required: ['number', 'listMessage'], }; export const contactMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - contactMessage: { - type: 'array', - items: { - type: 'object', - properties: { - fullName: { type: 'string' }, - wuid: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"wuid" must be a numeric string', - }, - phoneNumber: { type: 'string', minLength: 10 }, - organization: { type: 'string' }, - email: { type: 'string' }, - url: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + contactMessage: { + type: 'array', + items: { + type: 'object', + properties: { + fullName: { type: 'string' }, + wuid: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"wuid" must be a numeric string', + }, + phoneNumber: { type: 'string', minLength: 10 }, + organization: { type: 'string' }, + email: { type: 'string' }, + url: { type: 'string' }, + }, + required: ['fullName', 'phoneNumber'], + ...isNotEmpty('fullName'), + }, + minItems: 1, + uniqueItems: true, }, - required: ['fullName', 'phoneNumber'], - ...isNotEmpty('fullName'), - }, - minItems: 1, - uniqueItems: true, }, - }, - required: ['number', 'contactMessage'], + required: ['number', 'contactMessage'], }; export const reactionMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reactionMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'remoteJid', 'fromMe'], - ...isNotEmpty('id', 'remoteJid'), + $id: v4(), + type: 'object', + properties: { + reactionMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'remoteJid', 'fromMe'], + ...isNotEmpty('id', 'remoteJid'), + }, + reaction: { type: 'string' }, + }, + required: ['key', 'reaction'], + ...isNotEmpty('reaction'), }, - reaction: { type: 'string' }, - }, - required: ['key', 'reaction'], - ...isNotEmpty('reaction'), }, - }, - required: ['reactionMessage'], + required: ['reactionMessage'], }; // Chat Schema export const whatsappNumberSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - description: '"numbers" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + description: '"numbers" must be an array of numeric strings', + }, + }, }, - }, }; export const readMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - read_messages: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + read_messages: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, }, - }, - required: ['read_messages'], + required: ['read_messages'], }; export const privacySettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - privacySettings: { - type: 'object', - properties: { - readreceipts: { type: 'string', enum: ['all', 'none'] }, - profile: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], + $id: v4(), + type: 'object', + properties: { + privacySettings: { + type: 'object', + properties: { + readreceipts: { type: 'string', enum: ['all', 'none'] }, + profile: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + status: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + online: { type: 'string', enum: ['all', 'match_last_seen'] }, + last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, + groupadd: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + }, + required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], + ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - status: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - online: { type: 'string', enum: ['all', 'match_last_seen'] }, - last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, - groupadd: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - }, - required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], - ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - }, - required: ['privacySettings'], + required: ['privacySettings'], }; export const archiveChatSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - lastMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), + $id: v4(), + type: 'object', + properties: { + lastMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, + messageTimestamp: { type: 'integer', minLength: 1 }, + }, + required: ['key'], + ...isNotEmpty('messageTimestamp'), }, - messageTimestamp: { type: 'integer', minLength: 1 }, - }, - required: ['key'], - ...isNotEmpty('messageTimestamp'), + archive: { type: 'boolean', enum: [true, false] }, }, - archive: { type: 'boolean', enum: [true, false] }, - }, - required: ['lastMessage', 'archive'], + required: ['lastMessage', 'archive'], }; export const deleteMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - participant: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid', 'participant'), + $id: v4(), + type: 'object', + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + participant: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid', 'participant'), }; export const contactValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - pushName: { type: 'string', minLength: 1 }, - id: { type: 'string', minLength: 1 }, - }, - ...isNotEmpty('_id', 'id', 'pushName'), + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + pushName: { type: 'string', minLength: 1 }, + id: { type: 'string', minLength: 1 }, + }, + ...isNotEmpty('_id', 'id', 'pushName'), + }, }, - }, }; export const profileNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - name: { type: 'string' }, - }, - ...isNotEmpty('name'), + $id: v4(), + type: 'object', + properties: { + name: { type: 'string' }, + }, + ...isNotEmpty('name'), }; export const profileStatusSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - status: { type: 'string' }, - }, - ...isNotEmpty('status'), + $id: v4(), + type: 'object', + properties: { + status: { type: 'string' }, + }, + ...isNotEmpty('status'), }; export const profilePictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { type: 'string' }, - picture: { type: 'string' }, - }, + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + picture: { type: 'string' }, + }, }; export const profileSchema: JSONSchema7 = { - type: 'object', - properties: { - wuid: { type: 'string' }, - name: { type: 'string' }, - picture: { type: 'string' }, - status: { type: 'string' }, - isBusiness: { type: 'boolean' }, - }, + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, }; export const messageValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - key: { - type: 'object', - if: { - propertyNames: { - enum: ['fromMe', 'remoteJid', 'id'], - }, - }, - then: { + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', properties: { - remoteJid: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - id: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - fromMe: { type: 'boolean', enum: [true, false] }, + _id: { type: 'string', minLength: 1 }, + key: { + type: 'object', + if: { + propertyNames: { + enum: ['fromMe', 'remoteJid', 'id'], + }, + }, + then: { + properties: { + remoteJid: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + id: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + }, + }, + message: { type: 'object' }, }, - }, + ...isNotEmpty('_id'), }, - message: { type: 'object' }, - }, - ...isNotEmpty('_id'), + limit: { type: 'integer' }, }, - limit: { type: 'integer' }, - }, }; export const messageUpSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string' }, - remoteJid: { type: 'string' }, - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - participant: { type: 'string' }, - status: { - type: 'string', - enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string' }, + remoteJid: { type: 'string' }, + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + participant: { type: 'string' }, + status: { + type: 'string', + enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], + }, + }, + ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), }, - }, - ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), + limit: { type: 'integer' }, }, - limit: { type: 'integer' }, - }, }; // Group Schema export const createGroupSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - subject: { type: 'string' }, - description: { type: 'string' }, - profilePicture: { type: 'string' }, - promoteParticipants: { type: 'boolean', enum: [true, false] }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + subject: { type: 'string' }, + description: { type: 'string' }, + profilePicture: { type: 'string' }, + promoteParticipants: { type: 'boolean', enum: [true, false] }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, }, - }, - required: ['subject', 'participants'], - ...isNotEmpty('subject', 'description', 'profilePicture'), + required: ['subject', 'participants'], + ...isNotEmpty('subject', 'description', 'profilePicture'), }; export const groupJidSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, - }, - required: ['groupJid'], - ...isNotEmpty('groupJid'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, + }, + required: ['groupJid'], + ...isNotEmpty('groupJid'), }; export const getParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - getParticipants: { type: 'string', enum: ['true', 'false'] }, - }, - required: ['getParticipants'], - ...isNotEmpty('getParticipants'), + $id: v4(), + type: 'object', + properties: { + getParticipants: { type: 'string', enum: ['true', 'false'] }, + }, + required: ['getParticipants'], + ...isNotEmpty('getParticipants'), }; export const groupSendInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"numbers" must be an array of numeric strings', - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"numbers" must be an array of numeric strings', + }, + }, }, - }, - required: ['groupJid', 'description', 'numbers'], - ...isNotEmpty('groupJid', 'description', 'numbers'), + required: ['groupJid', 'description', 'numbers'], + ...isNotEmpty('groupJid', 'description', 'numbers'), }; export const groupInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, - }, - required: ['inviteCode'], - ...isNotEmpty('inviteCode'), + $id: v4(), + type: 'object', + properties: { + inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, + }, + required: ['inviteCode'], + ...isNotEmpty('inviteCode'), }; export const updateParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['add', 'remove', 'promote', 'demote'], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['add', 'remove', 'promote', 'demote'], + }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, - }, - required: ['groupJid', 'action', 'participants'], - ...isNotEmpty('groupJid', 'action'), + required: ['groupJid', 'action', 'participants'], + ...isNotEmpty('groupJid', 'action'), }; export const updateSettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], + }, }, - }, - required: ['groupJid', 'action'], - ...isNotEmpty('groupJid', 'action'), + required: ['groupJid', 'action'], + ...isNotEmpty('groupJid', 'action'), }; export const toggleEphemeralSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - expiration: { - type: 'number', - enum: [0, 86400, 604800, 7776000], + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + expiration: { + type: 'number', + enum: [0, 86400, 604800, 7776000], + }, }, - }, - required: ['groupJid', 'expiration'], - ...isNotEmpty('groupJid', 'expiration'), + required: ['groupJid', 'expiration'], + ...isNotEmpty('groupJid', 'expiration'), }; export const updateGroupPictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - image: { type: 'string' }, - }, - required: ['groupJid', 'image'], - ...isNotEmpty('groupJid', 'image'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + image: { type: 'string' }, + }, + required: ['groupJid', 'image'], + ...isNotEmpty('groupJid', 'image'), }; export const updateGroupSubjectSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - subject: { type: 'string' }, - }, - required: ['groupJid', 'subject'], - ...isNotEmpty('groupJid', 'subject'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['groupJid', 'subject'], + ...isNotEmpty('groupJid', 'subject'), }; export const updateGroupDescriptionSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - }, - required: ['groupJid', 'description'], - ...isNotEmpty('groupJid', 'description'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['groupJid', 'description'], + ...isNotEmpty('groupJid', 'description'), }; // Webhook Schema export const webhookSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - url: { type: 'string' }, - enabled: { type: 'boolean', enum: [true, false] }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'CALL', - 'NEW_JWT_TOKEN', - ], - }, + $id: v4(), + type: 'object', + properties: { + url: { type: 'string' }, + enabled: { type: 'boolean', enum: [true, false] }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, + }, }, - }, - required: ['url', 'enabled'], - ...isNotEmpty('url'), + required: ['url', 'enabled'], + ...isNotEmpty('url'), }; export const chatwootSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - enabled: { type: 'boolean', enum: [true, false] }, - account_id: { type: 'string' }, - token: { type: 'string' }, - url: { type: 'string' }, - sign_msg: { type: 'boolean', enum: [true, false] }, - reopen_conversation: { type: 'boolean', enum: [true, false] }, - conversation_pending: { type: 'boolean', enum: [true, false] }, - }, - required: [ - 'enabled', - 'account_id', - 'token', - 'url', - 'sign_msg', - 'reopen_conversation', - 'conversation_pending', - ], - ...isNotEmpty( - 'account_id', - 'token', - 'url', - 'sign_msg', - 'reopen_conversation', - 'conversation_pending', - ), + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + account_id: { type: 'string' }, + token: { type: 'string' }, + url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, + reopen_conversation: { type: 'boolean', enum: [true, false] }, + conversation_pending: { type: 'boolean', enum: [true, false] }, + }, + required: ['enabled', 'account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'], + ...isNotEmpty('account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'), }; export const settingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reject_call: { type: 'boolean', enum: [true, false] }, - msg_call: { type: 'string' }, - groups_ignore: { type: 'boolean', enum: [true, false] }, - always_online: { type: 'boolean', enum: [true, false] }, - read_messages: { type: 'boolean', enum: [true, false] }, - read_status: { type: 'boolean', enum: [true, false] }, - }, - required: [ - 'reject_call', - 'groups_ignore', - 'always_online', - 'read_messages', - 'read_status', - ], - ...isNotEmpty( - 'reject_call', - 'groups_ignore', - 'always_online', - 'read_messages', - 'read_status', - ), + $id: v4(), + type: 'object', + properties: { + reject_call: { type: 'boolean', enum: [true, false] }, + msg_call: { type: 'string' }, + groups_ignore: { type: 'boolean', enum: [true, false] }, + always_online: { type: 'boolean', enum: [true, false] }, + read_messages: { type: 'boolean', enum: [true, false] }, + read_status: { type: 'boolean', enum: [true, false] }, + }, + required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'], + ...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'), }; diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index ad92e607..67591cb3 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -1,105 +1,99 @@ import { isURL } from 'class-validator'; -import { BadRequestException } from '../../exceptions'; -import { InstanceDto } from '../dto/instance.dto'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { ChatwootService } from '../services/chatwoot.service'; -import { Logger } from '../../config/logger.config'; -import { waMonitor } from '../whatsapp.module'; + import { ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { BadRequestException } from '../../exceptions'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootService } from '../services/chatwoot.service'; +import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor( - private readonly chatwootService: ChatwootService, - private readonly configService: ConfigService, - ) {} + constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} - public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - logger.verbose( - 'requested createChatwoot from ' + instance.instanceName + ' instance', - ); + public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { + logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); - if (data.enabled) { - if (!isURL(data.url, { require_tld: false })) { - throw new BadRequestException('url is not valid'); - } + if (data.enabled) { + if (!isURL(data.url, { require_tld: false })) { + throw new BadRequestException('url is not valid'); + } - if (!data.account_id) { - throw new BadRequestException('account_id is required'); - } + if (!data.account_id) { + throw new BadRequestException('account_id is required'); + } - if (!data.token) { - throw new BadRequestException('token is required'); - } + if (!data.token) { + throw new BadRequestException('token is required'); + } - if (data.sign_msg !== true && data.sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } + if (data.sign_msg !== true && data.sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + } + + if (!data.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + data.sign_msg = false; + data.reopen_conversation = false; + data.conversation_pending = false; + } + + data.name_inbox = instance.instanceName; + + const result = this.chatwootService.create(instance, data); + + const urlServer = this.configService.get('SERVER').URL; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } - if (!data.enabled) { - logger.verbose('chatwoot disabled'); - data.account_id = ''; - data.token = ''; - data.url = ''; - data.sign_msg = false; - data.reopen_conversation = false; - data.conversation_pending = false; + public async findChatwoot(instance: InstanceDto) { + logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); + const result = await this.chatwootService.find(instance); + + const urlServer = this.configService.get('SERVER').URL; + + if (Object.keys(result).length === 0) { + return { + enabled: false, + url: '', + account_id: '', + token: '', + sign_msg: false, + name_inbox: '', + webhook_url: '', + }; + } + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } - data.name_inbox = instance.instanceName; + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); + const chatwootService = new ChatwootService(waMonitor, this.configService); - const result = this.chatwootService.create(instance, data); - - const urlServer = this.configService.get('SERVER').URL; - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; - } - - public async findChatwoot(instance: InstanceDto) { - logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - const result = await this.chatwootService.find(instance); - - const urlServer = this.configService.get('SERVER').URL; - - if (Object.keys(result).length === 0) { - return { - enabled: false, - url: '', - account_id: '', - token: '', - sign_msg: false, - name_inbox: '', - webhook_url: '', - }; + return chatwootService.receiveWebhook(instance, data); } - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; + public async newInstance(data: any) { + const chatwootService = new ChatwootService(waMonitor, this.configService); - return response; - } - - public async receiveWebhook(instance: InstanceDto, data: any) { - logger.verbose( - 'requested receiveWebhook from ' + instance.instanceName + ' instance', - ); - const chatwootService = new ChatwootService(waMonitor, this.configService); - - return chatwootService.receiveWebhook(instance, data); - } - - public async newInstance(data: any) { - const chatwootService = new ChatwootService(waMonitor, this.configService); - - return chatwootService.newInstance(data); - } + return chatwootService.newInstance(data); + } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index d9c351f7..f0a89ee8 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -1,376 +1,356 @@ import { delay } from '@whiskeysockets/baileys'; +import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; -import { Auth, ConfigService, HttpServer } from '../../config/env.config'; + +import { ConfigService, HttpServer } from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { RedisCache } from '../../db/redis.client'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; import { AuthService, OldToken } from '../services/auth.service'; -import { WAMonitoringService } from '../services/monitor.service'; -import { WAStartupService } from '../services/whatsapp.service'; -import { WebhookService } from '../services/webhook.service'; import { ChatwootService } from '../services/chatwoot.service'; -import { Logger } from '../../config/logger.config'; -import { wa } from '../types/wa.types'; -import { RedisCache } from '../../db/redis.client'; -import { isURL } from 'class-validator'; +import { WAMonitoringService } from '../services/monitor.service'; import { SettingsService } from '../services/settings.service'; +import { WebhookService } from '../services/webhook.service'; +import { WAStartupService } from '../services/whatsapp.service'; +import { wa } from '../types/wa.types'; export class InstanceController { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly eventEmitter: EventEmitter2, - private readonly authService: AuthService, - private readonly webhookService: WebhookService, - private readonly chatwootService: ChatwootService, - private readonly settingsService: SettingsService, - private readonly cache: RedisCache, - ) {} + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly eventEmitter: EventEmitter2, + private readonly authService: AuthService, + private readonly webhookService: WebhookService, + private readonly chatwootService: ChatwootService, + private readonly settingsService: SettingsService, + private readonly cache: RedisCache, + ) {} - private readonly logger = new Logger(InstanceController.name); + private readonly logger = new Logger(InstanceController.name); - public async createInstance({ - instanceName, - webhook, - webhook_by_events, - events, - qrcode, - number, - token, - chatwoot_account_id, - chatwoot_token, - chatwoot_url, - chatwoot_sign_msg, - chatwoot_reopen_conversation, - chatwoot_conversation_pending, - reject_call, - msg_call, - groups_ignore, - always_online, - read_messages, - read_status, - }: InstanceDto) { - try { - this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); - - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException( - 'The instance name must be lowercase and without special characters', - ); - } - - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); - - this.logger.verbose('creating instance'); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); - - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); - - this.logger.verbose('hash: ' + hash + ' generated'); - - let getEvents: string[]; - - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } - - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - - this.logger.verbose('creating settings'); - const settings: wa.LocalSettings = { - reject_call: reject_call || false, - msg_call: msg_call || '', - groups_ignore: groups_ignore || false, - always_online: always_online || false, - read_messages: read_messages || false, - read_status: read_status || false, - }; - - this.logger.verbose('settings: ' + JSON.stringify(settings)); - - this.settingsService.create(instance, settings); - - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; - - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(number); - await delay(5000); - getQrcode = instance.qrCode; - } - - const result = { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - settings, - qrcode: getQrcode, - }; - - this.logger.verbose('instance created'); - this.logger.verbose(result); - - return result; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } - - if ( - chatwoot_reopen_conversation !== true && - chatwoot_reopen_conversation !== false - ) { - throw new BadRequestException('reopen_conversation is required'); - } - - if ( - chatwoot_conversation_pending !== true && - chatwoot_conversation_pending !== false - ) { - throw new BadRequestException('conversation_pending is required'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - number, - reopen_conversation: chatwoot_reopen_conversation || false, - conversation_pending: chatwoot_conversation_pending || false, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - number, - ); - } catch (error) { - this.logger.log(error); - } - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, + public async createInstance({ + instanceName, webhook, webhook_by_events, - events: getEvents, - settings, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - reopen_conversation: chatwoot_reopen_conversation || false, - conversation_pending: chatwoot_conversation_pending || false, - number, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; - } catch (error) { - console.log(error); - return { error: true, message: error.toString() }; - } - } + events, + qrcode, + number, + token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, + chatwoot_sign_msg, + chatwoot_reopen_conversation, + chatwoot_conversation_pending, + reject_call, + msg_call, + groups_ignore, + always_online, + read_messages, + read_status, + }: InstanceDto) { + try { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); - public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { - try { - this.logger.verbose( - 'requested connectToWhatsapp from ' + instanceName + ' instance', - ); + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException('The instance name must be lowercase and without special characters'); + } - const instance = this.waMonitor.waInstances[instanceName]; - const state = instance?.connectionStatus?.state; + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); - this.logger.verbose('state: ' + state); + this.logger.verbose('creating instance'); + const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); - if (state == 'open') { - return await this.connectionState({ instanceName }); - } + this.logger.verbose('instance: ' + instance.instanceName + ' created'); - if (state == 'connecting') { - return instance.qrCode; - } + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); - if (state == 'close') { - this.logger.verbose('connecting'); - await instance.connectToWhatsapp(number); + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); - await delay(5000); - return instance.qrCode; - } + this.logger.verbose('hash: ' + hash + ' generated'); - return { - instance: { - instanceName: instanceName, - status: state, - }, - qrcode: instance?.qrCode, - }; - } catch (error) { - this.logger.error(error); - } - } + let getEvents: string[]; - public async restartInstance({ instanceName }: InstanceDto) { - try { - this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); + } - this.logger.verbose('logging out instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); - return { error: false, message: 'Instance restarted' }; - } catch (error) { - this.logger.error(error); - } - } + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } - public async connectionState({ instanceName }: InstanceDto) { - this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); - return { - instance: { - instanceName: instanceName, - state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, - }, - }; - } + this.logger.verbose('creating settings'); + const settings: wa.LocalSettings = { + reject_call: reject_call || false, + msg_call: msg_call || '', + groups_ignore: groups_ignore || false, + always_online: always_online || false, + read_messages: read_messages || false, + read_status: read_status || false, + }; - public async fetchInstances({ instanceName }: InstanceDto) { - this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); - if (instanceName) { - this.logger.verbose('instanceName: ' + instanceName); - return this.waMonitor.instanceInfo(instanceName); + this.logger.verbose('settings: ' + JSON.stringify(settings)); + + this.settingsService.create(instance, settings); + + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(number); + await delay(5000); + getQrcode = instance.qrCode; + } + + const result = { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + settings, + qrcode: getQrcode, + }; + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + + if (chatwoot_reopen_conversation !== true && chatwoot_reopen_conversation !== false) { + throw new BadRequestException('reopen_conversation is required'); + } + + if (chatwoot_conversation_pending !== true && chatwoot_conversation_pending !== false) { + throw new BadRequestException('conversation_pending is required'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + number, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + number, + ); + } catch (error) { + this.logger.log(error); + } + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + settings, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + number, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, + }; + } catch (error) { + console.log(error); + return { error: true, message: error.toString() }; + } } - return this.waMonitor.instanceInfo(); - } + public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { + try { + this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance'); - public async logout({ instanceName }: InstanceDto) { - this.logger.verbose('requested logout from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); + const instance = this.waMonitor.waInstances[instanceName]; + const state = instance?.connectionStatus?.state; - if (instance.state === 'close') { - throw new BadRequestException( - 'The "' + instanceName + '" instance is not connected', - ); + this.logger.verbose('state: ' + state); + + if (state == 'open') { + return await this.connectionState({ instanceName }); + } + + if (state == 'connecting') { + return instance.qrCode; + } + + if (state == 'close') { + this.logger.verbose('connecting'); + await instance.connectToWhatsapp(number); + + await delay(5000); + return instance.qrCode; + } + + return { + instance: { + instanceName: instanceName, + status: state, + }, + qrcode: instance?.qrCode, + }; + } catch (error) { + this.logger.error(error); + } } - try { - this.logger.verbose('logging out instance: ' + instanceName); - await this.waMonitor.waInstances[instanceName]?.client?.logout( - 'Log out instance: ' + instanceName, - ); + public async restartInstance({ instanceName }: InstanceDto) { + try { + this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - this.logger.verbose('close connection instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - return { error: false, message: 'Instance logged out' }; - } catch (error) { - throw new InternalServerErrorException(error.toString()); + return { error: false, message: 'Instance restarted' }; + } catch (error) { + this.logger.error(error); + } } - } - public async deleteInstance({ instanceName }: InstanceDto) { - this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); - - if (instance.state === 'open') { - throw new BadRequestException( - 'The "' + instanceName + '" instance needs to be disconnected', - ); + public async connectionState({ instanceName }: InstanceDto) { + this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); + return { + instance: { + instanceName: instanceName, + state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, + }, + }; } - try { - if (instance.state === 'connecting') { - this.logger.verbose('logging out instance: ' + instanceName); - await this.logout({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - return { error: false, message: 'Instance deleted' }; - } else { - this.logger.verbose('deleting instance: ' + instanceName); + public async fetchInstances({ instanceName }: InstanceDto) { + this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); + if (instanceName) { + this.logger.verbose('instanceName: ' + instanceName); + return this.waMonitor.instanceInfo(instanceName); + } - delete this.waMonitor.waInstances[instanceName]; - this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { error: false, message: 'Instance deleted' }; - } - } catch (error) { - throw new BadRequestException(error.toString()); + return this.waMonitor.instanceInfo(); } - } - public async refreshToken(_: InstanceDto, oldToken: OldToken) { - this.logger.verbose('requested refreshToken'); - return await this.authService.refreshToken(oldToken); - } + public async logout({ instanceName }: InstanceDto) { + this.logger.verbose('requested logout from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); + + if (instance.state === 'close') { + throw new BadRequestException('The "' + instanceName + '" instance is not connected'); + } + + try { + this.logger.verbose('logging out instance: ' + instanceName); + await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); + + this.logger.verbose('close connection instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + + return { error: false, message: 'Instance logged out' }; + } catch (error) { + throw new InternalServerErrorException(error.toString()); + } + } + + public async deleteInstance({ instanceName }: InstanceDto) { + this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); + + if (instance.state === 'open') { + throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); + } + try { + if (instance.state === 'connecting') { + this.logger.verbose('logging out instance: ' + instanceName); + + await this.logout({ instanceName }); + delete this.waMonitor.waInstances[instanceName]; + return { error: false, message: 'Instance deleted' }; + } else { + this.logger.verbose('deleting instance: ' + instanceName); + + delete this.waMonitor.waInstances[instanceName]; + this.eventEmitter.emit('remove.instance', instanceName, 'inner'); + return { error: false, message: 'Instance deleted' }; + } + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async refreshToken(_: InstanceDto, oldToken: OldToken) { + this.logger.verbose('requested refreshToken'); + return await this.authService.refreshToken(oldToken); + } } diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index f538abe6..1a82a0b2 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -1,25 +1,24 @@ -import { isURL } from 'class-validator'; -import { BadRequestException } from '../../exceptions'; +// import { isURL } from 'class-validator'; + +import { Logger } from '../../config/logger.config'; +// import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; import { SettingsService } from '../services/settings.service'; -import { Logger } from '../../config/logger.config'; const logger = new Logger('SettingsController'); export class SettingsController { - constructor(private readonly settingsService: SettingsService) {} + constructor(private readonly settingsService: SettingsService) {} - public async createSettings(instance: InstanceDto, data: SettingsDto) { - logger.verbose( - 'requested createSettings from ' + instance.instanceName + ' instance', - ); + public async createSettings(instance: InstanceDto, data: SettingsDto) { + logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.create(instance, data); - } + return this.settingsService.create(instance, data); + } - public async findSettings(instance: InstanceDto) { - logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.find(instance); - } + public async findSettings(instance: InstanceDto) { + logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); + return this.settingsService.find(instance); + } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 4681ef76..487a30d4 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,93 +1,84 @@ -import { - WAPrivacyOnlineValue, - WAPrivacyValue, - WAReadReceiptsValue, - proto, -} from '@whiskeysockets/baileys'; +import { proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { - constructor( - public readonly jid: string, - public readonly exists: boolean, - public readonly name?: string, - ) {} + constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} } export class getBase64FromMediaMessageDto { - message: proto.WebMessageInfo; - convertToMp4?: boolean; + message: proto.WebMessageInfo; + convertToMp4?: boolean; } export class WhatsAppNumberDto { - numbers: string[]; + numbers: string[]; } export class NumberDto { - number: string; + number: string; } export class NumberBusiness { - wid?: string; - jid?: string; - exists?: boolean; - isBusiness: boolean; - name?: string; - message?: string; - description?: string; - email?: string; - website?: string[]; - address?: string; + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; } export class ProfileNameDto { - name: string; + name: string; } export class ProfileStatusDto { - status: string; + status: string; } export class ProfilePictureDto { - number?: string; - // url or base64 - picture?: string; + number?: string; + // url or base64 + picture?: string; } class Key { - id: string; - fromMe: boolean; - remoteJid: string; + id: string; + fromMe: boolean; + remoteJid: string; } export class ReadMessageDto { - read_messages: Key[]; + read_messages: Key[]; } class LastMessage { - key: Key; - messageTimestamp?: number; + key: Key; + messageTimestamp?: number; } export class ArchiveChatDto { - lastMessage: LastMessage; - archive: boolean; + lastMessage: LastMessage; + archive: boolean; } class PrivacySetting { - readreceipts: WAReadReceiptsValue; - profile: WAPrivacyValue; - status: WAPrivacyValue; - online: WAPrivacyOnlineValue; - last: WAPrivacyValue; - groupadd: WAPrivacyValue; + readreceipts: WAReadReceiptsValue; + profile: WAPrivacyValue; + status: WAPrivacyValue; + online: WAPrivacyOnlineValue; + last: WAPrivacyValue; + groupadd: WAPrivacyValue; } export class PrivacySettingDto { - privacySettings: PrivacySetting; + privacySettings: PrivacySetting; } export class DeleteMessage { - id: string; - fromMe: boolean; - remoteJid: string; - participant?: string; + id: string; + fromMe: boolean; + remoteJid: string; + participant?: string; } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index b270c869..212e0090 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -1,11 +1,11 @@ export class ChatwootDto { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index 6dfdc45c..62a1b890 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -1,52 +1,52 @@ export class CreateGroupDto { - subject: string; - participants: string[]; - description?: string; - promoteParticipants?: boolean; + subject: string; + participants: string[]; + description?: string; + promoteParticipants?: boolean; } export class GroupPictureDto { - groupJid: string; - image: string; + groupJid: string; + image: string; } export class GroupSubjectDto { - groupJid: string; - subject: string; + groupJid: string; + subject: string; } export class GroupDescriptionDto { - groupJid: string; - description: string; + groupJid: string; + description: string; } export class GroupJid { - groupJid: string; + groupJid: string; } export class GetParticipant { - getParticipants: string; + getParticipants: string; } export class GroupInvite { - inviteCode: string; + inviteCode: string; } export class GroupSendInvite { - groupJid: string; - description: string; - numbers: string[]; + groupJid: string; + description: string; + numbers: string[]; } export class GroupUpdateParticipantDto extends GroupJid { - action: 'add' | 'remove' | 'promote' | 'demote'; - participants: string[]; + action: 'add' | 'remove' | 'promote' | 'demote'; + participants: string[]; } export class GroupUpdateSettingDto extends GroupJid { - action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; + action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; } export class GroupToggleEphemeralDto extends GroupJid { - expiration: 0 | 86400 | 604800 | 7776000; + expiration: 0 | 86400 | 604800 | 7776000; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index c317060f..827b1243 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -1,21 +1,21 @@ export class InstanceDto { - instanceName: string; - qrcode?: boolean; - number?: string; - token?: string; - webhook?: string; - webhook_by_events?: boolean; - events?: string[]; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; - chatwoot_account_id?: string; - chatwoot_token?: string; - chatwoot_url?: string; - chatwoot_sign_msg?: boolean; - chatwoot_reopen_conversation?: boolean; - chatwoot_conversation_pending?: boolean; + instanceName: string; + qrcode?: boolean; + number?: string; + token?: string; + webhook?: string; + webhook_by_events?: boolean; + events?: string[]; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; + chatwoot_sign_msg?: boolean; + chatwoot_reopen_conversation?: boolean; + chatwoot_conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts index 594ab3a4..9094b888 100644 --- a/src/whatsapp/dto/settings.dto.ts +++ b/src/whatsapp/dto/settings.dto.ts @@ -1,8 +1,8 @@ export class SettingsDto { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index bac226e9..3b1a99a2 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -1,33 +1,30 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { - _id?: string; - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; + _id?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } const chatwootSchema = new Schema({ - _id: { type: String, _id: true }, - enabled: { type: Boolean, required: true }, - account_id: { type: String, required: true }, - token: { type: String, required: true }, - url: { type: String, required: true }, - name_inbox: { type: String, required: true }, - sign_msg: { type: Boolean, required: true }, - number: { type: String, required: true }, + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + account_id: { type: String, required: true }, + token: { type: String, required: true }, + url: { type: String, required: true }, + name_inbox: { type: String, required: true }, + sign_msg: { type: Boolean, required: true }, + number: { type: String, required: true }, }); -export const ChatwootModel = dbserver?.model( - ChatwootRaw.name, - chatwootSchema, - 'chatwoot', -); +export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot'); export type IChatwootModel = typeof ChatwootModel; diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index b6d2488d..935f8f3d 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -1,29 +1,26 @@ import { Schema } from 'mongoose'; + import { dbserver } from '../../db/db.connect'; export class SettingsRaw { - _id?: string; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; + _id?: string; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } const settingsSchema = new Schema({ - _id: { type: String, _id: true }, - reject_call: { type: Boolean, required: true }, - msg_call: { type: String, required: true }, - groups_ignore: { type: Boolean, required: true }, - always_online: { type: Boolean, required: true }, - read_messages: { type: Boolean, required: true }, - read_status: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + reject_call: { type: Boolean, required: true }, + msg_call: { type: String, required: true }, + groups_ignore: { type: Boolean, required: true }, + always_online: { type: Boolean, required: true }, + read_messages: { type: Boolean, required: true }, + read_status: { type: Boolean, required: true }, }); -export const SettingsModel = dbserver?.model( - SettingsRaw.name, - settingsSchema, - 'settings', -); +export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings'); export type ISettingsModel = typeof SettingsModel; diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index d506cc46..46d92418 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -1,121 +1,117 @@ -import { MessageRepository } from './message.repository'; -import { ChatRepository } from './chat.repository'; -import { ContactRepository } from './contact.repository'; -import { MessageUpRepository } from './messageUp.repository'; -import { MongoClient } from 'mongodb'; -import { WebhookRepository } from './webhook.repository'; -import { ChatwootRepository } from './chatwoot.repository'; -import { SettingsRepository } from './settings.repository'; - -import { AuthRepository } from './auth.repository'; -import { Auth, ConfigService, Database } from '../../config/env.config'; -import { join } from 'path'; import fs from 'fs'; +import { MongoClient } from 'mongodb'; +import { join } from 'path'; + +import { Auth, ConfigService, Database } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; +import { AuthRepository } from './auth.repository'; +import { ChatRepository } from './chat.repository'; +import { ChatwootRepository } from './chatwoot.repository'; +import { ContactRepository } from './contact.repository'; +import { MessageRepository } from './message.repository'; +import { MessageUpRepository } from './messageUp.repository'; +import { SettingsRepository } from './settings.repository'; +import { WebhookRepository } from './webhook.repository'; export class RepositoryBroker { - constructor( - public readonly message: MessageRepository, - public readonly chat: ChatRepository, - public readonly contact: ContactRepository, - public readonly messageUpdate: MessageUpRepository, - public readonly webhook: WebhookRepository, - public readonly chatwoot: ChatwootRepository, - public readonly settings: SettingsRepository, - public readonly auth: AuthRepository, - private configService: ConfigService, - dbServer?: MongoClient, - ) { - this.dbClient = dbServer; - this.__init_repo_without_db__(); - } - - private dbClient?: MongoClient; - private readonly logger = new Logger('RepositoryBroker'); - - public get dbServer() { - return this.dbClient; - } - - private __init_repo_without_db__() { - this.logger.verbose('initializing repository without db'); - if (!this.configService.get('DATABASE').ENABLED) { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - try { - const authDir = join( - storePath, - 'auth', - this.configService.get('AUTHENTICATION').TYPE, - ); - const chatsDir = join(storePath, 'chats'); - const contactsDir = join(storePath, 'contacts'); - const messagesDir = join(storePath, 'messages'); - const messageUpDir = join(storePath, 'message-up'); - const webhookDir = join(storePath, 'webhook'); - const chatwootDir = join(storePath, 'chatwoot'); - const settingsDir = join(storePath, 'settings'); - const tempDir = join(storePath, 'temp'); - - if (!fs.existsSync(authDir)) { - this.logger.verbose('creating auth dir: ' + authDir); - fs.mkdirSync(authDir, { recursive: true }); - } - if (!fs.existsSync(chatsDir)) { - this.logger.verbose('creating chats dir: ' + chatsDir); - fs.mkdirSync(chatsDir, { recursive: true }); - } - if (!fs.existsSync(contactsDir)) { - this.logger.verbose('creating contacts dir: ' + contactsDir); - fs.mkdirSync(contactsDir, { recursive: true }); - } - if (!fs.existsSync(messagesDir)) { - this.logger.verbose('creating messages dir: ' + messagesDir); - fs.mkdirSync(messagesDir, { recursive: true }); - } - if (!fs.existsSync(messageUpDir)) { - this.logger.verbose('creating message-up dir: ' + messageUpDir); - fs.mkdirSync(messageUpDir, { recursive: true }); - } - if (!fs.existsSync(webhookDir)) { - this.logger.verbose('creating webhook dir: ' + webhookDir); - fs.mkdirSync(webhookDir, { recursive: true }); - } - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(settingsDir)) { - this.logger.verbose('creating settings dir: ' + settingsDir); - fs.mkdirSync(settingsDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } - } else { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - - const tempDir = join(storePath, 'temp'); - const chatwootDir = join(storePath, 'chatwoot'); - - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - try { - } catch (error) { - this.logger.error(error); - } + constructor( + public readonly message: MessageRepository, + public readonly chat: ChatRepository, + public readonly contact: ContactRepository, + public readonly messageUpdate: MessageUpRepository, + public readonly webhook: WebhookRepository, + public readonly chatwoot: ChatwootRepository, + public readonly settings: SettingsRepository, + public readonly auth: AuthRepository, + private configService: ConfigService, + dbServer?: MongoClient, + ) { + this.dbClient = dbServer; + this.__init_repo_without_db__(); + } + + private dbClient?: MongoClient; + private readonly logger = new Logger('RepositoryBroker'); + + public get dbServer() { + return this.dbClient; + } + + private __init_repo_without_db__() { + this.logger.verbose('initializing repository without db'); + if (!this.configService.get('DATABASE').ENABLED) { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + try { + const authDir = join(storePath, 'auth', this.configService.get('AUTHENTICATION').TYPE); + const chatsDir = join(storePath, 'chats'); + const contactsDir = join(storePath, 'contacts'); + const messagesDir = join(storePath, 'messages'); + const messageUpDir = join(storePath, 'message-up'); + const webhookDir = join(storePath, 'webhook'); + const chatwootDir = join(storePath, 'chatwoot'); + const settingsDir = join(storePath, 'settings'); + const tempDir = join(storePath, 'temp'); + + if (!fs.existsSync(authDir)) { + this.logger.verbose('creating auth dir: ' + authDir); + fs.mkdirSync(authDir, { recursive: true }); + } + if (!fs.existsSync(chatsDir)) { + this.logger.verbose('creating chats dir: ' + chatsDir); + fs.mkdirSync(chatsDir, { recursive: true }); + } + if (!fs.existsSync(contactsDir)) { + this.logger.verbose('creating contacts dir: ' + contactsDir); + fs.mkdirSync(contactsDir, { recursive: true }); + } + if (!fs.existsSync(messagesDir)) { + this.logger.verbose('creating messages dir: ' + messagesDir); + fs.mkdirSync(messagesDir, { recursive: true }); + } + if (!fs.existsSync(messageUpDir)) { + this.logger.verbose('creating message-up dir: ' + messageUpDir); + fs.mkdirSync(messageUpDir, { recursive: true }); + } + if (!fs.existsSync(webhookDir)) { + this.logger.verbose('creating webhook dir: ' + webhookDir); + fs.mkdirSync(webhookDir, { recursive: true }); + } + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(settingsDir)) { + this.logger.verbose('creating settings dir: ' + settingsDir); + fs.mkdirSync(settingsDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } else { + try { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + const chatwootDir = join(storePath, 'chatwoot'); + + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } } - } } diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index 6527099e..4556f4ec 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -5,7 +5,7 @@ import { chatwootSchema, instanceNameSchema } from '../../validate/validate.sche import { RouterBroker } from '../abstract/abstract.router'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; -import { ChatwootService } from '../services/chatwoot.service'; +// import { ChatwootService } from '../services/chatwoot.service'; import { chatwootController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; diff --git a/src/whatsapp/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts index feb37cd0..be364885 100644 --- a/src/whatsapp/routers/settings.router.ts +++ b/src/whatsapp/routers/settings.router.ts @@ -5,7 +5,7 @@ import { instanceNameSchema, settingsSchema } from '../../validate/validate.sche import { RouterBroker } from '../abstract/abstract.router'; import { InstanceDto } from '../dto/instance.dto'; import { SettingsDto } from '../dto/settings.dto'; -import { SettingsService } from '../services/settings.service'; +// import { SettingsService } from '../services/settings.service'; import { settingsController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 2d470c37..a2bd9bff 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,1671 +1,1584 @@ -import { InstanceDto } from '../dto/instance.dto'; -import path from 'path'; -import { ChatwootDto } from '../dto/chatwoot.dto'; -import { WAMonitoringService } from './monitor.service'; -import { Logger } from '../../config/logger.config'; import ChatwootClient from '@figuro/chatwoot-sdk'; -import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import axios from 'axios'; import FormData from 'form-data'; -import { SendTextDto } from '../dto/sendMessage.dto'; +import { createReadStream, readFileSync, unlinkSync, writeFileSync } from 'fs'; import mimeTypes from 'mime-types'; -import { SendAudioDto } from '../dto/sendMessage.dto'; -import { SendMediaDto } from '../dto/sendMessage.dto'; -import { ROOT_DIR } from '../../config/path.config'; +// import { type } from 'os'; +import path from 'path'; + import { ConfigService, HttpServer } from '../../config/env.config'; -import { type } from 'os'; +import { Logger } from '../../config/logger.config'; +import { ROOT_DIR } from '../../config/path.config'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { InstanceDto } from '../dto/instance.dto'; +import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; +import { WAMonitoringService } from './monitor.service'; export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; + private messageCacheFile: string; + private messageCache: Set; - private readonly logger = new Logger(ChatwootService.name); + private readonly logger = new Logger(ChatwootService.name); - private provider: any; + private provider: any; - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - ) { - this.messageCache = new Set(); - } - - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); - } - } - - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); - } - - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); - } - - private async getProvider(instance: InstanceDto) { - this.logger.verbose('get provider to instance: ' + instance.instanceName); - try { - const provider = await this.waMonitor.waInstances[ - instance.instanceName - ].findChatwoot(); - - if (!provider) { - this.logger.warn('provider not found'); - return null; - } - - this.logger.verbose('provider found'); - - return provider; - } catch (error) { - this.logger.error('provider not found'); - return null; - } - } - - private async clientCw(instance: InstanceDto) { - this.logger.verbose('get client to instance: ' + instance.instanceName); - const provider = await this.getProvider(instance); - - if (!provider) { - this.logger.error('provider not found'); - return null; + constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { + this.messageCache = new Set(); } - this.logger.verbose('provider found'); - - this.provider = provider; - - this.logger.verbose('create client to instance: ' + instance.instanceName); - const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, - }); - - this.logger.verbose('client created'); - - return client; - } - - public create(instance: InstanceDto, data: ChatwootDto) { - this.logger.verbose('create chatwoot: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); - - this.logger.verbose('chatwoot created'); - return data; - } - - public async find(instance: InstanceDto): Promise { - this.logger.verbose('find chatwoot: ' + instance.instanceName); - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch (error) { - this.logger.error('chatwoot not found'); - return { enabled: null, url: '' }; - } - } - - public async getContact(instance: InstanceDto, id: number) { - this.logger.verbose('get contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await client.contact.getContactable({ - accountId: this.provider.account_id, - id, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact found'); - return contact; - } - - public async initInstanceChatwoot( - instance: InstanceDto, - inboxName: string, - webhookUrl: string, - qrcode: boolean, - number: string, - ) { - this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inbox in chatwoot'); - const findInbox: any = await client.inboxes.list({ - accountId: this.provider.account_id, - }); - - this.logger.verbose('check duplicate inbox'); - const checkDuplicate = findInbox.payload - .map((inbox) => inbox.name) - .includes(inboxName); - - let inboxId: number; - - if (!checkDuplicate) { - this.logger.verbose('create inbox in chatwoot'); - const data = { - type: 'api', - webhook_url: webhookUrl, - }; - - const inbox = await client.inboxes.create({ - accountId: this.provider.account_id, - data: { - name: inboxName, - channel: data as any, - }, - }); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } else { - this.logger.verbose('find inbox in chatwoot'); - const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } - - this.logger.verbose('find contact in chatwoot and create if not exists'); - const contact = - (await this.findContact(instance, '123456')) || - ((await this.createContact( - instance, - '123456', - inboxId, - false, - 'EvolutionAPI', - )) as any); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact.id || contact.payload.contact.id; - - if (qrcode) { - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }; - - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('create message for init instance in chatwoot'); - - let contentMsg = '/init'; - - if (number) { - contentMsg = `/init:${number}`; - } - - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: contentMsg, - message_type: 'outgoing', - }, - }); - - if (!message) { - this.logger.warn('conversation not found'); - return null; - } - } - - this.logger.verbose('instance chatwoot initialized'); - return true; - } - - public async createContact( - instance: InstanceDto, - phoneNumber: string, - inboxId: number, - isGroup: boolean, - name?: string, - avatar_url?: string, - ) { - this.logger.verbose('create contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let data: any = {}; - if (!isGroup) { - this.logger.verbose('create contact in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - phone_number: `+${phoneNumber}`, - avatar_url: avatar_url, - }; - } else { - this.logger.verbose('create contact group in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: phoneNumber, - avatar_url: avatar_url, - }; - } - - this.logger.verbose('create contact in chatwoot'); - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact created'); - return contact; - } - - public async updateContact(instance: InstanceDto, id: number, data: any) { - this.logger.verbose('update contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('update contact in chatwoot'); - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); - - this.logger.verbose('contact updated'); - return contact; - } - - public async findContact(instance: InstanceDto, phoneNumber: string) { - this.logger.verbose('find contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let query: any; - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('format phone number'); - query = `+${phoneNumber}`; - } else { - this.logger.verbose('format group id'); - query = phoneNumber; - } - - this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); - } else { - this.logger.verbose('return group'); - return contact.payload.find((contact) => contact.identifier === query); - } - } - - public async createConversation(instance: InstanceDto, body: any) { - this.logger.verbose('create conversation to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const isGroup = body.key.remoteJid.includes('@g.us'); - - this.logger.verbose('is group: ' + isGroup); - - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - - this.logger.verbose('chat id: ' + chatId); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - if (isGroup) { - this.logger.verbose('get group name'); - const group = await this.waMonitor.waInstances[ - instance.instanceName - ].client.groupMetadata(chatId); - - nameContact = `${group.subject} (GROUP)`; - - this.logger.verbose('find or create participant in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[ - instance.instanceName - ].profilePicture(body.key.participant.split('@')[0]); - - const findParticipant = await this.findContact( - instance, - body.key.participant.split('@')[0], - ); - - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - ); + private loadMessageCache(): Set { + this.logger.verbose('load message cache'); + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); } - } - - this.logger.verbose('find or create contact in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[ - instance.instanceName - ].profilePicture(chatId); - - const findContact = await this.findContact(instance, chatId); - - let contact: any; - if (body.key.fromMe) { - if (findContact) { - contact = findContact; - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } else { - if (findContact) { - if (!findContact.name || findContact.name === chatId) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - contact = findContact; - } - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = - contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - this.logger.verbose('update contact name in chatwoot'); - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } - - this.logger.verbose('get contact conversations in chatwoot'); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; - - if (contactConversations) { - let conversation: any; - if (this.provider.reopen_conversation) { - conversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); - } else { - conversation = contactConversations.payload.find( - (conversation) => - conversation.status !== 'resolved' && - conversation.inbox_id == filterInbox.id, - ); - } - this.logger.verbose('return conversation if exists'); - - if (conversation) { - this.logger.verbose('conversation found'); - return conversation.id; - } - } - - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: filterInbox.id.toString(), - }; - - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('conversation created'); - return conversation.id; - } catch (error) { - this.logger.error(error); - } - } - - public async getInbox(instance: InstanceDto) { - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; } - this.logger.verbose('find inboxes in chatwoot'); - const inbox = (await client.inboxes.list({ - accountId: this.provider.account_id, - })) as any; - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; + private saveMessageCache() { + this.logger.verbose('save message cache'); + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + this.logger.verbose('message cache saved'); } - this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find( - (inbox) => inbox.name === instance.instanceName, - ); - - if (!findByName) { - this.logger.warn('inbox not found'); - return null; + private clearMessageCache() { + this.logger.verbose('clear message cache'); + this.messageCache.clear(); + this.saveMessageCache(); } - this.logger.verbose('return inbox'); - return findByName; - } - - public async createMessage( - instance: InstanceDto, - conversationId: number, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('message created'); - - return message; - } - - public async createBotMessage( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create bot message to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => - conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: content, - message_type: messageType, - attachments: attachments, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('bot message created'); - - return message; - } - - private async sendData( - conversationId: number, - file: string, - messageType: 'incoming' | 'outgoing' | undefined, - content?: string, - ) { - this.logger.verbose('send data to chatwoot'); - - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - unlinkSync(file); - } - } - - public async createBotQr( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - file?: string, - ) { - this.logger.verbose('create bot qr to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); - - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => - conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); - - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('send data to chatwoot'); - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - if (file) { - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, - }; - - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - } - } - - public async sendAttachment( - waInstance: any, - number: string, - media: any, - caption?: string, - ) { - this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); - - try { - this.logger.verbose('get media type'); - const parts = media.split('/'); - - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); - - const mimeType = mimeTypes.lookup(fileName).toString(); - this.logger.verbose('mime type: ' + mimeType); - - let type = 'document'; - - switch (mimeType.split('/')[0]) { - case 'image': - type = 'image'; - break; - case 'video': - type = 'video'; - break; - case 'audio': - type = 'audio'; - break; - default: - type = 'document'; - break; - } - - this.logger.verbose('type: ' + type); - - if (type === 'audio') { - this.logger.verbose('send audio to instance: ' + waInstance.instanceName); - const data: SendAudioDto = { - number: number, - audioMessage: { - audio: media, - }, - options: { - delay: 1200, - presence: 'recording', - }, - }; - - await waInstance?.audioWhatsapp(data); - - this.logger.verbose('audio sent'); - return; - } - - this.logger.verbose('send media to instance: ' + waInstance.instanceName); - const data: SendMediaDto = { - number: number, - mediaMessage: { - mediatype: type as any, - fileName: fileName, - media: media, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - if (caption) { - this.logger.verbose('caption found'); - data.mediaMessage.caption = caption; - } - - await waInstance?.mediaMessage(data); - - this.logger.verbose('media sent'); - return; - } catch (error) { - this.logger.error(error); - } - } - - public async receiveWebhook(instance: InstanceDto, body: any) { - try { - this.logger.verbose( - 'receive webhook to chatwoot instance: ' + instance.instanceName, - ); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private) return { message: 'bot' }; - - this.logger.verbose('check if is group'); - const chatId = - body.conversation.meta.sender?.phone_number?.replace('+', '') || - body.conversation.meta.sender?.identifier; - const messageReceived = body.content; - const senderName = body?.sender?.name; - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (chatId === '123456' && body.message_type === 'outgoing') { - this.logger.verbose('check if is command'); - - const command = messageReceived.replace('/', ''); - - if (command.includes('init') || command.includes('iniciar')) { - this.logger.verbose('command init found'); - const state = waInstance?.connectionStatus?.state; - - if (state !== 'open') { - this.logger.verbose('connect to whatsapp'); - const number = command.split(':')[1]; - await waInstance.connectToWhatsapp(number); - } else { - this.logger.verbose('whatsapp already connected'); - await this.createBotMessage( - instance, - `🚨 ${body.inbox.name} instance is connected.`, - 'incoming', - ); - } - } - - if (command === 'status') { - this.logger.verbose('command status found'); - - const state = waInstance?.connectionStatus?.state; - - if (!state) { - this.logger.verbose('state not found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance not found.`, - 'incoming', - ); - } - - if (state) { - this.logger.verbose('state: ' + state + ' found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance status: *${state}*`, - 'incoming', - ); - } - } - - if (command === 'disconnect' || command === 'desconectar') { - this.logger.verbose('command disconnect found'); - - const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgLogout, 'incoming'); - - this.logger.verbose('disconnect to whatsapp'); - await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); - await waInstance?.client?.ws?.close(); - } - - if (command.includes('new_instance')) { - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - const data = { - instanceName: command.split(':')[1], - qrcode: true, - chatwoot_account_id: this.provider.account_id, - chatwoot_token: this.provider.token, - chatwoot_url: this.provider.url, - chatwoot_sign_msg: this.provider.sign_msg, - }; - - if (command.split(':')[2]) { - data['number'] = command.split(':')[2]; - } - - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: data, - }; - - await axios.request(config); - } - } - - if ( - body.message_type === 'outgoing' && - body?.conversation?.messages?.length && - chatId !== '123456' - ) { - this.logger.verbose('check if is group'); - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - this.logger.verbose('cache file path: ' + this.messageCacheFile); - - this.messageCache = this.loadMessageCache(); - this.logger.verbose('cache file loaded'); - this.logger.verbose(this.messageCache); - - this.logger.verbose('check if message is cached'); - if (this.messageCache.has(body.id.toString())) { - this.logger.verbose('message is cached'); - return { message: 'bot' }; - } - - this.logger.verbose('clear cache'); - this.clearMessageCache(); - - this.logger.verbose('Format message to send'); - let formatText: string; - if (senderName === null || senderName === undefined) { - formatText = messageReceived; - } else { - formatText = this.provider.sign_msg - ? `*${senderName}:*\n\n${messageReceived}` - : messageReceived; - } - - for (const message of body.conversation.messages) { - this.logger.verbose('check if message is media'); - if (message.attachments && message.attachments.length > 0) { - this.logger.verbose('message is media'); - for (const attachment of message.attachments) { - this.logger.verbose('send media to whatsapp'); - if (!messageReceived) { - this.logger.verbose('message do not have text'); - formatText = null; - } - - await this.sendAttachment( - waInstance, - chatId, - attachment.data_url, - formatText, - ); + private async getProvider(instance: InstanceDto) { + this.logger.verbose('get provider to instance: ' + instance.instanceName); + try { + const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + + if (!provider) { + this.logger.warn('provider not found'); + return null; } - } else { - this.logger.verbose('message is text'); - this.logger.verbose('send text to whatsapp'); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: formatText, - }, - options: { - delay: 1200, - presence: 'composing', - }, + this.logger.verbose('provider found'); + + return provider; + } catch (error) { + this.logger.error('provider not found'); + return null; + } + } + + private async clientCw(instance: InstanceDto) { + this.logger.verbose('get client to instance: ' + instance.instanceName); + const provider = await this.getProvider(instance); + + if (!provider) { + this.logger.error('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + this.provider = provider; + + this.logger.verbose('create client to instance: ' + instance.instanceName); + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + this.logger.verbose('client created'); + + return client; + } + + public create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + this.logger.verbose('chatwoot created'); + return data; + } + + public async find(instance: InstanceDto): Promise { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + try { + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + this.logger.error('chatwoot not found'); + return { enabled: null, url: '' }; + } + } + + public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose('get contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await client.contact.getContactable({ + accountId: this.provider.account_id, + id, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact found'); + return contact; + } + + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + number: string, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); + + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); + + let inboxId: number; + + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, }; - await waInstance?.textMessage(data); - } + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; + } else { + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; } - } - if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is template'); + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact(instance, '123456', inboxId, false, 'EvolutionAPI')) as any); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), - }, - options: { - delay: 1200, - presence: 'composing', - }, + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = '/init'; + + if (number) { + contentMsg = `/init:${number}`; + } + + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: contentMsg, + message_type: 'outgoing', + }, + }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } + } + + this.logger.verbose('instance chatwoot initialized'); + return true; + } + + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + isGroup: boolean, + name?: string, + avatar_url?: string, + ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let data: any = {}; + if (!isGroup) { + this.logger.verbose('create contact in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + avatar_url: avatar_url, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + avatar_url: avatar_url, + }; + } + + this.logger.verbose('create contact in chatwoot'); + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('contact created'); + return contact; + } + + public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose('update contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + if (!id) { + this.logger.warn('id is required'); + return null; + } + + this.logger.verbose('update contact in chatwoot'); + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); + + this.logger.verbose('contact updated'); + return contact; + } + + public async findContact(instance: InstanceDto, phoneNumber: string) { + this.logger.verbose('find contact to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + let query: any; + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('format phone number'); + query = `+${phoneNumber}`; + } else { + this.logger.verbose('format group id'); + query = phoneNumber; + } + + this.logger.verbose('find contact in chatwoot'); + const contact: any = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('return contact'); + return contact.payload.find((contact) => contact.phone_number === query); + } else { + this.logger.verbose('return group'); + return contact.payload.find((contact) => contact.identifier === query); + } + } + + public async createConversation(instance: InstanceDto, body: any) { + this.logger.verbose('create conversation to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const isGroup = body.key.remoteJid.includes('@g.us'); + + this.logger.verbose('is group: ' + isGroup); + + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; + + this.logger.verbose('chat id: ' + chatId); + + let nameContact: string; + + nameContact = !body.key.fromMe ? body.pushName : chatId; + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + if (isGroup) { + this.logger.verbose('get group name'); + const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); + + nameContact = `${group.subject} (GROUP)`; + + this.logger.verbose('find or create participant in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + body.key.participant.split('@')[0], + ); + + const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); + + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } + } else { + await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + ); + } + } + + this.logger.verbose('find or create contact in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); + + const findContact = await this.findContact(instance, chatId); + + let contact: any; + if (body.key.fromMe) { + if (findContact) { + contact = findContact; + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } else { + if (findContact) { + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = findContact; + } + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } + + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + let conversation: any; + if (this.provider.reopen_conversation) { + conversation = contactConversations.payload.find( + (conversation) => conversation.inbox_id == filterInbox.id, + ); + } else { + conversation = contactConversations.payload.find( + (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + } + this.logger.verbose('return conversation if exists'); + + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } + + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find inbox by name'); + const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName); + + if (!findByName) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('return inbox'); + return findByName; + } + + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('message created'); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create bot message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + ) { + this.logger.verbose('send data to chatwoot'); + + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, }; - this.logger.verbose('send text to whatsapp'); + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); - await waInstance?.textMessage(data); - } + this.logger.verbose('remove temp file'); + unlinkSync(file); - return { message: 'bot' }; - } catch (error) { - this.logger.error(error); - - return { message: 'bot' }; + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + unlinkSync(file); + } } - } - private isMediaMessage(message: any) { - this.logger.verbose('check if is media message'); - const media = [ - 'imageMessage', - 'documentMessage', - 'documentWithCaptionMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - ]; + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + this.logger.verbose('create bot qr to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); - const messageKeys = Object.keys(message); + if (!client) { + this.logger.warn('client not found'); + return null; + } - const result = messageKeys.some((key) => media.includes(key)); + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); - this.logger.verbose('is media message: ' + result); - return result; - } + if (!contact) { + this.logger.warn('contact not found'); + return null; + } - private getTypeMessage(msg: any) { - this.logger.verbose('get type message'); + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); - const types = { - conversation: msg.conversation, - imageMessage: msg.imageMessage?.caption, - videoMessage: msg.videoMessage?.caption, - extendedTextMessage: msg.extendedTextMessage?.text, - messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: undefined, - documentMessage: msg.documentMessage?.caption, - documentWithCaptionMessage: - msg.documentWithCaptionMessage?.message?.documentMessage?.caption, - audioMessage: msg.audioMessage?.caption, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: msg.locationMessage, - liveLocationMessage: msg.liveLocationMessage, - }; + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } - this.logger.verbose('type message: ' + types); + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); - return types; - } + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } - private getMessageContent(types: any) { - this.logger.verbose('get message content'); - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); - const result = typeKey ? types[typeKey] : undefined; + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } - if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { - const latitude = result.degreesLatitude; - const longitude = result.degreesLongitude; + this.logger.verbose('send data to chatwoot'); + const data = new FormData(); - const formattedLocation = `**Location:** + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + if (file) { + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + } + } + + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { + this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + + try { + this.logger.verbose('get media type'); + const parts = media.split('/'); + + const fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); + + const mimeType = mimeTypes.lookup(fileName).toString(); + this.logger.verbose('mime type: ' + mimeType); + + let type = 'document'; + + switch (mimeType.split('/')[0]) { + case 'image': + type = 'image'; + break; + case 'video': + type = 'video'; + break; + case 'audio': + type = 'audio'; + break; + default: + type = 'document'; + break; + } + + this.logger.verbose('type: ' + type); + + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, + }; + + await waInstance?.audioWhatsapp(data); + + this.logger.verbose('audio sent'); + return; + } + + this.logger.verbose('send media to instance: ' + waInstance.instanceName); + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption) { + this.logger.verbose('caption found'); + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + this.logger.verbose('media sent'); + return; + } catch (error) { + this.logger.error(error); + } + } + + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('check if is bot'); + if (!body?.conversation || body.private) return { message: 'bot' }; + + this.logger.verbose('check if is group'); + const chatId = + body.conversation.meta.sender?.phone_number?.replace('+', '') || + body.conversation.meta.sender?.identifier; + const messageReceived = body.content; + const senderName = body?.sender?.name; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + this.logger.verbose('check if is command'); + + const command = messageReceived.replace('/', ''); + + if (command.includes('init') || command.includes('iniciar')) { + this.logger.verbose('command init found'); + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + this.logger.verbose('connect to whatsapp'); + const number = command.split(':')[1]; + await waInstance.connectToWhatsapp(number); + } else { + this.logger.verbose('whatsapp already connected'); + await this.createBotMessage( + instance, + `🚨 ${body.inbox.name} instance is connected.`, + 'incoming', + ); + } + } + + if (command === 'status') { + this.logger.verbose('command status found'); + + const state = waInstance?.connectionStatus?.state; + + if (!state) { + this.logger.verbose('state not found'); + await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance not found.`, 'incoming'); + } + + if (state) { + this.logger.verbose('state: ' + state + ' found'); + await this.createBotMessage( + instance, + `⚠️ ${body.inbox.name} instance status: *${state}*`, + 'incoming', + ); + } + } + + if (command === 'disconnect' || command === 'desconectar') { + this.logger.verbose('command disconnect found'); + + const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgLogout, 'incoming'); + + this.logger.verbose('disconnect to whatsapp'); + await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); + await waInstance?.client?.ws?.close(); + } + + if (command.includes('new_instance')) { + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const data = { + instanceName: command.split(':')[1], + qrcode: true, + chatwoot_account_id: this.provider.account_id, + chatwoot_token: this.provider.token, + chatwoot_url: this.provider.url, + chatwoot_sign_msg: this.provider.sign_msg, + }; + + if (command.split(':')[2]) { + data['number'] = command.split(':')[2]; + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: data, + }; + + await axios.request(config); + } + } + + if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { + this.logger.verbose('check if is group'); + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + this.logger.verbose('cache file path: ' + this.messageCacheFile); + + this.messageCache = this.loadMessageCache(); + this.logger.verbose('cache file loaded'); + this.logger.verbose(this.messageCache); + + this.logger.verbose('check if message is cached'); + if (this.messageCache.has(body.id.toString())) { + this.logger.verbose('message is cached'); + return { message: 'bot' }; + } + + this.logger.verbose('clear cache'); + this.clearMessageCache(); + + this.logger.verbose('Format message to send'); + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + formatText = this.provider.sign_msg ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; + } + + for (const message of body.conversation.messages) { + this.logger.verbose('check if message is media'); + if (message.attachments && message.attachments.length > 0) { + this.logger.verbose('message is media'); + for (const attachment of message.attachments) { + this.logger.verbose('send media to whatsapp'); + if (!messageReceived) { + this.logger.verbose('message do not have text'); + formatText = null; + } + + await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); + } + } else { + this.logger.verbose('message is text'); + + this.logger.verbose('send text to whatsapp'); + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + await waInstance?.textMessage(data); + } + } + } + + if (body.message_type === 'template' && body.event === 'message_created') { + this.logger.verbose('check if is template'); + + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + this.logger.verbose('send text to whatsapp'); + + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + + const result = messageKeys.some((key) => media.includes(key)); + + this.logger.verbose('is media message: ' + result); + return result; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: undefined, + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, + }; + + this.logger.verbose('type message: ' + types); + + return types; + } + + private getMessageContent(types: any) { + this.logger.verbose('get message content'); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + const result = typeKey ? types[typeKey] : undefined; + + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { + const latitude = result.degreesLatitude; + const longitude = result.degreesLongitude; + + const formattedLocation = `**Location:** **latitude:** ${latitude} **longitude:** ${longitude} https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} `; - this.logger.verbose('message content: ' + formattedLocation); + this.logger.verbose('message content: ' + formattedLocation); - return formattedLocation; - } - - if (typeKey === 'contactMessage') { - const vCardData = result.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; + return formattedLocation; } - }); - let formattedContact = `**Contact:** + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** **name:** ${contactInfo['FN']}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); + + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; } - }); - this.logger.verbose('message content: ' + formattedContact); - return formattedContact; - } + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; - if (typeKey === 'contactsArrayMessage') { - const formattedContacts = result.contacts.map((contact) => { - const vCardData = contact.vcard.split('\n'); - const contactInfo = {}; + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = `**Contact:** + let formattedContact = `**Contact:** **name:** ${contact.displayName}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); - return formattedContact; - }); + return formattedContact; + }); - const formattedContactsArray = formattedContacts.join('\n\n'); + const formattedContactsArray = formattedContacts.join('\n\n'); - this.logger.verbose('formatted contacts: ' + formattedContactsArray); + this.logger.verbose('formatted contacts: ' + formattedContactsArray); - return formattedContactsArray; - } - - this.logger.verbose('message content: ' + result); - - return result; - } - - private getConversationMessage(msg: any) { - this.logger.verbose('get conversation message'); - - const types = this.getTypeMessage(msg); - - const messageContent = this.getMessageContent(types); - - this.logger.verbose('conversation message: ' + messageContent); - - return messageContent; - } - - public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { - this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (!waInstance) { - this.logger.warn('wa instance not found'); - return null; - } - - if (event === 'messages.upsert') { - this.logger.verbose('event messages.upsert'); - - if (body.key.remoteJid === 'status@broadcast') { - this.logger.verbose('status broadcast found'); - return; + return formattedContactsArray; } + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { this.logger.verbose('get conversation message'); - const bodyMessage = await this.getConversationMessage(body.message); - const isMedia = this.isMediaMessage(body.message); + const types = this.getTypeMessage(msg); - if (!bodyMessage && !isMedia) { - this.logger.warn('no body message found'); - return; - } + const messageContent = this.getMessageContent(types); - this.logger.verbose('get conversation in chatwoot'); - const getConversion = await this.createConversation(instance, body); + this.logger.verbose('conversation message: ' + messageContent); - if (!getConversion) { - this.logger.warn('conversation not found'); - return; - } - - const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; - - this.logger.verbose('message type: ' + messageType); - - this.logger.verbose('is media: ' + isMedia); - - this.logger.verbose('check if is media'); - if (isMedia) { - this.logger.verbose('message is media'); - - this.logger.verbose('get base64 from media message'); - const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ - message: { - ...body, - }, - }); - - const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; - - const fileData = Buffer.from(downloadBase64.base64, 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - - this.logger.verbose('temp file name: ' + nameFile); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData( - getConversion, - fileName, - messageType, - content, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData( - getConversion, - fileName, - messageType, - bodyMessage, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage( - instance, - getConversion, - content, - messageType, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage( - instance, - getConversion, - bodyMessage, - messageType, - ); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - if (event === 'status.instance') { - this.logger.verbose('event status.instance'); - const data = body; - const inbox = await this.getInbox(instance); - - if (!inbox) { - this.logger.warn('inbox not found'); - return; - } - - const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgStatus, 'incoming'); - } - - if (event === 'connection.update') { - this.logger.verbose('event connection.update'); - - if (body.status === 'open') { - const msgConnection = `🚀 Connection successfully established!`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - } - } - - if (event === 'qrcode.updated') { - this.logger.verbose('event qrcode.updated'); - if (body.statusCode === 500) { - this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; - - this.logger.verbose('send message to chatwoot'); - return await this.createBotMessage(instance, erroQRcode, 'incoming'); - } else { - this.logger.verbose('qrcode success'); - const fileData = Buffer.from( - body?.qrcode.base64.replace('data:image/png;base64,', ''), - 'base64', - ); - - const fileName = `${path.join( - waInstance?.storePath, - 'temp', - `${`${instance}.png`}`, - )}`; - - this.logger.verbose('temp file name: ' + fileName); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr( - instance, - 'QRCode successfully generated!', - 'incoming', - fileName, - ); - - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; - - if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( - 0, - 4, - )}-${body.qrcode.pairingCode.substring(4, 8)}`; - } - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgQrCode, 'incoming'); - } - } - } catch (error) { - this.logger.error(error); + return messageContent; } - } - public async newInstance(data: any) { - try { - const instanceName = data.instanceName; - const qrcode = true; - const number = data.number; - const accountId = data.accountId; - const chatwootToken = data.token; - const chatwootUrl = data.url; - const signMsg = true; - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); - const requestData = { - instanceName, - qrcode, - chatwoot_account_id: accountId, - chatwoot_token: chatwootToken, - chatwoot_url: chatwootUrl, - chatwoot_sign_msg: signMsg, - }; + if (!client) { + this.logger.warn('client not found'); + return null; + } - if (number) { - requestData['number'] = number; - } + const waInstance = this.waMonitor.waInstances[instance.instanceName]; - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: requestData, - }; + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } - // await axios.request(config); + if (event === 'messages.upsert') { + this.logger.verbose('event messages.upsert'); - return true; - } catch (error) { - this.logger.error(error); - return null; + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); + + const isMedia = this.isMediaMessage(body.message); + + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; + } + + this.logger.verbose('get conversation in chatwoot'); + const getConversion = await this.createConversation(instance, body); + + if (!getConversion) { + this.logger.warn('conversation not found'); + return; + } + + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + this.logger.verbose('message type: ' + messageType); + + this.logger.verbose('is media: ' + isMedia); + + this.logger.verbose('check if is media'); + if (isMedia) { + this.logger.verbose('message is media'); + + this.logger.verbose('get base64 from media message'); + const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ + message: { + ...body, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + + this.logger.verbose('temp file name: ' + nameFile); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, content); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, bodyMessage); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, content, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, bodyMessage, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + if (event === 'status.instance') { + this.logger.verbose('event status.instance'); + const data = body; + const inbox = await this.getInbox(instance); + + if (!inbox) { + this.logger.warn('inbox not found'); + return; + } + + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); + + if (body.status === 'open') { + const msgConnection = `🚀 Connection successfully established!`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'qrcode.updated') { + this.logger.verbose('event qrcode.updated'); + if (body.statusCode === 500) { + this.logger.verbose('qrcode error'); + const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; + + this.logger.verbose('send message to chatwoot'); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + this.logger.verbose('qrcode success'); + const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; + + this.logger.verbose('temp file name: ' + fileName); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('send qrcode to chatwoot'); + await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); + + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( + 0, + 4, + )}-${body.qrcode.pairingCode.substring(4, 8)}`; + } + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); + } + } + + public async newInstance(data: any) { + try { + const instanceName = data.instanceName; + const qrcode = true; + const number = data.number; + const accountId = data.accountId; + const chatwootToken = data.token; + const chatwootUrl = data.url; + const signMsg = true; + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const requestData = { + instanceName, + qrcode, + chatwoot_account_id: accountId, + chatwoot_token: chatwootToken, + chatwoot_url: chatwootUrl, + chatwoot_sign_msg: signMsg, + }; + + if (number) { + requestData['number'] = number; + } + + // eslint-disable-next-line + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: requestData, + }; + + // await axios.request(config); + + return true; + } catch (error) { + this.logger.error(error); + return null; + } } - } } diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 1108d77c..1504366a 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -2,7 +2,6 @@ import { execSync } from 'child_process'; import EventEmitter2 from 'eventemitter2'; import { opendirSync, readdirSync, rmSync } from 'fs'; import { Db } from 'mongodb'; -import mongoose from 'mongoose'; import { join } from 'path'; import { Auth, ConfigService, Database, DelInstance, HttpServer, Redis } from '../../config/env.config'; diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 4c9bf62d..1678822f 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,3067 +1,2926 @@ +import ffmpegPath from '@ffmpeg-installer/ffmpeg'; +import { Boom } from '@hapi/boom'; import makeWASocket, { - AnyMessageContent, - BufferedEventData, - BufferJSON, - CacheStore, - makeCacheableSignalKeyStore, - Chat, - ConnectionState, - Contact, - delay, - DisconnectReason, - downloadMediaMessage, - fetchLatestBaileysVersion, - generateWAMessageFromContent, - getContentType, - getDevice, - GroupMetadata, - isJidGroup, - isJidUser, - MessageUpsertType, - MiscMessageGenerationOptions, - ParticipantAction, - prepareWAMessageMedia, - proto, - useMultiFileAuthState, - UserFacingSocketConfig, - WABrowserDescription, - WAMediaUpload, - WAMessage, - WAMessageUpdate, - WASocket, - getAggregateVotesInPollMessage, + AnyMessageContent, + BufferedEventData, + BufferJSON, + CacheStore, + Chat, + ConnectionState, + Contact, + delay, + DisconnectReason, + downloadMediaMessage, + fetchLatestBaileysVersion, + generateWAMessageFromContent, + getAggregateVotesInPollMessage, + getContentType, + getDevice, + GroupMetadata, + isJidGroup, + isJidUser, + makeCacheableSignalKeyStore, + MessageUpsertType, + MiscMessageGenerationOptions, + ParticipantAction, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + UserFacingSocketConfig, + WABrowserDescription, + WAMediaUpload, + WAMessage, + WAMessageUpdate, + WASocket, } from '@whiskeysockets/baileys'; -import { - Auth, - CleanStoreConf, - ConfigService, - ConfigSessionPhone, - Database, - HttpServer, - QrCode, - Redis, - Webhook, -} from '../../config/env.config'; -import fs from 'fs'; -import { Logger } from '../../config/logger.config'; -import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; import axios from 'axios'; -import { v4 } from 'uuid'; +import { exec, execSync } from 'child_process'; +import { arrayUnique, isBase64, isURL } from 'class-validator'; +import EventEmitter2 from 'eventemitter2'; +import fs, { existsSync, readFileSync } from 'fs'; +import Long from 'long'; +import NodeCache from 'node-cache'; +import { getMIMEType } from 'node-mime-types'; +import { release } from 'os'; +import { join } from 'path'; +import P from 'pino'; import qrcode, { QRCodeToDataURLOptions } from 'qrcode'; import qrcodeTerminal from 'qrcode-terminal'; -import { Events, TypeMediaMessage, wa, MessageSubtype } from '../types/wa.types'; -import { Boom } from '@hapi/boom'; -import EventEmitter2 from 'eventemitter2'; -import { release } from 'os'; -import P from 'pino'; -import { execSync, exec } from 'child_process'; -import ffmpegPath from '@ffmpeg-installer/ffmpeg'; -import { RepositoryBroker } from '../repository/repository.manager'; -import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; -import { ContactRaw } from '../models/contact.model'; -import { ChatRaw } from '../models/chat.model'; -import { getMIMEType } from 'node-mime-types'; -import { - ContactMessage, - MediaMessage, - Options, - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendReactionDto, - SendTextDto, - SendPollDto, - SendStickerDto, - SendStatusDto, - StatusMessage, -} from '../dto/sendMessage.dto'; -import { arrayUnique, isBase64, isURL } from 'class-validator'; -import { - ArchiveChatDto, - DeleteMessage, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - WhatsAppNumberDto, - getBase64FromMediaMessageDto, -} from '../dto/chat.dto'; -import { MessageQuery } from '../repository/message.repository'; -import { ContactQuery } from '../repository/contact.repository'; -import { - BadRequestException, - InternalServerErrorException, - NotFoundException, -} from '../../exceptions'; -import { - CreateGroupDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, - GroupToggleEphemeralDto, - GroupSubjectDto, - GroupDescriptionDto, - GroupSendInvite, - GetParticipant, -} from '../dto/group.dto'; -import { MessageUpQuery } from '../repository/messageUp.repository'; -import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; -import Long from 'long'; -import { WebhookRaw } from '../models/webhook.model'; -import { ChatwootRaw } from '../models/chatwoot.model'; -import { SettingsRaw } from '../models'; -import { dbserver } from '../../db/db.connect'; -import NodeCache from 'node-cache'; -import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import sharp from 'sharp'; +import { v4 } from 'uuid'; + +import { + Auth, + CleanStoreConf, + ConfigService, + ConfigSessionPhone, + Database, + HttpServer, + Log, + QrCode, + Redis, + Webhook, +} from '../../config/env.config'; +import { Logger } from '../../config/logger.config'; +import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; +import { dbserver } from '../../db/db.connect'; import { RedisCache } from '../../db/redis.client'; -import { Log } from '../../config/env.config'; -import { ChatwootService } from './chatwoot.service'; +import { BadRequestException, InternalServerErrorException, NotFoundException } from '../../exceptions'; +import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; +import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; +import { + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberBusiness, + OnWhatsAppDto, + PrivacySettingDto, + ReadMessageDto, + WhatsAppNumberDto, +} from '../dto/chat.dto'; +import { + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, +} from '../dto/group.dto'; +import { + ContactMessage, + MediaMessage, + Options, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, + StatusMessage, +} from '../dto/sendMessage.dto'; +import { SettingsRaw } from '../models'; +import { ChatRaw } from '../models/chat.model'; +import { ChatwootRaw } from '../models/chatwoot.model'; +import { ContactRaw } from '../models/contact.model'; +import { MessageRaw, MessageUpdateRaw } from '../models/message.model'; +import { WebhookRaw } from '../models/webhook.model'; +import { ContactQuery } from '../repository/contact.repository'; +import { MessageQuery } from '../repository/message.repository'; +import { MessageUpQuery } from '../repository/messageUp.repository'; +import { RepositoryBroker } from '../repository/repository.manager'; +import { Events, MessageSubtype, TypeMediaMessage, wa } from '../types/wa.types'; import { waMonitor } from '../whatsapp.module'; +import { ChatwootService } from './chatwoot.service'; export class WAStartupService { - constructor( - private readonly configService: ConfigService, - private readonly eventEmitter: EventEmitter2, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('WAStartupService initialized'); - this.cleanStore(); - this.instance.qrcode = { count: 0 }; - } - - private readonly logger = new Logger(WAStartupService.name); - private readonly instance: wa.Instance = {}; - public client: WASocket; - private readonly localWebhook: wa.LocalWebHook = {}; - private readonly localChatwoot: wa.LocalChatwoot = {}; - private readonly localSettings: wa.LocalSettings = {}; - private stateConnection: wa.StateConnection = { state: 'close' }; - public readonly storePath = join(ROOT_DIR, 'store'); - private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache(); - private endSession = false; - private logBaileys = this.configService.get('LOG').BAILEYS; - - private phoneNumber: string; - - private chatwootService = new ChatwootService(waMonitor, this.configService); - - public set instanceName(name: string) { - this.logger.verbose(`Initializing instance '${name}'`); - if (!name) { - this.logger.verbose('Instance name not found, generating random name with uuid'); - this.instance.name = v4(); - return; - } - this.instance.name = name; - this.logger.verbose(`Instance '${this.instance.name}' initialized`); - this.logger.verbose('Sending instance status to webhook'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'created', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'created', - }, - ); - } - } - - public get instanceName() { - this.logger.verbose('Getting instance name'); - return this.instance.name; - } - - public get wuid() { - this.logger.verbose('Getting remoteJid of instance'); - return this.instance.wuid; - } - - public async getProfileName() { - this.logger.verbose('Getting profile name'); - let profileName = this.client.user?.name ?? this.client.user?.verifiedName; - if (!profileName) { - this.logger.verbose('Profile name not found, trying to get from database'); - if (this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('Database enabled, trying to get from database'); - const collection = dbserver - .getClient() - .db( - this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + - '-instances', - ) - .collection(this.instanceName); - const data = await collection.findOne({ _id: 'creds' }); - if (data) { - this.logger.verbose('Profile name found in database'); - const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { - this.logger.verbose('Profile name found in file'); - const creds = JSON.parse( - readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { - encoding: 'utf-8', - }), - ); - profileName = creds.me?.name || creds.me?.verifiedName; - } + constructor( + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('WAStartupService initialized'); + this.cleanStore(); + this.instance.qrcode = { count: 0 }; } - this.logger.verbose(`Profile name: ${profileName}`); - return profileName; - } + private readonly logger = new Logger(WAStartupService.name); + private readonly instance: wa.Instance = {}; + public client: WASocket; + private readonly localWebhook: wa.LocalWebHook = {}; + private readonly localChatwoot: wa.LocalChatwoot = {}; + private readonly localSettings: wa.LocalSettings = {}; + private stateConnection: wa.StateConnection = { state: 'close' }; + public readonly storePath = join(ROOT_DIR, 'store'); + private readonly msgRetryCounterCache: CacheStore = new NodeCache(); + private readonly userDevicesCache: CacheStore = new NodeCache(); + private endSession = false; + private logBaileys = this.configService.get('LOG').BAILEYS; - public async getProfileStatus() { - this.logger.verbose('Getting profile status'); - const status = await this.client.fetchStatus(this.instance.wuid); + private phoneNumber: string; - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; - } + private chatwootService = new ChatwootService(waMonitor, this.configService); - public get profilePictureUrl() { - this.logger.verbose('Getting profile picture url'); - return this.instance.profilePictureUrl; - } - - public get qrCode(): wa.QrCode { - this.logger.verbose('Getting qrcode'); - - return { - pairingCode: this.instance.qrcode?.pairingCode, - code: this.instance.qrcode?.code, - base64: this.instance.qrcode?.base64, - }; - } - - private async loadWebhook() { - this.logger.verbose('Loading webhook'); - const data = await this.repository.webhook.find(this.instanceName); - this.localWebhook.url = data?.url; - this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); - - this.localWebhook.enabled = data?.enabled; - this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); - - this.localWebhook.events = data?.events; - this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); - - this.localWebhook.webhook_by_events = data?.webhook_by_events; - this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); - - this.logger.verbose('Webhook loaded'); - } - - public async setWebhook(data: WebhookRaw) { - this.logger.verbose('Setting webhook'); - await this.repository.webhook.create(data, this.instanceName); - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - Object.assign(this.localWebhook, data); - this.logger.verbose('Webhook set'); - } - - public async findWebhook() { - this.logger.verbose('Finding webhook'); - const data = await this.repository.webhook.find(this.instanceName); - - if (!data) { - this.logger.verbose('Webhook not found'); - throw new NotFoundException('Webhook not found'); - } - - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - return data; - } - - private async loadChatwoot() { - this.logger.verbose('Loading chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - this.localChatwoot.enabled = data?.enabled; - this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); - - this.localChatwoot.account_id = data?.account_id; - this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); - - this.localChatwoot.token = data?.token; - this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); - - this.localChatwoot.url = data?.url; - this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); - - this.localChatwoot.name_inbox = data?.name_inbox; - this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); - - this.localChatwoot.sign_msg = data?.sign_msg; - this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); - - this.localChatwoot.number = data?.number; - this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); - - this.localChatwoot.reopen_conversation = data?.reopen_conversation; - this.logger.verbose( - `Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`, - ); - - this.localChatwoot.conversation_pending = data?.conversation_pending; - this.logger.verbose( - `Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`, - ); - - this.logger.verbose('Chatwoot loaded'); - } - - public async setChatwoot(data: ChatwootRaw) { - this.logger.verbose('Setting chatwoot'); - await this.repository.chatwoot.create(data, this.instanceName); - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); - this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); - - Object.assign(this.localChatwoot, data); - this.logger.verbose('Chatwoot set'); - } - - public async findChatwoot() { - this.logger.verbose('Finding chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - - if (!data) { - this.logger.verbose('Chatwoot not found'); - return null; - } - - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); - this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); - - return data; - } - - private async loadSettings() { - this.logger.verbose('Loading settings'); - const data = await this.repository.settings.find(this.instanceName); - this.localSettings.reject_call = data?.reject_call; - this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); - - this.localSettings.msg_call = data?.msg_call; - this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); - - this.localSettings.groups_ignore = data?.groups_ignore; - this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); - - this.localSettings.always_online = data?.always_online; - this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); - - this.localSettings.read_messages = data?.read_messages; - this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); - - this.localSettings.read_status = data?.read_status; - this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); - - this.logger.verbose('Settings loaded'); - } - - public async setSettings(data: SettingsRaw) { - this.logger.verbose('Setting settings'); - await this.repository.settings.create(data, this.instanceName); - this.logger.verbose(`Settings reject_call: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - this.logger.verbose(`Settings always_online: ${data.always_online}`); - this.logger.verbose(`Settings read_messages: ${data.read_messages}`); - this.logger.verbose(`Settings read_status: ${data.read_status}`); - Object.assign(this.localSettings, data); - this.logger.verbose('Settings set'); - - this.client?.ws?.close(); - } - - public async findSettings() { - this.logger.verbose('Finding settings'); - const data = await this.repository.settings.find(this.instanceName); - - if (!data) { - this.logger.verbose('Settings not found'); - return null; - } - - this.logger.verbose(`Settings url: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - this.logger.verbose(`Settings always_online: ${data.always_online}`); - this.logger.verbose(`Settings read_messages: ${data.read_messages}`); - this.logger.verbose(`Settings read_status: ${data.read_status}`); - return data; - } - - public async sendDataWebhook(event: Events, data: T, local = true) { - const webhookGlobal = this.configService.get('WEBHOOK'); - const webhookLocal = this.localWebhook.events; - const serverUrl = this.configService.get('SERVER').URL; - const we = event.replace(/[\.-]/gm, '_').toUpperCase(); - const transformedWe = we.replace(/_/gm, '-').toLowerCase(); - - const expose = - this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; - const tokenStore = await this.repository.auth.find(this.instanceName); - const instanceApikey = tokenStore?.apikey || 'Apikey not found'; - - const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - if (local) { - if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { - this.logger.verbose('Sending data to webhook local'); - let baseURL; - - if (this.localWebhook.webhook_by_events) { - baseURL = `${this.localWebhook.url}/${transformedWe}`; - } else { - baseURL = this.localWebhook.url; + public set instanceName(name: string) { + this.logger.verbose(`Initializing instance '${name}'`); + if (!name) { + this.logger.verbose('Instance name not found, generating random name with uuid'); + this.instance.name = v4(); + return; } - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-local', - url: baseURL, - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - apikey: (expose && instanceApikey) || null, - }; - - if (expose && instanceApikey) { - logData['apikey'] = instanceApikey; - } - - this.logger.log(logData); - } - - try { - if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { - const httpService = axios.create({ baseURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - }; - - if (expose && instanceApikey) { - postData['apikey'] = instanceApikey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-local', - 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]) { - this.logger.verbose('Sending data to webhook global'); - const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; - - let globalURL; - - if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { - globalURL = `${globalWebhook.URL}/${transformedWe}`; - } else { - globalURL = globalWebhook.URL; - } - - const localUrl = this.localWebhook.url; - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-global', - url: globalURL, - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - logData['apikey'] = globalApiKey; - } - - this.logger.log(logData); - } - - try { - if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { - const httpService = axios.create({ baseURL: globalURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - postData['apikey'] = globalApiKey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-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, - }); - } - } - } - } - - private async connectionUpdate({ - qr, - connection, - lastDisconnect, - }: Partial) { - this.logger.verbose('Connection update'); - if (qr) { - this.logger.verbose('QR code found'); - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { - this.logger.verbose('QR code limit reached'); - - this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); - this.sendDataWebhook(Events.QRCODE_UPDATED, { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }, - ); - } - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - state: 'refused', - statusReason: DisconnectReason.connectionClosed, - }); - - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.instance.name = name; + this.logger.verbose(`Instance '${this.instance.name}' initialized`); + this.logger.verbose('Sending instance status to webhook'); this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('endSession defined as true'); - this.endSession = true; - - this.logger.verbose('Emmiting event logout.instance'); - return this.eventEmitter.emit('no.connection', this.instance.name); - } - - this.logger.verbose('Incrementing QR code count'); - this.instance.qrcode.count++; - - const optsQrcode: QRCodeToDataURLOptions = { - margin: 3, - scale: 4, - errorCorrectionLevel: 'H', - color: { light: '#ffffff', dark: '#198754' }, - }; - - if (this.phoneNumber) { - await delay(2000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode( - this.phoneNumber, - ); - } else { - this.instance.qrcode.pairingCode = null; - } - - this.logger.verbose('Generating QR code'); - qrcode.toDataURL(qr, optsQrcode, (error, base64) => { - if (error) { - this.logger.error('Qrcode generate failed:' + error.toString()); - return; - } - - this.instance.qrcode.base64 = base64; - this.instance.qrcode.code = qr; - - this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, + status: 'created', }); if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }, - ); + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'created', + }, + ); } - }); - - this.logger.verbose('Generating QR code in terminal'); - qrcodeTerminal.generate(qr, { small: true }, (qrcode) => - this.logger.log( - `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), - ); } - if (connection) { - this.logger.verbose('Connection found'); - this.stateConnection = { - state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, - }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - ...this.stateConnection, - }); + public get instanceName() { + this.logger.verbose('Getting instance name'); + return this.instance.name; } - if (connection === 'close') { - this.logger.verbose('Connection closed'); - const shouldReconnect = - (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - this.logger.verbose('Reconnecting to whatsapp'); - await this.connectToWhatsapp(); - } else { - this.logger.verbose('Do not reconnect to whatsapp'); - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); + public get wuid() { + this.logger.verbose('Getting remoteJid of instance'); + return this.instance.wuid; + } - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); + public async getProfileName() { + this.logger.verbose('Getting profile name'); + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; + if (!profileName) { + this.logger.verbose('Profile name not found, trying to get from database'); + if (this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('Database enabled, trying to get from database'); + const collection = dbserver + .getClient() + .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(this.instanceName); + const data = await collection.findOne({ _id: 'creds' }); + if (data) { + this.logger.verbose('Profile name found in database'); + const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { + this.logger.verbose('Profile name found in file'); + const creds = JSON.parse( + readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { + encoding: 'utf-8', + }), + ); + profileName = creds.me?.name || creds.me?.verifiedName; + } } - this.logger.verbose('Emittin event logout.instance'); - this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.logger.verbose(`Profile name: ${profileName}`); + return profileName; + } + + public async getProfileStatus() { + this.logger.verbose('Getting profile status'); + const status = await this.client.fetchStatus(this.instance.wuid); + + this.logger.verbose(`Profile status: ${status.status}`); + return status.status; + } + + public get profilePictureUrl() { + this.logger.verbose('Getting profile picture url'); + return this.instance.profilePictureUrl; + } + + public get qrCode(): wa.QrCode { + this.logger.verbose('Getting qrcode'); + + return { + pairingCode: this.instance.qrcode?.pairingCode, + code: this.instance.qrcode?.code, + base64: this.instance.qrcode?.base64, + }; + } + + private async loadWebhook() { + this.logger.verbose('Loading webhook'); + const data = await this.repository.webhook.find(this.instanceName); + this.localWebhook.url = data?.url; + this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); + + this.localWebhook.enabled = data?.enabled; + this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); + + this.localWebhook.events = data?.events; + this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); + + this.localWebhook.webhook_by_events = data?.webhook_by_events; + this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); + + this.logger.verbose('Webhook loaded'); + } + + public async setWebhook(data: WebhookRaw) { + this.logger.verbose('Setting webhook'); + await this.repository.webhook.create(data, this.instanceName); + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + Object.assign(this.localWebhook, data); + this.logger.verbose('Webhook set'); + } + + public async findWebhook() { + this.logger.verbose('Finding webhook'); + const data = await this.repository.webhook.find(this.instanceName); + + if (!data) { + this.logger.verbose('Webhook not found'); + throw new NotFoundException('Webhook not found'); + } + + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + return data; + } + + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.localChatwoot.name_inbox = data?.name_inbox; + this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); + + this.localChatwoot.sign_msg = data?.sign_msg; + this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + + this.localChatwoot.number = data?.number; + this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); + + this.localChatwoot.reopen_conversation = data?.reopen_conversation; + this.logger.verbose(`Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`); + + this.localChatwoot.conversation_pending = data?.conversation_pending; + this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); + + this.logger.verbose('Chatwoot loaded'); + } + + public async setChatwoot(data: ChatwootRaw) { + this.logger.verbose('Setting chatwoot'); + await this.repository.chatwoot.create(data, this.instanceName); + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + Object.assign(this.localChatwoot, data); + this.logger.verbose('Chatwoot set'); + } + + public async findChatwoot() { + this.logger.verbose('Finding chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + + if (!data) { + this.logger.verbose('Chatwoot not found'); + return null; + } + + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + return data; + } + + private async loadSettings() { + this.logger.verbose('Loading settings'); + const data = await this.repository.settings.find(this.instanceName); + this.localSettings.reject_call = data?.reject_call; + this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); + + this.localSettings.msg_call = data?.msg_call; + this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); + + this.localSettings.groups_ignore = data?.groups_ignore; + this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + + this.localSettings.always_online = data?.always_online; + this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); + + this.localSettings.read_messages = data?.read_messages; + this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); + + this.localSettings.read_status = data?.read_status; + this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); + + this.logger.verbose('Settings loaded'); + } + + public async setSettings(data: SettingsRaw) { + this.logger.verbose('Setting settings'); + await this.repository.settings.create(data, this.instanceName); + this.logger.verbose(`Settings reject_call: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + Object.assign(this.localSettings, data); + this.logger.verbose('Settings set'); + this.client?.ws?.close(); - this.client.end(new Error('Close connection')); - this.logger.verbose('Connection closed'); - } } - if (connection === 'open') { - this.logger.verbose('Connection opened'); - this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); - this.instance.profilePictureUrl = ( - await this.profilePicture(this.instance.wuid) - ).profilePictureUrl; - this.logger.info( - ` + public async findSettings() { + this.logger.verbose('Finding settings'); + const data = await this.repository.settings.find(this.instanceName); + + if (!data) { + this.logger.verbose('Settings not found'); + return null; + } + + this.logger.verbose(`Settings url: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + return data; + } + + public async sendDataWebhook(event: Events, data: T, local = true) { + const webhookGlobal = this.configService.get('WEBHOOK'); + const webhookLocal = this.localWebhook.events; + const serverUrl = this.configService.get('SERVER').URL; + const we = event.replace(/[.-]/gm, '_').toUpperCase(); + const transformedWe = we.replace(/_/gm, '-').toLowerCase(); + + const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; + const tokenStore = await this.repository.auth.find(this.instanceName); + const instanceApikey = tokenStore?.apikey || 'Apikey not found'; + + const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + if (local) { + if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { + this.logger.verbose('Sending data to webhook local'); + let baseURL: string; + + if (this.localWebhook.webhook_by_events) { + baseURL = `${this.localWebhook.url}/${transformedWe}`; + } else { + baseURL = this.localWebhook.url; + } + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-local', + url: baseURL, + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + + try { + if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { + const httpService = axios.create({ baseURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + }; + + if (expose && instanceApikey) { + postData['apikey'] = instanceApikey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-local', + 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]) { + this.logger.verbose('Sending data to webhook global'); + const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; + + let globalURL; + + if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { + globalURL = `${globalWebhook.URL}/${transformedWe}`; + } else { + globalURL = globalWebhook.URL; + } + + const localUrl = this.localWebhook.url; + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-global', + url: globalURL, + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + logData['apikey'] = globalApiKey; + } + + this.logger.log(logData); + } + + try { + if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { + const httpService = axios.create({ baseURL: globalURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + postData['apikey'] = globalApiKey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-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, + }); + } + } + } + } + + private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + this.logger.verbose('Connection update'); + if (qr) { + this.logger.verbose('QR code found'); + if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + this.logger.verbose('QR code limit reached'); + + this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); + } + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + state: 'refused', + statusReason: DisconnectReason.connectionClosed, + }); + + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + + this.logger.verbose('endSession defined as true'); + this.endSession = true; + + this.logger.verbose('Emmiting event logout.instance'); + return this.eventEmitter.emit('no.connection', this.instance.name); + } + + this.logger.verbose('Incrementing QR code count'); + this.instance.qrcode.count++; + + const optsQrcode: QRCodeToDataURLOptions = { + margin: 3, + scale: 4, + errorCorrectionLevel: 'H', + color: { light: '#ffffff', dark: '#198754' }, + }; + + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); + } else { + this.instance.qrcode.pairingCode = null; + } + + this.logger.verbose('Generating QR code'); + qrcode.toDataURL(qr, optsQrcode, (error, base64) => { + if (error) { + this.logger.error('Qrcode generate failed:' + error.toString()); + return; + } + + this.instance.qrcode.base64 = base64; + this.instance.qrcode.code = qr; + + this.sendDataWebhook(Events.QRCODE_UPDATED, { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }, + ); + } + }); + + this.logger.verbose('Generating QR code in terminal'); + qrcodeTerminal.generate(qr, { small: true }, (qrcode) => + this.logger.log( + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + qrcode, + ), + ); + } + + if (connection) { + this.logger.verbose('Connection found'); + this.stateConnection = { + state: connection, + statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + }; + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); + } + + if (connection === 'close') { + this.logger.verbose('Connection closed'); + const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + this.logger.verbose('Reconnecting to whatsapp'); + await this.connectToWhatsapp(); + } else { + this.logger.verbose('Do not reconnect to whatsapp'); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + + this.logger.verbose('Emittin event logout.instance'); + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.client?.ws?.close(); + this.client.end(new Error('Close connection')); + this.logger.verbose('Connection closed'); + } + } + + if (connection === 'open') { + this.logger.verbose('Connection opened'); + this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); + this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; + this.logger.info( + ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ └──────────────────────────────┘`.replace(/^ +/gm, ' '), - ); + ); - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.CONNECTION_UPDATE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'open', - }, - ); - } - } - } - - private async getMessage(key: proto.IMessageKey, full = false) { - this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); - try { - const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.name, key: { id: key.id } }, - })) as unknown as proto.IWebMessageInfo[]; - if (full) { - this.logger.verbose('Returning full message'); - return webMessageInfo[0]; - } - if (webMessageInfo[0].message?.pollCreationMessage) { - this.logger.verbose('Returning poll message'); - const messageSecretBase64 = - webMessageInfo[0].message?.messageContextInfo?.messageSecret; - - if (typeof messageSecretBase64 === 'string') { - const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - - const msg = { - messageContextInfo: { - messageSecret, - }, - pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, - }; - - return msg; + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } } - } - - this.logger.verbose('Returning message'); - return webMessageInfo[0].message; - } catch (error) { - return { conversation: '' }; } - } - private cleanStore() { - this.logger.verbose('Cronjob to clean store initialized'); - const cleanStore = this.configService.get('CLEAN_STORE'); - const database = this.configService.get('DATABASE'); - if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { - this.logger.verbose('Cronjob to clean store enabled'); - setInterval(() => { + private async getMessage(key: proto.IMessageKey, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); try { - for (const [key, value] of Object.entries(cleanStore)) { - if (value === true) { - execSync( - `rm -rf ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - this.logger.verbose( - `Cleaned ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as proto.IWebMessageInfo[]; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; } - } - } catch (error) {} - }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); - } - } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; - private async defineAuthState() { - this.logger.verbose('Defining auth state'); - const db = this.configService.get('DATABASE'); - const redis = this.configService.get('REDIS'); + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - if (redis?.ENABLED) { - this.logger.verbose('Redis enabled'); - this.cache.reference = this.instance.name; - return await useMultiFileAuthStateRedisDb(this.cache); + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } } - if (db.SAVE_DATA.INSTANCE && db.ENABLED) { - this.logger.verbose('Database enabled'); - return await useMultiFileAuthStateDb(this.instance.name); + private cleanStore() { + this.logger.verbose('Cronjob to clean store initialized'); + const cleanStore = this.configService.get('CLEAN_STORE'); + const database = this.configService.get('DATABASE'); + if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { + this.logger.verbose('Cronjob to clean store enabled'); + setInterval(() => { + try { + for (const [key, value] of Object.entries(cleanStore)) { + if (value === true) { + execSync( + `rm -rf ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); + this.logger.verbose( + `Cleaned ${join( + this.storePath, + key.toLowerCase().replace('_', '-'), + this.instance.name, + )}/*.json`, + ); + } + } + } catch (error) { + this.logger.error(error); + } + }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); + } } - this.logger.verbose('Store file enabled'); - return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); - } + private async defineAuthState() { + this.logger.verbose('Defining auth state'); + const db = this.configService.get('DATABASE'); + const redis = this.configService.get('REDIS'); - public async connectToWhatsapp(number?: string): Promise { - this.logger.verbose('Connecting to whatsapp'); - try { - this.loadWebhook(); - this.loadChatwoot(); - this.loadSettings(); + if (redis?.ENABLED) { + this.logger.verbose('Redis enabled'); + this.cache.reference = this.instance.name; + return await useMultiFileAuthStateRedisDb(this.cache); + } - this.instance.authState = await this.defineAuthState(); + if (db.SAVE_DATA.INSTANCE && db.ENABLED) { + this.logger.verbose('Database enabled'); + return await useMultiFileAuthStateDb(this.instance.name); + } - const { version } = await fetchLatestBaileysVersion(); - this.logger.verbose('Baileys version: ' + version); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); + this.logger.verbose('Store file enabled'); + return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + } - const socketConfig: UserFacingSocketConfig = { - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore( - this.instance.authState.state.keys, - P({ level: 'error' }), - ), - }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => - (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: true, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!( - message.buttonsMessage || - message.listMessage || - message.templateMessage - ); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, + public async connectToWhatsapp(number?: string): Promise { + this.logger.verbose('Connecting to whatsapp'); + try { + this.loadWebhook(); + this.loadChatwoot(); + this.loadSettings(); + + this.instance.authState = await this.defineAuthState(); + + const { version } = await fetchLatestBaileysVersion(); + this.logger.verbose('Baileys version: ' + version); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + this.logger.verbose('Browser: ' + JSON.stringify(browser)); + + const socketConfig: UserFacingSocketConfig = { + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser, + version, + markOnlineOnConnect: this.localSettings.always_online, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: true, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; }, - }, }; - } - return message; - }, - }; + this.endSession = false; - this.endSession = false; + this.logger.verbose('Creating socket'); - this.logger.verbose('Creating socket'); + this.client = makeWASocket(socketConfig); - this.client = makeWASocket(socketConfig); + this.logger.verbose('Socket created'); - this.logger.verbose('Socket created'); + this.eventHandler(); - this.eventHandler(); + this.logger.verbose('Socket event handler initialized'); - this.logger.verbose('Socket event handler initialized'); + this.phoneNumber = number; - this.phoneNumber = number; - - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } } - } - private readonly chatHandle = { - 'chats.upsert': async (chats: Chat[], database: Database) => { - this.logger.verbose('Event received: chats.upsert'); + private readonly chatHandle = { + 'chats.upsert': async (chats: Chat[], database: Database) => { + this.logger.verbose('Event received: chats.upsert'); - this.logger.verbose('Finding chats in database'); - const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.name }, - }); + this.logger.verbose('Finding chats in database'); + const chatsRepository = await this.repository.chat.find({ + where: { owner: this.instance.name }, + }); - this.logger.verbose('Verifying if chats exists in database to insert'); - const chatsRaw: ChatRaw[] = []; - for await (const chat of chats) { - if (chatsRepository.find((cr) => cr.id === chat.id)) { - continue; - } + this.logger.verbose('Verifying if chats exists in database to insert'); + const chatsRaw: ChatRaw[] = []; + for await (const chat of chats) { + if (chatsRepository.find((cr) => cr.id === chat.id)) { + continue; + } - chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); - } - - this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert( - chatsRaw, - this.instance.name, - database.SAVE_DATA.CHATS, - ); - }, - - 'chats.update': async ( - chats: Partial< - proto.IConversation & { - lastMessageRecvTimestamp?: number; - } & { - conditional: (bufferedData: BufferedEventData) => boolean; - } - >[], - ) => { - this.logger.verbose('Event received: chats.update'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { id: chat.id, owner: this.instance.wuid }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); - }, - - 'chats.delete': async (chats: string[]) => { - this.logger.verbose('Event received: chats.delete'); - - this.logger.verbose('Deleting chats in database'); - chats.forEach( - async (chat) => - await this.repository.chat.delete({ - where: { owner: this.instance.name, id: chat }, - }), - ); - - this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); - }, - }; - - private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[], database: Database) => { - this.logger.verbose('Event received: contacts.upsert'); - - this.logger.verbose('Finding contacts in database'); - const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - if (contactsRepository.find((cr) => cr.id === contact.id)) { - continue; - } - - contactsRaw.push({ - id: contact.id, - pushName: contact?.name || contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); - - this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert( - contactsRaw, - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - - 'contacts.update': async (contacts: Partial[], database: Database) => { - this.logger.verbose('Event received: contacts.update'); - - this.logger.verbose('Verifying if contacts exists in database to update'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - contactsRaw.push({ - id: contact.id, - pushName: contact?.name ?? contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - - this.logger.verbose('Updating contacts in database'); - await this.repository.contact.update( - contactsRaw, - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - }; - - private readonly messageHandle = { - 'messaging-history.set': async ( - { - messages, - chats, - isLatest, - }: { - chats: Chat[]; - contacts: Contact[]; - messages: proto.IWebMessageInfo[]; - isLatest: boolean; - }, - database: Database, - ) => { - this.logger.verbose('Event received: messaging-history.set'); - if (isLatest) { - this.logger.verbose('isLatest defined as true'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { - id: chat.id, - owner: this.instance.name, - lastMsgTimestamp: chat.lastMessageRecvTimestamp, - }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_SET'); - await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert( - chatsRaw, - this.instance.name, - database.SAVE_DATA.CHATS, - ); - } - - const messagesRaw: MessageRaw[] = []; - const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.name }, - }); - for await (const [, m] of Object.entries(messages)) { - if (!m.message) { - continue; - } - if ( - messagesRepository.find( - (mr) => mr.owner === this.instance.name && mr.key.id === m.key.id, - ) - ) { - continue; - } - - if (Long.isLong(m?.messageTimestamp)) { - m.messageTimestamp = m.messageTimestamp?.toNumber(); - } - - messagesRaw.push({ - key: m.key, - pushName: m.pushName, - participant: m.participant, - message: { ...m.message }, - messageType: getContentType(m.message), - messageTimestamp: m.messageTimestamp as number, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); - - messages = undefined; - }, - - 'messages.upsert': async ( - { - messages, - type, - }: { - messages: proto.IWebMessageInfo[]; - type: MessageUpsertType; - }, - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.upsert'); - const received = messages[0]; - - if ( - type !== 'notify' || - received.message?.protocolMessage || - received.message?.pollUpdateMessage - ) { - this.logger.verbose('message rejected'); - return; - } - - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } - - if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - - const messageRaw: MessageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - - if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - if (this.localSettings.read_status && received.key.id === 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - database.SAVE_DATA.NEW_MESSAGE, - ); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) - .profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)) - .profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update( - [contactRaw], - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - await this.repository.contact.insert( - [contactRaw], - this.instance.name, - database.SAVE_DATA.CONTACTS, - ); - }, - - 'messages.update': async ( - args: WAMessageUpdate[], - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.update'); - const status: Record = { - 0: 'ERROR', - 1: 'PENDING', - 2: 'SERVER_ACK', - 3: 'DELIVERY_ACK', - 4: 'READ', - 5: 'PLAYED', - }; - for await (const { key, update } of args) { - if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { - this.logger.verbose('Message update is valid'); - - let pollUpdates: any; - if (update.pollUpdates) { - this.logger.verbose('Poll update found'); - - this.logger.verbose('Getting poll message'); - const pollCreation = await this.getMessage(key); - this.logger.verbose(pollCreation); - - if (pollCreation) { - this.logger.verbose('Getting aggregate votes in poll message'); - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); + chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); } - } - if (status[update.status] === 'READ' && !key.fromMe) return; + this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); + await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - if (update.message === null && update.status === undefined) { - this.logger.verbose('Message deleted'); + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + }, - this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + 'chats.update': async ( + chats: Partial< + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { + conditional: (bufferedData: BufferedEventData) => boolean; + } + >[], + ) => { + this.logger.verbose('Event received: chats.update'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { id: chat.id, owner: this.instance.wuid }; + }); - const message: MessageUpdateRaw = { - ...key, - status: 'DELETED', - datetime: Date.now(), - owner: this.instance.name, + this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); + await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + }, + + 'chats.delete': async (chats: string[]) => { + this.logger.verbose('Event received: chats.delete'); + + this.logger.verbose('Deleting chats in database'); + chats.forEach( + async (chat) => + await this.repository.chat.delete({ + where: { owner: this.instance.name, id: chat }, + }), + ); + + this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); + await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + }, + }; + + private readonly contactHandle = { + 'contacts.upsert': async (contacts: Contact[], database: Database) => { + this.logger.verbose('Event received: contacts.upsert'); + + this.logger.verbose('Finding contacts in database'); + const contactsRepository = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if contacts exists in database to insert'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + if (contactsRepository.find((cr) => cr.id === contact.id)) { + continue; + } + + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Inserting contacts in database'); + await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + + 'contacts.update': async (contacts: Partial[], database: Database) => { + this.logger.verbose('Event received: contacts.update'); + + this.logger.verbose('Verifying if contacts exists in database to update'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name ?? contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + }; + + private readonly messageHandle = { + 'messaging-history.set': async ( + { + messages, + chats, + isLatest, + }: { + chats: Chat[]; + contacts: Contact[]; + messages: proto.IWebMessageInfo[]; + isLatest: boolean; + }, + database: Database, + ) => { + this.logger.verbose('Event received: messaging-history.set'); + if (isLatest) { + this.logger.verbose('isLatest defined as true'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { + id: chat.id, + owner: this.instance.name, + lastMsgTimestamp: chat.lastMessageRecvTimestamp, + }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_SET'); + await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + } + + const messagesRaw: MessageRaw[] = []; + const messagesRepository = await this.repository.message.find({ + where: { owner: this.instance.name }, + }); + for await (const [, m] of Object.entries(messages)) { + if (!m.message) { + continue; + } + if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { + continue; + } + + if (Long.isLong(m?.messageTimestamp)) { + m.messageTimestamp = m.messageTimestamp?.toNumber(); + } + + messagesRaw.push({ + key: m.key, + pushName: m.pushName, + participant: m.participant, + message: { ...m.message }, + messageType: getContentType(m.message), + messageTimestamp: m.messageTimestamp as number, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); + + messages = undefined; + }, + + 'messages.upsert': async ( + { + messages, + type, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + }, + database: Database, + settings: SettingsRaw, + ) => { + this.logger.verbose('Event received: messages.upsert'); + const received = messages[0]; + + if (type !== 'notify' || received.message?.protocolMessage || received.message?.pollUpdateMessage) { + this.logger.verbose('message rejected'); + return; + } + + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); + } + + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + + const messageRaw: MessageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), }; - this.logger.verbose(message); + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + } this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - return; - } + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); - const message: MessageUpdateRaw = { - ...key, - status: status[update.status], - datetime: Date.now(), - owner: this.instance.name, - pollUpdates, - }; - - this.logger.verbose(message); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - } - } - }, - }; - - private readonly groupHandler = { - 'groups.upsert': (groupMetadata: GroupMetadata[]) => { - this.logger.verbose('Event received: groups.upsert'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); - this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); - }, - - 'groups.update': (groupMetadataUpdate: Partial[]) => { - this.logger.verbose('Event received: groups.update'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); - this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); - }, - - 'group-participants.update': (participantsUpdate: { - id: string; - participants: string[]; - action: ParticipantAction; - }) => { - this.logger.verbose('Event received: group-participants.update'); - - this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - }, - }; - - private eventHandler() { - this.logger.verbose('Initializing event handler'); - this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - this.logger.verbose('Listening event: call'); - const call = events.call[0]; - - if (settings?.reject_call && call.status == 'offer') { - this.logger.verbose('Rejecting call'); - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { - this.logger.verbose('Sending message in call'); - const msg = await this.client.sendMessage(call.from, { - text: settings.msg_call, + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, }); - this.logger.verbose('Sending data to event messages.upsert'); - this.client.ev.emit('messages.upsert', { - messages: [msg], - type: 'notify', - }); - } - - this.logger.verbose('Sending data to webhook in event CALL'); - this.sendDataWebhook(Events.CALL, call); - } - - if (events['connection.update']) { - this.logger.verbose('Listening event: connection.update'); - this.connectionUpdate(events['connection.update']); - } - - if (events['creds.update']) { - this.logger.verbose('Listening event: creds.update'); - this.instance.authState.saveCreds(); - } - - if (events['messaging-history.set']) { - this.logger.verbose('Listening event: messaging-history.set'); - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload, database); - } - - if (events['messages.upsert']) { - this.logger.verbose('Listening event: messages.upsert'); - const payload = events['messages.upsert']; - this.messageHandle['messages.upsert'](payload, database, settings); - } - - if (events['messages.update']) { - this.logger.verbose('Listening event: messages.update'); - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, database, settings); - } - - if (events['presence.update']) { - this.logger.verbose('Listening event: presence.update'); - const payload = events['presence.update']; - - if (settings.groups_ignore && payload.id.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } - - if (!settings?.groups_ignore) { - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } - - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } - - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); - } - } - - if (events['chats.upsert']) { - this.logger.verbose('Listening event: chats.upsert'); - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload, database); - } - - if (events['chats.update']) { - this.logger.verbose('Listening event: chats.update'); - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } - - if (events['chats.delete']) { - this.logger.verbose('Listening event: chats.delete'); - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } - - if (events['contacts.upsert']) { - this.logger.verbose('Listening event: contacts.upsert'); - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload, database); - } - - if (events['contacts.update']) { - this.logger.verbose('Listening event: contacts.update'); - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload, database); - } - } - }); - } - - // Check if the number is MX or AR - private formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; - } - return jid; - } - - // Check if the number is br - private formatBRNumber(jid: string) { - const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); - if (regexp.test(jid)) { - const match = regexp.exec(jid); - if (match && match[1] === '55') { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 31) { - return match[0]; - } - return match[1] + match[2] + match[3]; - } - return jid; - } else { - return jid; - } - } - - private createJid(number: string): string { - this.logger.verbose('Creating jid with number: ' + number); - - if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); - return number; - } - - if (number.includes('@broadcast')) { - this.logger.verbose('Number already contains @broadcast'); - return number; - } - - number = number - ?.replace(/\s/g, '') - .replace(/\+/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .split(/\:/)[0] - .split('@')[0]; - - if (number.includes('-') && number.length >= 24) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - number = number.replace(/\D/g, ''); - - if (number.length >= 18) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); - return `${number}@s.whatsapp.net`; - } - - public async profilePicture(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile picture with jid: ' + jid); - try { - this.logger.verbose('Getting profile picture url'); - return { - wuid: jid, - profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), - }; - } catch (error) { - this.logger.verbose('Profile picture not found'); - return { - wuid: jid, - profilePictureUrl: null, - }; - } - } - - public async getStatus(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } - - public async fetchProfile(instanceName: string, number?: string) { - const jid = number ? this.createJid(number) : this.client?.user?.id; - - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - const info = await waMonitor.instanceInfo(instanceName); - const business = await this.fetchBusinessProfile(jid); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(jid); - const status = await this.getStatus(jid); - - return { - wuid: jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } - - private async sendMessageWithTyping( - number: string, - message: T, - options?: Options, - ) { - this.logger.verbose('Sending message with typing'); - - const numberWA = await this.whatsappNumber({ numbers: [number] }); - const isWA = numberWA[0]; - - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - try { - if (options?.delay) { - this.logger.verbose('Delaying message'); - - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); - this.logger.verbose( - 'Sending presence update: ' + options?.presence ?? 'composing', - ); - - await delay(options.delay); - this.logger.verbose('Set delay: ' + options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } - - const linkPreview = options?.linkPreview != false ? undefined : false; - - let quoted: WAMessage; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.message - ? m - : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - this.logger.verbose('Quoted message'); - } - - let mentions: string[]; - if (isJidGroup(sender)) { - try { - const groupMetadata = await this.client.groupMetadata(sender); - - if (!groupMetadata) { - throw new NotFoundException('Group not found'); - } - - if (options?.mentions) { - this.logger.verbose('Mentions defined'); - - if (options.mentions?.everyOne) { - this.logger.verbose('Mentions everyone'); - - this.logger.verbose('Getting group metadata'); - mentions = groupMetadata.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else if (options.mentions?.mentioned?.length) { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - return null; - // throw new BadRequestException('Mentions must be a number'); - } - return jid; - }); - } - } - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - - const messageSent = await (async () => { - const option = { - quoted, - }; - - if ( - !message['audio'] && - !message['poll'] && - !message['sticker'] && - !message['conversation'] && - sender !== 'status@broadcast' - ) { - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } - } - - if (message['conversation']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - text: message['conversation'], - mentions, - linkPreview: linkPreview, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (sender === 'status@broadcast') { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message['status'].content as unknown as AnyMessageContent, - { - backgroundColor: message['status'].option.backgroundColor, - font: message['status'].option.font, - statusJidList: message['status'].option.statusJidList, - } as unknown as MiscMessageGenerationOptions, - ); - } - - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - })(); - - const messageRaw: MessageRaw = { - key: messageSent.key, - pushName: messageSent.pushName, - message: { ...messageSent.message }, - messageType: getContentType(messageSent.message), - messageTimestamp: messageSent.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(messageSent.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - // if (this.localChatwoot.enabled) { - // this.chatwootService.eventWhatsapp( - // Events.SEND_MESSAGE, - // { instanceName: this.instance.name }, - // messageRaw, - // ); - // } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, - ); - - return messageSent; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Instance Controller - public get connectionStatus() { - this.logger.verbose('Getting connection status'); - return this.stateConnection; - } - - // Send Message Controller - public async textMessage(data: SendTextDto) { - this.logger.verbose('Sending text message'); - return await this.sendMessageWithTyping( - data.number, - { - conversation: data.textMessage.text, - }, - data?.options, - ); - } - - public async pollMessage(data: SendPollDto) { - this.logger.verbose('Sending poll message'); - return await this.sendMessageWithTyping( - data.number, - { - poll: { - name: data.pollMessage.name, - selectableCount: data.pollMessage.selectableCount, - values: data.pollMessage.values, - }, - }, - data?.options, - ); - } - - private async formatStatusMessage(status: StatusMessage) { - this.logger.verbose('Formatting status message'); - - if (!status.type) { - throw new BadRequestException('Type is required'); - } - - if (!status.content) { - throw new BadRequestException('Content is required'); - } - - if (status.allContacts) { - this.logger.verbose('All contacts defined as true'); - - this.logger.verbose('Getting contacts from database'); - const contacts = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - if (!contacts.length) { - throw new BadRequestException('Contacts not found'); - } - - this.logger.verbose('Getting contacts with push name'); - status.statusJidList = contacts - .filter((contact) => contact.pushName) - .map((contact) => contact.id); - - this.logger.verbose(status.statusJidList); - } - - if (!status.statusJidList?.length && !status.allContacts) { - throw new BadRequestException('StatusJidList is required'); - } - - if (status.type === 'text') { - this.logger.verbose('Type defined as text'); - - if (!status.backgroundColor) { - throw new BadRequestException('Background color is required'); - } - - if (!status.font) { - throw new BadRequestException('Font is required'); - } - - return { - content: { - text: status.content, - }, - option: { - backgroundColor: status.backgroundColor, - font: status.font, - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'image') { - this.logger.verbose('Type defined as image'); - - return { - content: { - image: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'video') { - this.logger.verbose('Type defined as video'); - - return { - content: { - video: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'audio') { - this.logger.verbose('Type defined as audio'); - - this.logger.verbose('Processing audio'); - const convert = await this.processAudio(status.content, 'status@broadcast'); - if (typeof convert === 'string') { - this.logger.verbose('Audio processed'); - const audio = fs.readFileSync(convert).toString('base64'); - - const result = { - content: { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - option: { - statusJidList: status.statusJidList, - }, - }; - - fs.unlinkSync(convert); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - throw new BadRequestException('Type not found'); - } - - public async statusMessage(data: SendStatusDto) { - this.logger.verbose('Sending status message'); - const status = await this.formatStatusMessage(data.statusMessage); - - return await this.sendMessageWithTyping('status@broadcast', { - status, - }); - } - - private async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - this.logger.verbose('Preparing media message'); - const prepareMedia = await prepareWAMessageMedia( - { - [mediaMessage.mediatype]: isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'), - } as any, - { upload: this.client.waUploadToServer }, - ); - - const mediaType = mediaMessage.mediatype + 'Message'; - this.logger.verbose('Media type: ' + mediaType); - - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - this.logger.verbose( - 'If media type is document and file name is not defined then', - ); - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - this.logger.verbose('File name: ' + mediaMessage.fileName); - } - - let mimetype: string; - - if (isURL(mediaMessage.media)) { - mimetype = getMIMEType(mediaMessage.media); - } else { - mimetype = getMIMEType(mediaMessage.fileName); - } - - this.logger.verbose('Mimetype: ' + mimetype); - - prepareMedia[mediaType].caption = mediaMessage?.caption; - prepareMedia[mediaType].mimetype = mimetype; - prepareMedia[mediaType].fileName = mediaMessage.fileName; - - if (mediaMessage.mediatype === 'video') { - this.logger.verbose('Is media type video then set gif playback as false'); - prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( - readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), - ); - prepareMedia[mediaType].gifPlayback = false; - } - - this.logger.verbose('Generating wa message from content'); - return generateWAMessageFromContent( - '', - { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, - ); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } - } - - private async convertToWebP(image: string, number: string) { - try { - this.logger.verbose('Converting image to WebP to sticker'); - - let imagePath: string; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to image name: ' + hash); - - const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; - this.logger.verbose('Output path: ' + outputPath); - - if (isBase64(image)) { - this.logger.verbose('Image is base64'); - - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); - const imageBuffer = Buffer.from(base64Data, 'base64'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } else { - this.logger.verbose('Image is url'); - - const timestamp = new Date().getTime(); - const url = `${image}?timestamp=${timestamp}`; - this.logger.verbose('including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting image from url'); - - const imageBuffer = Buffer.from(response.data, 'binary'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } - - await sharp(imagePath).webp().toFile(outputPath); - this.logger.verbose('Image converted to WebP'); - - fs.unlinkSync(imagePath); - this.logger.verbose('Temp image deleted'); - - return outputPath; - } catch (error) { - console.error('Erro ao converter a imagem para WebP:', error); - } - } - - public async mediaSticker(data: SendStickerDto) { - this.logger.verbose('Sending media sticker'); - const convert = await this.convertToWebP(data.stickerMessage.image, data.number); - const result = await this.sendMessageWithTyping( - data.number, - { - sticker: { url: convert }, - }, - data?.options, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted image deleted'); - - return result; - } - - public async mediaMessage(data: SendMediaDto) { - this.logger.verbose('Sending media message'); - const generate = await this.prepareMediaMessage(data.mediaMessage); - - return await this.sendMessageWithTyping( - data.number, - { ...generate.message }, - data?.options, - ); - } - - private async processAudio(audio: string, number: string) { - this.logger.verbose('Processing audio'); - let tempAudioPath: string; - let outputAudio: string; - - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to audio name: ' + hash); - - if (isURL(audio)) { - this.logger.verbose('Audio is url'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const timestamp = new Date().getTime(); - const url = `${audio}?timestamp=${timestamp}`; - - this.logger.verbose('Including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting audio from url'); - - fs.writeFileSync(tempAudioPath, response.data); - } else { - this.logger.verbose('Audio is base64'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const audioBuffer = Buffer.from(audio, 'base64'); - fs.writeFileSync(tempAudioPath, audioBuffer); - this.logger.verbose('Temp audio created'); - } - - this.logger.verbose('Converting audio to mp4'); - return new Promise((resolve, reject) => { - exec( - `${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, - (error, _stdout, _stderr) => { - fs.unlinkSync(tempAudioPath); - this.logger.verbose('Temp audio deleted'); - - if (error) reject(error); - - this.logger.verbose('Audio converted to mp4'); - resolve(outputAudio); - }, - ); - }); - } - - public async audioWhatsapp(data: SendAudioDto) { - this.logger.verbose('Sending audio whatsapp'); - - if (!data.options?.encoding && data.options?.encoding !== false) { - data.options.encoding = true; - } - - if (data.options?.encoding) { - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - return await this.sendMessageWithTyping( - data.number, - { - audio: isURL(data.audioMessage.audio) - ? { url: data.audioMessage.audio } - : Buffer.from(data.audioMessage.audio, 'base64'), - ptt: true, - mimetype: 'audio/ogg; codecs=opus', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - } - - public async buttonMessage(data: SendButtonDto) { - this.logger.verbose('Sending button message'); - const embeddedMedia: any = {}; - let mediatype = 'TEXT'; - - if (data.buttonMessage?.mediaMessage) { - mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; - embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; - const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); - embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; - embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; - } - - const btnItems = { - text: data.buttonMessage.buttons.map((btn) => btn.buttonText), - ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), - }; - - if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { - throw new BadRequestException( - 'Button texts cannot be repeated', - 'Button IDs cannot be repeated.', - ); - } - - return await this.sendMessageWithTyping( - data.number, - { - buttonsMessage: { - text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, - contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, - footerText: data.buttonMessage?.footerText, - buttons: data.buttonMessage.buttons.map((button) => { - return { - buttonText: { - displayText: button.buttonText, - }, - buttonId: button.buttonId, - type: 1, + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, }; - }), - headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], - [embeddedMedia?.mediaKey]: embeddedMedia?.message, + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); }, - }, - data?.options, - ); - } - public async locationMessage(data: SendLocationDto) { - this.logger.verbose('Sending location message'); - return await this.sendMessageWithTyping( - data.number, - { - locationMessage: { - degreesLatitude: data.locationMessage.latitude, - degreesLongitude: data.locationMessage.longitude, - name: data.locationMessage?.name, - address: data.locationMessage?.address, + 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { + this.logger.verbose('Event received: messages.update'); + const status: Record = { + 0: 'ERROR', + 1: 'PENDING', + 2: 'SERVER_ACK', + 3: 'DELIVERY_ACK', + 4: 'READ', + 5: 'PLAYED', + }; + for await (const { key, update } of args) { + if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + let pollUpdates: any; + if (update.pollUpdates) { + this.logger.verbose('Poll update found'); + + this.logger.verbose('Getting poll message'); + const pollCreation = await this.getMessage(key); + this.logger.verbose(pollCreation); + + if (pollCreation) { + this.logger.verbose('Getting aggregate votes in poll message'); + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } + } + + if (status[update.status] === 'READ' && !key.fromMe) return; + + if (update.message === null && update.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: status[update.status], + datetime: Date.now(), + owner: this.instance.name, + pollUpdates, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + } + } }, - }, - data?.options, - ); - } - - public async listMessage(data: SendListDto) { - this.logger.verbose('Sending list message'); - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.listMessage.title, - description: data.listMessage.description, - buttonText: data.listMessage?.buttonText, - footerText: data.listMessage?.footerText, - sections: data.listMessage.sections, - listType: 1, - }, - }, - data?.options, - ); - } - - public async contactMessage(data: SendContactDto) { - this.logger.verbose('Sending contact message'); - const message: proto.IMessage = {}; - - const vcard = (contact: ContactMessage) => { - this.logger.verbose('Creating vcard'); - let result = - 'BEGIN:VCARD\n' + - 'VERSION:3.0\n' + - `N:${contact.fullName}\n` + - `FN:${contact.fullName}\n`; - - if (contact.organization) { - this.logger.verbose('Organization defined'); - result += `ORG:${contact.organization};\n`; - } - - if (contact.email) { - this.logger.verbose('Email defined'); - result += `EMAIL:${contact.email}\n`; - } - - if (contact.url) { - this.logger.verbose('Url defined'); - result += `URL:${contact.url}\n`; - } - - if (!contact.wuid) { - this.logger.verbose('Wuid defined'); - contact.wuid = this.createJid(contact.phoneNumber); - } - - result += - `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + - 'item1.X-ABLabel:Celular\n' + - 'END:VCARD'; - - this.logger.verbose('Vcard created'); - return result; }; - if (data.contactMessage.length === 1) { - message.contactMessage = { - displayName: data.contactMessage[0].fullName, - vcard: vcard(data.contactMessage[0]), - }; - } else { - message.contactsArrayMessage = { - displayName: `${data.contactMessage.length} contacts`, - contacts: data.contactMessage.map((contact) => { - return { - displayName: contact.fullName, - vcard: vcard(contact), - }; - }), - }; - } + private readonly groupHandler = { + 'groups.upsert': (groupMetadata: GroupMetadata[]) => { + this.logger.verbose('Event received: groups.upsert'); - return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); - } - - public async reactionMessage(data: SendReactionDto) { - this.logger.verbose('Sending reaction message'); - return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { - reactionMessage: { - key: data.reactionMessage.key, - text: data.reactionMessage.reaction, - }, - }); - } - - // Chat Controller - public async whatsappNumber(data: WhatsAppNumberDto) { - this.logger.verbose('Getting whatsapp number'); - - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); - - if (isJidGroup(jid)) { - const group = await this.findGroup({ groupJid: jid }, 'inner'); - - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); - } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); - } - } - } - - return onWhatsapp; - } - - public async markMessageAsRead(data: ReadMessageDto) { - this.logger.verbose('Marking message as read'); - try { - const keys: proto.IMessageKey[] = []; - data.read_messages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { - keys.push({ - remoteJid: read.remoteJid, - fromMe: read.fromMe, - id: read.id, - }); - } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); - } - } - - public async archiveChat(data: ArchiveChatDto) { - this.logger.verbose('Archiving chat'); - try { - data.lastMessage.messageTimestamp = - data.lastMessage?.messageTimestamp ?? Date.now(); - await this.client.chatModify( - { - archive: data.archive, - lastMessages: [data.lastMessage], + this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); + this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); }, - data.lastMessage.key.remoteJid, - ); - return { - chatId: data.lastMessage.key.remoteJid, - archived: true, - }; - } catch (error) { - throw new InternalServerErrorException({ - archived: false, - message: [ - 'An error occurred while archiving the chat. Open a calling.', - error.toString(), - ], - }); - } - } + 'groups.update': (groupMetadataUpdate: Partial[]) => { + this.logger.verbose('Event received: groups.update'); - public async deleteMessage(del: DeleteMessage) { - this.logger.verbose('Deleting message'); - try { - return await this.client.sendMessage(del.remoteJid, { delete: del }); - } catch (error) { - throw new InternalServerErrorException( - 'Error while deleting message for everyone', - error?.toString(), - ); - } - } - - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { - this.logger.verbose('Getting base64 from media message'); - try { - const m = data?.message; - const convertToMp4 = data?.convertToMp4 ?? false; - - const msg = m?.message - ? m - : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - for (const subtype of MessageSubtype) { - if (msg.message[subtype]) { - msg.message = msg.message[subtype].message; - } - } - - let mediaMessage: any; - let mediaType: string; - - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; - } - } - - if (!mediaMessage) { - throw 'The message is not of the media type'; - } - - if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message = JSON.parse(JSON.stringify(msg.message)); - } - - this.logger.verbose('Downloading media message'); - const buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }), - reuploadRequest: this.client.updateMediaMessage, + this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); + this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); }, - ); - const typeMessage = getContentType(msg.message); - if (convertToMp4 && typeMessage === 'audioMessage') { - this.logger.verbose('Converting audio to mp4'); - const number = msg.key.remoteJid.split('@')[0]; - const convert = await this.processAudio(buffer.toString('base64'), number); + 'group-participants.update': (participantsUpdate: { + id: string; + participants: string[]; + action: ParticipantAction; + }) => { + this.logger.verbose('Event received: group-participants.update'); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - this.logger.verbose('Audio converted to mp4'); - - const result = { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: 'audio/mp4', - base64: Buffer.from(audio, 'base64').toString('base64'), - }; - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - this.logger.verbose('Media message downloaded'); - return result; - } - } - - this.logger.verbose('Media message downloaded'); - return { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], + this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); }, - mimetype: mediaMessage['mimetype'], - base64: buffer.toString('base64'), - }; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } + }; - public async fetchContacts(query: ContactQuery) { - this.logger.verbose('Fetching contacts'); - if (query?.where) { - query.where.owner = this.instance.name; - if (query.where?.id) { - query.where.id = this.createJid(query.where.id); - } - } else { - query = { - where: { - owner: this.instance.name, - }, - }; - } - return await this.repository.contact.find(query); - } + private eventHandler() { + this.logger.verbose('Initializing event handler'); + this.client.ev.process(async (events) => { + if (!this.endSession) { + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); - public async fetchMessages(query: MessageQuery) { - this.logger.verbose('Fetching messages'); - if (query?.where) { - if (query.where?.key?.remoteJid) { - query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.message.find(query); - } + if (events.call) { + this.logger.verbose('Listening event: call'); + const call = events.call[0]; - public async fetchStatusMessage(query: MessageUpQuery) { - this.logger.verbose('Fetching status messages'); - if (query?.where) { - if (query.where?.remoteJid) { - query.where.remoteJid = this.createJid(query.where.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.messageUpdate.find(query); - } + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } - public async fetchChats() { - this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.name } }); - } + if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + const msg = await this.client.sendMessage(call.from, { + text: settings.msg_call, + }); - public async fetchPrivacySettings() { - this.logger.verbose('Fetching privacy settings'); - return await this.client.fetchPrivacySettings(); - } + this.logger.verbose('Sending data to event messages.upsert'); + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); + } - public async updatePrivacySettings(settings: PrivacySettingDto) { - this.logger.verbose('Updating privacy settings'); - try { - await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); - this.logger.verbose('Read receipts privacy updated'); + this.logger.verbose('Sending data to webhook in event CALL'); + this.sendDataWebhook(Events.CALL, call); + } - await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); - this.logger.verbose('Profile picture privacy updated'); + if (events['connection.update']) { + this.logger.verbose('Listening event: connection.update'); + this.connectionUpdate(events['connection.update']); + } - await this.client.updateStatusPrivacy(settings.privacySettings.status); - this.logger.verbose('Status privacy updated'); + if (events['creds.update']) { + this.logger.verbose('Listening event: creds.update'); + this.instance.authState.saveCreds(); + } - await this.client.updateOnlinePrivacy(settings.privacySettings.online); - this.logger.verbose('Online privacy updated'); + if (events['messaging-history.set']) { + this.logger.verbose('Listening event: messaging-history.set'); + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload, database); + } - await this.client.updateLastSeenPrivacy(settings.privacySettings.last); - this.logger.verbose('Last seen privacy updated'); + if (events['messages.upsert']) { + this.logger.verbose('Listening event: messages.upsert'); + const payload = events['messages.upsert']; + this.messageHandle['messages.upsert'](payload, database, settings); + } - await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); - this.logger.verbose('Groups add privacy updated'); + if (events['messages.update']) { + this.logger.verbose('Listening event: messages.update'); + const payload = events['messages.update']; + this.messageHandle['messages.update'](payload, database, settings); + } - this.client?.ws?.close(); + if (events['presence.update']) { + this.logger.verbose('Listening event: presence.update'); + const payload = events['presence.update']; - return { - update: 'success', - data: { - readreceipts: settings.privacySettings.readreceipts, - profile: settings.privacySettings.profile, - status: settings.privacySettings.status, - online: settings.privacySettings.online, - last: settings.privacySettings.last, - groupadd: settings.privacySettings.groupadd, - }, - }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating privacy settings', - error.toString(), - ); - } - } + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } - public async fetchBusinessProfile(number: string): Promise { - this.logger.verbose('Fetching business profile'); - try { - const jid = number ? this.createJid(number) : this.instance.wuid; + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } - const profile = await this.client.getBusinessProfile(jid); - this.logger.verbose('Trying to get business profile'); + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } - if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift(), - }; - } + if (events['chats.upsert']) { + this.logger.verbose('Listening event: chats.upsert'); + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload, database); + } - this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile, - }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile name', - error.toString(), - ); - } - } + if (events['chats.update']) { + this.logger.verbose('Listening event: chats.update'); + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } - public async updateProfileName(name: string) { - this.logger.verbose('Updating profile name to ' + name); - try { - await this.client.updateProfileName(name); + if (events['chats.delete']) { + this.logger.verbose('Listening event: chats.delete'); + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile name', - error.toString(), - ); - } - } + if (events['contacts.upsert']) { + this.logger.verbose('Listening event: contacts.upsert'); + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload, database); + } - public async updateProfileStatus(status: string) { - this.logger.verbose('Updating profile status to: ' + status); - try { - await this.client.updateProfileStatus(status); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile status', - error.toString(), - ); - } - } - - public async updateProfilePicture(picture: string) { - this.logger.verbose('Updating profile picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(this.instance.wuid, pic); - this.logger.verbose('Profile picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating profile picture', - error.toString(), - ); - } - } - - public async removeProfilePicture() { - this.logger.verbose('Removing profile picture'); - try { - await this.client.removeProfilePicture(this.instance.wuid); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error removing profile picture', - error.toString(), - ); - } - } - - // Group - public async createGroup(create: CreateGroupDto) { - this.logger.verbose('Creating group: ' + create.subject); - try { - const participants = create.participants.map((p) => this.createJid(p)); - const { id } = await this.client.groupCreate(create.subject, participants); - this.logger.verbose('Group created: ' + id); - - if (create?.description) { - this.logger.verbose('Updating group description: ' + create.description); - await this.client.groupUpdateDescription(id, create.description); - } - - if (create?.promoteParticipants) { - this.logger.verbose('Prometing group participants: ' + create.description); - await this.updateGParticipant({ - groupJid: id, - action: 'promote', - participants: participants, + if (events['contacts.update']) { + this.logger.verbose('Listening event: contacts.update'); + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload, database); + } + } }); - } - - const group = await this.client.groupMetadata(id); - this.logger.verbose('Getting group metadata'); - - return group; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); } - } - public async updateGroupPicture(picture: GroupPictureDto) { - this.logger.verbose('Updating group picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture.image)) { - this.logger.verbose('Picture is url'); + // Check if the number is MX or AR + private formatMXOrARNumber(jid: string): string { + const countryCode = jid.substring(0, 2); - const timestamp = new Date().getTime(); - const url = `${picture.image}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + if (jid.length === 13) { + const number = countryCode + jid.substring(3); + return number; + } - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture.image)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture.image, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(picture.groupJid, pic); - this.logger.verbose('Group picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error update group picture', - error.toString(), - ); - } - } - - public async updateGroupSubject(data: GroupSubjectDto) { - this.logger.verbose('Updating group subject to: ' + data.subject); - try { - await this.client.groupUpdateSubject(data.groupJid, data.subject); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating group subject', - error.toString(), - ); - } - } - - public async updateGroupDescription(data: GroupDescriptionDto) { - this.logger.verbose('Updating group description to: ' + data.description); - try { - await this.client.groupUpdateDescription(data.groupJid, data.description); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException( - 'Error updating group description', - error.toString(), - ); - } - } - - public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { - this.logger.verbose('Fetching group'); - try { - return await this.client.groupMetadata(id.groupJid); - } catch (error) { - if (reply === 'inner') { - return; - } - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async fetchAllGroups(getParticipants: GetParticipant) { - this.logger.verbose('Fetching all groups'); - try { - const fetch = Object.values(await this.client.groupFetchAllParticipating()); - - const groups = fetch.map((group) => { - const result = { - id: group.id, - subject: group.subject, - subjectOwner: group.subjectOwner, - subjectTime: group.subjectTime, - size: group.size, - creation: group.creation, - owner: group.owner, - desc: group.desc, - descId: group.descId, - restrict: group.restrict, - announce: group.announce, - }; - - if (getParticipants.getParticipants == 'true') { - result['participants'] = group.participants; + return jid; } + return jid; + } + + // Check if the number is br + private formatBRNumber(jid: string) { + const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); + if (regexp.test(jid)) { + const match = regexp.exec(jid); + if (match && match[1] === '55') { + const joker = Number.parseInt(match[3][0]); + const ddd = Number.parseInt(match[2]); + if (joker < 7 || ddd < 31) { + return match[0]; + } + return match[1] + match[2] + match[3]; + } + return jid; + } else { + return jid; + } + } + + private createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); + + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); + return number; + } + + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; + } + + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(':')[0] + .split('@')[0]; + + if (number.includes('-') && number.length >= 24) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + number = number.replace(/\D/g, ''); + + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } + + public async profilePicture(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile picture with jid: ' + jid); + try { + this.logger.verbose('Getting profile picture url'); + return { + wuid: jid, + profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), + }; + } catch (error) { + this.logger.verbose('Profile picture not found'); + return { + wuid: jid, + profilePictureUrl: null, + }; + } + } + + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const business = await this.fetchBusinessProfile(jid); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + + private async sendMessageWithTyping(number: string, message: T, options?: Options) { + this.logger.verbose('Sending message with typing'); + + const numberWA = await this.whatsappNumber({ numbers: [number] }); + const isWA = numberWA[0]; + + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + try { + if (options?.delay) { + this.logger.verbose('Delaying message'); + + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); + + await delay(options.delay); + this.logger.verbose('Set delay: ' + options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } + + const linkPreview = options?.linkPreview != false ? undefined : false; + + let quoted: WAMessage; + + if (options?.quoted) { + const m = options?.quoted; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let mentions: string[]; + if (isJidGroup(sender)) { + try { + const groupMetadata = await this.client.groupMetadata(sender); + + if (!groupMetadata) { + throw new NotFoundException('Group not found'); + } + + if (options?.mentions) { + this.logger.verbose('Mentions defined'); + + if (options.mentions?.everyOne) { + this.logger.verbose('Mentions everyone'); + + this.logger.verbose('Getting group metadata'); + mentions = groupMetadata.participants.map((participant) => participant.id); + this.logger.verbose('Getting group metadata for mentions'); + } else if (options.mentions?.mentioned?.length) { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + return null; + // throw new BadRequestException('Mentions must be a number'); + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); + } + } + + const messageSent = await (async () => { + const option = { + quoted, + }; + + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + !message['conversation'] && + sender !== 'status@broadcast' + ) { + if (!message['audio']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, + }, + mentions, + }, + option as unknown as MiscMessageGenerationOptions, + ); + } + } + + if (message['conversation']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + text: message['conversation'], + mentions, + linkPreview: linkPreview, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } + + if (sender === 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, + { + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); + } + + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + })(); + + const messageRaw: MessageRaw = { + key: messageSent.key, + pushName: messageSent.pushName, + message: { ...messageSent.message }, + messageType: getContentType(messageSent.message), + messageTimestamp: messageSent.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + // if (this.localChatwoot.enabled) { + // this.chatwootService.eventWhatsapp( + // Events.SEND_MESSAGE, + // { instanceName: this.instance.name }, + // messageRaw, + // ); + // } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + return messageSent; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Instance Controller + public get connectionStatus() { + this.logger.verbose('Getting connection status'); + return this.stateConnection; + } + + // Send Message Controller + public async textMessage(data: SendTextDto) { + this.logger.verbose('Sending text message'); + return await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, + }, + data?.options, + ); + } + + public async pollMessage(data: SendPollDto) { + this.logger.verbose('Sending poll message'); + return await this.sendMessageWithTyping( + data.number, + { + poll: { + name: data.pollMessage.name, + selectableCount: data.pollMessage.selectableCount, + values: data.pollMessage.values, + }, + }, + data?.options, + ); + } + + private async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); + } + + if (!status.content) { + throw new BadRequestException('Content is required'); + } + + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); + + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); + } + + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); + } + + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); + + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } + + if (!status.font) { + throw new BadRequestException('Font is required'); + } + + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); + + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); + + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); + + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); + + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; + + fs.unlinkSync(convert); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + const status = await this.formatStatusMessage(data.statusMessage); + + return await this.sendMessageWithTyping('status@broadcast', { + status, + }); + } + + private async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + const prepareMedia = await prepareWAMessageMedia( + { + [mediaMessage.mediatype]: isURL(mediaMessage.media) + ? { url: mediaMessage.media } + : Buffer.from(mediaMessage.media, 'base64'), + } as any, + { upload: this.client.waUploadToServer }, + ); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose('If media type is document and file name is not defined then'); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + let mimetype: string; + + if (isURL(mediaMessage.media)) { + mimetype = getMIMEType(mediaMessage.media); + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } + + this.logger.verbose('Mimetype: ' + mimetype); + + prepareMedia[mediaType].caption = mediaMessage?.caption; + prepareMedia[mediaType].mimetype = mimetype; + prepareMedia[mediaType].fileName = mediaMessage.fileName; + + if (mediaMessage.mediatype === 'video') { + this.logger.verbose('Is media type video then set gif playback as false'); + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), + ); + prepareMedia[mediaType].gifPlayback = false; + } + + this.logger.verbose('Generating wa message from content'); + return generateWAMessageFromContent( + '', + { [mediaType]: { ...prepareMedia[mediaType] } }, + { userJid: this.instance.wuid }, + ); + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + private async convertToWebP(image: string, number: string) { + try { + this.logger.verbose('Converting image to WebP to sticker'); + + let imagePath: string; + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to image name: ' + hash); + + const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; + this.logger.verbose('Output path: ' + outputPath); + + if (isBase64(image)) { + this.logger.verbose('Image is base64'); + + const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const imageBuffer = Buffer.from(base64Data, 'base64'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } else { + this.logger.verbose('Image is url'); + + const timestamp = new Date().getTime(); + const url = `${image}?timestamp=${timestamp}`; + this.logger.verbose('including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting image from url'); + + const imageBuffer = Buffer.from(response.data, 'binary'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } + + await sharp(imagePath).webp().toFile(outputPath); + this.logger.verbose('Image converted to WebP'); + + fs.unlinkSync(imagePath); + this.logger.verbose('Temp image deleted'); + + return outputPath; + } catch (error) { + console.error('Erro ao converter a imagem para WebP:', error); + } + } + + public async mediaSticker(data: SendStickerDto) { + this.logger.verbose('Sending media sticker'); + const convert = await this.convertToWebP(data.stickerMessage.image, data.number); + const result = await this.sendMessageWithTyping( + data.number, + { + sticker: { url: convert }, + }, + data?.options, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted image deleted'); return result; - }); - - return groups; - } catch (error) { - throw new NotFoundException('Error fetching group', error.toString()); } - } - public async inviteCode(id: GroupJid) { - this.logger.verbose('Fetching invite code for group: ' + id.groupJid); - try { - const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { - throw new NotFoundException('No invite code', error.toString()); + public async mediaMessage(data: SendMediaDto) { + this.logger.verbose('Sending media message'); + const generate = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options); } - } - public async inviteInfo(id: GroupInvite) { - this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); - try { - return await this.client.groupGetInviteInfo(id.inviteCode); - } catch (error) { - throw new NotFoundException('No invite info', id.inviteCode); + private async processAudio(audio: string, number: string) { + this.logger.verbose('Processing audio'); + let tempAudioPath: string; + let outputAudio: string; + + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to audio name: ' + hash); + + if (isURL(audio)) { + this.logger.verbose('Audio is url'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const timestamp = new Date().getTime(); + const url = `${audio}?timestamp=${timestamp}`; + + this.logger.verbose('Including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting audio from url'); + + fs.writeFileSync(tempAudioPath, response.data); + } else { + this.logger.verbose('Audio is base64'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const audioBuffer = Buffer.from(audio, 'base64'); + fs.writeFileSync(tempAudioPath, audioBuffer); + this.logger.verbose('Temp audio created'); + } + + this.logger.verbose('Converting audio to mp4'); + return new Promise((resolve, reject) => { + exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { + fs.unlinkSync(tempAudioPath); + this.logger.verbose('Temp audio deleted'); + + if (error) reject(error); + + this.logger.verbose('Audio converted to mp4'); + resolve(outputAudio); + }); + }); } - } - public async sendInvite(id: GroupSendInvite) { - this.logger.verbose('Sending invite for group: ' + id.groupJid); - try { - const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); - this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); + public async audioWhatsapp(data: SendAudioDto) { + this.logger.verbose('Sending audio whatsapp'); - const inviteUrl = inviteCode.inviteUrl; - this.logger.verbose('Invite url: ' + inviteUrl); + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; + } - const numbers = id.numbers.map((number) => this.createJid(number)); - const description = id.description ?? ''; + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); - const msg = `${description}\n\n${inviteUrl}`; + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); - const message = { - conversation: msg, - }; + return result; + } else { + throw new InternalServerErrorException(convert); + } + } - for await (const number of numbers) { - await this.sendMessageWithTyping(number, message); - } - - this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - - return { send: true, inviteUrl }; - } catch (error) { - throw new NotFoundException('No send invite'); + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); } - } - public async revokeInviteCode(id: GroupJid) { - this.logger.verbose('Revoking invite code for group: ' + id.groupJid); - try { - const inviteCode = await this.client.groupRevokeInvite(id.groupJid); - return { revoked: true, inviteCode }; - } catch (error) { - throw new NotFoundException('Revoke error', error.toString()); - } - } + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; - public async findParticipants(id: GroupJid) { - this.logger.verbose('Fetching participants for group: ' + id.groupJid); - try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; - return { participants }; - } catch (error) { - throw new NotFoundException('No participants', error.toString()); - } - } + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + } - public async updateGParticipant(update: GroupUpdateParticipantDto) { - this.logger.verbose('Updating participants'); - try { - const participants = update.participants.map((p) => this.createJid(p)); - const updateParticipants = await this.client.groupParticipantsUpdate( - update.groupJid, - participants, - update.action, - ); - return { updateParticipants: updateParticipants }; - } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); - } - } + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; - public async updateGSetting(update: GroupUpdateSettingDto) { - this.logger.verbose('Updating setting for group: ' + update.groupJid); - try { - const updateSetting = await this.client.groupSettingUpdate( - update.groupJid, - update.action, - ); - return { updateSetting: updateSetting }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } + if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { + throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); + } - public async toggleEphemeral(update: GroupToggleEphemeralDto) { - this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); - try { - const toggleEphemeral = await this.client.groupToggleEphemeral( - update.groupJid, - update.expiration, - ); - return { success: true }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); + return await this.sendMessageWithTyping( + data.number, + { + buttonsMessage: { + text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, + contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, + footerText: data.buttonMessage?.footerText, + buttons: data.buttonMessage.buttons.map((button) => { + return { + buttonText: { + displayText: button.buttonText, + }, + buttonId: button.buttonId, + type: 1, + }; + }), + headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + }, + data?.options, + ); } - } - public async leaveGroup(id: GroupJid) { - this.logger.verbose('Leaving group: ' + id.groupJid); - try { - await this.client.groupLeave(id.groupJid); - return { groupJid: id.groupJid, leave: true }; - } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } + + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + return await this.sendMessageWithTyping( + data.number, + { + listMessage: { + title: data.listMessage.title, + description: data.listMessage.description, + buttonText: data.listMessage?.buttonText, + footerText: data.listMessage?.footerText, + sections: data.listMessage.sections, + listType: 1, + }, + }, + data?.options, + ); + } + + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + const message: proto.IMessage = {}; + + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } + + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += + `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + // Chat Controller + public async whatsappNumber(data: WhatsAppNumberDto) { + this.logger.verbose('Getting whatsapp number'); + + const onWhatsapp: OnWhatsAppDto[] = []; + for await (const number of data.numbers) { + let jid = this.createJid(number); + + if (isJidGroup(jid)) { + const group = await this.findGroup({ groupJid: jid }, 'inner'); + + if (!group) throw new BadRequestException('Group not found'); + + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else { + jid = !jid.startsWith('+') ? `+${jid}` : jid; + const verify = await this.client.onWhatsApp(jid); + + const result = verify[0]; + + if (!result) { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); + } else { + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); + } + } + } + + return onWhatsapp; + } + + public async markMessageAsRead(data: ReadMessageDto) { + this.logger.verbose('Marking message as read'); + try { + const keys: proto.IMessageKey[] = []; + data.read_messages.forEach((read) => { + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); + } + }); + await this.client.readMessages(keys); + return { message: 'Read messages', read: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Read messages fail', error.toString()); + } + } + + public async archiveChat(data: ArchiveChatDto) { + this.logger.verbose('Archiving chat'); + try { + data.lastMessage.messageTimestamp = data.lastMessage?.messageTimestamp ?? Date.now(); + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [data.lastMessage], + }, + data.lastMessage.key.remoteJid, + ); + + return { + chatId: data.lastMessage.key.remoteJid, + archived: true, + }; + } catch (error) { + throw new InternalServerErrorException({ + archived: false, + message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], + }); + } + } + + public async deleteMessage(del: DeleteMessage) { + this.logger.verbose('Deleting message'); + try { + return await this.client.sendMessage(del.remoteJid, { delete: del }); + } catch (error) { + throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); + } + } + + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { + this.logger.verbose('Getting base64 from media message'); + try { + const m = data?.message; + const convertToMp4 = data?.convertToMp4 ?? false; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + for (const subtype of MessageSubtype) { + if (msg.message[subtype]) { + msg.message = msg.message[subtype].message; + } + } + + let mediaMessage: any; + let mediaType: string; + + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; + } + } + + if (!mediaMessage) { + throw 'The message is not of the media type'; + } + + if (typeof mediaMessage['mediaKey'] === 'object') { + msg.message = JSON.parse(JSON.stringify(msg.message)); + } + + this.logger.verbose('Downloading media message'); + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }), + reuploadRequest: this.client.updateMediaMessage, + }, + ); + const typeMessage = getContentType(msg.message); + + if (convertToMp4 && typeMessage === 'audioMessage') { + this.logger.verbose('Converting audio to mp4'); + const number = msg.key.remoteJid.split('@')[0]; + const convert = await this.processAudio(buffer.toString('base64'), number); + + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + this.logger.verbose('Audio converted to mp4'); + + const result = { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: 'audio/mp4', + base64: Buffer.from(audio, 'base64').toString('base64'), + }; + + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); + + this.logger.verbose('Media message downloaded'); + return result; + } + } + + this.logger.verbose('Media message downloaded'); + return { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: mediaMessage['mimetype'], + base64: buffer.toString('base64'), + }; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + public async fetchContacts(query: ContactQuery) { + this.logger.verbose('Fetching contacts'); + if (query?.where) { + query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } + } else { + query = { + where: { + owner: this.instance.name, + }, + }; + } + return await this.repository.contact.find(query); + } + + public async fetchMessages(query: MessageQuery) { + this.logger.verbose('Fetching messages'); + if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.message.find(query); + } + + public async fetchStatusMessage(query: MessageUpQuery) { + this.logger.verbose('Fetching status messages'); + if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.messageUpdate.find(query); + } + + public async fetchChats() { + this.logger.verbose('Fetching chats'); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); + } + + public async fetchPrivacySettings() { + this.logger.verbose('Fetching privacy settings'); + return await this.client.fetchPrivacySettings(); + } + + public async updatePrivacySettings(settings: PrivacySettingDto) { + this.logger.verbose('Updating privacy settings'); + try { + await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); + this.logger.verbose('Read receipts privacy updated'); + + await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); + this.logger.verbose('Profile picture privacy updated'); + + await this.client.updateStatusPrivacy(settings.privacySettings.status); + this.logger.verbose('Status privacy updated'); + + await this.client.updateOnlinePrivacy(settings.privacySettings.online); + this.logger.verbose('Online privacy updated'); + + await this.client.updateLastSeenPrivacy(settings.privacySettings.last); + this.logger.verbose('Last seen privacy updated'); + + await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); + this.logger.verbose('Groups add privacy updated'); + + this.client?.ws?.close(); + + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating privacy settings', error.toString()); + } + } + + public async fetchBusinessProfile(number: string): Promise { + this.logger.verbose('Fetching business profile'); + try { + const jid = number ? this.createJid(number) : this.instance.wuid; + + const profile = await this.client.getBusinessProfile(jid); + this.logger.verbose('Trying to get business profile'); + + if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; + } + + this.logger.verbose('Business profile fetched'); + return { + isBusiness: true, + ...profile, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileName(name: string) { + this.logger.verbose('Updating profile name to ' + name); + try { + await this.client.updateProfileName(name); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileStatus(status: string) { + this.logger.verbose('Updating profile status to: ' + status); + try { + await this.client.updateProfileStatus(status); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile status', error.toString()); + } + } + + public async updateProfilePicture(picture: string) { + this.logger.verbose('Updating profile picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(this.instance.wuid, pic); + this.logger.verbose('Profile picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile picture', error.toString()); + } + } + + public async removeProfilePicture() { + this.logger.verbose('Removing profile picture'); + try { + await this.client.removeProfilePicture(this.instance.wuid); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error removing profile picture', error.toString()); + } + } + + // Group + public async createGroup(create: CreateGroupDto) { + this.logger.verbose('Creating group: ' + create.subject); + try { + const participants = create.participants.map((p) => this.createJid(p)); + const { id } = await this.client.groupCreate(create.subject, participants); + this.logger.verbose('Group created: ' + id); + + if (create?.description) { + this.logger.verbose('Updating group description: ' + create.description); + await this.client.groupUpdateDescription(id, create.description); + } + + if (create?.promoteParticipants) { + this.logger.verbose('Prometing group participants: ' + create.description); + await this.updateGParticipant({ + groupJid: id, + action: 'promote', + participants: participants, + }); + } + + const group = await this.client.groupMetadata(id); + this.logger.verbose('Getting group metadata'); + + return group; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException('Error creating group', error.toString()); + } + } + + public async updateGroupPicture(picture: GroupPictureDto) { + this.logger.verbose('Updating group picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture.image)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture.image}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture.image)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture.image, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(picture.groupJid, pic); + this.logger.verbose('Group picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error update group picture', error.toString()); + } + } + + public async updateGroupSubject(data: GroupSubjectDto) { + this.logger.verbose('Updating group subject to: ' + data.subject); + try { + await this.client.groupUpdateSubject(data.groupJid, data.subject); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group subject', error.toString()); + } + } + + public async updateGroupDescription(data: GroupDescriptionDto) { + this.logger.verbose('Updating group description to: ' + data.description); + try { + await this.client.groupUpdateDescription(data.groupJid, data.description); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group description', error.toString()); + } + } + + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { + this.logger.verbose('Fetching group'); + try { + return await this.client.groupMetadata(id.groupJid); + } catch (error) { + if (reply === 'inner') { + return; + } + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async fetchAllGroups(getParticipants: GetParticipant) { + this.logger.verbose('Fetching all groups'); + try { + const fetch = Object.values(await this.client.groupFetchAllParticipating()); + + const groups = fetch.map((group) => { + const result = { + id: group.id, + subject: group.subject, + subjectOwner: group.subjectOwner, + subjectTime: group.subjectTime, + size: group.size, + creation: group.creation, + owner: group.owner, + desc: group.desc, + descId: group.descId, + restrict: group.restrict, + announce: group.announce, + }; + + if (getParticipants.getParticipants == 'true') { + result['participants'] = group.participants; + } + + return result; + }); + + return groups; + } catch (error) { + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async inviteCode(id: GroupJid) { + this.logger.verbose('Fetching invite code for group: ' + id.groupJid); + try { + const code = await this.client.groupInviteCode(id.groupJid); + return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + } catch (error) { + throw new NotFoundException('No invite code', error.toString()); + } + } + + public async inviteInfo(id: GroupInvite) { + this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); + try { + return await this.client.groupGetInviteInfo(id.inviteCode); + } catch (error) { + throw new NotFoundException('No invite info', id.inviteCode); + } + } + + public async sendInvite(id: GroupSendInvite) { + this.logger.verbose('Sending invite for group: ' + id.groupJid); + try { + const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); + this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); + + const inviteUrl = inviteCode.inviteUrl; + this.logger.verbose('Invite url: ' + inviteUrl); + + const numbers = id.numbers.map((number) => this.createJid(number)); + const description = id.description ?? ''; + + const msg = `${description}\n\n${inviteUrl}`; + + const message = { + conversation: msg, + }; + + for await (const number of numbers) { + await this.sendMessageWithTyping(number, message); + } + + this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); + + return { send: true, inviteUrl }; + } catch (error) { + throw new NotFoundException('No send invite'); + } + } + + public async revokeInviteCode(id: GroupJid) { + this.logger.verbose('Revoking invite code for group: ' + id.groupJid); + try { + const inviteCode = await this.client.groupRevokeInvite(id.groupJid); + return { revoked: true, inviteCode }; + } catch (error) { + throw new NotFoundException('Revoke error', error.toString()); + } + } + + public async findParticipants(id: GroupJid) { + this.logger.verbose('Fetching participants for group: ' + id.groupJid); + try { + const participants = (await this.client.groupMetadata(id.groupJid)).participants; + return { participants }; + } catch (error) { + throw new NotFoundException('No participants', error.toString()); + } + } + + public async updateGParticipant(update: GroupUpdateParticipantDto) { + this.logger.verbose('Updating participants'); + try { + const participants = update.participants.map((p) => this.createJid(p)); + const updateParticipants = await this.client.groupParticipantsUpdate( + update.groupJid, + participants, + update.action, + ); + return { updateParticipants: updateParticipants }; + } catch (error) { + throw new BadRequestException('Error updating participants', error.toString()); + } + } + + public async updateGSetting(update: GroupUpdateSettingDto) { + this.logger.verbose('Updating setting for group: ' + update.groupJid); + try { + const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); + return { updateSetting: updateSetting }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async toggleEphemeral(update: GroupToggleEphemeralDto) { + this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); + try { + await this.client.groupToggleEphemeral(update.groupJid, update.expiration); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); + } + } + + public async leaveGroup(id: GroupJid) { + this.logger.verbose('Leaving group: ' + id.groupJid); + try { + await this.client.groupLeave(id.groupJid); + return { groupJid: id.groupJid, leave: true }; + } catch (error) { + throw new BadRequestException('Unable to leave the group', error.toString()); + } } - } } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index a0d514d8..59e9d012 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -2,101 +2,88 @@ import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'; export enum Events { - APPLICATION_STARTUP = 'application.startup', - QRCODE_UPDATED = 'qrcode.updated', - CONNECTION_UPDATE = 'connection.update', - STATUS_INSTANCE = 'status.instance', - MESSAGES_SET = 'messages.set', - MESSAGES_UPSERT = 'messages.upsert', - MESSAGES_UPDATE = 'messages.update', - MESSAGES_DELETE = 'messages.delete', - SEND_MESSAGE = 'send.message', - CONTACTS_SET = 'contacts.set', - CONTACTS_UPSERT = 'contacts.upsert', - CONTACTS_UPDATE = 'contacts.update', - PRESENCE_UPDATE = 'presence.update', - CHATS_SET = 'chats.set', - CHATS_UPDATE = 'chats.update', - CHATS_UPSERT = 'chats.upsert', - CHATS_DELETE = 'chats.delete', - GROUPS_UPSERT = 'groups.upsert', - GROUPS_UPDATE = 'groups.update', - GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', - CALL = 'call', + APPLICATION_STARTUP = 'application.startup', + QRCODE_UPDATED = 'qrcode.updated', + CONNECTION_UPDATE = 'connection.update', + STATUS_INSTANCE = 'status.instance', + MESSAGES_SET = 'messages.set', + MESSAGES_UPSERT = 'messages.upsert', + MESSAGES_UPDATE = 'messages.update', + MESSAGES_DELETE = 'messages.delete', + SEND_MESSAGE = 'send.message', + CONTACTS_SET = 'contacts.set', + CONTACTS_UPSERT = 'contacts.upsert', + CONTACTS_UPDATE = 'contacts.update', + PRESENCE_UPDATE = 'presence.update', + CHATS_SET = 'chats.set', + CHATS_UPDATE = 'chats.update', + CHATS_UPSERT = 'chats.upsert', + CHATS_DELETE = 'chats.delete', + GROUPS_UPSERT = 'groups.upsert', + GROUPS_UPDATE = 'groups.update', + GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + CALL = 'call', } export declare namespace wa { - export type QrCode = { - count?: number; - pairingCode?: string; - base64?: string; - code?: string; - }; - export type Instance = { - qrcode?: QrCode; - pairingCode?: string; - authState?: { state: AuthenticationState; saveCreds: () => void }; - name?: string; - wuid?: string; - profileName?: string; - profilePictureUrl?: string; - }; + export type QrCode = { + count?: number; + pairingCode?: string; + base64?: string; + code?: string; + }; + export type Instance = { + qrcode?: QrCode; + pairingCode?: string; + authState?: { state: AuthenticationState; saveCreds: () => void }; + name?: string; + wuid?: string; + profileName?: string; + profilePictureUrl?: string; + }; - export type LocalWebHook = { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; - }; + export type LocalWebHook = { + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; + }; - export type LocalChatwoot = { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; - }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; + }; - export type LocalSettings = { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; - }; + export type LocalSettings = { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + }; - export type StateConnection = { - instance?: string; - state?: WAConnectionState | 'refused'; - statusReason?: number; - }; + export type StateConnection = { + instance?: string; + state?: WAConnectionState | 'refused'; + statusReason?: number; + }; - export type StatusMessage = - | 'ERROR' - | 'PENDING' - | 'SERVER_ACK' - | 'DELIVERY_ACK' - | 'READ' - | 'DELETED' - | 'PLAYED'; + export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; } -export const TypeMediaMessage = [ - 'imageMessage', - 'documentMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', -]; +export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage']; export const MessageSubtype = [ - 'ephemeralMessage', - 'documentWithCaptionMessage', - 'viewOnceMessage', - 'viewOnceMessageV2', + 'ephemeralMessage', + 'documentWithCaptionMessage', + 'viewOnceMessage', + 'viewOnceMessageV2', ]; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index b8f3b1ad..f9a9456e 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -1,43 +1,40 @@ -import { Auth, configService } from '../config/env.config'; -import { Logger } from '../config/logger.config'; +import { configService } from '../config/env.config'; import { eventEmitter } from '../config/event.config'; -import { MessageRepository } from './repository/message.repository'; -import { WAMonitoringService } from './services/monitor.service'; -import { ChatRepository } from './repository/chat.repository'; -import { ContactRepository } from './repository/contact.repository'; -import { MessageUpRepository } from './repository/messageUp.repository'; +import { Logger } from '../config/logger.config'; +import { dbserver } from '../db/db.connect'; +import { RedisCache } from '../db/redis.client'; 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 { SendMessageController } from './controllers/sendMessage.controller'; -import { AuthService } from './services/auth.service'; -import { GroupController } from './controllers/group.controller'; -import { ViewsController } from './controllers/views.controller'; -import { WebhookService } from './services/webhook.service'; -import { WebhookController } from './controllers/webhook.controller'; -import { ChatwootService } from './services/chatwoot.service'; -import { ChatwootController } from './controllers/chatwoot.controller'; -import { RepositoryBroker } from './repository/repository.manager'; -import { - AuthModel, - ChatModel, - ContactModel, - MessageModel, - MessageUpModel, - ChatwootModel, - WebhookModel, - SettingsModel, -} from './models'; -import { dbserver } from '../db/db.connect'; -import { WebhookRepository } from './repository/webhook.repository'; -import { ChatwootRepository } from './repository/chatwoot.repository'; -import { AuthRepository } from './repository/auth.repository'; -import { WAStartupService } from './services/whatsapp.service'; -import { delay } from '@whiskeysockets/baileys'; -import { Events } from './types/wa.types'; -import { RedisCache } from '../db/redis.client'; -import { SettingsRepository } from './repository/settings.repository'; -import { SettingsService } from './services/settings.service'; import { SettingsController } from './controllers/settings.controller'; +import { ViewsController } from './controllers/views.controller'; +import { WebhookController } from './controllers/webhook.controller'; +import { + AuthModel, + ChatModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + SettingsModel, + WebhookModel, +} from './models'; +import { AuthRepository } from './repository/auth.repository'; +import { ChatRepository } from './repository/chat.repository'; +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 { RepositoryBroker } from './repository/repository.manager'; +import { SettingsRepository } from './repository/settings.repository'; +import { WebhookRepository } from './repository/webhook.repository'; +import { AuthService } from './services/auth.service'; +import { ChatwootService } from './services/chatwoot.service'; +import { WAMonitoringService } from './services/monitor.service'; +import { SettingsService } from './services/settings.service'; +import { WebhookService } from './services/webhook.service'; const logger = new Logger('WA MODULE'); @@ -51,26 +48,21 @@ const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( - messageRepository, - chatRepository, - contactRepository, - messageUpdateRepository, - webhookRepository, - chatwootRepository, - settingsRepository, - authRepository, - configService, - dbserver?.getClient(), + messageRepository, + chatRepository, + contactRepository, + messageUpdateRepository, + webhookRepository, + chatwootRepository, + settingsRepository, + authRepository, + configService, + dbserver?.getClient(), ); export const cache = new RedisCache(); -export const waMonitor = new WAMonitoringService( - eventEmitter, - configService, - repository, - cache, -); +export const waMonitor = new WAMonitoringService(eventEmitter, configService, repository, cache); const authService = new AuthService(configService, waMonitor, repository); @@ -87,15 +79,15 @@ const settingsService = new SettingsService(waMonitor); export const settingsController = new SettingsController(settingsService); export const instanceController = new InstanceController( - waMonitor, - configService, - repository, - eventEmitter, - authService, - webhookService, - chatwootService, - settingsService, - cache, + waMonitor, + configService, + repository, + eventEmitter, + authService, + webhookService, + chatwootService, + settingsService, + cache, ); export const viewsController = new ViewsController(waMonitor, configService); export const sendMessageController = new SendMessageController(waMonitor); From 0a851b935e6746c99aca79f7b0cab0d856e2c256 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 11:10:23 -0300 Subject: [PATCH 73/97] Update .gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 69d60b7a..2364b077 100644 --- a/.gitignore +++ b/.gitignore @@ -22,12 +22,13 @@ docker-compose.yaml /yarn.lock /package-lock.json -# IDE - VSCode +# IDEs .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +.nova/* # Prisma /prisma/migrations @@ -40,3 +41,6 @@ docker-compose.yaml /store /temp/* + +.DS_Store +*.DS_Store \ No newline at end of file From dc3d59bae1dfbab1354586f964688611083905d3 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 11:12:00 -0300 Subject: [PATCH 74/97] wip --- .prettierrc.js | 2 +- src/config/env.config.ts | 394 +- src/config/error.config.ts | 28 +- src/config/event.config.ts | 6 +- src/config/logger.config.ts | 210 +- src/db/db.connect.ts | 26 +- src/db/redis.client.ts | 168 +- src/exceptions/400.exception.ts | 14 +- src/exceptions/401.exception.ts | 14 +- src/exceptions/403.exception.ts | 14 +- src/exceptions/404.exception.ts | 14 +- src/exceptions/500.exception.ts | 14 +- src/main.ts | 136 +- src/utils/server-up.ts | 34 +- src/utils/use-multi-file-auth-state-db.ts | 152 +- .../use-multi-file-auth-state-redis-db.ts | 132 +- src/validate/validate.schema.ts | 1422 ++--- src/whatsapp/abstract/abstract.repository.ts | 80 +- src/whatsapp/abstract/abstract.router.ts | 359 +- src/whatsapp/controllers/chat.controller.ts | 166 +- .../controllers/chatwoot.controller.ts | 144 +- src/whatsapp/controllers/group.controller.ts | 144 +- .../controllers/instance.controller.ts | 608 +- .../controllers/sendMessage.controller.ts | 162 +- .../controllers/settings.controller.ts | 18 +- src/whatsapp/controllers/views.controller.ts | 26 +- .../controllers/webhook.controller.ts | 34 +- src/whatsapp/dto/chat.dto.ts | 78 +- src/whatsapp/dto/chatwoot.dto.ts | 18 +- src/whatsapp/dto/group.dto.ts | 40 +- src/whatsapp/dto/instance.dto.ts | 38 +- src/whatsapp/dto/sendMessage.dto.ts | 144 +- src/whatsapp/dto/settings.dto.ts | 12 +- src/whatsapp/dto/webhook.dto.ts | 8 +- src/whatsapp/guards/auth.guard.ts | 110 +- src/whatsapp/guards/instance.guard.ts | 72 +- src/whatsapp/models/auth.model.ts | 12 +- src/whatsapp/models/chat.model.ts | 14 +- src/whatsapp/models/chatwoot.model.ts | 36 +- src/whatsapp/models/contact.model.ts | 20 +- src/whatsapp/models/message.model.ts | 88 +- src/whatsapp/models/settings.model.ts | 28 +- src/whatsapp/models/webhook.model.ts | 20 +- src/whatsapp/repository/auth.repository.ts | 98 +- src/whatsapp/repository/chat.repository.ts | 172 +- .../repository/chatwoot.repository.ts | 98 +- src/whatsapp/repository/contact.repository.ts | 266 +- src/whatsapp/repository/message.repository.ts | 244 +- .../repository/messageUp.repository.ts | 183 +- src/whatsapp/repository/repository.manager.ts | 188 +- .../repository/settings.repository.ts | 98 +- src/whatsapp/repository/webhook.repository.ts | 96 +- src/whatsapp/routers/chat.router.ts | 560 +- src/whatsapp/routers/chatwoot.router.ts | 94 +- src/whatsapp/routers/group.router.ts | 474 +- src/whatsapp/routers/index.router.ts | 42 +- src/whatsapp/routers/instance.router.ts | 310 +- src/whatsapp/routers/sendMessage.router.ts | 362 +- src/whatsapp/routers/settings.router.ts | 66 +- src/whatsapp/routers/view.router.ts | 14 +- src/whatsapp/routers/webhook.router.ts | 66 +- src/whatsapp/services/auth.service.ts | 298 +- src/whatsapp/services/chatwoot.service.ts | 2777 +++++---- src/whatsapp/services/monitor.service.ts | 610 +- src/whatsapp/services/settings.service.ts | 42 +- src/whatsapp/services/webhook.service.ts | 42 +- src/whatsapp/services/whatsapp.service.ts | 5477 ++++++++--------- src/whatsapp/types/wa.types.ts | 142 +- src/whatsapp/whatsapp.module.ts | 54 +- 69 files changed, 9028 insertions(+), 9104 deletions(-) diff --git a/.prettierrc.js b/.prettierrc.js index 362adbcb..f55f3f06 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -4,7 +4,7 @@ module.exports = { singleQuote: true, printWidth: 120, arrowParens: 'always', - tabWidth: 4, + tabWidth: 2, useTabs: false, bracketSameLine: false, bracketSpacing: true, diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 4e5871e1..b90141ff 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -7,9 +7,9 @@ export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type Cors = { - ORIGIN: string[]; - METHODS: HttpMethods[]; - CREDENTIALS: boolean; + ORIGIN: string[]; + METHODS: HttpMethods[]; + CREDENTIALS: boolean; }; export type LogBaileys = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; @@ -17,90 +17,90 @@ export type LogBaileys = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' export type LogLevel = 'ERROR' | 'WARN' | 'DEBUG' | 'INFO' | 'LOG' | 'VERBOSE' | 'DARK' | 'WEBHOOKS'; export type Log = { - LEVEL: LogLevel[]; - COLOR: boolean; - BAILEYS: LogBaileys; + LEVEL: LogLevel[]; + COLOR: boolean; + BAILEYS: LogBaileys; }; export type SaveData = { - INSTANCE: boolean; - NEW_MESSAGE: boolean; - MESSAGE_UPDATE: boolean; - CONTACTS: boolean; - CHATS: boolean; + INSTANCE: boolean; + NEW_MESSAGE: boolean; + MESSAGE_UPDATE: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type StoreConf = { - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type CleanStoreConf = { - CLEANING_INTERVAL: number; - MESSAGES: boolean; - MESSAGE_UP: boolean; - CONTACTS: boolean; - CHATS: boolean; + CLEANING_INTERVAL: number; + MESSAGES: boolean; + MESSAGE_UP: boolean; + CONTACTS: boolean; + CHATS: boolean; }; export type DBConnection = { - URI: string; - DB_PREFIX_NAME: string; + URI: string; + DB_PREFIX_NAME: string; }; export type Database = { - CONNECTION: DBConnection; - ENABLED: boolean; - SAVE_DATA: SaveData; + CONNECTION: DBConnection; + ENABLED: boolean; + SAVE_DATA: SaveData; }; export type Redis = { - ENABLED: boolean; - URI: string; - PREFIX_KEY: string; + ENABLED: boolean; + URI: string; + PREFIX_KEY: string; }; export type EventsWebhook = { - APPLICATION_STARTUP: boolean; - QRCODE_UPDATED: boolean; - MESSAGES_SET: boolean; - MESSAGES_UPSERT: boolean; - MESSAGES_UPDATE: boolean; - MESSAGES_DELETE: boolean; - SEND_MESSAGE: boolean; - CONTACTS_SET: boolean; - CONTACTS_UPDATE: boolean; - CONTACTS_UPSERT: boolean; - PRESENCE_UPDATE: boolean; - CHATS_SET: boolean; - CHATS_UPDATE: boolean; - CHATS_DELETE: boolean; - CHATS_UPSERT: boolean; - CONNECTION_UPDATE: boolean; - GROUPS_UPSERT: boolean; - GROUP_UPDATE: boolean; - GROUP_PARTICIPANTS_UPDATE: boolean; - CALL: boolean; - NEW_JWT_TOKEN: boolean; + APPLICATION_STARTUP: boolean; + QRCODE_UPDATED: boolean; + MESSAGES_SET: boolean; + MESSAGES_UPSERT: boolean; + MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; + SEND_MESSAGE: boolean; + CONTACTS_SET: boolean; + CONTACTS_UPDATE: boolean; + CONTACTS_UPSERT: boolean; + PRESENCE_UPDATE: boolean; + CHATS_SET: boolean; + CHATS_UPDATE: boolean; + CHATS_DELETE: boolean; + CHATS_UPSERT: boolean; + CONNECTION_UPDATE: boolean; + GROUPS_UPSERT: boolean; + GROUP_UPDATE: boolean; + GROUP_PARTICIPANTS_UPDATE: boolean; + CALL: boolean; + NEW_JWT_TOKEN: boolean; }; export type ApiKey = { KEY: string }; export type Jwt = { EXPIRIN_IN: number; SECRET: string }; export type Auth = { - API_KEY: ApiKey; - EXPOSE_IN_FETCH_INSTANCES: boolean; - JWT: Jwt; - TYPE: 'jwt' | 'apikey'; + API_KEY: ApiKey; + EXPOSE_IN_FETCH_INSTANCES: boolean; + JWT: Jwt; + TYPE: 'jwt' | 'apikey'; }; export type DelInstance = number | boolean; export type GlobalWebhook = { - URL: string; - ENABLED: boolean; - WEBHOOK_BY_EVENTS: boolean; + URL: string; + ENABLED: boolean; + WEBHOOK_BY_EVENTS: boolean; }; export type SslConf = { PRIVKEY: string; FULLCHAIN: string }; export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook }; @@ -109,158 +109,158 @@ export type QrCode = { LIMIT: number }; export type Production = boolean; export interface Env { - SERVER: HttpServer; - CORS: Cors; - SSL_CONF: SslConf; - STORE: StoreConf; - CLEAN_STORE: CleanStoreConf; - DATABASE: Database; - REDIS: Redis; - LOG: Log; - DEL_INSTANCE: DelInstance; - WEBHOOK: Webhook; - CONFIG_SESSION_PHONE: ConfigSessionPhone; - QRCODE: QrCode; - AUTHENTICATION: Auth; - PRODUCTION?: Production; + SERVER: HttpServer; + CORS: Cors; + SSL_CONF: SslConf; + STORE: StoreConf; + CLEAN_STORE: CleanStoreConf; + DATABASE: Database; + REDIS: Redis; + LOG: Log; + DEL_INSTANCE: DelInstance; + WEBHOOK: Webhook; + CONFIG_SESSION_PHONE: ConfigSessionPhone; + QRCODE: QrCode; + AUTHENTICATION: Auth; + PRODUCTION?: Production; } export type Key = keyof Env; export class ConfigService { - constructor() { - this.loadEnv(); - } + constructor() { + this.loadEnv(); + } - private env: Env; + private env: Env; - public get(key: Key) { - return this.env[key] as T; - } + public get(key: Key) { + return this.env[key] as T; + } - private loadEnv() { - this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); - this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; - if (process.env?.DOCKER_ENV === 'true') { - this.env.SERVER.TYPE = 'http'; - this.env.SERVER.PORT = 8080; - } + private loadEnv() { + this.env = !(process.env?.DOCKER_ENV === 'true') ? this.envYaml() : this.envProcess(); + this.env.PRODUCTION = process.env?.NODE_ENV === 'PROD'; + if (process.env?.DOCKER_ENV === 'true') { + this.env.SERVER.TYPE = 'http'; + this.env.SERVER.PORT = 8080; } + } - private envYaml(): Env { - return load(readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' })) as Env; - } + private envYaml(): Env { + return load(readFileSync(join(process.cwd(), 'src', 'env.yml'), { encoding: 'utf-8' })) as Env; + } - private envProcess(): Env { - return { - SERVER: { - TYPE: process.env.SERVER_TYPE as 'http' | 'https', - PORT: Number.parseInt(process.env.SERVER_PORT), - URL: process.env.SERVER_URL, - }, - CORS: { - ORIGIN: process.env.CORS_ORIGIN.split(','), - METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], - CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', - }, - SSL_CONF: { - PRIVKEY: process.env?.SSL_CONF_PRIVKEY, - FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, - }, - STORE: { - MESSAGES: process.env?.STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.STORE_CONTACTS === 'true', - CHATS: process.env?.STORE_CHATS === 'true', - }, - CLEAN_STORE: { - CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) - ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) - : 7200, - MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', - MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', - CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', - CHATS: process.env?.CLEAN_STORE_CHATS === 'true', - }, - DATABASE: { - CONNECTION: { - URI: process.env.DATABASE_CONNECTION_URI, - DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, - }, - ENABLED: process.env?.DATABASE_ENABLED === 'true', - SAVE_DATA: { - INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', - NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', - MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', - CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', - CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', - }, - }, - REDIS: { - ENABLED: process.env?.REDIS_ENABLED === 'true', - URI: process.env.REDIS_URI, - PREFIX_KEY: process.env.REDIS_PREFIX_KEY, - }, - LOG: { - LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], - COLOR: process.env?.LOG_COLOR === 'true', - BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', - }, - DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) - ? process.env.DEL_INSTANCE === 'true' - : Number.parseInt(process.env.DEL_INSTANCE) || false, - WEBHOOK: { - GLOBAL: { - URL: process.env?.WEBHOOK_GLOBAL_URL, - ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', - WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', - }, - EVENTS: { - APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', - QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', - MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', - MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', - MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', - MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', - SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', - CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', - CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', - CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', - PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', - CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', - CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', - CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', - CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', - CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', - GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', - GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', - GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', - CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', - NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', - }, - }, - CONFIG_SESSION_PHONE: { - CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', - NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', - }, - QRCODE: { - LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, - }, - AUTHENTICATION: { - TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', - API_KEY: { - KEY: process.env.AUTHENTICATION_API_KEY, - }, - EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', - JWT: { - EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) - ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) - : 3600, - SECRET: process.env.AUTHENTICATION_JWT_SECRET, - }, - }, - }; - } + private envProcess(): Env { + return { + SERVER: { + TYPE: process.env.SERVER_TYPE as 'http' | 'https', + PORT: Number.parseInt(process.env.SERVER_PORT), + URL: process.env.SERVER_URL, + }, + CORS: { + ORIGIN: process.env.CORS_ORIGIN.split(','), + METHODS: process.env.CORS_METHODS.split(',') as HttpMethods[], + CREDENTIALS: process.env?.CORS_CREDENTIALS === 'true', + }, + SSL_CONF: { + PRIVKEY: process.env?.SSL_CONF_PRIVKEY, + FULLCHAIN: process.env?.SSL_CONF_FULLCHAIN, + }, + STORE: { + MESSAGES: process.env?.STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.STORE_CONTACTS === 'true', + CHATS: process.env?.STORE_CHATS === 'true', + }, + CLEAN_STORE: { + CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) + ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) + : 7200, + MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', + MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', + CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', + CHATS: process.env?.CLEAN_STORE_CHATS === 'true', + }, + DATABASE: { + CONNECTION: { + URI: process.env.DATABASE_CONNECTION_URI, + DB_PREFIX_NAME: process.env.DATABASE_CONNECTION_DB_PREFIX_NAME, + }, + ENABLED: process.env?.DATABASE_ENABLED === 'true', + SAVE_DATA: { + INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', + NEW_MESSAGE: process.env?.DATABASE_SAVE_DATA_NEW_MESSAGE === 'true', + MESSAGE_UPDATE: process.env?.DATABASE_SAVE_MESSAGE_UPDATE === 'true', + CONTACTS: process.env?.DATABASE_SAVE_DATA_CONTACTS === 'true', + CHATS: process.env?.DATABASE_SAVE_DATA_CHATS === 'true', + }, + }, + REDIS: { + ENABLED: process.env?.REDIS_ENABLED === 'true', + URI: process.env.REDIS_URI, + PREFIX_KEY: process.env.REDIS_PREFIX_KEY, + }, + LOG: { + LEVEL: process.env?.LOG_LEVEL.split(',') as LogLevel[], + COLOR: process.env?.LOG_COLOR === 'true', + BAILEYS: (process.env?.LOG_BAILEYS as LogBaileys) || 'error', + }, + DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) + ? process.env.DEL_INSTANCE === 'true' + : Number.parseInt(process.env.DEL_INSTANCE) || false, + WEBHOOK: { + GLOBAL: { + URL: process.env?.WEBHOOK_GLOBAL_URL, + ENABLED: process.env?.WEBHOOK_GLOBAL_ENABLED === 'true', + WEBHOOK_BY_EVENTS: process.env?.WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS === 'true', + }, + EVENTS: { + APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', + QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', + MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', + MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', + MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', + MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true', + SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', + CONTACTS_SET: process.env?.WEBHOOK_EVENTS_CONTACTS_SET === 'true', + CONTACTS_UPDATE: process.env?.WEBHOOK_EVENTS_CONTACTS_UPDATE === 'true', + CONTACTS_UPSERT: process.env?.WEBHOOK_EVENTS_CONTACTS_UPSERT === 'true', + PRESENCE_UPDATE: process.env?.WEBHOOK_EVENTS_PRESENCE_UPDATE === 'true', + CHATS_SET: process.env?.WEBHOOK_EVENTS_CHATS_SET === 'true', + CHATS_UPDATE: process.env?.WEBHOOK_EVENTS_CHATS_UPDATE === 'true', + CHATS_UPSERT: process.env?.WEBHOOK_EVENTS_CHATS_UPSERT === 'true', + CHATS_DELETE: process.env?.WEBHOOK_EVENTS_CHATS_DELETE === 'true', + CONNECTION_UPDATE: process.env?.WEBHOOK_EVENTS_CONNECTION_UPDATE === 'true', + GROUPS_UPSERT: process.env?.WEBHOOK_EVENTS_GROUPS_UPSERT === 'true', + GROUP_UPDATE: process.env?.WEBHOOK_EVENTS_GROUPS_UPDATE === 'true', + GROUP_PARTICIPANTS_UPDATE: process.env?.WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', + NEW_JWT_TOKEN: process.env?.WEBHOOK_EVENTS_NEW_JWT_TOKEN === 'true', + }, + }, + CONFIG_SESSION_PHONE: { + CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API', + NAME: process.env?.CONFIG_SESSION_PHONE_NAME || 'chrome', + }, + QRCODE: { + LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, + }, + AUTHENTICATION: { + TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', + API_KEY: { + KEY: process.env.AUTHENTICATION_API_KEY, + }, + EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', + JWT: { + EXPIRIN_IN: Number.isInteger(process.env?.AUTHENTICATION_JWT_EXPIRIN_IN) + ? Number.parseInt(process.env.AUTHENTICATION_JWT_EXPIRIN_IN) + : 3600, + SECRET: process.env.AUTHENTICATION_JWT_SECRET, + }, + }, + }; + } } export const configService = new ConfigService(); diff --git a/src/config/error.config.ts b/src/config/error.config.ts index 999205c4..6449d52e 100644 --- a/src/config/error.config.ts +++ b/src/config/error.config.ts @@ -1,21 +1,21 @@ import { Logger } from './logger.config'; export function onUnexpectedError() { - process.on('uncaughtException', (error, origin) => { - const logger = new Logger('uncaughtException'); - logger.error({ - origin, - stderr: process.stderr.fd, - error, - }); + process.on('uncaughtException', (error, origin) => { + const logger = new Logger('uncaughtException'); + logger.error({ + origin, + stderr: process.stderr.fd, + error, }); + }); - process.on('unhandledRejection', (error, origin) => { - const logger = new Logger('unhandledRejection'); - logger.error({ - origin, - stderr: process.stderr.fd, - error, - }); + process.on('unhandledRejection', (error, origin) => { + const logger = new Logger('unhandledRejection'); + logger.error({ + origin, + stderr: process.stderr.fd, + error, }); + }); } diff --git a/src/config/event.config.ts b/src/config/event.config.ts index ec917110..8451ffdf 100644 --- a/src/config/event.config.ts +++ b/src/config/event.config.ts @@ -1,7 +1,7 @@ import EventEmitter2 from 'eventemitter2'; export const eventEmitter = new EventEmitter2({ - delimiter: '.', - newListener: false, - ignoreErrors: false, + delimiter: '.', + newListener: false, + ignoreErrors: false, }); diff --git a/src/config/logger.config.ts b/src/config/logger.config.ts index 9fe6bac2..a5ca6a23 100644 --- a/src/config/logger.config.ts +++ b/src/config/logger.config.ts @@ -3,135 +3,135 @@ import dayjs from 'dayjs'; import { configService, Log } from './env.config'; const formatDateLog = (timestamp: number) => - dayjs(timestamp) - .toDate() - .toString() - .replace(/\sGMT.+/, ''); + dayjs(timestamp) + .toDate() + .toString() + .replace(/\sGMT.+/, ''); enum Color { - LOG = '\x1b[32m', - INFO = '\x1b[34m', - WARN = '\x1b[33m', - ERROR = '\x1b[31m', - DEBUG = '\x1b[36m', - VERBOSE = '\x1b[37m', - DARK = '\x1b[30m', + LOG = '\x1b[32m', + INFO = '\x1b[34m', + WARN = '\x1b[33m', + ERROR = '\x1b[31m', + DEBUG = '\x1b[36m', + VERBOSE = '\x1b[37m', + DARK = '\x1b[30m', } enum Command { - RESET = '\x1b[0m', - BRIGHT = '\x1b[1m', - UNDERSCORE = '\x1b[4m', + RESET = '\x1b[0m', + BRIGHT = '\x1b[1m', + UNDERSCORE = '\x1b[4m', } enum Level { - LOG = Color.LOG + '%s' + Command.RESET, - DARK = Color.DARK + '%s' + Command.RESET, - INFO = Color.INFO + '%s' + Command.RESET, - WARN = Color.WARN + '%s' + Command.RESET, - ERROR = Color.ERROR + '%s' + Command.RESET, - DEBUG = Color.DEBUG + '%s' + Command.RESET, - VERBOSE = Color.VERBOSE + '%s' + Command.RESET, + LOG = Color.LOG + '%s' + Command.RESET, + DARK = Color.DARK + '%s' + Command.RESET, + INFO = Color.INFO + '%s' + Command.RESET, + WARN = Color.WARN + '%s' + Command.RESET, + ERROR = Color.ERROR + '%s' + Command.RESET, + DEBUG = Color.DEBUG + '%s' + Command.RESET, + VERBOSE = Color.VERBOSE + '%s' + Command.RESET, } enum Type { - LOG = 'LOG', - WARN = 'WARN', - INFO = 'INFO', - DARK = 'DARK', - ERROR = 'ERROR', - DEBUG = 'DEBUG', - VERBOSE = 'VERBOSE', + LOG = 'LOG', + WARN = 'WARN', + INFO = 'INFO', + DARK = 'DARK', + ERROR = 'ERROR', + DEBUG = 'DEBUG', + VERBOSE = 'VERBOSE', } enum Background { - LOG = '\x1b[42m', - INFO = '\x1b[44m', - WARN = '\x1b[43m', - DARK = '\x1b[40m', - ERROR = '\x1b[41m', - DEBUG = '\x1b[46m', - VERBOSE = '\x1b[47m', + LOG = '\x1b[42m', + INFO = '\x1b[44m', + WARN = '\x1b[43m', + DARK = '\x1b[40m', + ERROR = '\x1b[41m', + DEBUG = '\x1b[46m', + VERBOSE = '\x1b[47m', } export class Logger { - private readonly configService = configService; - constructor(private context = 'Logger') {} + private readonly configService = configService; + constructor(private context = 'Logger') {} - public setContext(value: string) { - this.context = value; + public setContext(value: string) { + this.context = value; + } + + private console(value: any, type: Type) { + const types: Type[] = []; + + this.configService.get('LOG').LEVEL.forEach((level) => types.push(Type[level])); + + const typeValue = typeof value; + if (types.includes(type)) { + if (configService.get('LOG').COLOR) { + console.log( + /*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type], + '[Evolution API]', + Command.BRIGHT + Color[type], + process.pid.toString(), + Command.RESET, + Command.BRIGHT + Color[type], + '-', + Command.BRIGHT + Color.VERBOSE, + `${formatDateLog(Date.now())} `, + Command.RESET, + Color[type] + Background[type] + Command.BRIGHT, + `${type} ` + Command.RESET, + Color.WARN + Command.BRIGHT, + `[${this.context}]` + Command.RESET, + Color[type] + Command.BRIGHT, + `[${typeValue}]` + Command.RESET, + Color[type], + typeValue !== 'object' ? value : '', + Command.RESET, + ); + typeValue === 'object' ? console.log(/*Level.DARK,*/ value, '\n') : ''; + } else { + console.log( + '[Evolution API]', + process.pid.toString(), + '-', + `${formatDateLog(Date.now())} `, + `${type} `, + `[${this.context}]`, + `[${typeValue}]`, + value, + ); + } } + } - private console(value: any, type: Type) { - const types: Type[] = []; + public log(value: any) { + this.console(value, Type.LOG); + } - this.configService.get('LOG').LEVEL.forEach((level) => types.push(Type[level])); + public info(value: any) { + this.console(value, Type.INFO); + } - const typeValue = typeof value; - if (types.includes(type)) { - if (configService.get('LOG').COLOR) { - console.log( - /*Command.UNDERSCORE +*/ Command.BRIGHT + Level[type], - '[Evolution API]', - Command.BRIGHT + Color[type], - process.pid.toString(), - Command.RESET, - Command.BRIGHT + Color[type], - '-', - Command.BRIGHT + Color.VERBOSE, - `${formatDateLog(Date.now())} `, - Command.RESET, - Color[type] + Background[type] + Command.BRIGHT, - `${type} ` + Command.RESET, - Color.WARN + Command.BRIGHT, - `[${this.context}]` + Command.RESET, - Color[type] + Command.BRIGHT, - `[${typeValue}]` + Command.RESET, - Color[type], - typeValue !== 'object' ? value : '', - Command.RESET, - ); - typeValue === 'object' ? console.log(/*Level.DARK,*/ value, '\n') : ''; - } else { - console.log( - '[Evolution API]', - process.pid.toString(), - '-', - `${formatDateLog(Date.now())} `, - `${type} `, - `[${this.context}]`, - `[${typeValue}]`, - value, - ); - } - } - } + public warn(value: any) { + this.console(value, Type.WARN); + } - public log(value: any) { - this.console(value, Type.LOG); - } + public error(value: any) { + this.console(value, Type.ERROR); + } - public info(value: any) { - this.console(value, Type.INFO); - } + public verbose(value: any) { + this.console(value, Type.VERBOSE); + } - public warn(value: any) { - this.console(value, Type.WARN); - } + public debug(value: any) { + this.console(value, Type.DEBUG); + } - public error(value: any) { - this.console(value, Type.ERROR); - } - - public verbose(value: any) { - this.console(value, Type.VERBOSE); - } - - public debug(value: any) { - this.console(value, Type.DEBUG); - } - - public dark(value: any) { - this.console(value, Type.DARK); - } + public dark(value: any) { + this.console(value, Type.DARK); + } } diff --git a/src/db/db.connect.ts b/src/db/db.connect.ts index e7e965ac..b11610c7 100644 --- a/src/db/db.connect.ts +++ b/src/db/db.connect.ts @@ -7,19 +7,19 @@ const logger = new Logger('MongoDB'); const db = configService.get('DATABASE'); export const dbserver = (() => { - if (db.ENABLED) { - logger.verbose('connecting'); - const dbs = mongoose.createConnection(db.CONNECTION.URI, { - dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api', - }); - logger.verbose('connected in ' + db.CONNECTION.URI); - logger.info('ON - dbName: ' + dbs['$dbName']); + if (db.ENABLED) { + logger.verbose('connecting'); + const dbs = mongoose.createConnection(db.CONNECTION.URI, { + dbName: db.CONNECTION.DB_PREFIX_NAME + '-whatsapp-api', + }); + logger.verbose('connected in ' + db.CONNECTION.URI); + logger.info('ON - dbName: ' + dbs['$dbName']); - process.on('beforeExit', () => { - logger.verbose('instance destroyed'); - dbserver.destroy(true, (error) => logger.error(error)); - }); + process.on('beforeExit', () => { + logger.verbose('instance destroyed'); + dbserver.destroy(true, (error) => logger.error(error)); + }); - return dbs; - } + return dbs; + } })(); diff --git a/src/db/redis.client.ts b/src/db/redis.client.ts index 5f0604bc..dffeb949 100644 --- a/src/db/redis.client.ts +++ b/src/db/redis.client.ts @@ -5,101 +5,101 @@ import { Redis } from '../config/env.config'; import { Logger } from '../config/logger.config'; export class RedisCache { - constructor() { - this.logger.verbose('instance created'); - process.on('beforeExit', async () => { - this.logger.verbose('instance destroyed'); - if (this.statusConnection) { - this.logger.verbose('instance disconnect'); - await this.client.disconnect(); - } - }); + constructor() { + this.logger.verbose('instance created'); + process.on('beforeExit', async () => { + this.logger.verbose('instance destroyed'); + if (this.statusConnection) { + this.logger.verbose('instance disconnect'); + await this.client.disconnect(); + } + }); + } + + private statusConnection = false; + private instanceName: string; + private redisEnv: Redis; + + public set reference(reference: string) { + this.logger.verbose('set reference: ' + reference); + this.instanceName = reference; + } + + public async connect(redisEnv: Redis) { + this.logger.verbose('connecting'); + this.client = createClient({ url: redisEnv.URI }); + this.logger.verbose('connected in ' + redisEnv.URI); + await this.client.connect(); + this.statusConnection = true; + this.redisEnv = redisEnv; + } + + private readonly logger = new Logger(RedisCache.name); + private client: RedisClientType; + + public async instanceKeys(): Promise { + try { + this.logger.verbose('instance keys: ' + this.redisEnv.PREFIX_KEY + ':*'); + return await this.client.sendCommand(['keys', this.redisEnv.PREFIX_KEY + ':*']); + } catch (error) { + this.logger.error(error); } + } - private statusConnection = false; - private instanceName: string; - private redisEnv: Redis; - - public set reference(reference: string) { - this.logger.verbose('set reference: ' + reference); - this.instanceName = reference; + public async keyExists(key?: string) { + if (key) { + this.logger.verbose('keyExists: ' + key); + return !!(await this.instanceKeys()).find((i) => i === key); } + this.logger.verbose('keyExists: ' + this.instanceName); + return !!(await this.instanceKeys()).find((i) => i === this.instanceName); + } - public async connect(redisEnv: Redis) { - this.logger.verbose('connecting'); - this.client = createClient({ url: redisEnv.URI }); - this.logger.verbose('connected in ' + redisEnv.URI); - await this.client.connect(); - this.statusConnection = true; - this.redisEnv = redisEnv; + public async writeData(field: string, data: any) { + try { + this.logger.verbose('writeData: ' + field); + const json = JSON.stringify(data, BufferJSON.replacer); + + return await this.client.hSet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field, json); + } catch (error) { + this.logger.error(error); } + } - private readonly logger = new Logger(RedisCache.name); - private client: RedisClientType; + public async readData(field: string) { + try { + this.logger.verbose('readData: ' + field); + const data = await this.client.hGet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); - public async instanceKeys(): Promise { - try { - this.logger.verbose('instance keys: ' + this.redisEnv.PREFIX_KEY + ':*'); - return await this.client.sendCommand(['keys', this.redisEnv.PREFIX_KEY + ':*']); - } catch (error) { - this.logger.error(error); - } + if (data) { + this.logger.verbose('readData: ' + field + ' success'); + return JSON.parse(data, BufferJSON.reviver); + } + + this.logger.verbose('readData: ' + field + ' not found'); + return null; + } catch (error) { + this.logger.error(error); } + } - public async keyExists(key?: string) { - if (key) { - this.logger.verbose('keyExists: ' + key); - return !!(await this.instanceKeys()).find((i) => i === key); - } - this.logger.verbose('keyExists: ' + this.instanceName); - return !!(await this.instanceKeys()).find((i) => i === this.instanceName); + public async removeData(field: string) { + try { + this.logger.verbose('removeData: ' + field); + return await this.client.hDel(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); + } catch (error) { + this.logger.error(error); } + } - public async writeData(field: string, data: any) { - try { - this.logger.verbose('writeData: ' + field); - const json = JSON.stringify(data, BufferJSON.replacer); + public async delAll(hash?: string) { + try { + this.logger.verbose('instance delAll: ' + hash); + const result = await this.client.del(hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName); - return await this.client.hSet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field, json); - } catch (error) { - this.logger.error(error); - } - } - - public async readData(field: string) { - try { - this.logger.verbose('readData: ' + field); - const data = await this.client.hGet(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); - - if (data) { - this.logger.verbose('readData: ' + field + ' success'); - return JSON.parse(data, BufferJSON.reviver); - } - - this.logger.verbose('readData: ' + field + ' not found'); - return null; - } catch (error) { - this.logger.error(error); - } - } - - public async removeData(field: string) { - try { - this.logger.verbose('removeData: ' + field); - return await this.client.hDel(this.redisEnv.PREFIX_KEY + ':' + this.instanceName, field); - } catch (error) { - this.logger.error(error); - } - } - - public async delAll(hash?: string) { - try { - this.logger.verbose('instance delAll: ' + hash); - const result = await this.client.del(hash || this.redisEnv.PREFIX_KEY + ':' + this.instanceName); - - return result; - } catch (error) { - this.logger.error(error); - } + return result; + } catch (error) { + this.logger.error(error); } + } } diff --git a/src/exceptions/400.exception.ts b/src/exceptions/400.exception.ts index 8b5c9c16..833295c1 100644 --- a/src/exceptions/400.exception.ts +++ b/src/exceptions/400.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class BadRequestException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.BAD_REQUEST, - error: 'Bad Request', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.BAD_REQUEST, + error: 'Bad Request', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/401.exception.ts b/src/exceptions/401.exception.ts index 97724396..72734d4e 100644 --- a/src/exceptions/401.exception.ts +++ b/src/exceptions/401.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class UnauthorizedException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.UNAUTHORIZED, - error: 'Unauthorized', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.UNAUTHORIZED, + error: 'Unauthorized', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/403.exception.ts b/src/exceptions/403.exception.ts index 9670fe25..f53ca9a5 100644 --- a/src/exceptions/403.exception.ts +++ b/src/exceptions/403.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class ForbiddenException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.FORBIDDEN, - error: 'Forbidden', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.FORBIDDEN, + error: 'Forbidden', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/404.exception.ts b/src/exceptions/404.exception.ts index 44181fe5..1119d1a1 100644 --- a/src/exceptions/404.exception.ts +++ b/src/exceptions/404.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class NotFoundException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.NOT_FOUND, - error: 'Not Found', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.NOT_FOUND, + error: 'Not Found', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/exceptions/500.exception.ts b/src/exceptions/500.exception.ts index 88d70573..2a41dfa5 100644 --- a/src/exceptions/500.exception.ts +++ b/src/exceptions/500.exception.ts @@ -1,11 +1,11 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class InternalServerErrorException { - constructor(...objectError: any[]) { - throw { - status: HttpStatus.INTERNAL_SERVER_ERROR, - error: 'Internal Server Error', - message: objectError.length > 0 ? objectError : undefined, - }; - } + constructor(...objectError: any[]) { + throw { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: 'Internal Server Error', + message: objectError.length > 0 ? objectError : undefined, + }; + } } diff --git a/src/main.ts b/src/main.ts index 5d70661e..b0d2e03e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,94 +15,94 @@ import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { - waMonitor.loadInstance(); + waMonitor.loadInstance(); } function bootstrap() { - const logger = new Logger('SERVER'); - const app = express(); + const logger = new Logger('SERVER'); + const app = express(); - // Sentry.init({ - // dsn: '', - // integrations: [ - // // enable HTTP calls tracing - // new Sentry.Integrations.Http({ tracing: true }), - // // enable Express.js middleware tracing - // new Sentry.Integrations.Express({ app }), - // // Automatically instrument Node.js libraries and frameworks - // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), - // ], + // Sentry.init({ + // dsn: '', + // integrations: [ + // // enable HTTP calls tracing + // new Sentry.Integrations.Http({ tracing: true }), + // // enable Express.js middleware tracing + // new Sentry.Integrations.Express({ app }), + // // Automatically instrument Node.js libraries and frameworks + // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + // ], - // // Set tracesSampleRate to 1.0 to capture 100% - // // of transactions for performance monitoring. - // // We recommend adjusting this value in production - // tracesSampleRate: 1.0, - // }); + // // Set tracesSampleRate to 1.0 to capture 100% + // // of transactions for performance monitoring. + // // We recommend adjusting this value in production + // tracesSampleRate: 1.0, + // }); - // app.use(Sentry.Handlers.requestHandler()); + // app.use(Sentry.Handlers.requestHandler()); - // app.use(Sentry.Handlers.tracingHandler()); + // app.use(Sentry.Handlers.tracingHandler()); - app.use( - cors({ - origin(requestOrigin, callback) { - const { ORIGIN } = configService.get('CORS'); - !requestOrigin ? (requestOrigin = '*') : undefined; - if (ORIGIN.indexOf(requestOrigin) !== -1) { - return callback(null, true); - } - return callback(new Error('Not allowed by CORS')); - }, - methods: [...configService.get('CORS').METHODS], - credentials: configService.get('CORS').CREDENTIALS, - }), - urlencoded({ extended: true, limit: '136mb' }), - json({ limit: '136mb' }), - compression(), - ); + app.use( + cors({ + origin(requestOrigin, callback) { + const { ORIGIN } = configService.get('CORS'); + !requestOrigin ? (requestOrigin = '*') : undefined; + if (ORIGIN.indexOf(requestOrigin) !== -1) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, + methods: [...configService.get('CORS').METHODS], + credentials: configService.get('CORS').CREDENTIALS, + }), + urlencoded({ extended: true, limit: '136mb' }), + json({ limit: '136mb' }), + compression(), + ); - app.set('view engine', 'hbs'); - app.set('views', join(ROOT_DIR, 'views')); - app.use(express.static(join(ROOT_DIR, 'public'))); + app.set('view engine', 'hbs'); + app.set('views', join(ROOT_DIR, 'views')); + app.use(express.static(join(ROOT_DIR, 'public'))); - app.use('/', router); + app.use('/', router); - // app.use(Sentry.Handlers.errorHandler()); + // app.use(Sentry.Handlers.errorHandler()); - // app.use(function onError(err, req, res, next) { - // res.statusCode = 500; - // res.end(res.sentry + '\n'); - // }); + // app.use(function onError(err, req, res, next) { + // res.statusCode = 500; + // res.end(res.sentry + '\n'); + // }); - app.use( - (err: Error, req: Request, res: Response) => { - if (err) { - return res.status(err['status'] || 500).json(err); - } - }, - (req: Request, res: Response, next: NextFunction) => { - const { method, url } = req; + app.use( + (err: Error, req: Request, res: Response) => { + if (err) { + return res.status(err['status'] || 500).json(err); + } + }, + (req: Request, res: Response, next: NextFunction) => { + const { method, url } = req; - res.status(HttpStatus.NOT_FOUND).json({ - status: HttpStatus.NOT_FOUND, - message: `Cannot ${method.toUpperCase()} ${url}`, - error: 'Not Found', - }); + res.status(HttpStatus.NOT_FOUND).json({ + status: HttpStatus.NOT_FOUND, + message: `Cannot ${method.toUpperCase()} ${url}`, + error: 'Not Found', + }); - next(); - }, - ); + next(); + }, + ); - const httpServer = configService.get('SERVER'); + const httpServer = configService.get('SERVER'); - ServerUP.app = app; - const server = ServerUP[httpServer.TYPE]; + ServerUP.app = app; + const server = ServerUP[httpServer.TYPE]; - server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); + server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); - initWA(); + initWA(); - onUnexpectedError(); + onUnexpectedError(); } bootstrap(); diff --git a/src/utils/server-up.ts b/src/utils/server-up.ts index a6205249..e06caea7 100644 --- a/src/utils/server-up.ts +++ b/src/utils/server-up.ts @@ -6,24 +6,24 @@ import * as https from 'https'; import { configService, SslConf } from '../config/env.config'; export class ServerUP { - static #app: Express; + static #app: Express; - static set app(e: Express) { - this.#app = e; - } + static set app(e: Express) { + this.#app = e; + } - static get https() { - const { FULLCHAIN, PRIVKEY } = configService.get('SSL_CONF'); - return https.createServer( - { - cert: readFileSync(FULLCHAIN), - key: readFileSync(PRIVKEY), - }, - ServerUP.#app, - ); - } + static get https() { + const { FULLCHAIN, PRIVKEY } = configService.get('SSL_CONF'); + return https.createServer( + { + cert: readFileSync(FULLCHAIN), + key: readFileSync(PRIVKEY), + }, + ServerUP.#app, + ); + } - static get http() { - return http.createServer(ServerUP.#app); - } + static get http() { + return http.createServer(ServerUP.#app); + } } diff --git a/src/utils/use-multi-file-auth-state-db.ts b/src/utils/use-multi-file-auth-state-db.ts index a372fbcf..8b0d76e4 100644 --- a/src/utils/use-multi-file-auth-state-db.ts +++ b/src/utils/use-multi-file-auth-state-db.ts @@ -1,10 +1,10 @@ import { - AuthenticationCreds, - AuthenticationState, - BufferJSON, - initAuthCreds, - proto, - SignalDataTypeMap, + AuthenticationCreds, + AuthenticationState, + BufferJSON, + initAuthCreds, + proto, + SignalDataTypeMap, } from '@whiskeysockets/baileys'; import { configService, Database } from '../config/env.config'; @@ -12,86 +12,86 @@ import { Logger } from '../config/logger.config'; import { dbserver } from '../db/db.connect'; export async function useMultiFileAuthStateDb( - coll: string, + coll: string, ): Promise<{ state: AuthenticationState; saveCreds: () => Promise }> { - const logger = new Logger(useMultiFileAuthStateDb.name); + const logger = new Logger(useMultiFileAuthStateDb.name); - const client = dbserver.getClient(); + const client = dbserver.getClient(); - const collection = client - .db(configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(coll); + const collection = client + .db(configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(coll); - const writeData = async (data: any, key: string): Promise => { - try { - await client.connect(); - return await collection.replaceOne({ _id: key }, JSON.parse(JSON.stringify(data, BufferJSON.replacer)), { - upsert: true, - }); - } catch (error) { - logger.error(error); - } - }; + const writeData = async (data: any, key: string): Promise => { + try { + await client.connect(); + return await collection.replaceOne({ _id: key }, JSON.parse(JSON.stringify(data, BufferJSON.replacer)), { + upsert: true, + }); + } catch (error) { + logger.error(error); + } + }; - const readData = async (key: string): Promise => { - try { - await client.connect(); - const data = await collection.findOne({ _id: key }); - const creds = JSON.stringify(data); - return JSON.parse(creds, BufferJSON.reviver); - } catch (error) { - logger.error(error); - } - }; + const readData = async (key: string): Promise => { + try { + await client.connect(); + const data = await collection.findOne({ _id: key }); + const creds = JSON.stringify(data); + return JSON.parse(creds, BufferJSON.reviver); + } catch (error) { + logger.error(error); + } + }; - const removeData = async (key: string) => { - try { - await client.connect(); - return await collection.deleteOne({ _id: key }); - } catch (error) { - logger.error(error); - } - }; + const removeData = async (key: string) => { + try { + await client.connect(); + return await collection.deleteOne({ _id: key }); + } catch (error) { + logger.error(error); + } + }; - const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); + const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); - return { - state: { - creds, - keys: { - get: async (type, ids: string[]) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const data: { [_: string]: SignalDataTypeMap[type] } = {}; - await Promise.all( - ids.map(async (id) => { - let value = await readData(`${type}-${id}`); - if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.fromObject(value); - } + return { + state: { + creds, + keys: { + get: async (type, ids: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: { [_: string]: SignalDataTypeMap[type] } = {}; + await Promise.all( + ids.map(async (id) => { + let value = await readData(`${type}-${id}`); + if (type === 'app-state-sync-key' && value) { + value = proto.Message.AppStateSyncKeyData.fromObject(value); + } - data[id] = value; - }), - ); + data[id] = value; + }), + ); - return data; - }, - set: async (data: any) => { - const tasks: Promise[] = []; - for (const category in data) { - for (const id in data[category]) { - const value = data[category][id]; - const key = `${category}-${id}`; - tasks.push(value ? writeData(value, key) : removeData(key)); - } - } - - await Promise.all(tasks); - }, - }, + return data; }, - saveCreds: async () => { - return writeData(creds, 'creds'); + set: async (data: any) => { + const tasks: Promise[] = []; + for (const category in data) { + for (const id in data[category]) { + const value = data[category][id]; + const key = `${category}-${id}`; + tasks.push(value ? writeData(value, key) : removeData(key)); + } + } + + await Promise.all(tasks); }, - }; + }, + }, + saveCreds: async () => { + return writeData(creds, 'creds'); + }, + }; } diff --git a/src/utils/use-multi-file-auth-state-redis-db.ts b/src/utils/use-multi-file-auth-state-redis-db.ts index f26d7d02..57439ced 100644 --- a/src/utils/use-multi-file-auth-state-redis-db.ts +++ b/src/utils/use-multi-file-auth-state-redis-db.ts @@ -1,84 +1,84 @@ import { - AuthenticationCreds, - AuthenticationState, - initAuthCreds, - proto, - SignalDataTypeMap, + AuthenticationCreds, + AuthenticationState, + initAuthCreds, + proto, + SignalDataTypeMap, } from '@whiskeysockets/baileys'; import { Logger } from '../config/logger.config'; import { RedisCache } from '../db/redis.client'; export async function useMultiFileAuthStateRedisDb(cache: RedisCache): Promise<{ - state: AuthenticationState; - saveCreds: () => Promise; + state: AuthenticationState; + saveCreds: () => Promise; }> { - const logger = new Logger(useMultiFileAuthStateRedisDb.name); + const logger = new Logger(useMultiFileAuthStateRedisDb.name); - const writeData = async (data: any, key: string): Promise => { - try { - return await cache.writeData(key, data); - } catch (error) { - return logger.error({ localError: 'writeData', error }); - } - }; + const writeData = async (data: any, key: string): Promise => { + try { + return await cache.writeData(key, data); + } catch (error) { + return logger.error({ localError: 'writeData', error }); + } + }; - const readData = async (key: string): Promise => { - try { - return await cache.readData(key); - } catch (error) { - logger.error({ readData: 'writeData', error }); - return; - } - }; + const readData = async (key: string): Promise => { + try { + return await cache.readData(key); + } catch (error) { + logger.error({ readData: 'writeData', error }); + return; + } + }; - const removeData = async (key: string) => { - try { - return await cache.removeData(key); - } catch (error) { - logger.error({ readData: 'removeData', error }); - } - }; + const removeData = async (key: string) => { + try { + return await cache.removeData(key); + } catch (error) { + logger.error({ readData: 'removeData', error }); + } + }; - const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); + const creds: AuthenticationCreds = (await readData('creds')) || initAuthCreds(); - return { - state: { - creds, - keys: { - get: async (type, ids: string[]) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const data: { [_: string]: SignalDataTypeMap[type] } = {}; - await Promise.all( - ids.map(async (id) => { - let value = await readData(`${type}-${id}`); - if (type === 'app-state-sync-key' && value) { - value = proto.Message.AppStateSyncKeyData.fromObject(value); - } + return { + state: { + creds, + keys: { + get: async (type, ids: string[]) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const data: { [_: string]: SignalDataTypeMap[type] } = {}; + await Promise.all( + ids.map(async (id) => { + let value = await readData(`${type}-${id}`); + if (type === 'app-state-sync-key' && value) { + value = proto.Message.AppStateSyncKeyData.fromObject(value); + } - data[id] = value; - }), - ); + data[id] = value; + }), + ); - return data; - }, - set: async (data: any) => { - const tasks: Promise[] = []; - for (const category in data) { - for (const id in data[category]) { - const value = data[category][id]; - const key = `${category}-${id}`; - tasks.push(value ? await writeData(value, key) : await removeData(key)); - } - } - - await Promise.all(tasks); - }, - }, + return data; }, - saveCreds: async () => { - return await writeData(creds, 'creds'); + set: async (data: any) => { + const tasks: Promise[] = []; + for (const category in data) { + for (const id in data[category]) { + const value = data[category][id]; + const key = `${category}-${id}`; + tasks.push(value ? await writeData(value, key) : await removeData(key)); + } + } + + await Promise.all(tasks); }, - }; + }, + }, + saveCreds: async () => { + return await writeData(creds, 'creds'); + }, + }; } diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 8e021116..8c1a4667 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -2,896 +2,896 @@ import { JSONSchema7, JSONSchema7Definition } 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 }, - }; + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; }; // Instance Schema export const instanceNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - instanceName: { type: 'string' }, - webhook: { type: 'string' }, - webhook_by_events: { type: 'boolean' }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'CALL', - 'NEW_JWT_TOKEN', - ], - }, - }, - qrcode: { type: 'boolean', enum: [true, false] }, - number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, - token: { type: 'string' }, + $id: v4(), + type: 'object', + properties: { + instanceName: { type: 'string' }, + webhook: { type: 'string' }, + webhook_by_events: { type: 'boolean' }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, }, - ...isNotEmpty('instanceName'), + qrcode: { type: 'boolean', enum: [true, false] }, + number: { type: 'string', pattern: '^\\d+[\\.@\\w-]+' }, + token: { type: 'string' }, + }, + ...isNotEmpty('instanceName'), }; export const oldTokenSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - oldToken: { type: 'string' }, - }, - required: ['oldToken'], - ...isNotEmpty('oldToken'), + $id: v4(), + type: 'object', + properties: { + oldToken: { type: 'string' }, + }, + required: ['oldToken'], + ...isNotEmpty('oldToken'), }; const quotedOptionsSchema: JSONSchema7 = { - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id'], - ...isNotEmpty('id'), - }, - message: { type: 'object' }, + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id'], + ...isNotEmpty('id'), }, + message: { type: 'object' }, + }, }; const mentionsOptionsSchema: JSONSchema7 = { - properties: { - everyOne: { type: 'boolean', enum: [true, false] }, - mentioned: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"mentioned" must be an array of numeric strings', - }, - }, + properties: { + everyOne: { type: 'boolean', enum: [true, false] }, + mentioned: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"mentioned" must be an array of numeric strings', + }, }, + }, }; // Send Message Schema const optionsSchema: JSONSchema7 = { - properties: { - delay: { - type: 'integer', - description: 'Enter a value in milliseconds', - }, - presence: { - type: 'string', - enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], - }, - quoted: { ...quotedOptionsSchema }, - mentions: { ...mentionsOptionsSchema }, + properties: { + delay: { + type: 'integer', + description: 'Enter a value in milliseconds', }, + presence: { + type: 'string', + enum: ['unavailable', 'available', 'composing', 'recording', 'paused'], + }, + quoted: { ...quotedOptionsSchema }, + mentions: { ...mentionsOptionsSchema }, + }, }; const numberDefinition: JSONSchema7Definition = { - type: 'string', - description: 'Invalid format', + type: 'string', + description: 'Invalid format', }; export const textMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - textMessage: { - type: 'object', - properties: { - text: { type: 'string' }, - }, - required: ['text'], - ...isNotEmpty('text'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + textMessage: { + type: 'object', + properties: { + text: { type: 'string' }, + }, + required: ['text'], + ...isNotEmpty('text'), }, - required: ['textMessage', 'number'], + }, + required: ['textMessage', 'number'], }; export const pollMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - pollMessage: { - type: 'object', - properties: { - name: { type: 'string' }, - selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, - values: { - type: 'array', - minItems: 2, - maxItems: 10, - uniqueItems: true, - items: { - type: 'string', - }, - }, - }, - required: ['name', 'selectableCount', 'values'], - ...isNotEmpty('name', 'selectableCount', 'values'), + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + pollMessage: { + type: 'object', + properties: { + name: { type: 'string' }, + selectableCount: { type: 'integer', minimum: 0, maximum: 10 }, + values: { + type: 'array', + minItems: 2, + maxItems: 10, + uniqueItems: true, + items: { + type: 'string', + }, }, + }, + required: ['name', 'selectableCount', 'values'], + ...isNotEmpty('name', 'selectableCount', 'values'), }, - required: ['pollMessage', 'number'], + }, + required: ['pollMessage', 'number'], }; export const statusMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - statusMessage: { - type: 'object', - properties: { - type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, - content: { type: 'string' }, - caption: { type: 'string' }, - backgroundColor: { type: 'string' }, - font: { type: 'integer', minimum: 0, maximum: 5 }, - statusJidList: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - pattern: '^\\d+', - description: '"statusJidList" must be an array of numeric strings', - }, - }, - allContacts: { type: 'boolean', enum: [true, false] }, - }, - required: ['type', 'content'], - ...isNotEmpty('type', 'content'), + $id: v4(), + type: 'object', + properties: { + statusMessage: { + type: 'object', + properties: { + type: { type: 'string', enum: ['text', 'image', 'audio', 'video'] }, + content: { type: 'string' }, + caption: { type: 'string' }, + backgroundColor: { type: 'string' }, + font: { type: 'integer', minimum: 0, maximum: 5 }, + statusJidList: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + pattern: '^\\d+', + description: '"statusJidList" must be an array of numeric strings', + }, }, + allContacts: { type: 'boolean', enum: [true, false] }, + }, + required: ['type', 'content'], + ...isNotEmpty('type', 'content'), }, - required: ['statusMessage'], + }, + required: ['statusMessage'], }; export const mediaMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - mediaMessage: { - type: 'object', - properties: { - mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, - media: { type: 'string' }, - fileName: { type: 'string' }, - caption: { type: 'string' }, - }, - required: ['mediatype', 'media'], - ...isNotEmpty('fileName', 'caption', 'media'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + mediaMessage: { + type: 'object', + properties: { + mediatype: { type: 'string', enum: ['image', 'document', 'video', 'audio'] }, + media: { type: 'string' }, + fileName: { type: 'string' }, + caption: { type: 'string' }, + }, + required: ['mediatype', 'media'], + ...isNotEmpty('fileName', 'caption', 'media'), }, - required: ['mediaMessage', 'number'], + }, + required: ['mediaMessage', 'number'], }; export const stickerMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - stickerMessage: { - type: 'object', - properties: { - image: { type: 'string' }, - }, - required: ['image'], - ...isNotEmpty('image'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + stickerMessage: { + type: 'object', + properties: { + image: { type: 'string' }, + }, + required: ['image'], + ...isNotEmpty('image'), }, - required: ['stickerMessage', 'number'], + }, + required: ['stickerMessage', 'number'], }; export const audioMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - audioMessage: { - type: 'object', - properties: { - audio: { type: 'string' }, - }, - required: ['audio'], - ...isNotEmpty('audio'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + audioMessage: { + type: 'object', + properties: { + audio: { type: 'string' }, + }, + required: ['audio'], + ...isNotEmpty('audio'), }, - required: ['audioMessage', 'number'], + }, + required: ['audioMessage', 'number'], }; export const buttonMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - buttonMessage: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + buttonMessage: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttons: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'object', properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttons: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - buttonText: { type: 'string' }, - buttonId: { type: 'string' }, - }, - required: ['buttonText', 'buttonId'], - ...isNotEmpty('buttonText', 'buttonId'), - }, - }, - mediaMessage: { - type: 'object', - properties: { - media: { type: 'string' }, - fileName: { type: 'string' }, - mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, - }, - required: ['media', 'mediatype'], - ...isNotEmpty('media', 'fileName'), - }, + buttonText: { type: 'string' }, + buttonId: { type: 'string' }, }, - required: ['title', 'buttons'], - ...isNotEmpty('title', 'description'), + required: ['buttonText', 'buttonId'], + ...isNotEmpty('buttonText', 'buttonId'), + }, }, + mediaMessage: { + type: 'object', + properties: { + media: { type: 'string' }, + fileName: { type: 'string' }, + mediatype: { type: 'string', enum: ['image', 'document', 'video'] }, + }, + required: ['media', 'mediatype'], + ...isNotEmpty('media', 'fileName'), + }, + }, + required: ['title', 'buttons'], + ...isNotEmpty('title', 'description'), }, - required: ['number', 'buttonMessage'], + }, + required: ['number', 'buttonMessage'], }; export const locationMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - locationMessage: { - type: 'object', - properties: { - latitude: { type: 'number' }, - longitude: { type: 'number' }, - name: { type: 'string' }, - address: { type: 'string' }, - }, - required: ['latitude', 'longitude'], - ...isNotEmpty('name', 'addresss'), - }, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + locationMessage: { + type: 'object', + properties: { + latitude: { type: 'number' }, + longitude: { type: 'number' }, + name: { type: 'string' }, + address: { type: 'string' }, + }, + required: ['latitude', 'longitude'], + ...isNotEmpty('name', 'addresss'), }, - required: ['number', 'locationMessage'], + }, + required: ['number', 'locationMessage'], }; export const listMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - listMessage: { + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + listMessage: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + footerText: { type: 'string' }, + buttonText: { type: 'string' }, + sections: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { type: 'object', properties: { - title: { type: 'string' }, - description: { type: 'string' }, - footerText: { type: 'string' }, - buttonText: { type: 'string' }, - sections: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - rows: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'object', - properties: { - title: { type: 'string' }, - description: { type: 'string' }, - rowId: { type: 'string' }, - }, - required: ['title', 'description', 'rowId'], - ...isNotEmpty('title', 'description', 'rowId'), - }, - }, - }, - required: ['title', 'rows'], - ...isNotEmpty('title'), - }, + title: { type: 'string' }, + rows: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'object', + properties: { + title: { type: 'string' }, + description: { type: 'string' }, + rowId: { type: 'string' }, + }, + required: ['title', 'description', 'rowId'], + ...isNotEmpty('title', 'description', 'rowId'), }, + }, }, - required: ['title', 'description', 'buttonText', 'sections'], - ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), + required: ['title', 'rows'], + ...isNotEmpty('title'), + }, }, + }, + required: ['title', 'description', 'buttonText', 'sections'], + ...isNotEmpty('title', 'description', 'buttonText', 'footerText'), }, - required: ['number', 'listMessage'], + }, + required: ['number', 'listMessage'], }; export const contactMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - contactMessage: { - type: 'array', - items: { - type: 'object', - properties: { - fullName: { type: 'string' }, - wuid: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"wuid" must be a numeric string', - }, - phoneNumber: { type: 'string', minLength: 10 }, - organization: { type: 'string' }, - email: { type: 'string' }, - url: { type: 'string' }, - }, - required: ['fullName', 'phoneNumber'], - ...isNotEmpty('fullName'), - }, - minItems: 1, - uniqueItems: true, + $id: v4(), + type: 'object', + properties: { + number: { ...numberDefinition }, + options: { ...optionsSchema }, + contactMessage: { + type: 'array', + items: { + type: 'object', + properties: { + fullName: { type: 'string' }, + wuid: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"wuid" must be a numeric string', + }, + phoneNumber: { type: 'string', minLength: 10 }, + organization: { type: 'string' }, + email: { type: 'string' }, + url: { type: 'string' }, }, + required: ['fullName', 'phoneNumber'], + ...isNotEmpty('fullName'), + }, + minItems: 1, + uniqueItems: true, }, - required: ['number', 'contactMessage'], + }, + required: ['number', 'contactMessage'], }; export const reactionMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reactionMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'remoteJid', 'fromMe'], - ...isNotEmpty('id', 'remoteJid'), - }, - reaction: { type: 'string' }, - }, - required: ['key', 'reaction'], - ...isNotEmpty('reaction'), + $id: v4(), + type: 'object', + properties: { + reactionMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'remoteJid', 'fromMe'], + ...isNotEmpty('id', 'remoteJid'), }, + reaction: { type: 'string' }, + }, + required: ['key', 'reaction'], + ...isNotEmpty('reaction'), }, - required: ['reactionMessage'], + }, + required: ['reactionMessage'], }; // Chat Schema export const whatsappNumberSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - description: '"numbers" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + description: '"numbers" must be an array of numeric strings', + }, }, + }, }; export const readMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - read_messages: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, + $id: v4(), + type: 'object', + properties: { + read_messages: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), + }, }, - required: ['read_messages'], + }, + required: ['read_messages'], }; export const privacySettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - privacySettings: { - type: 'object', - properties: { - readreceipts: { type: 'string', enum: ['all', 'none'] }, - profile: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - status: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - online: { type: 'string', enum: ['all', 'match_last_seen'] }, - last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, - groupadd: { - type: 'string', - enum: ['all', 'contacts', 'contact_blacklist', 'none'], - }, - }, - required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], - ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), + $id: v4(), + type: 'object', + properties: { + privacySettings: { + type: 'object', + properties: { + readreceipts: { type: 'string', enum: ['all', 'none'] }, + profile: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], }, + status: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + online: { type: 'string', enum: ['all', 'match_last_seen'] }, + last: { type: 'string', enum: ['all', 'contacts', 'contact_blacklist', 'none'] }, + groupadd: { + type: 'string', + enum: ['all', 'contacts', 'contact_blacklist', 'none'], + }, + }, + required: ['readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'], + ...isNotEmpty('readreceipts', 'profile', 'status', 'online', 'last', 'groupadd'), }, - required: ['privacySettings'], + }, + required: ['privacySettings'], }; export const archiveChatSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - lastMessage: { - type: 'object', - properties: { - key: { - type: 'object', - properties: { - id: { type: 'string' }, - remoteJid: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid'), - }, - messageTimestamp: { type: 'integer', minLength: 1 }, - }, - required: ['key'], - ...isNotEmpty('messageTimestamp'), + $id: v4(), + type: 'object', + properties: { + lastMessage: { + type: 'object', + properties: { + key: { + type: 'object', + properties: { + id: { type: 'string' }, + remoteJid: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid'), }, - archive: { type: 'boolean', enum: [true, false] }, + messageTimestamp: { type: 'integer', minLength: 1 }, + }, + required: ['key'], + ...isNotEmpty('messageTimestamp'), }, - required: ['lastMessage', 'archive'], + archive: { type: 'boolean', enum: [true, false] }, + }, + required: ['lastMessage', 'archive'], }; export const deleteMessageSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - remoteJid: { type: 'string' }, - participant: { type: 'string' }, - }, - required: ['id', 'fromMe', 'remoteJid'], - ...isNotEmpty('id', 'remoteJid', 'participant'), + $id: v4(), + type: 'object', + properties: { + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + remoteJid: { type: 'string' }, + participant: { type: 'string' }, + }, + required: ['id', 'fromMe', 'remoteJid'], + ...isNotEmpty('id', 'remoteJid', 'participant'), }; export const contactValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - pushName: { type: 'string', minLength: 1 }, - id: { type: 'string', minLength: 1 }, - }, - ...isNotEmpty('_id', 'id', 'pushName'), - }, + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + pushName: { type: 'string', minLength: 1 }, + id: { type: 'string', minLength: 1 }, + }, + ...isNotEmpty('_id', 'id', 'pushName'), }, + }, }; export const profileNameSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - name: { type: 'string' }, - }, - ...isNotEmpty('name'), + $id: v4(), + type: 'object', + properties: { + name: { type: 'string' }, + }, + ...isNotEmpty('name'), }; export const profileStatusSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - status: { type: 'string' }, - }, - ...isNotEmpty('status'), + $id: v4(), + type: 'object', + properties: { + status: { type: 'string' }, + }, + ...isNotEmpty('status'), }; export const profilePictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { type: 'string' }, - picture: { type: 'string' }, - }, + $id: v4(), + type: 'object', + properties: { + number: { type: 'string' }, + picture: { type: 'string' }, + }, }; export const profileSchema: JSONSchema7 = { - type: 'object', - properties: { - wuid: { type: 'string' }, - name: { type: 'string' }, - picture: { type: 'string' }, - status: { type: 'string' }, - isBusiness: { type: 'boolean' }, - }, + type: 'object', + properties: { + wuid: { type: 'string' }, + name: { type: 'string' }, + picture: { type: 'string' }, + status: { type: 'string' }, + isBusiness: { type: 'boolean' }, + }, }; export const messageValidateSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string', minLength: 1 }, - key: { - type: 'object', - if: { - propertyNames: { - enum: ['fromMe', 'remoteJid', 'id'], - }, - }, - then: { - properties: { - remoteJid: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - id: { - type: 'string', - minLength: 1, - description: 'The property cannot be empty', - }, - fromMe: { type: 'boolean', enum: [true, false] }, - }, - }, - }, - message: { type: 'object' }, + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string', minLength: 1 }, + key: { + type: 'object', + if: { + propertyNames: { + enum: ['fromMe', 'remoteJid', 'id'], }, - ...isNotEmpty('_id'), + }, + then: { + properties: { + remoteJid: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + id: { + type: 'string', + minLength: 1, + description: 'The property cannot be empty', + }, + fromMe: { type: 'boolean', enum: [true, false] }, + }, + }, }, - limit: { type: 'integer' }, + message: { type: 'object' }, + }, + ...isNotEmpty('_id'), }, + limit: { type: 'integer' }, + }, }; export const messageUpSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - where: { - type: 'object', - properties: { - _id: { type: 'string' }, - remoteJid: { type: 'string' }, - id: { type: 'string' }, - fromMe: { type: 'boolean', enum: [true, false] }, - participant: { type: 'string' }, - status: { - type: 'string', - enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], - }, - }, - ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), + $id: v4(), + type: 'object', + properties: { + where: { + type: 'object', + properties: { + _id: { type: 'string' }, + remoteJid: { type: 'string' }, + id: { type: 'string' }, + fromMe: { type: 'boolean', enum: [true, false] }, + participant: { type: 'string' }, + status: { + type: 'string', + enum: ['ERROR', 'PENDING', 'SERVER_ACK', 'DELIVERY_ACK', 'READ', 'PLAYED'], }, - limit: { type: 'integer' }, + }, + ...isNotEmpty('_id', 'remoteJid', 'id', 'status'), }, + limit: { type: 'integer' }, + }, }; // Group Schema export const createGroupSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - subject: { type: 'string' }, - description: { type: 'string' }, - profilePicture: { type: 'string' }, - promoteParticipants: { type: 'boolean', enum: [true, false] }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + subject: { type: 'string' }, + description: { type: 'string' }, + profilePicture: { type: 'string' }, + promoteParticipants: { type: 'boolean', enum: [true, false] }, + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, }, - required: ['subject', 'participants'], - ...isNotEmpty('subject', 'description', 'profilePicture'), + }, + required: ['subject', 'participants'], + ...isNotEmpty('subject', 'description', 'profilePicture'), }; export const groupJidSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, - }, - required: ['groupJid'], - ...isNotEmpty('groupJid'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string', pattern: '^[\\d-]+@g.us$' }, + }, + required: ['groupJid'], + ...isNotEmpty('groupJid'), }; export const getParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - getParticipants: { type: 'string', enum: ['true', 'false'] }, - }, - required: ['getParticipants'], - ...isNotEmpty('getParticipants'), + $id: v4(), + type: 'object', + properties: { + getParticipants: { type: 'string', enum: ['true', 'false'] }, + }, + required: ['getParticipants'], + ...isNotEmpty('getParticipants'), }; export const groupSendInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - numbers: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"numbers" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + numbers: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"numbers" must be an array of numeric strings', + }, }, - required: ['groupJid', 'description', 'numbers'], - ...isNotEmpty('groupJid', 'description', 'numbers'), + }, + required: ['groupJid', 'description', 'numbers'], + ...isNotEmpty('groupJid', 'description', 'numbers'), }; export const groupInviteSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, - }, - required: ['inviteCode'], - ...isNotEmpty('inviteCode'), + $id: v4(), + type: 'object', + properties: { + inviteCode: { type: 'string', pattern: '^[a-zA-Z0-9]{22}$' }, + }, + required: ['inviteCode'], + ...isNotEmpty('inviteCode'), }; export const updateParticipantsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['add', 'remove', 'promote', 'demote'], - }, - participants: { - type: 'array', - minItems: 1, - uniqueItems: true, - items: { - type: 'string', - minLength: 10, - pattern: '\\d+', - description: '"participants" must be an array of numeric strings', - }, - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['add', 'remove', 'promote', 'demote'], }, - required: ['groupJid', 'action', 'participants'], - ...isNotEmpty('groupJid', 'action'), + participants: { + type: 'array', + minItems: 1, + uniqueItems: true, + items: { + type: 'string', + minLength: 10, + pattern: '\\d+', + description: '"participants" must be an array of numeric strings', + }, + }, + }, + required: ['groupJid', 'action', 'participants'], + ...isNotEmpty('groupJid', 'action'), }; export const updateSettingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - action: { - type: 'string', - enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + action: { + type: 'string', + enum: ['announcement', 'not_announcement', 'locked', 'unlocked'], }, - required: ['groupJid', 'action'], - ...isNotEmpty('groupJid', 'action'), + }, + required: ['groupJid', 'action'], + ...isNotEmpty('groupJid', 'action'), }; export const toggleEphemeralSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - expiration: { - type: 'number', - enum: [0, 86400, 604800, 7776000], - }, + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + expiration: { + type: 'number', + enum: [0, 86400, 604800, 7776000], }, - required: ['groupJid', 'expiration'], - ...isNotEmpty('groupJid', 'expiration'), + }, + required: ['groupJid', 'expiration'], + ...isNotEmpty('groupJid', 'expiration'), }; export const updateGroupPictureSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - image: { type: 'string' }, - }, - required: ['groupJid', 'image'], - ...isNotEmpty('groupJid', 'image'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + image: { type: 'string' }, + }, + required: ['groupJid', 'image'], + ...isNotEmpty('groupJid', 'image'), }; export const updateGroupSubjectSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - subject: { type: 'string' }, - }, - required: ['groupJid', 'subject'], - ...isNotEmpty('groupJid', 'subject'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + subject: { type: 'string' }, + }, + required: ['groupJid', 'subject'], + ...isNotEmpty('groupJid', 'subject'), }; export const updateGroupDescriptionSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - groupJid: { type: 'string' }, - description: { type: 'string' }, - }, - required: ['groupJid', 'description'], - ...isNotEmpty('groupJid', 'description'), + $id: v4(), + type: 'object', + properties: { + groupJid: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['groupJid', 'description'], + ...isNotEmpty('groupJid', 'description'), }; // Webhook Schema export const webhookSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - url: { type: 'string' }, - enabled: { type: 'boolean', enum: [true, false] }, - events: { - type: 'array', - minItems: 0, - items: { - type: 'string', - enum: [ - 'APPLICATION_STARTUP', - 'QRCODE_UPDATED', - 'MESSAGES_SET', - 'MESSAGES_UPSERT', - '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', - 'CALL', - 'NEW_JWT_TOKEN', - ], - }, - }, + $id: v4(), + type: 'object', + properties: { + url: { type: 'string' }, + enabled: { type: 'boolean', enum: [true, false] }, + events: { + type: 'array', + minItems: 0, + items: { + type: 'string', + enum: [ + 'APPLICATION_STARTUP', + 'QRCODE_UPDATED', + 'MESSAGES_SET', + 'MESSAGES_UPSERT', + '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', + 'CALL', + 'NEW_JWT_TOKEN', + ], + }, }, - required: ['url', 'enabled'], - ...isNotEmpty('url'), + }, + required: ['url', 'enabled'], + ...isNotEmpty('url'), }; export const chatwootSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - enabled: { type: 'boolean', enum: [true, false] }, - account_id: { type: 'string' }, - token: { type: 'string' }, - url: { type: 'string' }, - sign_msg: { type: 'boolean', enum: [true, false] }, - reopen_conversation: { type: 'boolean', enum: [true, false] }, - conversation_pending: { type: 'boolean', enum: [true, false] }, - }, - required: ['enabled', 'account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'], - ...isNotEmpty('account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'), + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + account_id: { type: 'string' }, + token: { type: 'string' }, + url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, + reopen_conversation: { type: 'boolean', enum: [true, false] }, + conversation_pending: { type: 'boolean', enum: [true, false] }, + }, + required: ['enabled', 'account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'], + ...isNotEmpty('account_id', 'token', 'url', 'sign_msg', 'reopen_conversation', 'conversation_pending'), }; export const settingsSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - reject_call: { type: 'boolean', enum: [true, false] }, - msg_call: { type: 'string' }, - groups_ignore: { type: 'boolean', enum: [true, false] }, - always_online: { type: 'boolean', enum: [true, false] }, - read_messages: { type: 'boolean', enum: [true, false] }, - read_status: { type: 'boolean', enum: [true, false] }, - }, - required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'], - ...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'), + $id: v4(), + type: 'object', + properties: { + reject_call: { type: 'boolean', enum: [true, false] }, + msg_call: { type: 'string' }, + groups_ignore: { type: 'boolean', enum: [true, false] }, + always_online: { type: 'boolean', enum: [true, false] }, + read_messages: { type: 'boolean', enum: [true, false] }, + read_status: { type: 'boolean', enum: [true, false] }, + }, + required: ['reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'], + ...isNotEmpty('reject_call', 'groups_ignore', 'always_online', 'read_messages', 'read_status'), }; diff --git a/src/whatsapp/abstract/abstract.repository.ts b/src/whatsapp/abstract/abstract.repository.ts index 739489eb..a5b7a841 100644 --- a/src/whatsapp/abstract/abstract.repository.ts +++ b/src/whatsapp/abstract/abstract.repository.ts @@ -7,61 +7,61 @@ import { ROOT_DIR } from '../../config/path.config'; export type IInsert = { insertCount: number }; export interface IRepository { - insert(data: any, instanceName: string, saveDb?: boolean): Promise; - update(data: any, instanceName: string, saveDb?: boolean): Promise; - find(query: any): Promise; - delete(query: any, force?: boolean): Promise; + insert(data: any, instanceName: string, saveDb?: boolean): Promise; + update(data: any, instanceName: string, saveDb?: boolean): Promise; + find(query: any): Promise; + delete(query: any, force?: boolean): Promise; - dbSettings: Database; - readonly storePath: string; + dbSettings: Database; + readonly storePath: string; } type WriteStore = { - path: string; - fileName: string; - data: U; + path: string; + fileName: string; + data: U; }; export abstract class Repository implements IRepository { - constructor(configService: ConfigService) { - this.dbSettings = configService.get('DATABASE'); + constructor(configService: ConfigService) { + this.dbSettings = configService.get('DATABASE'); + } + + dbSettings: Database; + readonly storePath = join(ROOT_DIR, 'store'); + + public writeStore = (create: WriteStore) => { + if (!existsSync(create.path)) { + mkdirSync(create.path, { recursive: true }); } + try { + writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), { + encoding: 'utf-8', + }); - dbSettings: Database; - readonly storePath = join(ROOT_DIR, 'store'); + return { message: 'create - success' }; + } finally { + create.data = undefined; + } + }; - public writeStore = (create: WriteStore) => { - if (!existsSync(create.path)) { - mkdirSync(create.path, { recursive: true }); - } - try { - writeFileSync(join(create.path, create.fileName + '.json'), JSON.stringify({ ...create.data }), { - encoding: 'utf-8', - }); - - return { message: 'create - success' }; - } finally { - create.data = undefined; - } - }; - - // eslint-disable-next-line + // eslint-disable-next-line public insert(data: any, instanceName: string, saveDb = false): Promise { - throw new Error('Method not implemented.'); - } + throw new Error('Method not implemented.'); + } - // eslint-disable-next-line + // eslint-disable-next-line public update(data: any, instanceName: string, saveDb = false): Promise { - throw new Error('Method not implemented.'); - } + throw new Error('Method not implemented.'); + } - // eslint-disable-next-line + // eslint-disable-next-line public find(query: any): Promise { - throw new Error('Method not implemented.'); - } + throw new Error('Method not implemented.'); + } - // eslint-disable-next-line + // eslint-disable-next-line delete(query: any, force?: boolean): Promise { - throw new Error('Method not implemented.'); - } + throw new Error('Method not implemented.'); + } } diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 86f05d6a..3c19e6bb 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -10,214 +10,211 @@ import { GetParticipant, GroupInvite, GroupJid } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; type DataValidate = { - request: Request; - schema: JSONSchema7; - ClassRef: any; - execute: (instance: InstanceDto, data: T) => Promise; + request: Request; + schema: JSONSchema7; + ClassRef: any; + execute: (instance: InstanceDto, data: T) => Promise; }; const logger = new Logger('Validate'); export abstract class RouterBroker { - constructor() {} - public routerPath(path: string, param = true) { - // const route = param ? '/:instanceName/' + path : '/' + path; - let route = '/' + path; - param ? (route += '/:instanceName') : null; + constructor() {} + public routerPath(path: string, param = true) { + // const route = param ? '/:instanceName/' + path : '/' + path; + let route = '/' + path; + param ? (route += '/:instanceName') : null; - return route; + return route; + } + + public async dataValidate(args: DataValidate) { + const { request, schema, ClassRef, execute } = args; + + const ref = new ClassRef(); + const body = request.body; + const instance = request.params as unknown as InstanceDto; + + if (request?.query && Object.keys(request.query).length > 0) { + Object.assign(instance, request.query); } - public async dataValidate(args: DataValidate) { - const { request, schema, ClassRef, execute } = args; - - const ref = new ClassRef(); - const body = request.body; - const instance = request.params as unknown as InstanceDto; - - if (request?.query && Object.keys(request.query).length > 0) { - Object.assign(instance, request.query); - } - - if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, body); - } - - Object.assign(ref, body); - - const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); - } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - - return await execute(instance, ref); + if (request.originalUrl.includes('/instance/create')) { + Object.assign(instance, body); } - public async groupNoValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + Object.assign(ref, body); - const instance = request.params as unknown as InstanceDto; + const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; - const ref = new ClassRef(); - - Object.assign(ref, request.body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); - } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); } - - return await execute(instance, ref); + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); } - public async groupValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + return await execute(instance, ref); + } - const groupJid = request.query as unknown as GroupJid; + public async groupNoValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; - if (!groupJid?.groupJid) { - throw new BadRequestException( - 'The group id needs to be informed in the query', - 'ex: "groupJid=120362@g.us"', - ); + const instance = request.params as unknown as InstanceDto; + + const ref = new ClassRef(); + + Object.assign(ref, request.body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); } - - const instance = request.params as unknown as InstanceDto; - const body = request.body; - - const ref = new ClassRef(); - - Object.assign(body, groupJid); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); - } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - - return await execute(instance, ref); + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); } - public async inviteCodeValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + return await execute(instance, ref); + } - const inviteCode = request.query as unknown as GroupInvite; + public async groupValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; - if (!inviteCode?.inviteCode) { - throw new BadRequestException( - 'The group invite code id needs to be informed in the query', - 'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)', - ); - } + const groupJid = request.query as unknown as GroupJid; - const instance = request.params as unknown as InstanceDto; - const body = request.body; - - const ref = new ClassRef(); - - Object.assign(body, inviteCode); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); - } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - - return await execute(instance, ref); + if (!groupJid?.groupJid) { + throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"'); } - public async getParticipantsValidate(args: DataValidate) { - const { request, ClassRef, schema, execute } = args; + const instance = request.params as unknown as InstanceDto; + const body = request.body; - const getParticipants = request.query as unknown as GetParticipant; + const ref = new ClassRef(); - if (!getParticipants?.getParticipants) { - throw new BadRequestException('The getParticipants needs to be informed in the query'); + Object.assign(body, groupJid); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); } - - const instance = request.params as unknown as InstanceDto; - const body = request.body; - - const ref = new ClassRef(); - - Object.assign(body, getParticipants); - Object.assign(ref, body); - - const v = validate(ref, schema); - - if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { - let message: string; - if (schema['description']) { - message = schema['description']; - } else { - message = stack.replace('instance.', ''); - } - return { - property: property.replace('instance.', ''), - message, - }; - }); - logger.error([...message]); - throw new BadRequestException(...message); - } - - return await execute(instance, ref); + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); } + + return await execute(instance, ref); + } + + public async inviteCodeValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; + + const inviteCode = request.query as unknown as GroupInvite; + + if (!inviteCode?.inviteCode) { + throw new BadRequestException( + 'The group invite code id needs to be informed in the query', + 'ex: "inviteCode=F1EX5QZxO181L3TMVP31gY" (Obtained from group join link)', + ); + } + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, inviteCode); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); + } + + public async getParticipantsValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; + + const getParticipants = request.query as unknown as GetParticipant; + + if (!getParticipants?.getParticipants) { + throw new BadRequestException('The getParticipants needs to be informed in the query'); + } + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, getParticipants); + Object.assign(ref, body); + + const v = validate(ref, schema); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); + } } diff --git a/src/whatsapp/controllers/chat.controller.ts b/src/whatsapp/controllers/chat.controller.ts index c1eff50b..0299841c 100644 --- a/src/whatsapp/controllers/chat.controller.ts +++ b/src/whatsapp/controllers/chat.controller.ts @@ -1,15 +1,15 @@ import { Logger } from '../../config/logger.config'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberDto, - PrivacySettingDto, - ProfileNameDto, - ProfilePictureDto, - ProfileStatusDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberDto, + PrivacySettingDto, + ProfileNameDto, + ProfilePictureDto, + ProfileStatusDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -20,95 +20,95 @@ import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('ChatController'); export class ChatController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { - logger.verbose('requested whatsappNumber from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); - } + public async whatsappNumber({ instanceName }: InstanceDto, data: WhatsAppNumberDto) { + logger.verbose('requested whatsappNumber from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].whatsappNumber(data); + } - public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { - logger.verbose('requested readMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); - } + public async readMessage({ instanceName }: InstanceDto, data: ReadMessageDto) { + logger.verbose('requested readMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].markMessageAsRead(data); + } - public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { - logger.verbose('requested archiveChat from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].archiveChat(data); - } + public async archiveChat({ instanceName }: InstanceDto, data: ArchiveChatDto) { + logger.verbose('requested archiveChat from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].archiveChat(data); + } - public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { - logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].deleteMessage(data); - } + public async deleteMessage({ instanceName }: InstanceDto, data: DeleteMessage) { + logger.verbose('requested deleteMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].deleteMessage(data); + } - public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) { - logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); - } + public async fetchProfilePicture({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].profilePicture(data.number); + } - public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { - logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number); - } + public async fetchProfile({ instanceName }: InstanceDto, data: NumberDto) { + logger.verbose('requested fetchProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchProfile(instanceName, data.number); + } - public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { - logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchContacts(query); - } + public async fetchContacts({ instanceName }: InstanceDto, query: ContactQuery) { + logger.verbose('requested fetchContacts from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchContacts(query); + } - public async getBase64FromMediaMessage({ instanceName }: InstanceDto, data: getBase64FromMediaMessageDto) { - logger.verbose('requested getBase64FromMediaMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data); - } + public async getBase64FromMediaMessage({ instanceName }: InstanceDto, data: getBase64FromMediaMessageDto) { + logger.verbose('requested getBase64FromMediaMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].getBase64FromMediaMessage(data); + } - public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) { - logger.verbose('requested fetchMessages from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchMessages(query); - } + public async fetchMessages({ instanceName }: InstanceDto, query: MessageQuery) { + logger.verbose('requested fetchMessages from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchMessages(query); + } - public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) { - logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query); - } + public async fetchStatusMessage({ instanceName }: InstanceDto, query: MessageUpQuery) { + logger.verbose('requested fetchStatusMessage from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchStatusMessage(query); + } - public async fetchChats({ instanceName }: InstanceDto) { - logger.verbose('requested fetchChats from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchChats(); - } + public async fetchChats({ instanceName }: InstanceDto) { + logger.verbose('requested fetchChats from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchChats(); + } - public async fetchPrivacySettings({ instanceName }: InstanceDto) { - logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); - } + public async fetchPrivacySettings({ instanceName }: InstanceDto) { + logger.verbose('requested fetchPrivacySettings from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchPrivacySettings(); + } - public async updatePrivacySettings({ instanceName }: InstanceDto, data: PrivacySettingDto) { - logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); - } + public async updatePrivacySettings({ instanceName }: InstanceDto, data: PrivacySettingDto) { + logger.verbose('requested updatePrivacySettings from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updatePrivacySettings(data); + } - public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) { - logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number); - } + public async fetchBusinessProfile({ instanceName }: InstanceDto, data: ProfilePictureDto) { + logger.verbose('requested fetchBusinessProfile from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].fetchBusinessProfile(data.number); + } - public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { - logger.verbose('requested updateProfileName from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); - } + public async updateProfileName({ instanceName }: InstanceDto, data: ProfileNameDto) { + logger.verbose('requested updateProfileName from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name); + } - public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) { - logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status); - } + public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) { + logger.verbose('requested updateProfileStatus from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status); + } - public async updateProfilePicture({ instanceName }: InstanceDto, data: ProfilePictureDto) { - logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].updateProfilePicture(data.picture); - } + public async updateProfilePicture({ instanceName }: InstanceDto, data: ProfilePictureDto) { + logger.verbose('requested updateProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].updateProfilePicture(data.picture); + } - public async removeProfilePicture({ instanceName }: InstanceDto) { - logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); - } + public async removeProfilePicture({ instanceName }: InstanceDto) { + logger.verbose('requested removeProfilePicture from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].removeProfilePicture(); + } } diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 67591cb3..ab291c43 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -11,89 +11,89 @@ import { waMonitor } from '../whatsapp.module'; const logger = new Logger('ChatwootController'); export class ChatwootController { - constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} + constructor(private readonly chatwootService: ChatwootService, private readonly configService: ConfigService) {} - public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); + public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { + logger.verbose('requested createChatwoot from ' + instance.instanceName + ' instance'); - if (data.enabled) { - if (!isURL(data.url, { require_tld: false })) { - throw new BadRequestException('url is not valid'); - } + if (data.enabled) { + if (!isURL(data.url, { require_tld: false })) { + throw new BadRequestException('url is not valid'); + } - if (!data.account_id) { - throw new BadRequestException('account_id is required'); - } + if (!data.account_id) { + throw new BadRequestException('account_id is required'); + } - if (!data.token) { - throw new BadRequestException('token is required'); - } + if (!data.token) { + throw new BadRequestException('token is required'); + } - if (data.sign_msg !== true && data.sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } - } - - if (!data.enabled) { - logger.verbose('chatwoot disabled'); - data.account_id = ''; - data.token = ''; - data.url = ''; - data.sign_msg = false; - data.reopen_conversation = false; - data.conversation_pending = false; - } - - data.name_inbox = instance.instanceName; - - const result = this.chatwootService.create(instance, data); - - const urlServer = this.configService.get('SERVER').URL; - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; + if (data.sign_msg !== true && data.sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } } - public async findChatwoot(instance: InstanceDto) { - logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - const result = await this.chatwootService.find(instance); - - const urlServer = this.configService.get('SERVER').URL; - - if (Object.keys(result).length === 0) { - return { - enabled: false, - url: '', - account_id: '', - token: '', - sign_msg: false, - name_inbox: '', - webhook_url: '', - }; - } - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }; - - return response; + if (!data.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + data.sign_msg = false; + data.reopen_conversation = false; + data.conversation_pending = false; } - public async receiveWebhook(instance: InstanceDto, data: any) { - logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); - const chatwootService = new ChatwootService(waMonitor, this.configService); + data.name_inbox = instance.instanceName; - return chatwootService.receiveWebhook(instance, data); + const result = this.chatwootService.create(instance, data); + + const urlServer = this.configService.get('SERVER').URL; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; + } + + public async findChatwoot(instance: InstanceDto) { + logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); + const result = await this.chatwootService.find(instance); + + const urlServer = this.configService.get('SERVER').URL; + + if (Object.keys(result).length === 0) { + return { + enabled: false, + url: '', + account_id: '', + token: '', + sign_msg: false, + name_inbox: '', + webhook_url: '', + }; } - public async newInstance(data: any) { - const chatwootService = new ChatwootService(waMonitor, this.configService); + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; - return chatwootService.newInstance(data); - } + return response; + } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose('requested receiveWebhook from ' + instance.instanceName + ' instance'); + const chatwootService = new ChatwootService(waMonitor, this.configService); + + return chatwootService.receiveWebhook(instance, data); + } + + public async newInstance(data: any) { + const chatwootService = new ChatwootService(waMonitor, this.configService); + + return chatwootService.newInstance(data); + } } diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index 86e78737..0cf093ca 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -1,16 +1,16 @@ import { Logger } from '../../config/logger.config'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; import { WAMonitoringService } from '../services/monitor.service'; @@ -18,80 +18,80 @@ import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('ChatController'); export class GroupController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async createGroup(instance: InstanceDto, create: CreateGroupDto) { - logger.verbose('requested createGroup from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); - } + public async createGroup(instance: InstanceDto, create: CreateGroupDto) { + logger.verbose('requested createGroup from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].createGroup(create); + } - public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { - logger.verbose('requested updateGroupPicture from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update); - } + public async updateGroupPicture(instance: InstanceDto, update: GroupPictureDto) { + logger.verbose('requested updateGroupPicture from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupPicture(update); + } - public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { - logger.verbose('requested updateGroupSubject from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update); - } + public async updateGroupSubject(instance: InstanceDto, update: GroupSubjectDto) { + logger.verbose('requested updateGroupSubject from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupSubject(update); + } - public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) { - logger.verbose('requested updateGroupDescription from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update); - } + public async updateGroupDescription(instance: InstanceDto, update: GroupDescriptionDto) { + logger.verbose('requested updateGroupDescription from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGroupDescription(update); + } - public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); - } + public async findGroupInfo(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested findGroupInfo from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); + } - public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { - logger.verbose('requested fetchAllGroups from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants); - } + public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { + logger.verbose('requested fetchAllGroups from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(getPaticipants); + } - public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); - } + public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested inviteCode from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].inviteCode(groupJid); + } - public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { - logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); - } + public async inviteInfo(instance: InstanceDto, inviteCode: GroupInvite) { + logger.verbose('requested inviteInfo from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].inviteInfo(inviteCode); + } - public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { - logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); - } + public async sendInvite(instance: InstanceDto, data: GroupSendInvite) { + logger.verbose('requested sendInvite from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].sendInvite(data); + } - public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); - } + public async revokeInviteCode(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested revokeInviteCode from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].revokeInviteCode(groupJid); + } - public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested findParticipants from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid); - } + public async findParticipants(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested findParticipants from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].findParticipants(groupJid); + } - public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) { - logger.verbose('requested updateGParticipate from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update); - } + public async updateGParticipate(instance: InstanceDto, update: GroupUpdateParticipantDto) { + logger.verbose('requested updateGParticipate from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGParticipant(update); + } - public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { - logger.verbose('requested updateGSetting from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); - } + public async updateGSetting(instance: InstanceDto, update: GroupUpdateSettingDto) { + logger.verbose('requested updateGSetting from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].updateGSetting(update); + } - public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { - logger.verbose('requested toggleEphemeral from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update); - } + public async toggleEphemeral(instance: InstanceDto, update: GroupToggleEphemeralDto) { + logger.verbose('requested toggleEphemeral from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].toggleEphemeral(update); + } - public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { - logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance'); - return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); - } + public async leaveGroup(instance: InstanceDto, groupJid: GroupJid) { + logger.verbose('requested leaveGroup from ' + instance.instanceName + ' instance'); + return await this.waMonitor.waInstances[instance.instanceName].leaveGroup(groupJid); + } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index f0a89ee8..e45644fe 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -17,340 +17,340 @@ import { WAStartupService } from '../services/whatsapp.service'; import { wa } from '../types/wa.types'; export class InstanceController { - constructor( - private readonly waMonitor: WAMonitoringService, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly eventEmitter: EventEmitter2, - private readonly authService: AuthService, - private readonly webhookService: WebhookService, - private readonly chatwootService: ChatwootService, - private readonly settingsService: SettingsService, - private readonly cache: RedisCache, - ) {} + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly eventEmitter: EventEmitter2, + private readonly authService: AuthService, + private readonly webhookService: WebhookService, + private readonly chatwootService: ChatwootService, + private readonly settingsService: SettingsService, + private readonly cache: RedisCache, + ) {} - private readonly logger = new Logger(InstanceController.name); + private readonly logger = new Logger(InstanceController.name); - public async createInstance({ - instanceName, + public async createInstance({ + instanceName, + webhook, + webhook_by_events, + events, + qrcode, + number, + token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, + chatwoot_sign_msg, + chatwoot_reopen_conversation, + chatwoot_conversation_pending, + reject_call, + msg_call, + groups_ignore, + always_online, + read_messages, + read_status, + }: InstanceDto) { + try { + this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + + if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { + throw new BadRequestException('The instance name must be lowercase and without special characters'); + } + + this.logger.verbose('checking duplicate token'); + await this.authService.checkDuplicateToken(token); + + this.logger.verbose('creating instance'); + const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); + + this.logger.verbose('instance: ' + instance.instanceName + ' created'); + + this.waMonitor.waInstances[instance.instanceName] = instance; + this.waMonitor.delInstanceTime(instance.instanceName); + + this.logger.verbose('generating hash'); + const hash = await this.authService.generateHash( + { + instanceName: instance.instanceName, + }, + token, + ); + + this.logger.verbose('hash: ' + hash + ' generated'); + + let getEvents: string[]; + + if (webhook) { + if (!isURL(webhook, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in webhook'); + } + + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + + this.logger.verbose('creating settings'); + const settings: wa.LocalSettings = { + reject_call: reject_call || false, + msg_call: msg_call || '', + groups_ignore: groups_ignore || false, + always_online: always_online || false, + read_messages: read_messages || false, + read_status: read_status || false, + }; + + this.logger.verbose('settings: ' + JSON.stringify(settings)); + + this.settingsService.create(instance, settings); + + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(number); + await delay(5000); + getQrcode = instance.qrCode; + } + + const result = { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + settings, + qrcode: getQrcode, + }; + + this.logger.verbose('instance created'); + this.logger.verbose(result); + + return result; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + + if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { + throw new BadRequestException('sign_msg is required'); + } + + if (chatwoot_reopen_conversation !== true && chatwoot_reopen_conversation !== false) { + throw new BadRequestException('reopen_conversation is required'); + } + + if (chatwoot_conversation_pending !== true && chatwoot_conversation_pending !== false) { + throw new BadRequestException('conversation_pending is required'); + } + + const urlServer = this.configService.get('SERVER').URL; + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + name_inbox: instance.instanceName, + number, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + number, + ); + } catch (error) { + this.logger.log(error); + } + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, webhook, webhook_by_events, - events, - qrcode, - number, - token, - chatwoot_account_id, - chatwoot_token, - chatwoot_url, - chatwoot_sign_msg, - chatwoot_reopen_conversation, - chatwoot_conversation_pending, - reject_call, - msg_call, - groups_ignore, - always_online, - read_messages, - read_status, - }: InstanceDto) { - try { - this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); + events: getEvents, + settings, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg || false, + reopen_conversation: chatwoot_reopen_conversation || false, + conversation_pending: chatwoot_conversation_pending || false, + number, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, + }; + } catch (error) { + console.log(error); + return { error: true, message: error.toString() }; + } + } - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException('The instance name must be lowercase and without special characters'); - } + public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { + try { + this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance'); - this.logger.verbose('checking duplicate token'); - await this.authService.checkDuplicateToken(token); + const instance = this.waMonitor.waInstances[instanceName]; + const state = instance?.connectionStatus?.state; - this.logger.verbose('creating instance'); - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); + this.logger.verbose('state: ' + state); - this.logger.verbose('instance: ' + instance.instanceName + ' created'); + if (state == 'open') { + return await this.connectionState({ instanceName }); + } - this.waMonitor.waInstances[instance.instanceName] = instance; - this.waMonitor.delInstanceTime(instance.instanceName); + if (state == 'connecting') { + return instance.qrCode; + } - this.logger.verbose('generating hash'); - const hash = await this.authService.generateHash( - { - instanceName: instance.instanceName, - }, - token, - ); + if (state == 'close') { + this.logger.verbose('connecting'); + await instance.connectToWhatsapp(number); - this.logger.verbose('hash: ' + hash + ' generated'); + await delay(5000); + return instance.qrCode; + } - let getEvents: string[]; + return { + instance: { + instanceName: instanceName, + status: state, + }, + qrcode: instance?.qrCode, + }; + } catch (error) { + this.logger.error(error); + } + } - if (webhook) { - if (!isURL(webhook, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in webhook'); - } + public async restartInstance({ instanceName }: InstanceDto) { + try { + this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } + return { error: false, message: 'Instance restarted' }; + } catch (error) { + this.logger.error(error); + } + } - this.logger.verbose('creating settings'); - const settings: wa.LocalSettings = { - reject_call: reject_call || false, - msg_call: msg_call || '', - groups_ignore: groups_ignore || false, - always_online: always_online || false, - read_messages: read_messages || false, - read_status: read_status || false, - }; + public async connectionState({ instanceName }: InstanceDto) { + this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); + return { + instance: { + instanceName: instanceName, + state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, + }, + }; + } - this.logger.verbose('settings: ' + JSON.stringify(settings)); - - this.settingsService.create(instance, settings); - - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { - let getQrcode: wa.QrCode; - - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(number); - await delay(5000); - getQrcode = instance.qrCode; - } - - const result = { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - settings, - qrcode: getQrcode, - }; - - this.logger.verbose('instance created'); - this.logger.verbose(result); - - return result; - } - - if (!chatwoot_account_id) { - throw new BadRequestException('account_id is required'); - } - - if (!chatwoot_token) { - throw new BadRequestException('token is required'); - } - - if (!chatwoot_url) { - throw new BadRequestException('url is required'); - } - - if (!isURL(chatwoot_url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property in chatwoot'); - } - - if (chatwoot_sign_msg !== true && chatwoot_sign_msg !== false) { - throw new BadRequestException('sign_msg is required'); - } - - if (chatwoot_reopen_conversation !== true && chatwoot_reopen_conversation !== false) { - throw new BadRequestException('reopen_conversation is required'); - } - - if (chatwoot_conversation_pending !== true && chatwoot_conversation_pending !== false) { - throw new BadRequestException('conversation_pending is required'); - } - - const urlServer = this.configService.get('SERVER').URL; - - try { - this.chatwootService.create(instance, { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - name_inbox: instance.instanceName, - number, - reopen_conversation: chatwoot_reopen_conversation || false, - conversation_pending: chatwoot_conversation_pending || false, - }); - - this.chatwootService.initInstanceChatwoot( - instance, - instance.instanceName, - `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - qrcode, - number, - ); - } catch (error) { - this.logger.log(error); - } - - return { - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - settings, - chatwoot: { - enabled: true, - account_id: chatwoot_account_id, - token: chatwoot_token, - url: chatwoot_url, - sign_msg: chatwoot_sign_msg || false, - reopen_conversation: chatwoot_reopen_conversation || false, - conversation_pending: chatwoot_conversation_pending || false, - number, - name_inbox: instance.instanceName, - webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, - }, - }; - } catch (error) { - console.log(error); - return { error: true, message: error.toString() }; - } + public async fetchInstances({ instanceName }: InstanceDto) { + this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); + if (instanceName) { + this.logger.verbose('instanceName: ' + instanceName); + return this.waMonitor.instanceInfo(instanceName); } - public async connectToWhatsapp({ instanceName, number = null }: InstanceDto) { - try { - this.logger.verbose('requested connectToWhatsapp from ' + instanceName + ' instance'); + return this.waMonitor.instanceInfo(); + } - const instance = this.waMonitor.waInstances[instanceName]; - const state = instance?.connectionStatus?.state; + public async logout({ instanceName }: InstanceDto) { + this.logger.verbose('requested logout from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); - this.logger.verbose('state: ' + state); - - if (state == 'open') { - return await this.connectionState({ instanceName }); - } - - if (state == 'connecting') { - return instance.qrCode; - } - - if (state == 'close') { - this.logger.verbose('connecting'); - await instance.connectToWhatsapp(number); - - await delay(5000); - return instance.qrCode; - } - - return { - instance: { - instanceName: instanceName, - status: state, - }, - qrcode: instance?.qrCode, - }; - } catch (error) { - this.logger.error(error); - } + if (instance.state === 'close') { + throw new BadRequestException('The "' + instanceName + '" instance is not connected'); } - public async restartInstance({ instanceName }: InstanceDto) { - try { - this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); + try { + this.logger.verbose('logging out instance: ' + instanceName); + await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); - this.logger.verbose('logging out instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); + this.logger.verbose('close connection instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - return { error: false, message: 'Instance restarted' }; - } catch (error) { - this.logger.error(error); - } + return { error: false, message: 'Instance logged out' }; + } catch (error) { + throw new InternalServerErrorException(error.toString()); } + } - public async connectionState({ instanceName }: InstanceDto) { - this.logger.verbose('requested connectionState from ' + instanceName + ' instance'); - return { - instance: { - instanceName: instanceName, - state: this.waMonitor.waInstances[instanceName]?.connectionStatus?.state, - }, - }; + public async deleteInstance({ instanceName }: InstanceDto) { + this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); + const { instance } = await this.connectionState({ instanceName }); + + if (instance.state === 'open') { + throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); } + try { + if (instance.state === 'connecting') { + this.logger.verbose('logging out instance: ' + instanceName); - public async fetchInstances({ instanceName }: InstanceDto) { - this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); - if (instanceName) { - this.logger.verbose('instanceName: ' + instanceName); - return this.waMonitor.instanceInfo(instanceName); - } + await this.logout({ instanceName }); + delete this.waMonitor.waInstances[instanceName]; + return { error: false, message: 'Instance deleted' }; + } else { + this.logger.verbose('deleting instance: ' + instanceName); - return this.waMonitor.instanceInfo(); + delete this.waMonitor.waInstances[instanceName]; + this.eventEmitter.emit('remove.instance', instanceName, 'inner'); + return { error: false, message: 'Instance deleted' }; + } + } catch (error) { + throw new BadRequestException(error.toString()); } + } - public async logout({ instanceName }: InstanceDto) { - this.logger.verbose('requested logout from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); - - if (instance.state === 'close') { - throw new BadRequestException('The "' + instanceName + '" instance is not connected'); - } - - try { - this.logger.verbose('logging out instance: ' + instanceName); - await this.waMonitor.waInstances[instanceName]?.client?.logout('Log out instance: ' + instanceName); - - this.logger.verbose('close connection instance: ' + instanceName); - this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - - return { error: false, message: 'Instance logged out' }; - } catch (error) { - throw new InternalServerErrorException(error.toString()); - } - } - - public async deleteInstance({ instanceName }: InstanceDto) { - this.logger.verbose('requested deleteInstance from ' + instanceName + ' instance'); - const { instance } = await this.connectionState({ instanceName }); - - if (instance.state === 'open') { - throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected'); - } - try { - if (instance.state === 'connecting') { - this.logger.verbose('logging out instance: ' + instanceName); - - await this.logout({ instanceName }); - delete this.waMonitor.waInstances[instanceName]; - return { error: false, message: 'Instance deleted' }; - } else { - this.logger.verbose('deleting instance: ' + instanceName); - - delete this.waMonitor.waInstances[instanceName]; - this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { error: false, message: 'Instance deleted' }; - } - } catch (error) { - throw new BadRequestException(error.toString()); - } - } - - public async refreshToken(_: InstanceDto, oldToken: OldToken) { - this.logger.verbose('requested refreshToken'); - return await this.authService.refreshToken(oldToken); - } + public async refreshToken(_: InstanceDto, oldToken: OldToken) { + this.logger.verbose('requested refreshToken'); + return await this.authService.refreshToken(oldToken); + } } diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index 593b858e..20e38ae5 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -4,112 +4,108 @@ import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, } from '../dto/sendMessage.dto'; import { WAMonitoringService } from '../services/monitor.service'; const logger = new Logger('MessageRouter'); export class SendMessageController { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { - logger.verbose('requested sendText from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].textMessage(data); + public async sendText({ instanceName }: InstanceDto, data: SendTextDto) { + logger.verbose('requested sendText from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].textMessage(data); + } + + public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { + logger.verbose('requested sendMedia from ' + instanceName + ' instance'); + + if ( + isBase64(data?.mediaMessage?.media) && + !data?.mediaMessage?.fileName && + data?.mediaMessage?.mediatype === 'document' + ) { + throw new BadRequestException('For base64 the file name must be informed.'); } - public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { - logger.verbose('requested sendMedia from ' + instanceName + ' instance'); - - if ( - isBase64(data?.mediaMessage?.media) && - !data?.mediaMessage?.fileName && - data?.mediaMessage?.mediatype === 'document' - ) { - throw new BadRequestException('For base64 the file name must be informed.'); - } - - logger.verbose( - 'isURL: ' + isURL(data?.mediaMessage?.media) + ', isBase64: ' + isBase64(data?.mediaMessage?.media), - ); - if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) { - return await this.waMonitor.waInstances[instanceName].mediaMessage(data); - } - throw new BadRequestException('Owned media must be a url or base64'); + logger.verbose('isURL: ' + isURL(data?.mediaMessage?.media) + ', isBase64: ' + isBase64(data?.mediaMessage?.media)); + if (isURL(data?.mediaMessage?.media) || isBase64(data?.mediaMessage?.media)) { + return await this.waMonitor.waInstances[instanceName].mediaMessage(data); } + throw new BadRequestException('Owned media must be a url or base64'); + } - public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) { - logger.verbose('requested sendSticker from ' + instanceName + ' instance'); + public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto) { + logger.verbose('requested sendSticker from ' + instanceName + ' instance'); - logger.verbose( - 'isURL: ' + isURL(data?.stickerMessage?.image) + ', isBase64: ' + isBase64(data?.stickerMessage?.image), - ); - if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) { - return await this.waMonitor.waInstances[instanceName].mediaSticker(data); - } - throw new BadRequestException('Owned media must be a url or base64'); + logger.verbose( + 'isURL: ' + isURL(data?.stickerMessage?.image) + ', isBase64: ' + isBase64(data?.stickerMessage?.image), + ); + if (isURL(data.stickerMessage.image) || isBase64(data.stickerMessage.image)) { + return await this.waMonitor.waInstances[instanceName].mediaSticker(data); } + throw new BadRequestException('Owned media must be a url or base64'); + } - public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { - logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance'); + public async sendWhatsAppAudio({ instanceName }: InstanceDto, data: SendAudioDto) { + logger.verbose('requested sendWhatsAppAudio from ' + instanceName + ' instance'); - logger.verbose( - 'isURL: ' + isURL(data?.audioMessage?.audio) + ', isBase64: ' + isBase64(data?.audioMessage?.audio), - ); - if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { - return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); - } - throw new BadRequestException('Owned media must be a url or base64'); + logger.verbose('isURL: ' + isURL(data?.audioMessage?.audio) + ', isBase64: ' + isBase64(data?.audioMessage?.audio)); + if (isURL(data.audioMessage.audio) || isBase64(data.audioMessage.audio)) { + return await this.waMonitor.waInstances[instanceName].audioWhatsapp(data); } + throw new BadRequestException('Owned media must be a url or base64'); + } - public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) { - logger.verbose('requested sendButtons from ' + instanceName + ' instance'); - if (isBase64(data.buttonMessage.mediaMessage?.media) && !data.buttonMessage.mediaMessage?.fileName) { - throw new BadRequestException('For bse64 the file name must be informed.'); - } - return await this.waMonitor.waInstances[instanceName].buttonMessage(data); + public async sendButtons({ instanceName }: InstanceDto, data: SendButtonDto) { + logger.verbose('requested sendButtons from ' + instanceName + ' instance'); + if (isBase64(data.buttonMessage.mediaMessage?.media) && !data.buttonMessage.mediaMessage?.fileName) { + throw new BadRequestException('For bse64 the file name must be informed.'); } + return await this.waMonitor.waInstances[instanceName].buttonMessage(data); + } - public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { - logger.verbose('requested sendLocation from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].locationMessage(data); - } + public async sendLocation({ instanceName }: InstanceDto, data: SendLocationDto) { + logger.verbose('requested sendLocation from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].locationMessage(data); + } - public async sendList({ instanceName }: InstanceDto, data: SendListDto) { - logger.verbose('requested sendList from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].listMessage(data); - } + public async sendList({ instanceName }: InstanceDto, data: SendListDto) { + logger.verbose('requested sendList from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].listMessage(data); + } - public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { - logger.verbose('requested sendContact from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].contactMessage(data); - } + public async sendContact({ instanceName }: InstanceDto, data: SendContactDto) { + logger.verbose('requested sendContact from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].contactMessage(data); + } - public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { - logger.verbose('requested sendReaction from ' + instanceName + ' instance'); - if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) { - throw new BadRequestException('"reaction" must be an emoji'); - } - return await this.waMonitor.waInstances[instanceName].reactionMessage(data); + public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) { + logger.verbose('requested sendReaction from ' + instanceName + ' instance'); + if (!data.reactionMessage.reaction.match(/[^()\w\sà-ú"-+]+/)) { + throw new BadRequestException('"reaction" must be an emoji'); } + return await this.waMonitor.waInstances[instanceName].reactionMessage(data); + } - public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { - logger.verbose('requested sendPoll from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].pollMessage(data); - } + public async sendPoll({ instanceName }: InstanceDto, data: SendPollDto) { + logger.verbose('requested sendPoll from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].pollMessage(data); + } - public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) { - logger.verbose('requested sendStatus from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].statusMessage(data); - } + public async sendStatus({ instanceName }: InstanceDto, data: SendStatusDto) { + logger.verbose('requested sendStatus from ' + instanceName + ' instance'); + return await this.waMonitor.waInstances[instanceName].statusMessage(data); + } } diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 1a82a0b2..1a8baafc 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -9,16 +9,16 @@ import { SettingsService } from '../services/settings.service'; const logger = new Logger('SettingsController'); export class SettingsController { - constructor(private readonly settingsService: SettingsService) {} + constructor(private readonly settingsService: SettingsService) {} - public async createSettings(instance: InstanceDto, data: SettingsDto) { - logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); + public async createSettings(instance: InstanceDto, data: SettingsDto) { + logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.create(instance, data); - } + return this.settingsService.create(instance, data); + } - public async findSettings(instance: InstanceDto) { - logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); - return this.settingsService.find(instance); - } + public async findSettings(instance: InstanceDto) { + logger.verbose('requested findSettings from ' + instance.instanceName + ' instance'); + return this.settingsService.find(instance); + } } diff --git a/src/whatsapp/controllers/views.controller.ts b/src/whatsapp/controllers/views.controller.ts index 269ea7de..5f4060ac 100644 --- a/src/whatsapp/controllers/views.controller.ts +++ b/src/whatsapp/controllers/views.controller.ts @@ -7,20 +7,20 @@ import { HttpStatus } from '../routers/index.router'; import { WAMonitoringService } from '../services/monitor.service'; export class ViewsController { - constructor(private readonly waMonit: WAMonitoringService, private readonly configService: ConfigService) {} + constructor(private readonly waMonit: WAMonitoringService, private readonly configService: ConfigService) {} - public async qrcode(request: Request, response: Response) { - try { - const param = request.params as unknown as InstanceDto; - const instance = this.waMonit.waInstances[param.instanceName]; - if (instance.connectionStatus.state === 'open') { - throw new BadRequestException('The instance is already connected'); - } - const type = this.configService.get('AUTHENTICATION').TYPE; + public async qrcode(request: Request, response: Response) { + try { + const param = request.params as unknown as InstanceDto; + const instance = this.waMonit.waInstances[param.instanceName]; + if (instance.connectionStatus.state === 'open') { + throw new BadRequestException('The instance is already connected'); + } + const type = this.configService.get('AUTHENTICATION').TYPE; - return response.status(HttpStatus.OK).render('qrcode', { type, ...param }); - } catch (error) { - console.log('ERROR: ', error); - } + return response.status(HttpStatus.OK).render('qrcode', { type, ...param }); + } catch (error) { + console.log('ERROR: ', error); } + } } diff --git a/src/whatsapp/controllers/webhook.controller.ts b/src/whatsapp/controllers/webhook.controller.ts index 073a22fc..281147db 100644 --- a/src/whatsapp/controllers/webhook.controller.ts +++ b/src/whatsapp/controllers/webhook.controller.ts @@ -9,26 +9,26 @@ import { WebhookService } from '../services/webhook.service'; const logger = new Logger('WebhookController'); export class WebhookController { - constructor(private readonly webhookService: WebhookService) {} + constructor(private readonly webhookService: WebhookService) {} - public async createWebhook(instance: InstanceDto, data: WebhookDto) { - logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); + public async createWebhook(instance: InstanceDto, data: WebhookDto) { + logger.verbose('requested createWebhook from ' + instance.instanceName + ' instance'); - if (data.enabled && !isURL(data.url, { require_tld: false })) { - throw new BadRequestException('Invalid "url" property'); - } - - if (!data.enabled) { - logger.verbose('webhook disabled'); - data.url = ''; - data.events = []; - } - - return this.webhookService.create(instance, data); + if (data.enabled && !isURL(data.url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property'); } - public async findWebhook(instance: InstanceDto) { - logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); - return this.webhookService.find(instance); + if (!data.enabled) { + logger.verbose('webhook disabled'); + data.url = ''; + data.events = []; } + + return this.webhookService.create(instance, data); + } + + public async findWebhook(instance: InstanceDto) { + logger.verbose('requested findWebhook from ' + instance.instanceName + ' instance'); + return this.webhookService.find(instance); + } } diff --git a/src/whatsapp/dto/chat.dto.ts b/src/whatsapp/dto/chat.dto.ts index 487a30d4..f2f9b1cc 100644 --- a/src/whatsapp/dto/chat.dto.ts +++ b/src/whatsapp/dto/chat.dto.ts @@ -1,84 +1,84 @@ import { proto, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue } from '@whiskeysockets/baileys'; export class OnWhatsAppDto { - constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} + constructor(public readonly jid: string, public readonly exists: boolean, public readonly name?: string) {} } export class getBase64FromMediaMessageDto { - message: proto.WebMessageInfo; - convertToMp4?: boolean; + message: proto.WebMessageInfo; + convertToMp4?: boolean; } export class WhatsAppNumberDto { - numbers: string[]; + numbers: string[]; } export class NumberDto { - number: string; + number: string; } export class NumberBusiness { - wid?: string; - jid?: string; - exists?: boolean; - isBusiness: boolean; - name?: string; - message?: string; - description?: string; - email?: string; - website?: string[]; - address?: string; + wid?: string; + jid?: string; + exists?: boolean; + isBusiness: boolean; + name?: string; + message?: string; + description?: string; + email?: string; + website?: string[]; + address?: string; } export class ProfileNameDto { - name: string; + name: string; } export class ProfileStatusDto { - status: string; + status: string; } export class ProfilePictureDto { - number?: string; - // url or base64 - picture?: string; + number?: string; + // url or base64 + picture?: string; } class Key { - id: string; - fromMe: boolean; - remoteJid: string; + id: string; + fromMe: boolean; + remoteJid: string; } export class ReadMessageDto { - read_messages: Key[]; + read_messages: Key[]; } class LastMessage { - key: Key; - messageTimestamp?: number; + key: Key; + messageTimestamp?: number; } export class ArchiveChatDto { - lastMessage: LastMessage; - archive: boolean; + lastMessage: LastMessage; + archive: boolean; } class PrivacySetting { - readreceipts: WAReadReceiptsValue; - profile: WAPrivacyValue; - status: WAPrivacyValue; - online: WAPrivacyOnlineValue; - last: WAPrivacyValue; - groupadd: WAPrivacyValue; + readreceipts: WAReadReceiptsValue; + profile: WAPrivacyValue; + status: WAPrivacyValue; + online: WAPrivacyOnlineValue; + last: WAPrivacyValue; + groupadd: WAPrivacyValue; } export class PrivacySettingDto { - privacySettings: PrivacySetting; + privacySettings: PrivacySetting; } export class DeleteMessage { - id: string; - fromMe: boolean; - remoteJid: string; - participant?: string; + id: string; + fromMe: boolean; + remoteJid: string; + participant?: string; } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index 212e0090..b270c869 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -1,11 +1,11 @@ export class ChatwootDto { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index 62a1b890..6dfdc45c 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -1,52 +1,52 @@ export class CreateGroupDto { - subject: string; - participants: string[]; - description?: string; - promoteParticipants?: boolean; + subject: string; + participants: string[]; + description?: string; + promoteParticipants?: boolean; } export class GroupPictureDto { - groupJid: string; - image: string; + groupJid: string; + image: string; } export class GroupSubjectDto { - groupJid: string; - subject: string; + groupJid: string; + subject: string; } export class GroupDescriptionDto { - groupJid: string; - description: string; + groupJid: string; + description: string; } export class GroupJid { - groupJid: string; + groupJid: string; } export class GetParticipant { - getParticipants: string; + getParticipants: string; } export class GroupInvite { - inviteCode: string; + inviteCode: string; } export class GroupSendInvite { - groupJid: string; - description: string; - numbers: string[]; + groupJid: string; + description: string; + numbers: string[]; } export class GroupUpdateParticipantDto extends GroupJid { - action: 'add' | 'remove' | 'promote' | 'demote'; - participants: string[]; + action: 'add' | 'remove' | 'promote' | 'demote'; + participants: string[]; } export class GroupUpdateSettingDto extends GroupJid { - action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; + action: 'announcement' | 'not_announcement' | 'unlocked' | 'locked'; } export class GroupToggleEphemeralDto extends GroupJid { - expiration: 0 | 86400 | 604800 | 7776000; + expiration: 0 | 86400 | 604800 | 7776000; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 827b1243..c317060f 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -1,21 +1,21 @@ export class InstanceDto { - instanceName: string; - qrcode?: boolean; - number?: string; - token?: string; - webhook?: string; - webhook_by_events?: boolean; - events?: string[]; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; - chatwoot_account_id?: string; - chatwoot_token?: string; - chatwoot_url?: string; - chatwoot_sign_msg?: boolean; - chatwoot_reopen_conversation?: boolean; - chatwoot_conversation_pending?: boolean; + instanceName: string; + qrcode?: boolean; + number?: string; + token?: string; + webhook?: string; + webhook_by_events?: boolean; + events?: string[]; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; + chatwoot_sign_msg?: boolean; + chatwoot_reopen_conversation?: boolean; + chatwoot_conversation_pending?: boolean; } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index 754d66e2..c2ddb3a2 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -1,150 +1,150 @@ import { proto, WAPresence } from '@whiskeysockets/baileys'; export class Quoted { - key: proto.IMessageKey; - message: proto.IMessage; + key: proto.IMessageKey; + message: proto.IMessage; } export class Mentions { - everyOne: boolean; - mentioned: string[]; + everyOne: boolean; + mentioned: string[]; } export class Options { - delay?: number; - presence?: WAPresence; - quoted?: Quoted; - mentions?: Mentions; - linkPreview?: boolean; - encoding?: boolean; + delay?: number; + presence?: WAPresence; + quoted?: Quoted; + mentions?: Mentions; + linkPreview?: boolean; + encoding?: boolean; } class OptionsMessage { - options: Options; + options: Options; } export class Metadata extends OptionsMessage { - number: string; + number: string; } class TextMessage { - text: string; + text: string; } export class StatusMessage { - type: string; - content: string; - statusJidList?: string[]; - allContacts?: boolean; - caption?: string; - backgroundColor?: string; - font?: number; + type: string; + content: string; + statusJidList?: string[]; + allContacts?: boolean; + caption?: string; + backgroundColor?: string; + font?: number; } class PollMessage { - name: string; - selectableCount: number; - values: string[]; - messageSecret?: Uint8Array; + name: string; + selectableCount: number; + values: string[]; + messageSecret?: Uint8Array; } export class SendTextDto extends Metadata { - textMessage: TextMessage; + textMessage: TextMessage; } export class SendStatusDto extends Metadata { - statusMessage: StatusMessage; + statusMessage: StatusMessage; } export class SendPollDto extends Metadata { - pollMessage: PollMessage; + pollMessage: PollMessage; } export type MediaType = 'image' | 'document' | 'video' | 'audio'; export class MediaMessage { - mediatype: MediaType; - caption?: string; - // for document - fileName?: string; - // url or base64 - media: string; + mediatype: MediaType; + caption?: string; + // for document + fileName?: string; + // url or base64 + media: string; } export class SendMediaDto extends Metadata { - mediaMessage: MediaMessage; + mediaMessage: MediaMessage; } class Sticker { - image: string; + image: string; } export class SendStickerDto extends Metadata { - stickerMessage: Sticker; + stickerMessage: Sticker; } class Audio { - audio: string; + audio: string; } export class SendAudioDto extends Metadata { - audioMessage: Audio; + audioMessage: Audio; } class Button { - buttonText: string; - buttonId: string; + buttonText: string; + buttonId: string; } class ButtonMessage { - title: string; - description: string; - footerText?: string; - buttons: Button[]; - mediaMessage?: MediaMessage; + title: string; + description: string; + footerText?: string; + buttons: Button[]; + mediaMessage?: MediaMessage; } export class SendButtonDto extends Metadata { - buttonMessage: ButtonMessage; + buttonMessage: ButtonMessage; } class LocationMessage { - latitude: number; - longitude: number; - name?: string; - address?: string; + latitude: number; + longitude: number; + name?: string; + address?: string; } export class SendLocationDto extends Metadata { - locationMessage: LocationMessage; + locationMessage: LocationMessage; } class Row { - title: string; - description: string; - rowId: string; + title: string; + description: string; + rowId: string; } class Section { - title: string; - rows: Row[]; + title: string; + rows: Row[]; } class ListMessage { - title: string; - description: string; - footerText?: string; - buttonText: string; - sections: Section[]; + title: string; + description: string; + footerText?: string; + buttonText: string; + sections: Section[]; } export class SendListDto extends Metadata { - listMessage: ListMessage; + listMessage: ListMessage; } export class ContactMessage { - fullName: string; - wuid: string; - phoneNumber: string; - organization?: string; - email?: string; - url?: string; + fullName: string; + wuid: string; + phoneNumber: string; + organization?: string; + email?: string; + url?: string; } export class SendContactDto extends Metadata { - contactMessage: ContactMessage[]; + contactMessage: ContactMessage[]; } class ReactionMessage { - key: proto.IMessageKey; - reaction: string; + key: proto.IMessageKey; + reaction: string; } export class SendReactionDto { - reactionMessage: ReactionMessage; + reactionMessage: ReactionMessage; } diff --git a/src/whatsapp/dto/settings.dto.ts b/src/whatsapp/dto/settings.dto.ts index 9094b888..594ab3a4 100644 --- a/src/whatsapp/dto/settings.dto.ts +++ b/src/whatsapp/dto/settings.dto.ts @@ -1,8 +1,8 @@ export class SettingsDto { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } diff --git a/src/whatsapp/dto/webhook.dto.ts b/src/whatsapp/dto/webhook.dto.ts index b41ec0e6..5203884d 100644 --- a/src/whatsapp/dto/webhook.dto.ts +++ b/src/whatsapp/dto/webhook.dto.ts @@ -1,6 +1,6 @@ export class WebhookDto { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; } diff --git a/src/whatsapp/guards/auth.guard.ts b/src/whatsapp/guards/auth.guard.ts index f4d30fc1..72148885 100644 --- a/src/whatsapp/guards/auth.guard.ts +++ b/src/whatsapp/guards/auth.guard.ts @@ -13,77 +13,71 @@ import { repository } from '../whatsapp.module'; const logger = new Logger('GUARD'); async function jwtGuard(req: Request, res: Response, next: NextFunction) { - const key = req.get('apikey'); + const key = req.get('apikey'); - if (key && configService.get('AUTHENTICATION').API_KEY.KEY !== key) { - throw new UnauthorizedException(); + if (key && configService.get('AUTHENTICATION').API_KEY.KEY !== key) { + throw new UnauthorizedException(); + } + + if (configService.get('AUTHENTICATION').API_KEY.KEY === key) { + return next(); + } + + if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { + throw new ForbiddenException('Missing global api key', 'The global api key must be set'); + } + + const jwtOpts = configService.get('AUTHENTICATION').JWT; + try { + const [bearer, token] = req.get('authorization').split(' '); + + if (bearer.toLowerCase() !== 'bearer') { + throw new UnauthorizedException(); } - if (configService.get('AUTHENTICATION').API_KEY.KEY === key) { - return next(); + if (!isJWT(token)) { + throw new UnauthorizedException(); } - if ( - (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && - !key - ) { - throw new ForbiddenException('Missing global api key', 'The global api key must be set'); + const param = req.params as unknown as InstanceDto; + const decode = jwt.verify(token, jwtOpts.SECRET, { + ignoreExpiration: jwtOpts.EXPIRIN_IN === 0, + }) as JwtPayload; + + if (param.instanceName !== decode.instanceName || name !== decode.apiName) { + throw new UnauthorizedException(); } - const jwtOpts = configService.get('AUTHENTICATION').JWT; - try { - const [bearer, token] = req.get('authorization').split(' '); - - if (bearer.toLowerCase() !== 'bearer') { - throw new UnauthorizedException(); - } - - if (!isJWT(token)) { - throw new UnauthorizedException(); - } - - const param = req.params as unknown as InstanceDto; - const decode = jwt.verify(token, jwtOpts.SECRET, { - ignoreExpiration: jwtOpts.EXPIRIN_IN === 0, - }) as JwtPayload; - - if (param.instanceName !== decode.instanceName || name !== decode.apiName) { - throw new UnauthorizedException(); - } - - return next(); - } catch (error) { - logger.error(error); - throw new UnauthorizedException(); - } + return next(); + } catch (error) { + logger.error(error); + throw new UnauthorizedException(); + } } async function apikey(req: Request, res: Response, next: NextFunction) { - const env = configService.get('AUTHENTICATION').API_KEY; - const key = req.get('apikey'); + const env = configService.get('AUTHENTICATION').API_KEY; + const key = req.get('apikey'); - if (env.KEY === key) { - return next(); + if (env.KEY === key) { + return next(); + } + + if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { + throw new ForbiddenException('Missing global api key', 'The global api key must be set'); + } + + try { + const param = req.params as unknown as InstanceDto; + const instanceKey = await repository.auth.find(param.instanceName); + if (instanceKey.apikey === key) { + return next(); } + } catch (error) { + logger.error(error); + } - if ( - (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && - !key - ) { - throw new ForbiddenException('Missing global api key', 'The global api key must be set'); - } - - try { - const param = req.params as unknown as InstanceDto; - const instanceKey = await repository.auth.find(param.instanceName); - if (instanceKey.apikey === key) { - return next(); - } - } catch (error) { - logger.error(error); - } - - throw new UnauthorizedException(); + throw new UnauthorizedException(); } export const authGuard = { jwt: jwtGuard, apikey }; diff --git a/src/whatsapp/guards/instance.guard.ts b/src/whatsapp/guards/instance.guard.ts index 6d65c743..69d20414 100644 --- a/src/whatsapp/guards/instance.guard.ts +++ b/src/whatsapp/guards/instance.guard.ts @@ -10,55 +10,55 @@ import { InstanceDto } from '../dto/instance.dto'; import { cache, waMonitor } from '../whatsapp.module'; async function getInstance(instanceName: string) { - const db = configService.get('DATABASE'); - const redisConf = configService.get('REDIS'); + const db = configService.get('DATABASE'); + const redisConf = configService.get('REDIS'); - const exists = !!waMonitor.waInstances[instanceName]; + const exists = !!waMonitor.waInstances[instanceName]; - if (redisConf.ENABLED) { - const keyExists = await cache.keyExists(); - return exists || keyExists; - } + if (redisConf.ENABLED) { + const keyExists = await cache.keyExists(); + return exists || keyExists; + } - if (db.ENABLED) { - const collection = dbserver - .getClient() - .db(db.CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(instanceName); - return exists || (await collection.find({}).toArray()).length > 0; - } + if (db.ENABLED) { + const collection = dbserver + .getClient() + .db(db.CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(instanceName); + return exists || (await collection.find({}).toArray()).length > 0; + } - return exists || existsSync(join(INSTANCE_DIR, instanceName)); + return exists || existsSync(join(INSTANCE_DIR, instanceName)); } export async function instanceExistsGuard(req: Request, _: Response, next: NextFunction) { - if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) { - return next(); - } + if (req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) { + return next(); + } - const param = req.params as unknown as InstanceDto; - if (!param?.instanceName) { - throw new BadRequestException('"instanceName" not provided.'); - } + const param = req.params as unknown as InstanceDto; + if (!param?.instanceName) { + throw new BadRequestException('"instanceName" not provided.'); + } - if (!(await getInstance(param.instanceName))) { - throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); - } + if (!(await getInstance(param.instanceName))) { + throw new NotFoundException(`The "${param.instanceName}" instance does not exist`); + } - next(); + next(); } export async function instanceLoggedGuard(req: Request, _: Response, next: NextFunction) { - if (req.originalUrl.includes('/instance/create')) { - const instance = req.body as InstanceDto; - if (await getInstance(instance.instanceName)) { - throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); - } - - if (waMonitor.waInstances[instance.instanceName]) { - delete waMonitor.waInstances[instance.instanceName]; - } + if (req.originalUrl.includes('/instance/create')) { + const instance = req.body as InstanceDto; + if (await getInstance(instance.instanceName)) { + throw new ForbiddenException(`This name "${instance.instanceName}" is already in use.`); } - next(); + if (waMonitor.waInstances[instance.instanceName]) { + delete waMonitor.waInstances[instance.instanceName]; + } + } + + next(); } diff --git a/src/whatsapp/models/auth.model.ts b/src/whatsapp/models/auth.model.ts index c9e48da1..5c5b6a41 100644 --- a/src/whatsapp/models/auth.model.ts +++ b/src/whatsapp/models/auth.model.ts @@ -3,15 +3,15 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class AuthRaw { - _id?: string; - jwt?: string; - apikey?: string; + _id?: string; + jwt?: string; + apikey?: string; } const authSchema = new Schema({ - _id: { type: String, _id: true }, - jwt: { type: String, minlength: 1 }, - apikey: { type: String, minlength: 1 }, + _id: { type: String, _id: true }, + jwt: { type: String, minlength: 1 }, + apikey: { type: String, minlength: 1 }, }); export const AuthModel = dbserver?.model(AuthRaw.name, authSchema, 'authentication'); diff --git a/src/whatsapp/models/chat.model.ts b/src/whatsapp/models/chat.model.ts index d44a4673..20153603 100644 --- a/src/whatsapp/models/chat.model.ts +++ b/src/whatsapp/models/chat.model.ts @@ -3,16 +3,16 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ChatRaw { - _id?: string; - id?: string; - owner: string; - lastMsgTimestamp?: number; + _id?: string; + id?: string; + owner: string; + lastMsgTimestamp?: number; } const chatSchema = new Schema({ - _id: { type: String, _id: true }, - id: { type: String, required: true, minlength: 1 }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + id: { type: String, required: true, minlength: 1 }, + owner: { type: String, required: true, minlength: 1 }, }); export const ChatModel = dbserver?.model(ChatRaw.name, chatSchema, 'chats'); diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index 3b1a99a2..d72f6e74 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -3,27 +3,27 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { - _id?: string; - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; + _id?: string; + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; } const chatwootSchema = new Schema({ - _id: { type: String, _id: true }, - enabled: { type: Boolean, required: true }, - account_id: { type: String, required: true }, - token: { type: String, required: true }, - url: { type: String, required: true }, - name_inbox: { type: String, required: true }, - sign_msg: { type: Boolean, required: true }, - number: { type: String, required: true }, + _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, + account_id: { type: String, required: true }, + token: { type: String, required: true }, + url: { type: String, required: true }, + name_inbox: { type: String, required: true }, + sign_msg: { type: Boolean, required: true }, + number: { type: String, required: true }, }); export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot'); diff --git a/src/whatsapp/models/contact.model.ts b/src/whatsapp/models/contact.model.ts index 84749945..d9b51e1e 100644 --- a/src/whatsapp/models/contact.model.ts +++ b/src/whatsapp/models/contact.model.ts @@ -3,19 +3,19 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class ContactRaw { - _id?: string; - pushName?: string; - id?: string; - profilePictureUrl?: string; - owner: string; + _id?: string; + pushName?: string; + id?: string; + profilePictureUrl?: string; + owner: string; } const contactSchema = new Schema({ - _id: { type: String, _id: true }, - pushName: { type: String, minlength: 1 }, - id: { type: String, required: true, minlength: 1 }, - profilePictureUrl: { type: String, minlength: 1 }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + pushName: { type: String, minlength: 1 }, + id: { type: String, required: true, minlength: 1 }, + profilePictureUrl: { type: String, minlength: 1 }, + owner: { type: String, required: true, minlength: 1 }, }); export const ContactModel = dbserver?.model(ContactRaw.name, contactSchema, 'contacts'); diff --git a/src/whatsapp/models/message.model.ts b/src/whatsapp/models/message.model.ts index bfa048cf..764b04d9 100644 --- a/src/whatsapp/models/message.model.ts +++ b/src/whatsapp/models/message.model.ts @@ -4,65 +4,65 @@ import { dbserver } from '../../db/db.connect'; import { wa } from '../types/wa.types'; class Key { - id?: string; - remoteJid?: string; - fromMe?: boolean; - participant?: string; + id?: string; + remoteJid?: string; + fromMe?: boolean; + participant?: string; } export class MessageRaw { - _id?: string; - key?: Key; - pushName?: string; - participant?: string; - message?: object; - messageType?: string; - messageTimestamp?: number | Long.Long; - owner: string; - source?: 'android' | 'web' | 'ios'; + _id?: string; + key?: Key; + pushName?: string; + participant?: string; + message?: object; + messageType?: string; + messageTimestamp?: number | Long.Long; + owner: string; + source?: 'android' | 'web' | 'ios'; } const messageSchema = new Schema({ - _id: { type: String, _id: true }, - key: { - id: { type: String, required: true, minlength: 1 }, - remoteJid: { type: String, required: true, minlength: 1 }, - fromMe: { type: Boolean, required: true }, - participant: { type: String, minlength: 1 }, - }, - pushName: { type: String }, - participant: { type: String }, - messageType: { type: String }, - message: { type: Object }, - source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, - messageTimestamp: { type: Number, required: true }, - owner: { type: String, required: true, minlength: 1 }, + _id: { type: String, _id: true }, + key: { + id: { type: String, required: true, minlength: 1 }, + remoteJid: { type: String, required: true, minlength: 1 }, + fromMe: { type: Boolean, required: true }, + participant: { type: String, minlength: 1 }, + }, + pushName: { type: String }, + participant: { type: String }, + messageType: { type: String }, + message: { type: Object }, + source: { type: String, minlength: 3, enum: ['android', 'web', 'ios'] }, + messageTimestamp: { type: Number, required: true }, + owner: { type: String, required: true, minlength: 1 }, }); export const MessageModel = dbserver?.model(MessageRaw.name, messageSchema, 'messages'); export type IMessageModel = typeof MessageModel; export class MessageUpdateRaw { - _id?: string; - remoteJid?: string; - id?: string; - fromMe?: boolean; - participant?: string; - datetime?: number; - status?: wa.StatusMessage; - owner: string; - pollUpdates?: any; + _id?: string; + remoteJid?: string; + id?: string; + fromMe?: boolean; + participant?: string; + datetime?: number; + status?: wa.StatusMessage; + owner: string; + pollUpdates?: any; } const messageUpdateSchema = new Schema({ - _id: { type: String, _id: true }, - remoteJid: { type: String, required: true, min: 1 }, - id: { type: String, required: true, min: 1 }, - fromMe: { type: Boolean, required: true }, - participant: { type: String, min: 1 }, - datetime: { type: Number, required: true, min: 1 }, - status: { type: String, required: true }, - owner: { type: String, required: true, min: 1 }, + _id: { type: String, _id: true }, + remoteJid: { type: String, required: true, min: 1 }, + id: { type: String, required: true, min: 1 }, + fromMe: { type: Boolean, required: true }, + participant: { type: String, min: 1 }, + datetime: { type: Number, required: true, min: 1 }, + status: { type: String, required: true }, + owner: { type: String, required: true, min: 1 }, }); export const MessageUpModel = dbserver?.model(MessageUpdateRaw.name, messageUpdateSchema, 'messageUpdate'); diff --git a/src/whatsapp/models/settings.model.ts b/src/whatsapp/models/settings.model.ts index 935f8f3d..c928be42 100644 --- a/src/whatsapp/models/settings.model.ts +++ b/src/whatsapp/models/settings.model.ts @@ -3,23 +3,23 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class SettingsRaw { - _id?: string; - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; + _id?: string; + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; } const settingsSchema = new Schema({ - _id: { type: String, _id: true }, - reject_call: { type: Boolean, required: true }, - msg_call: { type: String, required: true }, - groups_ignore: { type: Boolean, required: true }, - always_online: { type: Boolean, required: true }, - read_messages: { type: Boolean, required: true }, - read_status: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + reject_call: { type: Boolean, required: true }, + msg_call: { type: String, required: true }, + groups_ignore: { type: Boolean, required: true }, + always_online: { type: Boolean, required: true }, + read_messages: { type: Boolean, required: true }, + read_status: { type: Boolean, required: true }, }); export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings'); diff --git a/src/whatsapp/models/webhook.model.ts b/src/whatsapp/models/webhook.model.ts index 57d55a7b..fa91326c 100644 --- a/src/whatsapp/models/webhook.model.ts +++ b/src/whatsapp/models/webhook.model.ts @@ -3,19 +3,19 @@ import { Schema } from 'mongoose'; import { dbserver } from '../../db/db.connect'; export class WebhookRaw { - _id?: string; - url?: string; - enabled?: boolean; - events?: string[]; - webhook_by_events?: boolean; + _id?: string; + url?: string; + enabled?: boolean; + events?: string[]; + webhook_by_events?: boolean; } const webhookSchema = new Schema({ - _id: { type: String, _id: true }, - url: { type: String, required: true }, - enabled: { type: Boolean, required: true }, - events: { type: [String], required: true }, - webhook_by_events: { type: Boolean, required: true }, + _id: { type: String, _id: true }, + url: { type: String, required: true }, + enabled: { type: Boolean, required: true }, + events: { type: [String], required: true }, + webhook_by_events: { type: Boolean, required: true }, }); export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook'); diff --git a/src/whatsapp/repository/auth.repository.ts b/src/whatsapp/repository/auth.repository.ts index b93ea86d..4da8980b 100644 --- a/src/whatsapp/repository/auth.repository.ts +++ b/src/whatsapp/repository/auth.repository.ts @@ -8,58 +8,58 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { AuthRaw, IAuthModel } from '../models'; export class AuthRepository extends Repository { - constructor(private readonly authModel: IAuthModel, readonly configService: ConfigService) { - super(configService); - this.auth = configService.get('AUTHENTICATION'); + constructor(private readonly authModel: IAuthModel, readonly configService: ConfigService) { + super(configService); + this.auth = configService.get('AUTHENTICATION'); + } + + private readonly auth: Auth; + private readonly logger = new Logger('AuthRepository'); + + public async create(data: AuthRaw, instance: string): Promise { + try { + this.logger.verbose('creating auth'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving auth to db'); + const insert = await this.authModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('auth saved to db: ' + insert.modifiedCount + ' auth'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving auth to store'); + + this.writeStore({ + path: join(AUTH_DIR, this.auth.TYPE), + fileName: instance, + data, + }); + this.logger.verbose('auth saved to store in path: ' + join(AUTH_DIR, this.auth.TYPE) + '/' + instance); + + this.logger.verbose('auth created'); + return { insertCount: 1 }; + } catch (error) { + return { error } as any; } + } - private readonly auth: Auth; - private readonly logger = new Logger('AuthRepository'); + public async find(instance: string): Promise { + try { + this.logger.verbose('finding auth'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding auth in db'); + return await this.authModel.findOne({ _id: instance }); + } - public async create(data: AuthRaw, instance: string): Promise { - try { - this.logger.verbose('creating auth'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving auth to db'); - const insert = await this.authModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + this.logger.verbose('finding auth in store'); - this.logger.verbose('auth saved to db: ' + insert.modifiedCount + ' auth'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving auth to store'); - - this.writeStore({ - path: join(AUTH_DIR, this.auth.TYPE), - fileName: instance, - data, - }); - this.logger.verbose('auth saved to store in path: ' + join(AUTH_DIR, this.auth.TYPE) + '/' + instance); - - this.logger.verbose('auth created'); - return { insertCount: 1 }; - } catch (error) { - return { error } as any; - } - } - - public async find(instance: string): Promise { - try { - this.logger.verbose('finding auth'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding auth in db'); - return await this.authModel.findOne({ _id: instance }); - } - - this.logger.verbose('finding auth in store'); - - return JSON.parse( - readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), { - encoding: 'utf-8', - }), - ) as AuthRaw; - } catch (error) { - return {}; - } + return JSON.parse( + readFileSync(join(AUTH_DIR, this.auth.TYPE, instance + '.json'), { + encoding: 'utf-8', + }), + ) as AuthRaw; + } catch (error) { + return {}; } + } } diff --git a/src/whatsapp/repository/chat.repository.ts b/src/whatsapp/repository/chat.repository.ts index b3ff575a..68d653a4 100644 --- a/src/whatsapp/repository/chat.repository.ts +++ b/src/whatsapp/repository/chat.repository.ts @@ -7,111 +7,111 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ChatRaw, IChatModel } from '../models'; export class ChatQuery { - where: ChatRaw; + where: ChatRaw; } export class ChatRepository extends Repository { - constructor(private readonly chatModel: IChatModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly chatModel: IChatModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('ChatRepository'); + + public async insert(data: ChatRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting chats'); + if (data.length === 0) { + this.logger.verbose('no chats to insert'); + return; } - private readonly logger = new Logger('ChatRepository'); + try { + this.logger.verbose('saving chats to store'); + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving chats to db'); + const insert = await this.chatModel.insertMany([...data]); - public async insert(data: ChatRaw[], instanceName: string, saveDb = false): Promise { - this.logger.verbose('inserting chats'); - if (data.length === 0) { - this.logger.verbose('no chats to insert'); - return; - } + this.logger.verbose('chats saved to db: ' + insert.length + ' chats'); + return { insertCount: insert.length }; + } - try { - this.logger.verbose('saving chats to store'); - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving chats to db'); - const insert = await this.chatModel.insertMany([...data]); + this.logger.verbose('saving chats to store'); - this.logger.verbose('chats saved to db: ' + insert.length + ' chats'); - return { insertCount: insert.length }; - } + const store = this.configService.get('STORE'); - this.logger.verbose('saving chats to store'); + if (store.CHATS) { + this.logger.verbose('saving chats to store'); + data.forEach((chat) => { + this.writeStore({ + path: join(this.storePath, 'chats', instanceName), + fileName: chat.id, + data: chat, + }); + this.logger.verbose( + 'chats saved to store in path: ' + join(this.storePath, 'chats', instanceName) + '/' + chat.id, + ); + }); - const store = this.configService.get('STORE'); + this.logger.verbose('chats saved to store'); + return { insertCount: data.length }; + } - if (store.CHATS) { - this.logger.verbose('saving chats to store'); - data.forEach((chat) => { - this.writeStore({ - path: join(this.storePath, 'chats', instanceName), - fileName: chat.id, - data: chat, - }); - this.logger.verbose( - 'chats saved to store in path: ' + join(this.storePath, 'chats', instanceName) + '/' + chat.id, - ); - }); - - this.logger.verbose('chats saved to store'); - return { insertCount: data.length }; - } - - this.logger.verbose('chats not saved to store'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } + this.logger.verbose('chats not saved to store'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; } + } - public async find(query: ChatQuery): Promise { - try { - this.logger.verbose('finding chats'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding chats in db'); - return await this.chatModel.find({ owner: query.where.owner }); - } + public async find(query: ChatQuery): Promise { + try { + this.logger.verbose('finding chats'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chats in db'); + return await this.chatModel.find({ owner: query.where.owner }); + } - this.logger.verbose('finding chats in store'); + this.logger.verbose('finding chats in store'); - const chats: ChatRaw[] = []; - const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner)); - for await (const dirent of openDir) { - if (dirent.isFile()) { - chats.push( - JSON.parse( - readFileSync(join(this.storePath, 'chats', query.where.owner, dirent.name), { - encoding: 'utf-8', - }), - ), - ); - } - } - - this.logger.verbose('chats found in store: ' + chats.length + ' chats'); - return chats; - } catch (error) { - return []; + const chats: ChatRaw[] = []; + const openDir = opendirSync(join(this.storePath, 'chats', query.where.owner)); + for await (const dirent of openDir) { + if (dirent.isFile()) { + chats.push( + JSON.parse( + readFileSync(join(this.storePath, 'chats', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); } + } + + this.logger.verbose('chats found in store: ' + chats.length + ' chats'); + return chats; + } catch (error) { + return []; } + } - public async delete(query: ChatQuery) { - try { - this.logger.verbose('deleting chats'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('deleting chats in db'); - return await this.chatModel.deleteOne({ ...query.where }); - } + public async delete(query: ChatQuery) { + try { + this.logger.verbose('deleting chats'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('deleting chats in db'); + return await this.chatModel.deleteOne({ ...query.where }); + } - this.logger.verbose('deleting chats in store'); - rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), { - force: true, - recursive: true, - }); + this.logger.verbose('deleting chats in store'); + rmSync(join(this.storePath, 'chats', query.where.owner, query.where.id + '.josn'), { + force: true, + recursive: true, + }); - return { deleted: { chatId: query.where.id } }; - } catch (error) { - return { error: error?.toString() }; - } + return { deleted: { chatId: query.where.id } }; + } catch (error) { + return { error: error?.toString() }; } + } } diff --git a/src/whatsapp/repository/chatwoot.repository.ts b/src/whatsapp/repository/chatwoot.repository.ts index 6cc3f631..47398d68 100644 --- a/src/whatsapp/repository/chatwoot.repository.ts +++ b/src/whatsapp/repository/chatwoot.repository.ts @@ -7,58 +7,56 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ChatwootRaw, IChatwootModel } from '../models'; export class ChatwootRepository extends Repository { - constructor(private readonly chatwootModel: IChatwootModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly chatwootModel: IChatwootModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('ChatwootRepository'); + + public async create(data: ChatwootRaw, instance: string): Promise { + try { + this.logger.verbose('creating chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving chatwoot to db'); + const insert = await this.chatwootModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving chatwoot to store'); + + this.writeStore({ + path: join(this.storePath, 'chatwoot'), + fileName: instance, + data, + }); + + this.logger.verbose('chatwoot saved to store in path: ' + join(this.storePath, 'chatwoot') + '/' + instance); + + this.logger.verbose('chatwoot created'); + return { insertCount: 1 }; + } catch (error) { + return error; } + } - private readonly logger = new Logger('ChatwootRepository'); + public async find(instance: string): Promise { + try { + this.logger.verbose('finding chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chatwoot in db'); + return await this.chatwootModel.findOne({ _id: instance }); + } - public async create(data: ChatwootRaw, instance: string): Promise { - try { - this.logger.verbose('creating chatwoot'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving chatwoot to db'); - const insert = await this.chatwootModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); - - this.logger.verbose('chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving chatwoot to store'); - - this.writeStore({ - path: join(this.storePath, 'chatwoot'), - fileName: instance, - data, - }); - - this.logger.verbose( - 'chatwoot saved to store in path: ' + join(this.storePath, 'chatwoot') + '/' + instance, - ); - - this.logger.verbose('chatwoot created'); - return { insertCount: 1 }; - } catch (error) { - return error; - } - } - - public async find(instance: string): Promise { - try { - this.logger.verbose('finding chatwoot'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding chatwoot in db'); - return await this.chatwootModel.findOne({ _id: instance }); - } - - this.logger.verbose('finding chatwoot in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), { - encoding: 'utf-8', - }), - ) as ChatwootRaw; - } catch (error) { - return {}; - } + this.logger.verbose('finding chatwoot in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), { + encoding: 'utf-8', + }), + ) as ChatwootRaw; + } catch (error) { + return {}; } + } } diff --git a/src/whatsapp/repository/contact.repository.ts b/src/whatsapp/repository/contact.repository.ts index ed96ff56..03851607 100644 --- a/src/whatsapp/repository/contact.repository.ts +++ b/src/whatsapp/repository/contact.repository.ts @@ -7,171 +7,165 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ContactRaw, IContactModel } from '../models'; export class ContactQuery { - where: ContactRaw; + where: ContactRaw; } export class ContactRepository extends Repository { - constructor(private readonly contactModel: IContactModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly contactModel: IContactModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('ContactRepository'); + + public async insert(data: ContactRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting contacts'); + + if (data.length === 0) { + this.logger.verbose('no contacts to insert'); + return; } - private readonly logger = new Logger('ContactRepository'); + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving contacts to db'); - public async insert(data: ContactRaw[], instanceName: string, saveDb = false): Promise { - this.logger.verbose('inserting contacts'); + const insert = await this.contactModel.insertMany([...data]); - if (data.length === 0) { - this.logger.verbose('no contacts to insert'); - return; - } + this.logger.verbose('contacts saved to db: ' + insert.length + ' contacts'); + return { insertCount: insert.length }; + } - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving contacts to db'); + this.logger.verbose('saving contacts to store'); - const insert = await this.contactModel.insertMany([...data]); + const store = this.configService.get('STORE'); - this.logger.verbose('contacts saved to db: ' + insert.length + ' contacts'); - return { insertCount: insert.length }; - } + if (store.CONTACTS) { + this.logger.verbose('saving contacts to store'); + data.forEach((contact) => { + this.writeStore({ + path: join(this.storePath, 'contacts', instanceName), + fileName: contact.id, + data: contact, + }); + this.logger.verbose( + 'contacts saved to store in path: ' + join(this.storePath, 'contacts', instanceName) + '/' + contact.id, + ); + }); - this.logger.verbose('saving contacts to store'); + this.logger.verbose('contacts saved to store: ' + data.length + ' contacts'); + return { insertCount: data.length }; + } - const store = this.configService.get('STORE'); - - if (store.CONTACTS) { - this.logger.verbose('saving contacts to store'); - data.forEach((contact) => { - this.writeStore({ - path: join(this.storePath, 'contacts', instanceName), - fileName: contact.id, - data: contact, - }); - this.logger.verbose( - 'contacts saved to store in path: ' + - join(this.storePath, 'contacts', instanceName) + - '/' + - contact.id, - ); - }); - - this.logger.verbose('contacts saved to store: ' + data.length + ' contacts'); - return { insertCount: data.length }; - } - - this.logger.verbose('contacts not saved'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } + this.logger.verbose('contacts not saved'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; } + } - public async update(data: ContactRaw[], instanceName: string, saveDb = false): Promise { - try { - this.logger.verbose('updating contacts'); + public async update(data: ContactRaw[], instanceName: string, saveDb = false): Promise { + try { + this.logger.verbose('updating contacts'); - if (data.length === 0) { - this.logger.verbose('no contacts to update'); - return; - } + if (data.length === 0) { + this.logger.verbose('no contacts to update'); + return; + } - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('updating contacts in db'); + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('updating contacts in db'); - const contacts = data.map((contact) => { - return { - updateOne: { - filter: { id: contact.id }, - update: { ...contact }, - upsert: true, - }, - }; - }); + const contacts = data.map((contact) => { + return { + updateOne: { + filter: { id: contact.id }, + update: { ...contact }, + upsert: true, + }, + }; + }); - const { nModified } = await this.contactModel.bulkWrite(contacts); + const { nModified } = await this.contactModel.bulkWrite(contacts); - this.logger.verbose('contacts updated in db: ' + nModified + ' contacts'); - return { insertCount: nModified }; - } + this.logger.verbose('contacts updated in db: ' + nModified + ' contacts'); + return { insertCount: nModified }; + } - this.logger.verbose('updating contacts in store'); + this.logger.verbose('updating contacts in store'); - const store = this.configService.get('STORE'); + const store = this.configService.get('STORE'); - if (store.CONTACTS) { - this.logger.verbose('updating contacts in store'); - data.forEach((contact) => { - this.writeStore({ - path: join(this.storePath, 'contacts', instanceName), - fileName: contact.id, - data: contact, - }); - this.logger.verbose( - 'contacts updated in store in path: ' + - join(this.storePath, 'contacts', instanceName) + - '/' + - contact.id, - ); - }); + if (store.CONTACTS) { + this.logger.verbose('updating contacts in store'); + data.forEach((contact) => { + this.writeStore({ + path: join(this.storePath, 'contacts', instanceName), + fileName: contact.id, + data: contact, + }); + this.logger.verbose( + 'contacts updated in store in path: ' + join(this.storePath, 'contacts', instanceName) + '/' + contact.id, + ); + }); - this.logger.verbose('contacts updated in store: ' + data.length + ' contacts'); + this.logger.verbose('contacts updated in store: ' + data.length + ' contacts'); - return { insertCount: data.length }; - } + return { insertCount: data.length }; + } - this.logger.verbose('contacts not updated'); - return { insertCount: 0 }; - } catch (error) { - return error; - } finally { - data = undefined; - } + this.logger.verbose('contacts not updated'); + return { insertCount: 0 }; + } catch (error) { + return error; + } finally { + data = undefined; } + } - public async find(query: ContactQuery): Promise { - try { - this.logger.verbose('finding contacts'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding contacts in db'); - return await this.contactModel.find({ ...query.where }); - } + public async find(query: ContactQuery): Promise { + try { + this.logger.verbose('finding contacts'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding contacts in db'); + return await this.contactModel.find({ ...query.where }); + } - this.logger.verbose('finding contacts in store'); - const contacts: ContactRaw[] = []; - if (query?.where?.id) { - this.logger.verbose('finding contacts in store by id'); - contacts.push( - JSON.parse( - readFileSync(join(this.storePath, 'contacts', query.where.owner, query.where.id + '.json'), { - encoding: 'utf-8', - }), - ), - ); - } else { - this.logger.verbose('finding contacts in store by owner'); + this.logger.verbose('finding contacts in store'); + const contacts: ContactRaw[] = []; + if (query?.where?.id) { + this.logger.verbose('finding contacts in store by id'); + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.where.owner, query.where.id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + } else { + this.logger.verbose('finding contacts in store by owner'); - const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), { - encoding: 'utf-8', - }); - for await (const dirent of openDir) { - if (dirent.isFile()) { - contacts.push( - JSON.parse( - readFileSync(join(this.storePath, 'contacts', query.where.owner, dirent.name), { - encoding: 'utf-8', - }), - ), - ); - } - } - } - - this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); - return contacts; - } catch (error) { - return []; + const openDir = opendirSync(join(this.storePath, 'contacts', query.where.owner), { + encoding: 'utf-8', + }); + for await (const dirent of openDir) { + if (dirent.isFile()) { + contacts.push( + JSON.parse( + readFileSync(join(this.storePath, 'contacts', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } } + } + + this.logger.verbose('contacts found in store: ' + contacts.length + ' contacts'); + return contacts; + } catch (error) { + return []; } + } } diff --git a/src/whatsapp/repository/message.repository.ts b/src/whatsapp/repository/message.repository.ts index efd57783..ed362815 100644 --- a/src/whatsapp/repository/message.repository.ts +++ b/src/whatsapp/repository/message.repository.ts @@ -7,145 +7,141 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IMessageModel, MessageRaw } from '../models'; export class MessageQuery { - where: MessageRaw; - limit?: number; + where: MessageRaw; + limit?: number; } export class MessageRepository extends Repository { - constructor(private readonly messageModel: IMessageModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly messageModel: IMessageModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('MessageRepository'); + + public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise { + this.logger.verbose('inserting messages'); + + if (!Array.isArray(data) || data.length === 0) { + this.logger.verbose('no messages to insert'); + return; } - private readonly logger = new Logger('MessageRepository'); + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving messages to db'); + const cleanedData = data.map((obj) => { + const cleanedObj = { ...obj }; + if ('extendedTextMessage' in obj.message) { + const extendedTextMessage = obj.message.extendedTextMessage as { + contextInfo?: { + mentionedJid?: any; + }; + }; - public async insert(data: MessageRaw[], instanceName: string, saveDb = false): Promise { - this.logger.verbose('inserting messages'); - - if (!Array.isArray(data) || data.length === 0) { - this.logger.verbose('no messages to insert'); - return; - } - - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving messages to db'); - const cleanedData = data.map((obj) => { - const cleanedObj = { ...obj }; - if ('extendedTextMessage' in obj.message) { - const extendedTextMessage = obj.message.extendedTextMessage as { - contextInfo?: { - mentionedJid?: any; - }; - }; - - if (typeof extendedTextMessage === 'object' && extendedTextMessage !== null) { - if ('contextInfo' in extendedTextMessage) { - delete extendedTextMessage.contextInfo?.mentionedJid; - extendedTextMessage.contextInfo = {}; - } - } - } - return cleanedObj; - }); - - const insert = await this.messageModel.insertMany([...cleanedData]); - - this.logger.verbose('messages saved to db: ' + insert.length + ' messages'); - return { insertCount: insert.length }; + if (typeof extendedTextMessage === 'object' && extendedTextMessage !== null) { + if ('contextInfo' in extendedTextMessage) { + delete extendedTextMessage.contextInfo?.mentionedJid; + extendedTextMessage.contextInfo = {}; + } } + } + return cleanedObj; + }); - this.logger.verbose('saving messages to store'); + const insert = await this.messageModel.insertMany([...cleanedData]); - const store = this.configService.get('STORE'); + this.logger.verbose('messages saved to db: ' + insert.length + ' messages'); + return { insertCount: insert.length }; + } - if (store.MESSAGES) { - this.logger.verbose('saving messages to store'); + this.logger.verbose('saving messages to store'); - data.forEach((message) => { - this.writeStore({ - path: join(this.storePath, 'messages', instanceName), - fileName: message.key.id, - data: message, - }); - this.logger.verbose( - 'messages saved to store in path: ' + - join(this.storePath, 'messages', instanceName) + - '/' + - message.key.id, - ); - }); + const store = this.configService.get('STORE'); - this.logger.verbose('messages saved to store: ' + data.length + ' messages'); - return { insertCount: data.length }; - } + if (store.MESSAGES) { + this.logger.verbose('saving messages to store'); - this.logger.verbose('messages not saved to store'); - return { insertCount: 0 }; - } catch (error) { - console.log('ERROR: ', error); - return error; - } finally { - data = undefined; - } + data.forEach((message) => { + this.writeStore({ + path: join(this.storePath, 'messages', instanceName), + fileName: message.key.id, + data: message, + }); + this.logger.verbose( + 'messages saved to store in path: ' + join(this.storePath, 'messages', instanceName) + '/' + message.key.id, + ); + }); + + this.logger.verbose('messages saved to store: ' + data.length + ' messages'); + return { insertCount: data.length }; + } + + this.logger.verbose('messages not saved to store'); + return { insertCount: 0 }; + } catch (error) { + console.log('ERROR: ', error); + return error; + } finally { + data = undefined; } + } - public async find(query: MessageQuery) { - try { - this.logger.verbose('finding messages'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding messages in db'); - if (query?.where?.key) { - for (const [k, v] of Object.entries(query.where.key)) { - query.where['key.' + k] = v; - } - delete query?.where?.key; - } - - return await this.messageModel - .find({ ...query.where }) - .sort({ messageTimestamp: -1 }) - .limit(query?.limit ?? 0); - } - - this.logger.verbose('finding messages in store'); - const messages: MessageRaw[] = []; - if (query?.where?.key?.id) { - this.logger.verbose('finding messages in store by id'); - messages.push( - JSON.parse( - readFileSync( - join(this.storePath, 'messages', query.where.owner, query.where.key.id + '.json'), - { encoding: 'utf-8' }, - ), - ), - ); - } else { - this.logger.verbose('finding messages in store by owner'); - const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), { - encoding: 'utf-8', - }); - - for await (const dirent of openDir) { - if (dirent.isFile()) { - messages.push( - JSON.parse( - readFileSync(join(this.storePath, 'messages', query.where.owner, dirent.name), { - encoding: 'utf-8', - }), - ), - ); - } - } - } - - this.logger.verbose('messages found in store: ' + messages.length + ' messages'); - return messages - .sort((x, y) => { - return (y.messageTimestamp as number) - (x.messageTimestamp as number); - }) - .splice(0, query?.limit ?? messages.length); - } catch (error) { - return []; + public async find(query: MessageQuery) { + try { + this.logger.verbose('finding messages'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding messages in db'); + if (query?.where?.key) { + for (const [k, v] of Object.entries(query.where.key)) { + query.where['key.' + k] = v; + } + delete query?.where?.key; } + + return await this.messageModel + .find({ ...query.where }) + .sort({ messageTimestamp: -1 }) + .limit(query?.limit ?? 0); + } + + this.logger.verbose('finding messages in store'); + const messages: MessageRaw[] = []; + if (query?.where?.key?.id) { + this.logger.verbose('finding messages in store by id'); + messages.push( + JSON.parse( + readFileSync(join(this.storePath, 'messages', query.where.owner, query.where.key.id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + } else { + this.logger.verbose('finding messages in store by owner'); + const openDir = opendirSync(join(this.storePath, 'messages', query.where.owner), { + encoding: 'utf-8', + }); + + for await (const dirent of openDir) { + if (dirent.isFile()) { + messages.push( + JSON.parse( + readFileSync(join(this.storePath, 'messages', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } + } + } + + this.logger.verbose('messages found in store: ' + messages.length + ' messages'); + return messages + .sort((x, y) => { + return (y.messageTimestamp as number) - (x.messageTimestamp as number); + }) + .splice(0, query?.limit ?? messages.length); + } catch (error) { + return []; } + } } diff --git a/src/whatsapp/repository/messageUp.repository.ts b/src/whatsapp/repository/messageUp.repository.ts index 7a25467f..b97bf59b 100644 --- a/src/whatsapp/repository/messageUp.repository.ts +++ b/src/whatsapp/repository/messageUp.repository.ts @@ -7,117 +7,114 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IMessageUpModel, MessageUpdateRaw } from '../models'; export class MessageUpQuery { - where: MessageUpdateRaw; - limit?: number; + where: MessageUpdateRaw; + limit?: number; } export class MessageUpRepository extends Repository { - constructor(private readonly messageUpModel: IMessageUpModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly messageUpModel: IMessageUpModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('MessageUpRepository'); + + public async insert(data: MessageUpdateRaw[], instanceName: string, saveDb?: boolean): Promise { + this.logger.verbose('inserting message up'); + + if (data.length === 0) { + this.logger.verbose('no message up to insert'); + return; } - private readonly logger = new Logger('MessageUpRepository'); + try { + if (this.dbSettings.ENABLED && saveDb) { + this.logger.verbose('saving message up to db'); + const insert = await this.messageUpModel.insertMany([...data]); - public async insert(data: MessageUpdateRaw[], instanceName: string, saveDb?: boolean): Promise { - this.logger.verbose('inserting message up'); + this.logger.verbose('message up saved to db: ' + insert.length + ' message up'); + return { insertCount: insert.length }; + } - if (data.length === 0) { - this.logger.verbose('no message up to insert'); - return; - } + this.logger.verbose('saving message up to store'); - try { - if (this.dbSettings.ENABLED && saveDb) { - this.logger.verbose('saving message up to db'); - const insert = await this.messageUpModel.insertMany([...data]); + const store = this.configService.get('STORE'); - this.logger.verbose('message up saved to db: ' + insert.length + ' message up'); - return { insertCount: insert.length }; - } + if (store.MESSAGE_UP) { + this.logger.verbose('saving message up to store'); + data.forEach((update) => { + this.writeStore({ + path: join(this.storePath, 'message-up', instanceName), + fileName: update.id, + data: update, + }); + this.logger.verbose( + 'message up saved to store in path: ' + join(this.storePath, 'message-up', instanceName) + '/' + update.id, + ); + }); - this.logger.verbose('saving message up to store'); + this.logger.verbose('message up saved to store: ' + data.length + ' message up'); + return { insertCount: data.length }; + } - const store = this.configService.get('STORE'); - - if (store.MESSAGE_UP) { - this.logger.verbose('saving message up to store'); - data.forEach((update) => { - this.writeStore({ - path: join(this.storePath, 'message-up', instanceName), - fileName: update.id, - data: update, - }); - this.logger.verbose( - 'message up saved to store in path: ' + - join(this.storePath, 'message-up', instanceName) + - '/' + - update.id, - ); - }); - - this.logger.verbose('message up saved to store: ' + data.length + ' message up'); - return { insertCount: data.length }; - } - - this.logger.verbose('message up not saved to store'); - return { insertCount: 0 }; - } catch (error) { - return error; - } + this.logger.verbose('message up not saved to store'); + return { insertCount: 0 }; + } catch (error) { + return error; } + } - public async find(query: MessageUpQuery) { - try { - this.logger.verbose('finding message up'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding message up in db'); - return await this.messageUpModel - .find({ ...query.where }) - .sort({ datetime: -1 }) - .limit(query?.limit ?? 0); - } + public async find(query: MessageUpQuery) { + try { + this.logger.verbose('finding message up'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding message up in db'); + return await this.messageUpModel + .find({ ...query.where }) + .sort({ datetime: -1 }) + .limit(query?.limit ?? 0); + } - this.logger.verbose('finding message up in store'); + this.logger.verbose('finding message up in store'); - const messageUpdate: MessageUpdateRaw[] = []; - if (query?.where?.id) { - this.logger.verbose('finding message up in store by id'); + const messageUpdate: MessageUpdateRaw[] = []; + if (query?.where?.id) { + this.logger.verbose('finding message up in store by id'); - messageUpdate.push( - JSON.parse( - readFileSync(join(this.storePath, 'message-up', query.where.owner, query.where.id + '.json'), { - encoding: 'utf-8', - }), - ), - ); - } else { - this.logger.verbose('finding message up in store by owner'); + messageUpdate.push( + JSON.parse( + readFileSync(join(this.storePath, 'message-up', query.where.owner, query.where.id + '.json'), { + encoding: 'utf-8', + }), + ), + ); + } else { + this.logger.verbose('finding message up in store by owner'); - const openDir = opendirSync(join(this.storePath, 'message-up', query.where.owner), { - encoding: 'utf-8', - }); + const openDir = opendirSync(join(this.storePath, 'message-up', query.where.owner), { + encoding: 'utf-8', + }); - for await (const dirent of openDir) { - if (dirent.isFile()) { - messageUpdate.push( - JSON.parse( - readFileSync(join(this.storePath, 'message-up', query.where.owner, dirent.name), { - encoding: 'utf-8', - }), - ), - ); - } - } - } - - this.logger.verbose('message up found in store: ' + messageUpdate.length + ' message up'); - return messageUpdate - .sort((x, y) => { - return y.datetime - x.datetime; - }) - .splice(0, query?.limit ?? messageUpdate.length); - } catch (error) { - return []; + for await (const dirent of openDir) { + if (dirent.isFile()) { + messageUpdate.push( + JSON.parse( + readFileSync(join(this.storePath, 'message-up', query.where.owner, dirent.name), { + encoding: 'utf-8', + }), + ), + ); + } } + } + + this.logger.verbose('message up found in store: ' + messageUpdate.length + ' message up'); + return messageUpdate + .sort((x, y) => { + return y.datetime - x.datetime; + }) + .splice(0, query?.limit ?? messageUpdate.length); + } catch (error) { + return []; } + } } diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index 46d92418..e1292329 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -13,105 +13,105 @@ import { MessageUpRepository } from './messageUp.repository'; import { SettingsRepository } from './settings.repository'; import { WebhookRepository } from './webhook.repository'; export class RepositoryBroker { - constructor( - public readonly message: MessageRepository, - public readonly chat: ChatRepository, - public readonly contact: ContactRepository, - public readonly messageUpdate: MessageUpRepository, - public readonly webhook: WebhookRepository, - public readonly chatwoot: ChatwootRepository, - public readonly settings: SettingsRepository, - public readonly auth: AuthRepository, - private configService: ConfigService, - dbServer?: MongoClient, - ) { - this.dbClient = dbServer; - this.__init_repo_without_db__(); - } + constructor( + public readonly message: MessageRepository, + public readonly chat: ChatRepository, + public readonly contact: ContactRepository, + public readonly messageUpdate: MessageUpRepository, + public readonly webhook: WebhookRepository, + public readonly chatwoot: ChatwootRepository, + public readonly settings: SettingsRepository, + public readonly auth: AuthRepository, + private configService: ConfigService, + dbServer?: MongoClient, + ) { + this.dbClient = dbServer; + this.__init_repo_without_db__(); + } - private dbClient?: MongoClient; - private readonly logger = new Logger('RepositoryBroker'); + private dbClient?: MongoClient; + private readonly logger = new Logger('RepositoryBroker'); - public get dbServer() { - return this.dbClient; - } + public get dbServer() { + return this.dbClient; + } - private __init_repo_without_db__() { - this.logger.verbose('initializing repository without db'); - if (!this.configService.get('DATABASE').ENABLED) { - const storePath = join(process.cwd(), 'store'); + private __init_repo_without_db__() { + this.logger.verbose('initializing repository without db'); + if (!this.configService.get('DATABASE').ENABLED) { + const storePath = join(process.cwd(), 'store'); - this.logger.verbose('creating store path: ' + storePath); - try { - const authDir = join(storePath, 'auth', this.configService.get('AUTHENTICATION').TYPE); - const chatsDir = join(storePath, 'chats'); - const contactsDir = join(storePath, 'contacts'); - const messagesDir = join(storePath, 'messages'); - const messageUpDir = join(storePath, 'message-up'); - const webhookDir = join(storePath, 'webhook'); - const chatwootDir = join(storePath, 'chatwoot'); - const settingsDir = join(storePath, 'settings'); - const tempDir = join(storePath, 'temp'); + this.logger.verbose('creating store path: ' + storePath); + try { + const authDir = join(storePath, 'auth', this.configService.get('AUTHENTICATION').TYPE); + const chatsDir = join(storePath, 'chats'); + const contactsDir = join(storePath, 'contacts'); + const messagesDir = join(storePath, 'messages'); + const messageUpDir = join(storePath, 'message-up'); + const webhookDir = join(storePath, 'webhook'); + const chatwootDir = join(storePath, 'chatwoot'); + const settingsDir = join(storePath, 'settings'); + const tempDir = join(storePath, 'temp'); - if (!fs.existsSync(authDir)) { - this.logger.verbose('creating auth dir: ' + authDir); - fs.mkdirSync(authDir, { recursive: true }); - } - if (!fs.existsSync(chatsDir)) { - this.logger.verbose('creating chats dir: ' + chatsDir); - fs.mkdirSync(chatsDir, { recursive: true }); - } - if (!fs.existsSync(contactsDir)) { - this.logger.verbose('creating contacts dir: ' + contactsDir); - fs.mkdirSync(contactsDir, { recursive: true }); - } - if (!fs.existsSync(messagesDir)) { - this.logger.verbose('creating messages dir: ' + messagesDir); - fs.mkdirSync(messagesDir, { recursive: true }); - } - if (!fs.existsSync(messageUpDir)) { - this.logger.verbose('creating message-up dir: ' + messageUpDir); - fs.mkdirSync(messageUpDir, { recursive: true }); - } - if (!fs.existsSync(webhookDir)) { - this.logger.verbose('creating webhook dir: ' + webhookDir); - fs.mkdirSync(webhookDir, { recursive: true }); - } - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(settingsDir)) { - this.logger.verbose('creating settings dir: ' + settingsDir); - fs.mkdirSync(settingsDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } - } else { - try { - const storePath = join(process.cwd(), 'store'); - - this.logger.verbose('creating store path: ' + storePath); - - const tempDir = join(storePath, 'temp'); - const chatwootDir = join(storePath, 'chatwoot'); - - if (!fs.existsSync(chatwootDir)) { - this.logger.verbose('creating chatwoot dir: ' + chatwootDir); - fs.mkdirSync(chatwootDir, { recursive: true }); - } - if (!fs.existsSync(tempDir)) { - this.logger.verbose('creating temp dir: ' + tempDir); - fs.mkdirSync(tempDir, { recursive: true }); - } - } catch (error) { - this.logger.error(error); - } + if (!fs.existsSync(authDir)) { + this.logger.verbose('creating auth dir: ' + authDir); + fs.mkdirSync(authDir, { recursive: true }); } + if (!fs.existsSync(chatsDir)) { + this.logger.verbose('creating chats dir: ' + chatsDir); + fs.mkdirSync(chatsDir, { recursive: true }); + } + if (!fs.existsSync(contactsDir)) { + this.logger.verbose('creating contacts dir: ' + contactsDir); + fs.mkdirSync(contactsDir, { recursive: true }); + } + if (!fs.existsSync(messagesDir)) { + this.logger.verbose('creating messages dir: ' + messagesDir); + fs.mkdirSync(messagesDir, { recursive: true }); + } + if (!fs.existsSync(messageUpDir)) { + this.logger.verbose('creating message-up dir: ' + messageUpDir); + fs.mkdirSync(messageUpDir, { recursive: true }); + } + if (!fs.existsSync(webhookDir)) { + this.logger.verbose('creating webhook dir: ' + webhookDir); + fs.mkdirSync(webhookDir, { recursive: true }); + } + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(settingsDir)) { + this.logger.verbose('creating settings dir: ' + settingsDir); + fs.mkdirSync(settingsDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } + } else { + try { + const storePath = join(process.cwd(), 'store'); + + this.logger.verbose('creating store path: ' + storePath); + + const tempDir = join(storePath, 'temp'); + const chatwootDir = join(storePath, 'chatwoot'); + + if (!fs.existsSync(chatwootDir)) { + this.logger.verbose('creating chatwoot dir: ' + chatwootDir); + fs.mkdirSync(chatwootDir, { recursive: true }); + } + if (!fs.existsSync(tempDir)) { + this.logger.verbose('creating temp dir: ' + tempDir); + fs.mkdirSync(tempDir, { recursive: true }); + } + } catch (error) { + this.logger.error(error); + } } + } } diff --git a/src/whatsapp/repository/settings.repository.ts b/src/whatsapp/repository/settings.repository.ts index 96003d69..4d09d79f 100644 --- a/src/whatsapp/repository/settings.repository.ts +++ b/src/whatsapp/repository/settings.repository.ts @@ -7,58 +7,56 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { ISettingsModel, SettingsRaw } from '../models'; export class SettingsRepository extends Repository { - constructor(private readonly settingsModel: ISettingsModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly settingsModel: ISettingsModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('SettingsRepository'); + + public async create(data: SettingsRaw, instance: string): Promise { + try { + this.logger.verbose('creating settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving settings to db'); + const insert = await this.settingsModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('settings saved to db: ' + insert.modifiedCount + ' settings'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving settings to store'); + + this.writeStore({ + path: join(this.storePath, 'settings'), + fileName: instance, + data, + }); + + this.logger.verbose('settings saved to store in path: ' + join(this.storePath, 'settings') + '/' + instance); + + this.logger.verbose('settings created'); + return { insertCount: 1 }; + } catch (error) { + return error; } + } - private readonly logger = new Logger('SettingsRepository'); + public async find(instance: string): Promise { + try { + this.logger.verbose('finding settings'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding settings in db'); + return await this.settingsModel.findOne({ _id: instance }); + } - public async create(data: SettingsRaw, instance: string): Promise { - try { - this.logger.verbose('creating settings'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving settings to db'); - const insert = await this.settingsModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); - - this.logger.verbose('settings saved to db: ' + insert.modifiedCount + ' settings'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving settings to store'); - - this.writeStore({ - path: join(this.storePath, 'settings'), - fileName: instance, - data, - }); - - this.logger.verbose( - 'settings saved to store in path: ' + join(this.storePath, 'settings') + '/' + instance, - ); - - this.logger.verbose('settings created'); - return { insertCount: 1 }; - } catch (error) { - return error; - } - } - - public async find(instance: string): Promise { - try { - this.logger.verbose('finding settings'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding settings in db'); - return await this.settingsModel.findOne({ _id: instance }); - } - - this.logger.verbose('finding settings in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'settings', instance + '.json'), { - encoding: 'utf-8', - }), - ) as SettingsRaw; - } catch (error) { - return {}; - } + this.logger.verbose('finding settings in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'settings', instance + '.json'), { + encoding: 'utf-8', + }), + ) as SettingsRaw; + } catch (error) { + return {}; } + } } diff --git a/src/whatsapp/repository/webhook.repository.ts b/src/whatsapp/repository/webhook.repository.ts index f3e9d12f..10074516 100644 --- a/src/whatsapp/repository/webhook.repository.ts +++ b/src/whatsapp/repository/webhook.repository.ts @@ -7,56 +7,56 @@ import { IInsert, Repository } from '../abstract/abstract.repository'; import { IWebhookModel, WebhookRaw } from '../models'; export class WebhookRepository extends Repository { - constructor(private readonly webhookModel: IWebhookModel, private readonly configService: ConfigService) { - super(configService); + constructor(private readonly webhookModel: IWebhookModel, private readonly configService: ConfigService) { + super(configService); + } + + private readonly logger = new Logger('WebhookRepository'); + + public async create(data: WebhookRaw, instance: string): Promise { + try { + this.logger.verbose('creating webhook'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving webhook to db'); + const insert = await this.webhookModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); + + this.logger.verbose('webhook saved to db: ' + insert.modifiedCount + ' webhook'); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving webhook to store'); + + this.writeStore({ + path: join(this.storePath, 'webhook'), + fileName: instance, + data, + }); + + this.logger.verbose('webhook saved to store in path: ' + join(this.storePath, 'webhook') + '/' + instance); + + this.logger.verbose('webhook created'); + return { insertCount: 1 }; + } catch (error) { + return error; } + } - private readonly logger = new Logger('WebhookRepository'); + public async find(instance: string): Promise { + try { + this.logger.verbose('finding webhook'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding webhook in db'); + return await this.webhookModel.findOne({ _id: instance }); + } - public async create(data: WebhookRaw, instance: string): Promise { - try { - this.logger.verbose('creating webhook'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('saving webhook to db'); - const insert = await this.webhookModel.replaceOne({ _id: instance }, { ...data }, { upsert: true }); - - this.logger.verbose('webhook saved to db: ' + insert.modifiedCount + ' webhook'); - return { insertCount: insert.modifiedCount }; - } - - this.logger.verbose('saving webhook to store'); - - this.writeStore({ - path: join(this.storePath, 'webhook'), - fileName: instance, - data, - }); - - this.logger.verbose('webhook saved to store in path: ' + join(this.storePath, 'webhook') + '/' + instance); - - this.logger.verbose('webhook created'); - return { insertCount: 1 }; - } catch (error) { - return error; - } - } - - public async find(instance: string): Promise { - try { - this.logger.verbose('finding webhook'); - if (this.dbSettings.ENABLED) { - this.logger.verbose('finding webhook in db'); - return await this.webhookModel.findOne({ _id: instance }); - } - - this.logger.verbose('finding webhook in store'); - return JSON.parse( - readFileSync(join(this.storePath, 'webhook', instance + '.json'), { - encoding: 'utf-8', - }), - ) as WebhookRaw; - } catch (error) { - return {}; - } + this.logger.verbose('finding webhook in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'webhook', instance + '.json'), { + encoding: 'utf-8', + }), + ) as WebhookRaw; + } catch (error) { + return {}; } + } } diff --git a/src/whatsapp/routers/chat.router.ts b/src/whatsapp/routers/chat.router.ts index ee7e972f..285c29a0 100644 --- a/src/whatsapp/routers/chat.router.ts +++ b/src/whatsapp/routers/chat.router.ts @@ -2,31 +2,31 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - archiveChatSchema, - contactValidateSchema, - deleteMessageSchema, - messageUpSchema, - messageValidateSchema, - privacySettingsSchema, - profileNameSchema, - profilePictureSchema, - profileSchema, - profileStatusSchema, - readMessageSchema, - whatsappNumberSchema, + archiveChatSchema, + contactValidateSchema, + deleteMessageSchema, + messageUpSchema, + messageValidateSchema, + privacySettingsSchema, + profileNameSchema, + profilePictureSchema, + profileSchema, + profileStatusSchema, + readMessageSchema, + whatsappNumberSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberDto, - PrivacySettingDto, - ProfileNameDto, - ProfilePictureDto, - ProfileStatusDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberDto, + PrivacySettingDto, + ProfileNameDto, + ProfilePictureDto, + ProfileStatusDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { InstanceDto } from '../dto/instance.dto'; import { ContactQuery } from '../repository/contact.repository'; @@ -38,317 +38,317 @@ import { HttpStatus } from './index.router'; const logger = new Logger('ChatRouter'); export class ChatRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { - logger.verbose('request received in whatsappNumbers'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('whatsappNumbers'), ...guards, async (req, res) => { + logger.verbose('request received in whatsappNumbers'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: whatsappNumberSchema, - ClassRef: WhatsAppNumberDto, - execute: (instance, data) => chatController.whatsappNumber(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: whatsappNumberSchema, + ClassRef: WhatsAppNumberDto, + execute: (instance, data) => chatController.whatsappNumber(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { - logger.verbose('request received in markMessageAsRead'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('markMessageAsRead'), ...guards, async (req, res) => { + logger.verbose('request received in markMessageAsRead'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: readMessageSchema, - ClassRef: ReadMessageDto, - execute: (instance, data) => chatController.readMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: readMessageSchema, + ClassRef: ReadMessageDto, + execute: (instance, data) => chatController.readMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('archiveChat'), ...guards, async (req, res) => { - logger.verbose('request received in archiveChat'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('archiveChat'), ...guards, async (req, res) => { + logger.verbose('request received in archiveChat'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: archiveChatSchema, - ClassRef: ArchiveChatDto, - execute: (instance, data) => chatController.archiveChat(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: archiveChatSchema, + ClassRef: ArchiveChatDto, + execute: (instance, data) => chatController.archiveChat(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .delete(this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => { - logger.verbose('request received in deleteMessageForEveryone'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .delete(this.routerPath('deleteMessageForEveryone'), ...guards, async (req, res) => { + logger.verbose('request received in deleteMessageForEveryone'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: deleteMessageSchema, - ClassRef: DeleteMessage, - execute: (instance, data) => chatController.deleteMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: deleteMessageSchema, + ClassRef: DeleteMessage, + execute: (instance, data) => chatController.deleteMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => { - logger.verbose('request received in fetchProfilePictureUrl'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('fetchProfilePictureUrl'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfilePictureUrl'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchProfilePicture(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfilePicture(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { - logger.verbose('request received in fetchProfile'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('fetchProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileSchema, - ClassRef: NumberDto, - execute: (instance, data) => chatController.fetchProfile(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchProfile(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('findContacts'), ...guards, async (req, res) => { - logger.verbose('request received in findContacts'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('findContacts'), ...guards, async (req, res) => { + logger.verbose('request received in findContacts'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: contactValidateSchema, - ClassRef: ContactQuery, - execute: (instance, data) => chatController.fetchContacts(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: contactValidateSchema, + ClassRef: ContactQuery, + execute: (instance, data) => chatController.fetchContacts(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => { - logger.verbose('request received in getBase64FromMediaMessage'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('getBase64FromMediaMessage'), ...guards, async (req, res) => { + logger.verbose('request received in getBase64FromMediaMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: getBase64FromMediaMessageDto, - execute: (instance, data) => chatController.getBase64FromMediaMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: getBase64FromMediaMessageDto, + execute: (instance, data) => chatController.getBase64FromMediaMessage(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('findMessages'), ...guards, async (req, res) => { - logger.verbose('request received in findMessages'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('findMessages'), ...guards, async (req, res) => { + logger.verbose('request received in findMessages'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: messageValidateSchema, - ClassRef: MessageQuery, - execute: (instance, data) => chatController.fetchMessages(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: messageValidateSchema, + ClassRef: MessageQuery, + execute: (instance, data) => chatController.fetchMessages(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => { - logger.verbose('request received in findStatusMessage'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('findStatusMessage'), ...guards, async (req, res) => { + logger.verbose('request received in findStatusMessage'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: messageUpSchema, - ClassRef: MessageUpQuery, - execute: (instance, data) => chatController.fetchStatusMessage(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: messageUpSchema, + ClassRef: MessageUpQuery, + execute: (instance, data) => chatController.fetchStatusMessage(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('findChats'), ...guards, async (req, res) => { - logger.verbose('request received in findChats'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('findChats'), ...guards, async (req, res) => { + logger.verbose('request received in findChats'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => chatController.fetchChats(instance), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => chatController.fetchChats(instance), + }); - return res.status(HttpStatus.OK).json(response); - }) - // Profile routes - .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { - logger.verbose('request received in fetchPrivacySettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + // Profile routes + .get(this.routerPath('fetchPrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in fetchPrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => chatController.fetchPrivacySettings(instance), - }); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => chatController.fetchPrivacySettings(instance), + }); - return res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => { - logger.verbose('request received in updatePrivacySettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('updatePrivacySettings'), ...guards, async (req, res) => { + logger.verbose('request received in updatePrivacySettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: privacySettingsSchema, - ClassRef: PrivacySettingDto, - execute: (instance, data) => chatController.updatePrivacySettings(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: privacySettingsSchema, + ClassRef: PrivacySettingDto, + execute: (instance, data) => chatController.updatePrivacySettings(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => { - logger.verbose('request received in fetchBusinessProfile'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('fetchBusinessProfile'), ...guards, async (req, res) => { + logger.verbose('request received in fetchBusinessProfile'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance, data) => chatController.fetchBusinessProfile(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance, data) => chatController.fetchBusinessProfile(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfileName'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('updateProfileName'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileName'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileNameSchema, - ClassRef: ProfileNameDto, - execute: (instance, data) => chatController.updateProfileName(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileNameSchema, + ClassRef: ProfileNameDto, + execute: (instance, data) => chatController.updateProfileName(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfileStatus'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfileStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profileStatusSchema, - ClassRef: ProfileStatusDto, - execute: (instance, data) => chatController.updateProfileStatus(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profileStatusSchema, + ClassRef: ProfileStatusDto, + execute: (instance, data) => chatController.updateProfileStatus(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => { - logger.verbose('request received in updateProfilePicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('updateProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance, data) => chatController.updateProfilePicture(instance, data), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance, data) => chatController.updateProfilePicture(instance, data), + }); - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => { - logger.verbose('request received in removeProfilePicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('removeProfilePicture'), ...guards, async (req, res) => { + logger.verbose('request received in removeProfilePicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: profilePictureSchema, - ClassRef: ProfilePictureDto, - execute: (instance) => chatController.removeProfilePicture(instance), - }); + const response = await this.dataValidate({ + request: req, + schema: profilePictureSchema, + ClassRef: ProfilePictureDto, + execute: (instance) => chatController.removeProfilePicture(instance), + }); - return res.status(HttpStatus.OK).json(response); - }); - } + return res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index 4556f4ec..eb779587 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -12,58 +12,58 @@ import { HttpStatus } from './index.router'; const logger = new Logger('ChatwootRouter'); export class ChatwootRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: chatwootSchema, - ClassRef: ChatwootDto, - execute: (instance, data) => chatwootController.createChatwoot(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: chatwootSchema, + ClassRef: ChatwootDto, + execute: (instance, data) => chatwootController.createChatwoot(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findChatwoot'); + 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) => chatwootController.findChatwoot(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => chatwootController.findChatwoot(instance), + }); - res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('webhook'), async (req, res) => { - logger.verbose('request received in findChatwoot'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('webhook'), async (req, res) => { + logger.verbose('request received in findChatwoot'); + 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, data) => chatwootController.receiveWebhook(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance, data) => chatwootController.receiveWebhook(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 33c22f9b..f59e82a4 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -2,31 +2,31 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - createGroupSchema, - getParticipantsSchema, - groupInviteSchema, - groupJidSchema, - groupSendInviteSchema, - toggleEphemeralSchema, - updateGroupDescriptionSchema, - updateGroupPictureSchema, - updateGroupSubjectSchema, - updateParticipantsSchema, - updateSettingsSchema, + createGroupSchema, + getParticipantsSchema, + groupInviteSchema, + groupJidSchema, + groupSendInviteSchema, + toggleEphemeralSchema, + updateGroupDescriptionSchema, + updateGroupPictureSchema, + updateGroupSubjectSchema, + updateParticipantsSchema, + updateSettingsSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { groupController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -34,251 +34,251 @@ import { HttpStatus } from './index.router'; const logger = new Logger('GroupRouter'); export class GroupRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('create'), ...guards, async (req, res) => { - logger.verbose('request received in createGroup'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('create'), ...guards, async (req, res) => { + logger.verbose('request received in createGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: createGroupSchema, - ClassRef: CreateGroupDto, - execute: (instance, data) => groupController.createGroup(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: createGroupSchema, + ClassRef: CreateGroupDto, + execute: (instance, data) => groupController.createGroup(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupSubject'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupSubject'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupSubject'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupSubjectSchema, - ClassRef: GroupSubjectDto, - execute: (instance, data) => groupController.updateGroupSubject(instance, data), - }); + const response = await this.groupValidate({ + request: req, + schema: updateGroupSubjectSchema, + ClassRef: GroupSubjectDto, + execute: (instance, data) => groupController.updateGroupSubject(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupPicture'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupPicture'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupPicture'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupPictureSchema, - ClassRef: GroupPictureDto, - execute: (instance, data) => groupController.updateGroupPicture(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateGroupPictureSchema, + ClassRef: GroupPictureDto, + execute: (instance, data) => groupController.updateGroupPicture(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => { - logger.verbose('request received in updateGroupDescription'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateGroupDescription'), ...guards, async (req, res) => { + logger.verbose('request received in updateGroupDescription'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateGroupDescriptionSchema, - ClassRef: GroupDescriptionDto, - execute: (instance, data) => groupController.updateGroupDescription(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateGroupDescriptionSchema, + ClassRef: GroupDescriptionDto, + execute: (instance, data) => groupController.updateGroupDescription(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { - logger.verbose('request received in findGroupInfos'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('findGroupInfos'), ...guards, async (req, res) => { + logger.verbose('request received in findGroupInfos'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.findGroupInfo(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.findGroupInfo(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => { - logger.verbose('request received in fetchAllGroups'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchAllGroups'), ...guards, async (req, res) => { + logger.verbose('request received in fetchAllGroups'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.getParticipantsValidate({ - request: req, - schema: getParticipantsSchema, - ClassRef: GetParticipant, - execute: (instance, data) => groupController.fetchAllGroups(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.getParticipantsValidate({ + request: req, + schema: getParticipantsSchema, + ClassRef: GetParticipant, + execute: (instance, data) => groupController.fetchAllGroups(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('participants'), ...guards, async (req, res) => { - logger.verbose('request received in participants'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('participants'), ...guards, async (req, res) => { + logger.verbose('request received in participants'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.findParticipants(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.findParticipants(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('inviteCode'), ...guards, async (req, res) => { - logger.verbose('request received in inviteCode'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('inviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in inviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.inviteCode(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.inviteCode(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('inviteInfo'), ...guards, async (req, res) => { - logger.verbose('request received in inviteInfo'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('inviteInfo'), ...guards, async (req, res) => { + logger.verbose('request received in inviteInfo'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.inviteCodeValidate({ - request: req, - schema: groupInviteSchema, - ClassRef: GroupInvite, - execute: (instance, data) => groupController.inviteInfo(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.inviteCodeValidate({ + request: req, + schema: groupInviteSchema, + ClassRef: GroupInvite, + execute: (instance, data) => groupController.inviteInfo(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { - logger.verbose('request received in sendInvite'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('sendInvite'), ...guards, async (req, res) => { + logger.verbose('request received in sendInvite'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupNoValidate({ - request: req, - schema: groupSendInviteSchema, - ClassRef: GroupSendInvite, - execute: (instance, data) => groupController.sendInvite(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupNoValidate({ + request: req, + schema: groupSendInviteSchema, + ClassRef: GroupSendInvite, + execute: (instance, data) => groupController.sendInvite(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }) - .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { - logger.verbose('request received in revokeInviteCode'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('revokeInviteCode'), ...guards, async (req, res) => { + logger.verbose('request received in revokeInviteCode'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: groupJidSchema, - ClassRef: GroupJid, - execute: (instance, data) => groupController.revokeInviteCode(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: groupJidSchema, + ClassRef: GroupJid, + execute: (instance, data) => groupController.revokeInviteCode(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateParticipant'), ...guards, async (req, res) => { - logger.verbose('request received in updateParticipant'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateParticipant'), ...guards, async (req, res) => { + logger.verbose('request received in updateParticipant'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateParticipantsSchema, - ClassRef: GroupUpdateParticipantDto, - execute: (instance, data) => groupController.updateGParticipate(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateParticipantsSchema, + ClassRef: GroupUpdateParticipantDto, + execute: (instance, data) => groupController.updateGParticipate(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('updateSetting'), ...guards, async (req, res) => { - logger.verbose('request received in updateSetting'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('updateSetting'), ...guards, async (req, res) => { + logger.verbose('request received in updateSetting'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: updateSettingsSchema, - ClassRef: GroupUpdateSettingDto, - execute: (instance, data) => groupController.updateGSetting(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: updateSettingsSchema, + ClassRef: GroupUpdateSettingDto, + execute: (instance, data) => groupController.updateGSetting(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => { - logger.verbose('request received in toggleEphemeral'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('toggleEphemeral'), ...guards, async (req, res) => { + logger.verbose('request received in toggleEphemeral'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: toggleEphemeralSchema, - ClassRef: GroupToggleEphemeralDto, - execute: (instance, data) => groupController.toggleEphemeral(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: toggleEphemeralSchema, + ClassRef: GroupToggleEphemeralDto, + execute: (instance, data) => groupController.toggleEphemeral(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { - logger.verbose('request received in leaveGroup'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .delete(this.routerPath('leaveGroup'), ...guards, async (req, res) => { + logger.verbose('request received in leaveGroup'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.groupValidate({ - request: req, - schema: {}, - ClassRef: GroupJid, - execute: (instance, data) => groupController.leaveGroup(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.groupValidate({ + request: req, + schema: {}, + ClassRef: GroupJid, + execute: (instance, data) => groupController.leaveGroup(instance, data), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 941efeda..db082799 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -14,13 +14,13 @@ import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; enum HttpStatus { - OK = 200, - CREATED = 201, - NOT_FOUND = 404, - FORBIDDEN = 403, - BAD_REQUEST = 400, - UNAUTHORIZED = 401, - INTERNAL_SERVER_ERROR = 500, + OK = 200, + CREATED = 201, + NOT_FOUND = 404, + FORBIDDEN = 403, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + INTERNAL_SERVER_ERROR = 500, } const router = Router(); @@ -30,19 +30,19 @@ const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]]; const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8')); router - .get('/', (req, res) => { - res.status(HttpStatus.OK).json({ - status: HttpStatus.OK, - message: 'Welcome to the Evolution API, it is working!', - version: packageJson.version, - }); - }) - .use('/instance', new InstanceRouter(configService, ...guards).router, new ViewsRouter(instanceExistsGuard).router) - .use('/message', new MessageRouter(...guards).router) - .use('/chat', new ChatRouter(...guards).router) - .use('/group', new GroupRouter(...guards).router) - .use('/webhook', new WebhookRouter(...guards).router) - .use('/chatwoot', new ChatwootRouter(...guards).router) - .use('/settings', new SettingsRouter(...guards).router); + .get('/', (req, res) => { + res.status(HttpStatus.OK).json({ + status: HttpStatus.OK, + message: 'Welcome to the Evolution API, it is working!', + version: packageJson.version, + }); + }) + .use('/instance', new InstanceRouter(configService, ...guards).router, new ViewsRouter(instanceExistsGuard).router) + .use('/message', new MessageRouter(...guards).router) + .use('/chat', new ChatRouter(...guards).router) + .use('/group', new GroupRouter(...guards).router) + .use('/webhook', new WebhookRouter(...guards).router) + .use('/chatwoot', new ChatwootRouter(...guards).router) + .use('/settings', new SettingsRouter(...guards).router); export { HttpStatus, router }; diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index 2c0f6a38..ae6d4066 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -13,165 +13,163 @@ import { HttpStatus } from './index.router'; const logger = new Logger('InstanceRouter'); export class InstanceRouter extends RouterBroker { - constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { - super(); - const auth = configService.get('AUTHENTICATION'); - this.router - .post('/create', ...guards, async (req, res) => { - logger.verbose('request received in createInstance'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) { + super(); + const auth = configService.get('AUTHENTICATION'); + this.router + .post('/create', ...guards, async (req, res) => { + logger.verbose('request received in createInstance'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); + logger.verbose('request query: '); + logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: instanceNameSchema, - ClassRef: InstanceDto, - execute: (instance) => instanceController.createInstance(instance), - }); - - return res.status(HttpStatus.CREATED).json(response); - }) - .put(this.routerPath('restart'), ...guards, async (req, res) => { - logger.verbose('request received in restartInstance'); - 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) => instanceController.restartInstance(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('connect'), ...guards, async (req, res) => { - logger.verbose('request received in connectInstance'); - 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) => instanceController.connectToWhatsapp(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('connectionState'), ...guards, async (req, res) => { - logger.verbose('request received in connectionState'); - 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) => instanceController.connectionState(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { - logger.verbose('request received in fetchInstances'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: null, - ClassRef: InstanceDto, - execute: (instance) => instanceController.fetchInstances(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('logout'), ...guards, async (req, res) => { - logger.verbose('request received in logoutInstances'); - 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) => instanceController.logout(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }) - .delete(this.routerPath('delete'), ...guards, async (req, res) => { - logger.verbose('request received in deleteInstances'); - 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) => instanceController.deleteInstance(instance), - }); - - return res.status(HttpStatus.OK).json(response); - }); - - if (auth.TYPE === 'jwt') { - this.router.put('/refreshToken', async (req, res) => { - logger.verbose('request received in refreshToken'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: oldTokenSchema, - ClassRef: OldToken, - execute: (_, data) => instanceController.refreshToken(_, data), - }); - - return res.status(HttpStatus.CREATED).json(response); - }); - } - - this.router.delete('/deleteDatabase', async (req, res) => { - logger.verbose('request received in deleteDatabase'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const db = this.configService.get('DATABASE'); - if (db.ENABLED) { - try { - await dbserver.dropDatabase(); - return res.status(HttpStatus.CREATED).json({ error: false, message: 'Database deleted' }); - } catch (error) { - return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message }); - } - } - - return res - .status(HttpStatus.INTERNAL_SERVER_ERROR) - .json({ error: true, message: 'Database is not enabled' }); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => instanceController.createInstance(instance), }); + + return res.status(HttpStatus.CREATED).json(response); + }) + .put(this.routerPath('restart'), ...guards, async (req, res) => { + logger.verbose('request received in restartInstance'); + 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) => instanceController.restartInstance(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('connect'), ...guards, async (req, res) => { + logger.verbose('request received in connectInstance'); + 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) => instanceController.connectToWhatsapp(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('connectionState'), ...guards, async (req, res) => { + logger.verbose('request received in connectionState'); + 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) => instanceController.connectionState(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchInstances', false), ...guards, async (req, res) => { + logger.verbose('request received in fetchInstances'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => instanceController.fetchInstances(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('logout'), ...guards, async (req, res) => { + logger.verbose('request received in logoutInstances'); + 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) => instanceController.logout(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('delete'), ...guards, async (req, res) => { + logger.verbose('request received in deleteInstances'); + 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) => instanceController.deleteInstance(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }); + + if (auth.TYPE === 'jwt') { + this.router.put('/refreshToken', async (req, res) => { + logger.verbose('request received in refreshToken'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: oldTokenSchema, + ClassRef: OldToken, + execute: (_, data) => instanceController.refreshToken(_, data), + }); + + return res.status(HttpStatus.CREATED).json(response); + }); } - public readonly router = Router(); + this.router.delete('/deleteDatabase', async (req, res) => { + logger.verbose('request received in deleteDatabase'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const db = this.configService.get('DATABASE'); + if (db.ENABLED) { + try { + await dbserver.dropDatabase(); + return res.status(HttpStatus.CREATED).json({ error: false, message: 'Database deleted' }); + } catch (error) { + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message }); + } + } + + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: 'Database is not enabled' }); + }); + } + + public readonly router = Router(); } diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index 4550ff8b..d87db44d 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -2,31 +2,31 @@ import { RequestHandler, Router } from 'express'; import { Logger } from '../../config/logger.config'; import { - audioMessageSchema, - buttonMessageSchema, - contactMessageSchema, - listMessageSchema, - locationMessageSchema, - mediaMessageSchema, - pollMessageSchema, - reactionMessageSchema, - statusMessageSchema, - stickerMessageSchema, - textMessageSchema, + audioMessageSchema, + buttonMessageSchema, + contactMessageSchema, + listMessageSchema, + locationMessageSchema, + mediaMessageSchema, + pollMessageSchema, + reactionMessageSchema, + statusMessageSchema, + stickerMessageSchema, + textMessageSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, } from '../dto/sendMessage.dto'; import { sendMessageController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -34,186 +34,186 @@ import { HttpStatus } from './index.router'; const logger = new Logger('MessageRouter'); export class MessageRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('sendText'), ...guards, async (req, res) => { - logger.verbose('request received in sendText'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('sendText'), ...guards, async (req, res) => { + logger.verbose('request received in sendText'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: textMessageSchema, - ClassRef: SendTextDto, - execute: (instance, data) => sendMessageController.sendText(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: textMessageSchema, + ClassRef: SendTextDto, + execute: (instance, data) => sendMessageController.sendText(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendMedia'), ...guards, async (req, res) => { - logger.verbose('request received in sendMedia'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendMedia'), ...guards, async (req, res) => { + logger.verbose('request received in sendMedia'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: mediaMessageSchema, - ClassRef: SendMediaDto, - execute: (instance, data) => sendMessageController.sendMedia(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: mediaMessageSchema, + ClassRef: SendMediaDto, + execute: (instance, data) => sendMessageController.sendMedia(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => { - logger.verbose('request received in sendWhatsAppAudio'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendWhatsAppAudio'), ...guards, async (req, res) => { + logger.verbose('request received in sendWhatsAppAudio'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: audioMessageSchema, - ClassRef: SendMediaDto, - execute: (instance, data) => sendMessageController.sendWhatsAppAudio(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: audioMessageSchema, + ClassRef: SendMediaDto, + execute: (instance, data) => sendMessageController.sendWhatsAppAudio(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendButtons'), ...guards, async (req, res) => { - logger.verbose('request received in sendButtons'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendButtons'), ...guards, async (req, res) => { + logger.verbose('request received in sendButtons'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: buttonMessageSchema, - ClassRef: SendButtonDto, - execute: (instance, data) => sendMessageController.sendButtons(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: buttonMessageSchema, + ClassRef: SendButtonDto, + execute: (instance, data) => sendMessageController.sendButtons(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendLocation'), ...guards, async (req, res) => { - logger.verbose('request received in sendLocation'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendLocation'), ...guards, async (req, res) => { + logger.verbose('request received in sendLocation'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: locationMessageSchema, - ClassRef: SendLocationDto, - execute: (instance, data) => sendMessageController.sendLocation(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: locationMessageSchema, + ClassRef: SendLocationDto, + execute: (instance, data) => sendMessageController.sendLocation(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendList'), ...guards, async (req, res) => { - logger.verbose('request received in sendList'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendList'), ...guards, async (req, res) => { + logger.verbose('request received in sendList'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: listMessageSchema, - ClassRef: SendListDto, - execute: (instance, data) => sendMessageController.sendList(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: listMessageSchema, + ClassRef: SendListDto, + execute: (instance, data) => sendMessageController.sendList(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendContact'), ...guards, async (req, res) => { - logger.verbose('request received in sendContact'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendContact'), ...guards, async (req, res) => { + logger.verbose('request received in sendContact'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: contactMessageSchema, - ClassRef: SendContactDto, - execute: (instance, data) => sendMessageController.sendContact(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: contactMessageSchema, + ClassRef: SendContactDto, + execute: (instance, data) => sendMessageController.sendContact(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendReaction'), ...guards, async (req, res) => { - logger.verbose('request received in sendReaction'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendReaction'), ...guards, async (req, res) => { + logger.verbose('request received in sendReaction'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: reactionMessageSchema, - ClassRef: SendReactionDto, - execute: (instance, data) => sendMessageController.sendReaction(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: reactionMessageSchema, + ClassRef: SendReactionDto, + execute: (instance, data) => sendMessageController.sendReaction(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendPoll'), ...guards, async (req, res) => { - logger.verbose('request received in sendPoll'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendPoll'), ...guards, async (req, res) => { + logger.verbose('request received in sendPoll'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: pollMessageSchema, - ClassRef: SendPollDto, - execute: (instance, data) => sendMessageController.sendPoll(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: pollMessageSchema, + ClassRef: SendPollDto, + execute: (instance, data) => sendMessageController.sendPoll(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendStatus'), ...guards, async (req, res) => { - logger.verbose('request received in sendStatus'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendStatus'), ...guards, async (req, res) => { + logger.verbose('request received in sendStatus'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: statusMessageSchema, - ClassRef: SendStatusDto, - execute: (instance, data) => sendMessageController.sendStatus(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: statusMessageSchema, + ClassRef: SendStatusDto, + execute: (instance, data) => sendMessageController.sendStatus(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }) - .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { - logger.verbose('request received in sendSticker'); - logger.verbose('request body: '); - logger.verbose(req.body); + return res.status(HttpStatus.CREATED).json(response); + }) + .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { + logger.verbose('request received in sendSticker'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: stickerMessageSchema, - ClassRef: SendStickerDto, - execute: (instance, data) => sendMessageController.sendSticker(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: stickerMessageSchema, + ClassRef: SendStickerDto, + execute: (instance, data) => sendMessageController.sendSticker(instance, data), + }); - return res.status(HttpStatus.CREATED).json(response); - }); - } + return res.status(HttpStatus.CREATED).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/settings.router.ts b/src/whatsapp/routers/settings.router.ts index be364885..6bd4d549 100644 --- a/src/whatsapp/routers/settings.router.ts +++ b/src/whatsapp/routers/settings.router.ts @@ -12,42 +12,42 @@ import { HttpStatus } from './index.router'; const logger = new Logger('SettingsRouter'); export class SettingsRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setSettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setSettings'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: settingsSchema, - ClassRef: SettingsDto, - execute: (instance, data) => settingsController.createSettings(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: settingsSchema, + ClassRef: SettingsDto, + execute: (instance, data) => settingsController.createSettings(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findSettings'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findSettings'); + 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) => settingsController.findSettings(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => settingsController.findSettings(instance), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/view.router.ts b/src/whatsapp/routers/view.router.ts index 297f0125..c5e18129 100644 --- a/src/whatsapp/routers/view.router.ts +++ b/src/whatsapp/routers/view.router.ts @@ -4,13 +4,13 @@ import { RouterBroker } from '../abstract/abstract.router'; import { viewsController } from '../whatsapp.module'; export class ViewsRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); + constructor(...guards: RequestHandler[]) { + super(); - this.router.get(this.routerPath('qrcode'), ...guards, (req, res) => { - return viewsController.qrcode(req, res); - }); - } + this.router.get(this.routerPath('qrcode'), ...guards, (req, res) => { + return viewsController.qrcode(req, res); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/routers/webhook.router.ts b/src/whatsapp/routers/webhook.router.ts index ef95e5a2..835d6014 100644 --- a/src/whatsapp/routers/webhook.router.ts +++ b/src/whatsapp/routers/webhook.router.ts @@ -11,42 +11,42 @@ import { HttpStatus } from './index.router'; const logger = new Logger('WebhookRouter'); export class WebhookRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - logger.verbose('request received in setWebhook'); - logger.verbose('request body: '); - logger.verbose(req.body); + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setWebhook'); + logger.verbose('request body: '); + logger.verbose(req.body); - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: webhookSchema, - ClassRef: WebhookDto, - execute: (instance, data) => webhookController.createWebhook(instance, data), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: webhookSchema, + ClassRef: WebhookDto, + execute: (instance, data) => webhookController.createWebhook(instance, data), + }); - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - logger.verbose('request received in findWebhook'); - logger.verbose('request body: '); - logger.verbose(req.body); + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findWebhook'); + 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) => webhookController.findWebhook(instance), - }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => webhookController.findWebhook(instance), + }); - res.status(HttpStatus.OK).json(response); - }); - } + res.status(HttpStatus.OK).json(response); + }); + } - public readonly router = Router(); + public readonly router = Router(); } diff --git a/src/whatsapp/services/auth.service.ts b/src/whatsapp/services/auth.service.ts index f5cd0c38..915a07b7 100644 --- a/src/whatsapp/services/auth.service.ts +++ b/src/whatsapp/services/auth.service.ts @@ -12,166 +12,166 @@ import { RepositoryBroker } from '../repository/repository.manager'; import { WAMonitoringService } from './monitor.service'; export type JwtPayload = { - instanceName: string; - apiName: string; - jwt?: string; - apikey?: string; - tokenId: string; + instanceName: string; + apiName: string; + jwt?: string; + apikey?: string; + tokenId: string; }; export class OldToken { - oldToken: string; + oldToken: string; } export class AuthService { - constructor( - private readonly configService: ConfigService, - private readonly waMonitor: WAMonitoringService, - private readonly repository: RepositoryBroker, - ) {} + constructor( + private readonly configService: ConfigService, + private readonly waMonitor: WAMonitoringService, + private readonly repository: RepositoryBroker, + ) {} - private readonly logger = new Logger(AuthService.name); + private readonly logger = new Logger(AuthService.name); - private async jwt(instance: InstanceDto) { - const jwtOpts = this.configService.get('AUTHENTICATION').JWT; - const token = sign( + private async jwt(instance: InstanceDto) { + const jwtOpts = this.configService.get('AUTHENTICATION').JWT; + const token = sign( + { + instanceName: instance.instanceName, + apiName, + tokenId: v4(), + }, + jwtOpts.SECRET, + { expiresIn: jwtOpts.EXPIRIN_IN, encoding: 'utf8', subject: 'g-t' }, + ); + + this.logger.verbose('JWT token created: ' + token); + + const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName); + + this.logger.verbose('JWT token saved in database'); + + if (auth['error']) { + this.logger.error({ + localError: AuthService.name + '.jwt', + error: auth['error'], + }); + throw new BadRequestException('Authentication error', auth['error']?.toString()); + } + + return { jwt: token }; + } + + private async apikey(instance: InstanceDto, token?: string) { + const apikey = token ? token : v4().toUpperCase(); + + this.logger.verbose(token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey); + + const auth = await this.repository.auth.create({ apikey }, instance.instanceName); + + this.logger.verbose('APIKEY saved in database'); + + if (auth['error']) { + this.logger.error({ + localError: AuthService.name + '.apikey', + error: auth['error'], + }); + throw new BadRequestException('Authentication error', auth['error']?.toString()); + } + + return { apikey }; + } + + public async checkDuplicateToken(token: string) { + const instances = await this.waMonitor.instanceInfo(); + + this.logger.verbose('checking duplicate token'); + + const instance = instances.find((instance) => instance.instance.apikey === token); + + if (instance) { + throw new BadRequestException('Token already exists'); + } + + this.logger.verbose('available token'); + + return true; + } + + public async generateHash(instance: InstanceDto, token?: string) { + const options = this.configService.get('AUTHENTICATION'); + + this.logger.verbose('generating hash ' + options.TYPE + ' to instance: ' + instance.instanceName); + + return (await this[options.TYPE](instance, token)) as { jwt: string } | { apikey: string }; + } + + public async refreshToken({ oldToken }: OldToken) { + this.logger.verbose('refreshing token'); + + if (!isJWT(oldToken)) { + throw new BadRequestException('Invalid "oldToken"'); + } + + try { + const jwtOpts = this.configService.get('AUTHENTICATION').JWT; + + this.logger.verbose('checking oldToken'); + + const decode = verify(oldToken, jwtOpts.SECRET, { + ignoreExpiration: true, + }) as Pick; + + this.logger.verbose('checking token in database'); + + const tokenStore = await this.repository.auth.find(decode.instanceName); + + const decodeTokenStore = verify(tokenStore.jwt, jwtOpts.SECRET, { + ignoreExpiration: true, + }) as Pick; + + this.logger.verbose('checking tokenId'); + + if (decode.tokenId !== decodeTokenStore.tokenId) { + throw new BadRequestException('Invalid "oldToken"'); + } + + this.logger.verbose('generating new token'); + + const token = { + jwt: (await this.jwt({ instanceName: decode.instanceName })).jwt, + instanceName: decode.instanceName, + }; + + try { + this.logger.verbose('checking webhook'); + const webhook = await this.repository.webhook.find(decode.instanceName); + if (webhook?.enabled && this.configService.get('WEBHOOK').EVENTS.NEW_JWT_TOKEN) { + this.logger.verbose('sending webhook'); + + const httpService = axios.create({ baseURL: webhook.url }); + await httpService.post( + '', { - instanceName: instance.instanceName, - apiName, - tokenId: v4(), + event: 'new.jwt', + instance: decode.instanceName, + data: token, }, - jwtOpts.SECRET, - { expiresIn: jwtOpts.EXPIRIN_IN, encoding: 'utf8', subject: 'g-t' }, - ); - - this.logger.verbose('JWT token created: ' + token); - - const auth = await this.repository.auth.create({ jwt: token }, instance.instanceName); - - this.logger.verbose('JWT token saved in database'); - - if (auth['error']) { - this.logger.error({ - localError: AuthService.name + '.jwt', - error: auth['error'], - }); - throw new BadRequestException('Authentication error', auth['error']?.toString()); + { params: { owner: this.waMonitor.waInstances[decode.instanceName].wuid } }, + ); } + } catch (error) { + this.logger.error(error); + } - return { jwt: token }; - } - - private async apikey(instance: InstanceDto, token?: string) { - const apikey = token ? token : v4().toUpperCase(); - - this.logger.verbose(token ? 'APIKEY defined: ' + apikey : 'APIKEY created: ' + apikey); - - const auth = await this.repository.auth.create({ apikey }, instance.instanceName); - - this.logger.verbose('APIKEY saved in database'); - - if (auth['error']) { - this.logger.error({ - localError: AuthService.name + '.apikey', - error: auth['error'], - }); - throw new BadRequestException('Authentication error', auth['error']?.toString()); - } - - return { apikey }; - } - - public async checkDuplicateToken(token: string) { - const instances = await this.waMonitor.instanceInfo(); - - this.logger.verbose('checking duplicate token'); - - const instance = instances.find((instance) => instance.instance.apikey === token); - - if (instance) { - throw new BadRequestException('Token already exists'); - } - - this.logger.verbose('available token'); - - return true; - } - - public async generateHash(instance: InstanceDto, token?: string) { - const options = this.configService.get('AUTHENTICATION'); - - this.logger.verbose('generating hash ' + options.TYPE + ' to instance: ' + instance.instanceName); - - return (await this[options.TYPE](instance, token)) as { jwt: string } | { apikey: string }; - } - - public async refreshToken({ oldToken }: OldToken) { - this.logger.verbose('refreshing token'); - - if (!isJWT(oldToken)) { - throw new BadRequestException('Invalid "oldToken"'); - } - - try { - const jwtOpts = this.configService.get('AUTHENTICATION').JWT; - - this.logger.verbose('checking oldToken'); - - const decode = verify(oldToken, jwtOpts.SECRET, { - ignoreExpiration: true, - }) as Pick; - - this.logger.verbose('checking token in database'); - - const tokenStore = await this.repository.auth.find(decode.instanceName); - - const decodeTokenStore = verify(tokenStore.jwt, jwtOpts.SECRET, { - ignoreExpiration: true, - }) as Pick; - - this.logger.verbose('checking tokenId'); - - if (decode.tokenId !== decodeTokenStore.tokenId) { - throw new BadRequestException('Invalid "oldToken"'); - } - - this.logger.verbose('generating new token'); - - const token = { - jwt: (await this.jwt({ instanceName: decode.instanceName })).jwt, - instanceName: decode.instanceName, - }; - - try { - this.logger.verbose('checking webhook'); - const webhook = await this.repository.webhook.find(decode.instanceName); - if (webhook?.enabled && this.configService.get('WEBHOOK').EVENTS.NEW_JWT_TOKEN) { - this.logger.verbose('sending webhook'); - - const httpService = axios.create({ baseURL: webhook.url }); - await httpService.post( - '', - { - event: 'new.jwt', - instance: decode.instanceName, - data: token, - }, - { params: { owner: this.waMonitor.waInstances[decode.instanceName].wuid } }, - ); - } - } catch (error) { - this.logger.error(error); - } - - this.logger.verbose('token refreshed'); - - return token; - } catch (error) { - this.logger.error({ - localError: AuthService.name + '.refreshToken', - error, - }); - throw new BadRequestException('Invalid "oldToken"'); - } + this.logger.verbose('token refreshed'); + + return token; + } catch (error) { + this.logger.error({ + localError: AuthService.name + '.refreshToken', + error, + }); + throw new BadRequestException('Invalid "oldToken"'); } + } } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index a2bd9bff..5acfe05b 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -15,1570 +15,1539 @@ import { SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto' import { WAMonitoringService } from './monitor.service'; export class ChatwootService { - private messageCacheFile: string; - private messageCache: Set; + private messageCacheFile: string; + private messageCache: Set; - private readonly logger = new Logger(ChatwootService.name); + private readonly logger = new Logger(ChatwootService.name); - private provider: any; + private provider: any; - constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { - this.messageCache = new Set(); + constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) { + this.messageCache = new Set(); + } + + private loadMessageCache(): Set { + this.logger.verbose('load message cache'); + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); + } + } + + private saveMessageCache() { + this.logger.verbose('save message cache'); + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + this.logger.verbose('message cache saved'); + } + + private clearMessageCache() { + this.logger.verbose('clear message cache'); + this.messageCache.clear(); + this.saveMessageCache(); + } + + private async getProvider(instance: InstanceDto) { + this.logger.verbose('get provider to instance: ' + instance.instanceName); + try { + const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + + if (!provider) { + this.logger.warn('provider not found'); + return null; + } + + this.logger.verbose('provider found'); + + return provider; + } catch (error) { + this.logger.error('provider not found'); + return null; + } + } + + private async clientCw(instance: InstanceDto) { + this.logger.verbose('get client to instance: ' + instance.instanceName); + const provider = await this.getProvider(instance); + + if (!provider) { + this.logger.error('provider not found'); + return null; } - private loadMessageCache(): Set { - this.logger.verbose('load message cache'); - try { - const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); - const cacheArray = cacheData.split('\n'); - return new Set(cacheArray); - } catch (error) { - return new Set(); - } + this.logger.verbose('provider found'); + + this.provider = provider; + + this.logger.verbose('create client to instance: ' + instance.instanceName); + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + this.logger.verbose('client created'); + + return client; + } + + public create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + this.logger.verbose('chatwoot created'); + return data; + } + + public async find(instance: InstanceDto): Promise { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + try { + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + this.logger.error('chatwoot not found'); + return { enabled: null, url: '' }; + } + } + + public async getContact(instance: InstanceDto, id: number) { + this.logger.verbose('get contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; } - private saveMessageCache() { - this.logger.verbose('save message cache'); - const cacheData = Array.from(this.messageCache).join('\n'); - writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); - this.logger.verbose('message cache saved'); + if (!id) { + this.logger.warn('id is required'); + return null; } - private clearMessageCache() { - this.logger.verbose('clear message cache'); - this.messageCache.clear(); - this.saveMessageCache(); + this.logger.verbose('find contact in chatwoot'); + const contact = await client.contact.getContactable({ + accountId: this.provider.account_id, + id, + }); + + if (!contact) { + this.logger.warn('contact not found'); + return null; } - private async getProvider(instance: InstanceDto) { - this.logger.verbose('get provider to instance: ' + instance.instanceName); - try { - const provider = await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + this.logger.verbose('contact found'); + return contact; + } - if (!provider) { - this.logger.warn('provider not found'); - return null; - } + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + number: string, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - this.logger.verbose('provider found'); + const client = await this.clientCw(instance); - return provider; - } catch (error) { - this.logger.error('provider not found'); - return null; - } + if (!client) { + this.logger.warn('client not found'); + return null; } - private async clientCw(instance: InstanceDto) { - this.logger.verbose('get client to instance: ' + instance.instanceName); - const provider = await this.getProvider(instance); + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); - if (!provider) { - this.logger.error('provider not found'); - return null; - } + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); - this.logger.verbose('provider found'); + let inboxId: number; - this.provider = provider; + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, + }; - this.logger.verbose('create client to instance: ' + instance.instanceName); - const client = new ChatwootClient({ - config: { - basePath: provider.url, - with_credentials: true, - credentials: 'include', - token: provider.token, - }, - }); + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); - this.logger.verbose('client created'); + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } - return client; + inboxId = inbox.id; + } else { + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; } - public create(instance: InstanceDto, data: ChatwootDto) { - this.logger.verbose('create chatwoot: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact(instance, '123456', inboxId, false, 'EvolutionAPI')) as any); - this.logger.verbose('chatwoot created'); - return data; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async find(instance: InstanceDto): Promise { - this.logger.verbose('find chatwoot: ' + instance.instanceName); - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch (error) { - this.logger.error('chatwoot not found'); - return { enabled: null, url: '' }; - } + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + + let contentMsg = '/init'; + + if (number) { + contentMsg = `/init:${number}`; + } + + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: contentMsg, + message_type: 'outgoing', + }, + }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } } - public async getContact(instance: InstanceDto, id: number) { - this.logger.verbose('get contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + this.logger.verbose('instance chatwoot initialized'); + return true; + } - if (!client) { - this.logger.warn('client not found'); - return null; - } + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + isGroup: boolean, + name?: string, + avatar_url?: string, + ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); - if (!id) { - this.logger.warn('id is required'); - return null; - } + const client = await this.clientCw(instance); - this.logger.verbose('find contact in chatwoot'); - const contact = await client.contact.getContactable({ - accountId: this.provider.account_id, - id, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact found'); - return contact; + if (!client) { + this.logger.warn('client not found'); + return null; } - public async initInstanceChatwoot( - instance: InstanceDto, - inboxName: string, - webhookUrl: string, - qrcode: boolean, - number: string, - ) { - this.logger.verbose('init instance chatwoot: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inbox in chatwoot'); - const findInbox: any = await client.inboxes.list({ - accountId: this.provider.account_id, - }); - - this.logger.verbose('check duplicate inbox'); - const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); - - let inboxId: number; - - if (!checkDuplicate) { - this.logger.verbose('create inbox in chatwoot'); - const data = { - type: 'api', - webhook_url: webhookUrl, - }; - - const inbox = await client.inboxes.create({ - accountId: this.provider.account_id, - data: { - name: inboxName, - channel: data as any, - }, - }); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } else { - this.logger.verbose('find inbox in chatwoot'); - const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - inboxId = inbox.id; - } - - this.logger.verbose('find contact in chatwoot and create if not exists'); - const contact = - (await this.findContact(instance, '123456')) || - ((await this.createContact(instance, '123456', inboxId, false, 'EvolutionAPI')) as any); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact.id || contact.payload.contact.id; - - if (qrcode) { - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: inboxId.toString(), - }; - - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('create message for init instance in chatwoot'); - - let contentMsg = '/init'; - - if (number) { - contentMsg = `/init:${number}`; - } - - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: contentMsg, - message_type: 'outgoing', - }, - }); - - if (!message) { - this.logger.warn('conversation not found'); - return null; - } - } - - this.logger.verbose('instance chatwoot initialized'); - return true; + let data: any = {}; + if (!isGroup) { + this.logger.verbose('create contact in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + avatar_url: avatar_url, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + avatar_url: avatar_url, + }; } - public async createContact( - instance: InstanceDto, - phoneNumber: string, - inboxId: number, - isGroup: boolean, - name?: string, - avatar_url?: string, - ) { - this.logger.verbose('create contact to instance: ' + instance.instanceName); + this.logger.verbose('create contact in chatwoot'); + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, + }); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let data: any = {}; - if (!isGroup) { - this.logger.verbose('create contact in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - phone_number: `+${phoneNumber}`, - avatar_url: avatar_url, - }; - } else { - this.logger.verbose('create contact group in chatwoot'); - data = { - inbox_id: inboxId, - name: name || phoneNumber, - identifier: phoneNumber, - avatar_url: avatar_url, - }; - } - - this.logger.verbose('create contact in chatwoot'); - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - this.logger.verbose('contact created'); - return contact; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async updateContact(instance: InstanceDto, id: number, data: any) { - this.logger.verbose('update contact to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + this.logger.verbose('contact created'); + return contact; + } - if (!client) { - this.logger.warn('client not found'); - return null; - } + public async updateContact(instance: InstanceDto, id: number, data: any) { + this.logger.verbose('update contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); - if (!id) { - this.logger.warn('id is required'); - return null; - } - - this.logger.verbose('update contact in chatwoot'); - const contact = await client.contacts.update({ - accountId: this.provider.account_id, - id, - data, - }); - - this.logger.verbose('contact updated'); - return contact; + if (!client) { + this.logger.warn('client not found'); + return null; } - public async findContact(instance: InstanceDto, phoneNumber: string) { - this.logger.verbose('find contact to instance: ' + instance.instanceName); - - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - let query: any; - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('format phone number'); - query = `+${phoneNumber}`; - } else { - this.logger.verbose('format group id'); - query = phoneNumber; - } - - this.logger.verbose('find contact in chatwoot'); - const contact: any = await client.contacts.search({ - accountId: this.provider.account_id, - q: query, - }); - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - if (!phoneNumber.includes('@g.us')) { - this.logger.verbose('return contact'); - return contact.payload.find((contact) => contact.phone_number === query); - } else { - this.logger.verbose('return group'); - return contact.payload.find((contact) => contact.identifier === query); - } + if (!id) { + this.logger.warn('id is required'); + return null; } - public async createConversation(instance: InstanceDto, body: any) { - this.logger.verbose('create conversation to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); + this.logger.verbose('update contact in chatwoot'); + const contact = await client.contacts.update({ + accountId: this.provider.account_id, + id, + data, + }); - if (!client) { - this.logger.warn('client not found'); - return null; - } + this.logger.verbose('contact updated'); + return contact; + } - const isGroup = body.key.remoteJid.includes('@g.us'); + public async findContact(instance: InstanceDto, phoneNumber: string) { + this.logger.verbose('find contact to instance: ' + instance.instanceName); - this.logger.verbose('is group: ' + isGroup); + const client = await this.clientCw(instance); - const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - - this.logger.verbose('chat id: ' + chatId); - - let nameContact: string; - - nameContact = !body.key.fromMe ? body.pushName : chatId; - - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); - - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } - - if (isGroup) { - this.logger.verbose('get group name'); - const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); - - nameContact = `${group.subject} (GROUP)`; - - this.logger.verbose('find or create participant in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( - body.key.participant.split('@')[0], - ); - - const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); - - if (findParticipant) { - if (!findParticipant.name || findParticipant.name === chatId) { - await this.updateContact(instance, findParticipant.id, { - name: body.pushName, - avatar_url: picture_url.profilePictureUrl || null, - }); - } - } else { - await this.createContact( - instance, - body.key.participant.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - ); - } - } - - this.logger.verbose('find or create contact in chatwoot'); - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); - - const findContact = await this.findContact(instance, chatId); - - let contact: any; - if (body.key.fromMe) { - if (findContact) { - contact = findContact; - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } else { - if (findContact) { - if (!findContact.name || findContact.name === chatId) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); - } else { - contact = findContact; - } - } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); - } - } - - if (!contact) { - this.logger.warn('contact not found'); - return null; - } - - const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; - - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - this.logger.verbose('update contact name in chatwoot'); - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } - - this.logger.verbose('get contact conversations in chatwoot'); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; - - if (contactConversations) { - let conversation: any; - if (this.provider.reopen_conversation) { - conversation = contactConversations.payload.find( - (conversation) => conversation.inbox_id == filterInbox.id, - ); - } else { - conversation = contactConversations.payload.find( - (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - } - this.logger.verbose('return conversation if exists'); - - if (conversation) { - this.logger.verbose('conversation found'); - return conversation.id; - } - } - - this.logger.verbose('create conversation in chatwoot'); - const data = { - contact_id: contactId.toString(), - inbox_id: filterInbox.id.toString(), - }; - - if (this.provider.conversation_pending) { - data['status'] = 'pending'; - } - - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data, - }); - - if (!conversation) { - this.logger.warn('conversation not found'); - return null; - } - - this.logger.verbose('conversation created'); - return conversation.id; - } catch (error) { - this.logger.error(error); - } + if (!client) { + this.logger.warn('client not found'); + return null; } - public async getInbox(instance: InstanceDto) { - this.logger.verbose('get inbox to instance: ' + instance.instanceName); + let query: any; - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('find inboxes in chatwoot'); - const inbox = (await client.inboxes.list({ - accountId: this.provider.account_id, - })) as any; - - if (!inbox) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('find inbox by name'); - const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName); - - if (!findByName) { - this.logger.warn('inbox not found'); - return null; - } - - this.logger.verbose('return inbox'); - return findByName; + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('format phone number'); + query = `+${phoneNumber}`; + } else { + this.logger.verbose('format group id'); + query = phoneNumber; } - public async createMessage( - instance: InstanceDto, - conversationId: number, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create message to instance: ' + instance.instanceName); + this.logger.verbose('find contact in chatwoot'); + const contact: any = await client.contacts.search({ + accountId: this.provider.account_id, + q: query, + }); - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversationId, - data: { - content: content, - message_type: messageType, - attachments: attachments, - private: privateMessage || false, - }, - }); - - if (!message) { - this.logger.warn('message not found'); - return null; - } - - this.logger.verbose('message created'); - - return message; + if (!contact) { + this.logger.warn('contact not found'); + return null; } - public async createBotMessage( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - attachments?: { - content: unknown; - encoding: string; - filename: string; - }[], - ) { - this.logger.verbose('create bot message to instance: ' + instance.instanceName); + if (!phoneNumber.includes('@g.us')) { + this.logger.verbose('return contact'); + return contact.payload.find((contact) => contact.phone_number === query); + } else { + this.logger.verbose('return group'); + return contact.payload.find((contact) => contact.identifier === query); + } + } - const client = await this.clientCw(instance); + public async createConversation(instance: InstanceDto, body: any) { + this.logger.verbose('create conversation to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); - if (!client) { - this.logger.warn('client not found'); - return null; - } + if (!client) { + this.logger.warn('client not found'); + return null; + } - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); + const isGroup = body.key.remoteJid.includes('@g.us'); - if (!contact) { - this.logger.warn('contact not found'); - return null; - } + this.logger.verbose('is group: ' + isGroup); - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; - } + this.logger.verbose('chat id: ' + chatId); - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); + let nameContact: string; - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } + nameContact = !body.key.fromMe ? body.pushName : chatId; - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + if (isGroup) { + this.logger.verbose('get group name'); + const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); + + nameContact = `${group.subject} (GROUP)`; + + this.logger.verbose('find or create participant in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( + body.key.participant.split('@')[0], ); - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } + const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); - this.logger.verbose('create message in chatwoot'); - const message = await client.messages.create({ - accountId: this.provider.account_id, - conversationId: conversation.id, - data: { - content: content, - message_type: messageType, - attachments: attachments, - }, + if (findParticipant) { + if (!findParticipant.name || findParticipant.name === chatId) { + await this.updateContact(instance, findParticipant.id, { + name: body.pushName, + avatar_url: picture_url.profilePictureUrl || null, + }); + } + } else { + await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName, + picture_url.profilePictureUrl || null, + ); + } + } + + this.logger.verbose('find or create contact in chatwoot'); + + const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); + + const findContact = await this.findContact(instance, chatId); + + let contact: any; + if (body.key.fromMe) { + if (findContact) { + contact = findContact; + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } else { + if (findContact) { + if (!findContact.name || findContact.name === chatId) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = findContact; + } + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } + } + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, }); + } - if (!message) { - this.logger.warn('message not found'); - return null; + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + let conversation: any; + if (this.provider.reopen_conversation) { + conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); + } else { + conversation = contactConversations.payload.find( + (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); } + this.logger.verbose('return conversation if exists'); - this.logger.verbose('bot message created'); + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } - return message; + this.logger.verbose('create conversation in chatwoot'); + const data = { + contact_id: contactId.toString(), + inbox_id: filterInbox.id.toString(), + }; + + if (this.provider.conversation_pending) { + data['status'] = 'pending'; + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; } - private async sendData( - conversationId: number, - file: string, - messageType: 'incoming' | 'outgoing' | undefined, - content?: string, - ) { - this.logger.verbose('send data to chatwoot'); + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; - const data = new FormData(); + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } + this.logger.verbose('find inbox by name'); + const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName); - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); + if (!findByName) { + this.logger.warn('inbox not found'); + return null; + } - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); + this.logger.verbose('return inbox'); + return findByName; + } - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, - headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), - }, - data: data, + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + private: privateMessage || false, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('message created'); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + this.logger.verbose('create bot message to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('create message in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + ) { + this.logger.verbose('send data to chatwoot'); + + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + unlinkSync(file); + } + } + + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + this.logger.verbose('create bot qr to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find contact in chatwoot'); + const contact = await this.findContact(instance, '123456'); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + const filterInbox = await this.getInbox(instance); + + if (!filterInbox) { + this.logger.warn('inbox not found'); + return null; + } + + this.logger.verbose('find conversation in chatwoot'); + const findConversation = await client.conversations.list({ + accountId: this.provider.account_id, + inboxId: filterInbox.id, + }); + + if (!findConversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('find conversation by contact id'); + const conversation = findConversation.data.payload.find( + (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + if (!conversation) { + this.logger.warn('conversation not found'); + return; + } + + this.logger.verbose('send data to chatwoot'); + const data = new FormData(); + + if (content) { + this.logger.verbose('content found'); + data.append('content', content); + } + + this.logger.verbose('message type: ' + messageType); + data.append('message_type', messageType); + + if (file) { + this.logger.verbose('temp file found'); + data.append('attachments[]', createReadStream(file)); + } + + this.logger.verbose('get client to instance: ' + this.provider.instanceName); + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + this.logger.verbose('send data to chatwoot'); + try { + const { data } = await axios.request(config); + + this.logger.verbose('remove temp file'); + unlinkSync(file); + + this.logger.verbose('data sent'); + return data; + } catch (error) { + this.logger.error(error); + } + } + + public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { + this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + + try { + this.logger.verbose('get media type'); + const parts = media.split('/'); + + const fileName = decodeURIComponent(parts[parts.length - 1]); + this.logger.verbose('file name: ' + fileName); + + const mimeType = mimeTypes.lookup(fileName).toString(); + this.logger.verbose('mime type: ' + mimeType); + + let type = 'document'; + + switch (mimeType.split('/')[0]) { + case 'image': + type = 'image'; + break; + case 'video': + type = 'video'; + break; + case 'audio': + type = 'audio'; + break; + default: + type = 'document'; + break; + } + + this.logger.verbose('type: ' + type); + + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, }; - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); + await waInstance?.audioWhatsapp(data); - this.logger.verbose('remove temp file'); - unlinkSync(file); + this.logger.verbose('audio sent'); + return; + } - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); - unlinkSync(file); - } + this.logger.verbose('send media to instance: ' + waInstance.instanceName); + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption) { + this.logger.verbose('caption found'); + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + this.logger.verbose('media sent'); + return; + } catch (error) { + this.logger.error(error); } + } - public async createBotQr( - instance: InstanceDto, - content: string, - messageType: 'incoming' | 'outgoing' | undefined, - file?: string, - ) { - this.logger.verbose('create bot qr to instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); + const client = await this.clientCw(instance); - if (!client) { - this.logger.warn('client not found'); - return null; + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('check if is bot'); + if (!body?.conversation || body.private) return { message: 'bot' }; + + this.logger.verbose('check if is group'); + const chatId = + body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier; + const messageReceived = body.content; + const senderName = body?.sender?.name; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + this.logger.verbose('check if is command'); + + const command = messageReceived.replace('/', ''); + + if (command.includes('init') || command.includes('iniciar')) { + this.logger.verbose('command init found'); + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + this.logger.verbose('connect to whatsapp'); + const number = command.split(':')[1]; + await waInstance.connectToWhatsapp(number); + } else { + this.logger.verbose('whatsapp already connected'); + await this.createBotMessage(instance, `🚨 ${body.inbox.name} instance is connected.`, 'incoming'); + } } - this.logger.verbose('find contact in chatwoot'); - const contact = await this.findContact(instance, '123456'); + if (command === 'status') { + this.logger.verbose('command status found'); - if (!contact) { - this.logger.warn('contact not found'); - return null; + const state = waInstance?.connectionStatus?.state; + + if (!state) { + this.logger.verbose('state not found'); + await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance not found.`, 'incoming'); + } + + if (state) { + this.logger.verbose('state: ' + state + ' found'); + await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance status: *${state}*`, 'incoming'); + } } - this.logger.verbose('get inbox to instance: ' + instance.instanceName); - const filterInbox = await this.getInbox(instance); + if (command === 'disconnect' || command === 'desconectar') { + this.logger.verbose('command disconnect found'); - if (!filterInbox) { - this.logger.warn('inbox not found'); - return null; + const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgLogout, 'incoming'); + + this.logger.verbose('disconnect to whatsapp'); + await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); + await waInstance?.client?.ws?.close(); } - this.logger.verbose('find conversation in chatwoot'); - const findConversation = await client.conversations.list({ - accountId: this.provider.account_id, - inboxId: filterInbox.id, - }); + if (command.includes('new_instance')) { + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - if (!findConversation) { - this.logger.warn('conversation not found'); - return null; - } + const data = { + instanceName: command.split(':')[1], + qrcode: true, + chatwoot_account_id: this.provider.account_id, + chatwoot_token: this.provider.token, + chatwoot_url: this.provider.url, + chatwoot_sign_msg: this.provider.sign_msg, + }; - this.logger.verbose('find conversation by contact id'); - const conversation = findConversation.data.payload.find( - (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', - ); + if (command.split(':')[2]) { + data['number'] = command.split(':')[2]; + } - if (!conversation) { - this.logger.warn('conversation not found'); - return; - } - - this.logger.verbose('send data to chatwoot'); - const data = new FormData(); - - if (content) { - this.logger.verbose('content found'); - data.append('content', content); - } - - this.logger.verbose('message type: ' + messageType); - data.append('message_type', messageType); - - if (file) { - this.logger.verbose('temp file found'); - data.append('attachments[]', createReadStream(file)); - } - - this.logger.verbose('get client to instance: ' + this.provider.instanceName); - const config = { + const config = { method: 'post', maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, + url: `${urlServer}/instance/create`, headers: { - api_access_token: this.provider.token, - ...data.getHeaders(), + 'Content-Type': 'application/json', + apikey: apiKey, }, data: data, - }; + }; - this.logger.verbose('send data to chatwoot'); - try { - const { data } = await axios.request(config); - - this.logger.verbose('remove temp file'); - unlinkSync(file); - - this.logger.verbose('data sent'); - return data; - } catch (error) { - this.logger.error(error); + await axios.request(config); } - } + } - public async sendAttachment(waInstance: any, number: string, media: any, caption?: string) { - this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); + if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { + this.logger.verbose('check if is group'); - try { - this.logger.verbose('get media type'); - const parts = media.split('/'); + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + this.logger.verbose('cache file path: ' + this.messageCacheFile); - const fileName = decodeURIComponent(parts[parts.length - 1]); - this.logger.verbose('file name: ' + fileName); + this.messageCache = this.loadMessageCache(); + this.logger.verbose('cache file loaded'); + this.logger.verbose(this.messageCache); - const mimeType = mimeTypes.lookup(fileName).toString(); - this.logger.verbose('mime type: ' + mimeType); + this.logger.verbose('check if message is cached'); + if (this.messageCache.has(body.id.toString())) { + this.logger.verbose('message is cached'); + return { message: 'bot' }; + } - let type = 'document'; + this.logger.verbose('clear cache'); + this.clearMessageCache(); - switch (mimeType.split('/')[0]) { - case 'image': - type = 'image'; - break; - case 'video': - type = 'video'; - break; - case 'audio': - type = 'audio'; - break; - default: - type = 'document'; - break; + this.logger.verbose('Format message to send'); + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + formatText = this.provider.sign_msg ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; + } + + for (const message of body.conversation.messages) { + this.logger.verbose('check if message is media'); + if (message.attachments && message.attachments.length > 0) { + this.logger.verbose('message is media'); + for (const attachment of message.attachments) { + this.logger.verbose('send media to whatsapp'); + if (!messageReceived) { + this.logger.verbose('message do not have text'); + formatText = null; + } + + await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); } + } else { + this.logger.verbose('message is text'); - this.logger.verbose('type: ' + type); - - if (type === 'audio') { - this.logger.verbose('send audio to instance: ' + waInstance.instanceName); - const data: SendAudioDto = { - number: number, - audioMessage: { - audio: media, - }, - options: { - delay: 1200, - presence: 'recording', - }, - }; - - await waInstance?.audioWhatsapp(data); - - this.logger.verbose('audio sent'); - return; - } - - this.logger.verbose('send media to instance: ' + waInstance.instanceName); - const data: SendMediaDto = { - number: number, - mediaMessage: { - mediatype: type as any, - fileName: fileName, - media: media, - }, - options: { - delay: 1200, - presence: 'composing', - }, + this.logger.verbose('send text to whatsapp'); + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, }; - if (caption) { - this.logger.verbose('caption found'); - data.mediaMessage.caption = caption; - } - - await waInstance?.mediaMessage(data); - - this.logger.verbose('media sent'); - return; - } catch (error) { - this.logger.error(error); + await waInstance?.textMessage(data); + } } - } + } - public async receiveWebhook(instance: InstanceDto, body: any) { - try { - this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); - const client = await this.clientCw(instance); + if (body.message_type === 'template' && body.event === 'message_created') { + this.logger.verbose('check if is template'); - if (!client) { - this.logger.warn('client not found'); - return null; - } - - this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private) return { message: 'bot' }; - - this.logger.verbose('check if is group'); - const chatId = - body.conversation.meta.sender?.phone_number?.replace('+', '') || - body.conversation.meta.sender?.identifier; - const messageReceived = body.content; - const senderName = body?.sender?.name; - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (chatId === '123456' && body.message_type === 'outgoing') { - this.logger.verbose('check if is command'); - - const command = messageReceived.replace('/', ''); - - if (command.includes('init') || command.includes('iniciar')) { - this.logger.verbose('command init found'); - const state = waInstance?.connectionStatus?.state; - - if (state !== 'open') { - this.logger.verbose('connect to whatsapp'); - const number = command.split(':')[1]; - await waInstance.connectToWhatsapp(number); - } else { - this.logger.verbose('whatsapp already connected'); - await this.createBotMessage( - instance, - `🚨 ${body.inbox.name} instance is connected.`, - 'incoming', - ); - } - } - - if (command === 'status') { - this.logger.verbose('command status found'); - - const state = waInstance?.connectionStatus?.state; - - if (!state) { - this.logger.verbose('state not found'); - await this.createBotMessage(instance, `⚠️ ${body.inbox.name} instance not found.`, 'incoming'); - } - - if (state) { - this.logger.verbose('state: ' + state + ' found'); - await this.createBotMessage( - instance, - `⚠️ ${body.inbox.name} instance status: *${state}*`, - 'incoming', - ); - } - } - - if (command === 'disconnect' || command === 'desconectar') { - this.logger.verbose('command disconnect found'); - - const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgLogout, 'incoming'); - - this.logger.verbose('disconnect to whatsapp'); - await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); - await waInstance?.client?.ws?.close(); - } - - if (command.includes('new_instance')) { - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - const data = { - instanceName: command.split(':')[1], - qrcode: true, - chatwoot_account_id: this.provider.account_id, - chatwoot_token: this.provider.token, - chatwoot_url: this.provider.url, - chatwoot_sign_msg: this.provider.sign_msg, - }; - - if (command.split(':')[2]) { - data['number'] = command.split(':')[2]; - } - - const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: data, - }; - - await axios.request(config); - } - } - - if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') { - this.logger.verbose('check if is group'); - - this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); - this.logger.verbose('cache file path: ' + this.messageCacheFile); - - this.messageCache = this.loadMessageCache(); - this.logger.verbose('cache file loaded'); - this.logger.verbose(this.messageCache); - - this.logger.verbose('check if message is cached'); - if (this.messageCache.has(body.id.toString())) { - this.logger.verbose('message is cached'); - return { message: 'bot' }; - } - - this.logger.verbose('clear cache'); - this.clearMessageCache(); - - this.logger.verbose('Format message to send'); - let formatText: string; - if (senderName === null || senderName === undefined) { - formatText = messageReceived; - } else { - formatText = this.provider.sign_msg ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; - } - - for (const message of body.conversation.messages) { - this.logger.verbose('check if message is media'); - if (message.attachments && message.attachments.length > 0) { - this.logger.verbose('message is media'); - for (const attachment of message.attachments) { - this.logger.verbose('send media to whatsapp'); - if (!messageReceived) { - this.logger.verbose('message do not have text'); - formatText = null; - } - - await this.sendAttachment(waInstance, chatId, attachment.data_url, formatText); - } - } else { - this.logger.verbose('message is text'); - - this.logger.verbose('send text to whatsapp'); - const data: SendTextDto = { - number: chatId, - textMessage: { - text: formatText, - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - await waInstance?.textMessage(data); - } - } - } - - if (body.message_type === 'template' && body.event === 'message_created') { - this.logger.verbose('check if is template'); - - const data: SendTextDto = { - number: chatId, - textMessage: { - text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), - }, - options: { - delay: 1200, - presence: 'composing', - }, - }; - - this.logger.verbose('send text to whatsapp'); - - await waInstance?.textMessage(data); - } - - return { message: 'bot' }; - } catch (error) { - this.logger.error(error); - - return { message: 'bot' }; - } - } - - private isMediaMessage(message: any) { - this.logger.verbose('check if is media message'); - const media = [ - 'imageMessage', - 'documentMessage', - 'documentWithCaptionMessage', - 'audioMessage', - 'videoMessage', - 'stickerMessage', - ]; - - const messageKeys = Object.keys(message); - - const result = messageKeys.some((key) => media.includes(key)); - - this.logger.verbose('is media message: ' + result); - return result; - } - - private getTypeMessage(msg: any) { - this.logger.verbose('get type message'); - - const types = { - conversation: msg.conversation, - imageMessage: msg.imageMessage?.caption, - videoMessage: msg.videoMessage?.caption, - extendedTextMessage: msg.extendedTextMessage?.text, - messageContextInfo: msg.messageContextInfo?.stanzaId, - stickerMessage: undefined, - documentMessage: msg.documentMessage?.caption, - documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, - audioMessage: msg.audioMessage?.caption, - contactMessage: msg.contactMessage?.vcard, - contactsArrayMessage: msg.contactsArrayMessage, - locationMessage: msg.locationMessage, - liveLocationMessage: msg.liveLocationMessage, + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), + }, + options: { + delay: 1200, + presence: 'composing', + }, }; - this.logger.verbose('type message: ' + types); + this.logger.verbose('send text to whatsapp'); - return types; + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; } + } - private getMessageContent(types: any) { - this.logger.verbose('get message content'); - const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; - const result = typeKey ? types[typeKey] : undefined; + const messageKeys = Object.keys(message); - if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { - const latitude = result.degreesLatitude; - const longitude = result.degreesLongitude; + const result = messageKeys.some((key) => media.includes(key)); - const formattedLocation = `**Location:** + this.logger.verbose('is media message: ' + result); + return result; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: undefined, + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, + locationMessage: msg.locationMessage, + liveLocationMessage: msg.liveLocationMessage, + }; + + this.logger.verbose('type message: ' + types); + + return types; + } + + private getMessageContent(types: any) { + this.logger.verbose('get message content'); + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + const result = typeKey ? types[typeKey] : undefined; + + if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { + const latitude = result.degreesLatitude; + const longitude = result.degreesLongitude; + + const formattedLocation = `**Location:** **latitude:** ${latitude} **longitude:** ${longitude} https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} `; - this.logger.verbose('message content: ' + formattedLocation); + this.logger.verbose('message content: ' + formattedLocation); - return formattedLocation; + return formattedLocation; + } + + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; } + }); - if (typeKey === 'contactMessage') { - const vCardData = result.split('\n'); - const contactInfo = {}; - - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); - - let formattedContact = `**Contact:** + let formattedContact = `**Contact:** **name:** ${contactInfo['FN']}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); - - this.logger.verbose('message content: ' + formattedContact); - return formattedContact; + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; } + }); - if (typeKey === 'contactsArrayMessage') { - const formattedContacts = result.contacts.map((contact) => { - const vCardData = contact.vcard.split('\n'); - const contactInfo = {}; + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } - vCardData.forEach((line) => { - const [key, value] = line.split(':'); - if (key && value) { - contactInfo[key] = value; - } - }); + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; - let formattedContact = `**Contact:** + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** **name:** ${contact.displayName}`; - let numberCount = 1; - Object.keys(contactInfo).forEach((key) => { - if (key.startsWith('item') && key.includes('TEL')) { - const phoneNumber = contactInfo[key]; - formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; - numberCount++; - } - }); + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); - return formattedContact; - }); + return formattedContact; + }); - const formattedContactsArray = formattedContacts.join('\n\n'); + const formattedContactsArray = formattedContacts.join('\n\n'); - this.logger.verbose('formatted contacts: ' + formattedContactsArray); + this.logger.verbose('formatted contacts: ' + formattedContactsArray); - return formattedContactsArray; - } - - this.logger.verbose('message content: ' + result); - - return result; + return formattedContactsArray; } - private getConversationMessage(msg: any) { + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { + this.logger.verbose('get conversation message'); + + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + this.logger.verbose('conversation message: ' + messageContent); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } + + if (event === 'messages.upsert') { + this.logger.verbose('event messages.upsert'); + + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); - const types = this.getTypeMessage(msg); + const isMedia = this.isMediaMessage(body.message); - const messageContent = this.getMessageContent(types); - - this.logger.verbose('conversation message: ' + messageContent); - - return messageContent; - } - - public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { - this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); - try { - const client = await this.clientCw(instance); - - if (!client) { - this.logger.warn('client not found'); - return null; - } - - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - - if (!waInstance) { - this.logger.warn('wa instance not found'); - return null; - } - - if (event === 'messages.upsert') { - this.logger.verbose('event messages.upsert'); - - if (body.key.remoteJid === 'status@broadcast') { - this.logger.verbose('status broadcast found'); - return; - } - - this.logger.verbose('get conversation message'); - const bodyMessage = await this.getConversationMessage(body.message); - - const isMedia = this.isMediaMessage(body.message); - - if (!bodyMessage && !isMedia) { - this.logger.warn('no body message found'); - return; - } - - this.logger.verbose('get conversation in chatwoot'); - const getConversion = await this.createConversation(instance, body); - - if (!getConversion) { - this.logger.warn('conversation not found'); - return; - } - - const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; - - this.logger.verbose('message type: ' + messageType); - - this.logger.verbose('is media: ' + isMedia); - - this.logger.verbose('check if is media'); - if (isMedia) { - this.logger.verbose('message is media'); - - this.logger.verbose('get base64 from media message'); - const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ - message: { - ...body, - }, - }); - - const random = Math.random().toString(36).substring(7); - const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; - - const fileData = Buffer.from(downloadBase64.base64, 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - - this.logger.verbose('temp file name: ' + nameFile); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversion, fileName, messageType, content); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.sendData(getConversion, fileName, messageType, bodyMessage); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - this.logger.verbose('check if is group'); - if (body.key.remoteJid.includes('@g.us')) { - this.logger.verbose('message is group'); - const participantName = body.pushName; - - let content: string; - - if (!body.key.fromMe) { - this.logger.verbose('message is not from me'); - content = `**${participantName}**\n\n${bodyMessage}`; - } else { - this.logger.verbose('message is from me'); - content = `${bodyMessage}`; - } - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversion, content, messageType); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } else { - this.logger.verbose('message is not group'); - - this.logger.verbose('send data to chatwoot'); - const send = await this.createMessage(instance, getConversion, bodyMessage, messageType); - - if (!send) { - this.logger.warn('message not sent'); - return; - } - - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); - - this.messageCache = this.loadMessageCache(); - - this.messageCache.add(send.id.toString()); - - this.logger.verbose('save message cache'); - this.saveMessageCache(); - - return send; - } - } - - if (event === 'status.instance') { - this.logger.verbose('event status.instance'); - const data = body; - const inbox = await this.getInbox(instance); - - if (!inbox) { - this.logger.warn('inbox not found'); - return; - } - - const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgStatus, 'incoming'); - } - - if (event === 'connection.update') { - this.logger.verbose('event connection.update'); - - if (body.status === 'open') { - const msgConnection = `🚀 Connection successfully established!`; - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - } - } - - if (event === 'qrcode.updated') { - this.logger.verbose('event qrcode.updated'); - if (body.statusCode === 500) { - this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; - - this.logger.verbose('send message to chatwoot'); - return await this.createBotMessage(instance, erroQRcode, 'incoming'); - } else { - this.logger.verbose('qrcode success'); - const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); - - const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; - - this.logger.verbose('temp file name: ' + fileName); - - this.logger.verbose('create temp file'); - writeFileSync(fileName, fileData, 'utf8'); - - this.logger.verbose('send qrcode to chatwoot'); - await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); - - let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; - - if (body?.qrcode?.pairingCode) { - msgQrCode = - msgQrCode + - `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring( - 0, - 4, - )}-${body.qrcode.pairingCode.substring(4, 8)}`; - } - - this.logger.verbose('send message to chatwoot'); - await this.createBotMessage(instance, msgQrCode, 'incoming'); - } - } - } catch (error) { - this.logger.error(error); + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; } - } - public async newInstance(data: any) { - try { - const instanceName = data.instanceName; - const qrcode = true; - const number = data.number; - const accountId = data.accountId; - const chatwootToken = data.token; - const chatwootUrl = data.url; - const signMsg = true; - const urlServer = this.configService.get('SERVER').URL; - const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + this.logger.verbose('get conversation in chatwoot'); + const getConversion = await this.createConversation(instance, body); - const requestData = { - instanceName, - qrcode, - chatwoot_account_id: accountId, - chatwoot_token: chatwootToken, - chatwoot_url: chatwootUrl, - chatwoot_sign_msg: signMsg, - }; + if (!getConversion) { + this.logger.warn('conversation not found'); + return; + } - if (number) { - requestData['number'] = number; + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + this.logger.verbose('message type: ' + messageType); + + this.logger.verbose('is media: ' + isMedia); + + this.logger.verbose('check if is media'); + if (isMedia) { + this.logger.verbose('message is media'); + + this.logger.verbose('get base64 from media message'); + const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ + message: { + ...body, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + + this.logger.verbose('temp file name: ' + nameFile); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; } - // eslint-disable-next-line + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, content); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.sendData(getConversion, fileName, messageType, bodyMessage); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + this.logger.verbose('check if is group'); + if (body.key.remoteJid.includes('@g.us')) { + this.logger.verbose('message is group'); + const participantName = body.pushName; + + let content: string; + + if (!body.key.fromMe) { + this.logger.verbose('message is not from me'); + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + this.logger.verbose('message is from me'); + content = `${bodyMessage}`; + } + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, content, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } else { + this.logger.verbose('message is not group'); + + this.logger.verbose('send data to chatwoot'); + const send = await this.createMessage(instance, getConversion, bodyMessage, messageType); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + this.messageCacheFile = path.join(ROOT_DIR, 'store', 'chatwoot', `${instance.instanceName}_cache.txt`); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.logger.verbose('save message cache'); + this.saveMessageCache(); + + return send; + } + } + + if (event === 'status.instance') { + this.logger.verbose('event status.instance'); + const data = body; + const inbox = await this.getInbox(instance); + + if (!inbox) { + this.logger.warn('inbox not found'); + return; + } + + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + this.logger.verbose('event connection.update'); + + if (body.status === 'open') { + const msgConnection = `🚀 Connection successfully established!`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'qrcode.updated') { + this.logger.verbose('event qrcode.updated'); + if (body.statusCode === 500) { + this.logger.verbose('qrcode error'); + const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the /init message again.`; + + this.logger.verbose('send message to chatwoot'); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + this.logger.verbose('qrcode success'); + const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; + + this.logger.verbose('temp file name: ' + fileName); + + this.logger.verbose('create temp file'); + writeFileSync(fileName, fileData, 'utf8'); + + this.logger.verbose('send qrcode to chatwoot'); + await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); + + let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; + + if (body?.qrcode?.pairingCode) { + msgQrCode = + msgQrCode + + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( + 4, + 8, + )}`; + } + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); + } + } + + public async newInstance(data: any) { + try { + const instanceName = data.instanceName; + const qrcode = true; + const number = data.number; + const accountId = data.accountId; + const chatwootToken = data.token; + const chatwootUrl = data.url; + const signMsg = true; + const urlServer = this.configService.get('SERVER').URL; + const apiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + const requestData = { + instanceName, + qrcode, + chatwoot_account_id: accountId, + chatwoot_token: chatwootToken, + chatwoot_url: chatwootUrl, + chatwoot_sign_msg: signMsg, + }; + + if (number) { + requestData['number'] = number; + } + + // eslint-disable-next-line const config = { - method: 'post', - maxBodyLength: Infinity, - url: `${urlServer}/instance/create`, - headers: { - 'Content-Type': 'application/json', - apikey: apiKey, - }, - data: requestData, - }; + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: requestData, + }; - // await axios.request(config); + // await axios.request(config); - return true; - } catch (error) { - this.logger.error(error); - return null; - } + return true; + } catch (error) { + this.logger.error(error); + return null; } + } } diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 1504366a..50f671d9 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -11,344 +11,344 @@ import { dbserver } from '../../db/db.connect'; import { RedisCache } from '../../db/redis.client'; import { NotFoundException } from '../../exceptions'; import { - AuthModel, - ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, - SettingsModel, - WebhookModel, + AuthModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + SettingsModel, + WebhookModel, } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { WAStartupService } from './whatsapp.service'; export class WAMonitoringService { - constructor( - private readonly eventEmitter: EventEmitter2, - private readonly configService: ConfigService, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('instance created'); + constructor( + private readonly eventEmitter: EventEmitter2, + private readonly configService: ConfigService, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('instance created'); - this.removeInstance(); - this.noConnection(); - this.delInstanceFiles(); + this.removeInstance(); + this.noConnection(); + this.delInstanceFiles(); - Object.assign(this.db, configService.get('DATABASE')); - Object.assign(this.redis, configService.get('REDIS')); + Object.assign(this.db, configService.get('DATABASE')); + Object.assign(this.redis, configService.get('REDIS')); - this.dbInstance = this.db.ENABLED - ? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances') - : undefined; + this.dbInstance = this.db.ENABLED + ? this.repository.dbServer?.db(this.db.CONNECTION.DB_PREFIX_NAME + '-instances') + : undefined; + } + + private readonly db: Partial = {}; + private readonly redis: Partial = {}; + + private dbInstance: Db; + + private dbStore = dbserver; + + private readonly logger = new Logger(WAMonitoringService.name); + public readonly waInstances: Record = {}; + + public delInstanceTime(instance: string) { + const time = this.configService.get('DEL_INSTANCE'); + if (typeof time === 'number' && time > 0) { + this.logger.verbose(`Instance "${instance}" don't have connection, will be removed in ${time} minutes`); + + setTimeout(async () => { + if (this.waInstances[instance]?.connectionStatus?.state !== 'open') { + if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') { + await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance); + this.waInstances[instance]?.client?.ws?.close(); + this.waInstances[instance]?.client?.end(undefined); + delete this.waInstances[instance]; + } else { + delete this.waInstances[instance]; + this.eventEmitter.emit('remove.instance', instance, 'inner'); + } + } + }, 1000 * 60 * time); + } + } + + public async instanceInfo(instanceName?: string) { + this.logger.verbose('get instance info'); + if (instanceName && !this.waInstances[instanceName]) { + throw new NotFoundException(`Instance "${instanceName}" not found`); } - private readonly db: Partial = {}; - private readonly redis: Partial = {}; + const instances: any[] = []; - private dbInstance: Db; + for await (const [key, value] of Object.entries(this.waInstances)) { + if (value) { + this.logger.verbose('get instance info: ' + key); + let chatwoot: any; - private dbStore = dbserver; + const urlServer = this.configService.get('SERVER').URL; - private readonly logger = new Logger(WAMonitoringService.name); - public readonly waInstances: Record = {}; + const findChatwoot = await this.waInstances[key].findChatwoot(); - public delInstanceTime(instance: string) { - const time = this.configService.get('DEL_INSTANCE'); - if (typeof time === 'number' && time > 0) { - this.logger.verbose(`Instance "${instance}" don't have connection, will be removed in ${time} minutes`); - - setTimeout(async () => { - if (this.waInstances[instance]?.connectionStatus?.state !== 'open') { - if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') { - await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance); - this.waInstances[instance]?.client?.ws?.close(); - this.waInstances[instance]?.client?.end(undefined); - delete this.waInstances[instance]; - } else { - delete this.waInstances[instance]; - this.eventEmitter.emit('remove.instance', instance, 'inner'); - } - } - }, 1000 * 60 * time); + if (findChatwoot && findChatwoot.enabled) { + chatwoot = { + ...findChatwoot, + webhook_url: `${urlServer}/chatwoot/webhook/${key}`, + }; } + + if (value.connectionStatus.state === 'open') { + this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); + + const instanceData = { + instance: { + instanceName: key, + owner: value.wuid, + profileName: (await value.getProfileName()) || 'not loaded', + profilePictureUrl: value.profilePictureUrl, + profileStatus: (await value.getProfileStatus()) || '', + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + + instanceData.instance['chatwoot'] = chatwoot; + } + + instances.push(instanceData); + } else { + this.logger.verbose('instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state); + + const instanceData = { + instance: { + instanceName: key, + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; + + instanceData.instance['chatwoot'] = chatwoot; + } + + instances.push(instanceData); + } + } } - public async instanceInfo(instanceName?: string) { - this.logger.verbose('get instance info'); - if (instanceName && !this.waInstances[instanceName]) { - throw new NotFoundException(`Instance "${instanceName}" not found`); - } + this.logger.verbose('return instance info: ' + instances.length); - const instances: any[] = []; + return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; + } - for await (const [key, value] of Object.entries(this.waInstances)) { - if (value) { - this.logger.verbose('get instance info: ' + key); - let chatwoot: any; - - const urlServer = this.configService.get('SERVER').URL; - - const findChatwoot = await this.waInstances[key].findChatwoot(); - - if (findChatwoot && findChatwoot.enabled) { - chatwoot = { - ...findChatwoot, - webhook_url: `${urlServer}/chatwoot/webhook/${key}`, - }; - } - - if (value.connectionStatus.state === 'open') { - this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); - - const instanceData = { - instance: { - instanceName: key, - owner: value.wuid, - profileName: (await value.getProfileName()) || 'not loaded', - profilePictureUrl: value.profilePictureUrl, - profileStatus: (await value.getProfileStatus()) || '', - status: value.connectionStatus.state, - }, - }; - - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; - - instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; - - instanceData.instance['chatwoot'] = chatwoot; - } - - instances.push(instanceData); - } else { - this.logger.verbose('instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state); - - const instanceData = { - instance: { - instanceName: key, - status: value.connectionStatus.state, - }, - }; - - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - instanceData.instance['serverUrl'] = this.configService.get('SERVER').URL; - - instanceData.instance['apikey'] = (await this.repository.auth.find(key)).apikey; - - instanceData.instance['chatwoot'] = chatwoot; - } - - instances.push(instanceData); - } - } - } - - this.logger.verbose('return instance info: ' + instances.length); - - return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; - } - - private delInstanceFiles() { - this.logger.verbose('cron to delete instance files started'); - setInterval(async () => { - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - const collections = await this.dbInstance.collections(); - collections.forEach(async (collection) => { - const name = collection.namespace.replace(/^[\w-]+./, ''); - await this.dbInstance.collection(name).deleteMany({ - $or: [{ _id: { $regex: /^app.state.*/ } }, { _id: { $regex: /^session-.*/ } }], - }); - this.logger.verbose('instance files deleted: ' + name); + private delInstanceFiles() { + this.logger.verbose('cron to delete instance files started'); + setInterval(async () => { + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + const collections = await this.dbInstance.collections(); + collections.forEach(async (collection) => { + const name = collection.namespace.replace(/^[\w-]+./, ''); + await this.dbInstance.collection(name).deleteMany({ + $or: [{ _id: { $regex: /^app.state.*/ } }, { _id: { $regex: /^session-.*/ } }], + }); + this.logger.verbose('instance files deleted: ' + name); + }); + // } else if (this.redis.ENABLED) { + } else { + const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); + for await (const dirent of dir) { + if (dirent.isDirectory()) { + const files = readdirSync(join(INSTANCE_DIR, dirent.name), { + encoding: 'utf-8', + }); + files.forEach(async (file) => { + if (file.match(/^app.state.*/) || file.match(/^session-.*/)) { + rmSync(join(INSTANCE_DIR, dirent.name, file), { + recursive: true, + force: true, }); - // } else if (this.redis.ENABLED) { - } else { - const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); - for await (const dirent of dir) { - if (dirent.isDirectory()) { - const files = readdirSync(join(INSTANCE_DIR, dirent.name), { - encoding: 'utf-8', - }); - files.forEach(async (file) => { - if (file.match(/^app.state.*/) || file.match(/^session-.*/)) { - rmSync(join(INSTANCE_DIR, dirent.name, file), { - recursive: true, - force: true, - }); - } - }); - this.logger.verbose('instance files deleted: ' + dirent.name); - } - } - } - }, 3600 * 1000 * 2); + } + }); + this.logger.verbose('instance files deleted: ' + dirent.name); + } + } + } + }, 3600 * 1000 * 2); + } + + public async cleaningUp(instanceName: string) { + this.logger.verbose('cleaning up instance: ' + instanceName); + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + this.logger.verbose('cleaning up instance in database: ' + instanceName); + await this.repository.dbServer.connect(); + const collections: any[] = await this.dbInstance.collections(); + if (collections.length > 0) { + await this.dbInstance.dropCollection(instanceName); + } + return; } - public async cleaningUp(instanceName: string) { - this.logger.verbose('cleaning up instance: ' + instanceName); - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - this.logger.verbose('cleaning up instance in database: ' + instanceName); - await this.repository.dbServer.connect(); - const collections: any[] = await this.dbInstance.collections(); - if (collections.length > 0) { - await this.dbInstance.dropCollection(instanceName); - } - return; - } - - if (this.redis.ENABLED) { - this.logger.verbose('cleaning up instance in redis: ' + instanceName); - this.cache.reference = instanceName; - await this.cache.delAll(); - return; - } - - this.logger.verbose('cleaning up instance in files: ' + instanceName); - rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + if (this.redis.ENABLED) { + this.logger.verbose('cleaning up instance in redis: ' + instanceName); + this.cache.reference = instanceName; + await this.cache.delAll(); + return; } - public async cleaningStoreFiles(instanceName: string) { - if (!this.db.ENABLED) { - this.logger.verbose('cleaning store files instance: ' + instanceName); - rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + this.logger.verbose('cleaning up instance in files: ' + instanceName); + rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); + } - execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'contacts', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'message-up', instanceName)}`); - execSync(`rm -rf ${join(STORE_DIR, 'messages', instanceName)}`); + public async cleaningStoreFiles(instanceName: string) { + if (!this.db.ENABLED) { + this.logger.verbose('cleaning store files instance: ' + instanceName); + rmSync(join(INSTANCE_DIR, instanceName), { recursive: true, force: true }); - execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); - execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); - execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); - execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); + execSync(`rm -rf ${join(STORE_DIR, 'chats', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'contacts', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'message-up', instanceName)}`); + execSync(`rm -rf ${join(STORE_DIR, 'messages', instanceName)}`); - return; + execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); + execSync(`rm -rf ${join(STORE_DIR, 'settings', instanceName + '*')}`); + + return; + } + + this.logger.verbose('cleaning store database instance: ' + instanceName); + + await AuthModel.deleteMany({ owner: instanceName }); + await ContactModel.deleteMany({ owner: instanceName }); + await MessageModel.deleteMany({ owner: instanceName }); + await MessageUpModel.deleteMany({ owner: instanceName }); + await AuthModel.deleteMany({ _id: instanceName }); + await WebhookModel.deleteMany({ _id: instanceName }); + await ChatwootModel.deleteMany({ _id: instanceName }); + await SettingsModel.deleteMany({ _id: instanceName }); + + return; + } + + public async loadInstance() { + this.logger.verbose('load instances'); + const set = async (name: string) => { + const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); + instance.instanceName = name; + this.logger.verbose('instance loaded: ' + name); + + await instance.connectToWhatsapp(); + this.logger.verbose('connectToWhatsapp: ' + name); + + this.waInstances[name] = instance; + }; + + try { + if (this.redis.ENABLED) { + this.logger.verbose('redis enabled'); + await this.cache.connect(this.redis as Redis); + const keys = await this.cache.instanceKeys(); + if (keys?.length > 0) { + this.logger.verbose('reading instance keys and setting instances'); + keys.forEach(async (k) => await set(k.split(':')[1])); + } else { + this.logger.verbose('no instance keys found'); } - - this.logger.verbose('cleaning store database instance: ' + instanceName); - - await AuthModel.deleteMany({ owner: instanceName }); - await ContactModel.deleteMany({ owner: instanceName }); - await MessageModel.deleteMany({ owner: instanceName }); - await MessageUpModel.deleteMany({ owner: instanceName }); - await AuthModel.deleteMany({ _id: instanceName }); - await WebhookModel.deleteMany({ _id: instanceName }); - await ChatwootModel.deleteMany({ _id: instanceName }); - await SettingsModel.deleteMany({ _id: instanceName }); - return; - } + } - public async loadInstance() { - this.logger.verbose('load instances'); - const set = async (name: string) => { - const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); - instance.instanceName = name; - this.logger.verbose('instance loaded: ' + name); - - await instance.connectToWhatsapp(); - this.logger.verbose('connectToWhatsapp: ' + name); - - this.waInstances[name] = instance; - }; - - try { - if (this.redis.ENABLED) { - this.logger.verbose('redis enabled'); - await this.cache.connect(this.redis as Redis); - const keys = await this.cache.instanceKeys(); - if (keys?.length > 0) { - this.logger.verbose('reading instance keys and setting instances'); - keys.forEach(async (k) => await set(k.split(':')[1])); - } else { - this.logger.verbose('no instance keys found'); - } - return; - } - - if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { - this.logger.verbose('database enabled'); - await this.repository.dbServer.connect(); - const collections: any[] = await this.dbInstance.collections(); - if (collections.length > 0) { - this.logger.verbose('reading collections and setting instances'); - collections.forEach(async (coll) => await set(coll.namespace.replace(/^[\w-]+\./, ''))); - } else { - this.logger.verbose('no collections found'); - } - return; - } - - this.logger.verbose('store in files enabled'); - const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); - for await (const dirent of dir) { - if (dirent.isDirectory()) { - this.logger.verbose('reading instance files and setting instances'); - const files = readdirSync(join(INSTANCE_DIR, dirent.name), { - encoding: 'utf-8', - }); - if (files.length === 0) { - rmSync(join(INSTANCE_DIR, dirent.name), { recursive: true, force: true }); - break; - } - - await set(dirent.name); - } else { - this.logger.verbose('no instance files found'); - } - } - } catch (error) { - this.logger.error(error); + if (this.db.ENABLED && this.db.SAVE_DATA.INSTANCE) { + this.logger.verbose('database enabled'); + await this.repository.dbServer.connect(); + const collections: any[] = await this.dbInstance.collections(); + if (collections.length > 0) { + this.logger.verbose('reading collections and setting instances'); + collections.forEach(async (coll) => await set(coll.namespace.replace(/^[\w-]+\./, ''))); + } else { + this.logger.verbose('no collections found'); } + return; + } + + this.logger.verbose('store in files enabled'); + const dir = opendirSync(INSTANCE_DIR, { encoding: 'utf-8' }); + for await (const dirent of dir) { + if (dirent.isDirectory()) { + this.logger.verbose('reading instance files and setting instances'); + const files = readdirSync(join(INSTANCE_DIR, dirent.name), { + encoding: 'utf-8', + }); + if (files.length === 0) { + rmSync(join(INSTANCE_DIR, dirent.name), { recursive: true, force: true }); + break; + } + + await set(dirent.name); + } else { + this.logger.verbose('no instance files found'); + } + } + } catch (error) { + this.logger.error(error); } + } - private removeInstance() { - this.eventEmitter.on('remove.instance', async (instanceName: string) => { - this.logger.verbose('remove instance: ' + instanceName); - try { - this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); - this.waInstances[instanceName] = undefined; - } catch (error) { - this.logger.error(error); - } + private removeInstance() { + this.eventEmitter.on('remove.instance', async (instanceName: string) => { + this.logger.verbose('remove instance: ' + instanceName); + try { + this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); + this.waInstances[instanceName] = undefined; + } catch (error) { + this.logger.error(error); + } - try { - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - this.cleaningStoreFiles(instanceName); - } finally { - this.logger.warn(`Instance "${instanceName}" - REMOVED`); - } + try { + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + this.cleaningStoreFiles(instanceName); + } finally { + this.logger.warn(`Instance "${instanceName}" - REMOVED`); + } + }); + this.eventEmitter.on('logout.instance', async (instanceName: string) => { + this.logger.verbose('logout instance: ' + instanceName); + try { + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + } finally { + this.logger.warn(`Instance "${instanceName}" - LOGOUT`); + } + }); + } + + private noConnection() { + this.logger.verbose('checking instances without connection'); + this.eventEmitter.on('no.connection', async (instanceName) => { + try { + this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); + this.waInstances[instanceName] = undefined; + + this.logger.verbose('request cleaning up instance: ' + instanceName); + this.cleaningUp(instanceName); + } catch (error) { + this.logger.error({ + localError: 'noConnection', + warn: 'Error deleting instance from memory.', + error, }); - this.eventEmitter.on('logout.instance', async (instanceName: string) => { - this.logger.verbose('logout instance: ' + instanceName); - try { - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - } finally { - this.logger.warn(`Instance "${instanceName}" - LOGOUT`); - } - }); - } - - private noConnection() { - this.logger.verbose('checking instances without connection'); - this.eventEmitter.on('no.connection', async (instanceName) => { - try { - this.logger.verbose('instance: ' + instanceName + ' - removing from memory'); - this.waInstances[instanceName] = undefined; - - this.logger.verbose('request cleaning up instance: ' + instanceName); - this.cleaningUp(instanceName); - } catch (error) { - this.logger.error({ - localError: 'noConnection', - warn: 'Error deleting instance from memory.', - error, - }); - } finally { - this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`); - } - }); - } + } finally { + this.logger.warn(`Instance "${instanceName}" - NOT CONNECTION`); + } + }); + } } diff --git a/src/whatsapp/services/settings.service.ts b/src/whatsapp/services/settings.service.ts index bbd5076f..6815ca40 100644 --- a/src/whatsapp/services/settings.service.ts +++ b/src/whatsapp/services/settings.service.ts @@ -4,29 +4,29 @@ import { SettingsDto } from '../dto/settings.dto'; import { WAMonitoringService } from './monitor.service'; export class SettingsService { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - private readonly logger = new Logger(SettingsService.name); + private readonly logger = new Logger(SettingsService.name); - public create(instance: InstanceDto, data: SettingsDto) { - this.logger.verbose('create settings: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setSettings(data); + public create(instance: InstanceDto, data: SettingsDto) { + this.logger.verbose('create settings: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setSettings(data); - return { settings: { ...instance, settings: data } }; - } - - public async find(instance: InstanceDto): Promise { - try { - this.logger.verbose('find settings: ' + instance.instanceName); - const result = await this.waMonitor.waInstances[instance.instanceName].findSettings(); - - if (Object.keys(result).length === 0) { - throw new Error('Settings not found'); - } - - return result; - } catch (error) { - return { reject_call: false, msg_call: '', groups_ignore: false }; - } + return { settings: { ...instance, settings: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find settings: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findSettings(); + + if (Object.keys(result).length === 0) { + throw new Error('Settings not found'); + } + + return result; + } catch (error) { + return { reject_call: false, msg_call: '', groups_ignore: false }; } + } } diff --git a/src/whatsapp/services/webhook.service.ts b/src/whatsapp/services/webhook.service.ts index 36233f1f..dd0a88cd 100644 --- a/src/whatsapp/services/webhook.service.ts +++ b/src/whatsapp/services/webhook.service.ts @@ -4,29 +4,29 @@ import { WebhookDto } from '../dto/webhook.dto'; import { WAMonitoringService } from './monitor.service'; export class WebhookService { - constructor(private readonly waMonitor: WAMonitoringService) {} + constructor(private readonly waMonitor: WAMonitoringService) {} - private readonly logger = new Logger(WebhookService.name); + private readonly logger = new Logger(WebhookService.name); - public create(instance: InstanceDto, data: WebhookDto) { - this.logger.verbose('create webhook: ' + instance.instanceName); - this.waMonitor.waInstances[instance.instanceName].setWebhook(data); + public create(instance: InstanceDto, data: WebhookDto) { + this.logger.verbose('create webhook: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setWebhook(data); - return { webhook: { ...instance, webhook: data } }; - } - - public async find(instance: InstanceDto): Promise { - try { - this.logger.verbose('find webhook: ' + instance.instanceName); - const result = await this.waMonitor.waInstances[instance.instanceName].findWebhook(); - - if (Object.keys(result).length === 0) { - throw new Error('Webhook not found'); - } - - return result; - } catch (error) { - return { enabled: false, url: '', events: [], webhook_by_events: false }; - } + return { webhook: { ...instance, webhook: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find webhook: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findWebhook(); + + if (Object.keys(result).length === 0) { + throw new Error('Webhook not found'); + } + + return result; + } catch (error) { + return { enabled: false, url: '', events: [], webhook_by_events: false }; } + } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 1678822f..59245103 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1,37 +1,37 @@ import ffmpegPath from '@ffmpeg-installer/ffmpeg'; import { Boom } from '@hapi/boom'; import makeWASocket, { - AnyMessageContent, - BufferedEventData, - BufferJSON, - CacheStore, - Chat, - ConnectionState, - Contact, - delay, - DisconnectReason, - downloadMediaMessage, - fetchLatestBaileysVersion, - generateWAMessageFromContent, - getAggregateVotesInPollMessage, - getContentType, - getDevice, - GroupMetadata, - isJidGroup, - isJidUser, - makeCacheableSignalKeyStore, - MessageUpsertType, - MiscMessageGenerationOptions, - ParticipantAction, - prepareWAMessageMedia, - proto, - useMultiFileAuthState, - UserFacingSocketConfig, - WABrowserDescription, - WAMediaUpload, - WAMessage, - WAMessageUpdate, - WASocket, + AnyMessageContent, + BufferedEventData, + BufferJSON, + CacheStore, + Chat, + ConnectionState, + Contact, + delay, + DisconnectReason, + downloadMediaMessage, + fetchLatestBaileysVersion, + generateWAMessageFromContent, + getAggregateVotesInPollMessage, + getContentType, + getDevice, + GroupMetadata, + isJidGroup, + isJidUser, + makeCacheableSignalKeyStore, + MessageUpsertType, + MiscMessageGenerationOptions, + ParticipantAction, + prepareWAMessageMedia, + proto, + useMultiFileAuthState, + UserFacingSocketConfig, + WABrowserDescription, + WAMediaUpload, + WAMessage, + WAMessageUpdate, + WASocket, } from '@whiskeysockets/baileys'; import axios from 'axios'; import { exec, execSync } from 'child_process'; @@ -50,16 +50,16 @@ import sharp from 'sharp'; import { v4 } from 'uuid'; import { - Auth, - CleanStoreConf, - ConfigService, - ConfigSessionPhone, - Database, - HttpServer, - Log, - QrCode, - Redis, - Webhook, + Auth, + CleanStoreConf, + ConfigService, + ConfigSessionPhone, + Database, + HttpServer, + Log, + QrCode, + Redis, + Webhook, } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { INSTANCE_DIR, ROOT_DIR } from '../../config/path.config'; @@ -69,44 +69,44 @@ import { BadRequestException, InternalServerErrorException, NotFoundException } import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; import { - ArchiveChatDto, - DeleteMessage, - getBase64FromMediaMessageDto, - NumberBusiness, - OnWhatsAppDto, - PrivacySettingDto, - ReadMessageDto, - WhatsAppNumberDto, + ArchiveChatDto, + DeleteMessage, + getBase64FromMediaMessageDto, + NumberBusiness, + OnWhatsAppDto, + PrivacySettingDto, + ReadMessageDto, + WhatsAppNumberDto, } from '../dto/chat.dto'; import { - CreateGroupDto, - GetParticipant, - GroupDescriptionDto, - GroupInvite, - GroupJid, - GroupPictureDto, - GroupSendInvite, - GroupSubjectDto, - GroupToggleEphemeralDto, - GroupUpdateParticipantDto, - GroupUpdateSettingDto, + CreateGroupDto, + GetParticipant, + GroupDescriptionDto, + GroupInvite, + GroupJid, + GroupPictureDto, + GroupSendInvite, + GroupSubjectDto, + GroupToggleEphemeralDto, + GroupUpdateParticipantDto, + GroupUpdateSettingDto, } from '../dto/group.dto'; import { - ContactMessage, - MediaMessage, - Options, - SendAudioDto, - SendButtonDto, - SendContactDto, - SendListDto, - SendLocationDto, - SendMediaDto, - SendPollDto, - SendReactionDto, - SendStatusDto, - SendStickerDto, - SendTextDto, - StatusMessage, + ContactMessage, + MediaMessage, + Options, + SendAudioDto, + SendButtonDto, + SendContactDto, + SendListDto, + SendLocationDto, + SendMediaDto, + SendPollDto, + SendReactionDto, + SendStatusDto, + SendStickerDto, + SendTextDto, + StatusMessage, } from '../dto/sendMessage.dto'; import { SettingsRaw } from '../models'; import { ChatRaw } from '../models/chat.model'; @@ -123,2804 +123,2791 @@ import { waMonitor } from '../whatsapp.module'; import { ChatwootService } from './chatwoot.service'; export class WAStartupService { - constructor( - private readonly configService: ConfigService, - private readonly eventEmitter: EventEmitter2, - private readonly repository: RepositoryBroker, - private readonly cache: RedisCache, - ) { - this.logger.verbose('WAStartupService initialized'); - this.cleanStore(); - this.instance.qrcode = { count: 0 }; + constructor( + private readonly configService: ConfigService, + private readonly eventEmitter: EventEmitter2, + private readonly repository: RepositoryBroker, + private readonly cache: RedisCache, + ) { + this.logger.verbose('WAStartupService initialized'); + this.cleanStore(); + this.instance.qrcode = { count: 0 }; + } + + private readonly logger = new Logger(WAStartupService.name); + private readonly instance: wa.Instance = {}; + public client: WASocket; + private readonly localWebhook: wa.LocalWebHook = {}; + private readonly localChatwoot: wa.LocalChatwoot = {}; + private readonly localSettings: wa.LocalSettings = {}; + private stateConnection: wa.StateConnection = { state: 'close' }; + public readonly storePath = join(ROOT_DIR, 'store'); + private readonly msgRetryCounterCache: CacheStore = new NodeCache(); + private readonly userDevicesCache: CacheStore = new NodeCache(); + private endSession = false; + private logBaileys = this.configService.get('LOG').BAILEYS; + + private phoneNumber: string; + + private chatwootService = new ChatwootService(waMonitor, this.configService); + + public set instanceName(name: string) { + this.logger.verbose(`Initializing instance '${name}'`); + if (!name) { + this.logger.verbose('Instance name not found, generating random name with uuid'); + this.instance.name = v4(); + return; + } + this.instance.name = name; + this.logger.verbose(`Instance '${this.instance.name}' initialized`); + this.logger.verbose('Sending instance status to webhook'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'created', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'created', + }, + ); + } + } + + public get instanceName() { + this.logger.verbose('Getting instance name'); + return this.instance.name; + } + + public get wuid() { + this.logger.verbose('Getting remoteJid of instance'); + return this.instance.wuid; + } + + public async getProfileName() { + this.logger.verbose('Getting profile name'); + let profileName = this.client.user?.name ?? this.client.user?.verifiedName; + if (!profileName) { + this.logger.verbose('Profile name not found, trying to get from database'); + if (this.configService.get('DATABASE').ENABLED) { + this.logger.verbose('Database enabled, trying to get from database'); + const collection = dbserver + .getClient() + .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') + .collection(this.instanceName); + const data = await collection.findOne({ _id: 'creds' }); + if (data) { + this.logger.verbose('Profile name found in database'); + const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); + profileName = creds.me?.name || creds.me?.verifiedName; + } + } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { + this.logger.verbose('Profile name found in file'); + const creds = JSON.parse( + readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { + encoding: 'utf-8', + }), + ); + profileName = creds.me?.name || creds.me?.verifiedName; + } } - private readonly logger = new Logger(WAStartupService.name); - private readonly instance: wa.Instance = {}; - public client: WASocket; - private readonly localWebhook: wa.LocalWebHook = {}; - private readonly localChatwoot: wa.LocalChatwoot = {}; - private readonly localSettings: wa.LocalSettings = {}; - private stateConnection: wa.StateConnection = { state: 'close' }; - public readonly storePath = join(ROOT_DIR, 'store'); - private readonly msgRetryCounterCache: CacheStore = new NodeCache(); - private readonly userDevicesCache: CacheStore = new NodeCache(); - private endSession = false; - private logBaileys = this.configService.get('LOG').BAILEYS; + this.logger.verbose(`Profile name: ${profileName}`); + return profileName; + } - private phoneNumber: string; + public async getProfileStatus() { + this.logger.verbose('Getting profile status'); + const status = await this.client.fetchStatus(this.instance.wuid); - private chatwootService = new ChatwootService(waMonitor, this.configService); + this.logger.verbose(`Profile status: ${status.status}`); + return status.status; + } - public set instanceName(name: string) { - this.logger.verbose(`Initializing instance '${name}'`); - if (!name) { - this.logger.verbose('Instance name not found, generating random name with uuid'); - this.instance.name = v4(); - return; + public get profilePictureUrl() { + this.logger.verbose('Getting profile picture url'); + return this.instance.profilePictureUrl; + } + + public get qrCode(): wa.QrCode { + this.logger.verbose('Getting qrcode'); + + return { + pairingCode: this.instance.qrcode?.pairingCode, + code: this.instance.qrcode?.code, + base64: this.instance.qrcode?.base64, + }; + } + + private async loadWebhook() { + this.logger.verbose('Loading webhook'); + const data = await this.repository.webhook.find(this.instanceName); + this.localWebhook.url = data?.url; + this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); + + this.localWebhook.enabled = data?.enabled; + this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); + + this.localWebhook.events = data?.events; + this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); + + this.localWebhook.webhook_by_events = data?.webhook_by_events; + this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); + + this.logger.verbose('Webhook loaded'); + } + + public async setWebhook(data: WebhookRaw) { + this.logger.verbose('Setting webhook'); + await this.repository.webhook.create(data, this.instanceName); + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + Object.assign(this.localWebhook, data); + this.logger.verbose('Webhook set'); + } + + public async findWebhook() { + this.logger.verbose('Finding webhook'); + const data = await this.repository.webhook.find(this.instanceName); + + if (!data) { + this.logger.verbose('Webhook not found'); + throw new NotFoundException('Webhook not found'); + } + + this.logger.verbose(`Webhook url: ${data.url}`); + this.logger.verbose(`Webhook events: ${data.events}`); + return data; + } + + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.localChatwoot.name_inbox = data?.name_inbox; + this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); + + this.localChatwoot.sign_msg = data?.sign_msg; + this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + + this.localChatwoot.number = data?.number; + this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); + + this.localChatwoot.reopen_conversation = data?.reopen_conversation; + this.logger.verbose(`Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`); + + this.localChatwoot.conversation_pending = data?.conversation_pending; + this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); + + this.logger.verbose('Chatwoot loaded'); + } + + public async setChatwoot(data: ChatwootRaw) { + this.logger.verbose('Setting chatwoot'); + await this.repository.chatwoot.create(data, this.instanceName); + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + Object.assign(this.localChatwoot, data); + this.logger.verbose('Chatwoot set'); + } + + public async findChatwoot() { + this.logger.verbose('Finding chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + + if (!data) { + this.logger.verbose('Chatwoot not found'); + return null; + } + + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); + this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + + return data; + } + + private async loadSettings() { + this.logger.verbose('Loading settings'); + const data = await this.repository.settings.find(this.instanceName); + this.localSettings.reject_call = data?.reject_call; + this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); + + this.localSettings.msg_call = data?.msg_call; + this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); + + this.localSettings.groups_ignore = data?.groups_ignore; + this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); + + this.localSettings.always_online = data?.always_online; + this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); + + this.localSettings.read_messages = data?.read_messages; + this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); + + this.localSettings.read_status = data?.read_status; + this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); + + this.logger.verbose('Settings loaded'); + } + + public async setSettings(data: SettingsRaw) { + this.logger.verbose('Setting settings'); + await this.repository.settings.create(data, this.instanceName); + this.logger.verbose(`Settings reject_call: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + Object.assign(this.localSettings, data); + this.logger.verbose('Settings set'); + + this.client?.ws?.close(); + } + + public async findSettings() { + this.logger.verbose('Finding settings'); + const data = await this.repository.settings.find(this.instanceName); + + if (!data) { + this.logger.verbose('Settings not found'); + return null; + } + + this.logger.verbose(`Settings url: ${data.reject_call}`); + this.logger.verbose(`Settings msg_call: ${data.msg_call}`); + this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); + this.logger.verbose(`Settings always_online: ${data.always_online}`); + this.logger.verbose(`Settings read_messages: ${data.read_messages}`); + this.logger.verbose(`Settings read_status: ${data.read_status}`); + return data; + } + + public async sendDataWebhook(event: Events, data: T, local = true) { + const webhookGlobal = this.configService.get('WEBHOOK'); + const webhookLocal = this.localWebhook.events; + const serverUrl = this.configService.get('SERVER').URL; + const we = event.replace(/[.-]/gm, '_').toUpperCase(); + const transformedWe = we.replace(/_/gm, '-').toLowerCase(); + + const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; + const tokenStore = await this.repository.auth.find(this.instanceName); + const instanceApikey = tokenStore?.apikey || 'Apikey not found'; + + const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; + + if (local) { + if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { + this.logger.verbose('Sending data to webhook local'); + let baseURL: string; + + if (this.localWebhook.webhook_by_events) { + baseURL = `${this.localWebhook.url}/${transformedWe}`; + } else { + baseURL = this.localWebhook.url; } - this.instance.name = name; - this.logger.verbose(`Instance '${this.instance.name}' initialized`); - this.logger.verbose('Sending instance status to webhook'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-local', + url: baseURL, + event, instance: this.instance.name, - status: 'created', + data, + destination: this.localWebhook.url, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + + try { + if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { + const httpService = axios.create({ baseURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: this.localWebhook.url, + server_url: serverUrl, + }; + + if (expose && instanceApikey) { + postData['apikey'] = instanceApikey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-local', + 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]) { + this.logger.verbose('Sending data to webhook global'); + const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; + + let globalURL; + + if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { + globalURL = `${globalWebhook.URL}/${transformedWe}`; + } else { + globalURL = globalWebhook.URL; + } + + const localUrl = this.localWebhook.url; + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: WAStartupService.name + '.sendDataWebhook-global', + url: globalURL, + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + logData['apikey'] = globalApiKey; + } + + this.logger.log(logData); + } + + try { + if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { + const httpService = axios.create({ baseURL: globalURL }); + const postData = { + event, + instance: this.instance.name, + data, + destination: localUrl, + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + postData['apikey'] = globalApiKey; + } + + await httpService.post('', postData); + } + } catch (error) { + this.logger.error({ + local: WAStartupService.name + '.sendDataWebhook-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, + }); + } + } + } + } + + private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { + this.logger.verbose('Connection update'); + if (qr) { + this.logger.verbose('QR code found'); + if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { + this.logger.verbose('QR code limit reached'); + + this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); + this.sendDataWebhook(Events.QRCODE_UPDATED, { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, }); if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'created', - }, - ); - } - } - - public get instanceName() { - this.logger.verbose('Getting instance name'); - return this.instance.name; - } - - public get wuid() { - this.logger.verbose('Getting remoteJid of instance'); - return this.instance.wuid; - } - - public async getProfileName() { - this.logger.verbose('Getting profile name'); - let profileName = this.client.user?.name ?? this.client.user?.verifiedName; - if (!profileName) { - this.logger.verbose('Profile name not found, trying to get from database'); - if (this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('Database enabled, trying to get from database'); - const collection = dbserver - .getClient() - .db(this.configService.get('DATABASE').CONNECTION.DB_PREFIX_NAME + '-instances') - .collection(this.instanceName); - const data = await collection.findOne({ _id: 'creds' }); - if (data) { - this.logger.verbose('Profile name found in database'); - const creds = JSON.parse(JSON.stringify(data), BufferJSON.reviver); - profileName = creds.me?.name || creds.me?.verifiedName; - } - } else if (existsSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'))) { - this.logger.verbose('Profile name found in file'); - const creds = JSON.parse( - readFileSync(join(INSTANCE_DIR, this.instanceName, 'creds.json'), { - encoding: 'utf-8', - }), - ); - profileName = creds.me?.name || creds.me?.verifiedName; - } + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); } - this.logger.verbose(`Profile name: ${profileName}`); - return profileName; - } + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + state: 'refused', + statusReason: DisconnectReason.connectionClosed, + }); - public async getProfileStatus() { - this.logger.verbose('Getting profile status'); - const status = await this.client.fetchStatus(this.instance.wuid); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); - this.logger.verbose(`Profile status: ${status.status}`); - return status.status; - } - - public get profilePictureUrl() { - this.logger.verbose('Getting profile picture url'); - return this.instance.profilePictureUrl; - } - - public get qrCode(): wa.QrCode { - this.logger.verbose('Getting qrcode'); - - return { - pairingCode: this.instance.qrcode?.pairingCode, - code: this.instance.qrcode?.code, - base64: this.instance.qrcode?.base64, - }; - } - - private async loadWebhook() { - this.logger.verbose('Loading webhook'); - const data = await this.repository.webhook.find(this.instanceName); - this.localWebhook.url = data?.url; - this.logger.verbose(`Webhook url: ${this.localWebhook.url}`); - - this.localWebhook.enabled = data?.enabled; - this.logger.verbose(`Webhook enabled: ${this.localWebhook.enabled}`); - - this.localWebhook.events = data?.events; - this.logger.verbose(`Webhook events: ${this.localWebhook.events}`); - - this.localWebhook.webhook_by_events = data?.webhook_by_events; - this.logger.verbose(`Webhook by events: ${this.localWebhook.webhook_by_events}`); - - this.logger.verbose('Webhook loaded'); - } - - public async setWebhook(data: WebhookRaw) { - this.logger.verbose('Setting webhook'); - await this.repository.webhook.create(data, this.instanceName); - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - Object.assign(this.localWebhook, data); - this.logger.verbose('Webhook set'); - } - - public async findWebhook() { - this.logger.verbose('Finding webhook'); - const data = await this.repository.webhook.find(this.instanceName); - - if (!data) { - this.logger.verbose('Webhook not found'); - throw new NotFoundException('Webhook not found'); + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); } - this.logger.verbose(`Webhook url: ${data.url}`); - this.logger.verbose(`Webhook events: ${data.events}`); - return data; - } + this.logger.verbose('endSession defined as true'); + this.endSession = true; - private async loadChatwoot() { - this.logger.verbose('Loading chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - this.localChatwoot.enabled = data?.enabled; - this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + this.logger.verbose('Emmiting event logout.instance'); + return this.eventEmitter.emit('no.connection', this.instance.name); + } - this.localChatwoot.account_id = data?.account_id; - this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + this.logger.verbose('Incrementing QR code count'); + this.instance.qrcode.count++; - this.localChatwoot.token = data?.token; - this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + const optsQrcode: QRCodeToDataURLOptions = { + margin: 3, + scale: 4, + errorCorrectionLevel: 'H', + color: { light: '#ffffff', dark: '#198754' }, + }; - this.localChatwoot.url = data?.url; - this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + if (this.phoneNumber) { + await delay(2000); + this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); + } else { + this.instance.qrcode.pairingCode = null; + } - this.localChatwoot.name_inbox = data?.name_inbox; - this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); - - this.localChatwoot.sign_msg = data?.sign_msg; - this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); - - this.localChatwoot.number = data?.number; - this.logger.verbose(`Chatwoot number: ${this.localChatwoot.number}`); - - this.localChatwoot.reopen_conversation = data?.reopen_conversation; - this.logger.verbose(`Chatwoot reopen conversation: ${this.localChatwoot.reopen_conversation}`); - - this.localChatwoot.conversation_pending = data?.conversation_pending; - this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); - - this.logger.verbose('Chatwoot loaded'); - } - - public async setChatwoot(data: ChatwootRaw) { - this.logger.verbose('Setting chatwoot'); - await this.repository.chatwoot.create(data, this.instanceName); - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); - this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); - - Object.assign(this.localChatwoot, data); - this.logger.verbose('Chatwoot set'); - } - - public async findChatwoot() { - this.logger.verbose('Finding chatwoot'); - const data = await this.repository.chatwoot.find(this.instanceName); - - if (!data) { - this.logger.verbose('Chatwoot not found'); - return null; + this.logger.verbose('Generating QR code'); + qrcode.toDataURL(qr, optsQrcode, (error, base64) => { + if (error) { + this.logger.error('Qrcode generate failed:' + error.toString()); + return; } - this.logger.verbose(`Chatwoot account id: ${data.account_id}`); - this.logger.verbose(`Chatwoot token: ${data.token}`); - this.logger.verbose(`Chatwoot url: ${data.url}`); - this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); - this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); - this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); - this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + this.instance.qrcode.base64 = base64; + this.instance.qrcode.code = qr; - return data; - } + this.sendDataWebhook(Events.QRCODE_UPDATED, { + qrcode: { + instance: this.instance.name, + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }); - private async loadSettings() { - this.logger.verbose('Loading settings'); - const data = await this.repository.settings.find(this.instanceName); - this.localSettings.reject_call = data?.reject_call; - this.logger.verbose(`Settings reject_call: ${this.localSettings.reject_call}`); - - this.localSettings.msg_call = data?.msg_call; - this.logger.verbose(`Settings msg_call: ${this.localSettings.msg_call}`); - - this.localSettings.groups_ignore = data?.groups_ignore; - this.logger.verbose(`Settings groups_ignore: ${this.localSettings.groups_ignore}`); - - this.localSettings.always_online = data?.always_online; - this.logger.verbose(`Settings always_online: ${this.localSettings.always_online}`); - - this.localSettings.read_messages = data?.read_messages; - this.logger.verbose(`Settings read_messages: ${this.localSettings.read_messages}`); - - this.localSettings.read_status = data?.read_status; - this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`); - - this.logger.verbose('Settings loaded'); - } - - public async setSettings(data: SettingsRaw) { - this.logger.verbose('Setting settings'); - await this.repository.settings.create(data, this.instanceName); - this.logger.verbose(`Settings reject_call: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - this.logger.verbose(`Settings always_online: ${data.always_online}`); - this.logger.verbose(`Settings read_messages: ${data.read_messages}`); - this.logger.verbose(`Settings read_status: ${data.read_status}`); - Object.assign(this.localSettings, data); - this.logger.verbose('Settings set'); - - this.client?.ws?.close(); - } - - public async findSettings() { - this.logger.verbose('Finding settings'); - const data = await this.repository.settings.find(this.instanceName); - - if (!data) { - this.logger.verbose('Settings not found'); - return null; - } - - this.logger.verbose(`Settings url: ${data.reject_call}`); - this.logger.verbose(`Settings msg_call: ${data.msg_call}`); - this.logger.verbose(`Settings groups_ignore: ${data.groups_ignore}`); - this.logger.verbose(`Settings always_online: ${data.always_online}`); - this.logger.verbose(`Settings read_messages: ${data.read_messages}`); - this.logger.verbose(`Settings read_status: ${data.read_status}`); - return data; - } - - public async sendDataWebhook(event: Events, data: T, local = true) { - const webhookGlobal = this.configService.get('WEBHOOK'); - const webhookLocal = this.localWebhook.events; - const serverUrl = this.configService.get('SERVER').URL; - const we = event.replace(/[.-]/gm, '_').toUpperCase(); - const transformedWe = we.replace(/_/gm, '-').toLowerCase(); - - const expose = this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES; - const tokenStore = await this.repository.auth.find(this.instanceName); - const instanceApikey = tokenStore?.apikey || 'Apikey not found'; - - const globalApiKey = this.configService.get('AUTHENTICATION').API_KEY.KEY; - - if (local) { - if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { - this.logger.verbose('Sending data to webhook local'); - let baseURL: string; - - if (this.localWebhook.webhook_by_events) { - baseURL = `${this.localWebhook.url}/${transformedWe}`; - } else { - baseURL = this.localWebhook.url; - } - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-local', - url: baseURL, - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - apikey: (expose && instanceApikey) || null, - }; - - if (expose && instanceApikey) { - logData['apikey'] = instanceApikey; - } - - this.logger.log(logData); - } - - try { - if (this.localWebhook.enabled && isURL(this.localWebhook.url)) { - const httpService = axios.create({ baseURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: this.localWebhook.url, - server_url: serverUrl, - }; - - if (expose && instanceApikey) { - postData['apikey'] = instanceApikey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-local', - 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]) { - this.logger.verbose('Sending data to webhook global'); - const globalWebhook = this.configService.get('WEBHOOK').GLOBAL; - - let globalURL; - - if (webhookGlobal.GLOBAL.WEBHOOK_BY_EVENTS) { - globalURL = `${globalWebhook.URL}/${transformedWe}`; - } else { - globalURL = globalWebhook.URL; - } - - const localUrl = this.localWebhook.url; - - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: WAStartupService.name + '.sendDataWebhook-global', - url: globalURL, - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - logData['apikey'] = globalApiKey; - } - - this.logger.log(logData); - } - - try { - if (globalWebhook && globalWebhook?.ENABLED && isURL(globalURL)) { - const httpService = axios.create({ baseURL: globalURL }); - const postData = { - event, - instance: this.instance.name, - data, - destination: localUrl, - server_url: serverUrl, - }; - - if (expose && globalApiKey) { - postData['apikey'] = globalApiKey; - } - - await httpService.post('', postData); - } - } catch (error) { - this.logger.error({ - local: WAStartupService.name + '.sendDataWebhook-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, - }); - } - } - } - } - - private async connectionUpdate({ qr, connection, lastDisconnect }: Partial) { - this.logger.verbose('Connection update'); - if (qr) { - this.logger.verbose('QR code found'); - if (this.instance.qrcode.count === this.configService.get('QRCODE').LIMIT) { - this.logger.verbose('QR code limit reached'); - - this.logger.verbose('Sending data to webhook in event QRCODE_UPDATED'); - this.sendDataWebhook(Events.QRCODE_UPDATED, { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - message: 'QR code limit reached, please login again', - statusCode: DisconnectReason.badSession, - }, - ); - } - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { - instance: this.instance.name, - state: 'refused', - statusReason: DisconnectReason.connectionClosed, - }); - - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('endSession defined as true'); - this.endSession = true; - - this.logger.verbose('Emmiting event logout.instance'); - return this.eventEmitter.emit('no.connection', this.instance.name); - } - - this.logger.verbose('Incrementing QR code count'); - this.instance.qrcode.count++; - - const optsQrcode: QRCodeToDataURLOptions = { - margin: 3, - scale: 4, - errorCorrectionLevel: 'H', - color: { light: '#ffffff', dark: '#198754' }, - }; - - if (this.phoneNumber) { - await delay(2000); - this.instance.qrcode.pairingCode = await this.client.requestPairingCode(this.phoneNumber); - } else { - this.instance.qrcode.pairingCode = null; - } - - this.logger.verbose('Generating QR code'); - qrcode.toDataURL(qr, optsQrcode, (error, base64) => { - if (error) { - this.logger.error('Qrcode generate failed:' + error.toString()); - return; - } - - this.instance.qrcode.base64 = base64; - this.instance.qrcode.code = qr; - - this.sendDataWebhook(Events.QRCODE_UPDATED, { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }); - - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.QRCODE_UPDATED, - { instanceName: this.instance.name }, - { - qrcode: { - instance: this.instance.name, - pairingCode: this.instance.qrcode.pairingCode, - code: qr, - base64, - }, - }, - ); - } - }); - - this.logger.verbose('Generating QR code in terminal'); - qrcodeTerminal.generate(qr, { small: true }, (qrcode) => - this.logger.log( - `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, - ), - ); - } - - if (connection) { - this.logger.verbose('Connection found'); - this.stateConnection = { - state: connection, - statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, - }; - - this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); - this.sendDataWebhook(Events.CONNECTION_UPDATE, { + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { instance: this.instance.name, - ...this.stateConnection, - }); + pairingCode: this.instance.qrcode.pairingCode, + code: qr, + base64, + }, + }, + ); + } + }); + + this.logger.verbose('Generating QR code in terminal'); + qrcodeTerminal.generate(qr, { small: true }, (qrcode) => + this.logger.log( + `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + + qrcode, + ), + ); + } + + if (connection) { + this.logger.verbose('Connection found'); + this.stateConnection = { + state: connection, + statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200, + }; + + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); + this.sendDataWebhook(Events.CONNECTION_UPDATE, { + instance: this.instance.name, + ...this.stateConnection, + }); + } + + if (connection === 'close') { + this.logger.verbose('Connection closed'); + const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; + if (shouldReconnect) { + this.logger.verbose('Reconnecting to whatsapp'); + await this.connectToWhatsapp(); + } else { + this.logger.verbose('Do not reconnect to whatsapp'); + this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); + this.sendDataWebhook(Events.STATUS_INSTANCE, { + instance: this.instance.name, + status: 'removed', + }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); } - if (connection === 'close') { - this.logger.verbose('Connection closed'); - const shouldReconnect = (lastDisconnect.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut; - if (shouldReconnect) { - this.logger.verbose('Reconnecting to whatsapp'); - await this.connectToWhatsapp(); - } else { - this.logger.verbose('Do not reconnect to whatsapp'); - this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); - this.sendDataWebhook(Events.STATUS_INSTANCE, { - instance: this.instance.name, - status: 'removed', - }); + this.logger.verbose('Emittin event logout.instance'); + this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); + this.client?.ws?.close(); + this.client.end(new Error('Close connection')); + this.logger.verbose('Connection closed'); + } + } - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.STATUS_INSTANCE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'removed', - }, - ); - } - - this.logger.verbose('Emittin event logout.instance'); - this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); - this.client?.ws?.close(); - this.client.end(new Error('Close connection')); - this.logger.verbose('Connection closed'); - } - } - - if (connection === 'open') { - this.logger.verbose('Connection opened'); - this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); - this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; - this.logger.info( - ` + if (connection === 'open') { + this.logger.verbose('Connection opened'); + this.instance.wuid = this.client.user.id.replace(/:\d+/, ''); + this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl; + this.logger.info( + ` ┌──────────────────────────────┐ │ CONNECTED TO WHATSAPP │ └──────────────────────────────┘`.replace(/^ +/gm, ' '), - ); + ); - if (this.localChatwoot.enabled) { - this.chatwootService.eventWhatsapp( - Events.CONNECTION_UPDATE, - { instanceName: this.instance.name }, - { - instance: this.instance.name, - status: 'open', - }, - ); - } - } + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.CONNECTION_UPDATE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'open', + }, + ); + } } + } - private async getMessage(key: proto.IMessageKey, full = false) { - this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + private async getMessage(key: proto.IMessageKey, full = false) { + this.logger.verbose('Getting message with key: ' + JSON.stringify(key)); + try { + const webMessageInfo = (await this.repository.message.find({ + where: { owner: this.instance.name, key: { id: key.id } }, + })) as unknown as proto.IWebMessageInfo[]; + if (full) { + this.logger.verbose('Returning full message'); + return webMessageInfo[0]; + } + if (webMessageInfo[0].message?.pollCreationMessage) { + this.logger.verbose('Returning poll message'); + const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; + + if (typeof messageSecretBase64 === 'string') { + const messageSecret = Buffer.from(messageSecretBase64, 'base64'); + + const msg = { + messageContextInfo: { + messageSecret, + }, + pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, + }; + + return msg; + } + } + + this.logger.verbose('Returning message'); + return webMessageInfo[0].message; + } catch (error) { + return { conversation: '' }; + } + } + + private cleanStore() { + this.logger.verbose('Cronjob to clean store initialized'); + const cleanStore = this.configService.get('CLEAN_STORE'); + const database = this.configService.get('DATABASE'); + if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { + this.logger.verbose('Cronjob to clean store enabled'); + setInterval(() => { try { - const webMessageInfo = (await this.repository.message.find({ - where: { owner: this.instance.name, key: { id: key.id } }, - })) as unknown as proto.IWebMessageInfo[]; - if (full) { - this.logger.verbose('Returning full message'); - return webMessageInfo[0]; + for (const [key, value] of Object.entries(cleanStore)) { + if (value === true) { + execSync( + `rm -rf ${join(this.storePath, key.toLowerCase().replace('_', '-'), this.instance.name)}/*.json`, + ); + this.logger.verbose( + `Cleaned ${join(this.storePath, key.toLowerCase().replace('_', '-'), this.instance.name)}/*.json`, + ); } - if (webMessageInfo[0].message?.pollCreationMessage) { - this.logger.verbose('Returning poll message'); - const messageSecretBase64 = webMessageInfo[0].message?.messageContextInfo?.messageSecret; - - if (typeof messageSecretBase64 === 'string') { - const messageSecret = Buffer.from(messageSecretBase64, 'base64'); - - const msg = { - messageContextInfo: { - messageSecret, - }, - pollCreationMessage: webMessageInfo[0].message?.pollCreationMessage, - }; - - return msg; - } - } - - this.logger.verbose('Returning message'); - return webMessageInfo[0].message; + } } catch (error) { - return { conversation: '' }; + this.logger.error(error); } + }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); + } + } + + private async defineAuthState() { + this.logger.verbose('Defining auth state'); + const db = this.configService.get('DATABASE'); + const redis = this.configService.get('REDIS'); + + if (redis?.ENABLED) { + this.logger.verbose('Redis enabled'); + this.cache.reference = this.instance.name; + return await useMultiFileAuthStateRedisDb(this.cache); } - private cleanStore() { - this.logger.verbose('Cronjob to clean store initialized'); - const cleanStore = this.configService.get('CLEAN_STORE'); + if (db.SAVE_DATA.INSTANCE && db.ENABLED) { + this.logger.verbose('Database enabled'); + return await useMultiFileAuthStateDb(this.instance.name); + } + + this.logger.verbose('Store file enabled'); + return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + } + + public async connectToWhatsapp(number?: string): Promise { + this.logger.verbose('Connecting to whatsapp'); + try { + this.loadWebhook(); + this.loadChatwoot(); + this.loadSettings(); + + this.instance.authState = await this.defineAuthState(); + + const { version } = await fetchLatestBaileysVersion(); + this.logger.verbose('Baileys version: ' + version); + const session = this.configService.get('CONFIG_SESSION_PHONE'); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; + this.logger.verbose('Browser: ' + JSON.stringify(browser)); + + const socketConfig: UserFacingSocketConfig = { + auth: { + creds: this.instance.authState.state.creds, + keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), + }, + logger: P({ level: this.logBaileys }), + printQRInTerminal: false, + browser, + version, + markOnlineOnConnect: this.localSettings.always_online, + connectTimeoutMs: 60_000, + qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, + emitOwnEvents: false, + msgRetryCounterCache: this.msgRetryCounterCache, + getMessage: async (key) => (await this.getMessage(key)) as Promise, + generateHighQualityLinkPreview: true, + syncFullHistory: true, + userDevicesCache: this.userDevicesCache, + transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, + patchMessageBeforeSending: (message) => { + const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); + if (requiresPatch) { + message = { + viewOnceMessageV2: { + message: { + messageContextInfo: { + deviceListMetadataVersion: 2, + deviceListMetadata: {}, + }, + ...message, + }, + }, + }; + } + + return message; + }, + }; + + this.endSession = false; + + this.logger.verbose('Creating socket'); + + this.client = makeWASocket(socketConfig); + + this.logger.verbose('Socket created'); + + this.eventHandler(); + + this.logger.verbose('Socket event handler initialized'); + + this.phoneNumber = number; + + return this.client; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString()); + } + } + + private readonly chatHandle = { + 'chats.upsert': async (chats: Chat[], database: Database) => { + this.logger.verbose('Event received: chats.upsert'); + + this.logger.verbose('Finding chats in database'); + const chatsRepository = await this.repository.chat.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if chats exists in database to insert'); + const chatsRaw: ChatRaw[] = []; + for await (const chat of chats) { + if (chatsRepository.find((cr) => cr.id === chat.id)) { + continue; + } + + chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); + } + + this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); + await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + }, + + 'chats.update': async ( + chats: Partial< + proto.IConversation & { + lastMessageRecvTimestamp?: number; + } & { + conditional: (bufferedData: BufferedEventData) => boolean; + } + >[], + ) => { + this.logger.verbose('Event received: chats.update'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { id: chat.id, owner: this.instance.wuid }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); + await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); + }, + + 'chats.delete': async (chats: string[]) => { + this.logger.verbose('Event received: chats.delete'); + + this.logger.verbose('Deleting chats in database'); + chats.forEach( + async (chat) => + await this.repository.chat.delete({ + where: { owner: this.instance.name, id: chat }, + }), + ); + + this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); + await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); + }, + }; + + private readonly contactHandle = { + 'contacts.upsert': async (contacts: Contact[], database: Database) => { + this.logger.verbose('Event received: contacts.upsert'); + + this.logger.verbose('Finding contacts in database'); + const contactsRepository = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + this.logger.verbose('Verifying if contacts exists in database to insert'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + if (contactsRepository.find((cr) => cr.id === contact.id)) { + continue; + } + + contactsRaw.push({ + id: contact.id, + pushName: contact?.name || contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); + + this.logger.verbose('Inserting contacts in database'); + await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + + 'contacts.update': async (contacts: Partial[], database: Database) => { + this.logger.verbose('Event received: contacts.update'); + + this.logger.verbose('Verifying if contacts exists in database to update'); + const contactsRaw: ContactRaw[] = []; + for await (const contact of contacts) { + contactsRaw.push({ + id: contact.id, + pushName: contact?.name ?? contact?.verifiedName, + profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + + this.logger.verbose('Updating contacts in database'); + await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); + }, + }; + + private readonly messageHandle = { + 'messaging-history.set': async ( + { + messages, + chats, + isLatest, + }: { + chats: Chat[]; + contacts: Contact[]; + messages: proto.IWebMessageInfo[]; + isLatest: boolean; + }, + database: Database, + ) => { + this.logger.verbose('Event received: messaging-history.set'); + if (isLatest) { + this.logger.verbose('isLatest defined as true'); + const chatsRaw: ChatRaw[] = chats.map((chat) => { + return { + id: chat.id, + owner: this.instance.name, + lastMsgTimestamp: chat.lastMessageRecvTimestamp, + }; + }); + + this.logger.verbose('Sending data to webhook in event CHATS_SET'); + await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + + this.logger.verbose('Inserting chats in database'); + await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); + } + + const messagesRaw: MessageRaw[] = []; + const messagesRepository = await this.repository.message.find({ + where: { owner: this.instance.name }, + }); + for await (const [, m] of Object.entries(messages)) { + if (!m.message) { + continue; + } + if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { + continue; + } + + if (Long.isLong(m?.messageTimestamp)) { + m.messageTimestamp = m.messageTimestamp?.toNumber(); + } + + messagesRaw.push({ + key: m.key, + pushName: m.pushName, + participant: m.participant, + message: { ...m.message }, + messageType: getContentType(m.message), + messageTimestamp: m.messageTimestamp as number, + owner: this.instance.name, + }); + } + + this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); + + messages = undefined; + }, + + 'messages.upsert': async ( + { + messages, + type, + }: { + messages: proto.IWebMessageInfo[]; + type: MessageUpsertType; + }, + database: Database, + settings: SettingsRaw, + ) => { + this.logger.verbose('Event received: messages.upsert'); + const received = messages[0]; + + if (type !== 'notify' || received.message?.protocolMessage || received.message?.pollUpdateMessage) { + this.logger.verbose('message rejected'); + return; + } + + if (Long.isLong(received.messageTimestamp)) { + received.messageTimestamp = received.messageTimestamp?.toNumber(); + } + + if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + + const messageRaw: MessageRaw = { + key: received.key, + pushName: received.pushName, + message: { ...received.message }, + messageType: getContentType(received.message), + messageTimestamp: received.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(received.key.id), + }; + + if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + if (this.localSettings.read_status && received.key.id === 'status@broadcast') { + await this.client.readMessages([received.key]); + } + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); + await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); + + this.logger.verbose('Verifying contact from message'); + const contact = await this.repository.contact.find({ + where: { owner: this.instance.name, id: received.key.remoteJid }, + }); + + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: received.pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + if (contactRaw.id === 'status@broadcast') { + this.logger.verbose('Contact is status@broadcast'); + return; + } + + if (contact?.length) { + this.logger.verbose('Contact found in database'); + const contactRaw: ContactRaw = { + id: received.key.remoteJid, + pushName: contact[0].pushName, + profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, + owner: this.instance.name, + }; + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); + await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + + this.logger.verbose('Updating contact in database'); + await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + return; + } + + this.logger.verbose('Contact not found in database'); + + this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); + await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); + + this.logger.verbose('Inserting contact in database'); + await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); + }, + + 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { + this.logger.verbose('Event received: messages.update'); + const status: Record = { + 0: 'ERROR', + 1: 'PENDING', + 2: 'SERVER_ACK', + 3: 'DELIVERY_ACK', + 4: 'READ', + 5: 'PLAYED', + }; + for await (const { key, update } of args) { + if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { + this.logger.verbose('Message update is valid'); + + let pollUpdates: any; + if (update.pollUpdates) { + this.logger.verbose('Poll update found'); + + this.logger.verbose('Getting poll message'); + const pollCreation = await this.getMessage(key); + this.logger.verbose(pollCreation); + + if (pollCreation) { + this.logger.verbose('Getting aggregate votes in poll message'); + pollUpdates = getAggregateVotesInPollMessage({ + message: pollCreation as proto.IMessage, + pollUpdates: update.pollUpdates, + }); + } + } + + if (status[update.status] === 'READ' && !key.fromMe) return; + + if (update.message === null && update.status === undefined) { + this.logger.verbose('Message deleted'); + + this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); + await this.sendDataWebhook(Events.MESSAGES_DELETE, key); + + const message: MessageUpdateRaw = { + ...key, + status: 'DELETED', + datetime: Date.now(), + owner: this.instance.name, + }; + + this.logger.verbose(message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert( + [message], + this.instance.name, + database.SAVE_DATA.MESSAGE_UPDATE, + ); + return; + } + + const message: MessageUpdateRaw = { + ...key, + status: status[update.status], + datetime: Date.now(), + owner: this.instance.name, + pollUpdates, + }; + + this.logger.verbose(message); + + this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); + await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); + + this.logger.verbose('Inserting message in database'); + await this.repository.messageUpdate.insert([message], this.instance.name, database.SAVE_DATA.MESSAGE_UPDATE); + } + } + }, + }; + + private readonly groupHandler = { + 'groups.upsert': (groupMetadata: GroupMetadata[]) => { + this.logger.verbose('Event received: groups.upsert'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); + this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); + }, + + 'groups.update': (groupMetadataUpdate: Partial[]) => { + this.logger.verbose('Event received: groups.update'); + + this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); + this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); + }, + + 'group-participants.update': (participantsUpdate: { + id: string; + participants: string[]; + action: ParticipantAction; + }) => { + this.logger.verbose('Event received: group-participants.update'); + + this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); + this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); + }, + }; + + private eventHandler() { + this.logger.verbose('Initializing event handler'); + this.client.ev.process(async (events) => { + if (!this.endSession) { const database = this.configService.get('DATABASE'); - if (cleanStore?.CLEANING_INTERVAL && !database.ENABLED) { - this.logger.verbose('Cronjob to clean store enabled'); - setInterval(() => { - try { - for (const [key, value] of Object.entries(cleanStore)) { - if (value === true) { - execSync( - `rm -rf ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - this.logger.verbose( - `Cleaned ${join( - this.storePath, - key.toLowerCase().replace('_', '-'), - this.instance.name, - )}/*.json`, - ); - } - } - } catch (error) { - this.logger.error(error); - } - }, (cleanStore?.CLEANING_INTERVAL ?? 3600) * 1000); + const settings = await this.findSettings(); + + if (events.call) { + this.logger.verbose('Listening event: call'); + const call = events.call[0]; + + if (settings?.reject_call && call.status == 'offer') { + this.logger.verbose('Rejecting call'); + this.client.rejectCall(call.id, call.from); + } + + if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { + this.logger.verbose('Sending message in call'); + const msg = await this.client.sendMessage(call.from, { + text: settings.msg_call, + }); + + this.logger.verbose('Sending data to event messages.upsert'); + this.client.ev.emit('messages.upsert', { + messages: [msg], + type: 'notify', + }); + } + + this.logger.verbose('Sending data to webhook in event CALL'); + this.sendDataWebhook(Events.CALL, call); } + + if (events['connection.update']) { + this.logger.verbose('Listening event: connection.update'); + this.connectionUpdate(events['connection.update']); + } + + if (events['creds.update']) { + this.logger.verbose('Listening event: creds.update'); + this.instance.authState.saveCreds(); + } + + if (events['messaging-history.set']) { + this.logger.verbose('Listening event: messaging-history.set'); + const payload = events['messaging-history.set']; + this.messageHandle['messaging-history.set'](payload, database); + } + + if (events['messages.upsert']) { + this.logger.verbose('Listening event: messages.upsert'); + const payload = events['messages.upsert']; + this.messageHandle['messages.upsert'](payload, database, settings); + } + + if (events['messages.update']) { + this.logger.verbose('Listening event: messages.update'); + const payload = events['messages.update']; + this.messageHandle['messages.update'](payload, database, settings); + } + + if (events['presence.update']) { + this.logger.verbose('Listening event: presence.update'); + const payload = events['presence.update']; + + if (settings.groups_ignore && payload.id.includes('@g.us')) { + this.logger.verbose('group ignored'); + return; + } + this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); + } + + if (!settings?.groups_ignore) { + if (events['groups.upsert']) { + this.logger.verbose('Listening event: groups.upsert'); + const payload = events['groups.upsert']; + this.groupHandler['groups.upsert'](payload); + } + + if (events['groups.update']) { + this.logger.verbose('Listening event: groups.update'); + const payload = events['groups.update']; + this.groupHandler['groups.update'](payload); + } + + if (events['group-participants.update']) { + this.logger.verbose('Listening event: group-participants.update'); + const payload = events['group-participants.update']; + this.groupHandler['group-participants.update'](payload); + } + } + + if (events['chats.upsert']) { + this.logger.verbose('Listening event: chats.upsert'); + const payload = events['chats.upsert']; + this.chatHandle['chats.upsert'](payload, database); + } + + if (events['chats.update']) { + this.logger.verbose('Listening event: chats.update'); + const payload = events['chats.update']; + this.chatHandle['chats.update'](payload); + } + + if (events['chats.delete']) { + this.logger.verbose('Listening event: chats.delete'); + const payload = events['chats.delete']; + this.chatHandle['chats.delete'](payload); + } + + if (events['contacts.upsert']) { + this.logger.verbose('Listening event: contacts.upsert'); + const payload = events['contacts.upsert']; + this.contactHandle['contacts.upsert'](payload, database); + } + + if (events['contacts.update']) { + this.logger.verbose('Listening event: contacts.update'); + const payload = events['contacts.update']; + this.contactHandle['contacts.update'](payload, database); + } + } + }); + } + + // Check if the number is MX or AR + private formatMXOrARNumber(jid: string): string { + const countryCode = jid.substring(0, 2); + + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + if (jid.length === 13) { + const number = countryCode + jid.substring(3); + return number; + } + + return jid; + } + return jid; + } + + // Check if the number is br + private formatBRNumber(jid: string) { + const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); + if (regexp.test(jid)) { + const match = regexp.exec(jid); + if (match && match[1] === '55') { + const joker = Number.parseInt(match[3][0]); + const ddd = Number.parseInt(match[2]); + if (joker < 7 || ddd < 31) { + return match[0]; + } + return match[1] + match[2] + match[3]; + } + return jid; + } else { + return jid; + } + } + + private createJid(number: string): string { + this.logger.verbose('Creating jid with number: ' + number); + + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { + this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); + return number; } - private async defineAuthState() { - this.logger.verbose('Defining auth state'); - const db = this.configService.get('DATABASE'); - const redis = this.configService.get('REDIS'); - - if (redis?.ENABLED) { - this.logger.verbose('Redis enabled'); - this.cache.reference = this.instance.name; - return await useMultiFileAuthStateRedisDb(this.cache); - } - - if (db.SAVE_DATA.INSTANCE && db.ENABLED) { - this.logger.verbose('Database enabled'); - return await useMultiFileAuthStateDb(this.instance.name); - } - - this.logger.verbose('Store file enabled'); - return await useMultiFileAuthState(join(INSTANCE_DIR, this.instance.name)); + if (number.includes('@broadcast')) { + this.logger.verbose('Number already contains @broadcast'); + return number; } - public async connectToWhatsapp(number?: string): Promise { - this.logger.verbose('Connecting to whatsapp'); + number = number + ?.replace(/\s/g, '') + .replace(/\+/g, '') + .replace(/\(/g, '') + .replace(/\)/g, '') + .split(':')[0] + .split('@')[0]; + + if (number.includes('-') && number.length >= 24) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + number = number.replace(/\D/g, ''); + + if (number.length >= 18) { + this.logger.verbose('Jid created is group: ' + `${number}@g.us`); + number = number.replace(/[^\d-]/g, ''); + return `${number}@g.us`; + } + + this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); + return `${number}@s.whatsapp.net`; + } + + public async profilePicture(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile picture with jid: ' + jid); + try { + this.logger.verbose('Getting profile picture url'); + return { + wuid: jid, + profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), + }; + } catch (error) { + this.logger.verbose('Profile picture not found'); + return { + wuid: jid, + profilePictureUrl: null, + }; + } + } + + public async getStatus(number: string) { + const jid = this.createJid(number); + + this.logger.verbose('Getting profile status with jid:' + jid); + try { + this.logger.verbose('Getting status'); + return { + wuid: jid, + status: (await this.client.fetchStatus(jid))?.status, + }; + } catch (error) { + this.logger.verbose('Status not found'); + return { + wuid: jid, + status: null, + }; + } + } + + public async fetchProfile(instanceName: string, number?: string) { + const jid = number ? this.createJid(number) : this.client?.user?.id; + + this.logger.verbose('Getting profile with jid: ' + jid); + try { + this.logger.verbose('Getting profile info'); + const business = await this.fetchBusinessProfile(jid); + + if (number) { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const picture = await this.profilePicture(jid); + const status = await this.getStatus(jid); + + return { + wuid: jid, + name: info?.name, + numberExists: info?.exists, + picture: picture?.profilePictureUrl, + status: status?.status, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } else { + const info = await waMonitor.instanceInfo(instanceName); + + return { + wuid: jid, + name: info?.instance?.profileName, + numberExists: true, + picture: info?.instance?.profilePictureUrl, + status: info?.instance?.profileStatus, + isBusiness: business.isBusiness, + email: business?.email, + description: business?.description, + website: business?.website?.shift(), + }; + } + } catch (error) { + this.logger.verbose('Profile not found'); + return { + wuid: jid, + name: null, + picture: null, + status: null, + os: null, + isBusiness: false, + }; + } + } + + private async sendMessageWithTyping(number: string, message: T, options?: Options) { + this.logger.verbose('Sending message with typing'); + + const numberWA = await this.whatsappNumber({ numbers: [number] }); + const isWA = numberWA[0]; + + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { + throw new BadRequestException(isWA); + } + + const sender = isWA.jid; + + try { + if (options?.delay) { + this.logger.verbose('Delaying message'); + + await this.client.presenceSubscribe(sender); + this.logger.verbose('Subscribing to presence'); + + await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); + this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); + + await delay(options.delay); + this.logger.verbose('Set delay: ' + options.delay); + + await this.client.sendPresenceUpdate('paused', sender); + this.logger.verbose('Sending presence update: paused'); + } + + const linkPreview = options?.linkPreview != false ? undefined : false; + + let quoted: WAMessage; + + if (options?.quoted) { + const m = options?.quoted; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + quoted = msg; + this.logger.verbose('Quoted message'); + } + + let mentions: string[]; + if (isJidGroup(sender)) { try { - this.loadWebhook(); - this.loadChatwoot(); - this.loadSettings(); + const groupMetadata = await this.client.groupMetadata(sender); - this.instance.authState = await this.defineAuthState(); + if (!groupMetadata) { + throw new NotFoundException('Group not found'); + } - const { version } = await fetchLatestBaileysVersion(); - this.logger.verbose('Baileys version: ' + version); - const session = this.configService.get('CONFIG_SESSION_PHONE'); - const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; - this.logger.verbose('Browser: ' + JSON.stringify(browser)); + if (options?.mentions) { + this.logger.verbose('Mentions defined'); - const socketConfig: UserFacingSocketConfig = { - auth: { - creds: this.instance.authState.state.creds, - keys: makeCacheableSignalKeyStore(this.instance.authState.state.keys, P({ level: 'error' })), + if (options.mentions?.everyOne) { + this.logger.verbose('Mentions everyone'); + + this.logger.verbose('Getting group metadata'); + mentions = groupMetadata.participants.map((participant) => participant.id); + this.logger.verbose('Getting group metadata for mentions'); + } else if (options.mentions?.mentioned?.length) { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + return null; + // throw new BadRequestException('Mentions must be a number'); + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); + } + } + + const messageSent = await (async () => { + const option = { + quoted, + }; + + if ( + !message['audio'] && + !message['poll'] && + !message['sticker'] && + !message['conversation'] && + sender !== 'status@broadcast' + ) { + if (!message['audio']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + { + forward: { + key: { remoteJid: this.instance.wuid, fromMe: true }, + message, }, - logger: P({ level: this.logBaileys }), - printQRInTerminal: false, - browser, - version, - markOnlineOnConnect: this.localSettings.always_online, - connectTimeoutMs: 60_000, - qrTimeout: 40_000, - defaultQueryTimeoutMs: undefined, - emitOwnEvents: false, - msgRetryCounterCache: this.msgRetryCounterCache, - getMessage: async (key) => (await this.getMessage(key)) as Promise, - generateHighQualityLinkPreview: true, - syncFullHistory: true, - userDevicesCache: this.userDevicesCache, - transactionOpts: { maxCommitRetries: 1, delayBetweenTriesMs: 10 }, - patchMessageBeforeSending: (message) => { - const requiresPatch = !!(message.buttonsMessage || message.listMessage || message.templateMessage); - if (requiresPatch) { - message = { - viewOnceMessageV2: { - message: { - messageContextInfo: { - deviceListMetadataVersion: 2, - deviceListMetadata: {}, - }, - ...message, - }, - }, - }; - } - - return message; - }, - }; - - this.endSession = false; - - this.logger.verbose('Creating socket'); - - this.client = makeWASocket(socketConfig); - - this.logger.verbose('Socket created'); - - this.eventHandler(); - - this.logger.verbose('Socket event handler initialized'); - - this.phoneNumber = number; - - return this.client; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString()); - } - } - - private readonly chatHandle = { - 'chats.upsert': async (chats: Chat[], database: Database) => { - this.logger.verbose('Event received: chats.upsert'); - - this.logger.verbose('Finding chats in database'); - const chatsRepository = await this.repository.chat.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if chats exists in database to insert'); - const chatsRaw: ChatRaw[] = []; - for await (const chat of chats) { - if (chatsRepository.find((cr) => cr.id === chat.id)) { - continue; - } - - chatsRaw.push({ id: chat.id, owner: this.instance.wuid }); - } - - this.logger.verbose('Sending data to webhook in event CHATS_UPSERT'); - await this.sendDataWebhook(Events.CHATS_UPSERT, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - }, - - 'chats.update': async ( - chats: Partial< - proto.IConversation & { - lastMessageRecvTimestamp?: number; - } & { - conditional: (bufferedData: BufferedEventData) => boolean; - } - >[], - ) => { - this.logger.verbose('Event received: chats.update'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { id: chat.id, owner: this.instance.wuid }; - }); - - this.logger.verbose('Sending data to webhook in event CHATS_UPDATE'); - await this.sendDataWebhook(Events.CHATS_UPDATE, chatsRaw); - }, - - 'chats.delete': async (chats: string[]) => { - this.logger.verbose('Event received: chats.delete'); - - this.logger.verbose('Deleting chats in database'); - chats.forEach( - async (chat) => - await this.repository.chat.delete({ - where: { owner: this.instance.name, id: chat }, - }), + mentions, + }, + option as unknown as MiscMessageGenerationOptions, ); + } + } - this.logger.verbose('Sending data to webhook in event CHATS_DELETE'); - await this.sendDataWebhook(Events.CHATS_DELETE, [...chats]); - }, - }; - - private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[], database: Database) => { - this.logger.verbose('Event received: contacts.upsert'); - - this.logger.verbose('Finding contacts in database'); - const contactsRepository = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - this.logger.verbose('Verifying if contacts exists in database to insert'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - if (contactsRepository.find((cr) => cr.id === contact.id)) { - continue; - } - - contactsRaw.push({ - id: contact.id, - pushName: contact?.name || contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw); - - this.logger.verbose('Inserting contacts in database'); - await this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - - 'contacts.update': async (contacts: Partial[], database: Database) => { - this.logger.verbose('Event received: contacts.update'); - - this.logger.verbose('Verifying if contacts exists in database to update'); - const contactsRaw: ContactRaw[] = []; - for await (const contact of contacts) { - contactsRaw.push({ - id: contact.id, - pushName: contact?.name ?? contact?.verifiedName, - profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - - this.logger.verbose('Updating contacts in database'); - await this.repository.contact.update(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS); - }, - }; - - private readonly messageHandle = { - 'messaging-history.set': async ( + if (message['conversation']) { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, { - messages, - chats, - isLatest, - }: { - chats: Chat[]; - contacts: Contact[]; - messages: proto.IWebMessageInfo[]; - isLatest: boolean; - }, - database: Database, - ) => { - this.logger.verbose('Event received: messaging-history.set'); - if (isLatest) { - this.logger.verbose('isLatest defined as true'); - const chatsRaw: ChatRaw[] = chats.map((chat) => { - return { - id: chat.id, - owner: this.instance.name, - lastMsgTimestamp: chat.lastMessageRecvTimestamp, - }; - }); + text: message['conversation'], + mentions, + linkPreview: linkPreview, + } as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, + ); + } - this.logger.verbose('Sending data to webhook in event CHATS_SET'); - await this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - - this.logger.verbose('Inserting chats in database'); - await this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS); - } - - const messagesRaw: MessageRaw[] = []; - const messagesRepository = await this.repository.message.find({ - where: { owner: this.instance.name }, - }); - for await (const [, m] of Object.entries(messages)) { - if (!m.message) { - continue; - } - if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) { - continue; - } - - if (Long.isLong(m?.messageTimestamp)) { - m.messageTimestamp = m.messageTimestamp?.toNumber(); - } - - messagesRaw.push({ - key: m.key, - pushName: m.pushName, - participant: m.participant, - message: { ...m.message }, - messageType: getContentType(m.message), - messageTimestamp: m.messageTimestamp as number, - owner: this.instance.name, - }); - } - - this.logger.verbose('Sending data to webhook in event MESSAGES_SET'); - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]); - - messages = undefined; - }, - - 'messages.upsert': async ( + if (sender === 'status@broadcast') { + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message['status'].content as unknown as AnyMessageContent, { - messages, - type, - }: { - messages: proto.IWebMessageInfo[]; - type: MessageUpsertType; - }, - database: Database, - settings: SettingsRaw, - ) => { - this.logger.verbose('Event received: messages.upsert'); - const received = messages[0]; - - if (type !== 'notify' || received.message?.protocolMessage || received.message?.pollUpdateMessage) { - this.logger.verbose('message rejected'); - return; - } - - if (Long.isLong(received.messageTimestamp)) { - received.messageTimestamp = received.messageTimestamp?.toNumber(); - } - - if (settings?.groups_ignore && received.key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - - const messageRaw: MessageRaw = { - key: received.key, - pushName: received.pushName, - message: { ...received.message }, - messageType: getContentType(received.message), - messageTimestamp: received.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(received.key.id), - }; - - if (this.localSettings.read_messages && received.key.id !== 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - if (this.localSettings.read_status && received.key.id === 'status@broadcast') { - await this.client.readMessages([received.key]); - } - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.MESSAGES_UPSERT, - { instanceName: this.instance.name }, - messageRaw, - ); - } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert([messageRaw], this.instance.name, database.SAVE_DATA.NEW_MESSAGE); - - this.logger.verbose('Verifying contact from message'); - const contact = await this.repository.contact.find({ - where: { owner: this.instance.name, id: received.key.remoteJid }, - }); - - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: received.pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - if (contactRaw.id === 'status@broadcast') { - this.logger.verbose('Contact is status@broadcast'); - return; - } - - if (contact?.length) { - this.logger.verbose('Contact found in database'); - const contactRaw: ContactRaw = { - id: received.key.remoteJid, - pushName: contact[0].pushName, - profilePictureUrl: (await this.profilePicture(received.key.remoteJid)).profilePictureUrl, - owner: this.instance.name, - }; - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); - await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); - - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactRaw, - ); - } - - this.logger.verbose('Updating contact in database'); - await this.repository.contact.update([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - return; - } - - this.logger.verbose('Contact not found in database'); - - this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT'); - await this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw); - - this.logger.verbose('Inserting contact in database'); - await this.repository.contact.insert([contactRaw], this.instance.name, database.SAVE_DATA.CONTACTS); - }, - - 'messages.update': async (args: WAMessageUpdate[], database: Database, settings: SettingsRaw) => { - this.logger.verbose('Event received: messages.update'); - const status: Record = { - 0: 'ERROR', - 1: 'PENDING', - 2: 'SERVER_ACK', - 3: 'DELIVERY_ACK', - 4: 'READ', - 5: 'PLAYED', - }; - for await (const { key, update } of args) { - if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { - this.logger.verbose('Message update is valid'); - - let pollUpdates: any; - if (update.pollUpdates) { - this.logger.verbose('Poll update found'); - - this.logger.verbose('Getting poll message'); - const pollCreation = await this.getMessage(key); - this.logger.verbose(pollCreation); - - if (pollCreation) { - this.logger.verbose('Getting aggregate votes in poll message'); - pollUpdates = getAggregateVotesInPollMessage({ - message: pollCreation as proto.IMessage, - pollUpdates: update.pollUpdates, - }); - } - } - - if (status[update.status] === 'READ' && !key.fromMe) return; - - if (update.message === null && update.status === undefined) { - this.logger.verbose('Message deleted'); - - this.logger.verbose('Sending data to webhook in event MESSAGE_DELETE'); - await this.sendDataWebhook(Events.MESSAGES_DELETE, key); - - const message: MessageUpdateRaw = { - ...key, - status: 'DELETED', - datetime: Date.now(), - owner: this.instance.name, - }; - - this.logger.verbose(message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - return; - } - - const message: MessageUpdateRaw = { - ...key, - status: status[update.status], - datetime: Date.now(), - owner: this.instance.name, - pollUpdates, - }; - - this.logger.verbose(message); - - this.logger.verbose('Sending data to webhook in event MESSAGES_UPDATE'); - await this.sendDataWebhook(Events.MESSAGES_UPDATE, message); - - this.logger.verbose('Inserting message in database'); - await this.repository.messageUpdate.insert( - [message], - this.instance.name, - database.SAVE_DATA.MESSAGE_UPDATE, - ); - } - } - }, - }; - - private readonly groupHandler = { - 'groups.upsert': (groupMetadata: GroupMetadata[]) => { - this.logger.verbose('Event received: groups.upsert'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPSERT'); - this.sendDataWebhook(Events.GROUPS_UPSERT, groupMetadata); - }, - - 'groups.update': (groupMetadataUpdate: Partial[]) => { - this.logger.verbose('Event received: groups.update'); - - this.logger.verbose('Sending data to webhook in event GROUPS_UPDATE'); - this.sendDataWebhook(Events.GROUPS_UPDATE, groupMetadataUpdate); - }, - - 'group-participants.update': (participantsUpdate: { - id: string; - participants: string[]; - action: ParticipantAction; - }) => { - this.logger.verbose('Event received: group-participants.update'); - - this.logger.verbose('Sending data to webhook in event GROUP_PARTICIPANTS_UPDATE'); - this.sendDataWebhook(Events.GROUP_PARTICIPANTS_UPDATE, participantsUpdate); - }, - }; - - private eventHandler() { - this.logger.verbose('Initializing event handler'); - this.client.ev.process(async (events) => { - if (!this.endSession) { - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - if (events.call) { - this.logger.verbose('Listening event: call'); - const call = events.call[0]; - - if (settings?.reject_call && call.status == 'offer') { - this.logger.verbose('Rejecting call'); - this.client.rejectCall(call.id, call.from); - } - - if (settings?.msg_call.trim().length > 0 && call.status == 'offer') { - this.logger.verbose('Sending message in call'); - const msg = await this.client.sendMessage(call.from, { - text: settings.msg_call, - }); - - this.logger.verbose('Sending data to event messages.upsert'); - this.client.ev.emit('messages.upsert', { - messages: [msg], - type: 'notify', - }); - } - - this.logger.verbose('Sending data to webhook in event CALL'); - this.sendDataWebhook(Events.CALL, call); - } - - if (events['connection.update']) { - this.logger.verbose('Listening event: connection.update'); - this.connectionUpdate(events['connection.update']); - } - - if (events['creds.update']) { - this.logger.verbose('Listening event: creds.update'); - this.instance.authState.saveCreds(); - } - - if (events['messaging-history.set']) { - this.logger.verbose('Listening event: messaging-history.set'); - const payload = events['messaging-history.set']; - this.messageHandle['messaging-history.set'](payload, database); - } - - if (events['messages.upsert']) { - this.logger.verbose('Listening event: messages.upsert'); - const payload = events['messages.upsert']; - this.messageHandle['messages.upsert'](payload, database, settings); - } - - if (events['messages.update']) { - this.logger.verbose('Listening event: messages.update'); - const payload = events['messages.update']; - this.messageHandle['messages.update'](payload, database, settings); - } - - if (events['presence.update']) { - this.logger.verbose('Listening event: presence.update'); - const payload = events['presence.update']; - - if (settings.groups_ignore && payload.id.includes('@g.us')) { - this.logger.verbose('group ignored'); - return; - } - this.sendDataWebhook(Events.PRESENCE_UPDATE, payload); - } - - if (!settings?.groups_ignore) { - if (events['groups.upsert']) { - this.logger.verbose('Listening event: groups.upsert'); - const payload = events['groups.upsert']; - this.groupHandler['groups.upsert'](payload); - } - - if (events['groups.update']) { - this.logger.verbose('Listening event: groups.update'); - const payload = events['groups.update']; - this.groupHandler['groups.update'](payload); - } - - if (events['group-participants.update']) { - this.logger.verbose('Listening event: group-participants.update'); - const payload = events['group-participants.update']; - this.groupHandler['group-participants.update'](payload); - } - } - - if (events['chats.upsert']) { - this.logger.verbose('Listening event: chats.upsert'); - const payload = events['chats.upsert']; - this.chatHandle['chats.upsert'](payload, database); - } - - if (events['chats.update']) { - this.logger.verbose('Listening event: chats.update'); - const payload = events['chats.update']; - this.chatHandle['chats.update'](payload); - } - - if (events['chats.delete']) { - this.logger.verbose('Listening event: chats.delete'); - const payload = events['chats.delete']; - this.chatHandle['chats.delete'](payload); - } - - if (events['contacts.upsert']) { - this.logger.verbose('Listening event: contacts.upsert'); - const payload = events['contacts.upsert']; - this.contactHandle['contacts.upsert'](payload, database); - } - - if (events['contacts.update']) { - this.logger.verbose('Listening event: contacts.update'); - const payload = events['contacts.update']; - this.contactHandle['contacts.update'](payload, database); - } - } - }); - } - - // Check if the number is MX or AR - private formatMXOrARNumber(jid: string): string { - const countryCode = jid.substring(0, 2); - - if (Number(countryCode) === 52 || Number(countryCode) === 54) { - if (jid.length === 13) { - const number = countryCode + jid.substring(3); - return number; - } - - return jid; - } - return jid; - } - - // Check if the number is br - private formatBRNumber(jid: string) { - const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/); - if (regexp.test(jid)) { - const match = regexp.exec(jid); - if (match && match[1] === '55') { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 31) { - return match[0]; - } - return match[1] + match[2] + match[3]; - } - return jid; - } else { - return jid; - } - } - - private createJid(number: string): string { - this.logger.verbose('Creating jid with number: ' + number); - - if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { - this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); - return number; + backgroundColor: message['status'].option.backgroundColor, + font: message['status'].option.font, + statusJidList: message['status'].option.statusJidList, + } as unknown as MiscMessageGenerationOptions, + ); } - if (number.includes('@broadcast')) { - this.logger.verbose('Number already contains @broadcast'); - return number; - } - - number = number - ?.replace(/\s/g, '') - .replace(/\+/g, '') - .replace(/\(/g, '') - .replace(/\)/g, '') - .split(':')[0] - .split('@')[0]; - - if (number.includes('-') && number.length >= 24) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - number = number.replace(/\D/g, ''); - - if (number.length >= 18) { - this.logger.verbose('Jid created is group: ' + `${number}@g.us`); - number = number.replace(/[^\d-]/g, ''); - return `${number}@g.us`; - } - - this.logger.verbose('Jid created is whatsapp: ' + `${number}@s.whatsapp.net`); - return `${number}@s.whatsapp.net`; - } - - public async profilePicture(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile picture with jid: ' + jid); - try { - this.logger.verbose('Getting profile picture url'); - return { - wuid: jid, - profilePictureUrl: await this.client.profilePictureUrl(jid, 'image'), - }; - } catch (error) { - this.logger.verbose('Profile picture not found'); - return { - wuid: jid, - profilePictureUrl: null, - }; - } - } - - public async getStatus(number: string) { - const jid = this.createJid(number); - - this.logger.verbose('Getting profile status with jid:' + jid); - try { - this.logger.verbose('Getting status'); - return { - wuid: jid, - status: (await this.client.fetchStatus(jid))?.status, - }; - } catch (error) { - this.logger.verbose('Status not found'); - return { - wuid: jid, - status: null, - }; - } - } - - public async fetchProfile(instanceName: string, number?: string) { - const jid = number ? this.createJid(number) : this.client?.user?.id; - - this.logger.verbose('Getting profile with jid: ' + jid); - try { - this.logger.verbose('Getting profile info'); - const business = await this.fetchBusinessProfile(jid); - - if (number) { - const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); - const picture = await this.profilePicture(jid); - const status = await this.getStatus(jid); - - return { - wuid: jid, - name: info?.name, - numberExists: info?.exists, - picture: picture?.profilePictureUrl, - status: status?.status, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } else { - const info = await waMonitor.instanceInfo(instanceName); - - return { - wuid: jid, - name: info?.instance?.profileName, - numberExists: true, - picture: info?.instance?.profilePictureUrl, - status: info?.instance?.profileStatus, - isBusiness: business.isBusiness, - email: business?.email, - description: business?.description, - website: business?.website?.shift(), - }; - } - } catch (error) { - this.logger.verbose('Profile not found'); - return { - wuid: jid, - name: null, - picture: null, - status: null, - os: null, - isBusiness: false, - }; - } - } - - private async sendMessageWithTyping(number: string, message: T, options?: Options) { - this.logger.verbose('Sending message with typing'); - - const numberWA = await this.whatsappNumber({ numbers: [number] }); - const isWA = numberWA[0]; - - if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { - throw new BadRequestException(isWA); - } - - const sender = isWA.jid; - - try { - if (options?.delay) { - this.logger.verbose('Delaying message'); - - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); - - await this.client.sendPresenceUpdate(options?.presence ?? 'composing', sender); - this.logger.verbose('Sending presence update: ' + options?.presence ?? 'composing'); - - await delay(options.delay); - this.logger.verbose('Set delay: ' + options.delay); - - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); - } - - const linkPreview = options?.linkPreview != false ? undefined : false; - - let quoted: WAMessage; - - if (options?.quoted) { - const m = options?.quoted; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - quoted = msg; - this.logger.verbose('Quoted message'); - } - - let mentions: string[]; - if (isJidGroup(sender)) { - try { - const groupMetadata = await this.client.groupMetadata(sender); - - if (!groupMetadata) { - throw new NotFoundException('Group not found'); - } - - if (options?.mentions) { - this.logger.verbose('Mentions defined'); - - if (options.mentions?.everyOne) { - this.logger.verbose('Mentions everyone'); - - this.logger.verbose('Getting group metadata'); - mentions = groupMetadata.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else if (options.mentions?.mentioned?.length) { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - return null; - // throw new BadRequestException('Mentions must be a number'); - } - return jid; - }); - } - } - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - - const messageSent = await (async () => { - const option = { - quoted, - }; - - if ( - !message['audio'] && - !message['poll'] && - !message['sticker'] && - !message['conversation'] && - sender !== 'status@broadcast' - ) { - if (!message['audio']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - forward: { - key: { remoteJid: this.instance.wuid, fromMe: true }, - message, - }, - mentions, - }, - option as unknown as MiscMessageGenerationOptions, - ); - } - } - - if (message['conversation']) { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - { - text: message['conversation'], - mentions, - linkPreview: linkPreview, - } as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - } - - if (sender === 'status@broadcast') { - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message['status'].content as unknown as AnyMessageContent, - { - backgroundColor: message['status'].option.backgroundColor, - font: message['status'].option.font, - statusJidList: message['status'].option.statusJidList, - } as unknown as MiscMessageGenerationOptions, - ); - } - - this.logger.verbose('Sending message'); - return await this.client.sendMessage( - sender, - message as unknown as AnyMessageContent, - option as unknown as MiscMessageGenerationOptions, - ); - })(); - - const messageRaw: MessageRaw = { - key: messageSent.key, - pushName: messageSent.pushName, - message: { ...messageSent.message }, - messageType: getContentType(messageSent.message), - messageTimestamp: messageSent.messageTimestamp as number, - owner: this.instance.name, - source: getDevice(messageSent.key.id), - }; - - this.logger.log(messageRaw); - - this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); - await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); - - // if (this.localChatwoot.enabled) { - // this.chatwootService.eventWhatsapp( - // Events.SEND_MESSAGE, - // { instanceName: this.instance.name }, - // messageRaw, - // ); - // } - - this.logger.verbose('Inserting message in database'); - await this.repository.message.insert( - [messageRaw], - this.instance.name, - this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, - ); - - return messageSent; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - // Instance Controller - public get connectionStatus() { - this.logger.verbose('Getting connection status'); - return this.stateConnection; - } - - // Send Message Controller - public async textMessage(data: SendTextDto) { - this.logger.verbose('Sending text message'); - return await this.sendMessageWithTyping( - data.number, - { - conversation: data.textMessage.text, - }, - data?.options, + this.logger.verbose('Sending message'); + return await this.client.sendMessage( + sender, + message as unknown as AnyMessageContent, + option as unknown as MiscMessageGenerationOptions, ); + })(); + + const messageRaw: MessageRaw = { + key: messageSent.key, + pushName: messageSent.pushName, + message: { ...messageSent.message }, + messageType: getContentType(messageSent.message), + messageTimestamp: messageSent.messageTimestamp as number, + owner: this.instance.name, + source: getDevice(messageSent.key.id), + }; + + this.logger.log(messageRaw); + + this.logger.verbose('Sending data to webhook in event SEND_MESSAGE'); + await this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw); + + // if (this.localChatwoot.enabled) { + // this.chatwootService.eventWhatsapp( + // Events.SEND_MESSAGE, + // { instanceName: this.instance.name }, + // messageRaw, + // ); + // } + + this.logger.verbose('Inserting message in database'); + await this.repository.message.insert( + [messageRaw], + this.instance.name, + this.configService.get('DATABASE').SAVE_DATA.NEW_MESSAGE, + ); + + return messageSent; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + // Instance Controller + public get connectionStatus() { + this.logger.verbose('Getting connection status'); + return this.stateConnection; + } + + // Send Message Controller + public async textMessage(data: SendTextDto) { + this.logger.verbose('Sending text message'); + return await this.sendMessageWithTyping( + data.number, + { + conversation: data.textMessage.text, + }, + data?.options, + ); + } + + public async pollMessage(data: SendPollDto) { + this.logger.verbose('Sending poll message'); + return await this.sendMessageWithTyping( + data.number, + { + poll: { + name: data.pollMessage.name, + selectableCount: data.pollMessage.selectableCount, + values: data.pollMessage.values, + }, + }, + data?.options, + ); + } + + private async formatStatusMessage(status: StatusMessage) { + this.logger.verbose('Formatting status message'); + + if (!status.type) { + throw new BadRequestException('Type is required'); } - public async pollMessage(data: SendPollDto) { - this.logger.verbose('Sending poll message'); - return await this.sendMessageWithTyping( - data.number, - { - poll: { - name: data.pollMessage.name, - selectableCount: data.pollMessage.selectableCount, - values: data.pollMessage.values, - }, - }, - data?.options, + if (!status.content) { + throw new BadRequestException('Content is required'); + } + + if (status.allContacts) { + this.logger.verbose('All contacts defined as true'); + + this.logger.verbose('Getting contacts from database'); + const contacts = await this.repository.contact.find({ + where: { owner: this.instance.name }, + }); + + if (!contacts.length) { + throw new BadRequestException('Contacts not found'); + } + + this.logger.verbose('Getting contacts with push name'); + status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); + + this.logger.verbose(status.statusJidList); + } + + if (!status.statusJidList?.length && !status.allContacts) { + throw new BadRequestException('StatusJidList is required'); + } + + if (status.type === 'text') { + this.logger.verbose('Type defined as text'); + + if (!status.backgroundColor) { + throw new BadRequestException('Background color is required'); + } + + if (!status.font) { + throw new BadRequestException('Font is required'); + } + + return { + content: { + text: status.content, + }, + option: { + backgroundColor: status.backgroundColor, + font: status.font, + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'image') { + this.logger.verbose('Type defined as image'); + + return { + content: { + image: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'video') { + this.logger.verbose('Type defined as video'); + + return { + content: { + video: { + url: status.content, + }, + caption: status.caption, + }, + option: { + statusJidList: status.statusJidList, + }, + }; + } + if (status.type === 'audio') { + this.logger.verbose('Type defined as audio'); + + this.logger.verbose('Processing audio'); + const convert = await this.processAudio(status.content, 'status@broadcast'); + if (typeof convert === 'string') { + this.logger.verbose('Audio processed'); + const audio = fs.readFileSync(convert).toString('base64'); + + const result = { + content: { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + option: { + statusJidList: status.statusJidList, + }, + }; + + fs.unlinkSync(convert); + + return result; + } else { + throw new InternalServerErrorException(convert); + } + } + + throw new BadRequestException('Type not found'); + } + + public async statusMessage(data: SendStatusDto) { + this.logger.verbose('Sending status message'); + const status = await this.formatStatusMessage(data.statusMessage); + + return await this.sendMessageWithTyping('status@broadcast', { + status, + }); + } + + private async prepareMediaMessage(mediaMessage: MediaMessage) { + try { + this.logger.verbose('Preparing media message'); + const prepareMedia = await prepareWAMessageMedia( + { + [mediaMessage.mediatype]: isURL(mediaMessage.media) + ? { url: mediaMessage.media } + : Buffer.from(mediaMessage.media, 'base64'), + } as any, + { upload: this.client.waUploadToServer }, + ); + + const mediaType = mediaMessage.mediatype + 'Message'; + this.logger.verbose('Media type: ' + mediaType); + + if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { + this.logger.verbose('If media type is document and file name is not defined then'); + const regex = new RegExp(/.*\/(.+?)\./); + const arrayMatch = regex.exec(mediaMessage.media); + mediaMessage.fileName = arrayMatch[1]; + this.logger.verbose('File name: ' + mediaMessage.fileName); + } + + let mimetype: string; + + if (isURL(mediaMessage.media)) { + mimetype = getMIMEType(mediaMessage.media); + } else { + mimetype = getMIMEType(mediaMessage.fileName); + } + + this.logger.verbose('Mimetype: ' + mimetype); + + prepareMedia[mediaType].caption = mediaMessage?.caption; + prepareMedia[mediaType].mimetype = mimetype; + prepareMedia[mediaType].fileName = mediaMessage.fileName; + + if (mediaMessage.mediatype === 'video') { + this.logger.verbose('Is media type video then set gif playback as false'); + prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( + readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), ); + prepareMedia[mediaType].gifPlayback = false; + } + + this.logger.verbose('Generating wa message from content'); + return generateWAMessageFromContent( + '', + { [mediaType]: { ...prepareMedia[mediaType] } }, + { userJid: this.instance.wuid }, + ); + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException(error?.toString() || error); + } + } + + private async convertToWebP(image: string, number: string) { + try { + this.logger.verbose('Converting image to WebP to sticker'); + + let imagePath: string; + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to image name: ' + hash); + + const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; + this.logger.verbose('Output path: ' + outputPath); + + if (isBase64(image)) { + this.logger.verbose('Image is base64'); + + const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); + const imageBuffer = Buffer.from(base64Data, 'base64'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } else { + this.logger.verbose('Image is url'); + + const timestamp = new Date().getTime(); + const url = `${image}?timestamp=${timestamp}`; + this.logger.verbose('including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting image from url'); + + const imageBuffer = Buffer.from(response.data, 'binary'); + imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; + this.logger.verbose('Image path: ' + imagePath); + + await sharp(imageBuffer).toFile(imagePath); + this.logger.verbose('Image created'); + } + + await sharp(imagePath).webp().toFile(outputPath); + this.logger.verbose('Image converted to WebP'); + + fs.unlinkSync(imagePath); + this.logger.verbose('Temp image deleted'); + + return outputPath; + } catch (error) { + console.error('Erro ao converter a imagem para WebP:', error); + } + } + + public async mediaSticker(data: SendStickerDto) { + this.logger.verbose('Sending media sticker'); + const convert = await this.convertToWebP(data.stickerMessage.image, data.number); + const result = await this.sendMessageWithTyping( + data.number, + { + sticker: { url: convert }, + }, + data?.options, + ); + + fs.unlinkSync(convert); + this.logger.verbose('Converted image deleted'); + + return result; + } + + public async mediaMessage(data: SendMediaDto) { + this.logger.verbose('Sending media message'); + const generate = await this.prepareMediaMessage(data.mediaMessage); + + return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options); + } + + private async processAudio(audio: string, number: string) { + this.logger.verbose('Processing audio'); + let tempAudioPath: string; + let outputAudio: string; + + const hash = `${number}-${new Date().getTime()}`; + this.logger.verbose('Hash to audio name: ' + hash); + + if (isURL(audio)) { + this.logger.verbose('Audio is url'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const timestamp = new Date().getTime(); + const url = `${audio}?timestamp=${timestamp}`; + + this.logger.verbose('Including timestamp in url: ' + url); + + const response = await axios.get(url, { responseType: 'arraybuffer' }); + this.logger.verbose('Getting audio from url'); + + fs.writeFileSync(tempAudioPath, response.data); + } else { + this.logger.verbose('Audio is base64'); + + outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; + tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + + this.logger.verbose('Output audio path: ' + outputAudio); + this.logger.verbose('Temp audio path: ' + tempAudioPath); + + const audioBuffer = Buffer.from(audio, 'base64'); + fs.writeFileSync(tempAudioPath, audioBuffer); + this.logger.verbose('Temp audio created'); } - private async formatStatusMessage(status: StatusMessage) { - this.logger.verbose('Formatting status message'); + this.logger.verbose('Converting audio to mp4'); + return new Promise((resolve, reject) => { + exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { + fs.unlinkSync(tempAudioPath); + this.logger.verbose('Temp audio deleted'); - if (!status.type) { - throw new BadRequestException('Type is required'); - } + if (error) reject(error); - if (!status.content) { - throw new BadRequestException('Content is required'); - } + this.logger.verbose('Audio converted to mp4'); + resolve(outputAudio); + }); + }); + } - if (status.allContacts) { - this.logger.verbose('All contacts defined as true'); + public async audioWhatsapp(data: SendAudioDto) { + this.logger.verbose('Sending audio whatsapp'); - this.logger.verbose('Getting contacts from database'); - const contacts = await this.repository.contact.find({ - where: { owner: this.instance.name }, - }); - - if (!contacts.length) { - throw new BadRequestException('Contacts not found'); - } - - this.logger.verbose('Getting contacts with push name'); - status.statusJidList = contacts.filter((contact) => contact.pushName).map((contact) => contact.id); - - this.logger.verbose(status.statusJidList); - } - - if (!status.statusJidList?.length && !status.allContacts) { - throw new BadRequestException('StatusJidList is required'); - } - - if (status.type === 'text') { - this.logger.verbose('Type defined as text'); - - if (!status.backgroundColor) { - throw new BadRequestException('Background color is required'); - } - - if (!status.font) { - throw new BadRequestException('Font is required'); - } - - return { - content: { - text: status.content, - }, - option: { - backgroundColor: status.backgroundColor, - font: status.font, - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'image') { - this.logger.verbose('Type defined as image'); - - return { - content: { - image: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'video') { - this.logger.verbose('Type defined as video'); - - return { - content: { - video: { - url: status.content, - }, - caption: status.caption, - }, - option: { - statusJidList: status.statusJidList, - }, - }; - } - if (status.type === 'audio') { - this.logger.verbose('Type defined as audio'); - - this.logger.verbose('Processing audio'); - const convert = await this.processAudio(status.content, 'status@broadcast'); - if (typeof convert === 'string') { - this.logger.verbose('Audio processed'); - const audio = fs.readFileSync(convert).toString('base64'); - - const result = { - content: { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - option: { - statusJidList: status.statusJidList, - }, - }; - - fs.unlinkSync(convert); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - throw new BadRequestException('Type not found'); + if (!data.options?.encoding && data.options?.encoding !== false) { + data.options.encoding = true; } - public async statusMessage(data: SendStatusDto) { - this.logger.verbose('Sending status message'); - const status = await this.formatStatusMessage(data.statusMessage); - - return await this.sendMessageWithTyping('status@broadcast', { - status, - }); - } - - private async prepareMediaMessage(mediaMessage: MediaMessage) { - try { - this.logger.verbose('Preparing media message'); - const prepareMedia = await prepareWAMessageMedia( - { - [mediaMessage.mediatype]: isURL(mediaMessage.media) - ? { url: mediaMessage.media } - : Buffer.from(mediaMessage.media, 'base64'), - } as any, - { upload: this.client.waUploadToServer }, - ); - - const mediaType = mediaMessage.mediatype + 'Message'; - this.logger.verbose('Media type: ' + mediaType); - - if (mediaMessage.mediatype === 'document' && !mediaMessage.fileName) { - this.logger.verbose('If media type is document and file name is not defined then'); - const regex = new RegExp(/.*\/(.+?)\./); - const arrayMatch = regex.exec(mediaMessage.media); - mediaMessage.fileName = arrayMatch[1]; - this.logger.verbose('File name: ' + mediaMessage.fileName); - } - - let mimetype: string; - - if (isURL(mediaMessage.media)) { - mimetype = getMIMEType(mediaMessage.media); - } else { - mimetype = getMIMEType(mediaMessage.fileName); - } - - this.logger.verbose('Mimetype: ' + mimetype); - - prepareMedia[mediaType].caption = mediaMessage?.caption; - prepareMedia[mediaType].mimetype = mimetype; - prepareMedia[mediaType].fileName = mediaMessage.fileName; - - if (mediaMessage.mediatype === 'video') { - this.logger.verbose('Is media type video then set gif playback as false'); - prepareMedia[mediaType].jpegThumbnail = Uint8Array.from( - readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')), - ); - prepareMedia[mediaType].gifPlayback = false; - } - - this.logger.verbose('Generating wa message from content'); - return generateWAMessageFromContent( - '', - { [mediaType]: { ...prepareMedia[mediaType] } }, - { userJid: this.instance.wuid }, - ); - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException(error?.toString() || error); - } - } - - private async convertToWebP(image: string, number: string) { - try { - this.logger.verbose('Converting image to WebP to sticker'); - - let imagePath: string; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to image name: ' + hash); - - const outputPath = `${join(this.storePath, 'temp', `${hash}.webp`)}`; - this.logger.verbose('Output path: ' + outputPath); - - if (isBase64(image)) { - this.logger.verbose('Image is base64'); - - const base64Data = image.replace(/^data:image\/(jpeg|png|gif);base64,/, ''); - const imageBuffer = Buffer.from(base64Data, 'base64'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } else { - this.logger.verbose('Image is url'); - - const timestamp = new Date().getTime(); - const url = `${image}?timestamp=${timestamp}`; - this.logger.verbose('including timestamp in url: ' + url); - - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting image from url'); - - const imageBuffer = Buffer.from(response.data, 'binary'); - imagePath = `${join(this.storePath, 'temp', `temp-${hash}.png`)}`; - this.logger.verbose('Image path: ' + imagePath); - - await sharp(imageBuffer).toFile(imagePath); - this.logger.verbose('Image created'); - } - - await sharp(imagePath).webp().toFile(outputPath); - this.logger.verbose('Image converted to WebP'); - - fs.unlinkSync(imagePath); - this.logger.verbose('Temp image deleted'); - - return outputPath; - } catch (error) { - console.error('Erro ao converter a imagem para WebP:', error); - } - } - - public async mediaSticker(data: SendStickerDto) { - this.logger.verbose('Sending media sticker'); - const convert = await this.convertToWebP(data.stickerMessage.image, data.number); - const result = await this.sendMessageWithTyping( - data.number, - { - sticker: { url: convert }, - }, - data?.options, + if (data.options?.encoding) { + const convert = await this.processAudio(data.audioMessage.audio, data.number); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + const result = this.sendMessageWithTyping( + data.number, + { + audio: Buffer.from(audio, 'base64'), + ptt: true, + mimetype: 'audio/mp4', + }, + { presence: 'recording', delay: data?.options?.delay }, ); fs.unlinkSync(convert); - this.logger.verbose('Converted image deleted'); + this.logger.verbose('Converted audio deleted'); return result; + } else { + throw new InternalServerErrorException(convert); + } } - public async mediaMessage(data: SendMediaDto) { - this.logger.verbose('Sending media message'); - const generate = await this.prepareMediaMessage(data.mediaMessage); + return await this.sendMessageWithTyping( + data.number, + { + audio: isURL(data.audioMessage.audio) + ? { url: data.audioMessage.audio } + : Buffer.from(data.audioMessage.audio, 'base64'), + ptt: true, + mimetype: 'audio/ogg; codecs=opus', + }, + { presence: 'recording', delay: data?.options?.delay }, + ); + } - return await this.sendMessageWithTyping(data.number, { ...generate.message }, data?.options); + public async buttonMessage(data: SendButtonDto) { + this.logger.verbose('Sending button message'); + const embeddedMedia: any = {}; + let mediatype = 'TEXT'; + + if (data.buttonMessage?.mediaMessage) { + mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; + embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; + const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); + embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; + embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; } - private async processAudio(audio: string, number: string) { - this.logger.verbose('Processing audio'); - let tempAudioPath: string; - let outputAudio: string; + const btnItems = { + text: data.buttonMessage.buttons.map((btn) => btn.buttonText), + ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + }; - const hash = `${number}-${new Date().getTime()}`; - this.logger.verbose('Hash to audio name: ' + hash); + if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { + throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); + } - if (isURL(audio)) { - this.logger.verbose('Audio is url'); + return await this.sendMessageWithTyping( + data.number, + { + buttonsMessage: { + text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, + contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, + footerText: data.buttonMessage?.footerText, + buttons: data.buttonMessage.buttons.map((button) => { + return { + buttonText: { + displayText: button.buttonText, + }, + buttonId: button.buttonId, + type: 1, + }; + }), + headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], + [embeddedMedia?.mediaKey]: embeddedMedia?.message, + }, + }, + data?.options, + ); + } - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; + public async locationMessage(data: SendLocationDto) { + this.logger.verbose('Sending location message'); + return await this.sendMessageWithTyping( + data.number, + { + locationMessage: { + degreesLatitude: data.locationMessage.latitude, + degreesLongitude: data.locationMessage.longitude, + name: data.locationMessage?.name, + address: data.locationMessage?.address, + }, + }, + data?.options, + ); + } - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); + public async listMessage(data: SendListDto) { + this.logger.verbose('Sending list message'); + return await this.sendMessageWithTyping( + data.number, + { + listMessage: { + title: data.listMessage.title, + description: data.listMessage.description, + buttonText: data.listMessage?.buttonText, + footerText: data.listMessage?.footerText, + sections: data.listMessage.sections, + listType: 1, + }, + }, + data?.options, + ); + } - const timestamp = new Date().getTime(); - const url = `${audio}?timestamp=${timestamp}`; + public async contactMessage(data: SendContactDto) { + this.logger.verbose('Sending contact message'); + const message: proto.IMessage = {}; - this.logger.verbose('Including timestamp in url: ' + url); + const vcard = (contact: ContactMessage) => { + this.logger.verbose('Creating vcard'); + let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; - const response = await axios.get(url, { responseType: 'arraybuffer' }); - this.logger.verbose('Getting audio from url'); + if (contact.organization) { + this.logger.verbose('Organization defined'); + result += `ORG:${contact.organization};\n`; + } - fs.writeFileSync(tempAudioPath, response.data); + if (contact.email) { + this.logger.verbose('Email defined'); + result += `EMAIL:${contact.email}\n`; + } + + if (contact.url) { + this.logger.verbose('Url defined'); + result += `URL:${contact.url}\n`; + } + + if (!contact.wuid) { + this.logger.verbose('Wuid defined'); + contact.wuid = this.createJid(contact.phoneNumber); + } + + result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + + this.logger.verbose('Vcard created'); + return result; + }; + + if (data.contactMessage.length === 1) { + message.contactMessage = { + displayName: data.contactMessage[0].fullName, + vcard: vcard(data.contactMessage[0]), + }; + } else { + message.contactsArrayMessage = { + displayName: `${data.contactMessage.length} contacts`, + contacts: data.contactMessage.map((contact) => { + return { + displayName: contact.fullName, + vcard: vcard(contact), + }; + }), + }; + } + + return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + } + + public async reactionMessage(data: SendReactionDto) { + this.logger.verbose('Sending reaction message'); + return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { + reactionMessage: { + key: data.reactionMessage.key, + text: data.reactionMessage.reaction, + }, + }); + } + + // Chat Controller + public async whatsappNumber(data: WhatsAppNumberDto) { + this.logger.verbose('Getting whatsapp number'); + + const onWhatsapp: OnWhatsAppDto[] = []; + for await (const number of data.numbers) { + let jid = this.createJid(number); + + if (isJidGroup(jid)) { + const group = await this.findGroup({ groupJid: jid }, 'inner'); + + if (!group) throw new BadRequestException('Group not found'); + + onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); + } else { + jid = !jid.startsWith('+') ? `+${jid}` : jid; + const verify = await this.client.onWhatsApp(jid); + + const result = verify[0]; + + if (!result) { + onWhatsapp.push(new OnWhatsAppDto(jid, false)); } else { - this.logger.verbose('Audio is base64'); - - outputAudio = `${join(this.storePath, 'temp', `${hash}.mp4`)}`; - tempAudioPath = `${join(this.storePath, 'temp', `temp-${hash}.mp3`)}`; - - this.logger.verbose('Output audio path: ' + outputAudio); - this.logger.verbose('Temp audio path: ' + tempAudioPath); - - const audioBuffer = Buffer.from(audio, 'base64'); - fs.writeFileSync(tempAudioPath, audioBuffer); - this.logger.verbose('Temp audio created'); + onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); } + } + } + return onWhatsapp; + } + + public async markMessageAsRead(data: ReadMessageDto) { + this.logger.verbose('Marking message as read'); + try { + const keys: proto.IMessageKey[] = []; + data.read_messages.forEach((read) => { + if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { + keys.push({ + remoteJid: read.remoteJid, + fromMe: read.fromMe, + id: read.id, + }); + } + }); + await this.client.readMessages(keys); + return { message: 'Read messages', read: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Read messages fail', error.toString()); + } + } + + public async archiveChat(data: ArchiveChatDto) { + this.logger.verbose('Archiving chat'); + try { + data.lastMessage.messageTimestamp = data.lastMessage?.messageTimestamp ?? Date.now(); + await this.client.chatModify( + { + archive: data.archive, + lastMessages: [data.lastMessage], + }, + data.lastMessage.key.remoteJid, + ); + + return { + chatId: data.lastMessage.key.remoteJid, + archived: true, + }; + } catch (error) { + throw new InternalServerErrorException({ + archived: false, + message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], + }); + } + } + + public async deleteMessage(del: DeleteMessage) { + this.logger.verbose('Deleting message'); + try { + return await this.client.sendMessage(del.remoteJid, { delete: del }); + } catch (error) { + throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); + } + } + + public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { + this.logger.verbose('Getting base64 from media message'); + try { + const m = data?.message; + const convertToMp4 = data?.convertToMp4 ?? false; + + const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); + + if (!msg) { + throw 'Message not found'; + } + + for (const subtype of MessageSubtype) { + if (msg.message[subtype]) { + msg.message = msg.message[subtype].message; + } + } + + let mediaMessage: any; + let mediaType: string; + + for (const type of TypeMediaMessage) { + mediaMessage = msg.message[type]; + if (mediaMessage) { + mediaType = type; + break; + } + } + + if (!mediaMessage) { + throw 'The message is not of the media type'; + } + + if (typeof mediaMessage['mediaKey'] === 'object') { + msg.message = JSON.parse(JSON.stringify(msg.message)); + } + + this.logger.verbose('Downloading media message'); + const buffer = await downloadMediaMessage( + { key: msg?.key, message: msg?.message }, + 'buffer', + {}, + { + logger: P({ level: 'error' }), + reuploadRequest: this.client.updateMediaMessage, + }, + ); + const typeMessage = getContentType(msg.message); + + if (convertToMp4 && typeMessage === 'audioMessage') { this.logger.verbose('Converting audio to mp4'); - return new Promise((resolve, reject) => { - exec(`${ffmpegPath.path} -i ${tempAudioPath} -vn -ab 128k -ar 44100 -f ipod ${outputAudio} -y`, (error) => { - fs.unlinkSync(tempAudioPath); - this.logger.verbose('Temp audio deleted'); + const number = msg.key.remoteJid.split('@')[0]; + const convert = await this.processAudio(buffer.toString('base64'), number); - if (error) reject(error); + if (typeof convert === 'string') { + const audio = fs.readFileSync(convert).toString('base64'); + this.logger.verbose('Audio converted to mp4'); - this.logger.verbose('Audio converted to mp4'); - resolve(outputAudio); - }); - }); - } - - public async audioWhatsapp(data: SendAudioDto) { - this.logger.verbose('Sending audio whatsapp'); - - if (!data.options?.encoding && data.options?.encoding !== false) { - data.options.encoding = true; - } - - if (data.options?.encoding) { - const convert = await this.processAudio(data.audioMessage.audio, data.number); - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - const result = this.sendMessageWithTyping( - data.number, - { - audio: Buffer.from(audio, 'base64'), - ptt: true, - mimetype: 'audio/mp4', - }, - { presence: 'recording', delay: data?.options?.delay }, - ); - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - return result; - } else { - throw new InternalServerErrorException(convert); - } - } - - return await this.sendMessageWithTyping( - data.number, - { - audio: isURL(data.audioMessage.audio) - ? { url: data.audioMessage.audio } - : Buffer.from(data.audioMessage.audio, 'base64'), - ptt: true, - mimetype: 'audio/ogg; codecs=opus', + const result = { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], }, - { presence: 'recording', delay: data?.options?.delay }, - ); - } + mimetype: 'audio/mp4', + base64: Buffer.from(audio, 'base64').toString('base64'), + }; - public async buttonMessage(data: SendButtonDto) { - this.logger.verbose('Sending button message'); - const embeddedMedia: any = {}; - let mediatype = 'TEXT'; + fs.unlinkSync(convert); + this.logger.verbose('Converted audio deleted'); - if (data.buttonMessage?.mediaMessage) { - mediatype = data.buttonMessage.mediaMessage?.mediatype.toUpperCase() ?? 'TEXT'; - embeddedMedia.mediaKey = mediatype.toLowerCase() + 'Message'; - const generate = await this.prepareMediaMessage(data.buttonMessage.mediaMessage); - embeddedMedia.message = generate.message[embeddedMedia.mediaKey]; - embeddedMedia.contentText = `*${data.buttonMessage.title}*\n\n${data.buttonMessage.description}`; + this.logger.verbose('Media message downloaded'); + return result; } + } - const btnItems = { - text: data.buttonMessage.buttons.map((btn) => btn.buttonText), - ids: data.buttonMessage.buttons.map((btn) => btn.buttonId), + this.logger.verbose('Media message downloaded'); + return { + mediaType, + fileName: mediaMessage['fileName'], + caption: mediaMessage['caption'], + size: { + fileLength: mediaMessage['fileLength'], + height: mediaMessage['height'], + width: mediaMessage['width'], + }, + mimetype: mediaMessage['mimetype'], + base64: buffer.toString('base64'), + }; + } catch (error) { + this.logger.error(error); + throw new BadRequestException(error.toString()); + } + } + + public async fetchContacts(query: ContactQuery) { + this.logger.verbose('Fetching contacts'); + if (query?.where) { + query.where.owner = this.instance.name; + if (query.where?.id) { + query.where.id = this.createJid(query.where.id); + } + } else { + query = { + where: { + owner: this.instance.name, + }, + }; + } + return await this.repository.contact.find(query); + } + + public async fetchMessages(query: MessageQuery) { + this.logger.verbose('Fetching messages'); + if (query?.where) { + if (query.where?.key?.remoteJid) { + query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.message.find(query); + } + + public async fetchStatusMessage(query: MessageUpQuery) { + this.logger.verbose('Fetching status messages'); + if (query?.where) { + if (query.where?.remoteJid) { + query.where.remoteJid = this.createJid(query.where.remoteJid); + } + query.where.owner = this.instance.name; + } else { + query = { + where: { + owner: this.instance.name, + }, + limit: query?.limit, + }; + } + return await this.repository.messageUpdate.find(query); + } + + public async fetchChats() { + this.logger.verbose('Fetching chats'); + return await this.repository.chat.find({ where: { owner: this.instance.name } }); + } + + public async fetchPrivacySettings() { + this.logger.verbose('Fetching privacy settings'); + return await this.client.fetchPrivacySettings(); + } + + public async updatePrivacySettings(settings: PrivacySettingDto) { + this.logger.verbose('Updating privacy settings'); + try { + await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); + this.logger.verbose('Read receipts privacy updated'); + + await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); + this.logger.verbose('Profile picture privacy updated'); + + await this.client.updateStatusPrivacy(settings.privacySettings.status); + this.logger.verbose('Status privacy updated'); + + await this.client.updateOnlinePrivacy(settings.privacySettings.online); + this.logger.verbose('Online privacy updated'); + + await this.client.updateLastSeenPrivacy(settings.privacySettings.last); + this.logger.verbose('Last seen privacy updated'); + + await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); + this.logger.verbose('Groups add privacy updated'); + + this.client?.ws?.close(); + + return { + update: 'success', + data: { + readreceipts: settings.privacySettings.readreceipts, + profile: settings.privacySettings.profile, + status: settings.privacySettings.status, + online: settings.privacySettings.online, + last: settings.privacySettings.last, + groupadd: settings.privacySettings.groupadd, + }, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating privacy settings', error.toString()); + } + } + + public async fetchBusinessProfile(number: string): Promise { + this.logger.verbose('Fetching business profile'); + try { + const jid = number ? this.createJid(number) : this.instance.wuid; + + const profile = await this.client.getBusinessProfile(jid); + this.logger.verbose('Trying to get business profile'); + + if (!profile) { + const info = await this.whatsappNumber({ numbers: [jid] }); + + return { + isBusiness: false, + message: 'Not is business profile', + ...info?.shift(), + }; + } + + this.logger.verbose('Business profile fetched'); + return { + isBusiness: true, + ...profile, + }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileName(name: string) { + this.logger.verbose('Updating profile name to ' + name); + try { + await this.client.updateProfileName(name); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile name', error.toString()); + } + } + + public async updateProfileStatus(status: string) { + this.logger.verbose('Updating profile status to: ' + status); + try { + await this.client.updateProfileStatus(status); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile status', error.toString()); + } + } + + public async updateProfilePicture(picture: string) { + this.logger.verbose('Updating profile picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(this.instance.wuid, pic); + this.logger.verbose('Profile picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating profile picture', error.toString()); + } + } + + public async removeProfilePicture() { + this.logger.verbose('Removing profile picture'); + try { + await this.client.removeProfilePicture(this.instance.wuid); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error removing profile picture', error.toString()); + } + } + + // Group + public async createGroup(create: CreateGroupDto) { + this.logger.verbose('Creating group: ' + create.subject); + try { + const participants = create.participants.map((p) => this.createJid(p)); + const { id } = await this.client.groupCreate(create.subject, participants); + this.logger.verbose('Group created: ' + id); + + if (create?.description) { + this.logger.verbose('Updating group description: ' + create.description); + await this.client.groupUpdateDescription(id, create.description); + } + + if (create?.promoteParticipants) { + this.logger.verbose('Prometing group participants: ' + create.description); + await this.updateGParticipant({ + groupJid: id, + action: 'promote', + participants: participants, + }); + } + + const group = await this.client.groupMetadata(id); + this.logger.verbose('Getting group metadata'); + + return group; + } catch (error) { + this.logger.error(error); + throw new InternalServerErrorException('Error creating group', error.toString()); + } + } + + public async updateGroupPicture(picture: GroupPictureDto) { + this.logger.verbose('Updating group picture'); + try { + let pic: WAMediaUpload; + if (isURL(picture.image)) { + this.logger.verbose('Picture is url'); + + const timestamp = new Date().getTime(); + const url = `${picture.image}?timestamp=${timestamp}`; + this.logger.verbose('Including timestamp in url: ' + url); + + pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; + this.logger.verbose('Getting picture from url'); + } else if (isBase64(picture.image)) { + this.logger.verbose('Picture is base64'); + pic = Buffer.from(picture.image, 'base64'); + this.logger.verbose('Getting picture from base64'); + } else { + throw new BadRequestException('"profilePicture" must be a url or a base64'); + } + await this.client.updateProfilePicture(picture.groupJid, pic); + this.logger.verbose('Group picture updated'); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error update group picture', error.toString()); + } + } + + public async updateGroupSubject(data: GroupSubjectDto) { + this.logger.verbose('Updating group subject to: ' + data.subject); + try { + await this.client.groupUpdateSubject(data.groupJid, data.subject); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group subject', error.toString()); + } + } + + public async updateGroupDescription(data: GroupDescriptionDto) { + this.logger.verbose('Updating group description to: ' + data.description); + try { + await this.client.groupUpdateDescription(data.groupJid, data.description); + + return { update: 'success' }; + } catch (error) { + throw new InternalServerErrorException('Error updating group description', error.toString()); + } + } + + public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { + this.logger.verbose('Fetching group'); + try { + return await this.client.groupMetadata(id.groupJid); + } catch (error) { + if (reply === 'inner') { + return; + } + throw new NotFoundException('Error fetching group', error.toString()); + } + } + + public async fetchAllGroups(getParticipants: GetParticipant) { + this.logger.verbose('Fetching all groups'); + try { + const fetch = Object.values(await this.client.groupFetchAllParticipating()); + + const groups = fetch.map((group) => { + const result = { + id: group.id, + subject: group.subject, + subjectOwner: group.subjectOwner, + subjectTime: group.subjectTime, + size: group.size, + creation: group.creation, + owner: group.owner, + desc: group.desc, + descId: group.descId, + restrict: group.restrict, + announce: group.announce, }; - if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) { - throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.'); + if (getParticipants.getParticipants == 'true') { + result['participants'] = group.participants; } - return await this.sendMessageWithTyping( - data.number, - { - buttonsMessage: { - text: !embeddedMedia?.mediaKey ? data.buttonMessage.title : undefined, - contentText: embeddedMedia?.contentText ?? data.buttonMessage.description, - footerText: data.buttonMessage?.footerText, - buttons: data.buttonMessage.buttons.map((button) => { - return { - buttonText: { - displayText: button.buttonText, - }, - buttonId: button.buttonId, - type: 1, - }; - }), - headerType: proto.Message.ButtonsMessage.HeaderType[mediatype], - [embeddedMedia?.mediaKey]: embeddedMedia?.message, - }, - }, - data?.options, - ); + return result; + }); + + return groups; + } catch (error) { + throw new NotFoundException('Error fetching group', error.toString()); } + } - public async locationMessage(data: SendLocationDto) { - this.logger.verbose('Sending location message'); - return await this.sendMessageWithTyping( - data.number, - { - locationMessage: { - degreesLatitude: data.locationMessage.latitude, - degreesLongitude: data.locationMessage.longitude, - name: data.locationMessage?.name, - address: data.locationMessage?.address, - }, - }, - data?.options, - ); + public async inviteCode(id: GroupJid) { + this.logger.verbose('Fetching invite code for group: ' + id.groupJid); + try { + const code = await this.client.groupInviteCode(id.groupJid); + return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; + } catch (error) { + throw new NotFoundException('No invite code', error.toString()); } + } - public async listMessage(data: SendListDto) { - this.logger.verbose('Sending list message'); - return await this.sendMessageWithTyping( - data.number, - { - listMessage: { - title: data.listMessage.title, - description: data.listMessage.description, - buttonText: data.listMessage?.buttonText, - footerText: data.listMessage?.footerText, - sections: data.listMessage.sections, - listType: 1, - }, - }, - data?.options, - ); + public async inviteInfo(id: GroupInvite) { + this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); + try { + return await this.client.groupGetInviteInfo(id.inviteCode); + } catch (error) { + throw new NotFoundException('No invite info', id.inviteCode); } + } - public async contactMessage(data: SendContactDto) { - this.logger.verbose('Sending contact message'); - const message: proto.IMessage = {}; + public async sendInvite(id: GroupSendInvite) { + this.logger.verbose('Sending invite for group: ' + id.groupJid); + try { + const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); + this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); - const vcard = (contact: ContactMessage) => { - this.logger.verbose('Creating vcard'); - let result = 'BEGIN:VCARD\n' + 'VERSION:3.0\n' + `N:${contact.fullName}\n` + `FN:${contact.fullName}\n`; + const inviteUrl = inviteCode.inviteUrl; + this.logger.verbose('Invite url: ' + inviteUrl); - if (contact.organization) { - this.logger.verbose('Organization defined'); - result += `ORG:${contact.organization};\n`; - } + const numbers = id.numbers.map((number) => this.createJid(number)); + const description = id.description ?? ''; - if (contact.email) { - this.logger.verbose('Email defined'); - result += `EMAIL:${contact.email}\n`; - } + const msg = `${description}\n\n${inviteUrl}`; - if (contact.url) { - this.logger.verbose('Url defined'); - result += `URL:${contact.url}\n`; - } + const message = { + conversation: msg, + }; - if (!contact.wuid) { - this.logger.verbose('Wuid defined'); - contact.wuid = this.createJid(contact.phoneNumber); - } + for await (const number of numbers) { + await this.sendMessageWithTyping(number, message); + } - result += - `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD'; + this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - this.logger.verbose('Vcard created'); - return result; - }; - - if (data.contactMessage.length === 1) { - message.contactMessage = { - displayName: data.contactMessage[0].fullName, - vcard: vcard(data.contactMessage[0]), - }; - } else { - message.contactsArrayMessage = { - displayName: `${data.contactMessage.length} contacts`, - contacts: data.contactMessage.map((contact) => { - return { - displayName: contact.fullName, - vcard: vcard(contact), - }; - }), - }; - } - - return await this.sendMessageWithTyping(data.number, { ...message }, data?.options); + return { send: true, inviteUrl }; + } catch (error) { + throw new NotFoundException('No send invite'); } + } - public async reactionMessage(data: SendReactionDto) { - this.logger.verbose('Sending reaction message'); - return await this.sendMessageWithTyping(data.reactionMessage.key.remoteJid, { - reactionMessage: { - key: data.reactionMessage.key, - text: data.reactionMessage.reaction, - }, - }); + public async revokeInviteCode(id: GroupJid) { + this.logger.verbose('Revoking invite code for group: ' + id.groupJid); + try { + const inviteCode = await this.client.groupRevokeInvite(id.groupJid); + return { revoked: true, inviteCode }; + } catch (error) { + throw new NotFoundException('Revoke error', error.toString()); } + } - // Chat Controller - public async whatsappNumber(data: WhatsAppNumberDto) { - this.logger.verbose('Getting whatsapp number'); - - const onWhatsapp: OnWhatsAppDto[] = []; - for await (const number of data.numbers) { - let jid = this.createJid(number); - - if (isJidGroup(jid)) { - const group = await this.findGroup({ groupJid: jid }, 'inner'); - - if (!group) throw new BadRequestException('Group not found'); - - onWhatsapp.push(new OnWhatsAppDto(group.id, !!group?.id, group?.subject)); - } else { - jid = !jid.startsWith('+') ? `+${jid}` : jid; - const verify = await this.client.onWhatsApp(jid); - - const result = verify[0]; - - if (!result) { - onWhatsapp.push(new OnWhatsAppDto(jid, false)); - } else { - onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); - } - } - } - - return onWhatsapp; + public async findParticipants(id: GroupJid) { + this.logger.verbose('Fetching participants for group: ' + id.groupJid); + try { + const participants = (await this.client.groupMetadata(id.groupJid)).participants; + return { participants }; + } catch (error) { + throw new NotFoundException('No participants', error.toString()); } + } - public async markMessageAsRead(data: ReadMessageDto) { - this.logger.verbose('Marking message as read'); - try { - const keys: proto.IMessageKey[] = []; - data.read_messages.forEach((read) => { - if (isJidGroup(read.remoteJid) || isJidUser(read.remoteJid)) { - keys.push({ - remoteJid: read.remoteJid, - fromMe: read.fromMe, - id: read.id, - }); - } - }); - await this.client.readMessages(keys); - return { message: 'Read messages', read: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Read messages fail', error.toString()); - } + public async updateGParticipant(update: GroupUpdateParticipantDto) { + this.logger.verbose('Updating participants'); + try { + const participants = update.participants.map((p) => this.createJid(p)); + const updateParticipants = await this.client.groupParticipantsUpdate( + update.groupJid, + participants, + update.action, + ); + return { updateParticipants: updateParticipants }; + } catch (error) { + throw new BadRequestException('Error updating participants', error.toString()); } + } - public async archiveChat(data: ArchiveChatDto) { - this.logger.verbose('Archiving chat'); - try { - data.lastMessage.messageTimestamp = data.lastMessage?.messageTimestamp ?? Date.now(); - await this.client.chatModify( - { - archive: data.archive, - lastMessages: [data.lastMessage], - }, - data.lastMessage.key.remoteJid, - ); - - return { - chatId: data.lastMessage.key.remoteJid, - archived: true, - }; - } catch (error) { - throw new InternalServerErrorException({ - archived: false, - message: ['An error occurred while archiving the chat. Open a calling.', error.toString()], - }); - } + public async updateGSetting(update: GroupUpdateSettingDto) { + this.logger.verbose('Updating setting for group: ' + update.groupJid); + try { + const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); + return { updateSetting: updateSetting }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); } + } - public async deleteMessage(del: DeleteMessage) { - this.logger.verbose('Deleting message'); - try { - return await this.client.sendMessage(del.remoteJid, { delete: del }); - } catch (error) { - throw new InternalServerErrorException('Error while deleting message for everyone', error?.toString()); - } + public async toggleEphemeral(update: GroupToggleEphemeralDto) { + this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); + try { + await this.client.groupToggleEphemeral(update.groupJid, update.expiration); + return { success: true }; + } catch (error) { + throw new BadRequestException('Error updating setting', error.toString()); } + } - public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto) { - this.logger.verbose('Getting base64 from media message'); - try { - const m = data?.message; - const convertToMp4 = data?.convertToMp4 ?? false; - - const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); - - if (!msg) { - throw 'Message not found'; - } - - for (const subtype of MessageSubtype) { - if (msg.message[subtype]) { - msg.message = msg.message[subtype].message; - } - } - - let mediaMessage: any; - let mediaType: string; - - for (const type of TypeMediaMessage) { - mediaMessage = msg.message[type]; - if (mediaMessage) { - mediaType = type; - break; - } - } - - if (!mediaMessage) { - throw 'The message is not of the media type'; - } - - if (typeof mediaMessage['mediaKey'] === 'object') { - msg.message = JSON.parse(JSON.stringify(msg.message)); - } - - this.logger.verbose('Downloading media message'); - const buffer = await downloadMediaMessage( - { key: msg?.key, message: msg?.message }, - 'buffer', - {}, - { - logger: P({ level: 'error' }), - reuploadRequest: this.client.updateMediaMessage, - }, - ); - const typeMessage = getContentType(msg.message); - - if (convertToMp4 && typeMessage === 'audioMessage') { - this.logger.verbose('Converting audio to mp4'); - const number = msg.key.remoteJid.split('@')[0]; - const convert = await this.processAudio(buffer.toString('base64'), number); - - if (typeof convert === 'string') { - const audio = fs.readFileSync(convert).toString('base64'); - this.logger.verbose('Audio converted to mp4'); - - const result = { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: 'audio/mp4', - base64: Buffer.from(audio, 'base64').toString('base64'), - }; - - fs.unlinkSync(convert); - this.logger.verbose('Converted audio deleted'); - - this.logger.verbose('Media message downloaded'); - return result; - } - } - - this.logger.verbose('Media message downloaded'); - return { - mediaType, - fileName: mediaMessage['fileName'], - caption: mediaMessage['caption'], - size: { - fileLength: mediaMessage['fileLength'], - height: mediaMessage['height'], - width: mediaMessage['width'], - }, - mimetype: mediaMessage['mimetype'], - base64: buffer.toString('base64'), - }; - } catch (error) { - this.logger.error(error); - throw new BadRequestException(error.toString()); - } - } - - public async fetchContacts(query: ContactQuery) { - this.logger.verbose('Fetching contacts'); - if (query?.where) { - query.where.owner = this.instance.name; - if (query.where?.id) { - query.where.id = this.createJid(query.where.id); - } - } else { - query = { - where: { - owner: this.instance.name, - }, - }; - } - return await this.repository.contact.find(query); - } - - public async fetchMessages(query: MessageQuery) { - this.logger.verbose('Fetching messages'); - if (query?.where) { - if (query.where?.key?.remoteJid) { - query.where.key.remoteJid = this.createJid(query.where.key.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.message.find(query); - } - - public async fetchStatusMessage(query: MessageUpQuery) { - this.logger.verbose('Fetching status messages'); - if (query?.where) { - if (query.where?.remoteJid) { - query.where.remoteJid = this.createJid(query.where.remoteJid); - } - query.where.owner = this.instance.name; - } else { - query = { - where: { - owner: this.instance.name, - }, - limit: query?.limit, - }; - } - return await this.repository.messageUpdate.find(query); - } - - public async fetchChats() { - this.logger.verbose('Fetching chats'); - return await this.repository.chat.find({ where: { owner: this.instance.name } }); - } - - public async fetchPrivacySettings() { - this.logger.verbose('Fetching privacy settings'); - return await this.client.fetchPrivacySettings(); - } - - public async updatePrivacySettings(settings: PrivacySettingDto) { - this.logger.verbose('Updating privacy settings'); - try { - await this.client.updateReadReceiptsPrivacy(settings.privacySettings.readreceipts); - this.logger.verbose('Read receipts privacy updated'); - - await this.client.updateProfilePicturePrivacy(settings.privacySettings.profile); - this.logger.verbose('Profile picture privacy updated'); - - await this.client.updateStatusPrivacy(settings.privacySettings.status); - this.logger.verbose('Status privacy updated'); - - await this.client.updateOnlinePrivacy(settings.privacySettings.online); - this.logger.verbose('Online privacy updated'); - - await this.client.updateLastSeenPrivacy(settings.privacySettings.last); - this.logger.verbose('Last seen privacy updated'); - - await this.client.updateGroupsAddPrivacy(settings.privacySettings.groupadd); - this.logger.verbose('Groups add privacy updated'); - - this.client?.ws?.close(); - - return { - update: 'success', - data: { - readreceipts: settings.privacySettings.readreceipts, - profile: settings.privacySettings.profile, - status: settings.privacySettings.status, - online: settings.privacySettings.online, - last: settings.privacySettings.last, - groupadd: settings.privacySettings.groupadd, - }, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating privacy settings', error.toString()); - } - } - - public async fetchBusinessProfile(number: string): Promise { - this.logger.verbose('Fetching business profile'); - try { - const jid = number ? this.createJid(number) : this.instance.wuid; - - const profile = await this.client.getBusinessProfile(jid); - this.logger.verbose('Trying to get business profile'); - - if (!profile) { - const info = await this.whatsappNumber({ numbers: [jid] }); - - return { - isBusiness: false, - message: 'Not is business profile', - ...info?.shift(), - }; - } - - this.logger.verbose('Business profile fetched'); - return { - isBusiness: true, - ...profile, - }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileName(name: string) { - this.logger.verbose('Updating profile name to ' + name); - try { - await this.client.updateProfileName(name); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile name', error.toString()); - } - } - - public async updateProfileStatus(status: string) { - this.logger.verbose('Updating profile status to: ' + status); - try { - await this.client.updateProfileStatus(status); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile status', error.toString()); - } - } - - public async updateProfilePicture(picture: string) { - this.logger.verbose('Updating profile picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(this.instance.wuid, pic); - this.logger.verbose('Profile picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating profile picture', error.toString()); - } - } - - public async removeProfilePicture() { - this.logger.verbose('Removing profile picture'); - try { - await this.client.removeProfilePicture(this.instance.wuid); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error removing profile picture', error.toString()); - } - } - - // Group - public async createGroup(create: CreateGroupDto) { - this.logger.verbose('Creating group: ' + create.subject); - try { - const participants = create.participants.map((p) => this.createJid(p)); - const { id } = await this.client.groupCreate(create.subject, participants); - this.logger.verbose('Group created: ' + id); - - if (create?.description) { - this.logger.verbose('Updating group description: ' + create.description); - await this.client.groupUpdateDescription(id, create.description); - } - - if (create?.promoteParticipants) { - this.logger.verbose('Prometing group participants: ' + create.description); - await this.updateGParticipant({ - groupJid: id, - action: 'promote', - participants: participants, - }); - } - - const group = await this.client.groupMetadata(id); - this.logger.verbose('Getting group metadata'); - - return group; - } catch (error) { - this.logger.error(error); - throw new InternalServerErrorException('Error creating group', error.toString()); - } - } - - public async updateGroupPicture(picture: GroupPictureDto) { - this.logger.verbose('Updating group picture'); - try { - let pic: WAMediaUpload; - if (isURL(picture.image)) { - this.logger.verbose('Picture is url'); - - const timestamp = new Date().getTime(); - const url = `${picture.image}?timestamp=${timestamp}`; - this.logger.verbose('Including timestamp in url: ' + url); - - pic = (await axios.get(url, { responseType: 'arraybuffer' })).data; - this.logger.verbose('Getting picture from url'); - } else if (isBase64(picture.image)) { - this.logger.verbose('Picture is base64'); - pic = Buffer.from(picture.image, 'base64'); - this.logger.verbose('Getting picture from base64'); - } else { - throw new BadRequestException('"profilePicture" must be a url or a base64'); - } - await this.client.updateProfilePicture(picture.groupJid, pic); - this.logger.verbose('Group picture updated'); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error update group picture', error.toString()); - } - } - - public async updateGroupSubject(data: GroupSubjectDto) { - this.logger.verbose('Updating group subject to: ' + data.subject); - try { - await this.client.groupUpdateSubject(data.groupJid, data.subject); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group subject', error.toString()); - } - } - - public async updateGroupDescription(data: GroupDescriptionDto) { - this.logger.verbose('Updating group description to: ' + data.description); - try { - await this.client.groupUpdateDescription(data.groupJid, data.description); - - return { update: 'success' }; - } catch (error) { - throw new InternalServerErrorException('Error updating group description', error.toString()); - } - } - - public async findGroup(id: GroupJid, reply: 'inner' | 'out' = 'out') { - this.logger.verbose('Fetching group'); - try { - return await this.client.groupMetadata(id.groupJid); - } catch (error) { - if (reply === 'inner') { - return; - } - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async fetchAllGroups(getParticipants: GetParticipant) { - this.logger.verbose('Fetching all groups'); - try { - const fetch = Object.values(await this.client.groupFetchAllParticipating()); - - const groups = fetch.map((group) => { - const result = { - id: group.id, - subject: group.subject, - subjectOwner: group.subjectOwner, - subjectTime: group.subjectTime, - size: group.size, - creation: group.creation, - owner: group.owner, - desc: group.desc, - descId: group.descId, - restrict: group.restrict, - announce: group.announce, - }; - - if (getParticipants.getParticipants == 'true') { - result['participants'] = group.participants; - } - - return result; - }); - - return groups; - } catch (error) { - throw new NotFoundException('Error fetching group', error.toString()); - } - } - - public async inviteCode(id: GroupJid) { - this.logger.verbose('Fetching invite code for group: ' + id.groupJid); - try { - const code = await this.client.groupInviteCode(id.groupJid); - return { inviteUrl: `https://chat.whatsapp.com/${code}`, inviteCode: code }; - } catch (error) { - throw new NotFoundException('No invite code', error.toString()); - } - } - - public async inviteInfo(id: GroupInvite) { - this.logger.verbose('Fetching invite info for code: ' + id.inviteCode); - try { - return await this.client.groupGetInviteInfo(id.inviteCode); - } catch (error) { - throw new NotFoundException('No invite info', id.inviteCode); - } - } - - public async sendInvite(id: GroupSendInvite) { - this.logger.verbose('Sending invite for group: ' + id.groupJid); - try { - const inviteCode = await this.inviteCode({ groupJid: id.groupJid }); - this.logger.verbose('Getting invite code: ' + inviteCode.inviteCode); - - const inviteUrl = inviteCode.inviteUrl; - this.logger.verbose('Invite url: ' + inviteUrl); - - const numbers = id.numbers.map((number) => this.createJid(number)); - const description = id.description ?? ''; - - const msg = `${description}\n\n${inviteUrl}`; - - const message = { - conversation: msg, - }; - - for await (const number of numbers) { - await this.sendMessageWithTyping(number, message); - } - - this.logger.verbose('Invite sent for numbers: ' + numbers.join(', ')); - - return { send: true, inviteUrl }; - } catch (error) { - throw new NotFoundException('No send invite'); - } - } - - public async revokeInviteCode(id: GroupJid) { - this.logger.verbose('Revoking invite code for group: ' + id.groupJid); - try { - const inviteCode = await this.client.groupRevokeInvite(id.groupJid); - return { revoked: true, inviteCode }; - } catch (error) { - throw new NotFoundException('Revoke error', error.toString()); - } - } - - public async findParticipants(id: GroupJid) { - this.logger.verbose('Fetching participants for group: ' + id.groupJid); - try { - const participants = (await this.client.groupMetadata(id.groupJid)).participants; - return { participants }; - } catch (error) { - throw new NotFoundException('No participants', error.toString()); - } - } - - public async updateGParticipant(update: GroupUpdateParticipantDto) { - this.logger.verbose('Updating participants'); - try { - const participants = update.participants.map((p) => this.createJid(p)); - const updateParticipants = await this.client.groupParticipantsUpdate( - update.groupJid, - participants, - update.action, - ); - return { updateParticipants: updateParticipants }; - } catch (error) { - throw new BadRequestException('Error updating participants', error.toString()); - } - } - - public async updateGSetting(update: GroupUpdateSettingDto) { - this.logger.verbose('Updating setting for group: ' + update.groupJid); - try { - const updateSetting = await this.client.groupSettingUpdate(update.groupJid, update.action); - return { updateSetting: updateSetting }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async toggleEphemeral(update: GroupToggleEphemeralDto) { - this.logger.verbose('Toggling ephemeral for group: ' + update.groupJid); - try { - await this.client.groupToggleEphemeral(update.groupJid, update.expiration); - return { success: true }; - } catch (error) { - throw new BadRequestException('Error updating setting', error.toString()); - } - } - - public async leaveGroup(id: GroupJid) { - this.logger.verbose('Leaving group: ' + id.groupJid); - try { - await this.client.groupLeave(id.groupJid); - return { groupJid: id.groupJid, leave: true }; - } catch (error) { - throw new BadRequestException('Unable to leave the group', error.toString()); - } + public async leaveGroup(id: GroupJid) { + this.logger.verbose('Leaving group: ' + id.groupJid); + try { + await this.client.groupLeave(id.groupJid); + return { groupJid: id.groupJid, leave: true }; + } catch (error) { + throw new BadRequestException('Unable to leave the group', error.toString()); } + } } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 59e9d012..d4769a49 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -2,88 +2,88 @@ import { AuthenticationState, WAConnectionState } from '@whiskeysockets/baileys'; export enum Events { - APPLICATION_STARTUP = 'application.startup', - QRCODE_UPDATED = 'qrcode.updated', - CONNECTION_UPDATE = 'connection.update', - STATUS_INSTANCE = 'status.instance', - MESSAGES_SET = 'messages.set', - MESSAGES_UPSERT = 'messages.upsert', - MESSAGES_UPDATE = 'messages.update', - MESSAGES_DELETE = 'messages.delete', - SEND_MESSAGE = 'send.message', - CONTACTS_SET = 'contacts.set', - CONTACTS_UPSERT = 'contacts.upsert', - CONTACTS_UPDATE = 'contacts.update', - PRESENCE_UPDATE = 'presence.update', - CHATS_SET = 'chats.set', - CHATS_UPDATE = 'chats.update', - CHATS_UPSERT = 'chats.upsert', - CHATS_DELETE = 'chats.delete', - GROUPS_UPSERT = 'groups.upsert', - GROUPS_UPDATE = 'groups.update', - GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', - CALL = 'call', + APPLICATION_STARTUP = 'application.startup', + QRCODE_UPDATED = 'qrcode.updated', + CONNECTION_UPDATE = 'connection.update', + STATUS_INSTANCE = 'status.instance', + MESSAGES_SET = 'messages.set', + MESSAGES_UPSERT = 'messages.upsert', + MESSAGES_UPDATE = 'messages.update', + MESSAGES_DELETE = 'messages.delete', + SEND_MESSAGE = 'send.message', + CONTACTS_SET = 'contacts.set', + CONTACTS_UPSERT = 'contacts.upsert', + CONTACTS_UPDATE = 'contacts.update', + PRESENCE_UPDATE = 'presence.update', + CHATS_SET = 'chats.set', + CHATS_UPDATE = 'chats.update', + CHATS_UPSERT = 'chats.upsert', + CHATS_DELETE = 'chats.delete', + GROUPS_UPSERT = 'groups.upsert', + GROUPS_UPDATE = 'groups.update', + GROUP_PARTICIPANTS_UPDATE = 'group-participants.update', + CALL = 'call', } export declare namespace wa { - export type QrCode = { - count?: number; - pairingCode?: string; - base64?: string; - code?: string; - }; - export type Instance = { - qrcode?: QrCode; - pairingCode?: string; - authState?: { state: AuthenticationState; saveCreds: () => void }; - name?: string; - wuid?: string; - profileName?: string; - profilePictureUrl?: string; - }; + export type QrCode = { + count?: number; + pairingCode?: string; + base64?: string; + code?: string; + }; + export type Instance = { + qrcode?: QrCode; + pairingCode?: string; + authState?: { state: AuthenticationState; saveCreds: () => void }; + name?: string; + wuid?: string; + profileName?: string; + profilePictureUrl?: string; + }; - export type LocalWebHook = { - enabled?: boolean; - url?: string; - events?: string[]; - webhook_by_events?: boolean; - }; + export type LocalWebHook = { + enabled?: boolean; + url?: string; + events?: string[]; + webhook_by_events?: boolean; + }; - export type LocalChatwoot = { - enabled?: boolean; - account_id?: string; - token?: string; - url?: string; - name_inbox?: string; - sign_msg?: boolean; - number?: string; - reopen_conversation?: boolean; - conversation_pending?: boolean; - }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + number?: string; + reopen_conversation?: boolean; + conversation_pending?: boolean; + }; - export type LocalSettings = { - reject_call?: boolean; - msg_call?: string; - groups_ignore?: boolean; - always_online?: boolean; - read_messages?: boolean; - read_status?: boolean; - }; + export type LocalSettings = { + reject_call?: boolean; + msg_call?: string; + groups_ignore?: boolean; + always_online?: boolean; + read_messages?: boolean; + read_status?: boolean; + }; - export type StateConnection = { - instance?: string; - state?: WAConnectionState | 'refused'; - statusReason?: number; - }; + export type StateConnection = { + instance?: string; + state?: WAConnectionState | 'refused'; + statusReason?: number; + }; - export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; + export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; } export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage']; export const MessageSubtype = [ - 'ephemeralMessage', - 'documentWithCaptionMessage', - 'viewOnceMessage', - 'viewOnceMessageV2', + 'ephemeralMessage', + 'documentWithCaptionMessage', + 'viewOnceMessage', + 'viewOnceMessageV2', ]; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index f9a9456e..b742afa9 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -12,14 +12,14 @@ import { SettingsController } from './controllers/settings.controller'; import { ViewsController } from './controllers/views.controller'; import { WebhookController } from './controllers/webhook.controller'; import { - AuthModel, - ChatModel, - ChatwootModel, - ContactModel, - MessageModel, - MessageUpModel, - SettingsModel, - WebhookModel, + AuthModel, + ChatModel, + ChatwootModel, + ContactModel, + MessageModel, + MessageUpModel, + SettingsModel, + WebhookModel, } from './models'; import { AuthRepository } from './repository/auth.repository'; import { ChatRepository } from './repository/chat.repository'; @@ -48,16 +48,16 @@ const settingsRepository = new SettingsRepository(SettingsModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( - messageRepository, - chatRepository, - contactRepository, - messageUpdateRepository, - webhookRepository, - chatwootRepository, - settingsRepository, - authRepository, - configService, - dbserver?.getClient(), + messageRepository, + chatRepository, + contactRepository, + messageUpdateRepository, + webhookRepository, + chatwootRepository, + settingsRepository, + authRepository, + configService, + dbserver?.getClient(), ); export const cache = new RedisCache(); @@ -79,15 +79,15 @@ const settingsService = new SettingsService(waMonitor); export const settingsController = new SettingsController(settingsService); export const instanceController = new InstanceController( - waMonitor, - configService, - repository, - eventEmitter, - authService, - webhookService, - chatwootService, - settingsService, - cache, + waMonitor, + configService, + repository, + eventEmitter, + authService, + webhookService, + chatwootService, + settingsService, + cache, ); export const viewsController = new ViewsController(waMonitor, configService); export const sendMessageController = new SendMessageController(waMonitor); From 85ca0683ed8924a5b714fabcc86a3cefa5891324 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:15:09 -0300 Subject: [PATCH 75/97] fix: fixed bug of creating new inbox by chatwoot --- CHANGELOG.md | 4 ++++ src/whatsapp/services/chatwoot.service.ts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf5498ca..8ea22ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.4.6 (homolog) + +* Fixed bug of creating new inbox by chatwoot + # 1.4.5 (2023-07-26 09:32) ### Fixed diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 5e846019..f4d8477c 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -998,6 +998,8 @@ export class ChatwootService { chatwoot_token: this.provider.token, chatwoot_url: this.provider.url, chatwoot_sign_msg: this.provider.sign_msg, + chatwoot_reopen_conversation: this.provider.reopen_conversation, + chatwoot_conversation_pending: this.provider.conversation_pending, }; if (command.split(':')[2]) { @@ -1530,7 +1532,7 @@ export class ChatwootService { } // eslint-disable-next-line - const config = { + const config = { method: 'post', maxBodyLength: Infinity, url: `${urlServer}/instance/create`, From e7ed66603794a2542a3d38eb0f3631da6fb912d2 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 17:27:25 -0300 Subject: [PATCH 76/97] Update abstract.router.ts --- src/whatsapp/abstract/abstract.router.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index cb224cd6..c44c6880 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -98,22 +98,24 @@ export abstract class RouterBroker { public async groupValidate(args: DataValidate) { const { request, ClassRef, schema, execute } = args; - - const groupJid = request.query as unknown as GroupJid; - - if (!groupJid?.groupJid) { - throw new BadRequestException( - 'The group id needs to be informed in the query', - 'ex: "groupJid=120362@g.us"', - ); - } - const instance = request.params as unknown as InstanceDto; const body = request.body; + if (!body?.groupJid) { + if (request.query.groupJid) { + Object.assign(body, { + groupJid: request.query.groupJid + }); + } else { + throw new BadRequestException( + 'The group id needs to be informed in the query', + 'ex: "groupJid=120362@g.us"', + ); + } + } + const ref = new ClassRef(); - Object.assign(body, groupJid); Object.assign(ref, body); const v = validate(ref, schema); From 1b93aac8c55cef36800e73600381484327bd343a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:33:31 -0300 Subject: [PATCH 77/97] fix: When conversation reopens is pending when conversation pending is true --- CHANGELOG.md | 1 + package.json | 2 +- src/whatsapp/services/chatwoot.service.ts | 10 ++++++++++ src/whatsapp/services/whatsapp.service.ts | 4 ++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea22ae2..a4642d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 1.4.6 (homolog) * Fixed bug of creating new inbox by chatwoot +* When conversation reopens is pending when conversation pending is true # 1.4.5 (2023-07-26 09:32) diff --git a/package.json b/package.json index ac392703..f717738c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.5", + "version": "1.4.6", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index f4d8477c..0f2f46c8 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -507,6 +507,16 @@ export class ChatwootService { let conversation: any; if (this.provider.reopen_conversation) { conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); + + if (this.provider.conversation_pending) { + await client.conversations.toggleStatus({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + status: 'pending', + }, + }); + } } else { conversation = contactConversations.payload.find( (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 59245103..6de7d847 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -695,7 +695,7 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event STATUS_INSTANCE'); this.sendDataWebhook(Events.STATUS_INSTANCE, { instance: this.instance.name, - status: 'removed', + status: 'closed', }); if (this.localChatwoot.enabled) { @@ -704,7 +704,7 @@ export class WAStartupService { { instanceName: this.instance.name }, { instance: this.instance.name, - status: 'removed', + status: 'closed', }, ); } From e4548f696151d6cdb610ba2dfe4afc856f237385 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:39:32 -0300 Subject: [PATCH 78/97] fix: Added docker-compose file with dockerhub image --- docker-compose.yaml.example.dockerhub | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docker-compose.yaml.example.dockerhub diff --git a/docker-compose.yaml.example.dockerhub b/docker-compose.yaml.example.dockerhub new file mode 100644 index 00000000..6136997d --- /dev/null +++ b/docker-compose.yaml.example.dockerhub @@ -0,0 +1,28 @@ +version: '3.3' + +services: + api: + container_name: evolution_api + image: davidsongomes/evolution-api:latest + restart: always + ports: + - 8080:8080 + volumes: + - evolution_instances:/evolution/instances + - evolution_store:/evolution/store + networks: + - evolution-net + env_file: + - ./Docker/.env + command: ['node', './dist/src/main.js'] + expose: + - 8080 + +volumes: + evolution_instances: + evolution_store: + +networks: + evolution-net: + external: true + \ No newline at end of file From 312ee249b6b259a544ff3f41db10f8b9b4c9245a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:39:59 -0300 Subject: [PATCH 79/97] version: 1.4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4642d97..4f6766a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Fixed bug of creating new inbox by chatwoot * When conversation reopens is pending when conversation pending is true +* Added docker-compose file with dockerhub image # 1.4.5 (2023-07-26 09:32) From 73e92f9ef5311802accb57a30bcd404d4faa6255 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:41:06 -0300 Subject: [PATCH 80/97] version: 1.4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6766a9..ed0b7517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 1.4.6 (homolog) +### Fixed + * Fixed bug of creating new inbox by chatwoot * When conversation reopens is pending when conversation pending is true * Added docker-compose file with dockerhub image From af5746bb39054bb1c75b5dacf2238efc245aa239 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:45:28 -0300 Subject: [PATCH 81/97] Revert "GroupJid por Query ou por Body" --- src/whatsapp/abstract/abstract.router.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index d18cf36d..3c19e6bb 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -101,24 +101,18 @@ export abstract class RouterBroker { public async groupValidate(args: DataValidate) { const { request, ClassRef, schema, execute } = args; + const groupJid = request.query as unknown as GroupJid; + + if (!groupJid?.groupJid) { + throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"'); + } + const instance = request.params as unknown as InstanceDto; const body = request.body; - if (!body?.groupJid) { - if (request.query.groupJid) { - Object.assign(body, { - groupJid: request.query.groupJid - }); - } else { - throw new BadRequestException( - 'The group id needs to be informed in the query', - 'ex: "groupJid=120362@g.us"', - ); - } - } - const ref = new ClassRef(); + Object.assign(body, groupJid); Object.assign(ref, body); const v = validate(ref, schema); From 457dbe583198f34c4da7d423bd1abe9c0bc27ea9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:46:45 -0300 Subject: [PATCH 82/97] version: 1.4.6 --- src/whatsapp/abstract/abstract.router.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 3c19e6bb..bf1eee1b 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -101,18 +101,21 @@ export abstract class RouterBroker { public async groupValidate(args: DataValidate) { const { request, ClassRef, schema, execute } = args; - const groupJid = request.query as unknown as GroupJid; - - if (!groupJid?.groupJid) { - throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"'); - } - const instance = request.params as unknown as InstanceDto; const body = request.body; + if (!body?.groupJid) { + if (request.query.groupJid) { + Object.assign(body, { + groupJid: request.query.groupJid, + }); + } else { + throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"'); + } + } + const ref = new ClassRef(); - Object.assign(body, groupJid); Object.assign(ref, body); const v = validate(ref, schema); From 1bf2278f31a038ed8af36ee4647f16d401701274 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 17:54:29 -0300 Subject: [PATCH 83/97] version: 1.4.6 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0b7517..2a5235d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.6 (homolog) +# 1.4.6 (2023-07-26 17:54) ### Fixed From 28a7d9c62b90eeac30143a03ac077e246478ba71 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 18:02:45 -0300 Subject: [PATCH 84/97] fix: Adjusts in instance name --- src/whatsapp/controllers/instance.controller.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index e45644fe..64867a2d 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -55,19 +55,12 @@ export class InstanceController { try { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); - if (instanceName !== instanceName.toLowerCase().replace(/[^a-z0-9]/g, '')) { - throw new BadRequestException('The instance name must be lowercase and without special characters'); - } - this.logger.verbose('checking duplicate token'); await this.authService.checkDuplicateToken(token); this.logger.verbose('creating instance'); const instance = new WAStartupService(this.configService, this.eventEmitter, this.repository, this.cache); - instance.instanceName = instanceName - .toLowerCase() - .replace(/[^a-z0-9]/g, '') - .replace(' ', ''); + instance.instanceName = instanceName; this.logger.verbose('instance: ' + instance.instanceName + ' created'); From 9bdbfc6f4f403cf6c5deffcb02f2872bb4f55968 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Wed, 26 Jul 2023 18:28:18 -0300 Subject: [PATCH 85/97] wip --- src/whatsapp/abstract/abstract.router.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index d18cf36d..230c49af 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -104,11 +104,11 @@ export abstract class RouterBroker { const instance = request.params as unknown as InstanceDto; const body = request.body; - if (!body?.groupJid) { - if (request.query.groupJid) { - Object.assign(body, { - groupJid: request.query.groupJid - }); + let groupJid = body?.groupJid; + + if (!groupJid) { + if (request.query?.groupJid) { + groupJid = request.query.groupJid; } else { throw new BadRequestException( 'The group id needs to be informed in the query', @@ -117,6 +117,14 @@ export abstract class RouterBroker { } } + if (!groupJid.endsWith('@g.us')) { + groupJid = groupJid + '@g.us'; + } + + Object.assign(body, { + groupJid: groupJid + }); + const ref = new ClassRef(); Object.assign(ref, body); From 80e3116cd8e879db47bf1c429d4c270e8dc08813 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 26 Jul 2023 21:45:23 -0300 Subject: [PATCH 86/97] text: Fix problem No Session --- .DS_Store | Bin 6148 -> 6148 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.DS_Store b/.DS_Store index 11ffa2bd7bc24f00311a501886899f63ac042d9d..f07b5fd410a708405b4339fdbe299d449393688a 100644 GIT binary patch delta 32 ocmZoMXfc@J&&aVcU^gQp$7UWTX2#7+nUh#17EIgB&heKY0HA9M0ssI2 delta 172 zcmZoMXfc@J&&azmU^gQp?`9q*X2yCJh7yKUhFpe%oOHwB Date: Wed, 26 Jul 2023 21:45:49 -0300 Subject: [PATCH 87/97] text: Fix problem No Session --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 465c7324..db23ce9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.6", + "version": "1.4.7", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 127d5b97c405c67c5f66deb32c902e2c529ff859 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:36:18 -0300 Subject: [PATCH 88/97] fix: fixed error return bug --- CHANGELOG.md | 6 +++ src/exceptions/400.exception.ts | 1 + src/main.ts | 43 +++---------------- src/whatsapp/abstract/abstract.router.ts | 12 +++--- .../controllers/settings.controller.ts | 1 - src/whatsapp/repository/repository.manager.ts | 1 - 6 files changed, 20 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5235d4..5013ceaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.7 (2023-07-26 17:54) + +### Fixed + +* Fixed error return bug + # 1.4.6 (2023-07-26 17:54) ### Fixed diff --git a/src/exceptions/400.exception.ts b/src/exceptions/400.exception.ts index 833295c1..a9256bcb 100644 --- a/src/exceptions/400.exception.ts +++ b/src/exceptions/400.exception.ts @@ -2,6 +2,7 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class BadRequestException { constructor(...objectError: any[]) { + console.log('BadRequestException', objectError); throw { status: HttpStatus.BAD_REQUEST, error: 'Bad Request', diff --git a/src/main.ts b/src/main.ts index b0d2e03e..6e15f84d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,15 @@ -import 'express-async-errors'; - -// import * as Sentry from '@sentry/node'; import compression from 'compression'; +import { configService, Cors, HttpServer } from './config/env.config'; import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; - -import { configService, Cors, HttpServer } from './config/env.config'; import { onUnexpectedError } from './config/error.config'; import { Logger } from './config/logger.config'; import { ROOT_DIR } from './config/path.config'; -import { ServerUP } from './utils/server-up'; -import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; +import { HttpStatus, router } from './whatsapp/routers/index.router'; +import 'express-async-errors'; +import { ServerUP } from './utils/server-up'; function initWA() { waMonitor.loadInstance(); @@ -22,27 +19,6 @@ function bootstrap() { const logger = new Logger('SERVER'); const app = express(); - // Sentry.init({ - // dsn: '', - // integrations: [ - // // enable HTTP calls tracing - // new Sentry.Integrations.Http({ tracing: true }), - // // enable Express.js middleware tracing - // new Sentry.Integrations.Express({ app }), - // // Automatically instrument Node.js libraries and frameworks - // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), - // ], - - // // Set tracesSampleRate to 1.0 to capture 100% - // // of transactions for performance monitoring. - // // We recommend adjusting this value in production - // tracesSampleRate: 1.0, - // }); - - // app.use(Sentry.Handlers.requestHandler()); - - // app.use(Sentry.Handlers.tracingHandler()); - app.use( cors({ origin(requestOrigin, callback) { @@ -67,18 +43,13 @@ function bootstrap() { app.use('/', router); - // app.use(Sentry.Handlers.errorHandler()); - - // app.use(function onError(err, req, res, next) { - // res.statusCode = 500; - // res.end(res.sentry + '\n'); - // }); - app.use( - (err: Error, req: Request, res: Response) => { + (err: Error, req: Request, res: Response, next: NextFunction) => { if (err) { return res.status(err['status'] || 500).json(err); } + + next(); }, (req: Request, res: Response, next: NextFunction) => { const { method, url } = req; diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 136b946a..170b06fb 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -6,7 +6,7 @@ import { validate } from 'jsonschema'; import { Logger } from '../../config/logger.config'; import { BadRequestException } from '../../exceptions'; -import { GetParticipant, GroupInvite, GroupJid } from '../dto/group.dto'; +import { GetParticipant, GroupInvite } from '../dto/group.dto'; import { InstanceDto } from '../dto/instance.dto'; type DataValidate = { @@ -105,7 +105,7 @@ export abstract class RouterBroker { const body = request.body; let groupJid = body?.groupJid; - + if (!groupJid) { if (request.query?.groupJid) { groupJid = request.query.groupJid; @@ -113,15 +113,15 @@ export abstract class RouterBroker { throw new BadRequestException('The group id needs to be informed in the query', 'ex: "groupJid=120362@g.us"'); } } - + if (!groupJid.endsWith('@g.us')) { groupJid = groupJid + '@g.us'; } - + Object.assign(body, { - groupJid: groupJid + groupJid: groupJid, }); - + const ref = new ClassRef(); Object.assign(ref, body); diff --git a/src/whatsapp/controllers/settings.controller.ts b/src/whatsapp/controllers/settings.controller.ts index 32713b1f..1a8baafc 100644 --- a/src/whatsapp/controllers/settings.controller.ts +++ b/src/whatsapp/controllers/settings.controller.ts @@ -12,7 +12,6 @@ export class SettingsController { constructor(private readonly settingsService: SettingsService) {} public async createSettings(instance: InstanceDto, data: SettingsDto) { - logger.verbose('requested createSettings from ' + instance.instanceName + ' instance'); return this.settingsService.create(instance, data); diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index ae02849f..e1292329 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -109,7 +109,6 @@ export class RepositoryBroker { this.logger.verbose('creating temp dir: ' + tempDir); fs.mkdirSync(tempDir, { recursive: true }); } - } catch (error) { this.logger.error(error); } From 14f3f3d2ac930757a5b7d962df16d09809c8b55e Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:36:37 -0300 Subject: [PATCH 89/97] fix: fixed error return bug --- src/main.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 6e15f84d..a5b7fe8c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,15 +1,17 @@ +import 'express-async-errors'; + import compression from 'compression'; -import { configService, Cors, HttpServer } from './config/env.config'; import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; + +import { configService, Cors, HttpServer } from './config/env.config'; import { onUnexpectedError } from './config/error.config'; import { Logger } from './config/logger.config'; import { ROOT_DIR } from './config/path.config'; -import { waMonitor } from './whatsapp/whatsapp.module'; -import { HttpStatus, router } from './whatsapp/routers/index.router'; -import 'express-async-errors'; import { ServerUP } from './utils/server-up'; +import { HttpStatus, router } from './whatsapp/routers/index.router'; +import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { waMonitor.loadInstance(); From d3fce5fc89b648b3774ca9846bb137462138f23c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:45:02 -0300 Subject: [PATCH 90/97] fix: Fixed problem of getting message when deleting message in chatwoot --- CHANGELOG.md | 1 + src/whatsapp/services/chatwoot.service.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5013ceaf..97ea5c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Fixed * Fixed error return bug +* Fixed problem of getting message when deleting message in chatwoot # 1.4.6 (2023-07-26 17:54) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0f2f46c8..283a4198 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -930,6 +930,7 @@ export class ChatwootService { } public async receiveWebhook(instance: InstanceDto, body: any) { + console.log(body); try { this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); const client = await this.clientCw(instance); @@ -940,7 +941,7 @@ export class ChatwootService { } this.logger.verbose('check if is bot'); - if (!body?.conversation || body.private) return { message: 'bot' }; + if (!body?.conversation || body.private || body.event === 'message_updated') return { message: 'bot' }; this.logger.verbose('check if is group'); const chatId = From f74a7e87bdf4502c872311a43476537614fc0a88 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:48:03 -0300 Subject: [PATCH 91/97] version: 1.4.7 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ea5c42..85974fac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.4.7 (2023-07-26 17:54) +# 1.4.7 (2023-07-27 08:47) ### Fixed From f95d938fb677e0b5936903f57a392043d0eb16b9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:51:58 -0300 Subject: [PATCH 92/97] fix: Adjusts in return errors --- src/exceptions/400.exception.ts | 1 - src/main.ts | 103 ++++++++++++---------- src/whatsapp/services/chatwoot.service.ts | 1 - 3 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/exceptions/400.exception.ts b/src/exceptions/400.exception.ts index a9256bcb..833295c1 100644 --- a/src/exceptions/400.exception.ts +++ b/src/exceptions/400.exception.ts @@ -2,7 +2,6 @@ import { HttpStatus } from '../whatsapp/routers/index.router'; export class BadRequestException { constructor(...objectError: any[]) { - console.log('BadRequestException', objectError); throw { status: HttpStatus.BAD_REQUEST, error: 'Bad Request', diff --git a/src/main.ts b/src/main.ts index a5b7fe8c..8a14f746 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,68 +14,77 @@ import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { - waMonitor.loadInstance(); + waMonitor.loadInstance(); } function bootstrap() { - const logger = new Logger('SERVER'); - const app = express(); + const logger = new Logger('SERVER'); + const app = express(); - app.use( - cors({ - origin(requestOrigin, callback) { - const { ORIGIN } = configService.get('CORS'); - !requestOrigin ? (requestOrigin = '*') : undefined; - if (ORIGIN.indexOf(requestOrigin) !== -1) { - return callback(null, true); - } - return callback(new Error('Not allowed by CORS')); - }, - methods: [...configService.get('CORS').METHODS], - credentials: configService.get('CORS').CREDENTIALS, - }), - urlencoded({ extended: true, limit: '136mb' }), - json({ limit: '136mb' }), - compression(), - ); + app.use( + cors({ + origin(requestOrigin, callback) { + const { ORIGIN } = configService.get('CORS'); + !requestOrigin ? (requestOrigin = '*') : undefined; + if (ORIGIN.indexOf(requestOrigin) !== -1) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, + methods: [...configService.get('CORS').METHODS], + credentials: configService.get('CORS').CREDENTIALS, + }), + urlencoded({ extended: true, limit: '136mb' }), + json({ limit: '136mb' }), + compression(), + ); - app.set('view engine', 'hbs'); - app.set('views', join(ROOT_DIR, 'views')); - app.use(express.static(join(ROOT_DIR, 'public'))); + app.set('view engine', 'hbs'); + app.set('views', join(ROOT_DIR, 'views')); + app.use(express.static(join(ROOT_DIR, 'public'))); - app.use('/', router); + app.use('/', router); - app.use( - (err: Error, req: Request, res: Response, next: NextFunction) => { - if (err) { - return res.status(err['status'] || 500).json(err); - } + app.use( + (err: Error, req: Request, res: Response, next: NextFunction) => { + if (err) { + return res.status(err['status'] || 500).json({ + status: 'ERROR', + error: err['error'] || 'Internal Server Error', + response: { + message: err['message'] || 'Internal Server Error', + }, + } + ); + } - next(); - }, - (req: Request, res: Response, next: NextFunction) => { - const { method, url } = req; + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + const { method, url } = req; - res.status(HttpStatus.NOT_FOUND).json({ - status: HttpStatus.NOT_FOUND, - message: `Cannot ${method.toUpperCase()} ${url}`, - error: 'Not Found', - }); + res.status(HttpStatus.NOT_FOUND).json({ + status: HttpStatus.NOT_FOUND, + error: 'Not Found', + response: { + message: `Cannot ${method.toUpperCase()} ${url}`, + }, + }); - next(); - }, - ); + next(); + }, + ); - const httpServer = configService.get('SERVER'); + const httpServer = configService.get('SERVER'); - ServerUP.app = app; - const server = ServerUP[httpServer.TYPE]; + ServerUP.app = app; + const server = ServerUP[httpServer.TYPE]; - server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); + server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); - initWA(); + initWA(); - onUnexpectedError(); + onUnexpectedError(); } bootstrap(); diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 283a4198..23efc66f 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -930,7 +930,6 @@ export class ChatwootService { } public async receiveWebhook(instance: InstanceDto, body: any) { - console.log(body); try { this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); const client = await this.clientCw(instance); From 9af7f679300ac0d62fb65a6d03b1abad235291aa Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 08:52:45 -0300 Subject: [PATCH 93/97] version: 1.4.7 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85974fac..358e9ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Fixed error return bug * Fixed problem of getting message when deleting message in chatwoot +* Change in error return pattern # 1.4.6 (2023-07-26 17:54) From 332ec69ee8935b0a786e0ca407721afff6b6c6f2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 10:27:41 -0300 Subject: [PATCH 94/97] fix: Adjusts in return errors --- src/exceptions/401.exception.ts | 2 +- src/main.ts | 111 +++++++++--------- src/whatsapp/abstract/abstract.router.ts | 15 +-- .../controllers/instance.controller.ts | 12 +- src/whatsapp/routers/instance.router.ts | 4 +- 5 files changed, 73 insertions(+), 71 deletions(-) diff --git a/src/exceptions/401.exception.ts b/src/exceptions/401.exception.ts index 72734d4e..d5424c71 100644 --- a/src/exceptions/401.exception.ts +++ b/src/exceptions/401.exception.ts @@ -5,7 +5,7 @@ export class UnauthorizedException { throw { status: HttpStatus.UNAUTHORIZED, error: 'Unauthorized', - message: objectError.length > 0 ? objectError : undefined, + message: objectError.length > 0 ? objectError : 'Unauthorized', }; } } diff --git a/src/main.ts b/src/main.ts index 8a14f746..1077d64d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,77 +14,76 @@ import { HttpStatus, router } from './whatsapp/routers/index.router'; import { waMonitor } from './whatsapp/whatsapp.module'; function initWA() { - waMonitor.loadInstance(); + waMonitor.loadInstance(); } function bootstrap() { - const logger = new Logger('SERVER'); - const app = express(); + const logger = new Logger('SERVER'); + const app = express(); - app.use( - cors({ - origin(requestOrigin, callback) { - const { ORIGIN } = configService.get('CORS'); - !requestOrigin ? (requestOrigin = '*') : undefined; - if (ORIGIN.indexOf(requestOrigin) !== -1) { - return callback(null, true); - } - return callback(new Error('Not allowed by CORS')); - }, - methods: [...configService.get('CORS').METHODS], - credentials: configService.get('CORS').CREDENTIALS, - }), - urlencoded({ extended: true, limit: '136mb' }), - json({ limit: '136mb' }), - compression(), - ); + app.use( + cors({ + origin(requestOrigin, callback) { + const { ORIGIN } = configService.get('CORS'); + !requestOrigin ? (requestOrigin = '*') : undefined; + if (ORIGIN.indexOf(requestOrigin) !== -1) { + return callback(null, true); + } + return callback(new Error('Not allowed by CORS')); + }, + methods: [...configService.get('CORS').METHODS], + credentials: configService.get('CORS').CREDENTIALS, + }), + urlencoded({ extended: true, limit: '136mb' }), + json({ limit: '136mb' }), + compression(), + ); - app.set('view engine', 'hbs'); - app.set('views', join(ROOT_DIR, 'views')); - app.use(express.static(join(ROOT_DIR, 'public'))); + app.set('view engine', 'hbs'); + app.set('views', join(ROOT_DIR, 'views')); + app.use(express.static(join(ROOT_DIR, 'public'))); - app.use('/', router); + app.use('/', router); - app.use( - (err: Error, req: Request, res: Response, next: NextFunction) => { - if (err) { - return res.status(err['status'] || 500).json({ - status: 'ERROR', - error: err['error'] || 'Internal Server Error', - response: { - message: err['message'] || 'Internal Server Error', - }, - } - ); - } + app.use( + (err: Error, req: Request, res: Response, next: NextFunction) => { + if (err) { + return res.status(err['status'] || 500).json({ + status: err['status'] || 500, + error: err['error'] || 'Internal Server Error', + response: { + message: err['message'] || 'Internal Server Error', + }, + }); + } - next(); + next(); + }, + (req: Request, res: Response, next: NextFunction) => { + const { method, url } = req; + + res.status(HttpStatus.NOT_FOUND).json({ + status: HttpStatus.NOT_FOUND, + error: 'Not Found', + response: { + message: [`Cannot ${method.toUpperCase()} ${url}`], }, - (req: Request, res: Response, next: NextFunction) => { - const { method, url } = req; + }); - res.status(HttpStatus.NOT_FOUND).json({ - status: HttpStatus.NOT_FOUND, - error: 'Not Found', - response: { - message: `Cannot ${method.toUpperCase()} ${url}`, - }, - }); + next(); + }, + ); - next(); - }, - ); + const httpServer = configService.get('SERVER'); - const httpServer = configService.get('SERVER'); + ServerUP.app = app; + const server = ServerUP[httpServer.TYPE]; - ServerUP.app = app; - const server = ServerUP[httpServer.TYPE]; + server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); - server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT)); + initWA(); - initWA(); - - onUnexpectedError(); + onUnexpectedError(); } bootstrap(); diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index 170b06fb..7c603880 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -48,20 +48,21 @@ export abstract class RouterBroker { const v = schema ? validate(ref, schema) : { valid: true, errors: [] }; if (!v.valid) { - const message: any[] = v.errors.map(({ property, stack, schema }) => { + const message: any[] = v.errors.map(({ stack, schema }) => { let message: string; if (schema['description']) { message = schema['description']; } else { message = stack.replace('instance.', ''); } - return { - property: property.replace('instance.', ''), - message, - }; + return message; + // return { + // property: property.replace('instance.', ''), + // message, + // }; }); - logger.error([...message]); - throw new BadRequestException(...message); + logger.error(message); + throw new BadRequestException(message); } return await execute(instance, ref); diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 64867a2d..2b62af23 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -220,8 +220,8 @@ export class InstanceController { }, }; } catch (error) { - console.log(error); - return { error: true, message: error.toString() }; + this.logger.error(error.message[0]); + throw new BadRequestException(error.message[0]); } } @@ -269,7 +269,7 @@ export class InstanceController { this.logger.verbose('logging out instance: ' + instanceName); this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - return { error: false, message: 'Instance restarted' }; + return { status: 'SUCCESS', error: false, response: { message: 'Instance restarted' } }; } catch (error) { this.logger.error(error); } @@ -310,7 +310,7 @@ export class InstanceController { this.logger.verbose('close connection instance: ' + instanceName); this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); - return { error: false, message: 'Instance logged out' }; + return { status: 'SUCCESS', error: false, response: { message: 'Instance logged out' } }; } catch (error) { throw new InternalServerErrorException(error.toString()); } @@ -329,13 +329,13 @@ export class InstanceController { await this.logout({ instanceName }); delete this.waMonitor.waInstances[instanceName]; - return { error: false, message: 'Instance deleted' }; + return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; } else { this.logger.verbose('deleting instance: ' + instanceName); delete this.waMonitor.waInstances[instanceName]; this.eventEmitter.emit('remove.instance', instanceName, 'inner'); - return { error: false, message: 'Instance deleted' }; + return { status: 'SUCCESS', error: false, response: { message: 'Instance deleted' } }; } } catch (error) { throw new BadRequestException(error.toString()); diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index ae6d4066..20aa434c 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -161,7 +161,9 @@ export class InstanceRouter extends RouterBroker { if (db.ENABLED) { try { await dbserver.dropDatabase(); - return res.status(HttpStatus.CREATED).json({ error: false, message: 'Database deleted' }); + return res + .status(HttpStatus.CREATED) + .json({ status: 'SUCCESS', error: false, response: { message: 'database deleted' } }); } catch (error) { return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error: true, message: error.message }); } From 65e2ecf88ed186eb9c6283c6741ec59f16a42035 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 27 Jul 2023 10:28:26 -0300 Subject: [PATCH 95/97] version: 1.4.8 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358e9ccc..552a7d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.4.8 (2023-07-27 10:27) + +### Fixed + +* Fixed error return bug + # 1.4.7 (2023-07-27 08:47) ### Fixed diff --git a/package.json b/package.json index db23ce9f..54511a9c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.4.7", + "version": "1.4.8", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 3d02fabef423ac62177135a8c8376d6a1a8389b4 Mon Sep 17 00:00:00 2001 From: Alan Mosko Date: Thu, 27 Jul 2023 10:47:26 -0300 Subject: [PATCH 96/97] Update Dockerfile --- Dockerfile | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b3ac950..01f24799 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,17 @@ FROM node:16.18-alpine -LABEL version="1.1.3" description="Api to control whatsapp features through http requests." +LABEL version="1.4.7" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@agenciadgcode.com" RUN apk update && apk upgrade && \ - apk add --no-cache git + apk add --no-cache git tzdata ffmpeg wget curl WORKDIR /evolution COPY ./package.json . +ENV TZ=America/Sao_Paulo ENV DOCKER_ENV=true ENV SERVER_URL=http://localhost:8080 @@ -40,17 +41,17 @@ ENV DATABASE_ENABLED=false ENV DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true ENV DATABASE_CONNECTION_DB_PREFIX_NAME=evolution -ENV DATABASE_SAVE_DATA_INSTANCE=false -ENV DATABASE_SAVE_DATA_NEW_MESSAGE=false -ENV DATABASE_SAVE_MESSAGE_UPDATE=false -ENV DATABASE_SAVE_DATA_CONTACTS=false -ENV DATABASE_SAVE_DATA_CHATS=false +ENV DATABASE_SAVE_DATA_INSTANCE=true +ENV DATABASE_SAVE_DATA_NEW_MESSAGE=true +ENV DATABASE_SAVE_MESSAGE_UPDATE=true +ENV DATABASE_SAVE_DATA_CONTACTS=true +ENV DATABASE_SAVE_DATA_CHATS=true ENV REDIS_ENABLED=false ENV REDIS_URI=redis://redis:6379 ENV REDIS_PREFIX_KEY=evolution -ENV WEBHOOK_GLOBAL_URL= +ENV WEBHOOK_GLOBAL_URL= ENV WEBHOOK_GLOBAL_ENABLED=false ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false @@ -91,18 +92,13 @@ ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true ENV AUTHENTICATION_JWT_EXPIRIN_IN=0 ENV AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`' -ENV AUTHENTICATION_INSTANCE_MODE=server - -ENV AUTHENTICATION_INSTANCE_NAME=evolution -ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL= -ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 -ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 -ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL= - RUN npm install COPY . . RUN npm run build +HEALTHCHECK --interval=1m --retries=250 --start-period=2m \ + CMD curl --fail http://$SERVER_URL/ || exit 1 + CMD [ "node", "./dist/src/main.js" ] From 2614088faecc1e40d9887ce3f88b1c85c3622d0b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 31 Jul 2023 15:25:48 -0300 Subject: [PATCH 97/97] Revert "Update Dockerfile" --- Dockerfile | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01f24799..0b3ac950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,16 @@ FROM node:16.18-alpine -LABEL version="1.4.7" description="Api to control whatsapp features through http requests." +LABEL version="1.1.3" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@agenciadgcode.com" RUN apk update && apk upgrade && \ - apk add --no-cache git tzdata ffmpeg wget curl + apk add --no-cache git WORKDIR /evolution COPY ./package.json . -ENV TZ=America/Sao_Paulo ENV DOCKER_ENV=true ENV SERVER_URL=http://localhost:8080 @@ -41,17 +40,17 @@ ENV DATABASE_ENABLED=false ENV DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true ENV DATABASE_CONNECTION_DB_PREFIX_NAME=evolution -ENV DATABASE_SAVE_DATA_INSTANCE=true -ENV DATABASE_SAVE_DATA_NEW_MESSAGE=true -ENV DATABASE_SAVE_MESSAGE_UPDATE=true -ENV DATABASE_SAVE_DATA_CONTACTS=true -ENV DATABASE_SAVE_DATA_CHATS=true +ENV DATABASE_SAVE_DATA_INSTANCE=false +ENV DATABASE_SAVE_DATA_NEW_MESSAGE=false +ENV DATABASE_SAVE_MESSAGE_UPDATE=false +ENV DATABASE_SAVE_DATA_CONTACTS=false +ENV DATABASE_SAVE_DATA_CHATS=false ENV REDIS_ENABLED=false ENV REDIS_URI=redis://redis:6379 ENV REDIS_PREFIX_KEY=evolution -ENV WEBHOOK_GLOBAL_URL= +ENV WEBHOOK_GLOBAL_URL= ENV WEBHOOK_GLOBAL_ENABLED=false ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false @@ -92,13 +91,18 @@ ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true ENV AUTHENTICATION_JWT_EXPIRIN_IN=0 ENV AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`' +ENV AUTHENTICATION_INSTANCE_MODE=server + +ENV AUTHENTICATION_INSTANCE_NAME=evolution +ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL= +ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 +ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 +ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL= + RUN npm install COPY . . RUN npm run build -HEALTHCHECK --interval=1m --retries=250 --start-period=2m \ - CMD curl --fail http://$SERVER_URL/ || exit 1 - CMD [ "node", "./dist/src/main.js" ]