From f7f0f8251b24793564fa97778255c25a026eec07 Mon Sep 17 00:00:00 2001 From: "@milesibastos" Date: Tue, 21 May 2024 11:48:13 -0300 Subject: [PATCH 01/12] chore: build docker image to platforms: linux/amd64,linux/arm64 --- .github/workflows/publish_docker_image.yml | 40 ++++++++++------------ 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 7c9901bc..f28cfbe7 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -2,18 +2,13 @@ name: Build Docker image on: push: - branches: - - develop - - main tags: - - v* - workflow_dispatch: + - "v*.*.*" jobs: - build: + build_deploy: + name: Build and Deploy runs-on: ubuntu-latest - env: - GIT_REF: ${{ github.head_ref || github.ref_name }} # ref_name to get tags/branches permissions: contents: read packages: write @@ -21,21 +16,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: set docker tag - run: | - echo "DOCKER_TAG=ghcr.io/atendai/evolution-api:$GIT_REF" >> $GITHUB_ENV - - - name: replace docker tag if main - if: github.ref_name == 'main' - run: | - echo "DOCKER_TAG=ghcr.io/atendai/evolution-api:latest" >> $GITHUB_ENV - - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: @@ -44,10 +39,13 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v2 + id: docker_build + uses: docker/build-push-action@v5 with: - context: . - file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: ${{ env.DOCKER_TAG }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file From 2fcb476c50d561de454491047fe89da6977a39f4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 23 May 2024 11:30:47 -0300 Subject: [PATCH 02/12] Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) --- src/api/controllers/instance.controller.ts | 30 +++++++++++++++++----- src/api/guards/auth.guard.ts | 19 +++++++++++--- src/api/repository/auth.repository.ts | 14 ++++++++++ src/api/routes/instance.router.ts | 4 ++- src/api/services/monitor.service.ts | 5 +++- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index 0ffa885d..0d743f37 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -3,9 +3,9 @@ import { isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import { v4 } from 'uuid'; -import { ConfigService, HttpServer, WaBusiness } from '../../config/env.config'; +import { Auth, ConfigService, HttpServer, WaBusiness } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; -import { BadRequestException, InternalServerErrorException } from '../../exceptions'; +import { BadRequestException, InternalServerErrorException, UnauthorizedException } from '../../exceptions'; import { InstanceDto, SetPresenceDto } from '../dto/instance.dto'; import { ChatwootService } from '../integrations/chatwoot/services/chatwoot.service'; import { RabbitmqService } from '../integrations/rabbitmq/services/rabbitmq.service'; @@ -679,11 +679,27 @@ export class InstanceController { }; } - public async fetchInstances({ instanceName, instanceId, number }: InstanceDto) { - if (instanceName) { - this.logger.verbose('requested fetchInstances from ' + instanceName + ' instance'); - this.logger.verbose('instanceName: ' + instanceName); - return this.waMonitor.instanceInfo(instanceName); + public async fetchInstances({ instanceName, instanceId, number }: InstanceDto, key: string) { + const env = this.configService.get('AUTHENTICATION').API_KEY; + + let name = instanceName; + let arrayReturn = false; + + if (env.KEY !== key) { + const instanceByKey = await this.repository.auth.findByKey(key); + console.log('instanceByKey', instanceByKey); + if (instanceByKey) { + name = instanceByKey._id; + arrayReturn = true; + } else { + throw new UnauthorizedException(); + } + } + + if (name) { + this.logger.verbose('requested fetchInstances from ' + name + ' instance'); + this.logger.verbose('instanceName: ' + name); + return this.waMonitor.instanceInfo(name, arrayReturn); } else if (instanceId || number) { return this.waMonitor.instanceInfoById(instanceId, number); } diff --git a/src/api/guards/auth.guard.ts b/src/api/guards/auth.guard.ts index ccc73a58..b7c7c7c0 100644 --- a/src/api/guards/auth.guard.ts +++ b/src/api/guards/auth.guard.ts @@ -59,6 +59,10 @@ async function apikey(req: Request, _: Response, next: NextFunction) { const env = configService.get('AUTHENTICATION').API_KEY; const key = req.get('apikey'); + if (!key) { + throw new UnauthorizedException(); + } + if (env.KEY === key) { return next(); } @@ -66,12 +70,19 @@ async function apikey(req: Request, _: Response, next: NextFunction) { if ((req.originalUrl.includes('/instance/create') || req.originalUrl.includes('/instance/fetchInstances')) && !key) { throw new ForbiddenException('Missing global api key', 'The global api key must be set'); } + const param = req.params as unknown as InstanceDto; try { - const param = req.params as unknown as InstanceDto; - const instanceKey = await repository.auth.find(param.instanceName); - if (instanceKey.apikey === key) { - return next(); + if (param?.instanceName) { + const instanceKey = await repository.auth.find(param.instanceName); + if (instanceKey?.apikey === key) { + return next(); + } + } else { + const instanceByKey = await repository.auth.findByKey(key); + if (instanceByKey) { + return next(); + } } } catch (error) { logger.error(error); diff --git a/src/api/repository/auth.repository.ts b/src/api/repository/auth.repository.ts index 1e4bbb81..6a296902 100644 --- a/src/api/repository/auth.repository.ts +++ b/src/api/repository/auth.repository.ts @@ -68,6 +68,20 @@ export class AuthRepository extends Repository { } } + public async findByKey(key: string): Promise { + try { + this.logger.verbose('finding auth'); + if (this.dbSettings.ENABLED) { + this.logger.verbose('finding auth in db'); + return await this.authModel.findOne({ apikey: key }); + } + + return {}; + } catch (error) { + return {}; + } + } + public async list(): Promise { try { if (this.dbSettings.ENABLED) { diff --git a/src/api/routes/instance.router.ts b/src/api/routes/instance.router.ts index a56271a4..d2cc6fb8 100644 --- a/src/api/routes/instance.router.ts +++ b/src/api/routes/instance.router.ts @@ -103,13 +103,15 @@ export class InstanceRouter extends RouterBroker { logger.verbose('request body: '); logger.verbose(req.body); + const key = req.get('apikey'); + logger.verbose('request query: '); logger.verbose(req.query); const response = await this.dataValidate({ request: req, schema: null, ClassRef: InstanceDto, - execute: (instance) => instanceController.fetchInstances(instance), + execute: (instance) => instanceController.fetchInstances(instance, key), }); return res.status(HttpStatus.OK).json(response); diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index afeffa95..af93fa74 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -83,7 +83,7 @@ export class WAMonitoringService { } } - public async instanceInfo(instanceName?: string) { + public async instanceInfo(instanceName?: string, arrayReturn = false) { this.logger.verbose('get instance info'); if (instanceName && !this.waInstances[instanceName]) { throw new NotFoundException(`Instance "${instanceName}" not found`); @@ -171,6 +171,9 @@ export class WAMonitoringService { this.logger.verbose('return instance info: ' + instances.length); + if (arrayReturn) { + return [instances.find((i) => i.instance.instanceName === instanceName) ?? instances]; + } return instances.find((i) => i.instance.instanceName === instanceName) ?? instances; } From f7dcab37360a4c077a8cd0f5534c734c3cde14ff Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 23 May 2024 11:31:45 -0300 Subject: [PATCH 03/12] Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 074fad8e..d30abd4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 1.8.0 (develop) + +### Feature + +* Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) + # 1.7.5 (2024-05-21 08:50) ### Fixed From 68dcc1c86aeef0743490be1ea9646d9e248d5fe9 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Thu, 23 May 2024 11:56:52 -0300 Subject: [PATCH 04/12] adjusts in create instance --- src/api/controllers/instance.controller.ts | 1 - src/api/guards/auth.guard.ts | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/controllers/instance.controller.ts b/src/api/controllers/instance.controller.ts index 0d743f37..0ead0be1 100644 --- a/src/api/controllers/instance.controller.ts +++ b/src/api/controllers/instance.controller.ts @@ -687,7 +687,6 @@ export class InstanceController { if (env.KEY !== key) { const instanceByKey = await this.repository.auth.findByKey(key); - console.log('instanceByKey', instanceByKey); if (instanceByKey) { name = instanceByKey._id; arrayReturn = true; diff --git a/src/api/guards/auth.guard.ts b/src/api/guards/auth.guard.ts index b7c7c7c0..8168e80f 100644 --- a/src/api/guards/auth.guard.ts +++ b/src/api/guards/auth.guard.ts @@ -79,9 +79,11 @@ async function apikey(req: Request, _: Response, next: NextFunction) { return next(); } } else { - const instanceByKey = await repository.auth.findByKey(key); - if (instanceByKey) { - return next(); + if (req.originalUrl.includes('/instance/fetchInstances')) { + const instanceByKey = await repository.auth.findByKey(key); + if (instanceByKey) { + return next(); + } } } } catch (error) { From f0d40eea1594a10db63505f8be3d24c7f3d4f3ae Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 24 May 2024 18:48:54 -0300 Subject: [PATCH 05/12] New global mode for rabbitmq events --- CHANGELOG.md | 1 + Docker/.env.example | 28 +++- Dockerfile | 32 +++- package.json | 2 +- .../integrations/rabbitmq/libs/amqp.server.ts | 35 +++++ src/api/services/channel.service.ts | 145 +++++++++++++----- src/config/env.config.ts | 66 +++++++- src/dev-env.yml | 30 +++- src/docs/swagger.yaml | 2 +- src/main.ts | 8 +- 10 files changed, 298 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d30abd4a..a4f2197c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Feature * Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) +* New global mode for rabbitmq events # 1.7.5 (2024-05-21 08:50) diff --git a/Docker/.env.example b/Docker/.env.example index 04dd0805..3bee33ca 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -47,9 +47,33 @@ DATABASE_SAVE_DATA_CONTACTS=false DATABASE_SAVE_DATA_CHATS=false RABBITMQ_ENABLED=false -RABBITMQ_RABBITMQ_MODE=global -RABBITMQ_EXCHANGE_NAME=evolution_exchange RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 +RABBITMQ_EXCHANGE_NAME=evolution_exchange +RABBITMQ_GLOBAL_ENABLED=false +RABBITMQ_EVENTS_APPLICATION_STARTUP=false +RABBITMQ_EVENTS_QRCODE_UPDATED=true +RABBITMQ_EVENTS_MESSAGES_SET=true +RABBITMQ_EVENTS_MESSAGES_UPSERT=true +RABBITMQ_EVENTS_MESSAGES_UPDATE=true +RABBITMQ_EVENTS_MESSAGES_DELETE=true +RABBITMQ_EVENTS_SEND_MESSAGE=true +RABBITMQ_EVENTS_CONTACTS_SET=true +RABBITMQ_EVENTS_CONTACTS_UPSERT=true +RABBITMQ_EVENTS_CONTACTS_UPDATE=true +RABBITMQ_EVENTS_PRESENCE_UPDATE=true +RABBITMQ_EVENTS_CHATS_SET=true +RABBITMQ_EVENTS_CHATS_UPSERT=true +RABBITMQ_EVENTS_CHATS_UPDATE=true +RABBITMQ_EVENTS_CHATS_DELETE=true +RABBITMQ_EVENTS_GROUPS_UPSERT=true +RABBITMQ_EVENTS_GROUPS_UPDATE=true +RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE=true +RABBITMQ_EVENTS_CONNECTION_UPDATE=true +RABBITMQ_EVENTS_LABELS_EDIT=true +RABBITMQ_EVENTS_LABELS_ASSOCIATION=true +RABBITMQ_EVENTS_CALL=true +RABBITMQ_EVENTS_TYPEBOT_START=false +RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS=false WEBSOCKET_ENABLED=false WEBSOCKET_GLOBAL_EVENTS=false diff --git a/Dockerfile b/Dockerfile index 2dac29c0..9db93aeb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:20.7.0-alpine AS builder -LABEL version="1.7.5" description="Api to control whatsapp features through http requests." +LABEL version="1.8.0" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL contact="contato@agenciadgcode.com" @@ -59,9 +59,35 @@ ENV DATABASE_SAVE_DATA_CONTACTS=false ENV DATABASE_SAVE_DATA_CHATS=false ENV RABBITMQ_ENABLED=false -ENV RABBITMQ_MODE=global -ENV RABBITMQ_EXCHANGE_NAME=evolution_exchange ENV RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672 +ENV RABBITMQ_EXCHANGE_NAME=evolution_exchange +ENV RABBITMQ_GLOBAL_ENABLED=false +ENV RABBITMQ_EVENTS_APPLICATION_STARTUP=false +ENV RABBITMQ_EVENTS_INSTANCE_CREATE=false +ENV RABBITMQ_EVENTS_INSTANCE_DELETE=false +ENV RABBITMQ_EVENTS_QRCODE_UPDATED=true +ENV RABBITMQ_EVENTS_MESSAGES_SET=true +ENV RABBITMQ_EVENTS_MESSAGES_UPSERT=true +ENV RABBITMQ_EVENTS_MESSAGES_UPDATE=true +ENV RABBITMQ_EVENTS_MESSAGES_DELETE=true +ENV RABBITMQ_EVENTS_SEND_MESSAGE=true +ENV RABBITMQ_EVENTS_CONTACTS_SET=true +ENV RABBITMQ_EVENTS_CONTACTS_UPSERT=true +ENV RABBITMQ_EVENTS_CONTACTS_UPDATE=true +ENV RABBITMQ_EVENTS_PRESENCE_UPDATE=true +ENV RABBITMQ_EVENTS_CHATS_SET=true +ENV RABBITMQ_EVENTS_CHATS_UPSERT=true +ENV RABBITMQ_EVENTS_CHATS_UPDATE=true +ENV RABBITMQ_EVENTS_CHATS_DELETE=true +ENV RABBITMQ_EVENTS_GROUPS_UPSERT=true +ENV RABBITMQ_EVENTS_GROUPS_UPDATE=true +ENV RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE=true +ENV RABBITMQ_EVENTS_CONNECTION_UPDATE=true +ENV RABBITMQ_EVENTS_LABELS_EDIT=true +ENV RABBITMQ_EVENTS_LABELS_ASSOCIATION=true +ENV RABBITMQ_EVENTS_CALL=true +ENV RABBITMQ_EVENTS_TYPEBOT_START=false +ENV RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS=false ENV WEBSOCKET_ENABLED=false ENV WEBSOCKET_GLOBAL_EVENTS=false diff --git a/package.json b/package.json index af6198c7..b6f78480 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "1.7.5", + "version": "1.8.0", "description": "Rest api for communication with WhatsApp", "main": "./dist/src/main.js", "scripts": { diff --git a/src/api/integrations/rabbitmq/libs/amqp.server.ts b/src/api/integrations/rabbitmq/libs/amqp.server.ts index 5628fac9..99c10f66 100644 --- a/src/api/integrations/rabbitmq/libs/amqp.server.ts +++ b/src/api/integrations/rabbitmq/libs/amqp.server.ts @@ -42,6 +42,41 @@ export const getAMQP = (): amqp.Channel | null => { return amqpChannel; }; +export const initGlobalQueues = () => { + logger.info('Initializing global queues'); + const events = configService.get('RABBITMQ').EVENTS; + + if (!events) { + logger.warn('No events to initialize on AMQP'); + return; + } + + const eventKeys = Object.keys(events); + + eventKeys.forEach((event) => { + if (events[event] === false) return; + + const queueName = `${event.replace(/_/g, '.').toLowerCase()}`; + const amqp = getAMQP(); + const exchangeName = 'evolution_exchange'; + + amqp.assertExchange(exchangeName, 'topic', { + durable: true, + autoDelete: false, + }); + + amqp.assertQueue(queueName, { + durable: true, + autoDelete: false, + arguments: { + 'x-queue-type': 'quorum', + }, + }); + + amqp.bindQueue(queueName, exchangeName, event); + }); +}; + export const initQueues = (instanceName: string, events: string[]) => { if (!events || !events.length) return; diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 0a35154c..d42c5b1a 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -13,6 +13,7 @@ import { Database, HttpServer, Log, + Rabbitmq, Sqs, Webhook, Websocket, @@ -688,6 +689,9 @@ export class ChannelStartupService { const rabbitmqLocal = this.localRabbitmq.events; const sqsLocal = this.localSqs.events; const serverUrl = this.configService.get('SERVER').URL; + const rabbitmqEnabled = this.configService.get('RABBITMQ').ENABLED; + const rabbitmqGlobal = this.configService.get('RABBITMQ').GLOBAL_ENABLED; + const rabbitmqEvents = this.configService.get('RABBITMQ').EVENTS; const we = event.replace(/[.-]/gm, '_').toUpperCase(); const transformedWe = we.replace(/_/gm, '-').toLowerCase(); const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds @@ -698,67 +702,134 @@ export class ChannelStartupService { const tokenStore = await this.repository.auth.find(this.instanceName); const instanceApikey = tokenStore?.apikey || 'Apikey not found'; - if (this.localRabbitmq.enabled) { + if (rabbitmqEnabled) { const amqp = getAMQP(); - - if (amqp) { + if (this.localRabbitmq.enabled && amqp) { if (Array.isArray(rabbitmqLocal) && rabbitmqLocal.includes(we)) { const exchangeName = this.instanceName ?? 'evolution_exchange'; - // await amqp.assertExchange(exchangeName, 'topic', { - // durable: true, - // autoDelete: false, - // }); + let retry = 0; - await this.assertExchangeAsync(amqp, exchangeName, 'topic', { - durable: true, - autoDelete: false, - }); + while (retry < 3) { + try { + await amqp.assertExchange(exchangeName, 'topic', { + durable: true, + autoDelete: false, + }); - const queueName = `${this.instanceName}.${event}`; + const queueName = `${this.instanceName}.${event}`; - await amqp.assertQueue(queueName, { - durable: true, - autoDelete: false, - arguments: { - 'x-queue-type': 'quorum', - }, - }); + await amqp.assertQueue(queueName, { + durable: true, + autoDelete: false, + arguments: { + 'x-queue-type': 'quorum', + }, + }); - await amqp.bindQueue(queueName, exchangeName, event); + await amqp.bindQueue(queueName, exchangeName, event); - const message = { - event, - instance: this.instance.name, - data, - server_url: serverUrl, - date_time: now, - sender: this.wuid, - }; + const message = { + event, + instance: this.instance.name, + data, + server_url: serverUrl, + date_time: now, + sender: this.wuid, + }; - if (expose && instanceApikey) { - message['apikey'] = instanceApikey; + if (expose && instanceApikey) { + message['apikey'] = instanceApikey; + } + + await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: ChannelStartupService.name + '.sendData-RabbitMQ', + event, + instance: this.instance.name, + data, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + date_time: now, + sender: this.wuid, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); + } + break; + } catch (error) { + retry++; + } } + } + } - await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); + if (rabbitmqGlobal && rabbitmqEvents[we] && amqp) { + const exchangeName = 'evolution_exchange'; - if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { - const logData = { - local: ChannelStartupService.name + '.sendData-RabbitMQ', + let retry = 0; + + while (retry < 3) { + try { + await amqp.assertExchange(exchangeName, 'topic', { + durable: true, + autoDelete: false, + }); + + const queueName = transformedWe; + + await amqp.assertQueue(queueName, { + durable: true, + autoDelete: false, + arguments: { + 'x-queue-type': 'quorum', + }, + }); + + await amqp.bindQueue(queueName, exchangeName, event); + + const message = { event, instance: this.instance.name, data, server_url: serverUrl, - apikey: (expose && instanceApikey) || null, date_time: now, sender: this.wuid, }; if (expose && instanceApikey) { - logData['apikey'] = instanceApikey; + message['apikey'] = instanceApikey; + } + await amqp.publish(exchangeName, event, Buffer.from(JSON.stringify(message))); + + if (this.configService.get('LOG').LEVEL.includes('WEBHOOKS')) { + const logData = { + local: ChannelStartupService.name + '.sendData-RabbitMQ-Global', + event, + instance: this.instance.name, + data, + server_url: serverUrl, + apikey: (expose && instanceApikey) || null, + date_time: now, + sender: this.wuid, + }; + + if (expose && instanceApikey) { + logData['apikey'] = instanceApikey; + } + + this.logger.log(logData); } - this.logger.log(logData); + break; + } catch (error) { + retry++; } } } diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 4f37b090..eab883f7 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -63,11 +63,42 @@ export type Database = { SAVE_DATA: SaveData; }; +export type EventsRabbitmq = { + APPLICATION_STARTUP: boolean; + INSTANCE_CREATE: boolean; + INSTANCE_DELETE: boolean; + QRCODE_UPDATED: boolean; + MESSAGES_SET: boolean; + MESSAGES_UPSERT: boolean; + MESSAGES_UPDATE: boolean; + MESSAGES_DELETE: boolean; + SEND_MESSAGE: boolean; + CONTACTS_SET: boolean; + CONTACTS_UPDATE: boolean; + CONTACTS_UPSERT: boolean; + PRESENCE_UPDATE: boolean; + CHATS_SET: boolean; + CHATS_UPDATE: boolean; + CHATS_DELETE: boolean; + CHATS_UPSERT: boolean; + CONNECTION_UPDATE: boolean; + LABELS_EDIT: boolean; + LABELS_ASSOCIATION: boolean; + GROUPS_UPSERT: boolean; + GROUP_UPDATE: boolean; + GROUP_PARTICIPANTS_UPDATE: boolean; + CALL: boolean; + NEW_JWT_TOKEN: boolean; + TYPEBOT_START: boolean; + TYPEBOT_CHANGE_STATUS: boolean; +}; + export type Rabbitmq = { ENABLED: boolean; - MODE: string; // global, single, isolated - EXCHANGE_NAME: string; // available for global and single, isolated mode will use instance name as exchange URI: string; + EXCHANGE_NAME: string; + GLOBAL_ENABLED: boolean; + EVENTS: EventsRabbitmq; }; export type Sqs = { @@ -276,9 +307,38 @@ export class ConfigService { }, RABBITMQ: { ENABLED: process.env?.RABBITMQ_ENABLED === 'true', - MODE: process.env?.RABBITMQ_MODE || 'isolated', + GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true', EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange', URI: process.env.RABBITMQ_URI || '', + EVENTS: { + APPLICATION_STARTUP: process.env?.RABBITMQ_EVENTS_APPLICATION_STARTUP === 'true', + INSTANCE_CREATE: process.env?.RABBITMQ_EVENTS_INSTANCE_CREATE === 'true', + INSTANCE_DELETE: process.env?.RABBITMQ_EVENTS_INSTANCE_DELETE === 'true', + QRCODE_UPDATED: process.env?.RABBITMQ_EVENTS_QRCODE_UPDATED === 'true', + MESSAGES_SET: process.env?.RABBITMQ_EVENTS_MESSAGES_SET === 'true', + MESSAGES_UPSERT: process.env?.RABBITMQ_EVENTS_MESSAGES_UPSERT === 'true', + MESSAGES_UPDATE: process.env?.RABBITMQ_EVENTS_MESSAGES_UPDATE === 'true', + MESSAGES_DELETE: process.env?.RABBITMQ_EVENTS_MESSAGES_DELETE === 'true', + SEND_MESSAGE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE === 'true', + CONTACTS_SET: process.env?.RABBITMQ_EVENTS_CONTACTS_SET === 'true', + CONTACTS_UPDATE: process.env?.RABBITMQ_EVENTS_CONTACTS_UPDATE === 'true', + CONTACTS_UPSERT: process.env?.RABBITMQ_EVENTS_CONTACTS_UPSERT === 'true', + PRESENCE_UPDATE: process.env?.RABBITMQ_EVENTS_PRESENCE_UPDATE === 'true', + CHATS_SET: process.env?.RABBITMQ_EVENTS_CHATS_SET === 'true', + CHATS_UPDATE: process.env?.RABBITMQ_EVENTS_CHATS_UPDATE === 'true', + CHATS_UPSERT: process.env?.RABBITMQ_EVENTS_CHATS_UPSERT === 'true', + CHATS_DELETE: process.env?.RABBITMQ_EVENTS_CHATS_DELETE === 'true', + CONNECTION_UPDATE: process.env?.RABBITMQ_EVENTS_CONNECTION_UPDATE === 'true', + LABELS_EDIT: process.env?.RABBITMQ_EVENTS_LABELS_EDIT === 'true', + LABELS_ASSOCIATION: process.env?.RABBITMQ_EVENTS_LABELS_ASSOCIATION === 'true', + GROUPS_UPSERT: process.env?.RABBITMQ_EVENTS_GROUPS_UPSERT === 'true', + GROUP_UPDATE: process.env?.RABBITMQ_EVENTS_GROUPS_UPDATE === 'true', + GROUP_PARTICIPANTS_UPDATE: process.env?.RABBITMQ_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true', + CALL: process.env?.RABBITMQ_EVENTS_CALL === 'true', + NEW_JWT_TOKEN: process.env?.RABBITMQ_EVENTS_NEW_JWT_TOKEN === 'true', + TYPEBOT_START: process.env?.RABBITMQ_EVENTS_TYPEBOT_START === 'true', + TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + }, }, SQS: { ENABLED: process.env?.SQS_ENABLED === 'true', diff --git a/src/dev-env.yml b/src/dev-env.yml index e7f0fae3..d1f638f6 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -79,9 +79,35 @@ DATABASE: RABBITMQ: ENABLED: false - MODE: "global" - EXCHANGE_NAME: "evolution_exchange" URI: "amqp://guest:guest@localhost:5672" + EXCHANGE_NAME: evolution_exchange + GLOBAL_ENABLED: true + EVENTS: + APPLICATION_STARTUP: false + INSTANCE_CREATE: false + INSTANCE_DELETE: false + QRCODE_UPDATED: false + MESSAGES_SET: false + MESSAGES_UPSERT: true + MESSAGES_UPDATE: true + MESSAGES_DELETE: false + SEND_MESSAGE: false + CONTACTS_SET: false + CONTACTS_UPSERT: false + CONTACTS_UPDATE: false + PRESENCE_UPDATE: false + CHATS_SET: false + CHATS_UPSERT: false + CHATS_UPDATE: false + CHATS_DELETE: false + GROUPS_UPSERT: true + GROUP_UPDATE: true + GROUP_PARTICIPANTS_UPDATE: true + CONNECTION_UPDATE: true + CALL: false + # This events is used with Typebot + TYPEBOT_START: false + TYPEBOT_CHANGE_STATUS: false SQS: ENABLED: true diff --git a/src/docs/swagger.yaml b/src/docs/swagger.yaml index 0bcab52d..59b252d3 100644 --- a/src/docs/swagger.yaml +++ b/src/docs/swagger.yaml @@ -25,7 +25,7 @@ info: [![Run in Postman](https://run.pstmn.io/button.svg)](https://god.gw.postman.com/run-collection/26869335-5546d063-156b-4529-915f-909dd628c090?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D26869335-5546d063-156b-4529-915f-909dd628c090%26entityType%3Dcollection%26workspaceId%3D339a4ee7-378b-45c9-b5b8-fd2c0a9c2442) - version: 1.7.5 + version: 1.8.0 contact: name: DavidsonGomes email: contato@agenciadgcode.com diff --git a/src/main.ts b/src/main.ts index 942c725f..815e2a11 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,7 @@ import cors from 'cors'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; -import { initAMQP } from './api/integrations/rabbitmq/libs/amqp.server'; +import { initAMQP, initGlobalQueues } from './api/integrations/rabbitmq/libs/amqp.server'; import { initSQS } from './api/integrations/sqs/libs/sqs.server'; import { initIO } from './api/integrations/websocket/libs/socket.server'; import { HttpStatus, router } from './api/routes/index.router'; @@ -128,7 +128,11 @@ function bootstrap() { initIO(server); - if (configService.get('RABBITMQ')?.ENABLED) initAMQP(); + if (configService.get('RABBITMQ')?.ENABLED) { + initAMQP().then(() => { + if (configService.get('RABBITMQ')?.GLOBAL_ENABLED) initGlobalQueues(); + }); + } if (configService.get('SQS')?.ENABLED) initSQS(); From d7c3779ff7da5e5c88e14d5499264392c5a50208 Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Sat, 25 May 2024 10:05:04 -0300 Subject: [PATCH 06/12] fix: Add merge_brazil_contacts flag to ChannelStartupService --- src/api/services/channel.service.ts | 3 +++ src/api/types/wa.types.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index d42c5b1a..ccfb30d1 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -306,6 +306,9 @@ export class ChannelStartupService { this.localChatwoot.conversation_pending = data?.conversation_pending; this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`); + this.localChatwoot.merge_brazil_contacts = data?.merge_brazil_contacts; + this.logger.verbose(`Chatwoot merge brazil contacts: ${this.localChatwoot.merge_brazil_contacts}`); + this.localChatwoot.import_contacts = data?.import_contacts; this.logger.verbose(`Chatwoot import contacts: ${this.localChatwoot.import_contacts}`); diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 066691e4..9c33ac6f 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -69,6 +69,7 @@ export declare namespace wa { number?: string; reopen_conversation?: boolean; conversation_pending?: boolean; + merge_brazil_contacts?: boolean; import_contacts?: boolean; import_messages?: boolean; days_limit_import_messages?: number; From ca0b0d460262becf822fdfb9f7d47b7c894722d0 Mon Sep 17 00:00:00 2001 From: Deivison Lincoln Date: Sat, 25 May 2024 10:09:37 -0300 Subject: [PATCH 07/12] Add merge_brazil_contacts flag to ChannelStartupService --- src/api/services/channel.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index ccfb30d1..ac4eae90 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -332,6 +332,7 @@ export class ChannelStartupService { this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); + this.logger.verbose(`Chatwoot merge brazil contacts: ${data.merge_brazil_contacts}`); this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`); this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`); this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`); @@ -360,7 +361,7 @@ export class ChannelStartupService { this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`); this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`); this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`); - this.logger.verbose(`Chatwoot merge brazilian contacts: ${data.import_contacts}`); + this.logger.verbose(`Chatwoot merge brazilian contacts: ${data.merge_brazil_contacts}`); this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`); this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`); this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`); From e4b6f4ff0d49416e8dd70c7b6239576d15068407 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 27 May 2024 09:23:53 -0300 Subject: [PATCH 08/12] correction in message formatting when generated by AI as markdown in typebot --- CHANGELOG.md | 3 ++ .../typebot/services/typebot.service.ts | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f2197c..35cab59a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * Now in the manager, when logging in with the client's apikey, the listing only shows the instance corresponding to the provided apikey (only with MongoDB) * New global mode for rabbitmq events +### Fixed +* Correction in message formatting when generated by AI as markdown in typebot + # 1.7.5 (2024-05-21 08:50) ### Fixed diff --git a/src/api/integrations/typebot/services/typebot.service.ts b/src/api/integrations/typebot/services/typebot.service.ts index eae67117..a79f9d3d 100644 --- a/src/api/integrations/typebot/services/typebot.service.ts +++ b/src/api/integrations/typebot/services/typebot.service.ts @@ -519,18 +519,32 @@ export class TypebotService { text += element.text; } - if ( - element.children && - (element.type === 'p' || - element.type === 'a' || - element.type === 'inline-variable' || - element.type === 'variable') - ) { + if (element.children && element.type !== 'a') { for (const child of element.children) { text += applyFormatting(child); } } + if (element.type === 'p') { + text = text.trim() + '\n'; + } + + if (element.type === 'ol') { + text = + '\n' + + text + .split('\n') + .map((line, index) => (line ? `${index + 1}. ${line}` : '')) + .join('\n'); + } + + if (element.type === 'li') { + text = text + .split('\n') + .map((line) => (line ? ` ${line}` : '')) + .join('\n'); + } + let formats = ''; if (element.bold) { @@ -558,6 +572,7 @@ export class TypebotService { for (const message of messages) { if (message.type === 'text') { let formattedText = ''; + console.log('message.content', message.content); for (const richText of message.content.richText) { for (const element of richText.children) { From 4c3fb5e762b7d9fd78ff20c2bfb8de44d3958b8f Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 27 May 2024 09:27:42 -0300 Subject: [PATCH 09/12] correction in message formatting when generated by AI as markdown in typebot --- src/api/integrations/typebot/services/typebot.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/api/integrations/typebot/services/typebot.service.ts b/src/api/integrations/typebot/services/typebot.service.ts index a79f9d3d..ebdaa920 100644 --- a/src/api/integrations/typebot/services/typebot.service.ts +++ b/src/api/integrations/typebot/services/typebot.service.ts @@ -572,7 +572,6 @@ export class TypebotService { for (const message of messages) { if (message.type === 'text') { let formattedText = ''; - console.log('message.content', message.content); for (const richText of message.content.richText) { for (const element of richText.children) { From bf88fdbb31633c3039daab53eb020ab42f075b76 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 27 May 2024 12:57:18 -0300 Subject: [PATCH 10/12] fix: adjust send presence --- .../channels/whatsapp.baileys.service.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/api/services/channels/whatsapp.baileys.service.ts b/src/api/services/channels/whatsapp.baileys.service.ts index ebc568dc..2840d9a7 100644 --- a/src/api/services/channels/whatsapp.baileys.service.ts +++ b/src/api/services/channels/whatsapp.baileys.service.ts @@ -1995,18 +1995,37 @@ export class BaileysStartupService extends ChannelStartupService { const sender = isWA.jid; - this.logger.verbose('Sending presence'); - await this.client.presenceSubscribe(sender); - this.logger.verbose('Subscribing to presence'); + if (data?.options?.delay && data?.options?.delay > 20000) { + let remainingDelay = data?.options.delay; + while (remainingDelay > 20000) { + await this.client.presenceSubscribe(sender); - await this.client.sendPresenceUpdate(data.options?.presence ?? 'composing', sender); - this.logger.verbose('Sending presence update: ' + data.options?.presence ?? 'composing'); + await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender); - await delay(data.options.delay); - this.logger.verbose('Set delay: ' + data.options.delay); + await delay(20000); - await this.client.sendPresenceUpdate('paused', sender); - this.logger.verbose('Sending presence update: paused'); + await this.client.sendPresenceUpdate('paused', sender); + + remainingDelay -= 20000; + } + if (remainingDelay > 0) { + await this.client.presenceSubscribe(sender); + + await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender); + + await delay(remainingDelay); + + await this.client.sendPresenceUpdate('paused', sender); + } + } else { + await this.client.presenceSubscribe(sender); + + await this.client.sendPresenceUpdate((data?.options?.presence as WAPresence) ?? 'composing', sender); + + await delay(data?.options?.delay); + + await this.client.sendPresenceUpdate('paused', sender); + } } catch (error) { this.logger.error(error); throw new BadRequestException(error.toString()); From 3191e9b450db75bc62c928561ec8dd3b249122ea Mon Sep 17 00:00:00 2001 From: edisoncm-ti Date: Mon, 27 May 2024 15:14:52 -0300 Subject: [PATCH 11/12] fix: Correction to Postgres connection string in environment files --- Docker/.env.example | 2 +- src/dev-env.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Docker/.env.example b/Docker/.env.example index f55e3d65..4a7aada7 100644 --- a/Docker/.env.example +++ b/Docker/.env.example @@ -125,7 +125,7 @@ CHATWOOT_MESSAGE_DELETE=false # false | true # If you leave this option as true, when sending a message in Chatwoot, the client's last message will be marked as read on WhatsApp. CHATWOOT_MESSAGE_READ=false # false | true # This db connection is used to import messages from whatsapp to chatwoot database -CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgres://user:password@hostname:port/dbname +CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgres://user:password@hostname:port/dbname?sslmode=disable CHATWOOT_IMPORT_DATABASE_PLACEHOLDER_MEDIA_MESSAGE=true # Defines an authentication type for the api diff --git a/src/dev-env.yml b/src/dev-env.yml index adc308e8..29895b52 100644 --- a/src/dev-env.yml +++ b/src/dev-env.yml @@ -172,7 +172,7 @@ CHATWOOT: # This db connection is used to import messages from whatsapp to chatwoot database DATABASE: CONNECTION: - URI: "postgres://user:password@hostname:port/dbname" + URI: "postgres://user:password@hostname:port/dbname?sslmode=disable" PLACEHOLDER_MEDIA_MESSAGE: true # Cache to optimize application performance From ebfc6d4fa5dfef1f0b1566797d84cef26ab189c4 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Mon, 27 May 2024 15:51:06 -0300 Subject: [PATCH 12/12] fix: git actions --- .github/workflows/publish_docker_image.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index f28cfbe7..69a79a18 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -20,7 +20,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ghcr.io/${{ github.repository }} + images: atendai/evolution-api tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} @@ -34,9 +34,8 @@ jobs: - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push id: docker_build