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);