From 95045db74e9f531008142edd402d4a770d37cf5b Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 11:15:45 -0300 Subject: [PATCH 01/41] fix: Adjusts in number validation for AR and MX numbers --- CHANGELOG.md | 7 +++++++ Docker/mongodb/docker-compose.yaml | 12 +++++++----- Docker/redis/docker-compose.yaml | 10 ++++++++++ package.json | 4 ++-- src/whatsapp/services/whatsapp.service.ts | 20 ++++++++++++++++---- 5 files changed, 42 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2a59f72a..afd077da1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 1.1.6 (homolog) + +### Fixed + +* Adjusts in docker-compose files +* Adjusts in number validation for AR and MX numbers + # 1.1.5 (2023-07-12 07:17) ### Fixed diff --git a/Docker/mongodb/docker-compose.yaml b/Docker/mongodb/docker-compose.yaml index 714109c90..2e4d74f36 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 55e738473..229603976 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -8,6 +8,16 @@ services: redis: image: redis:latest container_name: redis + command: > + redis-server + --port 6379 + --appendonly yes + --save 900 1 + --save 300 10 + --save 60 10000 + --appendfsync everysec + volumes: + - evolution_redis:/data ports: - 6379:6379 networks: diff --git a/package.json b/package.json index dd1b8743c..f11b12292 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.1.4", + "version": "1.1.6", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { @@ -43,7 +43,7 @@ "@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", "axios": "^1.3.5", "class-validator": "^0.13.2", "compression": "^1.7.4", diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 3bfa25f07..8918d610a 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1160,6 +1160,8 @@ export class WAStartupService { } return match[1] === '52' ? '52' + match[3] : '54' + match[3]; } + + return jid; } return jid; } @@ -1177,12 +1179,14 @@ export class WAStartupService { } return match[1] + match[2] + match[3]; } + return jid; } else { return jid; } } private createJid(number: string): string { + console.log(number); this.logger.verbose('Creating jid with number: ' + number); if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); @@ -1203,6 +1207,8 @@ export class WAStartupService { } const formattedMXARNumber = this.formatMXOrARNumber(number); + console.log(formattedMXARNumber, number); + if (formattedMXARNumber !== number) { this.logger.verbose( 'Jid created is whatsapp in format MXAR: ' + @@ -1951,16 +1957,22 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { + console.log('number', number); const jid = this.createJid(number); + console.log('jid', jid); if (isJidGroup(jid)) { const group = await this.findGroup({ groupJid: jid }, 'inner'); 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 { + console.log('onWhatsapp', result); onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); - } catch (error) { - onWhatsapp.push(new OnWhatsAppDto(number, false)); } } } From 0a4e52e6634af6c2de267c34478ecc5f419d96a9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 11:52:00 -0300 Subject: [PATCH 02/41] fix: Adjusts in env files, removed save old_messages --- CHANGELOG.md | 1 + Docker/.env.example | 1 - Docker/redis/docker-compose.yaml | 4 ---- Dockerfile | 1 - src/config/env.config.ts | 2 -- src/dev-env.yml | 1 - 6 files changed, 1 insertion(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd077da1..c038f6f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Adjusts in docker-compose files * Adjusts in number validation for AR and MX numbers +* Adjusts in env files, removed save old_messages # 1.1.5 (2023-07-12 07:17) diff --git a/Docker/.env.example b/Docker/.env.example index b7fdd95b1..5a2946f1d 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -31,7 +31,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 diff --git a/Docker/redis/docker-compose.yaml b/Docker/redis/docker-compose.yaml index 229603976..5b102b114 100644 --- a/Docker/redis/docker-compose.yaml +++ b/Docker/redis/docker-compose.yaml @@ -12,10 +12,6 @@ services: redis-server --port 6379 --appendonly yes - --save 900 1 - --save 300 10 - --save 60 10000 - --appendfsync everysec volumes: - evolution_redis:/data ports: diff --git a/Dockerfile b/Dockerfile index 3cdbeee6c..5c477518f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,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 diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7fa4fe5cc..52518c505 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -33,7 +33,6 @@ export type Log = { export type SaveData = { INSTANCE: boolean; - OLD_MESSAGE: boolean; NEW_MESSAGE: boolean; MESSAGE_UPDATE: boolean; CONTACTS: boolean; @@ -203,7 +202,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', diff --git a/src/dev-env.yml b/src/dev-env.yml index 86fd00666..590ca50a9 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -68,7 +68,6 @@ DATABASE: # 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 From ae8801dd8a752e32b612f6136414f7d899628d70 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 14:10:24 -0300 Subject: [PATCH 03/41] fix: Adjusts in env files, removed save old_messages --- CHANGELOG.md | 1 + Docker/.env.example | 2 ++ src/dev-env.yml | 2 ++ src/whatsapp/services/whatsapp.service.ts | 17 +++++++++-------- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c038f6f74..e59e4eb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * 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 # 1.1.5 (2023-07-12 07:17) diff --git a/Docker/.env.example b/Docker/.env.example index 5a2946f1d..c15237b34 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -75,6 +75,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. diff --git a/src/dev-env.yml b/src/dev-env.yml index 590ca50a9..9dbd14aef 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -119,6 +119,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 diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 8918d610a..e2486e690 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1186,7 +1186,6 @@ export class WAStartupService { } private createJid(number: string): string { - console.log(number); this.logger.verbose('Creating jid with number: ' + number); if (number.includes('@g.us') || number.includes('@s.whatsapp.net')) { this.logger.verbose('Number already contains @g.us or @s.whatsapp.net'); @@ -1207,7 +1206,6 @@ export class WAStartupService { } const formattedMXARNumber = this.formatMXOrARNumber(number); - console.log(formattedMXARNumber, number); if (formattedMXARNumber !== number) { this.logger.verbose( @@ -1253,7 +1251,9 @@ 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); } @@ -1263,7 +1263,9 @@ export class WAStartupService { if (isJidGroup(sender)) { try { this.logger.verbose('Getting group metadata'); - await this.client.groupMetadata(sender); + const metadata = await this.client.groupMetadata(sender); + + console.log('metadata', metadata); } catch (error) { throw new NotFoundException('Group not found'); } @@ -1363,7 +1365,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, @@ -1957,11 +1958,12 @@ export class WAStartupService { const onWhatsapp: OnWhatsAppDto[] = []; for await (const number of data.numbers) { - console.log('number', number); const jid = this.createJid(number); - console.log('jid', jid); 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 { const verify = await this.client.onWhatsApp(jid); @@ -1971,7 +1973,6 @@ export class WAStartupService { if (!result) { onWhatsapp.push(new OnWhatsAppDto(jid, false)); } else { - console.log('onWhatsapp', result); onWhatsapp.push(new OnWhatsAppDto(result.jid, result.exists)); } } From a8bd32bef341456f0ff93445f23645534dae01be Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 14:31:05 -0300 Subject: [PATCH 04/41] fix: Adjusts in env files, removed save old_messages --- Docker/.env.example | 1 + Dockerfile | 1 + src/config/env.config.ts | 2 ++ src/dev-env.yml | 1 + src/validate/validate.schema.ts | 2 ++ 5 files changed, 7 insertions(+) diff --git a/Docker/.env.example b/Docker/.env.example index c15237b34..ca28842c5 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -49,6 +49,7 @@ WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false ## Set the events you want to hear WEBHOOK_EVENTS_APPLICATION_STARTUP=false WEBHOOK_EVENTS_QRCODE_UPDATED=true +WEBHOOK_EVENTS_SEND_MESSAGE=true WEBHOOK_EVENTS_MESSAGES_SET=true WEBHOOK_EVENTS_MESSAGES_UPSERT=true WEBHOOK_EVENTS_MESSAGES_UPDATE=true diff --git a/Dockerfile b/Dockerfile index 5c477518f..d9cf02432 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,7 @@ 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_SEND_MESSAGE=$WEBHOOK_EVENTS_SEND_MESSAGE 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 diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 52518c505..44850225b 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -73,6 +73,7 @@ export type Redis = { export type EventsWebhook = { APPLICATION_STARTUP: boolean; QRCODE_UPDATED: boolean; + SEND_MESSAGE: boolean; MESSAGES_SET: boolean; MESSAGES_UPSERT: boolean; MESSAGES_UPDATE: boolean; @@ -230,6 +231,7 @@ export class ConfigService { EVENTS: { APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', + SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', diff --git a/src/dev-env.yml b/src/dev-env.yml index 9dbd14aef..efa1cc5ae 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -91,6 +91,7 @@ WEBHOOK: EVENTS: APPLICATION_STARTUP: false QRCODE_UPDATED: true + SEND_MESSAGE: true MESSAGES_SET: true MESSAGES_UPSERT: true MESSAGES_UPDATE: true diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 9cf411e5f..4a349198d 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -36,6 +36,7 @@ export const instanceNameSchema: JSONSchema7 = { enum: [ 'APPLICATION_STARTUP', 'QRCODE_UPDATED', + 'SEND_MESSAGE', 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', @@ -832,6 +833,7 @@ export const webhookSchema: JSONSchema7 = { enum: [ 'APPLICATION_STARTUP', 'QRCODE_UPDATED', + 'SEND_MESSAGE', 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', From 86ce00365a132587cf6975da360c10e3f1a92d54 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 14:39:43 -0300 Subject: [PATCH 05/41] fix: Adjusts in env files, removed save old_messages --- Docker/.env.example | 2 +- Dockerfile | 4 ++-- src/config/env.config.ts | 4 ++-- src/dev-env.yml | 2 +- src/validate/validate.schema.ts | 4 ++-- src/whatsapp/types/wa.types.ts | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Docker/.env.example b/Docker/.env.example index ca28842c5..18fa32bdc 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -49,10 +49,10 @@ WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false ## Set the events you want to hear WEBHOOK_EVENTS_APPLICATION_STARTUP=false WEBHOOK_EVENTS_QRCODE_UPDATED=true -WEBHOOK_EVENTS_SEND_MESSAGE=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 diff --git a/Dockerfile b/Dockerfile index d9cf02432..c9ef576da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,10 +55,10 @@ 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_SEND_MESSAGE=$WEBHOOK_EVENTS_SEND_MESSAGE 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 diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 44850225b..6ad188b16 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -73,10 +73,10 @@ export type Redis = { export type EventsWebhook = { APPLICATION_STARTUP: boolean; QRCODE_UPDATED: boolean; - SEND_MESSAGE: boolean; MESSAGES_SET: boolean; MESSAGES_UPSERT: boolean; MESSAGES_UPDATE: boolean; + SEND_MESSAGE: boolean; CONTACTS_SET: boolean; CONTACTS_UPDATE: boolean; CONTACTS_UPSERT: boolean; @@ -231,10 +231,10 @@ export class ConfigService { EVENTS: { APPLICATION_STARTUP: process.env?.WEBHOOK_EVENTS_APPLICATION_STARTUP === 'true', QRCODE_UPDATED: process.env?.WEBHOOK_EVENTS_QRCODE_UPDATED === 'true', - SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true', MESSAGES_SET: process.env?.WEBHOOK_EVENTS_MESSAGES_SET === 'true', MESSAGES_UPSERT: process.env?.WEBHOOK_EVENTS_MESSAGES_UPSERT === 'true', MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true', + 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', diff --git a/src/dev-env.yml b/src/dev-env.yml index efa1cc5ae..85061e125 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -91,10 +91,10 @@ WEBHOOK: EVENTS: APPLICATION_STARTUP: false QRCODE_UPDATED: true - SEND_MESSAGE: true MESSAGES_SET: true MESSAGES_UPSERT: true MESSAGES_UPDATE: true + SEND_MESSAGE: true CONTACTS_SET: true CONTACTS_UPSERT: true CONTACTS_UPDATE: true diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 4a349198d..9427b1d77 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -36,10 +36,10 @@ export const instanceNameSchema: JSONSchema7 = { enum: [ 'APPLICATION_STARTUP', 'QRCODE_UPDATED', - 'SEND_MESSAGE', 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', + 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', 'CONTACTS_UPDATE', @@ -833,10 +833,10 @@ export const webhookSchema: JSONSchema7 = { enum: [ 'APPLICATION_STARTUP', 'QRCODE_UPDATED', - 'SEND_MESSAGE', 'MESSAGES_SET', 'MESSAGES_UPSERT', 'MESSAGES_UPDATE', + 'SEND_MESSAGE', 'CONTACTS_SET', 'CONTACTS_UPSERT', 'CONTACTS_UPDATE', diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 3432f1b55..1686f0311 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', From 57b61070d91c1aaa36fb1b629f2f877f60de4cc2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 15:41:07 -0300 Subject: [PATCH 06/41] feat: Save chatwoot creds --- Docker/.env.example | 5 +- Dockerfile | 3 + src/config/env.config.ts | 6 + src/dev-env.yml | 17 +- src/validate/validate.schema.ts | 13 ++ .../controllers/chatwoot.controller.ts | 48 ++++ .../controllers/instance.controller.ts | 215 ++++++++++++------ src/whatsapp/dto/chatwoot.dto.ts | 7 + src/whatsapp/dto/instance.dto.ts | 3 + src/whatsapp/models/chatwoot.model.ts | 25 ++ src/whatsapp/models/index.ts | 1 + .../repository/chatwoot.repository.ts | 75 ++++++ src/whatsapp/repository/repository.manager.ts | 5 + src/whatsapp/routers/chatwoot.router.ts | 51 +++++ src/whatsapp/routers/index.router.ts | 4 +- src/whatsapp/services/chatwoot.service.ts | 26 +++ src/whatsapp/services/whatsapp.service.ts | 30 +++ src/whatsapp/types/wa.types.ts | 8 + src/whatsapp/whatsapp.module.ts | 13 +- 19 files changed, 481 insertions(+), 74 deletions(-) create mode 100644 src/whatsapp/controllers/chatwoot.controller.ts create mode 100644 src/whatsapp/dto/chatwoot.dto.ts create mode 100644 src/whatsapp/models/chatwoot.model.ts create mode 100644 src/whatsapp/repository/chatwoot.repository.ts create mode 100644 src/whatsapp/routers/chatwoot.router.ts create mode 100644 src/whatsapp/services/chatwoot.service.ts diff --git a/Docker/.env.example b/Docker/.env.example index 18fa32bdc..f3b190918 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -91,4 +91,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/Dockerfile b/Dockerfile index c9ef576da..726064e56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,6 +88,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/src/config/env.config.ts b/src/config/env.config.ts index 6ad188b16..d13eec07f 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -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; @@ -275,6 +278,9 @@ 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 85061e125..f51c88b8b 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -11,7 +11,7 @@ SERVER: CORS: ORIGIN: - - '*' + - "*" # - yourdomain.com METHODS: - POST @@ -63,7 +63,7 @@ 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: @@ -75,8 +75,8 @@ DATABASE: REDIS: ENABLED: false - URI: 'redis://localhost:6379' - PREFIX_KEY: 'evolution' + URI: "redis://localhost:6379" + PREFIX_KEY: "evolution" # Webhook Settings WEBHOOK: @@ -87,7 +87,7 @@ 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 @@ -112,7 +112,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 @@ -136,8 +136,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 9427b1d77..9b75fd083 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -857,3 +857,16 @@ 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' }, + }, + required: ['enabled', 'account_id', 'token', 'url'], + ...isNotEmpty('account_id', 'token', 'url'), +}; diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts new file mode 100644 index 000000000..c48e92852 --- /dev/null +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -0,0 +1,48 @@ +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'; + +const logger = new Logger('ChatwootController'); + +export class ChatwootController { + constructor(private readonly chatwootService: ChatwootService) {} + + 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.enabled) { + logger.verbose('chatwoot disabled'); + data.account_id = ''; + data.token = ''; + data.url = ''; + } + + data.name_inbox = instance.instanceName; + + return this.chatwootService.create(instance, data); + } + + public async findChatwoot(instance: InstanceDto) { + logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); + return this.chatwootService.find(instance); + } +} diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index c2abbffa3..9ffd7e47f 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -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,9 @@ export class InstanceController { events, qrcode, token, + chatwoot_account_id, + chatwoot_token, + chatwoot_url, }: InstanceDto) { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -73,34 +78,70 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); - let getEvents: string[]; - - if (webhook) { - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getEvents: string[]; + + if (webhook) { + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } } + + this.logger.verbose('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, + }; } - this.logger.verbose('instance created'); - this.logger.verbose({ - 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'); + } + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + name_inbox: instance.instanceName, + }); + } catch (error) { + this.logger.log(error); + } return { instance: { @@ -108,8 +149,13 @@ export class InstanceController { status: 'created', }, hash, - webhook, - events: getEvents, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + name_inbox: instance.instanceName, + }, }; } else { this.logger.verbose('server mode'); @@ -141,45 +187,83 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); - let getEvents: string[]; - - if (webhook) { - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); + if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + let getEvents: string[]; + + if (webhook) { + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + + let getQrcode: wa.QrCode; + + if (qrcode) { + this.logger.verbose('creating qrcode'); + await instance.connectToWhatsapp(); + await delay(2000); + getQrcode = instance.qrCode; } + + this.logger.verbose('instance created'); + this.logger.verbose({ + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }); + + return { + instance: { + instanceName: instance.instanceName, + status: 'created', + }, + hash, + webhook, + webhook_by_events, + events: getEvents, + qrcode: getQrcode, + }; } - let getQrcode: wa.QrCode; + if (!chatwoot_account_id) { + throw new BadRequestException('account_id is required'); + } - if (qrcode) { - this.logger.verbose('creating qrcode'); - await instance.connectToWhatsapp(); - await delay(2000); - getQrcode = instance.qrCode; + if (!chatwoot_token) { + throw new BadRequestException('token is required'); } - 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_url) { + throw new BadRequestException('url is required'); + } + + try { + this.chatwootService.create(instance, { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + name_inbox: instance.instanceName, + }); + } catch (error) { + this.logger.log(error); + } return { instance: { @@ -187,10 +271,13 @@ export class InstanceController { status: 'created', }, hash, - webhook, - webhook_by_events, - events: getEvents, - qrcode: getQrcode, + chatwoot: { + enabled: true, + account_id: chatwoot_account_id, + token: chatwoot_token, + url: chatwoot_url, + name_inbox: instance.instanceName, + }, }; } } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts new file mode 100644 index 000000000..a65bbf71f --- /dev/null +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -0,0 +1,7 @@ +export class ChatwootDto { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; +} diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 8a3902e93..479a1daeb 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -5,4 +5,7 @@ export class InstanceDto { events?: string[]; qrcode?: boolean; token?: string; + chatwoot_account_id?: string; + chatwoot_token?: string; + chatwoot_url?: string; } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts new file mode 100644 index 000000000..cb298c917 --- /dev/null +++ b/src/whatsapp/models/chatwoot.model.ts @@ -0,0 +1,25 @@ +import { Schema } from 'mongoose'; +import { dbserver } from '../../db/db.connect'; + +export class ChatwootRaw { + _id?: string; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; +} + +const chatwootSchema = new Schema({ + _id: { type: String, _id: true }, + account_id: { type: String, required: true }, + token: { type: String, required: true }, + url: { type: String, required: true }, + name_inbox: { type: String, 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 11f760d9c..e0b773f00 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 000000000..3d24022ab --- /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 bd41e09ee..9740b436b 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 000000000..e20303047 --- /dev/null +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -0,0 +1,51 @@ +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 { 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); + }); + } + + public readonly router = Router(); +} diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index 80f23c415..c25d320e0 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, @@ -32,6 +33,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 000000000..74ea9eeab --- /dev/null +++ b/src/whatsapp/services/chatwoot.service.ts @@ -0,0 +1,26 @@ +import { InstanceDto } from '../dto/instance.dto'; +import { ChatwootDto } from '../dto/chatwoot.dto'; +import { WAMonitoringService } from './monitor.service'; +import { Logger } from '../../config/logger.config'; + +export class ChatwootService { + constructor(private readonly waMonitor: WAMonitoringService) {} + + private readonly logger = new Logger(ChatwootService.name); + + public create(instance: InstanceDto, data: ChatwootDto) { + this.logger.verbose('create chatwoot: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); + + return { chatwoot: { ...instance, chatwoot: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find chatwoot: ' + instance.instanceName); + return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); + } catch (error) { + return { enabled: null, url: '' }; + } + } +} diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index e2486e690..f7da72ef8 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -114,6 +114,7 @@ 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'; @@ -138,6 +139,7 @@ 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'); private readonly msgRetryCounterCache: CacheStore = new NodeCache(); @@ -268,6 +270,34 @@ export class WAStartupService { return data; } + 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}`); + + 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}`); + return data; + } + public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); const webhookLocal = this.localWebhook.events; diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index 1686f0311..e08aef78c 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -41,6 +41,14 @@ export declare namespace wa { webhook_by_events?: boolean; }; + export type LocalChatwoot = { + enabled?: boolean; + account_id?: string; + token?: string; + url?: string; + name_inbox?: string; + }; + export type StateConnection = { instance?: string; state?: WAConnectionState | 'refused'; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index f05b83237..a03ca18de 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); + 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); From 514fb56209e5aa51f8fc0ed7bf29a5c5612715c6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 16:37:21 -0300 Subject: [PATCH 07/41] feat: chatwoot service and webhook endpoint --- package.json | 1 + src/whatsapp/routers/chatwoot.router.ts | 7 + src/whatsapp/services/chatwoot.service.ts | 335 ++++++++++++++++++++++ 3 files changed, 343 insertions(+) diff --git a/package.json b/package.json index f11b12292..1faf7fb92 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@figuro/chatwoot-sdk": "^1.1.14", "@hapi/boom": "^10.0.1", "@whiskeysockets/baileys": "github:vphelipe/WhiskeySockets-Baileys#master", "axios": "^1.3.5", diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index e20303047..a31c42b1f 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -4,6 +4,7 @@ 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'; @@ -44,6 +45,12 @@ export class ChatwootRouter extends RouterBroker { }); res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('webhook'), ...guards, async (req, res) => { + const { body } = req; + const { instance } = req.query; + + res.status(HttpStatus.OK).json({ message: 'bot' }); }); } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 74ea9eeab..42d17d560 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -2,12 +2,47 @@ import { InstanceDto } from '../dto/instance.dto'; 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, unlinkSync } from 'fs'; +import axios from 'axios'; +import FormData from 'form-data'; export class ChatwootService { constructor(private readonly waMonitor: WAMonitoringService) {} private readonly logger = new Logger(ChatwootService.name); + private provider: any; + + private async getProvider(instance: InstanceDto) { + const provider = await this.waMonitor.waInstances[ + instance.instanceName + ].findChatwoot(); + + return provider; + } + + private async clientCw(instance: InstanceDto) { + const provider = await this.getProvider(instance); + + if (!provider) { + throw new Error('provider not found'); + } + + this.provider = provider; + + const client = new ChatwootClient({ + config: { + basePath: provider.url, + with_credentials: true, + credentials: 'include', + token: provider.token, + }, + }); + + return client; + } + public create(instance: InstanceDto, data: ChatwootDto) { this.logger.verbose('create chatwoot: ' + instance.instanceName); this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); @@ -23,4 +58,304 @@ export class ChatwootService { return { enabled: null, url: '' }; } } + + public async getContact(instance: InstanceDto, id: number) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + if (!id) { + throw new Error('id is required'); + } + + const contact = await client.contact.getContactable({ + accountId: this.provider.accountId, + id, + }); + + return contact; + } + + public async createContact( + instance: InstanceDto, + phoneNumber: string, + inboxId: number, + accountId: number, + name?: string, + ) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const contact = await client.contacts.create({ + accountId: this.provider.accountId, + data: { + inbox_id: inboxId, + name: name || phoneNumber, + phone_number: `+${phoneNumber}`, + }, + }); + + return contact; + } + + public async updateContact(instance: InstanceDto, id: number, data: any) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + if (!id) { + throw new Error('id is required'); + } + + const contact = await client.contacts.update({ + accountId: this.provider.accountId, + id, + data, + }); + + return contact; + } + + public async findContact(instance: InstanceDto, phoneNumber: string) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const contact = await client.contacts.search({ + accountId: this.provider.accountId, + q: `+${phoneNumber}`, + }); + + return contact.payload.find((contact) => contact.phone_number === `+${phoneNumber}`); + } + + public async createConversation(instance: InstanceDto, body: any) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const chatId = body.data.key.remoteJid.split('@')[0]; + const nameContact = !body.data.key.fromMe ? body.data.pushName : chatId; + + const filterInbox = await this.getInbox(instance); + + const contact = + (await this.findContact(instance, chatId)) || + ((await this.createContact(instance, chatId, filterInbox.id, nameContact)) as any); + + const contactId = contact.id || contact.payload.contact.id; + + if (!body.data.key.fromMe && contact.name === chatId && nameContact !== chatId) { + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } + + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.accountId, + id: contactId, + })) as any; + + if (contactConversations) { + const conversation = contactConversations.payload.find( + (conversation) => + conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + if (conversation) { + return conversation.id; + } + } + + const conversation = await client.conversations.create({ + accountId: this.provider.accountId, + data: { + contact_id: `${contactId}`, + inbox_id: `${filterInbox.id}`, + }, + }); + + return conversation.id; + } + + public async getInbox(instance: InstanceDto) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const inbox = (await client.inboxes.list({ + accountId: this.provider.accountId, + })) as any; + const findByName = inbox.payload.find((inbox) => inbox.name === instance); + return findByName; + } + + public async createMessage( + instance: InstanceDto, + conversationId: number, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + const client = await this.clientCw(instance); + + const message = await client.messages.create({ + accountId: this.provider.accountId, + conversationId: conversationId, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + return message; + } + + public async createBotMessage( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + attachments?: { + content: unknown; + encoding: string; + filename: string; + }[], + ) { + const client = await this.clientCw(instance); + + const contact = await this.findContact(instance, '123456'); + + const filterInbox = await this.getInbox(instance); + + const findConversation = await client.conversations.list({ + accountId: this.provider.accountId, + inboxId: filterInbox.id, + }); + const conversation = findConversation.data.payload.find( + (conversation) => + conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + const message = await client.messages.create({ + accountId: this.provider.accountId, + conversationId: conversation.id, + data: { + content: content, + message_type: messageType, + attachments: attachments, + }, + }); + + return message; + } + + private async sendData( + conversationId: number, + file: string, + messageType: 'incoming' | 'outgoing' | undefined, + content?: string, + ) { + const data = new FormData(); + + if (content) { + data.append('content', content); + } + + data.append('message_type', messageType); + + data.append('attachments[]', createReadStream(file)); + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + try { + const { data } = await axios.request(config); + unlinkSync(file); + return data; + } catch (error) { + console.log(error); + } + } + + public async createBotQr( + instance: InstanceDto, + content: string, + messageType: 'incoming' | 'outgoing' | undefined, + file?: string, + ) { + const client = await this.clientCw(instance); + + const contact = await this.findContact(instance, '123456'); + + const filterInbox = await this.getInbox(instance); + + const findConversation = await client.conversations.list({ + accountId: this.provider.accountId, + inboxId: filterInbox.id, + }); + const conversation = findConversation.data.payload.find( + (conversation) => + conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', + ); + + const data = new FormData(); + + if (content) { + data.append('content', content); + } + + data.append('message_type', messageType); + + if (file) { + data.append('attachments[]', createReadStream(file)); + } + + const config = { + method: 'post', + maxBodyLength: Infinity, + url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversation.id}/messages`, + headers: { + api_access_token: this.provider.token, + ...data.getHeaders(), + }, + data: data, + }; + + try { + const { data } = await axios.request(config); + unlinkSync(file); + return data; + } catch (error) { + console.log(error); + } + } + + public async chatwootWebhook(instance: InstanceDto, body: any) { + return true; + } } From 69380146e48cbf0105e0825da70b3880cf2cf742 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 16:39:02 -0300 Subject: [PATCH 08/41] fix: Adjusts in env files, removed save old_messages --- Docker/.env.example | 3 ++- src/dev-env.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Docker/.env.example b/Docker/.env.example index 18fa32bdc..55c4a765b 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -40,7 +40,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 diff --git a/src/dev-env.yml b/src/dev-env.yml index 85061e125..e60e7436e 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -78,7 +78,8 @@ REDIS: 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: From 052303cc9341985c70d403ddb87cd63d51787089 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 20:28:51 -0300 Subject: [PATCH 09/41] feat: chatwoot integration completed --- .../controllers/chatwoot.controller.ts | 7 + src/whatsapp/models/chatwoot.model.ts | 2 + src/whatsapp/routers/chatwoot.router.ts | 18 +- src/whatsapp/services/chatwoot.service.ts | 402 +++++++++++++++++- src/whatsapp/services/whatsapp.service.ts | 103 ++++- 5 files changed, 507 insertions(+), 25 deletions(-) diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index c48e92852..e5887d265 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -45,4 +45,11 @@ export class ChatwootController { logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); return this.chatwootService.find(instance); } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose( + 'requested receiveWebhook from ' + instance.instanceName + ' instance', + ); + return this.chatwootService.receiveWebhook(instance, data); + } } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index cb298c917..1ecdcf821 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -3,6 +3,7 @@ import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { _id?: string; + enabled?: boolean; account_id?: string; token?: string; url?: string; @@ -11,6 +12,7 @@ export class ChatwootRaw { 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 }, diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index a31c42b1f..3d87f1373 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -46,11 +46,21 @@ export class ChatwootRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) - .post(this.routerPath('webhook'), ...guards, async (req, res) => { - const { body } = req; - const { instance } = req.query; + .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({ message: 'bot' }); + res.status(HttpStatus.OK).json(response); }); } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 42d17d560..6cfc3aabe 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,11 +1,16 @@ 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, unlinkSync } from 'fs'; +import { createReadStream, 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'; export class ChatwootService { constructor(private readonly waMonitor: WAMonitoringService) {} @@ -71,7 +76,7 @@ export class ChatwootService { } const contact = await client.contact.getContactable({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id, }); @@ -92,7 +97,7 @@ export class ChatwootService { } const contact = await client.contacts.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, data: { inbox_id: inboxId, name: name || phoneNumber, @@ -115,7 +120,7 @@ export class ChatwootService { } const contact = await client.contacts.update({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id, data, }); @@ -131,7 +136,7 @@ export class ChatwootService { } const contact = await client.contacts.search({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, q: `+${phoneNumber}`, }); @@ -145,8 +150,8 @@ export class ChatwootService { throw new Error('client not found'); } - const chatId = body.data.key.remoteJid.split('@')[0]; - const nameContact = !body.data.key.fromMe ? body.data.pushName : chatId; + const chatId = body.key.remoteJid.split('@')[0]; + const nameContact = !body.key.fromMe ? body.pushName : chatId; const filterInbox = await this.getInbox(instance); @@ -156,14 +161,14 @@ export class ChatwootService { const contactId = contact.id || contact.payload.contact.id; - if (!body.data.key.fromMe && contact.name === chatId && nameContact !== chatId) { + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { await this.updateContact(instance, contactId, { name: nameContact, }); } const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id: contactId, })) as any; @@ -178,7 +183,7 @@ export class ChatwootService { } const conversation = await client.conversations.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, data: { contact_id: `${contactId}`, inbox_id: `${filterInbox.id}`, @@ -196,9 +201,12 @@ export class ChatwootService { } const inbox = (await client.inboxes.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, })) as any; - const findByName = inbox.payload.find((inbox) => inbox.name === instance); + + const findByName = inbox.payload.find( + (inbox) => inbox.name === instance.instanceName, + ); return findByName; } @@ -216,7 +224,7 @@ export class ChatwootService { const client = await this.clientCw(instance); const message = await client.messages.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, conversationId: conversationId, data: { content: content, @@ -245,16 +253,17 @@ export class ChatwootService { const filterInbox = await this.getInbox(instance); const findConversation = await client.conversations.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, inboxId: filterInbox.id, }); + const conversation = findConversation.data.payload.find( (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', ); const message = await client.messages.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, conversationId: conversation.id, data: { content: content, @@ -285,7 +294,7 @@ export class ChatwootService { const config = { method: 'post', maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), @@ -315,7 +324,7 @@ export class ChatwootService { const filterInbox = await this.getInbox(instance); const findConversation = await client.conversations.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, inboxId: filterInbox.id, }); const conversation = findConversation.data.payload.find( @@ -338,7 +347,7 @@ export class ChatwootService { const config = { method: 'post', maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversation.id}/messages`, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), @@ -355,7 +364,360 @@ export class ChatwootService { } } - public async chatwootWebhook(instance: InstanceDto, body: any) { - return true; + public async sendAttachment( + waInstance: any, + number: string, + media: any, + caption?: string, + ) { + try { + const parts = media.split('/'); + const fileName = decodeURIComponent(parts[parts.length - 1]); + + const mimeType = mimeTypes.lookup(fileName).toString(); + + 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; + } + + if (type === 'audio') { + const data: SendAudioDto = { + number: number, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, + }; + + await waInstance?.audioWhatsapp(data); + + return; + } + + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption && type !== 'audio') { + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + return; + } catch (error) { + throw new Error(error); + } + } + + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + if (!body?.conversation || body.private) return { message: 'bot' }; + + const chatId = body.conversation.meta.sender.phone_number.replace('+', ''); + const messageReceived = body.content; + const senderName = body?.sender?.name; + const accountId = body.account.id as number; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + const command = messageReceived.replace('/', ''); + + if (command === 'iniciar') { + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + await waInstance.connectToWhatsapp(); + } else { + await this.createBotMessage( + instance, + `🚨 Instância ${body.inbox.name} já está conectada.`, + 'incoming', + ); + } + } + + if (command === 'status') { + const state = waInstance?.connectionStatus?.state; + + if (!state) { + await this.createBotMessage( + instance, + `⚠️ Instância ${body.inbox.name} não existe.`, + 'incoming', + ); + } + + if (state) { + await this.createBotMessage( + instance, + `⚠️ Status da instância ${body.inbox.name}: *${state}*`, + 'incoming', + ); + } + } + + if (command === 'desconectar') { + const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${body.inbox.name}*: `; + + await this.createBotMessage(instance, msgLogout, 'incoming'); + 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' + ) { + // if (IMPORT_MESSAGES_SENT && messages_sent.includes(body.id)) { + // console.log(`🚨 Não importar mensagens enviadas, ficaria duplicado.`); + + // const indexMessage = messages_sent.indexOf(body.id); + // messages_sent.splice(indexMessage, 1); + + // return { message: 'bot' }; + // } + + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + // formatText = TOSIGN ? `*${senderName}*: ${messageReceived}` : messageReceived; + formatText = `*${senderName}*: ${messageReceived}`; + } + + for (const message of body.conversation.messages) { + if (message.attachments && message.attachments.length > 0) { + for (const attachment of message.attachments) { + console.log(attachment); + if (!messageReceived) { + formatText = null; + } + + await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + ); + } + } else { + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + await waInstance?.textMessage(data); + } + } + } + + return { message: 'bot' }; + } catch (error) { + console.log(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + const media = [ + 'imageMessage', + 'documentMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + return messageKeys.some((key) => media.includes(key)); + } + + private getTypeMessage(msg: any) { + 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, + audioMessage: msg.audioMessage?.caption, + }; + + return types; + } + + private getMessageContent(types: any) { + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + return typeKey ? types[typeKey] : undefined; + } + + private getConversationMessage(msg: any) { + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + try { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (event === 'messages.upsert') { + // if (body.key.fromMe && !IMPORT_MESSAGES_SENT) { + // return; + // } + + if (body.key.remoteJid === 'status@broadcast') { + console.log(`🚨 Ignorando status do whatsapp.`); + return; + } + + const getConversion = await this.createConversation(instance, body); + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + if (!getConversion) { + console.log('🚨 Erro ao criar conversa'); + return; + } + + const isMedia = this.isMediaMessage(body.message); + + const bodyMessage = await this.getConversationMessage(body.message); + + if (isMedia) { + 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}`)}`; + + writeFileSync(fileName, fileData, 'utf8'); + + return await this.sendData(getConversion, fileName, messageType, bodyMessage); + } + + const send = await this.createMessage( + instance, + getConversion, + bodyMessage, + messageType, + ); + + return send; + } + + if (event === 'status.instance') { + const data = body; + const inbox = await this.getInbox(instance); + const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + if (body.state === 'open') { + const msgConnection = `🚀 Conexão realizada com sucesso!`; + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'contacts.update') { + const data = body; + + if (data.length) { + for (const item of data) { + const number = item.id.split('@')[0]; + const photo = item.profilePictureUrl || null; + const find = await this.findContact(instance, number); + + if (find) { + await this.updateContact(instance, find.id, { + avatar_url: photo, + }); + } + } + } + } + + if (event === 'qrcode.updated') { + if (body.statusCode === 500) { + const erroQRcode = `🚨 Limite de geração de QRCode atingido, para gerar um novo QRCode, envie a mensagem /iniciar novamente.`; + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + const fileData = Buffer.from( + body?.qrcode.base64.replace('data:image/png;base64,', ''), + 'base64', + ); + + const fileName = `${path.join( + waInstance?.storePath, + 'temp', + `${`${instance}.png`}`, + )}`; + + writeFileSync(fileName, fileData, 'utf8'); + + 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:`; + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + console.log(error); + } } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f7da72ef8..0f756d181 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -122,6 +122,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( @@ -141,12 +143,14 @@ export class WAStartupService { 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) { @@ -161,6 +165,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() { @@ -270,6 +285,24 @@ 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.logger.verbose('Chatwoot loaded'); + } + public async setChatwoot(data: ChatwootRaw) { this.logger.verbose('Setting chatwoot'); await this.repository.chatwoot.create(data, this.instanceName); @@ -429,6 +462,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, @@ -442,6 +486,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; @@ -472,6 +527,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'); @@ -512,6 +577,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(); @@ -626,6 +702,7 @@ export class WAStartupService { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); + this.loadChatwoot(); this.instance.authState = await this.defineAuthState(); @@ -817,6 +894,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, @@ -940,6 +1025,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], @@ -978,6 +1071,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], From 702dbebfbe016382e59b52d745b46566d0c72752 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 20:58:18 -0300 Subject: [PATCH 10/41] feat: chatwoot integration completed --- src/whatsapp/services/chatwoot.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 6cfc3aabe..0f3c806f5 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -87,7 +87,6 @@ export class ChatwootService { instance: InstanceDto, phoneNumber: string, inboxId: number, - accountId: number, name?: string, ) { const client = await this.clientCw(instance); From 3eda2a1f2aefeebd77240b4d1c4477aec2ec12c6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Wed, 12 Jul 2023 21:01:06 -0300 Subject: [PATCH 11/41] feat: chatwoot integration completed --- CHANGELOG.md | 5 ++++- package.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59e4eb3f..c0d822a6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ -# 1.1.6 (homolog) +# 1.2.0 (homolog) +### Features + +* Native integration with chatwoot ### Fixed * Adjusts in docker-compose files diff --git a/package.json b/package.json index 1faf7fb92..45d727f01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.1.6", + "version": "1.2.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { From 0fc3b47c88da0fc38ecd680d420a4683b5ce399e Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 00:27:18 -0300 Subject: [PATCH 12/41] feat: chatwoot integration completed --- src/validate/validate.schema.ts | 5 +- .../controllers/chatwoot.controller.ts | 28 +++++++- .../controllers/instance.controller.ts | 29 +++++++- src/whatsapp/dto/chatwoot.dto.ts | 1 + src/whatsapp/dto/instance.dto.ts | 1 + src/whatsapp/models/chatwoot.model.ts | 2 + src/whatsapp/services/chatwoot.service.ts | 72 +++++++++++++++---- src/whatsapp/services/whatsapp.service.ts | 9 +++ src/whatsapp/types/wa.types.ts | 1 + 9 files changed, 126 insertions(+), 22 deletions(-) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 9b75fd083..03b01a4aa 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -866,7 +866,8 @@ export const chatwootSchema: JSONSchema7 = { account_id: { type: 'string' }, token: { type: 'string' }, url: { type: 'string' }, + sign_msg: { type: 'boolean', enum: [true, false] }, }, - required: ['enabled', 'account_id', 'token', 'url'], - ...isNotEmpty('account_id', 'token', 'url'), + required: ['enabled', 'account_id', 'token', 'url', 'sign_msg'], + ...isNotEmpty('account_id', 'token', 'url', 'sign_msg'), }; diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index e5887d265..71519c397 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -4,6 +4,7 @@ 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'; const logger = new Logger('ChatwootController'); @@ -27,6 +28,10 @@ export class ChatwootController { if (!data.token) { throw new BadRequestException('token is required'); } + + if (!data.sign_msg) { + throw new BadRequestException('sign_msg is required'); + } } if (!data.enabled) { @@ -34,22 +39,39 @@ export class ChatwootController { data.account_id = ''; data.token = ''; data.url = ''; + data.sign_msg = false; } data.name_inbox = instance.instanceName; - return this.chatwootService.create(instance, data); + const result = this.chatwootService.create(instance, data); + + const response = { + ...result, + webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } public async findChatwoot(instance: InstanceDto) { logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - return this.chatwootService.find(instance); + const result = this.chatwootService.find(instance); + + const response = { + ...result, + webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + }; + + return response; } public async receiveWebhook(instance: InstanceDto, data: any) { logger.verbose( 'requested receiveWebhook from ' + instance.instanceName + ' instance', ); - return this.chatwootService.receiveWebhook(instance, data); + const chatwootService = new ChatwootService(waMonitor); + + return chatwootService.receiveWebhook(instance, data); } } diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 9ffd7e47f..51ad9b375 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -37,6 +37,7 @@ export class InstanceController { chatwoot_account_id, chatwoot_token, chatwoot_url, + chatwoot_sign_msg, }: InstanceDto) { this.logger.verbose('requested createInstance from ' + instanceName + ' instance'); @@ -78,7 +79,12 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + if ( + !chatwoot_account_id || + !chatwoot_token || + !chatwoot_url || + !chatwoot_sign_msg + ) { let getEvents: string[]; if (webhook) { @@ -131,12 +137,17 @@ export class InstanceController { throw new BadRequestException('url is required'); } + if (!chatwoot_sign_msg) { + throw new BadRequestException('sign_msg is required'); + } + 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, }); } catch (error) { @@ -154,7 +165,9 @@ export class InstanceController { account_id: chatwoot_account_id, token: chatwoot_token, url: chatwoot_url, + sign_msg: chatwoot_sign_msg, name_inbox: instance.instanceName, + webhook_url: `/chatwoot/webhook/${instance.instanceName}`, }, }; } else { @@ -187,7 +200,12 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); - if (!chatwoot_account_id || !chatwoot_token || !chatwoot_url) { + if ( + !chatwoot_account_id || + !chatwoot_token || + !chatwoot_url || + !chatwoot_sign_msg + ) { let getEvents: string[]; if (webhook) { @@ -253,12 +271,17 @@ export class InstanceController { throw new BadRequestException('url is required'); } + if (!chatwoot_sign_msg) { + throw new BadRequestException('sign_msg is required'); + } + 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, }); } catch (error) { @@ -276,7 +299,9 @@ export class InstanceController { account_id: chatwoot_account_id, token: chatwoot_token, url: chatwoot_url, + sign_msg: chatwoot_sign_msg, name_inbox: instance.instanceName, + webhook_url: `/chatwoot/webhook/${instance.instanceName}`, }, }; } diff --git a/src/whatsapp/dto/chatwoot.dto.ts b/src/whatsapp/dto/chatwoot.dto.ts index a65bbf71f..e78b0676e 100644 --- a/src/whatsapp/dto/chatwoot.dto.ts +++ b/src/whatsapp/dto/chatwoot.dto.ts @@ -4,4 +4,5 @@ export class ChatwootDto { token?: string; url?: string; name_inbox?: string; + sign_msg?: boolean; } diff --git a/src/whatsapp/dto/instance.dto.ts b/src/whatsapp/dto/instance.dto.ts index 479a1daeb..ce282e03f 100644 --- a/src/whatsapp/dto/instance.dto.ts +++ b/src/whatsapp/dto/instance.dto.ts @@ -8,4 +8,5 @@ export class InstanceDto { 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 index 1ecdcf821..ca0823097 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -8,6 +8,7 @@ export class ChatwootRaw { token?: string; url?: string; name_inbox?: string; + sign_msg?: boolean; } const chatwootSchema = new Schema({ @@ -17,6 +18,7 @@ const chatwootSchema = new Schema({ 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( diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0f3c806f5..c31a745a6 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -4,21 +4,43 @@ 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, unlinkSync, writeFileSync } from 'fs'; +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 NodeCache from 'node-cache'; +import { ROOT_DIR } from '../../config/path.config'; export class ChatwootService { - constructor(private readonly waMonitor: WAMonitoringService) {} + 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 { + try { + const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); + const cacheArray = cacheData.split('\n'); + return new Set(cacheArray); + } catch (error) { + return new Set(); + } + } + + private saveMessageCache() { + const cacheData = Array.from(this.messageCache).join('\n'); + writeFileSync(this.messageCacheFile, cacheData, 'utf-8'); + } + private async getProvider(instance: InstanceDto) { const provider = await this.waMonitor.waInstances[ instance.instanceName @@ -436,12 +458,13 @@ export class ChatwootService { public async receiveWebhook(instance: InstanceDto, body: any) { try { + const client = await this.clientCw(instance); + if (!body?.conversation || body.private) return { message: 'bot' }; const chatId = body.conversation.meta.sender.phone_number.replace('+', ''); const messageReceived = body.content; const senderName = body?.sender?.name; - const accountId = body.account.id as number; const waInstance = this.waMonitor.waInstances[instance.instanceName]; if (chatId === '123456' && body.message_type === 'outgoing') { @@ -495,27 +518,31 @@ export class ChatwootService { body?.conversation?.messages?.length && chatId !== '123456' ) { - // if (IMPORT_MESSAGES_SENT && messages_sent.includes(body.id)) { - // console.log(`🚨 Não importar mensagens enviadas, ficaria duplicado.`); + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); - // const indexMessage = messages_sent.indexOf(body.id); - // messages_sent.splice(indexMessage, 1); + this.messageCache = this.loadMessageCache(); - // return { message: 'bot' }; - // } + if (this.messageCache.has(body.id.toString())) { + return { message: 'bot' }; + } let formatText: string; if (senderName === null || senderName === undefined) { formatText = messageReceived; } else { - // formatText = TOSIGN ? `*${senderName}*: ${messageReceived}` : messageReceived; - formatText = `*${senderName}*: ${messageReceived}`; + formatText = this.provider.sign_msg + ? `*${senderName}*: ${messageReceived}` + : messageReceived; } for (const message of body.conversation.messages) { if (message.attachments && message.attachments.length > 0) { for (const attachment of message.attachments) { - console.log(attachment); if (!messageReceived) { formatText = null; } @@ -609,7 +636,6 @@ export class ChatwootService { // } if (body.key.remoteJid === 'status@broadcast') { - console.log(`🚨 Ignorando status do whatsapp.`); return; } @@ -617,7 +643,6 @@ export class ChatwootService { const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; if (!getConversion) { - console.log('🚨 Erro ao criar conversa'); return; } @@ -637,7 +662,11 @@ export class ChatwootService { const fileData = Buffer.from(downloadBase64.base64, 'base64'); - const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + const fileName = `${path.join( + waInstance?.storePath, + 'chatwoot', + `${nameFile}`, + )}`; writeFileSync(fileName, fileData, 'utf8'); @@ -651,6 +680,19 @@ export class ChatwootService { messageType, ); + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.saveMessageCache(); + return send; } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 0f756d181..e21e57c2d 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -300,6 +300,12 @@ export class WAStartupService { 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'); } @@ -310,6 +316,7 @@ export class WAStartupService { 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'); @@ -328,6 +335,8 @@ export class WAStartupService { 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; } diff --git a/src/whatsapp/types/wa.types.ts b/src/whatsapp/types/wa.types.ts index e08aef78c..1ebe3b408 100644 --- a/src/whatsapp/types/wa.types.ts +++ b/src/whatsapp/types/wa.types.ts @@ -47,6 +47,7 @@ export declare namespace wa { token?: string; url?: string; name_inbox?: string; + sign_msg?: boolean; }; export type StateConnection = { From db54f247a271bafaa39c9355be5334822c105a4a Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 00:34:34 -0300 Subject: [PATCH 13/41] feat: chatwoot integration completed --- src/whatsapp/services/monitor.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index fc2618663..0f4efcac5 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -232,6 +232,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 + '*')}`); } } From be782ba512860f9140a4f609b7e3894fa05eac09 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 07:19:32 -0300 Subject: [PATCH 14/41] feat: Added returning or non-returning participants option in fetchAllGroups --- CHANGELOG.md | 3 ++ src/validate/validate.schema.ts | 10 +++++ src/whatsapp/abstract/abstract.router.ts | 45 +++++++++++++++++++- src/whatsapp/controllers/group.controller.ts | 7 ++- src/whatsapp/dto/group.dto.ts | 4 ++ src/whatsapp/routers/group.router.ts | 10 +++-- src/whatsapp/services/whatsapp.service.ts | 29 ++++++++++++- 7 files changed, 99 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d822a6f..bb3347350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,15 @@ ### Features * Native integration with chatwoot +* Added returning or non-returning participants option in fetchAllGroups + ### 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 # 1.1.5 (2023-07-12 07:17) diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 03b01a4aa..1fbb331e4 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -700,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', diff --git a/src/whatsapp/abstract/abstract.router.ts b/src/whatsapp/abstract/abstract.router.ts index e0ed588c6..e657c2c09 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/group.controller.ts b/src/whatsapp/controllers/group.controller.ts index e02547966..f4d381cea 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/dto/group.dto.ts b/src/whatsapp/dto/group.dto.ts index fba86ae2c..bc36e27f2 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/routers/group.router.ts b/src/whatsapp/routers/group.router.ts index 8cb3032ea..4c1b30232 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/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index e21e57c2d..9d56730cb 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -109,6 +109,7 @@ 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'; @@ -2567,10 +2568,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()); } From b4a9941452ce5c8541936810f56e8cabab4a351c Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 08:29:08 -0300 Subject: [PATCH 15/41] feat: Native integration with chatwoot --- src/whatsapp/services/chatwoot.service.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index c31a745a6..24fbcb011 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -46,6 +46,10 @@ export class ChatwootService { instance.instanceName ].findChatwoot(); + if (!provider) { + return null; + } + return provider; } @@ -102,6 +106,10 @@ export class ChatwootService { id, }); + if (!contact) { + return null; + } + return contact; } @@ -126,6 +134,10 @@ export class ChatwootService { }, }); + if (!contact) { + return null; + } + return contact; } @@ -536,7 +548,7 @@ export class ChatwootService { formatText = messageReceived; } else { formatText = this.provider.sign_msg - ? `*${senderName}*: ${messageReceived}` + ? `*${senderName}:*\n\n${messageReceived}` : messageReceived; } @@ -631,10 +643,6 @@ export class ChatwootService { const waInstance = this.waMonitor.waInstances[instance.instanceName]; if (event === 'messages.upsert') { - // if (body.key.fromMe && !IMPORT_MESSAGES_SENT) { - // return; - // } - if (body.key.remoteJid === 'status@broadcast') { return; } @@ -699,6 +707,11 @@ export class ChatwootService { if (event === 'status.instance') { const data = body; const inbox = await this.getInbox(instance); + + if (!inbox) { + return; + } + const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; await this.createBotMessage(instance, msgStatus, 'incoming'); } From eb83d8930745dca0a91084f741a5b52031d17a11 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 11:19:48 -0300 Subject: [PATCH 16/41] feat: Show webhook_url for chatwoot --- src/config/env.config.ts | 17 +++++---- src/dev-env.yml | 1 + .../controllers/chatwoot.controller.ts | 16 ++++++--- .../controllers/instance.controller.ts | 10 ++++-- src/whatsapp/services/chatwoot.service.ts | 35 ++++++++++++++----- src/whatsapp/services/monitor.service.ts | 18 ++++++++++ src/whatsapp/services/whatsapp.service.ts | 10 ++++-- src/whatsapp/whatsapp.module.ts | 2 +- 8 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index d13eec07f..1849ceefc 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 = { @@ -98,9 +98,9 @@ export type Instance = { NAME: string; WEBHOOK_URL: string; MODE: string; - CHATWOOT_ACCOUNT_ID: string; - CHATWOOT_TOKEN: string; - CHATWOOT_URL: string; + CHATWOOT_ACCOUNT_ID?: string; + CHATWOOT_TOKEN?: string; + CHATWOOT_URL?: string; }; export type Auth = { API_KEY: ApiKey; @@ -159,6 +159,7 @@ export class ConfigService { if (process.env?.DOCKER_ENV === 'true') { this.env.SERVER.TYPE = 'http'; this.env.SERVER.PORT = 8080; + this.env.SERVER.URL = `http://localhost:${this.env.SERVER.PORT}`; } } @@ -173,6 +174,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(','), @@ -278,9 +280,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, + 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 552739712..244ff73db 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -8,6 +8,7 @@ SERVER: TYPE: http # https PORT: 8080 # 443 + URL: localhost CORS: ORIGIN: diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index 71519c397..a4367833f 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -5,11 +5,15 @@ 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) {} + constructor( + private readonly chatwootService: ChatwootService, + private readonly configService: ConfigService, + ) {} public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { logger.verbose( @@ -46,9 +50,11 @@ export class ChatwootController { const result = this.chatwootService.create(instance, data); + const urlServer = this.configService.get('SERVER').URL; + const response = { ...result, - webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, }; return response; @@ -56,11 +62,13 @@ export class ChatwootController { public async findChatwoot(instance: InstanceDto) { logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); - const result = this.chatwootService.find(instance); + const result = await this.chatwootService.find(instance); + + const urlServer = this.configService.get('SERVER').URL; const response = { ...result, - webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, }; return response; diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index 51ad9b375..b04933dbe 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'; @@ -154,6 +154,8 @@ export class InstanceController { this.logger.log(error); } + const urlServer = this.configService.get('SERVER').URL; + return { instance: { instanceName: instance.instanceName, @@ -167,7 +169,7 @@ export class InstanceController { url: chatwoot_url, sign_msg: chatwoot_sign_msg, name_inbox: instance.instanceName, - webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, }, }; } else { @@ -288,6 +290,8 @@ export class InstanceController { this.logger.log(error); } + const urlServer = this.configService.get('SERVER').URL; + return { instance: { instanceName: instance.instanceName, @@ -301,7 +305,7 @@ export class InstanceController { url: chatwoot_url, sign_msg: chatwoot_sign_msg, name_inbox: instance.instanceName, - webhook_url: `/chatwoot/webhook/${instance.instanceName}`, + webhook_url: `${urlServer}/chatwoot/webhook/${instance.instanceName}`, }, }; } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 24fbcb011..802ea353e 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -42,22 +42,26 @@ export class ChatwootService { } private async getProvider(instance: InstanceDto) { - const provider = await this.waMonitor.waInstances[ - instance.instanceName - ].findChatwoot(); + try { + const provider = await this.waMonitor.waInstances[ + instance.instanceName + ].findChatwoot(); - if (!provider) { + if (!provider) { + return null; + } + + return provider; + } catch (error) { return null; } - - return provider; } private async clientCw(instance: InstanceDto) { const provider = await this.getProvider(instance); if (!provider) { - throw new Error('provider not found'); + this.logger.error('provider not found'); } this.provider = provider; @@ -78,7 +82,7 @@ export class ChatwootService { this.logger.verbose('create chatwoot: ' + instance.instanceName); this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); - return { chatwoot: { ...instance, chatwoot: data } }; + return data; } public async find(instance: InstanceDto): Promise { @@ -583,6 +587,21 @@ export class ChatwootService { } } + if (body.message_type === 'template' && body.content_type === 'input_csat') { + const data: SendTextDto = { + number: chatId, + textMessage: { + text: body.content, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + await waInstance?.textMessage(data); + } + return { message: 'bot' }; } catch (error) { console.log(error); diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 0f4efcac5..60058254f 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; @@ -101,6 +115,7 @@ export class WAMonitoringService { profilePictureUrl: value.profilePictureUrl, status: (await value.getProfileStatus()) || '', apikey, + chatwoot, }, }); } else { @@ -114,6 +129,7 @@ export class WAMonitoringService { profileName: (await value.getProfileName()) || 'not loaded', profilePictureUrl: value.profilePictureUrl, status: (await value.getProfileStatus()) || '', + chatwoot, }, }); } @@ -134,6 +150,7 @@ export class WAMonitoringService { instanceName: key, status: value.connectionStatus.state, apikey, + chatwoot, }, }); } else { @@ -144,6 +161,7 @@ export class WAMonitoringService { instance: { instanceName: key, status: value.connectionStatus.state, + chatwoot, }, }); } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 9d56730cb..068a61be5 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, @@ -343,6 +343,7 @@ export class WAStartupService { public async sendDataWebhook(event: Events, data: T, local = true) { const webhookGlobal = this.configService.get('WEBHOOK'); + const urlServer = this.configService.get('SERVER').URL; const webhookLocal = this.localWebhook.events; const we = event.replace(/[\.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); @@ -367,6 +368,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + urlServer, }); } @@ -378,6 +380,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: this.localWebhook.url, + urlServer, }); } } catch (error) { @@ -425,6 +428,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + urlServer, }); } @@ -436,6 +440,7 @@ export class WAStartupService { instance: this.instance.name, data, destination: localUrl, + urlServer, }); } } catch (error) { @@ -719,8 +724,7 @@ export class WAStartupService { 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 = { diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index a03ca18de..64d463d20 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -74,7 +74,7 @@ export const webhookController = new WebhookController(webhookService); const chatwootService = new ChatwootService(waMonitor); -export const chatwootController = new ChatwootController(chatwootService); +export const chatwootController = new ChatwootController(chatwootService, configService); export const instanceController = new InstanceController( waMonitor, From 6232190cfe555e2e76b8bd4a754368255469df82 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 11:35:30 -0300 Subject: [PATCH 17/41] fix: Adjust in send document with caption from chatwoot --- src/whatsapp/services/chatwoot.service.ts | 4 +++- src/whatsapp/whatsapp.module.ts | 27 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 802ea353e..900f95b9c 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -11,7 +11,6 @@ import { SendTextDto } from '../dto/sendMessage.dto'; import mimeTypes from 'mime-types'; import { SendAudioDto } from '../dto/sendMessage.dto'; import { SendMediaDto } from '../dto/sendMessage.dto'; -import NodeCache from 'node-cache'; import { ROOT_DIR } from '../../config/path.config'; export class ChatwootService { @@ -614,6 +613,7 @@ export class ChatwootService { const media = [ 'imageMessage', 'documentMessage', + 'documentWithCaptionMessage', 'audioMessage', 'videoMessage', 'stickerMessage', @@ -632,6 +632,8 @@ export class ChatwootService { messageContextInfo: msg.messageContextInfo?.stanzaId, stickerMessage: msg.stickerMessage?.fileSha256.toString('base64'), documentMessage: msg.documentMessage?.caption, + documentWithCaptionMessage: + msg.documentWithCaptionMessage?.message?.documentMessage?.caption, audioMessage: msg.audioMessage?.caption, }; diff --git a/src/whatsapp/whatsapp.module.ts b/src/whatsapp/whatsapp.module.ts index 64d463d20..c57b9bf81 100644 --- a/src/whatsapp/whatsapp.module.ts +++ b/src/whatsapp/whatsapp.module.ts @@ -116,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; @@ -137,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; From e64926a429b2b14f2467f512aa40f3d35c84daba Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 11:35:42 -0300 Subject: [PATCH 18/41] fix: Adjust in send document with caption from chatwoot --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb3347350..c527c5ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * 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 # 1.1.5 (2023-07-12 07:17) From 86985231ff64177faf0f86506cf79ec654a5fe99 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 12:32:10 -0300 Subject: [PATCH 19/41] feat: added group integration to chatwoot --- src/whatsapp/services/chatwoot.service.ts | 217 ++++++++++++++++------ 1 file changed, 156 insertions(+), 61 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 900f95b9c..48adfed42 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -120,6 +120,7 @@ export class ChatwootService { instance: InstanceDto, phoneNumber: string, inboxId: number, + isGroup: boolean, name?: string, ) { const client = await this.clientCw(instance); @@ -128,13 +129,23 @@ export class ChatwootService { throw new Error('client not found'); } - const contact = await client.contacts.create({ - accountId: this.provider.account_id, - data: { + let data: any = {}; + if (!isGroup) { + data = { inbox_id: inboxId, name: name || phoneNumber, phone_number: `+${phoneNumber}`, - }, + }; + } else { + data = { + inbox_id: inboxId, + name: name || phoneNumber, + identifier: phoneNumber, + }; + } + const contact = await client.contacts.create({ + accountId: this.provider.account_id, + data, }); if (!contact) { @@ -171,62 +182,109 @@ export class ChatwootService { throw new Error('client not found'); } - const contact = await client.contacts.search({ + let query: any; + + if (!phoneNumber.includes('@g.us')) { + query = `+${phoneNumber}`; + } else { + query = phoneNumber; + } + + const contact: any = await client.contacts.search({ accountId: this.provider.account_id, - q: `+${phoneNumber}`, + q: query, }); - return contact.payload.find((contact) => contact.phone_number === `+${phoneNumber}`); + if (!phoneNumber.includes('@g.us')) { + return contact.payload.find((contact) => contact.phone_number === query); + } else { + return contact.payload.find((contact) => contact.identifier === query); + } } public async createConversation(instance: InstanceDto, body: any) { - const client = await this.clientCw(instance); + try { + const client = await this.clientCw(instance); - if (!client) { - throw new Error('client not found'); - } + if (!client) { + throw new Error('client not found'); + } - const chatId = body.key.remoteJid.split('@')[0]; - const nameContact = !body.key.fromMe ? body.pushName : chatId; + const isGroup = body.key.remoteJid.includes('@g.us'); - const filterInbox = await this.getInbox(instance); + const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; - const contact = - (await this.findContact(instance, chatId)) || - ((await this.createContact(instance, chatId, filterInbox.id, nameContact)) as any); + let nameContact: string; - const contactId = contact.id || contact.payload.contact.id; + nameContact = !body.key.fromMe ? body.pushName : chatId; - if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { - await this.updateContact(instance, contactId, { - name: nameContact, - }); - } + const filterInbox = await this.getInbox(instance); - const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.account_id, - id: contactId, - })) as any; + if (isGroup) { + const group = await this.waMonitor.waInstances[ + instance.instanceName + ].client.groupMetadata(chatId); - if (contactConversations) { - const conversation = contactConversations.payload.find( - (conversation) => - conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, - ); - if (conversation) { - return conversation.id; + nameContact = `${group.subject} (GROUP)`; + + 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); + + console.log('participant', participant); } - } - const conversation = await client.conversations.create({ - accountId: this.provider.account_id, - data: { - contact_id: `${contactId}`, - inbox_id: `${filterInbox.id}`, - }, - }); + const contact = + (await this.findContact(instance, chatId)) || + ((await this.createContact( + instance, + chatId, + filterInbox.id, + isGroup, + nameContact, + )) as any); + + const contactId = contact.id || contact.payload.contact.id; + + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { + await this.updateContact(instance, contactId, { + name: nameContact, + }); + } - return conversation.id; + const contactConversations = (await client.contacts.listConversations({ + accountId: this.provider.account_id, + id: contactId, + })) as any; + + if (contactConversations) { + const conversation = contactConversations.payload.find( + (conversation) => + conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, + ); + if (conversation) { + return conversation.id; + } + } + + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: `${contactId}`, + inbox_id: `${filterInbox.id}`, + }, + }); + + return conversation.id; + } catch (error) { + console.log(error); + } } public async getInbox(instance: InstanceDto) { @@ -477,7 +535,9 @@ export class ChatwootService { if (!body?.conversation || body.private) return { message: 'bot' }; - const chatId = body.conversation.meta.sender.phone_number.replace('+', ''); + 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]; @@ -699,30 +759,65 @@ export class ChatwootService { writeFileSync(fileName, fileData, 'utf8'); - return await this.sendData(getConversion, fileName, messageType, bodyMessage); + if (body.key.remoteJid.includes('@g.us')) { + const participantName = body.pushName; + + const content = `**${participantName}**\n\n${bodyMessage}`; + return await this.sendData(getConversion, fileName, messageType, content); + } else { + return await this.sendData(getConversion, fileName, messageType, bodyMessage); + } } - const send = await this.createMessage( - instance, - getConversion, - bodyMessage, - messageType, - ); + if (body.key.remoteJid.includes('@g.us')) { + const participantName = body.pushName; - this.messageCacheFile = path.join( - ROOT_DIR, - 'store', - 'chatwoot', - `${instance.instanceName}_cache.txt`, - ); + const content = `**${participantName}**\n\n${bodyMessage}`; - this.messageCache = this.loadMessageCache(); + const send = await this.createMessage( + instance, + getConversion, + content, + messageType, + ); + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); - this.messageCache.add(send.id.toString()); + this.saveMessageCache(); - this.saveMessageCache(); + return send; + } else { + const send = await this.createMessage( + instance, + getConversion, + bodyMessage, + messageType, + ); + + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); - return send; + this.saveMessageCache(); + + return send; + } } if (event === 'status.instance') { From e5cd577fbf0ab6590beee652ee3182e25592c1d7 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 12:32:24 -0300 Subject: [PATCH 20/41] feat: added group integration to chatwoot --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c527c5ffe..44a49cfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Native integration with chatwoot * Added returning or non-returning participants option in fetchAllGroups +* Added group integration to chatwoot ### Fixed From 3e13ae97400ab377b900ac8c754a68c2504be3b2 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 12:41:29 -0300 Subject: [PATCH 21/41] feat: added group integration to chatwoot --- src/whatsapp/services/chatwoot.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 48adfed42..24d078a29 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -61,6 +61,7 @@ export class ChatwootService { if (!provider) { this.logger.error('provider not found'); + return null; } this.provider = provider; From 8622e1e4ff2d34a8e8285046e94dfdf448bd0ab6 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 15:39:40 -0300 Subject: [PATCH 22/41] feat: automation chatwoot --- Docker/.env.example | 2 + package.json | 2 +- .../controllers/instance.controller.ts | 97 +++++++++++-------- src/whatsapp/services/chatwoot.service.ts | 81 ++++++++++++++++ src/whatsapp/services/monitor.service.ts | 2 - 5 files changed, 141 insertions(+), 43 deletions(-) diff --git a/Docker/.env.example b/Docker/.env.example index e2b99a1e2..f4e665aa6 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 diff --git a/package.json b/package.json index 45d727f01..c0e8c825a 100644 --- a/package.json +++ b/package.json @@ -42,9 +42,9 @@ "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", "@ffmpeg-installer/ffmpeg": "^1.1.0", - "@figuro/chatwoot-sdk": "^1.1.14", "@hapi/boom": "^10.0.1", "@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/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index b04933dbe..b08abc78b 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -79,30 +79,30 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); + let getEvents: string[]; + + if (webhook) { + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + if ( !chatwoot_account_id || !chatwoot_token || !chatwoot_url || !chatwoot_sign_msg ) { - let getEvents: string[]; - - if (webhook) { - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - this.logger.verbose('instance created'); this.logger.verbose({ instance: { @@ -141,6 +141,8 @@ export class InstanceController { throw new BadRequestException('sign_msg is required'); } + const urlServer = this.configService.get('SERVER').URL; + try { this.chatwootService.create(instance, { enabled: true, @@ -150,12 +152,17 @@ export class InstanceController { 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); } - const urlServer = this.configService.get('SERVER').URL; - return { instance: { instanceName: instance.instanceName, @@ -202,30 +209,30 @@ export class InstanceController { this.logger.verbose('hash: ' + hash + ' generated'); + let getEvents: string[]; + + if (webhook) { + this.logger.verbose('creating webhook'); + try { + this.webhookService.create(instance, { + enabled: true, + url: webhook, + events, + webhook_by_events, + }); + + getEvents = (await this.webhookService.find(instance)).events; + } catch (error) { + this.logger.log(error); + } + } + if ( !chatwoot_account_id || !chatwoot_token || !chatwoot_url || !chatwoot_sign_msg ) { - let getEvents: string[]; - - if (webhook) { - this.logger.verbose('creating webhook'); - try { - this.webhookService.create(instance, { - enabled: true, - url: webhook, - events, - webhook_by_events, - }); - - getEvents = (await this.webhookService.find(instance)).events; - } catch (error) { - this.logger.log(error); - } - } - let getQrcode: wa.QrCode; if (qrcode) { @@ -277,6 +284,8 @@ export class InstanceController { throw new BadRequestException('sign_msg is required'); } + const urlServer = this.configService.get('SERVER').URL; + try { this.chatwootService.create(instance, { enabled: true, @@ -286,18 +295,26 @@ export class InstanceController { 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); } - const urlServer = this.configService.get('SERVER').URL; - return { instance: { instanceName: instance.instanceName, status: 'created', }, hash, + webhook, + webhook_by_events, + events: getEvents, chatwoot: { enabled: true, account_id: chatwoot_account_id, diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 24d078a29..0f76e5065 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -117,6 +117,87 @@ export class ChatwootService { return contact; } + public async initInstanceChatwoot( + instance: InstanceDto, + inboxName: string, + webhookUrl: string, + qrcode: boolean, + ) { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const findInbox: any = await client.inboxes.list({ + accountId: this.provider.account_id, + }); + + const checkDuplicate = findInbox.payload + .map((inbox) => inbox.name) + .includes(inboxName); + + let inboxId: number; + + if (!checkDuplicate) { + 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) { + return null; + } + + inboxId = inbox.id; + } else { + const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); + + inboxId = inbox.id; + } + + const contact = + (await this.findContact(instance, '123456')) || + ((await this.createContact( + instance, + '123456', + inboxId, + false, + 'EvolutionAPI', + )) as any); + + const contactId = contact.id || contact.payload.contact.id; + + if (qrcode) { + const conversation = await client.conversations.create({ + accountId: this.provider.account_id, + data: { + contact_id: contactId.toString(), + inbox_id: inboxId.toString(), + }, + }); + + await client.messages.create({ + accountId: this.provider.account_id, + conversationId: conversation.id, + data: { + content: '/iniciar', + message_type: 'outgoing', + }, + }); + } + + return true; + } + public async createContact( instance: InstanceDto, phoneNumber: string, diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index 60058254f..c07e9f32d 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -129,7 +129,6 @@ export class WAMonitoringService { profileName: (await value.getProfileName()) || 'not loaded', profilePictureUrl: value.profilePictureUrl, status: (await value.getProfileStatus()) || '', - chatwoot, }, }); } @@ -161,7 +160,6 @@ export class WAMonitoringService { instance: { instanceName: key, status: value.connectionStatus.state, - chatwoot, }, }); } From fa60b684257e7d9bbf212e8038157fd7b63befbd Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 15:45:14 -0300 Subject: [PATCH 23/41] feat: automation chatwoot --- src/whatsapp/services/whatsapp.service.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 068a61be5..2794761ac 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1544,6 +1544,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], From b7bfe995200a14946b0bcf6ae822d6774b7a5b8f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 15:53:40 -0300 Subject: [PATCH 24/41] feat: automation chatwoot --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44a49cfd4..3397574fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * 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 ### Fixed From 926f58f336da62fc49c573c1071c83f30b441991 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 16:19:31 -0300 Subject: [PATCH 25/41] feat: automation chatwoot --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 726064e56..a7e558f35 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" From 99b0288d1b3a51a379bdd6c0d4a3e2f09d2aafbd Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 16:26:01 -0300 Subject: [PATCH 26/41] feat: automation chatwoot --- src/config/env.config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 1849ceefc..76bb55eac 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -159,7 +159,6 @@ export class ConfigService { if (process.env?.DOCKER_ENV === 'true') { this.env.SERVER.TYPE = 'http'; this.env.SERVER.PORT = 8080; - this.env.SERVER.URL = `http://localhost:${this.env.SERVER.PORT}`; } } From 834a80c35a2052b1dd71d6facabc67e1c1d54caa Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 16:34:16 -0300 Subject: [PATCH 27/41] feat: automation chatwoot --- src/whatsapp/services/chatwoot.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 0f76e5065..82cb8b538 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -318,8 +318,6 @@ export class ChatwootService { false, body.pushName || body.key.participant.split('@')[0], )) as any); - - console.log('participant', participant); } const contact = From d309ede623b3896363f0b76324e48720b62c6f40 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 17:31:15 -0300 Subject: [PATCH 28/41] fix: fixed message with undefind in chatwoot --- src/whatsapp/services/chatwoot.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 82cb8b538..8418c198b 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -819,6 +819,10 @@ export class ChatwootService { const bodyMessage = await this.getConversationMessage(body.message); + if (!bodyMessage) { + return; + } + if (isMedia) { const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ message: { @@ -839,7 +843,7 @@ export class ChatwootService { writeFileSync(fileName, fileData, 'utf8'); - if (body.key.remoteJid.includes('@g.us')) { + if (body.key.remoteJid.includes('@g.us') && !body.key.fromMe) { const participantName = body.pushName; const content = `**${participantName}**\n\n${bodyMessage}`; From ef03292e35868dcd30b94d4626a0ff4e8336ebc5 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 17:31:32 -0300 Subject: [PATCH 29/41] fix: fixed message with undefind in chatwoot --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3397574fd..f77faee2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * 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 # 1.1.5 (2023-07-12 07:17) From 7e88a9084d1ae34a0bce3d76c875dfaf2ca927fc Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 19:23:59 -0300 Subject: [PATCH 30/41] fix: test duplicate message media in groups chatwoot --- CHANGELOG.md | 1 + src/whatsapp/routers/index.router.ts | 6 ++ src/whatsapp/services/chatwoot.service.ts | 79 ++++++++++++++++++----- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f77faee2a..ef15f8bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * 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 / # 1.1.5 (2023-07-12 07:17) diff --git a/src/whatsapp/routers/index.router.ts b/src/whatsapp/routers/index.router.ts index c25d320e0..082ffe699 100644 --- a/src/whatsapp/routers/index.router.ts +++ b/src/whatsapp/routers/index.router.ts @@ -25,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, diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 8418c198b..03407a340 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -389,6 +389,7 @@ export class ChatwootService { conversationId: number, content: string, messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, attachments?: { content: unknown; encoding: string; @@ -404,6 +405,7 @@ export class ChatwootService { content: content, message_type: messageType, attachments: attachments, + private: privateMessage, }, }); @@ -414,6 +416,7 @@ export class ChatwootService { instance: InstanceDto, content: string, messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, attachments?: { content: unknown; encoding: string; @@ -436,6 +439,10 @@ export class ChatwootService { conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', ); + if (!conversation) { + return; + } + const message = await client.messages.create({ accountId: this.provider.account_id, conversationId: conversation.id, @@ -443,6 +450,7 @@ export class ChatwootService { content: content, message_type: messageType, attachments: attachments, + private: privateMessage, }, }); @@ -453,6 +461,7 @@ export class ChatwootService { conversationId: number, file: string, messageType: 'incoming' | 'outgoing' | undefined, + privateMessage: boolean, content?: string, ) { const data = new FormData(); @@ -463,6 +472,8 @@ export class ChatwootService { data.append('message_type', messageType); + data.append('private', privateMessage); + data.append('attachments[]', createReadStream(file)); const config = { @@ -478,10 +489,12 @@ export class ChatwootService { try { const { data } = await axios.request(config); + unlinkSync(file); return data; } catch (error) { console.log(error); + unlinkSync(file); } } @@ -635,6 +648,7 @@ export class ChatwootService { instance, `🚨 Instância ${body.inbox.name} já está conectada.`, 'incoming', + false, ); } } @@ -647,6 +661,7 @@ export class ChatwootService { instance, `⚠️ Instância ${body.inbox.name} não existe.`, 'incoming', + false, ); } @@ -655,6 +670,7 @@ export class ChatwootService { instance, `⚠️ Status da instância ${body.inbox.name}: *${state}*`, 'incoming', + false, ); } } @@ -662,7 +678,7 @@ export class ChatwootService { if (command === 'desconectar') { const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${body.inbox.name}*: `; - await this.createBotMessage(instance, msgLogout, 'incoming'); + await this.createBotMessage(instance, msgLogout, 'incoming', false); await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); await waInstance?.client?.ws?.close(); } @@ -819,7 +835,7 @@ export class ChatwootService { const bodyMessage = await this.getConversationMessage(body.message); - if (!bodyMessage) { + if (!bodyMessage && !isMedia) { return; } @@ -835,25 +851,57 @@ export class ChatwootService { const fileData = Buffer.from(downloadBase64.base64, 'base64'); - const fileName = `${path.join( - waInstance?.storePath, - 'chatwoot', - `${nameFile}`, - )}`; + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - writeFileSync(fileName, fileData, 'utf8'); + writeFileSync(fileName, fileData); if (body.key.remoteJid.includes('@g.us') && !body.key.fromMe) { const participantName = body.pushName; const content = `**${participantName}**\n\n${bodyMessage}`; - return await this.sendData(getConversion, fileName, messageType, content); + const send = await this.sendData( + getConversion, + fileName, + messageType, + false, + content, + ); + + if (!send) { + return; + } + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.saveMessageCache(); + + return send; } else { - return await this.sendData(getConversion, fileName, messageType, bodyMessage); + const send = await this.sendData( + getConversion, + fileName, + messageType, + false, + bodyMessage, + ); + + if (!send) { + return; + } + + this.messageCache = this.loadMessageCache(); + + this.messageCache.add(send.id.toString()); + + this.saveMessageCache(); + + return send; } } - if (body.key.remoteJid.includes('@g.us')) { + if (body.key.remoteJid.includes('@g.us') && !body.key.fromMe) { const participantName = body.pushName; const content = `**${participantName}**\n\n${bodyMessage}`; @@ -863,6 +911,7 @@ export class ChatwootService { getConversion, content, messageType, + false, ); this.messageCacheFile = path.join( @@ -885,6 +934,7 @@ export class ChatwootService { getConversion, bodyMessage, messageType, + false, ); this.messageCacheFile = path.join( @@ -899,7 +949,6 @@ export class ChatwootService { this.messageCache.add(send.id.toString()); this.saveMessageCache(); - return send; } } @@ -913,13 +962,13 @@ export class ChatwootService { } const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; - await this.createBotMessage(instance, msgStatus, 'incoming'); + await this.createBotMessage(instance, msgStatus, 'incoming', false); } if (event === 'connection.update') { if (body.state === 'open') { const msgConnection = `🚀 Conexão realizada com sucesso!`; - await this.createBotMessage(instance, msgConnection, 'incoming'); + await this.createBotMessage(instance, msgConnection, 'incoming', false); } } @@ -944,7 +993,7 @@ export class ChatwootService { if (event === 'qrcode.updated') { if (body.statusCode === 500) { const erroQRcode = `🚨 Limite de geração de QRCode atingido, para gerar um novo QRCode, envie a mensagem /iniciar novamente.`; - return await this.createBotMessage(instance, erroQRcode, 'incoming'); + return await this.createBotMessage(instance, erroQRcode, 'incoming', false); } else { const fileData = Buffer.from( body?.qrcode.base64.replace('data:image/png;base64,', ''), From 2565b934a5713a3a1449d747e93bec09228cdb80 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 19:55:23 -0300 Subject: [PATCH 31/41] fix: test duplicate message media in groups chatwoot --- src/whatsapp/services/chatwoot.service.ts | 69 +++++++++++++++-------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 03407a340..76e2d3ff2 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -389,7 +389,6 @@ export class ChatwootService { conversationId: number, content: string, messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, attachments?: { content: unknown; encoding: string; @@ -405,7 +404,6 @@ export class ChatwootService { content: content, message_type: messageType, attachments: attachments, - private: privateMessage, }, }); @@ -416,7 +414,6 @@ export class ChatwootService { instance: InstanceDto, content: string, messageType: 'incoming' | 'outgoing' | undefined, - privateMessage?: boolean, attachments?: { content: unknown; encoding: string; @@ -450,7 +447,6 @@ export class ChatwootService { content: content, message_type: messageType, attachments: attachments, - private: privateMessage, }, }); @@ -461,7 +457,6 @@ export class ChatwootService { conversationId: number, file: string, messageType: 'incoming' | 'outgoing' | undefined, - privateMessage: boolean, content?: string, ) { const data = new FormData(); @@ -472,8 +467,6 @@ export class ChatwootService { data.append('message_type', messageType); - data.append('private', privateMessage); - data.append('attachments[]', createReadStream(file)); const config = { @@ -489,7 +482,6 @@ export class ChatwootService { try { const { data } = await axios.request(config); - unlinkSync(file); return data; } catch (error) { @@ -648,7 +640,6 @@ export class ChatwootService { instance, `🚨 Instância ${body.inbox.name} já está conectada.`, 'incoming', - false, ); } } @@ -661,7 +652,6 @@ export class ChatwootService { instance, `⚠️ Instância ${body.inbox.name} não existe.`, 'incoming', - false, ); } @@ -670,7 +660,6 @@ export class ChatwootService { instance, `⚠️ Status da instância ${body.inbox.name}: *${state}*`, 'incoming', - false, ); } } @@ -678,7 +667,7 @@ export class ChatwootService { if (command === 'desconectar') { const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${body.inbox.name}*: `; - await this.createBotMessage(instance, msgLogout, 'incoming', false); + await this.createBotMessage(instance, msgLogout, 'incoming'); await waInstance?.client?.logout('Log out instance: ' + instance.instanceName); await waInstance?.client?.ws?.close(); } @@ -853,17 +842,23 @@ export class ChatwootService { const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; - writeFileSync(fileName, fileData); + writeFileSync(fileName, fileData, 'utf8'); - if (body.key.remoteJid.includes('@g.us') && !body.key.fromMe) { + if (body.key.remoteJid.includes('@g.us')) { const participantName = body.pushName; - const content = `**${participantName}**\n\n${bodyMessage}`; + let content: string; + + if (!body.key.fromMe) { + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + content = `${bodyMessage}`; + } + const send = await this.sendData( getConversion, fileName, messageType, - false, content, ); @@ -871,6 +866,13 @@ export class ChatwootService { return; } + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + this.messageCache = this.loadMessageCache(); this.messageCache.add(send.id.toString()); @@ -883,7 +885,6 @@ export class ChatwootService { getConversion, fileName, messageType, - false, bodyMessage, ); @@ -891,6 +892,13 @@ export class ChatwootService { return; } + this.messageCacheFile = path.join( + ROOT_DIR, + 'store', + 'chatwoot', + `${instance.instanceName}_cache.txt`, + ); + this.messageCache = this.loadMessageCache(); this.messageCache.add(send.id.toString()); @@ -901,19 +909,28 @@ export class ChatwootService { } } - if (body.key.remoteJid.includes('@g.us') && !body.key.fromMe) { + if (body.key.remoteJid.includes('@g.us')) { const participantName = body.pushName; - const content = `**${participantName}**\n\n${bodyMessage}`; + let content: string; + + if (!body.key.fromMe) { + content = `**${participantName}**\n\n${bodyMessage}`; + } else { + content = `${bodyMessage}`; + } const send = await this.createMessage( instance, getConversion, content, messageType, - false, ); + if (!send) { + return; + } + this.messageCacheFile = path.join( ROOT_DIR, 'store', @@ -934,9 +951,12 @@ export class ChatwootService { getConversion, bodyMessage, messageType, - false, ); + if (!send) { + return; + } + this.messageCacheFile = path.join( ROOT_DIR, 'store', @@ -949,6 +969,7 @@ export class ChatwootService { this.messageCache.add(send.id.toString()); this.saveMessageCache(); + return send; } } @@ -962,13 +983,13 @@ export class ChatwootService { } const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; - await this.createBotMessage(instance, msgStatus, 'incoming', false); + await this.createBotMessage(instance, msgStatus, 'incoming'); } if (event === 'connection.update') { if (body.state === 'open') { const msgConnection = `🚀 Conexão realizada com sucesso!`; - await this.createBotMessage(instance, msgConnection, 'incoming', false); + await this.createBotMessage(instance, msgConnection, 'incoming'); } } @@ -993,7 +1014,7 @@ export class ChatwootService { if (event === 'qrcode.updated') { if (body.statusCode === 500) { const erroQRcode = `🚨 Limite de geração de QRCode atingido, para gerar um novo QRCode, envie a mensagem /iniciar novamente.`; - return await this.createBotMessage(instance, erroQRcode, 'incoming', false); + return await this.createBotMessage(instance, erroQRcode, 'incoming'); } else { const fileData = Buffer.from( body?.qrcode.base64.replace('data:image/png;base64,', ''), From 8f062fab7d2e4361767d8b50b64fe6ac84988a27 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 20:05:26 -0300 Subject: [PATCH 32/41] fix: optimize send message from group with mentions --- src/whatsapp/services/whatsapp.service.ts | 59 +++++++++++------------ 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 2794761ac..52faf2d54 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1405,17 +1405,6 @@ export class WAStartupService { const sender = isJidGroup(jid) ? jid : isWA.jid; - if (isJidGroup(sender)) { - try { - this.logger.verbose('Getting group metadata'); - const metadata = await this.client.groupMetadata(sender); - - console.log('metadata', metadata); - } catch (error) { - throw new NotFoundException('Group not found'); - } - } - try { if (options?.delay) { this.logger.verbose('Delaying message'); @@ -1443,29 +1432,39 @@ export class WAStartupService { } let mentions: string[]; + if (isJidGroup(sender)) { + try { + this.logger.verbose('Getting group metadata'); + const groupMetadata = await this.client.groupMetadata(sender); - 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) { + this.logger.verbose('Mentions defined'); - if (options.mentions.everyOne) { - this.logger.verbose('Mentions everyone'); + if ( + !Array.isArray(options.mentions.mentioned) && + !options.mentions.everyOne + ) { + throw new BadRequestException('Mentions must be an array'); + } - 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 (options.mentions.everyOne) { + this.logger.verbose('Mentions everyone'); + + 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; + }); } - return jid; - }); + } + } catch (error) { + throw new NotFoundException('Group not found'); } } From 16d03d20ac7bdf12ba1a7fa57f3246c315aa7367 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 20:05:38 -0300 Subject: [PATCH 33/41] fix: optimize send message from group with mentions --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef15f8bba..b37de88e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ * 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 # 1.1.5 (2023-07-12 07:17) From 1c5ae4eb4d3f583c5137d733bee27b45f7f0b6dc Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 13 Jul 2023 20:12:29 -0300 Subject: [PATCH 34/41] fix: optimize send message from group with mentions --- src/whatsapp/services/chatwoot.service.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 76e2d3ff2..df35fa920 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -98,11 +98,11 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } if (!id) { - throw new Error('id is required'); + console.log('id is required'); } const contact = await client.contact.getContactable({ @@ -126,7 +126,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } const findInbox: any = await client.inboxes.list({ @@ -208,7 +208,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } let data: any = {}; @@ -241,11 +241,11 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } if (!id) { - throw new Error('id is required'); + console.log('id is required'); } const contact = await client.contacts.update({ @@ -261,7 +261,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } let query: any; @@ -289,7 +289,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } const isGroup = body.key.remoteJid.includes('@g.us'); @@ -371,7 +371,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } const inbox = (await client.inboxes.list({ @@ -610,7 +610,7 @@ export class ChatwootService { return; } catch (error) { - throw new Error(error); + console.log(error); } } @@ -803,7 +803,7 @@ export class ChatwootService { const client = await this.clientCw(instance); if (!client) { - throw new Error('client not found'); + console.log('client not found'); } const waInstance = this.waMonitor.waInstances[instance.instanceName]; From a7b05f1e856f96ac43863ef339ab97b2d5853b10 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 06:40:33 -0300 Subject: [PATCH 35/41] fix: optimize send message from group with mentions --- src/whatsapp/services/chatwoot.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index df35fa920..aa19725e8 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -389,6 +389,7 @@ export class ChatwootService { conversationId: number, content: string, messageType: 'incoming' | 'outgoing' | undefined, + privateMessage?: boolean, attachments?: { content: unknown; encoding: string; @@ -404,6 +405,7 @@ export class ChatwootService { content: content, message_type: messageType, attachments: attachments, + private: privateMessage || false, }, }); From be492a3e3f9b479ee0a2ebc16733f2c9ed1a9f32 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 08:31:43 -0300 Subject: [PATCH 36/41] fix: optimize send message from group with mentions --- src/whatsapp/services/whatsapp.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index 52faf2d54..027a97df8 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -1434,9 +1434,6 @@ export class WAStartupService { let mentions: string[]; if (isJidGroup(sender)) { try { - this.logger.verbose('Getting group metadata'); - const groupMetadata = await this.client.groupMetadata(sender); - if (options?.mentions) { this.logger.verbose('Mentions defined'); @@ -1450,6 +1447,8 @@ export class WAStartupService { 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 { From bb4490307d340c9185463a4d682641d6712f0c05 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 08:42:26 -0300 Subject: [PATCH 37/41] fix: Fixed name of the profile status in fetchInstances --- CHANGELOG.md | 1 + src/whatsapp/services/monitor.service.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b37de88e9..6471a23fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ * 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 # 1.1.5 (2023-07-12 07:17) diff --git a/src/whatsapp/services/monitor.service.ts b/src/whatsapp/services/monitor.service.ts index c07e9f32d..462c9f961 100644 --- a/src/whatsapp/services/monitor.service.ts +++ b/src/whatsapp/services/monitor.service.ts @@ -113,7 +113,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, apikey, chatwoot, }, @@ -128,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, }, }); } From 01d4a95d2d52fd2ead1392608f4e6fa64e62537e Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 09:42:56 -0300 Subject: [PATCH 38/41] fix: Fixed error 500 when logout in instance with status = close --- src/whatsapp/controllers/instance.controller.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index b08abc78b..a1f742bf2 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -401,6 +401,12 @@ 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('Instance already logged out'); + } + try { this.logger.verbose('logging out instance: ' + instanceName); await this.waMonitor.waInstances[instanceName]?.client?.logout( From 78bd4fd7de70301ecd0ceab059f97eb0e0a5bb88 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 09:43:04 -0300 Subject: [PATCH 39/41] fix: Fixed error 500 when logout in instance with status = close --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6471a23fc..16c11cc11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ * 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) From d680900f07bb40363f8215543671e9c09878931f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 11:56:42 -0300 Subject: [PATCH 40/41] feat: Added verbose logs and format chatwoot service --- .../controllers/instance.controller.ts | 11 +- src/whatsapp/services/chatwoot.service.ts | 425 +++++++++++++++++- 2 files changed, 409 insertions(+), 27 deletions(-) diff --git a/src/whatsapp/controllers/instance.controller.ts b/src/whatsapp/controllers/instance.controller.ts index a1f742bf2..ab2c4229a 100644 --- a/src/whatsapp/controllers/instance.controller.ts +++ b/src/whatsapp/controllers/instance.controller.ts @@ -404,7 +404,9 @@ export class InstanceController { const stateConn = await this.connectionState({ instanceName }); if (stateConn.state === 'close') { - throw new BadRequestException('Instance already logged out'); + throw new BadRequestException( + 'The "' + instanceName + '" instance is not connected', + ); } try { @@ -427,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/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index aa19725e8..4500fde81 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -26,6 +26,7 @@ export class ChatwootService { } private loadMessageCache(): Set { + this.logger.verbose('load message cache'); try { const cacheData = readFileSync(this.messageCacheFile, 'utf-8'); const cacheArray = cacheData.split('\n'); @@ -36,27 +37,35 @@ export class ChatwootService { } 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) { @@ -64,8 +73,11 @@ export class ChatwootService { 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, @@ -75,6 +87,8 @@ export class ChatwootService { }, }); + this.logger.verbose('client created'); + return client; } @@ -82,38 +96,46 @@ export class ChatwootService { 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 { - this.logger.verbose('find chatwoot: ' + instance.instanceName); 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) { - console.log('client not found'); + this.logger.warn('client not found'); + return null; } if (!id) { - console.log('id is required'); + 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; } @@ -123,16 +145,21 @@ export class ChatwootService { webhookUrl: string, qrcode: boolean, ) { + this.logger.verbose('init instance chatwoot: ' + instance.instanceName); + const client = await this.clientCw(instance); if (!client) { - console.log('client not found'); + 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); @@ -140,6 +167,7 @@ export class ChatwootService { let inboxId: number; if (!checkDuplicate) { + this.logger.verbose('create inbox in chatwoot'); const data = { type: 'api', webhook_url: webhookUrl, @@ -154,16 +182,24 @@ export class ChatwootService { }); 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( @@ -174,9 +210,15 @@ export class ChatwootService { '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: { @@ -185,7 +227,13 @@ export class ChatwootService { }, }); - await client.messages.create({ + 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: { @@ -193,8 +241,14 @@ export class ChatwootService { message_type: 'outgoing', }, }); + + if (!message) { + this.logger.warn('conversation not found'); + return null; + } } + this.logger.verbose('instance chatwoot initialized'); return true; } @@ -205,110 +259,151 @@ export class ChatwootService { isGroup: boolean, name?: string, ) { + this.logger.verbose('create contact to instance: ' + instance.instanceName); + const client = await this.clientCw(instance); if (!client) { - console.log('client not found'); + 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) { - console.log('client not found'); + this.logger.warn('client not found'); + return null; } if (!id) { - console.log('id is required'); + 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) { - console.log('client not found'); + 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) { - console.log('client not found'); + 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( @@ -320,6 +415,7 @@ export class ChatwootService { )) as any); } + this.logger.verbose('find or create contact in chatwoot'); const contact = (await this.findContact(instance, chatId)) || ((await this.createContact( @@ -330,29 +426,39 @@ export class ChatwootService { 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: { @@ -361,26 +467,49 @@ export class ChatwootService { }, }); + if (!conversation) { + this.logger.warn('conversation not found'); + return null; + } + + this.logger.verbose('conversation created'); return conversation.id; } catch (error) { - console.log(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) { - console.log('client not found'); + 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; } @@ -396,8 +525,16 @@ export class ChatwootService { 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, @@ -409,6 +546,13 @@ export class ChatwootService { }, }); + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('message created'); + return message; } @@ -422,26 +566,54 @@ export class ChatwootService { 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, @@ -452,6 +624,13 @@ export class ChatwootService { }, }); + if (!message) { + this.logger.warn('message not found'); + return null; + } + + this.logger.verbose('bot message created'); + return message; } @@ -461,16 +640,22 @@ export class ChatwootService { 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, @@ -482,12 +667,17 @@ export class ChatwootService { 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) { - console.log(error); + this.logger.error(error); unlinkSync(file); } } @@ -498,33 +688,69 @@ export class ChatwootService { 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, @@ -536,12 +762,17 @@ export class ChatwootService { 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) { - console.log(error); + this.logger.error(error); } } @@ -551,11 +782,17 @@ export class ChatwootService { 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'; @@ -574,7 +811,10 @@ export class ChatwootService { break; } + this.logger.verbose('type: ' + type); + if (type === 'audio') { + this.logger.verbose('send audio to instance: ' + waInstance.instanceName); const data: SendAudioDto = { number: number, audioMessage: { @@ -588,9 +828,11 @@ export class ChatwootService { 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: { @@ -604,24 +846,36 @@ export class ChatwootService { }, }; - if (caption && type !== 'audio') { + if (caption) { + this.logger.verbose('caption found'); data.mediaMessage.caption = caption; } await waInstance?.mediaMessage(data); + this.logger.verbose('media sent'); return; } catch (error) { - console.log(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; @@ -630,14 +884,19 @@ export class ChatwootService { 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.`, @@ -647,9 +906,12 @@ export class ChatwootService { } 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.`, @@ -658,6 +920,7 @@ export class ChatwootService { } if (state) { + this.logger.verbose('state: ' + state + ' found'); await this.createBotMessage( instance, `⚠️ Status da instância ${body.inbox.name}: *${state}*`, @@ -667,9 +930,14 @@ export class ChatwootService { } 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(); } @@ -680,19 +948,27 @@ export class ChatwootService { 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; @@ -703,9 +979,13 @@ export class ChatwootService { } 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; } @@ -717,6 +997,9 @@ export class ChatwootService { ); } } else { + this.logger.verbose('message is text'); + + this.logger.verbose('send text to whatsapp'); const data: SendTextDto = { number: chatId, textMessage: { @@ -734,6 +1017,8 @@ export class ChatwootService { } if (body.message_type === 'template' && body.content_type === 'input_csat') { + this.logger.verbose('check if is csat'); + const data: SendTextDto = { number: chatId, textMessage: { @@ -745,18 +1030,21 @@ export class ChatwootService { }, }; + this.logger.verbose('send text to whatsapp'); + await waInstance?.textMessage(data); } return { message: 'bot' }; } catch (error) { - console.log(error); + this.logger.error(error); return { message: 'bot' }; } } private isMediaMessage(message: any) { + this.logger.verbose('check if is media message'); const media = [ 'imageMessage', 'documentMessage', @@ -767,10 +1055,16 @@ export class ChatwootService { ]; const messageKeys = Object.keys(message); - return messageKeys.some((key) => media.includes(key)); + + 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, @@ -784,53 +1078,88 @@ export class ChatwootService { 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); - return typeKey ? types[typeKey] : 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) { - console.log('client not found'); + 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); - const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; 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, @@ -844,19 +1173,28 @@ export class ChatwootService { 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, @@ -865,6 +1203,7 @@ export class ChatwootService { ); if (!send) { + this.logger.warn('message not sent'); return; } @@ -879,10 +1218,14 @@ export class ChatwootService { 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, @@ -891,6 +1234,7 @@ export class ChatwootService { ); if (!send) { + this.logger.warn('message not sent'); return; } @@ -905,23 +1249,29 @@ export class ChatwootService { 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, @@ -930,6 +1280,7 @@ export class ChatwootService { ); if (!send) { + this.logger.warn('message not sent'); return; } @@ -944,10 +1295,14 @@ export class ChatwootService { 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, @@ -956,6 +1311,7 @@ export class ChatwootService { ); if (!send) { + this.logger.warn('message not sent'); return; } @@ -970,6 +1326,7 @@ export class ChatwootService { this.messageCache.add(send.id.toString()); + this.logger.verbose('save message cache'); this.saveMessageCache(); return send; @@ -977,34 +1334,47 @@ export class ChatwootService { } 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, }); @@ -1014,10 +1384,15 @@ export class ChatwootService { } 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', @@ -1029,8 +1404,12 @@ export class ChatwootService { `${`${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!', @@ -1039,11 +1418,13 @@ export class ChatwootService { ); 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) { - console.log(error); + this.logger.error(error); } } } From e19b8255d3ee64526719d2472ca1c8d0f7a2e5d4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 14 Jul 2023 13:16:54 -0300 Subject: [PATCH 41/41] feat: Added verbose logs and format chatwoot service --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c11cc11..9fd69a552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * 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