diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a59f72..9fd69a55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +# 1.2.0 (homolog) + +### Features + +* Native integration with chatwoot +* Added returning or non-returning participants option in fetchAllGroups +* Added group integration to chatwoot +* Added automation on create instance to chatwoot +* Added verbose logs and format chatwoot service + +### Fixed + +* Adjusts in docker-compose files +* Adjusts in number validation for AR and MX numbers +* Adjusts in env files, removed save old_messages +* Fix when sending a message to a group I don't belong returns a bad request +* Fits the format on return from the fetchAllGroups endpoint +* Adjust in send document with caption from chatwoot +* Fixed message with undefind in chatwoot +* Changed message in path / +* Test duplicate message media in groups chatwoot +* Optimize send message from group with mentions +* Fixed name of the profile status in fetchInstances +* Fixed error 500 when logout in instance with status = close + # 1.1.5 (2023-07-12 07:17) ### Fixed diff --git a/Docker/.env.example b/Docker/.env.example index b7fdd95b..f4e665aa 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -1,3 +1,5 @@ +SERVER_URL='' # ex.: http://localhost:3333 + CORS_ORIGIN='*' # Or separate by commas - ex.: 'yourdomain1.com, yourdomain2.com' CORS_METHODS='POST,GET,PUT,DELETE' CORS_CREDENTIALS=true @@ -31,7 +33,6 @@ DATABASE_CONNECTION_DB_PREFIX_NAME=evolution # Choose the data you want to save in the application's database or store DATABASE_SAVE_DATA_INSTANCE=false -DATABASE_SAVE_DATA_OLD_MESSAGE=false DATABASE_SAVE_DATA_NEW_MESSAGE=false DATABASE_SAVE_MESSAGE_UPDATE=false DATABASE_SAVE_DATA_CONTACTS=false @@ -41,7 +42,8 @@ REDIS_ENABLED=false REDIS_URI=redis://redis:6379 REDIS_PREFIX_KEY=evolution -# Webhook Settings +# Global Webhook Settings +# Each instance's Webhook URL and events will be requested at the time it is created ## Define a global webhook that will listen for enabled events from all instances WEBHOOK_GLOBAL_URL='' WEBHOOK_GLOBAL_ENABLED=false @@ -53,6 +55,7 @@ WEBHOOK_EVENTS_QRCODE_UPDATED=true WEBHOOK_EVENTS_MESSAGES_SET=true WEBHOOK_EVENTS_MESSAGES_UPSERT=true WEBHOOK_EVENTS_MESSAGES_UPDATE=true +WEBHOOK_EVENTS_SEND_MESSAGE=true WEBHOOK_EVENTS_CONTACTS_SET=true WEBHOOK_EVENTS_CONTACTS_UPSERT=true WEBHOOK_EVENTS_CONTACTS_UPDATE=true @@ -76,6 +79,8 @@ CONFIG_SESSION_PHONE_NAME=chrome # chrome | firefox | edge | opera | safari QRCODE_LIMIT=30 # Defines an authentication type for the api +# We recommend using the apikey because it will allow you to use a custom token, +# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token AUTHENTICATION_TYPE='apikey' # jwt or 'apikey' ## Define a global apikey to access all instances. ### OBS: This key must be inserted in the request header to create an instance. @@ -89,4 +94,7 @@ AUTHENTICATION_JWT_SECRET='L0YWtjb2w554WFqPG' AUTHENTICATION_INSTANCE_MODE=server # container or server # if you are using container mode, set the container name and the webhook url to default instance AUTHENTICATION_INSTANCE_NAME=evolution -AUTHENTICATION_INSTANCE_WEBHOOK_URL='' \ No newline at end of file +AUTHENTICATION_INSTANCE_WEBHOOK_URL='' +AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID=1 +AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN=123456 +AUTHENTICATION_INSTANCE_CHATWOOT_URL='' \ No newline at end of file diff --git a/Docker/mongodb/docker-compose.yaml b/Docker/mongodb/docker-compose.yaml index 714109c9..2e4d74f3 100644 --- a/Docker/mongodb/docker-compose.yaml +++ b/Docker/mongodb/docker-compose.yaml @@ -9,14 +9,16 @@ services: 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 + - MONGO_INITDB_ROOT_USERNAME=root + - MONGO_INITDB_ROOT_PASSWORD=root + - PUID=1000 + - PGID=1000 + volumes: + - evolution_mongodb_data:/data/db + - evolution_mongodb_configdb:/data/configdb networks: - evolution-net expose: diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index 55e73847..5b102b11 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -8,6 +8,12 @@ services: redis: image: redis:latest container_name: redis + command: > + redis-server + --port 6379 + --appendonly yes + volumes: + - evolution_redis:/data ports: - 6379:6379 networks: diff --git a/Dockerfile b/Dockerfile index 3cdbeee6..a7e558f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ ENV DOCKER_ENV=true ENV SERVER_TYPE="http" ENV SERVER_PORT=8080 +ENV SERVER_URL=$SERVER_URL ENV CORS_ORIGIN="*" ENV CORS_METHODS="POST,GET,PUT,DELETE" @@ -40,7 +41,6 @@ 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_OLD_MESSAGE=$DATABASE_SAVE_DATA_OLD_MESSAGE 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 @@ -57,8 +57,9 @@ ENV WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=$WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS 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_UPDATE=$WEBHOOK_EVENTS_MESSAGES_UPDATE 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 @@ -88,6 +89,9 @@ ENV AUTHENTICATION_JWT_SECRET="L=0YWt]b2w[WF>#>:&E`" 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 RUN npm install diff --git a/package.json b/package.json index dd1b8743..c0e8c825 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.1.4", + "version": "1.2.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -43,7 +43,8 @@ "@adiwajshing/keyed-db": "^0.2.4", "@ffmpeg-installer/ffmpeg": "^1.1.0", "@hapi/boom": "^10.0.1", - "@whiskeysockets/baileys": "github:EvolutionAPI/Baileys", + "@whiskeysockets/baileys": "github:vphelipe/WhiskeySockets-Baileys#master", + "@figuro/chatwoot-sdk": "^1.1.14", "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 7fa4fe5c..76bb55ea 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import { SRC_DIR } from './path.config'; import { isBooleanString } from 'class-validator'; -export type HttpServer = { TYPE: 'http' | 'https'; PORT: number }; +export type HttpServer = { TYPE: 'http' | 'https'; PORT: number; URL: string }; export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE'; export type Cors = { @@ -33,7 +33,6 @@ export type Log = { export type SaveData = { INSTANCE: boolean; - OLD_MESSAGE: boolean; NEW_MESSAGE: boolean; MESSAGE_UPDATE: boolean; CONTACTS: boolean; @@ -77,6 +76,7 @@ export type EventsWebhook = { MESSAGES_SET: boolean; MESSAGES_UPSERT: boolean; MESSAGES_UPDATE: boolean; + SEND_MESSAGE: boolean; CONTACTS_SET: boolean; CONTACTS_UPDATE: boolean; CONTACTS_UPSERT: boolean; @@ -98,6 +98,9 @@ export type Instance = { NAME: string; WEBHOOK_URL: string; MODE: string; + CHATWOOT_ACCOUNT_ID?: string; + CHATWOOT_TOKEN?: string; + CHATWOOT_URL?: string; }; export type Auth = { API_KEY: ApiKey; @@ -170,6 +173,7 @@ export class ConfigService { 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(','), @@ -203,7 +207,6 @@ export class ConfigService { ENABLED: process.env?.DATABASE_ENABLED === 'true', SAVE_DATA: { INSTANCE: process.env?.DATABASE_SAVE_DATA_INSTANCE === 'true', - OLD_MESSAGE: process.env?.DATABASE_SAVE_DATA_OLD_MESSAGE === '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', @@ -235,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', + 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', @@ -275,6 +279,10 @@ export class ConfigService { NAME: process.env.AUTHENTICATION_INSTANCE_NAME, WEBHOOK_URL: process.env.AUTHENTICATION_INSTANCE_WEBHOOK_URL, MODE: process.env.AUTHENTICATION_INSTANCE_MODE, + CHATWOOT_ACCOUNT_ID: + process.env.AUTHENTICATION_INSTANCE_CHATWOOT_ACCOUNT_ID || '', + CHATWOOT_TOKEN: process.env.AUTHENTICATION_INSTANCE_CHATWOOT_TOKEN || '', + CHATWOOT_URL: process.env.AUTHENTICATION_INSTANCE_CHATWOOT_URL || '', }, }, }; diff --git a/src/dev-env.yml b/src/dev-env.yml index 86fd0066..244ff73d 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -8,10 +8,11 @@ SERVER: TYPE: http # https PORT: 8080 # 443 + URL: localhost CORS: ORIGIN: - - '*' + - "*" # - yourdomain.com METHODS: - POST @@ -63,12 +64,11 @@ CLEAN_STORE: DATABASE: ENABLED: false CONNECTION: - URI: 'mongodb://root:root@localhost:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true' + URI: "mongodb://root:root@localhost:27017/?authSource=admin&readPreference=primary&ssl=false&directConnection=true" DB_PREFIX_NAME: evolution # Choose the data you want to save in the application's database or store SAVE_DATA: INSTANCE: false - OLD_MESSAGE: false NEW_MESSAGE: false MESSAGE_UPDATE: false CONTACTS: false @@ -76,10 +76,11 @@ DATABASE: REDIS: ENABLED: false - URI: 'redis://localhost:6379' - PREFIX_KEY: 'evolution' + URI: "redis://localhost:6379" + PREFIX_KEY: "evolution" -# Webhook Settings +# Global Webhook Settings +# Each instance's Webhook URL and events will be requested at the time it is created WEBHOOK: # Define a global webhook that will listen for enabled events from all instances GLOBAL: @@ -88,13 +89,14 @@ WEBHOOK: # With this option activated, you work with a url per webhook event, respecting the global url and the name of each event WEBHOOK_BY_EVENTS: false # Automatically maps webhook paths - # Set the events you want to hear + # Set the events you want to hear EVENTS: APPLICATION_STARTUP: false QRCODE_UPDATED: true MESSAGES_SET: true MESSAGES_UPSERT: true MESSAGES_UPDATE: true + SEND_MESSAGE: true CONTACTS_SET: true CONTACTS_UPSERT: true CONTACTS_UPDATE: true @@ -112,7 +114,7 @@ WEBHOOK: CONFIG_SESSION_PHONE: # Name that will be displayed on smartphone connection - CLIENT: 'Evolution API' + CLIENT: "Evolution API" NAME: chrome # chrome | firefox | edge | opera | safari # Set qrcode display limit @@ -120,6 +122,8 @@ QRCODE: LIMIT: 30 # Defines an authentication type for the api +# We recommend using the apikey because it will allow you to use a custom token, +# if you use jwt, a random token will be generated and may be expired and you will have to generate a new token AUTHENTICATION: TYPE: apikey # jwt or apikey # Define a global apikey to access all instances @@ -134,8 +138,11 @@ AUTHENTICATION: SECRET: L=0YWt]b2w[WF>#>:&E` # Set the instance name and webhook url to create an instance in init the application INSTANCE: - # With this option activated, you work with a url per webhook event, respecting the local url and the name of each event + # With this option activated, you work with a url per webhook event, respecting the local url and the name of each event MODE: server # container or server # if you are using container mode, set the container name and the webhook url to default instance NAME: evolution WEBHOOK_URL: + CHATWOOT_ACCOUNT_ID: 1 + CHATWOOT_TOKEN: 123456 + CHATWOOT_URL: diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 9cf411e5..1fbb331e 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', + 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', 'CONTACTS_UPDATE', @@ -699,6 +700,16 @@ export const groupJidSchema: JSONSchema7 = { ...isNotEmpty('groupJid'), }; +export const getParticipantsSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + getParticipants: { type: 'string', enum: ['true', 'false'] }, + }, + required: ['getParticipants'], + ...isNotEmpty('getParticipants'), +}; + export const groupSendInviteSchema: JSONSchema7 = { $id: v4(), type: 'object', @@ -835,6 +846,7 @@ export const webhookSchema: JSONSchema7 = { 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', + 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', 'CONTACTS_UPDATE', @@ -855,3 +867,17 @@ export const webhookSchema: JSONSchema7 = { required: ['url', 'enabled'], ...isNotEmpty('url'), }; + +export const chatwootSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean', enum: [true, false] }, + account_id: { type: 'string' }, + token: { type: 'string' }, + url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, + }, + required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], + ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), +}; diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index e0ed588c..e657c2c0 100644 --- a/src/whatsapp/abstract/abstract.router.ts +++ b/src/whatsapp/abstract/abstract.router.ts @@ -5,7 +5,7 @@ import { validate } from 'jsonschema'; import { BadRequestException } from '../../exceptions'; import 'express-async-errors'; import { Logger } from '../../config/logger.config'; -import { GroupInvite, GroupJid } from '../dto/group.dto'; +import { GetParticipant, GroupInvite, GroupJid } from '../dto/group.dto'; type DataValidate = { request: Request; @@ -181,4 +181,47 @@ export abstract class RouterBroker { return await execute(instance, ref); } + + public async getParticipantsValidate(args: DataValidate) { + const { request, ClassRef, schema, execute } = args; + + const getParticipants = request.query as unknown as GetParticipant; + + if (!getParticipants?.getParticipants) { + throw new BadRequestException( + 'The getParticipants needs to be informed in the query', + ); + } + + const instance = request.params as unknown as InstanceDto; + const body = request.body; + + const ref = new ClassRef(); + + Object.assign(body, getParticipants); + Object.assign(ref, body); + + const v = validate(ref, schema); + + console.log(v, '@checkei aqui'); + + if (!v.valid) { + const message: any[] = v.errors.map(({ property, stack, schema }) => { + let message: string; + if (schema['description']) { + message = schema['description']; + } else { + message = stack.replace('instance.', ''); + } + return { + property: property.replace('instance.', ''), + message, + }; + }); + logger.error([...message]); + throw new BadRequestException(...message); + } + + return await execute(instance, ref); + } } diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts new file mode 100644 index 00000000..a4367833 --- /dev/null +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -0,0 +1,85 @@ +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'; + +const logger = new Logger('ChatwootController'); + +export class ChatwootController { + constructor( + private readonly chatwootService: ChatwootService, + private readonly configService: ConfigService, + ) {} + + 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.account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!data.token) { + throw new BadRequestException('token is required'); + } + + if (!data.sign_msg) { + throw new BadRequestException('sign_msg is required'); + } + } + + if (!data.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + data.sign_msg = false; + } + + data.name_inbox = instance.instanceName; + + const result = this.chatwootService.create(instance, data); + + const urlServer = this.configService.get('SERVER').URL; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; + } + + 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; + + const response = { + ...result, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; + } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose( + 'requested receiveWebhook from ' + instance.instanceName + ' instance', + ); + const chatwootService = new ChatwootService(waMonitor); + + return chatwootService.receiveWebhook(instance, data); + } +} diff --git a/src/whatsapp/controllers/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index e0254796..f4d381ce 100644 --- a/src/whatsapp/controllers/group.controller.ts +++ b/src/whatsapp/controllers/group.controller.ts @@ -1,5 +1,6 @@ import { CreateGroupDto, + GetParticipant, GroupDescriptionDto, GroupInvite, GroupJid, @@ -59,11 +60,13 @@ export class GroupController { return await this.waMonitor.waInstances[instance.instanceName].findGroup(groupJid); } - public async fetchAllGroups(instance: InstanceDto) { + public async fetchAllGroups(instance: InstanceDto, getPaticipants: GetParticipant) { logger.verbose( 'requested fetchAllGroups from ' + instance.instanceName + ' instance', ); - return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups(); + return await this.waMonitor.waInstances[instance.instanceName].fetchAllGroups( + getPaticipants, + ); } public async inviteCode(instance: InstanceDto, groupJid: GroupJid) { diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index c2abbffa..ab2c4229 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -1,6 +1,6 @@ import { delay } from '@whiskeysockets/baileys'; import EventEmitter2 from 'eventemitter2'; -import { Auth, ConfigService } from '../../config/env.config'; +import { Auth, ConfigService, HttpServer } from '../../config/env.config'; import { BadRequestException, InternalServerErrorException } from '../../exceptions'; import { InstanceDto } from '../dto/instance.dto'; import { RepositoryBroker } from '../repository/repository.manager'; @@ -8,6 +8,7 @@ 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'; @@ -20,6 +21,7 @@ export class InstanceController { private readonly eventEmitter: EventEmitter2, private readonly authService: AuthService, private readonly webhookService: WebhookService, + private readonly chatwootService: ChatwootService, private readonly cache: RedisCache, ) {} @@ -32,6 +34,10 @@ export class InstanceController { events, qrcode, token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, + chatwoot_sign_msg, }: InstanceDto) { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -91,16 +97,71 @@ export class InstanceController { } } - this.logger.verbose('instance created'); - this.logger.verbose({ - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - events: getEvents, - }); + if ( + !chatwoot_account_id || + !chatwoot_token || + !chatwoot_url || + !chatwoot_sign_msg + ) { + this.logger.verbose('instance created'); + this.logger.verbose({ + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + events: getEvents, + }); + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + events: getEvents, + }; + } + + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!chatwoot_sign_msg) { + throw new BadRequestException('sign_msg 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, + name_inbox: instance.instanceName, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + ); + } catch (error) { + this.logger.log(error); + } return { instance: { @@ -108,8 +169,15 @@ export class InstanceController { status: 'created', }, hash, - webhook, - events: getEvents, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, }; } else { this.logger.verbose('server mode'); @@ -159,27 +227,84 @@ export class InstanceController { } } - let getQrcode: wa.QrCode; + if ( + !chatwoot_account_id || + !chatwoot_token || + !chatwoot_url || + !chatwoot_sign_msg + ) { + let getQrcode: wa.QrCode; - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(); - await delay(2000); - getQrcode = instance.qrCode; + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(); + await delay(2000); + getQrcode = instance.qrCode; + } + + this.logger.verbose('instance created'); + this.logger.verbose({ + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }); + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }; } - this.logger.verbose('instance created'); - this.logger.verbose({ - instance: { - instanceName: instance.instanceName, - status: 'created', - }, - hash, - webhook, - webhook_by_events, - events: getEvents, - qrcode: getQrcode, - }); + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } + + if (!chatwoot_token) { + throw new BadRequestException('token is required'); + } + + if (!chatwoot_url) { + throw new BadRequestException('url is required'); + } + + if (!chatwoot_sign_msg) { + throw new BadRequestException('sign_msg 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, + name_inbox: instance.instanceName, + }); + + this.chatwootService.initInstanceChatwoot( + instance, + instance.instanceName, + `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + qrcode, + ); + } catch (error) { + this.logger.log(error); + } return { instance: { @@ -190,7 +315,15 @@ export class InstanceController { webhook, webhook_by_events, events: getEvents, - qrcode: getQrcode, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + sign_msg: chatwoot_sign_msg, + name_inbox: instance.instanceName, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, + }, }; } } @@ -268,6 +401,14 @@ export class InstanceController { public async logout({ instanceName }: InstanceDto) { this.logger.verbose('requested logout from ' + instanceName + ' instance'); + const stateConn = await this.connectionState({ instanceName }); + + if (stateConn.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( @@ -288,10 +429,9 @@ export class InstanceController { const stateConn = await this.connectionState({ instanceName }); if (stateConn.state === 'open') { - throw new BadRequestException([ - 'Deletion failed', - 'The instance needs to be disconnected', - ]); + throw new BadRequestException( + 'The "' + instanceName + '" instance needs to be disconnected', + ); } try { if (stateConn.state === 'connecting') { diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts new file mode 100644 index 00000000..e78b0676 --- /dev/null +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -0,0 +1,8 @@ +export class ChatwootDto { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; +} diff --git a/src/whatsapp/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index fba86ae2..bc36e27f 100644 --- a/src/whatsapp/dto/group.dto.ts +++ b/src/whatsapp/dto/group.dto.ts @@ -23,6 +23,10 @@ export class GroupJid { groupJid: string; } +export class GetParticipant { + getParticipants: string; +} + export class GroupInvite { inviteCode: string; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 8a3902e9..ce282e03 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -5,4 +5,8 @@ export class InstanceDto { events?: string[]; qrcode?: boolean; token?: string; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; + chatwoot_sign_msg?: boolean; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts new file mode 100644 index 00000000..ca082309 --- /dev/null +++ b/src/whatsapp/models/chatwoot.model.ts @@ -0,0 +1,29 @@ +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; +} + +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 }, +}); + +export const ChatwootModel = dbserver?.model( + ChatwootRaw.name, + chatwootSchema, + 'chatwoot', +); +export type IChatwootModel = typeof ChatwootModel; diff --git a/src/whatsapp/models/index.ts b/src/whatsapp/models/index.ts index 11f760d9..e0b773f0 100644 --- a/src/whatsapp/models/index.ts +++ b/src/whatsapp/models/index.ts @@ -3,3 +3,4 @@ export * from './contact.model'; export * from './message.model'; export * from './auth.model'; export * from './webhook.model'; +export * from './chatwoot.model'; diff --git a/src/whatsapp/repository/chatwoot.repository.ts b/src/whatsapp/repository/chatwoot.repository.ts new file mode 100644 index 00000000..3d24022a --- /dev/null +++ b/src/whatsapp/repository/chatwoot.repository.ts @@ -0,0 +1,75 @@ +import { IInsert, Repository } from '../abstract/abstract.repository'; +import { ConfigService } from '../../config/env.config'; +import { join } from 'path'; +import { readFileSync } from 'fs'; +import { IChatwootModel, ChatwootRaw } from '../models'; +import { Logger } from '../../config/logger.config'; + +export class ChatwootRepository extends Repository { + constructor( + private readonly chatwootModel: IChatwootModel, + private readonly configService: ConfigService, + ) { + super(configService); + } + + private readonly logger = new Logger('ChatwootRepository'); + + public async create(data: ChatwootRaw, instance: string): Promise { + try { + this.logger.verbose('creating chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('saving chatwoot to db'); + const insert = await this.chatwootModel.replaceOne( + { _id: instance }, + { ...data }, + { upsert: true }, + ); + + this.logger.verbose( + 'chatwoot saved to db: ' + insert.modifiedCount + ' chatwoot', + ); + return { insertCount: insert.modifiedCount }; + } + + this.logger.verbose('saving chatwoot to store'); + + this.writeStore({ + path: join(this.storePath, 'chatwoot'), + fileName: instance, + data, + }); + + this.logger.verbose( + 'chatwoot saved to store in path: ' + + join(this.storePath, 'chatwoot') + + '/' + + instance, + ); + + this.logger.verbose('chatwoot created'); + return { insertCount: 1 }; + } catch (error) { + return error; + } + } + + public async find(instance: string): Promise { + try { + this.logger.verbose('finding chatwoot'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding chatwoot in db'); + return await this.chatwootModel.findOne({ _id: instance }); + } + + this.logger.verbose('finding chatwoot in store'); + return JSON.parse( + readFileSync(join(this.storePath, 'chatwoot', instance + '.json'), { + encoding: 'utf-8', + }), + ) as ChatwootRaw; + } catch (error) { + return {}; + } + } +} diff --git a/src/whatsapp/repository/repository.manager.ts b/src/whatsapp/repository/repository.manager.ts index bd41e09e..9740b436 100644 --- a/src/whatsapp/repository/repository.manager.ts +++ b/src/whatsapp/repository/repository.manager.ts @@ -4,6 +4,7 @@ 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 { AuthRepository } from './auth.repository'; import { Auth, ConfigService, Database } from '../../config/env.config'; import { execSync } from 'child_process'; @@ -17,6 +18,7 @@ export class RepositoryBroker { public readonly contact: ContactRepository, public readonly messageUpdate: MessageUpRepository, public readonly webhook: WebhookRepository, + public readonly chatwoot: ChatwootRepository, public readonly auth: AuthRepository, private configService: ConfigService, dbServer?: MongoClient, @@ -64,6 +66,9 @@ export class RepositoryBroker { 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')}`); } diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts new file mode 100644 index 00000000..3d87f137 --- /dev/null +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -0,0 +1,68 @@ +import { RequestHandler, Router } from 'express'; +import { instanceNameSchema, chatwootSchema } from '../../validate/validate.schema'; +import { RouterBroker } from '../abstract/abstract.router'; +import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { chatwootController } from '../whatsapp.module'; +import { ChatwootService } from '../services/chatwoot.service'; +import { HttpStatus } from './index.router'; +import { Logger } from '../../config/logger.config'; + +const logger = new Logger('ChatwootRouter'); + +export class ChatwootRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('set'), ...guards, async (req, res) => { + logger.verbose('request received in setChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: chatwootSchema, + ClassRef: ChatwootDto, + execute: (instance, data) => chatwootController.createChatwoot(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('find'), ...guards, async (req, res) => { + logger.verbose('request received in findChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance) => chatwootController.findChatwoot(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('webhook'), async (req, res) => { + logger.verbose('request received in findChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance, data) => chatwootController.receiveWebhook(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 8cb3032e..4c1b3023 100644 --- a/src/whatsapp/routers/group.router.ts +++ b/src/whatsapp/routers/group.router.ts @@ -10,6 +10,7 @@ import { updateGroupDescriptionSchema, groupInviteSchema, groupSendInviteSchema, + getParticipantsSchema, } from '../../validate/validate.schema'; import { RouterBroker } from '../abstract/abstract.router'; import { @@ -23,6 +24,7 @@ import { GroupUpdateSettingDto, GroupToggleEphemeralDto, GroupSendInvite, + GetParticipant, } from '../dto/group.dto'; import { groupController } from '../whatsapp.module'; import { HttpStatus } from './index.router'; @@ -123,11 +125,11 @@ export class GroupRouter extends RouterBroker { logger.verbose('request query: '); logger.verbose(req.query); - const response = await this.groupNoValidate({ + const response = await this.getParticipantsValidate({ request: req, - schema: {}, - ClassRef: GroupJid, - execute: (instance) => groupController.fetchAllGroups(instance), + schema: getParticipantsSchema, + ClassRef: GetParticipant, + execute: (instance, data) => groupController.fetchAllGroups(instance, data), }); res.status(HttpStatus.OK).json(response); diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 80f23c41..082ffe69 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -8,6 +8,7 @@ import { InstanceRouter } from './instance.router'; import { MessageRouter } from './sendMessage.router'; import { ViewsRouter } from './view.router'; import { WebhookRouter } from './webhook.router'; +import { ChatwootRouter } from './chatwoot.router'; enum HttpStatus { OK = 200, @@ -24,6 +25,12 @@ const authType = configService.get('AUTHENTICATION').TYPE; const guards = [instanceExistsGuard, instanceLoggedGuard, authGuard[authType]]; router + .get('/', (req, res) => { + res.status(HttpStatus.OK).json({ + status: HttpStatus.OK, + message: 'Welcome to the Evolution API, it is working!', + }); + }) .use( '/instance', new InstanceRouter(configService, ...guards).router, @@ -32,6 +39,7 @@ router .use('/message', new MessageRouter(...guards).router) .use('/chat', new ChatRouter(...guards).router) .use('/group', new GroupRouter(...guards).router) - .use('/webhook', new WebhookRouter(...guards).router); + .use('/webhook', new WebhookRouter(...guards).router) + .use('/chatwoot', new ChatwootRouter(...guards).router); export { router, HttpStatus }; diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts new file mode 100644 index 00000000..4500fde8 --- /dev/null +++ b/src/whatsapp/services/chatwoot.service.ts @@ -0,0 +1,1430 @@ +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 mimeTypes from 'mime-types'; +import { SendAudioDto } from '../dto/sendMessage.dto'; +import { SendMediaDto } from '../dto/sendMessage.dto'; +import { ROOT_DIR } from '../../config/path.config'; + +export class ChatwootService { + private messageCacheFile: string; + private messageCache: Set; + + private readonly logger = new Logger(ChatwootService.name); + + private provider: any; + + constructor(private readonly waMonitor: WAMonitoringService) { + 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 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; + } + + 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, + ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inbox in chatwoot'); + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); + + this.logger.verbose('check duplicate inbox'); + const checkDuplicate = findInbox.payload + .map((inbox) => inbox.name) + .includes(inboxName); + + let inboxId: number; + + if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); + const data = { + type: 'api', + webhook_url: webhookUrl, + }; + + const inbox = await client.inboxes.create({ + accountId: this.provider.account_id, + data: { + name: inboxName, + channel: data as any, + }, + }); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; + } else { + this.logger.verbose('find inbox in chatwoot'); + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + inboxId = inbox.id; + } + + this.logger.verbose('find contact in chatwoot and create if not exists'); + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact( + instance, + '123456', + inboxId, + false, + 'EvolutionAPI', + )) as any); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + this.logger.verbose('create conversation in chatwoot'); + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('create message for init instance in chatwoot'); + const message = await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: '/iniciar', + 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, + ) { + 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}`, + }; + } else { + this.logger.verbose('create contact group in chatwoot'); + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + }; + } + + 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 participant = + (await this.findContact(instance, body.key.participant.split('@')[0])) || + ((await this.createContact( + instance, + body.key.participant.split('@')[0], + filterInbox.id, + false, + body.pushName || body.key.participant.split('@')[0], + )) as any); + } + + this.logger.verbose('find or create contact in chatwoot'); + const contact = + (await this.findContact(instance, chatId)) || + ((await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + )) as any); + + if (!contact) { + this.logger.warn('contact not found'); + return null; + } + + const contactId = contact.id || contact.payload.contact.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + this.logger.verbose('update contact name in chatwoot'); + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } + + this.logger.verbose('get contact conversations in chatwoot'); + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + this.logger.verbose('return conversation if exists'); + const conversation = contactConversations.payload.find( + (conversation) => + conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + if (conversation) { + this.logger.verbose('conversation found'); + return conversation.id; + } + } + + this.logger.verbose('create conversation in chatwoot'); + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: `${contactId}`, + inbox_id: `${filterInbox.id}`, + }, + }); + + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); + return conversation.id; + } catch (error) { + this.logger.error(error); + } + } + + public async getInbox(instance: InstanceDto) { + this.logger.verbose('get inbox to instance: ' + instance.instanceName); + + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + this.logger.verbose('find inboxes in chatwoot'); + const inbox = (await client.inboxes.list({ + accountId: this.provider.account_id, + })) as any; + + if (!inbox) { + this.logger.warn('inbox not found'); + return null; + } + + 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 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 === 'iniciar') { + this.logger.verbose('command iniciar found'); + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + this.logger.verbose('connect to whatsapp'); + await waInstance.connectToWhatsapp(); + } else { + this.logger.verbose('whatsapp already connected'); + await this.createBotMessage( + instance, + `🚨 Instância ${body.inbox.name} já está conectada.`, + '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, + `⚠️ Instância ${body.inbox.name} não existe.`, + 'incoming', + ); + } + + if (state) { + this.logger.verbose('state: ' + state + ' found'); + await this.createBotMessage( + instance, + `⚠️ Status da instância ${body.inbox.name}: *${state}*`, + 'incoming', + ); + } + } + + if (command === 'desconectar') { + this.logger.verbose('command desconectar found'); + + const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${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 ( + 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('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.content_type === 'input_csat') { + this.logger.verbose('check if is csat'); + + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + this.logger.verbose('send text to whatsapp'); + + await waInstance?.textMessage(data); + } + + return { message: 'bot' }; + } catch (error) { + this.logger.error(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); + const media = [ + 'imageMessage', + 'documentMessage', + 'documentWithCaptionMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + + const result = messageKeys.some((key) => media.includes(key)); + + this.logger.verbose('is media message: ' + result); + return result; + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + imageMessage: msg.imageMessage?.caption, + videoMessage: msg.videoMessage?.caption, + extendedTextMessage: msg.extendedTextMessage?.text, + messageContextInfo: msg.messageContextInfo?.stanzaId, + stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), + documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: + msg.documentWithCaptionMessage?.message?.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + }; + + 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; + + this.logger.verbose('message content: ' + result); + + return result; + } + + private getConversationMessage(msg: any) { + this.logger.verbose('get conversation message'); + + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + this.logger.verbose('conversation message: ' + messageContent); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); + try { + const client = await this.clientCw(instance); + + if (!client) { + this.logger.warn('client not found'); + return null; + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (!waInstance) { + this.logger.warn('wa instance not found'); + return null; + } + + if (event === 'messages.upsert') { + this.logger.verbose('event messages.upsert'); + + if (body.key.remoteJid === 'status@broadcast') { + this.logger.verbose('status broadcast found'); + return; + } + + this.logger.verbose('get conversation 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); + + const isMedia = this.isMediaMessage(body.message); + + this.logger.verbose('is media: ' + isMedia); + + this.logger.verbose('get conversation message'); + const bodyMessage = await this.getConversationMessage(body.message); + + if (!bodyMessage && !isMedia) { + this.logger.warn('no body message found'); + return; + } + + this.logger.verbose('check if is media'); + if (isMedia) { + this.logger.verbose('message is media'); + + 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 = `⚡️ Status da instância ${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.state === 'open') { + const msgConnection = `🚀 Conexão realizada com sucesso!`; + + 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.`; + + 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 gerado com sucesso!', + 'incoming', + fileName, + ); + + const msgQrCode = `⚡️ QRCode gerado com sucesso!\n\nDigitalize este código QR nos próximos 40 segundos:`; + + this.logger.verbose('send message to chatwoot'); + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + this.logger.error(error); + } + } +} diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index fc261866..462c9f96 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -9,6 +9,7 @@ import { ConfigService, Database, DelInstance, + HttpServer, Redis, } from '../../config/env.config'; import { RepositoryBroker } from '../repository/repository.manager'; @@ -83,6 +84,19 @@ export class WAMonitoringService { for await (const [key, value] of Object.entries(this.waInstances)) { if (value) { this.logger.verbose('get instance info: ' + key); + let chatwoot: any; + + const urlServer = this.configService.get('SERVER').URL; + + const findChatwoot = await this.waInstances[key].findChatwoot(); + + if (findChatwoot.enabled) { + chatwoot = { + ...findChatwoot, + webhook_url: `${urlServer}/chatwoot/webhook/${key}`, + }; + } + if (value.connectionStatus.state === 'open') { this.logger.verbose('instance: ' + key + ' - connectionStatus: open'); let apikey: string; @@ -99,8 +113,10 @@ export class WAMonitoringService { owner: value.wuid, profileName: (await value.getProfileName()) || 'not loaded', profilePictureUrl: value.profilePictureUrl, - status: (await value.getProfileStatus()) || '', + profileStatus: (await value.getProfileStatus()) || '', + status: value.connectionStatus.state, apikey, + chatwoot, }, }); } else { @@ -113,7 +129,8 @@ export class WAMonitoringService { owner: value.wuid, profileName: (await value.getProfileName()) || 'not loaded', profilePictureUrl: value.profilePictureUrl, - status: (await value.getProfileStatus()) || '', + profileStatus: (await value.getProfileStatus()) || '', + status: value.connectionStatus.state, }, }); } @@ -134,6 +151,7 @@ export class WAMonitoringService { instanceName: key, status: value.connectionStatus.state, apikey, + chatwoot, }, }); } else { @@ -232,6 +250,7 @@ export class WAMonitoringService { execSync(`rm -rf ${join(STORE_DIR, 'auth', 'apikey', instanceName + '.json')}`); execSync(`rm -rf ${join(STORE_DIR, 'webhook', instanceName + '.json')}`); + execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`); } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3bfa25f0..027a97df 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -30,7 +30,6 @@ import makeWASocket, { WAMessageUpdate, WASocket, getAggregateVotesInPollMessage, - Browsers, } from '@whiskeysockets/baileys'; import { Auth, @@ -38,6 +37,7 @@ import { ConfigService, ConfigSessionPhone, Database, + HttpServer, QrCode, Redis, Webhook, @@ -109,11 +109,13 @@ import { GroupSubjectDto, GroupDescriptionDto, GroupSendInvite, + GetParticipant, } from '../dto/group.dto'; import { MessageUpQuery } from '../repository/messageUp.repository'; import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db'; import Long from 'long'; import { WebhookRaw } from '../models/webhook.model'; +import { ChatwootRaw } from '../models/chatwoot.model'; import { dbserver } from '../../db/db.connect'; import NodeCache from 'node-cache'; import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db'; @@ -121,6 +123,8 @@ import sharp from 'sharp'; import { RedisCache } from '../../db/redis.client'; import { Log } from '../../config/env.config'; import ProxyAgent from 'proxy-agent'; +import { ChatwootService } from './chatwoot.service'; +import { waMonitor } from '../whatsapp.module'; export class WAStartupService { constructor( @@ -138,13 +142,16 @@ export class WAStartupService { private readonly instance: wa.Instance = {}; public client: WASocket; private readonly localWebhook: wa.LocalWebHook = {}; + private readonly localChatwoot: wa.LocalChatwoot = {}; private stateConnection: wa.StateConnection = { state: 'close' }; - private readonly storePath = join(ROOT_DIR, 'store'); + 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 chatwootService = new ChatwootService(waMonitor); + public set instanceName(name: string) { this.logger.verbose(`Initializing instance '${name}'`); if (!name) { @@ -159,6 +166,17 @@ export class WAStartupService { 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() { @@ -268,8 +286,64 @@ export class WAStartupService { return data; } + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.localChatwoot.name_inbox = data?.name_inbox; + this.logger.verbose(`Chatwoot inbox name: ${this.localChatwoot.name_inbox}`); + + this.localChatwoot.sign_msg = data?.sign_msg; + this.logger.verbose(`Chatwoot sign msg: ${this.localChatwoot.sign_msg}`); + + this.logger.verbose('Chatwoot loaded'); + } + + public async setChatwoot(data: ChatwootRaw) { + this.logger.verbose('Setting chatwoot'); + await this.repository.chatwoot.create(data, this.instanceName); + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + + Object.assign(this.localChatwoot, data); + this.logger.verbose('Chatwoot set'); + } + + public async findChatwoot() { + this.logger.verbose('Finding chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + + if (!data) { + this.logger.verbose('Chatwoot not found'); + throw new NotFoundException('Chatwoot not found'); + } + + this.logger.verbose(`Chatwoot account id: ${data.account_id}`); + this.logger.verbose(`Chatwoot token: ${data.token}`); + this.logger.verbose(`Chatwoot url: ${data.url}`); + this.logger.verbose(`Chatwoot inbox name: ${data.name_inbox}`); + this.logger.verbose(`Chatwoot sign msg: ${data.sign_msg}`); + + return data; + } + 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 we = event.replace(/[\.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); @@ -294,6 +368,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + urlServer, }); } @@ -305,6 +380,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + urlServer, }); } } catch (error) { @@ -352,6 +428,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + urlServer, }); } @@ -363,6 +440,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + urlServer, }); } } catch (error) { @@ -399,6 +477,17 @@ export class WAStartupService { 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, @@ -412,6 +501,17 @@ export class WAStartupService { 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; @@ -442,6 +542,16 @@ export class WAStartupService { this.sendDataWebhook(Events.QRCODE_UPDATED, { qrcode: { instance: this.instance.name, code: qr, base64 }, }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { instance: this.instance.name, code: qr, base64 }, + }, + ); + } }); this.logger.verbose('Generating QR code in terminal'); @@ -482,6 +592,17 @@ export class WAStartupService { 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(); @@ -596,14 +717,14 @@ export class WAStartupService { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); + this.loadChatwoot(); 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()]; - const browser: WABrowserDescription = Browsers.appropriate(session.CLIENT); + const browser: WABrowserDescription = [session.CLIENT, session.NAME, release()]; this.logger.verbose('Browser: ' + JSON.stringify(browser)); const socketConfig: UserFacingSocketConfig = { @@ -787,6 +908,14 @@ 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, @@ -910,6 +1039,14 @@ export class WAStartupService { 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], @@ -948,6 +1085,14 @@ export class WAStartupService { 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], @@ -1160,6 +1305,8 @@ export class WAStartupService { } return match[1] === '52' ? '52' + match[3] : '54' + match[3]; } + + return jid; } return jid; } @@ -1177,6 +1324,7 @@ export class WAStartupService { } return match[1] + match[2] + match[3]; } + return jid; } else { return jid; } @@ -1203,6 +1351,7 @@ export class WAStartupService { } const formattedMXARNumber = this.formatMXOrARNumber(number); + if (formattedMXARNumber !== number) { this.logger.verbose( 'Jid created is whatsapp in format MXAR: ' + @@ -1247,22 +1396,15 @@ export class WAStartupService { this.logger.verbose('Sending message with typing'); const jid = this.createJid(number); - const isWA = (await this.whatsappNumber({ numbers: [jid] }))[0]; + const numberWA = await this.whatsappNumber({ numbers: [jid] }); + const isWA = numberWA[0]; + if (!isWA.exists && !isJidGroup(isWA.jid) && !isWA.jid.includes('@broadcast')) { throw new BadRequestException(isWA); } const sender = isJidGroup(jid) ? jid : isWA.jid; - if (isJidGroup(sender)) { - try { - this.logger.verbose('Getting group metadata'); - await this.client.groupMetadata(sender); - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - try { if (options?.delay) { this.logger.verbose('Delaying message'); @@ -1290,29 +1432,38 @@ export class WAStartupService { } let mentions: string[]; + if (isJidGroup(sender)) { + try { + if (options?.mentions) { + this.logger.verbose('Mentions defined'); - if (options?.mentions) { - this.logger.verbose('Mentions defined'); - - if (!Array.isArray(options.mentions.mentioned) && !options.mentions.everyOne) { - throw new BadRequestException('Mentions must be an array'); - } - - if (options.mentions.everyOne) { - this.logger.verbose('Mentions everyone'); - - const groupMetadata = await this.client.groupMetadata(sender); - mentions = groupMetadata.participants.map((participant) => participant.id); - this.logger.verbose('Getting group metadata for mentions'); - } else { - this.logger.verbose('Mentions manually defined'); - mentions = options.mentions.mentioned.map((mention) => { - const jid = this.createJid(mention); - if (isJidGroup(jid)) { - throw new BadRequestException('Mentions must be a number'); + if ( + !Array.isArray(options.mentions.mentioned) && + !options.mentions.everyOne + ) { + throw new BadRequestException('Mentions must be an array'); } - return jid; - }); + + if (options.mentions.everyOne) { + 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 { + this.logger.verbose('Mentions manually defined'); + mentions = options.mentions.mentioned.map((mention) => { + const jid = this.createJid(mention); + if (isJidGroup(jid)) { + throw new BadRequestException('Mentions must be a number'); + } + return jid; + }); + } + } + } catch (error) { + throw new NotFoundException('Group not found'); } } @@ -1357,7 +1508,6 @@ export class WAStartupService { if (sender.includes('@broadcast')) { this.logger.verbose('Sending message'); - console.log(message['status']); return await this.client.sendMessage( sender, message['status'].content as unknown as AnyMessageContent, @@ -1392,6 +1542,14 @@ export class WAStartupService { 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], @@ -1954,13 +2112,19 @@ export class WAStartupService { const 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 { - try { - const result = (await this.client.onWhatsApp(jid))[0]; + 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)); - } catch (error) { - onWhatsapp.push(new OnWhatsAppDto(number, false)); } } } @@ -2414,10 +2578,34 @@ export class WAStartupService { } } - public async fetchAllGroups() { + public async fetchAllGroups(getParticipants: GetParticipant) { this.logger.verbose('Fetching all groups'); try { - return await this.client.groupFetchAllParticipating(); + 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()); } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 3432f1b5..1ebe3b40 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -6,10 +6,10 @@ export enum Events { QRCODE_UPDATED = 'qrcode.updated', CONNECTION_UPDATE = 'connection.update', STATUS_INSTANCE = 'status.instance', - SEND_MESSAGE = 'send.message', MESSAGES_SET = 'messages.set', MESSAGES_UPSERT = 'messages.upsert', MESSAGES_UPDATE = 'messages.update', + SEND_MESSAGE = 'send.message', CONTACTS_SET = 'contacts.set', CONTACTS_UPSERT = 'contacts.upsert', CONTACTS_UPDATE = 'contacts.update', @@ -41,6 +41,15 @@ export declare namespace wa { webhook_by_events?: boolean; }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + sign_msg?: boolean; + }; + export type StateConnection = { instance?: string; state?: WAConnectionState | 'refused'; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index f05b8323..c57b9bf8 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -14,6 +14,8 @@ 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, @@ -21,10 +23,12 @@ import { ContactModel, MessageModel, MessageUpModel, + ChatwootModel, + WebhookModel, } from './models'; import { dbserver } from '../db/db.connect'; import { WebhookRepository } from './repository/webhook.repository'; -import { WebhookModel } from './models/webhook.model'; +import { ChatwootRepository } from './repository/chatwoot.repository'; import { AuthRepository } from './repository/auth.repository'; import { WAStartupService } from './services/whatsapp.service'; import { delay } from '@whiskeysockets/baileys'; @@ -38,6 +42,7 @@ const chatRepository = new ChatRepository(ChatModel, configService); const contactRepository = new ContactRepository(ContactModel, configService); const messageUpdateRepository = new MessageUpRepository(MessageUpModel, configService); const webhookRepository = new WebhookRepository(WebhookModel, configService); +const chatwootRepository = new ChatwootRepository(ChatwootModel, configService); const authRepository = new AuthRepository(AuthModel, configService); export const repository = new RepositoryBroker( @@ -46,6 +51,7 @@ export const repository = new RepositoryBroker( contactRepository, messageUpdateRepository, webhookRepository, + chatwootRepository, authRepository, configService, dbserver?.getClient(), @@ -66,6 +72,10 @@ const webhookService = new WebhookService(waMonitor); export const webhookController = new WebhookController(webhookService); +const chatwootService = new ChatwootService(waMonitor); + +export const chatwootController = new ChatwootController(chatwootService, configService); + export const instanceController = new InstanceController( waMonitor, configService, @@ -73,6 +83,7 @@ export const instanceController = new InstanceController( eventEmitter, authService, webhookService, + chatwootService, cache, ); export const viewsController = new ViewsController(waMonitor, configService); @@ -105,6 +116,17 @@ export async function initInstance() { configService.get('AUTHENTICATION').INSTANCE.WEBHOOK_URL; logger.verbose('Instance webhook: ' + instanceWebhook); + const chatwootAccountId = + configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_ACCOUNT_ID; + logger.verbose('Chatwoot account id: ' + chatwootAccountId); + + const chatwootToken = + configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_TOKEN; + logger.verbose('Chatwoot token: ' + chatwootToken); + + const chatwootUrl = configService.get('AUTHENTICATION').INSTANCE.CHATWOOT_URL; + logger.verbose('Chatwoot url: ' + chatwootUrl); + instance.instanceName = instanceName; waMonitor.waInstances[instance.instanceName] = instance; @@ -126,6 +148,22 @@ export async function initInstance() { } } + if (chatwootUrl && chatwootToken && chatwootAccountId) { + logger.verbose('Creating chatwoot for instance: ' + instanceName); + try { + chatwootService.create(instance, { + enabled: true, + url: chatwootUrl, + token: chatwootToken, + account_id: chatwootAccountId, + sign_msg: false, + }); + logger.verbose('Chatwoot created'); + } catch (error) { + logger.log(error); + } + } + try { const state = instance.connectionStatus?.state;