diff --git a/CHANGELOG.md b/CHANGELOG.md index 0afab659..4f8e26f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +# 1.3.0 (2023-07-19 11:33) + +### Features + +* Added messages.delete event +* Added restart instance endpoint +* Created automation for creating instances in the chatwoot bot with the command '#inbox_whatsapp:' +* Change Baileys version to: 6.4.0 +* Send contact in chatwoot +* Send contact array in chatwoot +* Added apiKey in webhook and serverUrl in fetchInstance if EXPOSE_IN_FETCH_INSTANCES: true +* Translation set to default (english) in chatwoot + +### Fixed + +* Fixed error to send message in large groups +* Docker files adjusted +* Fixed in the postman collection the webhookByEvent parameter by webhook_by_events +* Added validations in create instance +* Removed link preview endpoint, now it's done automatically from sending conventional text +* Added group membership validation before sending message to groups +* Adjusts in docker files +* Adjusts in returns in endpoints chatwoot and webhook +* Fixed ghost mentions in send text message +* Fixed bug that saved contacts from groups came without number in chatwoot +* Fixed problem to receive csat in chatwoot +* Fixed require fileName for document only in base64 for send media message +* Bug fix when sending mobile message change contact name to number in chatwoot +* Bug fix when connecting whatsapp does not send confirmation message +* Fixed quoted message with id or message directly +* Adjust in validation for mexican and argentine numbers +* Adjust in create store files + +### Integrations + +- Chatwoot: v2.18.0 + # 1.2.2 (2023-07-15 09:36) ### Fixed diff --git a/Docker/.env.example b/Docker/.env.example index 8913fac5..81eee8ca 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -59,6 +59,7 @@ WEBHOOK_EVENTS_QRCODE_UPDATED=true WEBHOOK_EVENTS_MESSAGES_SET=true WEBHOOK_EVENTS_MESSAGES_UPSERT=true WEBHOOK_EVENTS_MESSAGES_UPDATE=true +WEBHOOK_EVENTS_MESSAGES_DELETE=true WEBHOOK_EVENTS_SEND_MESSAGE=true WEBHOOK_EVENTS_CONTACTS_SET=true WEBHOOK_EVENTS_CONTACTS_UPSERT=true diff --git a/Docker/mongodb/docker-compose.yaml b/Docker/mongodb/docker-compose.yaml index 957db3ea..698ca50c 100644 --- a/Docker/mongodb/docker-compose.yaml +++ b/Docker/mongodb/docker-compose.yaml @@ -15,8 +15,6 @@ services: volumes: - evolution_mongodb_data:/data/db - evolution_mongodb_configdb:/data/configdb - networks: - - evolution-net expose: - 27017 @@ -38,6 +36,6 @@ volumes: evolution_mongodb_configdb: networks: - default: - name: evolution-net + evolution-net: + external: true \ No newline at end of file diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index 753bb9cb..6409b851 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -24,5 +24,5 @@ volumes: evolution_redis: networks: - default: - name: evolution-net + evolution-net: + external: true diff --git a/Dockerfile b/Dockerfile index a7e558f3..088f6fa1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,86 +13,90 @@ COPY ./package.json . ENV DOCKER_ENV=true -ENV SERVER_TYPE="http" -ENV SERVER_PORT=8080 -ENV SERVER_URL=$SERVER_URL +ENV SERVER_URL='http://localhost:8080' -ENV CORS_ORIGIN="*" -ENV CORS_METHODS="POST,GET,PUT,DELETE" +ENV CORS_ORIGIN=* +ENV CORS_METHODS=POST,GET,PUT,DELETE ENV CORS_CREDENTIALS=true -ENV LOG_LEVEL=$LOG_LEVEL -ENV LOG_COLOR=$LOG_COLOR +ENV LOG_LEVEL=ERROR,WARN,DEBUG,INFO,LOG,VERBOSE,DARK,WEBHOOKS +ENV LOG_COLOR=true +ENV LOG_BAILEYS=error -ENV DEL_INSTANCE=$DEL_INSTANCE +ENV DEL_INSTANCE=false -ENV STORE_MESSAGES=$STORE_MESSAGE -ENV STORE_MESSAGE_UP=$STORE_MESSAGE_UP -ENV STORE_CONTACTS=$STORE_CONTACTS -ENV STORE_CHATS=$STORE_CHATS +ENV STORE_MESSAGES=true +ENV STORE_MESSAGE_UP=true +ENV STORE_CONTACTS=true +ENV STORE_CHATS=true -ENV CLEAN_STORE_CLEANING_INTERVAL=$CLEAN_STORE_CLEANING_INTERVAL -ENV CLEAN_STORE_MESSAGES=$CLEAN_STORE_MESSAGE -ENV CLEAN_STORE_MESSAGE_UP=$CLEAN_STORE_MESSAGE_UP -ENV CLEAN_STORE_CONTACTS=$CLEAN_STORE_CONTACTS -ENV CLEAN_STORE_CHATS=$CLEAN_STORE_CHATS +ENV CLEAN_STORE_CLEANING_INTERVAL=7200 +ENV CLEAN_STORE_MESSAGES=true +ENV CLEAN_STORE_MESSAGE_UP=true +ENV CLEAN_STORE_CONTACTS=true +ENV CLEAN_STORE_CHATS=true -ENV DATABASE_ENABLED=$DATABASE_ENABLED -ENV DATABASE_CONNECTION_URI=$DATABASE_CONNECTION_URI -ENV DATABASE_CONNECTION_DB_PREFIX_NAME=$DATABASE_CONNECTION_DB_PREFIX_NAME -ENV DATABASE_SAVE_DATA_INSTANCE=$DATABASE_SAVE_DATA_INSTANCE -ENV DATABASE_SAVE_DATA_NEW_MESSAGE=$DATABASE_SAVE_DATA_NEW_MESSAGE -ENV DATABASE_SAVE_MESSAGE_UPDATE=$DATABASE_SAVE_MESSAGE_UPDATE -ENV DATABASE_SAVE_DATA_CONTACTS=$DATABASE_SAVE_DATA_CONTACTS -ENV DATABASE_SAVE_DATA_CHATS=$DATABASE_SAVE_DATA_CHATS +ENV DATABASE_ENABLED=false +ENV DATABASE_CONNECTION_URI=mongodb://root:root@mongodb:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true +ENV DATABASE_CONNECTION_DB_PREFIX_NAME=evolution -ENV REDIS_ENABLED=$REDIS_ENABLED -ENV REDIS_URI=$REDIS_URI -ENV REDIS_PREFIX_KEY=$REDIS_PREFIX_KEY +ENV DATABASE_SAVE_DATA_INSTANCE=false +ENV DATABASE_SAVE_DATA_NEW_MESSAGE=false +ENV DATABASE_SAVE_MESSAGE_UPDATE=false +ENV DATABASE_SAVE_DATA_CONTACTS=false +ENV DATABASE_SAVE_DATA_CHATS=false -ENV WEBHOOK_GLOBAL_URL=$WEBHOOK_GLOBAL_URL -ENV WEBHOOK_GLOBAL_ENABLED=$WEBHOOK_GLOBAL_ENABLED -ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=$WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS +ENV REDIS_ENABLED=false +ENV REDIS_URI=redis://redis:6379 +ENV REDIS_PREFIX_KEY=evolution -ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=$WEBHOOK_EVENTS_APPLICATION_STARTUP -ENV WEBHOOK_EVENTS_QRCODE_UPDATED=$WEBHOOK_EVENTS_QRCODE_UPDATED -ENV WEBHOOK_EVENTS_MESSAGES_SET=$WEBHOOK_EVENTS_MESSAGES_SET -ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=$WEBHOOK_EVENTS_MESSAGES_UPSERT -ENV WEBHOOK_EVENTS_MESSAGES_UPDATE=$WEBHOOK_EVENTS_MESSAGES_UPDATE -ENV WEBHOOK_EVENTS_SEND_MESSAGE=$WEBHOOK_EVENTS_SEND_MESSAGE -ENV WEBHOOK_EVENTS_CONTACTS_SET=$WEBHOOK_EVENTS_CONTACTS_SET -ENV WEBHOOK_EVENTS_CONTACTS_UPSERT=$WEBHOOK_EVENTS_CONTACTS_UPSERT -ENV WEBHOOK_EVENTS_CONTACTS_UPDATE=$WEBHOOK_EVENTS_CONTACTS_UPDATE -ENV WEBHOOK_EVENTS_PRESENCE_UPDATE=$WEBHOOK_EVENTS_PRESENCE_UPDATE -ENV WEBHOOK_EVENTS_CHATS_SET=$WEBHOOK_EVENTS_CHATS_SET -ENV WEBHOOK_EVENTS_CHATS_UPSERT=$WEBHOOK_EVENTS_CHATS_UPSERT -ENV WEBHOOK_EVENTS_CHATS_UPDATE=$WEBHOOK_EVENTS_CHATS_UPDATE -ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=$WEBHOOK_EVENTS_CONNECTION_UPDATE -ENV WEBHOOK_EVENTS_GROUPS_UPSERT=$WEBHOOK_EVENTS_GROUPS_UPSERT -ENV WEBHOOK_EVENTS_GROUPS_UPDATE=$WEBHOOK_EVENTS_GROUPS_UPDATE -ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=$WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE +ENV WEBHOOK_GLOBAL_URL= +ENV WEBHOOK_GLOBAL_ENABLED=false -ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=$WEBHOOK_EVENTS_NEW_JWT_TOKEN +ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false -ENV CONFIG_SESSION_PHONE_CLIENT=$CONFIG_SESSION_PHONE_CLIENT -ENV CONFIG_SESSION_PHONE_NAME=$CONFIG_SESSION_PHONE_NAME +ENV WEBHOOK_EVENTS_APPLICATION_STARTUP=false +ENV WEBHOOK_EVENTS_QRCODE_UPDATED=true +ENV WEBHOOK_EVENTS_MESSAGES_SET=true +ENV WEBHOOK_EVENTS_MESSAGES_UPSERT=true +ENV WEBHOOK_EVENTS_MESSAGES_UPDATE=true +ENV WEBHOOK_EVENTS_MESSAGES_DELETE=true +ENV WEBHOOK_EVENTS_SEND_MESSAGE=true +ENV WEBHOOK_EVENTS_CONTACTS_SET=true +ENV WEBHOOK_EVENTS_CONTACTS_UPSERT=true +ENV WEBHOOK_EVENTS_CONTACTS_UPDATE=true +ENV WEBHOOK_EVENTS_PRESENCE_UPDATE=true +ENV WEBHOOK_EVENTS_CHATS_SET=true +ENV WEBHOOK_EVENTS_CHATS_UPSERT=true +ENV WEBHOOK_EVENTS_CHATS_UPDATE=true +ENV WEBHOOK_EVENTS_CHATS_DELETE=true +ENV WEBHOOK_EVENTS_GROUPS_UPSERT=true +ENV WEBHOOK_EVENTS_GROUPS_UPDATE=true +ENV WEBHOOK_EVENTS_GROUP_PARTICIPANTS_UPDATE=true +ENV WEBHOOK_EVENTS_CONNECTION_UPDATE=true -ENV QRCODE_LIMIT=$QRCODE_LIMIT +ENV WEBHOOK_EVENTS_NEW_JWT_TOKEN=false -ENV AUTHENTICATION_TYPE=$AUTHENTICATION_TYPE +ENV CONFIG_SESSION_PHONE_CLIENT='Evolution API' +ENV CONFIG_SESSION_PHONE_NAME=chrome -ENV AUTHENTICATION_API_KEY=$AUTHENTICATION_API_KEY -ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=$AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES +ENV QRCODE_LIMIT=30 -ENV AUTHENTICATION_JWT_EXPIRIN_IN=$AUTHENTICATION_JWT_EXPIRIN_IN -ENV AUTHENTICATION_JWT_SECRET="L=0YWt]b2w[WF>#>:&E`" +ENV AUTHENTICATION_TYPE=apikey -ENV AUTHENTICATION_INSTANCE_NAME=$AUTHENTICATION_INSTANCE_NAME -ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL=$AUTHENTICATION_INSTANCE_WEBHOOK_URL -ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=$AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID -ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=$AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN -ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL=$AUTHENTICATION_INSTANCE_CHATWOOT_URL -ENV AUTHENTICATION_INSTANCE_MODE=$AUTHENTICATION_INSTANCE_MODE +ENV AUTHENTICATION_API_KEY=B6D711FCDE4D4FD5936544120E713976 +ENV AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true + +ENV AUTHENTICATION_JWT_EXPIRIN_IN=0 +ENV AUTHENTICATION_JWT_SECRET='L=0YWt]b2w[WF>#>:&E`' + +ENV AUTHENTICATION_INSTANCE_MODE=server + +ENV AUTHENTICATION_INSTANCE_NAME=evolution +ENV AUTHENTICATION_INSTANCE_WEBHOOK_URL= +ENV AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 +ENV AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 +ENV AUTHENTICATION_INSTANCE_CHATWOOT_URL= RUN npm install diff --git a/docker-compose-full.yaml b/docker-compose-full.yaml deleted file mode 100644 index 7c5036da..00000000 --- a/docker-compose-full.yaml +++ /dev/null @@ -1,72 +0,0 @@ -version: '3.3' - -services: - redis: - image: redis:latest - container_name: redis - ports: - - 6379:6379 - - rebrow: - image: marian/rebrow - ports: - - 5001:5001 - links: - - redis - - mongodb: - container_name: mongodb - image: mongo - restart: always - volumes: - - evolution_mongodb_data:/data/db - - evolution_mongodb_configdb:/data/configdb - ports: - - 27017:27017 - environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: root - expose: - - 27017 - - mongo-express: - image: mongo-express - environment: - ME_CONFIG_BASICAUTH_USERNAME: root - ME_CONFIG_BASICAUTH_PASSWORD: root - ME_CONFIG_MONGODB_SERVER: mongodb - ME_CONFIG_MONGODB_ADMINUSERNAME: root - ME_CONFIG_MONGODB_ADMINPASSWORD: root - ports: - - 8081:8081 - links: - - mongodb - api: - container_name: evolution_api - image: evolution/api:local - restart: always - ports: - - 8080:8080 - volumes: - - evolution_instances:/evolution/instances - - evolution_store:/evolution/store - env_file: - - ./Docker/.env - command: ['node', './dist/src/main.js'] - expose: - - 8080 - links: - - mongodb - - redis - -volumes: - evolution_instances: - evolution_store: - evolution_mongodb_data: - evolution_mongodb_configdb: - evolution_redis: - -networks: - default: - name: evolution-net - \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index c62e6ff4..c6d1bc73 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,6 +21,6 @@ volumes: evolution_store: networks: - default: - name: evolution-net + evolution-net: + external: true \ No newline at end of file diff --git a/package.json b/package.json index 1f313a5c..5c182838 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.2.2", + "version": "1.3.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -44,7 +44,8 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@figuro/chatwoot-sdk": "^1.1.14", "@hapi/boom": "^10.0.1", - "@whiskeysockets/baileys": "github:DavidsonGomes/Baileys", + "@sentry/node": "^7.59.2", + "@whiskeysockets/baileys": "^6.4.0", "axios": "^1.3.5", "class-validator": "^0.13.2", "compression": "^1.7.4", diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 76bb55ea..7221f474 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -1,7 +1,6 @@ import { readFileSync } from 'fs'; import { load } from 'js-yaml'; import { join } from 'path'; -import { SRC_DIR } from './path.config'; import { isBooleanString } from 'class-validator'; export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; @@ -76,6 +75,7 @@ export type EventsWebhook = { MESSAGES_SET: boolean; MESSAGES_UPSERT: boolean; MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; SEND_MESSAGE: boolean; CONTACTS_SET: boolean; CONTACTS_UPDATE: boolean; @@ -98,9 +98,9 @@ export type Instance = { NAME: string; WEBHOOK_URL: string; MODE: string; - CHATWOOT_ACCOUNT_ID?: string; - CHATWOOT_TOKEN?: string; - CHATWOOT_URL?: string; + CHATWOOT_ACCOUNT_ID: string; + CHATWOOT_TOKEN: string; + CHATWOOT_URL: string; }; export type Auth = { API_KEY: ApiKey; @@ -193,7 +193,7 @@ export class ConfigService { CLEAN_STORE: { CLEANING_INTERVAL: Number.isInteger(process.env?.CLEAN_STORE_CLEANING_TERMINAL) ? Number.parseInt(process.env.CLEAN_STORE_CLEANING_TERMINAL) - : undefined, + : 7200, MESSAGES: process.env?.CLEAN_STORE_MESSAGES === 'true', MESSAGE_UP: process.env?.CLEAN_STORE_MESSAGE_UP === 'true', CONTACTS: process.env?.CLEAN_STORE_CONTACTS === 'true', @@ -225,7 +225,7 @@ export class ConfigService { }, DEL_INSTANCE: isBooleanString(process.env?.DEL_INSTANCE) ? process.env.DEL_INSTANCE === 'true' - : Number.parseInt(process.env.DEL_INSTANCE), + : Number.parseInt(process.env.DEL_INSTANCE) || false, WEBHOOK: { GLOBAL: { URL: process.env?.WEBHOOK_GLOBAL_URL, @@ -238,6 +238,7 @@ export class ConfigService { 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', @@ -256,11 +257,11 @@ export class ConfigService { }, }, CONFIG_SESSION_PHONE: { - CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT, - NAME: process.env?.CONFIG_SESSION_PHONE_NAME, + 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), + LIMIT: Number.parseInt(process.env.QRCODE_LIMIT) || 30, }, AUTHENTICATION: { TYPE: process.env.AUTHENTICATION_TYPE as 'jwt', diff --git a/src/dev-env.yml b/src/dev-env.yml index 244ff73d..c0f907fc 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -39,7 +39,7 @@ LOG: - DARK - WEBHOOKS COLOR: true - BAILEYS: error # "fatal" | "error" | "warn" | "info" | "debug" | "trace" + BAILEYS: error # fatal | error | warn | info | debug | trace # Determine how long the instance should be deleted from memory in case of no connection. # Default time: 5 minutes @@ -96,6 +96,7 @@ WEBHOOK: MESSAGES_SET: true MESSAGES_UPSERT: true MESSAGES_UPDATE: true + MESSAGES_DELETE: true SEND_MESSAGE: true CONTACTS_SET: true CONTACTS_UPSERT: true diff --git a/src/main.ts b/src/main.ts index 7d16b5c6..ac66e7b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { waMonitor } from './whatsapp/whatsapp.module'; import { HttpStatus, router } from './whatsapp/routers/index.router'; import 'express-async-errors'; import { ServerUP } from './utils/server-up'; +import * as Sentry from '@sentry/node'; function initWA() { waMonitor.loadInstance(); @@ -19,6 +20,27 @@ function bootstrap() { const logger = new Logger('SERVER'); const app = express(); + // Sentry.init({ + // dsn: '', + // integrations: [ + // // enable HTTP calls tracing + // new Sentry.Integrations.Http({ tracing: true }), + // // enable Express.js middleware tracing + // new Sentry.Integrations.Express({ app }), + // // Automatically instrument Node.js libraries and frameworks + // ...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations(), + // ], + + // // Set tracesSampleRate to 1.0 to capture 100% + // // of transactions for performance monitoring. + // // We recommend adjusting this value in production + // tracesSampleRate: 1.0, + // }); + + // app.use(Sentry.Handlers.requestHandler()); + + // app.use(Sentry.Handlers.tracingHandler()); + app.use( cors({ origin(requestOrigin, callback) { @@ -43,6 +65,13 @@ function bootstrap() { app.use('/', router); + // app.use(Sentry.Handlers.errorHandler()); + + // app.use(function onError(err, req, res, next) { + // res.statusCode = 500; + // res.end(res.sentry + '\n'); + // }); + app.use( (err: Error, req: Request, res: Response, next: NextFunction) => { if (err) { diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 1fbb331e..2b9ce19a 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -39,6 +39,7 @@ export const instanceNameSchema: JSONSchema7 = { 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', + 'MESSAGES_DELETE', 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', @@ -81,8 +82,8 @@ const quotedOptionsSchema: JSONSchema7 = { remoteJid: { type: 'string' }, fromMe: { type: 'boolean', enum: [true, false] }, }, - required: ['id', 'remoteJid', 'fromMe'], - ...isNotEmpty('id', 'remoteJid'), + required: ['id'], + ...isNotEmpty('id'), }, message: { type: 'object' }, }, @@ -144,24 +145,6 @@ export const textMessageSchema: JSONSchema7 = { required: ['textMessage', 'number'], }; -export const linkPreviewSchema: JSONSchema7 = { - $id: v4(), - type: 'object', - properties: { - number: { ...numberDefinition }, - options: { ...optionsSchema }, - linkPreview: { - type: 'object', - properties: { - text: { type: 'string' }, - }, - required: ['text'], - ...isNotEmpty('text'), - }, - }, - required: ['linkPreview', 'number'], -}; - export const pollMessageSchema: JSONSchema7 = { $id: v4(), type: 'object', @@ -846,6 +829,7 @@ export const webhookSchema: JSONSchema7 = { 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', + 'MESSAGES_DELETE', 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index e657c2c0..cb224cd6 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -160,8 +160,6 @@ export abstract class RouterBroker { const v = validate(ref, schema); - console.log(v, '@checkei aqui'); - if (!v.valid) { const message: any[] = v.errors.map(({ property, stack, schema }) => { let message: string; @@ -203,8 +201,6 @@ export abstract class RouterBroker { const v = validate(ref, schema); - console.log(v, '@checkei aqui'); - if (!v.valid) { const message: any[] = v.errors.map(({ property, stack, schema }) => { let message: string; diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index a4367833..de0aef7a 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -66,6 +66,18 @@ export class ChatwootController { 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}`, @@ -78,7 +90,7 @@ export class ChatwootController { logger.verbose( 'requested receiveWebhook from ' + instance.instanceName + ' instance', ); - const chatwootService = new ChatwootService(waMonitor); + const chatwootService = new ChatwootService(waMonitor, this.configService); return chatwootService.receiveWebhook(instance, data); } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index a2b432e5..f0adb3a3 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -12,6 +12,7 @@ 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'; export class InstanceController { constructor( @@ -63,7 +64,10 @@ export class InstanceController { this.repository, this.cache, ); - instance.instanceName = instanceName; + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); this.logger.verbose('instance: ' + instance.instanceName + ' created'); this.waMonitor.waInstances[instance.instanceName] = instance; @@ -82,6 +86,9 @@ export class InstanceController { 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, { @@ -132,6 +139,10 @@ export class InstanceController { throw new BadRequestException('url is required'); } + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + const urlServer = this.configService.get('SERVER').URL; try { @@ -183,7 +194,10 @@ export class InstanceController { this.repository, this.cache, ); - instance.instanceName = instanceName; + instance.instanceName = instanceName + .toLowerCase() + .replace(/[^a-z0-9]/g, '') + .replace(' ', ''); this.logger.verbose('instance: ' + instance.instanceName + ' created'); @@ -203,6 +217,10 @@ export class InstanceController { 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, { @@ -266,6 +284,10 @@ export class InstanceController { throw new BadRequestException('url is required'); } + if (!isURL(chatwoot_url, { require_tld: false })) { + throw new BadRequestException('Invalid "url" property in chatwoot'); + } + const urlServer = this.configService.get('SERVER').URL; try { @@ -341,24 +363,8 @@ export class InstanceController { try { this.logger.verbose('requested restartInstance from ' + instanceName + ' instance'); - this.logger.verbose('deleting instance: ' + instanceName); - delete this.waMonitor.waInstances[instanceName]; - - this.logger.verbose('creating instance: ' + instanceName); - const instance = new WAStartupService( - this.configService, - this.eventEmitter, - this.repository, - this.cache, - ); - - instance.instanceName = instanceName; - - this.logger.verbose('instance: ' + instance.instanceName + ' created'); - - this.logger.verbose('connecting instance: ' + instanceName); - await instance.connectToWhatsapp(); - this.waMonitor.waInstances[instance.instanceName] = instance; + this.logger.verbose('logging out instance: ' + instanceName); + this.waMonitor.waInstances[instanceName]?.client?.ws?.close(); return { error: false, message: 'Instance restarted' }; } catch (error) { diff --git a/src/whatsapp/controllers/sendMessage.controller.ts b/src/whatsapp/controllers/sendMessage.controller.ts index c2d5298c..fb942a9c 100644 --- a/src/whatsapp/controllers/sendMessage.controller.ts +++ b/src/whatsapp/controllers/sendMessage.controller.ts @@ -5,7 +5,6 @@ import { SendAudioDto, SendButtonDto, SendContactDto, - SendLinkPreviewDto, SendListDto, SendLocationDto, SendMediaDto, @@ -31,9 +30,15 @@ export class SendMessageController { public async sendMedia({ instanceName }: InstanceDto, data: SendMediaDto) { logger.verbose('requested sendMedia from ' + instanceName + ' instance'); - if (isBase64(data?.mediaMessage?.media) && !data?.mediaMessage?.fileName) { - throw new BadRequestException('For bse64 the file name must be informed.'); + + if ( + isBase64(data?.mediaMessage?.media) && + !data?.mediaMessage?.fileName && + data?.mediaMessage?.mediatype === 'document' + ) { + throw new BadRequestException('For base64 the file name must be informed.'); } + logger.verbose( 'isURL: ' + isURL(data?.mediaMessage?.media) + @@ -119,9 +124,4 @@ export class SendMessageController { logger.verbose('requested sendStatus from ' + instanceName + ' instance'); return await this.waMonitor.waInstances[instanceName].statusMessage(data); } - - public async sendLinkPreview({ instanceName }: InstanceDto, data: SendLinkPreviewDto) { - logger.verbose('requested sendLinkPreview from ' + instanceName + ' instance'); - return await this.waMonitor.waInstances[instanceName].linkPreview(data); - } } diff --git a/src/whatsapp/dto/sendMessage.dto.ts b/src/whatsapp/dto/sendMessage.dto.ts index 4f1ff88b..51a55cec 100644 --- a/src/whatsapp/dto/sendMessage.dto.ts +++ b/src/whatsapp/dto/sendMessage.dto.ts @@ -28,10 +28,6 @@ class TextMessage { text: string; } -class linkPreviewMessage { - text: string; -} - export class StatusMessage { type: string; content: string; @@ -52,10 +48,6 @@ export class SendTextDto extends Metadata { textMessage: TextMessage; } -export class SendLinkPreviewDto extends Metadata { - linkPreview: linkPreviewMessage; -} - export class SendStatusDto extends Metadata { statusMessage: StatusMessage; } diff --git a/src/whatsapp/guards/instance.guard.ts b/src/whatsapp/guards/instance.guard.ts index 9e7a0e4b..1e79ff1d 100644 --- a/src/whatsapp/guards/instance.guard.ts +++ b/src/whatsapp/guards/instance.guard.ts @@ -11,7 +11,6 @@ import { import { InstanceDto } from '../dto/instance.dto'; import { cache, waMonitor } from '../whatsapp.module'; import { Database, Redis, configService } from '../../config/env.config'; -import { RedisCache } from '../../db/redis.client'; async function getInstance(instanceName: string) { const db = configService.get('DATABASE'); diff --git a/src/whatsapp/models/webhook.model.ts b/src/whatsapp/models/webhook.model.ts index 873491bf..62ee38f4 100644 --- a/src/whatsapp/models/webhook.model.ts +++ b/src/whatsapp/models/webhook.model.ts @@ -14,6 +14,7 @@ const webhookSchema = new Schema({ url: { type: String, required: true }, enabled: { type: Boolean, required: true }, events: { type: [String], required: true }, + webhook_by_events: { type: Boolean, required: true }, }); export const WebhookModel = dbserver?.model(WebhookRaw.name, webhookSchema, 'webhook'); diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index 9740b436..cdf15498 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -5,12 +5,13 @@ import { MessageUpRepository } from './messageUp.repository'; import { MongoClient } from 'mongodb'; import { WebhookRepository } from './webhook.repository'; import { ChatwootRepository } from './chatwoot.repository'; + import { AuthRepository } from './auth.repository'; import { Auth, ConfigService, Database } from '../../config/env.config'; import { execSync } from 'child_process'; import { join } from 'path'; +import fs from 'fs'; import { Logger } from '../../config/logger.config'; - export class RepositoryBroker { constructor( public readonly message: MessageRepository, @@ -23,7 +24,6 @@ export class RepositoryBroker { private configService: ConfigService, dbServer?: MongoClient, ) { - this.logger.verbose('initializing repository broker'); this.dbClient = dbServer; this.__init_repo_without_db__(); } @@ -38,39 +38,54 @@ export class RepositoryBroker { private __init_repo_without_db__() { this.logger.verbose('initializing repository without db'); if (!this.configService.get('DATABASE').ENABLED) { - this.logger.verbose('database is disabled'); - const storePath = join(process.cwd(), 'store'); this.logger.verbose('creating store path: ' + storePath); - execSync( - `mkdir -p ${join( + 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'); - this.logger.verbose('creating chats path: ' + join(storePath, 'chats')); - execSync(`mkdir -p ${join(storePath, 'chats')}`); - - this.logger.verbose('creating contacts path: ' + join(storePath, 'contacts')); - execSync(`mkdir -p ${join(storePath, 'contacts')}`); - - this.logger.verbose('creating messages path: ' + join(storePath, 'messages')); - execSync(`mkdir -p ${join(storePath, 'messages')}`); - - this.logger.verbose('creating message-up path: ' + join(storePath, 'message-up')); - execSync(`mkdir -p ${join(storePath, 'message-up')}`); - - this.logger.verbose('creating webhook path: ' + join(storePath, 'webhook')); - execSync(`mkdir -p ${join(storePath, 'webhook')}`); - - this.logger.verbose('creating chatwoot path: ' + join(storePath, 'chatwoot')); - execSync(`mkdir -p ${join(storePath, 'chatwoot')}`); - - this.logger.verbose('creating temp path: ' + join(storePath, 'temp')); - execSync(`mkdir -p ${join(storePath, 'temp')}`); + // Check if directories exist, create them if not + if (!fs.existsSync(authDir)) { + this.logger.verbose('creating auth dir: ' + authDir); + fs.mkdirSync(authDir, { recursive: true }); + } + 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 }); + } + } catch (error) { + this.logger.error(error); + } } } } diff --git a/src/whatsapp/routers/instance.router.ts b/src/whatsapp/routers/instance.router.ts index 799c8249..850ffebd 100644 --- a/src/whatsapp/routers/instance.router.ts +++ b/src/whatsapp/routers/instance.router.ts @@ -23,6 +23,7 @@ export class InstanceRouter extends RouterBroker { logger.verbose('request query: '); logger.verbose(req.query); + const response = await this.dataValidate({ request: req, schema: instanceNameSchema, diff --git a/src/whatsapp/routers/sendMessage.router.ts b/src/whatsapp/routers/sendMessage.router.ts index d8d3acea..f6f9c3eb 100644 --- a/src/whatsapp/routers/sendMessage.router.ts +++ b/src/whatsapp/routers/sendMessage.router.ts @@ -3,7 +3,6 @@ import { audioMessageSchema, buttonMessageSchema, contactMessageSchema, - linkPreviewSchema, listMessageSchema, locationMessageSchema, mediaMessageSchema, @@ -17,7 +16,6 @@ import { SendAudioDto, SendButtonDto, SendContactDto, - SendLinkPreviewDto, SendListDto, SendLocationDto, SendMediaDto, @@ -199,23 +197,6 @@ export class MessageRouter extends RouterBroker { return res.status(HttpStatus.CREATED).json(response); }) - .post(this.routerPath('sendLinkPreview'), ...guards, async (req, res) => { - logger.verbose('request received in sendLinkPreview'); - logger.verbose('request body: '); - logger.verbose(req.body); - - logger.verbose('request query: '); - logger.verbose(req.query); - const response = await this.dataValidate({ - request: req, - schema: linkPreviewSchema, - ClassRef: SendLinkPreviewDto, - execute: (instance, data) => - sendMessageController.sendLinkPreview(instance, data), - }); - - return res.status(HttpStatus.CREATED).json(response); - }) .post(this.routerPath('sendSticker'), ...guards, async (req, res) => { logger.verbose('request received in sendSticker'); logger.verbose('request body: '); diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 384cedd8..decb5822 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -12,6 +12,7 @@ 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 { ConfigService, HttpServer } from '../../config/env.config'; export class ChatwootService { private messageCacheFile: string; @@ -21,7 +22,10 @@ export class ChatwootService { private provider: any; - constructor(private readonly waMonitor: WAMonitoringService) { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + ) { this.messageCache = new Set(); } @@ -243,7 +247,7 @@ export class ChatwootService { accountId: this.provider.account_id, conversationId: conversation.id, data: { - content: '/iniciar', + content: '/init', message_type: 'outgoing', }, }); @@ -433,7 +437,7 @@ export class ChatwootService { instance, body.key.participant.split('@')[0], filterInbox.id, - isGroup, + false, body.pushName, picture_url.profilePictureUrl || null, ); @@ -449,21 +453,24 @@ export class ChatwootService { const findContact = await this.findContact(instance, chatId); let contact: any; - - if (findContact) { - contact = await this.updateContact(instance, findContact.id, { - name: nameContact, - avatar_url: picture_url.profilePictureUrl || null, - }); + if (body.key.fromMe) { + contact = findContact; } else { - contact = await this.createContact( - instance, - chatId, - filterInbox.id, - isGroup, - nameContact, - picture_url.profilePictureUrl || null, - ); + if (findContact) { + contact = await this.updateContact(instance, findContact.id, { + name: nameContact, + avatar_url: picture_url.profilePictureUrl || null, + }); + } else { + contact = await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + picture_url.profilePictureUrl || null, + ); + } } if (!contact) { @@ -471,7 +478,8 @@ export class ChatwootService { return null; } - const contactId = contact.payload.id || contact.payload.contact.id; + 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'); @@ -928,8 +936,8 @@ export class ChatwootService { const command = messageReceived.replace('/', ''); - if (command === 'iniciar') { - this.logger.verbose('command iniciar found'); + if (command === 'init' || command === 'iniciar') { + this.logger.verbose('command init found'); const state = waInstance?.connectionStatus?.state; if (state !== 'open') { @@ -939,7 +947,7 @@ export class ChatwootService { this.logger.verbose('whatsapp already connected'); await this.createBotMessage( instance, - `🚨 Instância ${body.inbox.name} já está conectada.`, + `🚨 ${body.inbox.name} instance is connected.`, 'incoming', ); } @@ -954,7 +962,7 @@ export class ChatwootService { this.logger.verbose('state not found'); await this.createBotMessage( instance, - `⚠️ Instância ${body.inbox.name} não existe.`, + `⚠️ ${body.inbox.name} instance not found.`, 'incoming', ); } @@ -963,16 +971,16 @@ export class ChatwootService { this.logger.verbose('state: ' + state + ' found'); await this.createBotMessage( instance, - `⚠️ Status da instância ${body.inbox.name}: *${state}*`, + `⚠️ ${body.inbox.name} instance status: *${state}*`, 'incoming', ); } } - if (command === 'desconectar') { - this.logger.verbose('command desconectar found'); + if (command === 'disconnect' || command === 'desconectar') { + this.logger.verbose('command disconnect found'); - const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${body.inbox.name}*: `; + const msgLogout = `🚨 Disconnecting Whatsapp from inbox *${body.inbox.name}*: `; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgLogout, 'incoming'); @@ -981,6 +989,33 @@ export class ChatwootService { await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); await waInstance?.client?.ws?.close(); } + + if (command.includes('#inbox_whatsapp')) { + 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, + }; + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${urlServer}/instance/create`, + headers: { + 'Content-Type': 'application/json', + apikey: apiKey, + }, + data: data, + }; + + await axios.request(config); + } } if ( @@ -1059,7 +1094,11 @@ export class ChatwootService { } } - if (body.message_type === 'template' && body.content_type === 'input_csat') { + if ( + body.message_type === 'template' && + body.content_type === 'input_csat' && + body.event === 'message_created' + ) { this.logger.verbose('check if is csat'); const data: SendTextDto = { @@ -1119,6 +1158,8 @@ export class ChatwootService { documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption, audioMessage: msg.audioMessage?.caption, + contactMessage: msg.contactMessage?.vcard, + contactsArrayMessage: msg.contactsArrayMessage, }; this.logger.verbose('type message: ' + types); @@ -1132,6 +1173,67 @@ export class ChatwootService { const result = typeKey ? types[typeKey] : undefined; + if (typeKey === 'contactMessage') { + const vCardData = result.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** + **name:** ${contactInfo['FN']}`; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); + + this.logger.verbose('message content: ' + formattedContact); + return formattedContact; + } + + if (typeKey === 'contactsArrayMessage') { + const formattedContacts = result.contacts.map((contact) => { + const vCardData = contact.vcard.split('\n'); + const contactInfo = {}; + + vCardData.forEach((line) => { + const [key, value] = line.split(':'); + if (key && value) { + contactInfo[key] = value; + } + }); + + let formattedContact = `**Contact:** + **name:** ${contact.displayName}`; + + let numberCount = 1; + Object.keys(contactInfo).forEach((key) => { + if (key.startsWith('item') && key.includes('TEL')) { + const phoneNumber = contactInfo[key]; + formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; + numberCount++; + } + }); + + return formattedContact; + }); + + const formattedContactsArray = formattedContacts.join('\n\n'); + + this.logger.verbose('formatted contacts: ' + formattedContactsArray); + + return formattedContactsArray; + } + this.logger.verbose('message content: ' + result); return result; @@ -1386,7 +1488,7 @@ export class ChatwootService { return; } - const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; + const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgStatus, 'incoming'); @@ -1394,43 +1496,20 @@ export class ChatwootService { if (event === 'connection.update') { this.logger.verbose('event connection.update'); - if (body.state === 'open') { - const msgConnection = `🚀 Conexão realizada com sucesso!`; + + if (body.status === 'open') { + const msgConnection = `🚀 Connection successfully established!`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgConnection, 'incoming'); } } - if (event === 'contacts.update') { - this.logger.verbose('event contacts.update'); - const data = body; - - if (data.length) { - this.logger.verbose('contacts found'); - for (const item of data) { - const number = item.id.split('@')[0]; - const photo = item.profilePictureUrl || null; - this.logger.verbose('find contact in chatwoot'); - const find = await this.findContact(instance, number); - - if (find) { - this.logger.verbose('contact found'); - - this.logger.verbose('update contact in chatwoot'); - await this.updateContact(instance, find.id, { - avatar_url: photo, - }); - } - } - } - } - if (event === 'qrcode.updated') { this.logger.verbose('event qrcode.updated'); if (body.statusCode === 500) { this.logger.verbose('qrcode error'); - const erroQRcode = `🚨 Limite de geração de QRCode atingido, para gerar um novo QRCode, envie a mensagem /iniciar novamente.`; + 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'); @@ -1455,12 +1534,12 @@ export class ChatwootService { this.logger.verbose('send qrcode to chatwoot'); await this.createBotQr( instance, - 'QRCode gerado com sucesso!', + 'QRCode successfully generated!', 'incoming', fileName, ); - const msgQrCode = `⚡️ QRCode gerado com sucesso!\n\nDigitalize este código QR nos próximos 40 segundos:`; + const msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds:`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgQrCode, 'incoming'); diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 462c9f96..8b347c21 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -99,72 +99,54 @@ export class WAMonitoringService { if (value.connectionStatus.state === 'open') { this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); - let apikey: string; - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - this.logger.verbose( - 'instance: ' + key + ' - hash exposed in fetch instances', - ); - const tokenStore = await this.repository.auth.find(key); - apikey = tokenStore.apikey || 'Apikey not found'; - instances.push({ - instance: { - instanceName: key, - owner: value.wuid, - profileName: (await value.getProfileName()) || 'not loaded', - profilePictureUrl: value.profilePictureUrl, - profileStatus: (await value.getProfileStatus()) || '', - status: value.connectionStatus.state, - apikey, - chatwoot, - }, - }); - } else { - this.logger.verbose( - 'instance: ' + key + ' - hash not exposed in fetch instances', - ); - instances.push({ - instance: { - instanceName: key, - owner: value.wuid, - profileName: (await value.getProfileName()) || 'not loaded', - profilePictureUrl: value.profilePictureUrl, - profileStatus: (await value.getProfileStatus()) || '', - status: value.connectionStatus.state, - }, - }); + const instanceData = { + instance: { + instanceName: key, + owner: value.wuid, + profileName: (await value.getProfileName()) || 'not loaded', + profilePictureUrl: value.profilePictureUrl, + profileStatus: (await value.getProfileStatus()) || '', + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = + this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = ( + await this.repository.auth.find(key) + ).apikey; + + instanceData.instance['chatwoot'] = chatwoot; } + + instances.push(instanceData); } else { this.logger.verbose( 'instance: ' + key + ' - connectionStatus: ' + value.connectionStatus.state, ); - let apikey: string; - if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { - this.logger.verbose( - 'instance: ' + key + ' - hash exposed in fetch instances', - ); - const tokenStore = await this.repository.auth.find(key); - apikey = tokenStore.apikey || 'Apikey not found'; - instances.push({ - instance: { - instanceName: key, - status: value.connectionStatus.state, - apikey, - chatwoot, - }, - }); - } else { - this.logger.verbose( - 'instance: ' + key + ' - hash not exposed in fetch instances', - ); - instances.push({ - instance: { - instanceName: key, - status: value.connectionStatus.state, - }, - }); + const instanceData = { + instance: { + instanceName: key, + status: value.connectionStatus.state, + }, + }; + + if (this.configService.get('AUTHENTICATION').EXPOSE_IN_FETCH_INSTANCES) { + instanceData.instance['serverUrl'] = + this.configService.get('SERVER').URL; + + instanceData.instance['apikey'] = ( + await this.repository.auth.find(key) + ).apikey; + + instanceData.instance['chatwoot'] = chatwoot; } + + instances.push(instanceData); } } } diff --git a/src/whatsapp/services/webhook.service.ts b/src/whatsapp/services/webhook.service.ts index dbc98d7c..2370e05b 100644 --- a/src/whatsapp/services/webhook.service.ts +++ b/src/whatsapp/services/webhook.service.ts @@ -18,9 +18,17 @@ export class WebhookService { public async find(instance: InstanceDto): Promise { try { this.logger.verbose('find webhook: ' + instance.instanceName); - return await this.waMonitor.waInstances[instance.instanceName].findWebhook(); + const result = await this.waMonitor.waInstances[ + instance.instanceName + ].findWebhook(); + + if (Object.keys(result).length === 0) { + throw new Error('Webhook not found'); + } + + return result; } catch (error) { - return { enabled: null, url: '' }; + return { enabled: false, url: '', events: [], webhook_by_events: false }; } } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 027a97df..65cb40bb 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -76,7 +76,6 @@ import { SendReactionDto, SendTextDto, SendPollDto, - SendLinkPreviewDto, SendStickerDto, SendStatusDto, StatusMessage, @@ -150,7 +149,7 @@ export class WAStartupService { private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; - private chatwootService = new ChatwootService(waMonitor); + private chatwootService = new ChatwootService(waMonitor, this.configService); public set instanceName(name: string) { this.logger.verbose(`Initializing instance '${name}'`); @@ -343,12 +342,19 @@ export class WAStartupService { public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); - const urlServer = this.configService.get('SERVER').URL; 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 instance = this.configService.get('AUTHENTICATION').INSTANCE; + 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 && instance.MODE !== 'container') { if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) { this.logger.verbose('Sending data to webhook local'); @@ -361,27 +367,40 @@ export class WAStartupService { } if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - this.logger.log({ + const logData = { local: WAStartupService.name + '.sendDataWebhook-local', url: baseURL, event, instance: this.instance.name, data, destination: this.localWebhook.url, - urlServer, - }); + 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 }); - await httpService.post('', { + const postData = { event, instance: this.instance.name, data, destination: this.localWebhook.url, - urlServer, - }); + server_url: serverUrl, + }; + + if (expose && instanceApikey) { + postData['apikey'] = instanceApikey; + } + + await httpService.post('', postData); } } catch (error) { this.logger.error({ @@ -394,6 +413,7 @@ export class WAStartupService { stack: error?.stack, name: error?.name, url: baseURL, + server_url: serverUrl, }); } } @@ -421,27 +441,39 @@ export class WAStartupService { } if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - this.logger.log({ + const logData = { local: WAStartupService.name + '.sendDataWebhook-global', url: globalURL, event, instance: this.instance.name, data, destination: localUrl, - urlServer, - }); + 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 }); - await httpService.post('', { + const postData = { event, instance: this.instance.name, data, destination: localUrl, - urlServer, - }); + server_url: serverUrl, + }; + + if (expose && globalApiKey) { + postData['apikey'] = globalApiKey; + } + + await httpService.post('', postData); } } catch (error) { this.logger.error({ @@ -454,6 +486,7 @@ export class WAStartupService { stack: error?.stack, name: error?.name, url: globalURL, + server_url: serverUrl, }); } } @@ -623,6 +656,17 @@ export class WAStartupService { │ 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', + }, + ); + } } } @@ -741,6 +785,7 @@ export class WAStartupService { version, connectTimeoutMs: 60_000, qrTimeout: 40_000, + defaultQueryTimeoutMs: undefined, emitOwnEvents: false, msgRetryCounterCache: this.msgRetryCounterCache, getMessage: async (key) => @@ -908,14 +953,6 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); - if (this.localChatwoot.enabled) { - await this.chatwootService.eventWhatsapp( - Events.CONTACTS_UPDATE, - { instanceName: this.instance.name }, - contactsRaw, - ); - } - this.logger.verbose('Updating contacts in database'); await this.repository.contact.update( contactsRaw, @@ -1126,11 +1163,7 @@ export class WAStartupService { 5: 'PLAYED', }; for await (const { key, update } of args) { - if ( - key.remoteJid !== 'status@broadcast' && - !key?.remoteJid?.match(/(:\d+)/) && - key.fromMe - ) { + if (key.remoteJid !== 'status@broadcast' && !key?.remoteJid?.match(/(:\d+)/)) { this.logger.verbose('Message update is valid'); let pollUpdates: any; @@ -1150,6 +1183,16 @@ export class WAStartupService { } } + 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); + return; + } + const message: MessageUpdateRaw = { ...key, status: status[update.status], @@ -1294,16 +1337,12 @@ export class WAStartupService { // Check if the number is MX or AR private formatMXOrARNumber(jid: string): 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] === '52' || match[1] === '54')) { - const joker = Number.parseInt(match[3][0]); - const ddd = Number.parseInt(match[2]); - if (joker < 7 || ddd < 11) { - return match[0]; - } - return match[1] === '52' ? '52' + match[3] : '54' + match[3]; + 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; @@ -1332,6 +1371,7 @@ export class WAStartupService { private createJid(number: string): string { this.logger.verbose('Creating jid with number: ' + number); + if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); return number; @@ -1342,22 +1382,31 @@ export class WAStartupService { return number; } - const formattedBRNumber = this.formatBRNumber(number); - if (formattedBRNumber !== number) { - this.logger.verbose( - 'Jid created is whatsapp in format BR: ' + `${formattedBRNumber}@s.whatsapp.net`, - ); - return `${formattedBRNumber}@s.whatsapp.net`; + const countryCode = number.substring(0, 2); + + if (Number(countryCode) === 55) { + const formattedBRNumber = this.formatBRNumber(number); + if (formattedBRNumber !== number) { + this.logger.verbose( + 'Jid created is whatsapp in format BR: ' + + `${formattedBRNumber}@s.whatsapp.net`, + ); + return `${formattedBRNumber}@s.whatsapp.net`; + } } - const formattedMXARNumber = this.formatMXOrARNumber(number); + if (Number(countryCode) === 52 || Number(countryCode) === 54) { + console.log('numero mexicano'); - if (formattedMXARNumber !== number) { - this.logger.verbose( - 'Jid created is whatsapp in format MXAR: ' + - `${formattedMXARNumber}@s.whatsapp.net`, - ); - return `${formattedMXARNumber}@s.whatsapp.net`; + const formattedMXARNumber = this.formatMXOrARNumber(number); + + if (formattedMXARNumber !== number) { + this.logger.verbose( + 'Jid created is whatsapp in format MXAR: ' + + `${formattedMXARNumber}@s.whatsapp.net`, + ); + return `${formattedMXARNumber}@s.whatsapp.net`; + } } if (number.includes('-')) { @@ -1427,13 +1476,29 @@ export class WAStartupService { let quoted: WAMessage; if (options?.quoted) { - quoted = 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'); @@ -1448,7 +1513,6 @@ export class WAStartupService { this.logger.verbose('Mentions everyone'); this.logger.verbose('Getting group metadata'); - const groupMetadata = await this.client.groupMetadata(sender); mentions = groupMetadata.participants.map((participant) => participant.id); this.logger.verbose('Getting group metadata for mentions'); } else { @@ -1475,9 +1539,9 @@ export class WAStartupService { if ( !message['audio'] && !message['poll'] && - !message['linkPreview'] && !message['sticker'] && - !sender.includes('@broadcast') + !message['conversation'] && + sender !== 'status@broadcast' ) { if (!message['audio']) { this.logger.verbose('Sending message'); @@ -1495,18 +1559,19 @@ export class WAStartupService { } } - if (message['linkPreview']) { + if (message['conversation']) { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, { - text: message['linkPreview'].text, + text: message['conversation'], + mentions, } as unknown as AnyMessageContent, option as unknown as MiscMessageGenerationOptions, ); } - if (sender.includes('@broadcast')) { + if (sender === 'status@broadcast') { this.logger.verbose('Sending message'); return await this.client.sendMessage( sender, @@ -1582,19 +1647,6 @@ export class WAStartupService { ); } - public async linkPreview(data: SendLinkPreviewDto) { - this.logger.verbose('Sending link preview'); - return await this.sendMessageWithTyping( - data.number, - { - linkPreview: { - text: data.linkPreview.text, - }, - }, - data?.options, - ); - } - public async pollMessage(data: SendPollDto) { this.logger.verbose('Sending poll message'); return await this.sendMessageWithTyping( @@ -1730,8 +1782,10 @@ export class WAStartupService { 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: await this.formatStatusMessage(data.statusMessage), + status, }); } @@ -2110,6 +2164,7 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { const jid = this.createJid(number); + // const jid = `${number}@s.whatsapp.net`; if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); @@ -2645,9 +2700,7 @@ export class WAStartupService { const msg = `${description}\n\n${inviteUrl}`; const message = { - linkPreview: { - text: msg, - }, + conversation: msg, }; for await (const number of numbers) { diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 1ebe3b40..169df515 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -9,6 +9,7 @@ export enum Events { 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', diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index c57b9bf8..46f8ecd1 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -72,7 +72,7 @@ const webhookService = new WebhookService(waMonitor); export const webhookController = new WebhookController(webhookService); -const chatwootService = new ChatwootService(waMonitor); +const chatwootService = new ChatwootService(waMonitor, configService); export const chatwootController = new ChatwootController(chatwootService, configService); diff --git a/start.sh b/start.sh index d716d558..06a55e4a 100755 --- a/start.sh +++ b/start.sh @@ -6,6 +6,7 @@ then echo "DOCKER_ENV=$DOCKER_ENV" echo else + mkdir -p ./dist/src cp ./src/env.yml ./dist/src fi echo "> removing dist"