Merge branch 'develop' into main

This commit is contained in:
Davidson Gomes 2025-05-13 06:28:07 -03:00 committed by GitHub
commit bb0b9b94ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 2372 additions and 955 deletions

View File

@ -1,5 +1,7 @@
.git
*Dockerfile*
*docker-compose*
package-lock.json
.env
node_modules
dist

View File

@ -62,6 +62,7 @@ RABBITMQ_EVENTS_MESSAGES_EDITED=false
RABBITMQ_EVENTS_MESSAGES_UPDATE=false
RABBITMQ_EVENTS_MESSAGES_DELETE=false
RABBITMQ_EVENTS_SEND_MESSAGE=false
RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
RABBITMQ_EVENTS_CONTACTS_SET=false
RABBITMQ_EVENTS_CONTACTS_UPSERT=false
RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -108,6 +109,7 @@ PUSHER_EVENTS_MESSAGES_EDITED=true
PUSHER_EVENTS_MESSAGES_UPDATE=true
PUSHER_EVENTS_MESSAGES_DELETE=true
PUSHER_EVENTS_SEND_MESSAGE=true
PUSHER_EVENTS_SEND_MESSAGE_UPDATE=true
PUSHER_EVENTS_CONTACTS_SET=true
PUSHER_EVENTS_CONTACTS_UPSERT=true
PUSHER_EVENTS_CONTACTS_UPDATE=true
@ -149,6 +151,7 @@ WEBHOOK_EVENTS_MESSAGES_EDITED=true
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
WEBHOOK_EVENTS_MESSAGES_DELETE=true
WEBHOOK_EVENTS_SEND_MESSAGE=true
WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
WEBHOOK_EVENTS_CONTACTS_SET=true
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
WEBHOOK_EVENTS_CONTACTS_UPDATE=true

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: type=semver,pattern=v{{version}}
- name: Set up QEMU

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: homolog
- name: Set up QEMU

View File

@ -20,7 +20,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: atendai/evolution-api
images: evoapicloud/evolution-api
tags: latest
- name: Set up QEMU

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
/dist
/node_modules
.cursor*
/Docker/.env
.vscode

View File

@ -1,3 +1,9 @@
# 2.2.4 (hotfix)
### Fixed
* Shell injection vulnerability
# 2.2.3 (2025-02-03 11:52)
### Fixed

View File

@ -2,7 +2,7 @@ version: "3.7"
services:
evolution_v2:
image: atendai/evolution-api:v2.1.2
image: evoapicloud/evolution-api:latest
volumes:
- evolution_instances:/evolution/instances
networks:
@ -34,6 +34,7 @@ services:
- RABBITMQ_EVENTS_MESSAGES_UPDATE=false
- RABBITMQ_EVENTS_MESSAGES_DELETE=false
- RABBITMQ_EVENTS_SEND_MESSAGE=false
- RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE=false
- RABBITMQ_EVENTS_CONTACTS_SET=false
- RABBITMQ_EVENTS_CONTACTS_UPSERT=false
- RABBITMQ_EVENTS_CONTACTS_UPDATE=false
@ -71,6 +72,7 @@ services:
- WEBHOOK_EVENTS_MESSAGES_UPDATE=true
- WEBHOOK_EVENTS_MESSAGES_DELETE=true
- WEBHOOK_EVENTS_SEND_MESSAGE=true
- WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE=true
- WEBHOOK_EVENTS_CONTACTS_SET=true
- WEBHOOK_EVENTS_CONTACTS_UPSERT=true
- WEBHOOK_EVENTS_CONTACTS_UPDATE=true

View File

@ -1,11 +1,11 @@
FROM node:20-alpine AS builder
RUN apk update && \
apk add git ffmpeg wget curl bash openssl
apk add --no-cache git ffmpeg wget curl bash openssl
LABEL version="2.2.3" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@atendai.com"
LABEL contact="contato@evolution-api.com"
WORKDIR /evolution

View File

@ -8,7 +8,7 @@ a. LOGO and copyright information: In the process of using Evolution API's front
b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
Please contact contato@atendai.com to inquire about licensing matters.
Please contact contato@evolution-api.com to inquire about licensing matters.
2. As a contributor, you should agree that:

View File

@ -2,6 +2,7 @@
<div align="center">
[![Docker Image (https://img.shields.io/badge/Docker-Image-blue)](https://hub.docker.com/r/evoapicloud/evolution-api)]
[![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](https://evolution-api.com/whatsapp)
[![Discord Community](https://img.shields.io/badge/Discord-Community-blue)](https://evolution-api.com/discord)
[![Postman Collection](https://img.shields.io/badge/Postman-Collection-orange)](https://evolution-api.com/postman)
@ -87,6 +88,7 @@ https://github.com/sponsors/EvolutionAPI
We are proud to collaborate with the following content creators who have contributed valuable insights and tutorials about Evolution API:
- [Promovaweb](https://www.youtube.com/@promovaweb)
- [Sandeco](https://www.youtube.com/@canalsandeco)
- [Comunidade ZDG](https://www.youtube.com/@ComunidadeZDG)
- [Francis MNO](https://www.youtube.com/@FrancisMNO)
- [Pablo Cabral](https://youtube.com/@pablocabral)
@ -111,7 +113,7 @@ Evolution API is licensed under the Apache License 2.0, with the following addit
2. **Usage Notification Requirement**: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer.
Please contact contato@atendai.com to inquire about licensing matters.
Please contact contato@evolution-api.com to inquire about licensing matters.
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).

View File

@ -1,7 +1,7 @@
services:
api:
container_name: evolution_api
image: atendai/evolution-api:homolog
image: evoapicloud/evolution-api:latest
restart: always
depends_on:
- redis

1779
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,9 @@
"db:deploy:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate deploy --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
"db:studio": "node runWithProvider.js \"npx prisma studio --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:migrate:dev": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate dev --schema ./prisma/DATABASE_PROVIDER-schema.prisma && cp -r ./prisma/migrations/* ./prisma/DATABASE_PROVIDER-migrations\"",
"db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\""
"db:migrate:dev:win": "node runWithProvider.js \"xcopy /E /I prisma\\DATABASE_PROVIDER-migrations prisma\\migrations && npx prisma migrate dev --schema prisma\\DATABASE_PROVIDER-schema.prisma\"",
"postinstall": "npx prisma generate --schema=prisma/postgresql-schema.prisma && npx prisma migrate deploy --schema=prisma/postgresql-schema.prisma"
},
"repository": {
"type": "git",
@ -41,7 +43,7 @@
],
"author": {
"name": "Davidson Gomes",
"email": "contato@atendai.com"
"email": "contato@evolution-api.com"
},
"license": "Apache-2.0",
"bugs": {
@ -75,6 +77,7 @@
"jimp": "^0.16.13",
"json-schema": "^0.4.0",
"jsonschema": "^1.4.1",
"jsonwebtoken": "^9.0.2",
"link-preview-js": "^3.0.13",
"long": "^5.2.3",
"mediainfo.js": "^0.3.4",
@ -82,6 +85,7 @@
"mime-types": "^2.1.35",
"minio": "^8.0.3",
"multer": "^1.4.5-lts.1",
"nats": "^2.29.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",
"openai": "^4.77.3",

View File

@ -0,0 +1,9 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Setting`
ADD COLUMN IF NOT EXISTS `wavoipToken` VARCHAR(100);

View File

@ -0,0 +1,175 @@
/*
Warnings:
- You are about to alter the column `createdAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chat` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Chatwoot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Contact` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Dify` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `DifySetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `EvolutionBotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Flowise` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `FlowiseSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `disconnectionAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Instance` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IntegrationSession` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `IsOnWhatsapp` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Label` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Media` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiBot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiCreds` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `OpenaiSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Proxy` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Pusher` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Rabbitmq` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Session` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Setting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Sqs` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Template` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Typebot` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `TypebotSetting` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Webhook` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `createdAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- You are about to alter the column `updatedAt` on the `Websocket` table. The data in that column could be lost. The data in that column will be cast from `Timestamp(0)` to `Timestamp`.
- A unique constraint covering the columns `[instanceId,remoteJid]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE `Chat` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `Chatwoot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Contact` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `Dify` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `DifySetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Flowise` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `FlowiseSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Instance` MODIFY `disconnectionAt` TIMESTAMP NULL,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `IntegrationSession` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `IsOnWhatsapp` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Label` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Media` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE `OpenaiBot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `OpenaiCreds` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `OpenaiSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Proxy` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Pusher` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Rabbitmq` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Session` MODIFY `createdAt` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE `Setting` ADD COLUMN `wavoipToken` VARCHAR(100) NULL,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Sqs` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Template` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Typebot` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NULL;
-- AlterTable
ALTER TABLE `TypebotSetting` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Webhook` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Websocket` MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX `Chat_instanceId_remoteJid_key` ON `Chat`(`instanceId`, `remoteJid`);

View File

@ -0,0 +1,26 @@
/*
Warnings:
- A unique constraint covering the columns `[remoteJid,instanceId]` on the table `Chat` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
SET @column_exists := (
SELECT COUNT(*)
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'Setting'
AND column_name = 'wavoipToken'
);
SET @sql := IF(@column_exists = 0,
'ALTER TABLE Setting ADD COLUMN wavoipToken VARCHAR(100);',
'SELECT "Column already exists";'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
ALTER TABLE Chat ADD CONSTRAINT unique_remote_instance UNIQUE (remoteJid, instanceId);

View File

@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
# It should be added in your version-control system (e.g., Git)
provider = "mysql"

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy?
Setting Setting?
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Websocket Websocket?
Typebot Typebot[]
@ -99,7 +100,7 @@ model Instance {
Template Template[]
Dify Dify[]
DifySetting DifySetting?
integrationSessions IntegrationSession[]
IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[]
@ -116,18 +117,19 @@ model Session {
}
model Chat {
id String @id @default(cuid())
remoteJid String @db.VarChar(100)
name String? @db.VarChar(100)
labels Json? @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
id String @id @default(cuid())
remoteJid String @db.VarChar(100)
name String? @db.VarChar(100)
labels Json? @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime? @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
unreadMessages Int @default(0)
@@unique([instanceId, remoteJid])
@@index([instanceId])
@@index([remoteJid])
@@unique([instanceId, remoteJid])
}
model Contact {
@ -170,6 +172,7 @@ model Message {
sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId])
}
@ -185,6 +188,7 @@ model MessageUpdate {
messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@@index([instanceId])
@@index([messageId])
}
@ -201,6 +205,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -269,6 +274,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -282,6 +288,16 @@ model Rabbitmq {
instanceId String @unique
}
model Nats {
id String @id @default(cuid())
enabled Boolean @default(false)
events Json @db.Json
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Sqs {
id String @id @default(cuid())
enabled Boolean @default(false)

View File

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Nats" (
"id" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"events" JSONB NOT NULL,
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP NOT NULL,
"instanceId" TEXT NOT NULL,
CONSTRAINT "Nats_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Nats_instanceId_key" ON "Nats"("instanceId");
-- AddForeignKey
ALTER TABLE "Nats" ADD CONSTRAINT "Nats_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -86,6 +86,7 @@ model Instance {
Proxy Proxy?
Setting Setting?
Rabbitmq Rabbitmq?
Nats Nats?
Sqs Sqs?
Websocket Websocket?
Typebot Typebot[]
@ -99,7 +100,7 @@ model Instance {
Template Template[]
Dify Dify[]
DifySetting DifySetting?
integrationSessions IntegrationSession[]
IntegrationSession IntegrationSession[]
EvolutionBot EvolutionBot[]
EvolutionBotSetting EvolutionBotSetting?
Flowise Flowise[]
@ -125,6 +126,7 @@ model Chat {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
unreadMessages Int @default(0)
@@index([instanceId])
@@index([remoteJid])
}
@ -168,6 +170,7 @@ model Message {
sessionId String?
session IntegrationSession? @relation(fields: [sessionId], references: [id])
@@index([instanceId])
}
@ -183,6 +186,7 @@ model MessageUpdate {
messageId String
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String
@@index([instanceId])
@@index([messageId])
}
@ -199,6 +203,7 @@ model Webhook {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -269,6 +274,7 @@ model Setting {
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
@@index([instanceId])
}
@ -282,6 +288,16 @@ model Rabbitmq {
instanceId String @unique
}
model Nats {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean
events Json @db.JsonB
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
}
model Sqs {
id String @id @default(cuid())
enabled Boolean @default(false) @db.Boolean

View File

@ -0,0 +1,15 @@
import { getCatalogDto, getCollectionsDto } from '@api/dto/business.dto';
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BusinessController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) {
return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data);
}
public async fetchCollections({ instanceName }: InstanceDto, data: getCollectionsDto) {
return await this.waMonitor.waInstances[instanceName].fetchCollections(instanceName, data);
}
}

View File

@ -170,6 +170,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},
@ -258,6 +261,9 @@ export class InstanceController {
rabbitmq: {
enabled: instanceData?.rabbitmq?.enabled,
},
nats: {
enabled: instanceData?.nats?.enabled,
},
sqs: {
enabled: instanceData?.sqs?.enabled,
},

View File

@ -18,6 +18,14 @@ import { WAMonitoringService } from '@api/services/monitor.service';
import { BadRequestException } from '@exceptions';
import { isBase64, isURL } from 'class-validator';
function isEmoji(str: string) {
if (str === '') return true;
const emojiRegex =
/^[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{1F000}-\u{1F02F}\u{1F0A0}-\u{1F0FF}\u{1F100}-\u{1F64F}\u{1F680}-\u{1F6FF}]$/u;
return emojiRegex.test(str);
}
export class SendMessageController {
constructor(private readonly waMonitor: WAMonitoringService) {}
@ -81,8 +89,8 @@ export class SendMessageController {
}
public async sendReaction({ instanceName }: InstanceDto, data: SendReactionDto) {
if (!data.reaction.match(/[^()\w\sà-ú"-+]+/)) {
throw new BadRequestException('"reaction" must be an emoji');
if (!isEmoji(data.reaction)) {
throw new BadRequestException('Reaction must be a single emoji or empty string');
}
return await this.waMonitor.waInstances[instanceName].reactionMessage(data);
}

View File

@ -0,0 +1,14 @@
export class NumberDto {
number: string;
}
export class getCatalogDto {
number?: string;
limit?: number;
cursor?: string;
}
export class getCollectionsDto {
number?: string;
limit?: number;
}

View File

@ -44,6 +44,7 @@ export class Metadata {
mentionsEveryOne?: boolean;
mentioned?: string[];
encoding?: boolean;
notConvertSticker?: boolean;
}
export class SendTextDto extends Metadata {

View File

@ -206,6 +206,20 @@ export class BusinessStartupService extends ChannelStartupService {
return content;
}
private messageLocationJson(received: any) {
const message = received.messages[0];
let content: any = {
locationMessage: {
degreesLatitude: message.location.latitude,
degreesLongitude: message.location.longitude,
name: message.location?.name,
address: message.location?.address,
},
};
message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content;
return content;
}
private messageContactsJson(received: any) {
const message = received.messages[0];
let content: any = {};
@ -283,6 +297,9 @@ export class BusinessStartupService extends ChannelStartupService {
case 'template':
messageType = 'conversation';
break;
case 'location':
messageType = 'locationMessage';
break;
default:
messageType = 'conversation';
break;
@ -438,6 +455,17 @@ export class BusinessStartupService extends ChannelStartupService {
source: 'unknown',
instanceId: this.instanceId,
};
} else if (received?.messages[0].location) {
messageRaw = {
key,
pushName,
message: this.messageLocationJson(received),
contextInfo: this.messageLocationJson(received)?.contextInfo,
messageType: this.renderMessageType(received.messages[0].type),
messageTimestamp: parseInt(received.messages[0].timestamp) as number,
source: 'unknown',
instanceId: this.instanceId,
};
} else {
messageRaw = {
key,
@ -724,7 +752,6 @@ export class BusinessStartupService extends ChannelStartupService {
try {
let quoted: any;
let webhookUrl: any;
const linkPreview = options?.linkPreview != false ? undefined : false;
if (options?.quoted) {
const m = options?.quoted;
@ -792,7 +819,7 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
text: {
body: message['conversation'],
preview_url: linkPreview,
preview_url: Boolean(options?.linkPreview),
},
};
quoted ? (content.context = { message_id: quoted.id }) : content;
@ -800,6 +827,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
const isVideo = message['mimetype']?.startsWith('video/');
content = {
messaging_product: 'whatsapp',
@ -808,8 +836,8 @@ export class BusinessStartupService extends ChannelStartupService {
to: number.replace(/\D/g, ''),
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
preview_url: Boolean(options?.linkPreview),
...(message['fileName'] && !isImage && !isVideo && { filename: message['fileName'] }),
caption: message['caption'],
},
};
@ -977,8 +1005,10 @@ export class BusinessStartupService extends ChannelStartupService {
private async getIdMedia(mediaMessage: any) {
const formData = new FormData();
const media = mediaMessage.media || mediaMessage.audio;
if (!media) throw new Error('Media or audio not found');
const fileStream = createReadStream(mediaMessage.media);
const fileStream = createReadStream(media);
formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype });
formData.append('typeFile', mediaMessage.mimetype);
@ -1079,7 +1109,7 @@ export class BusinessStartupService extends ChannelStartupService {
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
audio,
};
if (isURL(audio)) {
@ -1101,15 +1131,7 @@ export class BusinessStartupService extends ChannelStartupService {
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
const mediaData: SendAudioDto = { ...data };
if (file?.buffer) {
mediaData.audio = file.buffer.toString('base64');
} else if (isURL(mediaData.audio)) {
// DO NOTHING
// mediaData.audio = mediaData.audio;
} else {
console.error('El archivo no tiene buffer o file es undefined');
throw new Error('File or buffer is undefined');
}
if (file) mediaData.audio = file.buffer.toString('base64');
const message = await this.processAudio(mediaData.audio, data.number);

View File

@ -1,3 +1,4 @@
import { getCollectionsDto } from '@api/dto/business.dto';
import { OfferCallDto } from '@api/dto/call.dto';
import {
ArchiveChatDto,
@ -91,6 +92,7 @@ import makeWASocket, {
BufferedEventData,
BufferJSON,
CacheStore,
CatalogCollection,
Chat,
ConnectionState,
Contact,
@ -100,6 +102,7 @@ import makeWASocket, {
fetchLatestBaileysVersion,
generateWAMessageFromContent,
getAggregateVotesInPollMessage,
GetCatalogOptions,
getContentType,
getDevice,
GroupMetadata,
@ -113,6 +116,7 @@ import makeWASocket, {
MiscMessageGenerationOptions,
ParticipantAction,
prepareWAMessageMedia,
Product,
proto,
UserFacingSocketConfig,
WABrowserDescription,
@ -226,7 +230,10 @@ export class BaileysStartupService extends ChannelStartupService {
private authStateProvider: AuthStateProvider;
private readonly msgRetryCounterCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache();
private readonly userDevicesCache: CacheStore = new NodeCache({
stdTTL: 300000,
useClones: false
});
private endSession = false;
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
@ -1128,38 +1135,73 @@ export class BaileysStartupService extends ChannelStartupService {
}
}
if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) {
const editedMessage =
received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage;
if (editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
const editedMessage =
received?.message?.protocolMessage || received?.message?.editedMessage?.message?.protocolMessage;
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
if (received.message?.protocolMessage?.editedMessage && editedMessage) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'messages.edit',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage);
const oldMessage = await this.getMessage(editedMessage.key, true);
if ((oldMessage as any)?.id) {
const editedMessageTimestamp = Long.isLong(editedMessage?.timestampMs)
? editedMessage.timestampMs?.toNumber()
: (editedMessage.timestampMs as number);
await this.prismaRepository.message.update({
where: { id: (oldMessage as any).id },
data: {
message: editedMessage.editedMessage as any,
messageTimestamp: editedMessageTimestamp,
status: 'EDITED',
},
});
await this.prismaRepository.messageUpdate.create({
data: {
fromMe: editedMessage.key.fromMe,
keyId: editedMessage.key.id,
remoteJid: editedMessage.key.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
messageId: (oldMessage as any).id,
},
});
}
}
if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
// if (received.messageStubParameters && received.messageStubParameters[0] === 'Message absent from node') {
// this.logger.info(`Recovering message lost messageId: ${received.key.id}`);
await this.baileysCache.set(received.key.id, {
message: received,
retry: 0,
});
// await this.baileysCache.set(received.key.id, {
// message: received,
// retry: 0,
// });
// continue;
// }
// const retryCache = (await this.baileysCache.get(received.key.id)) || null;
// if (retryCache) {
// this.logger.info('Recovered message lost');
// await this.baileysCache.delete(received.key.id);
// }
// Cache to avoid duplicate messages
const messageKey = `${this.instance.id}_${received.key.id}`;
const cached = await this.baileysCache.get(messageKey);
if (cached && !editedMessage) {
this.logger.info(`Message duplicated ignored: ${received.key.id}`);
continue;
}
const retryCache = (await this.baileysCache.get(received.key.id)) || null;
if (retryCache) {
this.logger.info('Recovered message lost');
await this.baileysCache.delete(received.key.id);
}
await this.baileysCache.set(messageKey, true, 30 * 60);
if (
(type !== 'notify' && type !== 'append') ||
@ -1186,7 +1228,9 @@ export class BaileysStartupService extends ChannelStartupService {
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
received.pushName.trim().length > 0 &&
!received.key.fromMe &&
!received.key.remoteJid.includes('@g.us')
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
@ -1292,7 +1336,12 @@ export class BaileysStartupService extends ChannelStartupService {
const { buffer, mediaType, fileName, size } = media;
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
const fullName = join(
`${this.instance.id}`,
received.key.remoteJid,
mediaType,
`${Date.now()}_${fileName}`,
);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
'Content-Type': mimetype,
});
@ -1422,6 +1471,17 @@ export class BaileysStartupService extends ChannelStartupService {
continue;
}
const updateKey = `${this.instance.id}_${key.id}_${update.status}`;
const cached = await this.baileysCache.get(updateKey);
if (cached) {
this.logger.info(`Message duplicated ignored: ${key.id}`);
continue;
}
await this.baileysCache.set(updateKey, true, 30 * 60);
if (status[update.status] === 'READ' && key.fromMe) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
@ -1529,7 +1589,6 @@ export class BaileysStartupService extends ChannelStartupService {
const chatToInsert = {
remoteJid: message.remoteJid,
instanceId: this.instanceId,
name: message.pushName || '',
unreadMessages: 0,
};
@ -2654,9 +2713,6 @@ export class BaileysStartupService extends ChannelStartupService {
prepareMedia[mediaType].fileName = mediaMessage.fileName;
if (mediaMessage.mediatype === 'video') {
prepareMedia[mediaType].jpegThumbnail = Uint8Array.from(
readFileSync(join(process.cwd(), 'public', 'images', 'video-cover.png')),
);
prepareMedia[mediaType].gifPlayback = false;
}
@ -2703,21 +2759,43 @@ export class BaileysStartupService extends ChannelStartupService {
imageBuffer = Buffer.from(response.data, 'binary');
}
const webpBuffer = await sharp(imageBuffer).webp().toBuffer();
const isAnimated = this.isAnimated(image, imageBuffer);
return webpBuffer;
if (isAnimated) {
return await sharp(imageBuffer, { animated: true }).webp({ quality: 80 }).toBuffer();
} else {
return await sharp(imageBuffer).webp().toBuffer();
}
} catch (error) {
console.error('Erro ao converter a imagem para WebP:', error);
throw error;
}
}
private isAnimatedWebp(buffer: Buffer): boolean {
if (buffer.length < 12) return false;
return buffer.indexOf(Buffer.from('ANIM')) !== -1;
}
private isAnimated(image: string, buffer: Buffer): boolean {
const lowerCaseImage = image.toLowerCase();
if (lowerCaseImage.includes('.gif')) return true;
if (lowerCaseImage.includes('.webp')) return this.isAnimatedWebp(buffer);
return false;
}
public async mediaSticker(data: SendStickerDto, file?: any) {
const mediaData: SendStickerDto = { ...data };
if (file) mediaData.sticker = file.buffer.toString('base64');
const convert = await this.convertToWebP(data.sticker);
const convert = data?.notConvertSticker
? Buffer.from(data.sticker, 'base64')
: await this.convertToWebP(data.sticker);
const gifPlayback = data.sticker.includes('.gif');
const result = await this.sendMessageWithTyping(
data.number,
@ -2923,7 +3001,29 @@ export class BaileysStartupService extends ChannelStartupService {
.noVideo()
.audioCodec('libopus')
.addOutputOptions('-avoid_negative_ts make_zero')
.audioBitrate('128k')
.audioFrequency(48000)
.audioChannels(1)
.outputOptions([
'-write_xing',
'0',
'-compression_level',
'10',
'-application',
'voip',
'-fflags',
'+bitexact',
'-flags',
'+bitexact',
'-id3v2_version',
'0',
'-map_metadata',
'-1',
'-map_chapters',
'-1',
'-write_bext',
'0',
])
.pipe(outputAudioStream, { end: true })
.on('error', function (error) {
console.log('error', error);
@ -3573,6 +3673,18 @@ export class BaileysStartupService extends ChannelStartupService {
status: 'DELETED',
},
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: response.key.remoteJid,
fromMe: response.key.fromMe,
participant: response.key?.remoteJid,
status: 'DELETED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({
data: messageUpdate,
});
} else {
await this.prismaRepository.message.deleteMany({
where: {
@ -3899,13 +4011,84 @@ export class BaileysStartupService extends ChannelStartupService {
}
try {
return await this.client.sendMessage(jid, {
const oldMessage: any = await this.getMessage(data.key, true);
if (!oldMessage) throw new NotFoundException('Message not found');
if (oldMessage?.key?.remoteJid !== jid) {
throw new BadRequestException('RemoteJid does not match');
}
if (oldMessage?.messageTimestamp > Date.now() + 900000) {
// 15 minutes in milliseconds
throw new BadRequestException('Message is older than 15 minutes');
}
const messageSent = await this.client.sendMessage(jid, {
...(options as any),
edit: data.key,
});
if (messageSent) {
const editedMessage =
messageSent?.message?.protocolMessage || messageSent?.message?.editedMessage?.message?.protocolMessage;
if (editedMessage) {
this.sendDataWebhook(Events.SEND_MESSAGE_UPDATE, editedMessage);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled)
this.chatwootService.eventWhatsapp(
'send.message.update',
{ instanceName: this.instance.name, instanceId: this.instance.id },
editedMessage,
);
const messageId = messageSent.message?.protocolMessage?.key?.id;
if (messageId) {
let message = await this.prismaRepository.message.findFirst({
where: {
key: {
path: ['id'],
equals: messageId,
},
},
});
if (!message) throw new NotFoundException('Message not found');
if (!(message.key.valueOf() as any).fromMe) {
new BadRequestException('You cannot edit others messages');
}
if ((message.key.valueOf() as any)?.deleted) {
new BadRequestException('You cannot edit deleted messages');
}
if (oldMessage.messageType === 'conversation' || oldMessage.messageType === 'extendedTextMessage') {
oldMessage.message.conversation = data.text;
} else {
oldMessage.message[oldMessage.messageType].caption = data.text;
}
message = await this.prismaRepository.message.update({
where: { id: message.id },
data: {
message: oldMessage.message,
status: 'EDITED',
messageTimestamp: Math.floor(Date.now() / 1000), // Convert to int32 by dividing by 1000 to get seconds
},
});
const messageUpdate: any = {
messageId: message.id,
keyId: messageId,
remoteJid: messageSent.key.remoteJid,
fromMe: messageSent.key.fromMe,
participant: messageSent.key?.remoteJid,
status: 'EDITED',
instanceId: this.instanceId,
};
await this.prismaRepository.messageUpdate.create({
data: messageUpdate,
});
}
}
}
return messageSent;
} catch (error) {
this.logger.error(error);
throw new BadRequestException(error.toString());
throw error;
}
}
@ -4534,4 +4717,137 @@ export class BaileysStartupService extends ChannelStartupService {
return response;
}
//Business Controller
public async fetchCatalog(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit || 10;
const cursor = null;
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
let catalog = await this.getCatalog({ jid: info?.jid, limit, cursor });
let nextPageCursor = catalog.nextPageCursor;
let nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
let pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
let fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
let productsCatalog = catalog.products || [];
let countLoops = 0;
while (fetcherHasMore && countLoops < 4) {
catalog = await this.getCatalog({ jid: info?.jid, limit, cursor: nextPageCursor });
nextPageCursor = catalog.nextPageCursor;
nextPageCursorJson = nextPageCursor ? JSON.parse(atob(nextPageCursor)) : null;
pagination = nextPageCursorJson?.pagination_cursor
? JSON.parse(atob(nextPageCursorJson.pagination_cursor))
: null;
fetcherHasMore = pagination?.fetcher_has_more === true ? true : false;
productsCatalog = [...productsCatalog, ...catalog.products];
countLoops++;
}
return {
wuid: info?.jid || jid,
numberExists: info?.exists,
isBusiness: business.isBusiness,
catalogLength: productsCatalog.length,
catalog: productsCatalog,
};
} catch (error) {
console.log(error);
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCatalog({
jid,
limit,
cursor,
}: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor });
if (!catalog) {
return {
products: undefined,
nextPageCursor: undefined,
};
}
return catalog;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
public async fetchCollections(instanceName: string, data: getCollectionsDto) {
const jid = data.number ? createJid(data.number) : this.client?.user?.id;
const limit = data.limit <= 20 ? data.limit : 20; //(tem esse limite, não sei porque)
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
if (!onWhatsapp.exists) {
throw new BadRequestException(onWhatsapp);
}
try {
const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
const business = await this.fetchBusinessProfile(info?.jid);
const collections = await this.getCollections(info?.jid, limit);
return {
wuid: info?.jid || jid,
name: info?.name,
numberExists: info?.exists,
isBusiness: business.isBusiness,
collectionsLength: collections?.length,
collections: collections,
};
} catch (error) {
return {
wuid: jid,
name: null,
isBusiness: false,
};
}
}
public async getCollections(jid?: string | undefined, limit?: number): Promise<CatalogCollection[]> {
try {
jid = jid ? createJid(jid) : this.instance.wuid;
const result = await this.client.getCollections(jid, limit);
if (!result) {
return [
{
id: undefined,
name: undefined,
products: [],
status: undefined,
},
];
}
return result.collections;
} catch (error) {
throw new InternalServerErrorException('Error getCatalog', error.toString());
}
}
}

View File

@ -698,34 +698,33 @@ export class ChatwootService {
return null;
}
if (contactConversations.payload.length) {
let conversation: any;
let inboxConversation = contactConversations.payload.find(
(conversation) => conversation.inbox_id == filterInbox.id,
);
if (inboxConversation) {
if (this.provider.reopenConversation) {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
if (this.provider.conversationPending && conversation.status !== 'open') {
if (conversation) {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: conversation.id,
data: {
status: 'pending',
},
});
}
if (this.provider.conversationPending && inboxConversation.status !== 'open') {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
conversationId: inboxConversation.id,
data: {
status: 'pending',
},
});
}
} else {
conversation = contactConversations.payload.find(
inboxConversation = contactConversations.payload.find(
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
);
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
}
if (conversation) {
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
this.cache.set(cacheKey, conversation.id);
return conversation.id;
if (inboxConversation) {
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
this.cache.set(cacheKey, inboxConversation.id);
return inboxConversation.id;
}
}
@ -1106,7 +1105,7 @@ export class ChatwootService {
sendTelemetry('/message/sendWhatsAppAudio');
const messageSent = await waInstance?.audioWhatsapp(data, true);
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
return messageSent;
}
@ -1653,7 +1652,7 @@ export class ChatwootService {
stickerMessage: undefined,
documentMessage: msg.documentMessage?.caption,
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
audioMessage: msg.audioMessage?.caption,
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
contactMessage: msg.contactMessage?.vcard,
contactsArrayMessage: msg.contactsArrayMessage,
locationMessage: msg.locationMessage,
@ -1898,7 +1897,7 @@ export class ChatwootService {
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
: originalMessage;
if (bodyMessage && bodyMessage.includes('Por favor, classifique esta conversa, http')) {
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
return;
}
@ -2199,7 +2198,7 @@ export class ChatwootService {
}
}
if (event === 'messages.edit') {
if (event === 'messages.edit' || event === 'send.message.update') {
const editedText = `${
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;

View File

@ -1018,10 +1018,6 @@ export class TypebotController extends ChatbotController implements ChatbotContr
return;
}
if (session && !session.awaitUser) {
return;
}
if (debounceTime && debounceTime > 0) {
this.processDebounce(this.userMessageDebounce, content, remoteJid, debounceTime, async (debouncedContent) => {
await this.typebotService.processTypebot(

View File

@ -741,6 +741,10 @@ export class TypebotService {
}
}
if (session && !session.awaitUser) {
return;
}
if (session && session.status !== 'opened') {
return;
}

View File

@ -132,6 +132,7 @@ export class EventController {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@ -151,5 +152,8 @@ export class EventController {
'TYPEBOT_CHANGE_STATUS',
'REMOVE_INSTANCE',
'LOGOUT_INSTANCE',
'INSTANCE_CREATE',
'INSTANCE_DELETE',
'STATUS_INSTANCE',
];
}

View File

@ -26,6 +26,11 @@ export class EventDto {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;
@ -63,6 +68,11 @@ export function EventInstanceMixin<TBase extends Constructor>(Base: TBase) {
events?: string[];
};
nats?: {
enabled?: boolean;
events?: string[];
};
pusher?: {
enabled?: boolean;
appId?: string;

View File

@ -1,3 +1,4 @@
import { NatsController } from '@api/integrations/event/nats/nats.controller';
import { PusherController } from '@api/integrations/event/pusher/pusher.controller';
import { RabbitmqController } from '@api/integrations/event/rabbitmq/rabbitmq.controller';
import { SqsController } from '@api/integrations/event/sqs/sqs.controller';
@ -13,6 +14,7 @@ export class EventManager {
private websocketController: WebsocketController;
private webhookController: WebhookController;
private rabbitmqController: RabbitmqController;
private natsController: NatsController;
private sqsController: SqsController;
private pusherController: PusherController;
@ -23,6 +25,7 @@ export class EventManager {
this.websocket = new WebsocketController(prismaRepository, waMonitor);
this.webhook = new WebhookController(prismaRepository, waMonitor);
this.rabbitmq = new RabbitmqController(prismaRepository, waMonitor);
this.nats = new NatsController(prismaRepository, waMonitor);
this.sqs = new SqsController(prismaRepository, waMonitor);
this.pusher = new PusherController(prismaRepository, waMonitor);
}
@ -67,6 +70,14 @@ export class EventManager {
return this.rabbitmqController;
}
public set nats(nats: NatsController) {
this.natsController = nats;
}
public get nats() {
return this.natsController;
}
public set sqs(sqs: SqsController) {
this.sqsController = sqs;
}
@ -85,6 +96,7 @@ export class EventManager {
public init(httpServer: Server): void {
this.websocket.init(httpServer);
this.rabbitmq.init();
this.nats.init();
this.sqs.init();
this.pusher.init();
}
@ -103,6 +115,7 @@ export class EventManager {
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);
await this.nats.emit(eventData);
await this.sqs.emit(eventData);
await this.webhook.emit(eventData);
await this.pusher.emit(eventData);
@ -125,6 +138,14 @@ export class EventManager {
},
});
if (data.nats)
await this.nats.set(instanceName, {
nats: {
enabled: true,
events: data.nats?.events,
},
});
if (data.sqs)
await this.sqs.set(instanceName, {
sqs: {

View File

@ -1,3 +1,4 @@
import { NatsRouter } from '@api/integrations/event/nats/nats.router';
import { PusherRouter } from '@api/integrations/event/pusher/pusher.router';
import { RabbitmqRouter } from '@api/integrations/event/rabbitmq/rabbitmq.router';
import { SqsRouter } from '@api/integrations/event/sqs/sqs.router';
@ -14,6 +15,7 @@ export class EventRouter {
this.router.use('/webhook', new WebhookRouter(configService, ...guards).router);
this.router.use('/websocket', new WebsocketRouter(...guards).router);
this.router.use('/rabbitmq', new RabbitmqRouter(...guards).router);
this.router.use('/nats', new NatsRouter(...guards).router);
this.router.use('/pusher', new PusherRouter(...guards).router);
this.router.use('/sqs', new SqsRouter(...guards).router);
}

View File

@ -16,6 +16,9 @@ export const eventSchema: JSONSchema7 = {
rabbitmq: {
$ref: '#/$defs/event',
},
nats: {
$ref: '#/$defs/event',
},
sqs: {
$ref: '#/$defs/event',
},

View File

@ -0,0 +1,161 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Log, Nats } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { connect, NatsConnection, StringCodec } from 'nats';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
export class NatsController extends EventController implements EventControllerInterface {
public natsClient: NatsConnection | null = null;
private readonly logger = new Logger('NatsController');
private readonly sc = StringCodec();
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, configService.get<Nats>('NATS')?.ENABLED, 'nats');
}
public async init(): Promise<void> {
if (!this.status) {
return;
}
try {
const uri = configService.get<Nats>('NATS').URI;
this.natsClient = await connect({ servers: uri });
this.logger.info('NATS initialized');
if (configService.get<Nats>('NATS')?.GLOBAL_ENABLED) {
await this.initGlobalSubscriptions();
}
} catch (error) {
this.logger.error('Failed to connect to NATS:');
this.logger.error(error);
throw error;
}
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('nats')) {
return;
}
if (!this.status || !this.natsClient) {
return;
}
const instanceNats = await this.get(instanceName);
const natsLocal = instanceNats?.events;
const natsGlobal = configService.get<Nats>('NATS').GLOBAL_ENABLED;
const natsEvents = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const logEnabled = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const message = {
event,
instance: instanceName,
data,
server_url: serverUrl,
date_time: dateTime,
sender,
apikey: apiKey,
};
// Instância específica
if (instanceNats?.enabled) {
if (Array.isArray(natsLocal) && natsLocal.includes(we)) {
const subject = `${instanceName}.${event.toLowerCase()}`;
try {
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (instance): ${error}`);
}
}
}
// Global
if (natsGlobal && natsEvents[we]) {
try {
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
this.natsClient.publish(subject, this.sc.encode(JSON.stringify(message)));
if (logEnabled) {
const logData = {
local: `${origin}.sendData-NATS-Global`,
...message,
};
this.logger.log(logData);
}
} catch (error) {
this.logger.error(`Failed to publish to NATS (global): ${error}`);
}
}
}
private async initGlobalSubscriptions(): Promise<void> {
this.logger.info('Initializing global subscriptions');
const events = configService.get<Nats>('NATS').EVENTS;
const prefixKey = configService.get<Nats>('NATS').PREFIX_KEY;
if (!events) {
this.logger.warn('No events to initialize on NATS');
return;
}
const eventKeys = Object.keys(events);
for (const event of eventKeys) {
if (events[event] === false) continue;
const subject = prefixKey ? `${prefixKey}.${event.toLowerCase()}` : event.toLowerCase();
// Criar uma subscription para cada evento
try {
const subscription = this.natsClient.subscribe(subject);
this.logger.info(`Subscribed to: ${subject}`);
// Processar mensagens (exemplo básico)
(async () => {
for await (const msg of subscription) {
try {
const data = JSON.parse(this.sc.decode(msg.data));
// Aqui você pode adicionar a lógica de processamento
this.logger.debug(`Received message on ${subject}:`);
this.logger.debug(data);
} catch (error) {
this.logger.error(`Error processing message on ${subject}:`);
this.logger.error(error);
}
}
})();
} catch (error) {
this.logger.error(`Failed to subscribe to ${subject}:`);
this.logger.error(error);
}
}
}
}

View File

@ -0,0 +1,36 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { EventDto } from '@api/integrations/event/event.dto';
import { HttpStatus } from '@api/routes/index.router';
import { eventManager } from '@api/server.module';
import { eventSchema, instanceSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class NatsRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {
const response = await this.dataValidate<EventDto>({
request: req,
schema: eventSchema,
ClassRef: EventDto,
execute: (instance, data) => eventManager.nats.set(instance.instanceName, data),
});
res.status(HttpStatus.CREATED).json(response);
})
.get(this.routerPath('find'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => eventManager.nats.get(instance.instanceName),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -1,10 +1,11 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { SQS } from '@aws-sdk/client-sqs';
import { CreateQueueCommand, DeleteQueueCommand, ListQueuesCommand, SQS } from '@aws-sdk/client-sqs';
import { configService, Log, Sqs } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
import { EventDto } from '../event.dto';
export class SqsController extends EventController implements EventControllerInterface {
private sqs: SQS;
@ -45,6 +46,39 @@ export class SqsController extends EventController implements EventControllerInt
return this.sqs;
}
override async set(instanceName: string, data: EventDto): Promise<any> {
if (!this.status) {
return;
}
if (!data[this.name]?.enabled) {
data[this.name].events = [];
} else {
if (0 === data[this.name].events.length) {
data[this.name].events = EventController.events;
}
}
await this.saveQueues(instanceName, data[this.name].events, data[this.name]?.enabled);
const payload: any = {
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
update: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
},
create: {
enabled: data[this.name]?.enabled,
events: data[this.name].events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
};
console.log('*** payload: ', payload);
return this.prisma[this.name].upsert(payload);
}
public async emit({
instanceName,
origin,
@ -121,70 +155,92 @@ export class SqsController extends EventController implements EventControllerInt
}
}
public async initQueues(instanceName: string, events: string[]) {
if (!events || !events.length) return;
private async saveQueues(instanceName: string, events: string[], enable: boolean) {
if (enable) {
const eventsFinded = await this.listQueuesByInstance(instanceName);
console.log('eventsFinded', eventsFinded);
const queues = events.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
for (const event of events) {
const normalizedEvent = event.toLowerCase();
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
if (eventsFinded.includes(normalizedEvent)) {
this.logger.info(`A queue para o evento "${normalizedEvent}" já existe. Ignorando criação.`);
continue;
}
this.sqs.createQueue(
{
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
},
(err, data) => {
if (err) {
this.logger.error(`Error creating queue ${queueName}: ${err.message}`);
} else {
this.logger.info(`Queue ${queueName} created: ${data.QueueUrl}`);
}
},
);
});
const queueName = `${instanceName}_${normalizedEvent}.fifo`;
try {
const createCommand = new CreateQueueCommand({
QueueName: queueName,
Attributes: {
FifoQueue: 'true',
},
});
const data = await this.sqs.send(createCommand);
this.logger.info(`Queue ${queueName} criada: ${data.QueueUrl}`);
} catch (err: any) {
this.logger.error(`Erro ao criar queue ${queueName}: ${err.message}`);
}
}
}
}
public async removeQueues(instanceName: string, events: any) {
const eventsArray = Array.isArray(events) ? events.map((event) => String(event)) : [];
if (!events || !eventsArray.length) return;
private async listQueuesByInstance(instanceName: string) {
let existingQueues: string[] = [];
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
if (listData.QueueUrls && listData.QueueUrls.length > 0) {
// Extrai o nome da fila a partir da URL
existingQueues = listData.QueueUrls.map((queueUrl) => {
const parts = queueUrl.split('/');
return parts[parts.length - 1];
});
}
} catch (error: any) {
this.logger.error(`Erro ao listar filas para a instância ${instanceName}: ${error.message}`);
return;
}
const queues = eventsArray.map((event) => {
return `${event.replace(/_/g, '_').toLowerCase()}`;
});
// Mapeia os eventos já existentes nas filas: remove o prefixo e o sufixo ".fifo"
return existingQueues
.map((queueName) => {
// Espera-se que o nome seja `${instanceName}_${event}.fifo`
if (queueName.startsWith(`${instanceName}_`) && queueName.endsWith('.fifo')) {
return queueName.substring(instanceName.length + 1, queueName.length - 5).toLowerCase();
}
return '';
})
.filter((event) => event !== '');
}
queues.forEach((event) => {
const queueName = `${instanceName}_${event}.fifo`;
// Para uma futura feature de exclusão forçada das queues
private async removeQueuesByInstance(instanceName: string) {
try {
const listCommand = new ListQueuesCommand({
QueueNamePrefix: `${instanceName}_`,
});
const listData = await this.sqs.send(listCommand);
this.sqs.getQueueUrl(
{
QueueName: queueName,
},
(err, data) => {
if (err) {
this.logger.error(`Error getting queue URL for ${queueName}: ${err.message}`);
} else {
const queueUrl = data.QueueUrl;
if (!listData.QueueUrls || listData.QueueUrls.length === 0) {
this.logger.info(`No queues found for instance ${instanceName}`);
return;
}
this.sqs.deleteQueue(
{
QueueUrl: queueUrl,
},
(deleteErr) => {
if (deleteErr) {
this.logger.error(`Error deleting queue ${queueName}: ${deleteErr.message}`);
} else {
this.logger.info(`Queue ${queueName} deleted`);
}
},
);
}
},
);
});
for (const queueUrl of listData.QueueUrls) {
try {
const deleteCommand = new DeleteQueueCommand({ QueueUrl: queueUrl });
await this.sqs.send(deleteCommand);
this.logger.info(`Queue ${queueUrl} deleted`);
} catch (err: any) {
this.logger.error(`Error deleting queue ${queueUrl}: ${err.message}`);
}
}
} catch (err: any) {
this.logger.error(`Error listing queues for instance ${instanceName}: ${err.message}`);
}
}
}

View File

@ -6,7 +6,7 @@ import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import * as jwt from 'jsonwebtoken';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
@ -18,7 +18,7 @@ export class WebhookController extends EventController implements EventControlle
}
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
if (!isURL(data.webhook.url, { require_tld: false })) {
if (!/^(https?:\/\/)/.test(data.webhook.url)) {
throw new BadRequestException('Invalid "url" property');
}
@ -74,10 +74,20 @@ export class WebhookController extends EventController implements EventControlle
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
const webhookHeaders = { ...((instance?.headers as Record<string, string>) || {}) };
if (webhookHeaders && 'jwt_key' in webhookHeaders) {
const jwtKey = webhookHeaders['jwt_key'];
const jwtToken = this.generateJwtToken(jwtKey);
webhookHeaders['Authorization'] = `Bearer ${jwtToken}`;
delete webhookHeaders['jwt_key'];
}
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const regex = /^(https?:\/\/)/;
const webhookData = {
event,
@ -111,7 +121,7 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
if (instance?.enabled && regex.test(instance.url)) {
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
@ -155,7 +165,7 @@ export class WebhookController extends EventController implements EventControlle
}
try {
if (isURL(globalURL)) {
if (regex.test(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await this.retryWebhookRequest(
@ -230,4 +240,24 @@ export class WebhookController extends EventController implements EventControlle
}
}
}
private generateJwtToken(authToken: string): string {
try {
const payload = {
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration
app: 'evolution',
action: 'webhook',
};
const token = jwt.sign(payload, authToken, { algorithm: 'HS256' });
return token;
} catch (error) {
this.logger.error({
local: 'WebhookController.generateJwtToken',
message: `JWT generation failed: ${error?.message}`,
});
throw error;
}
}
}

View File

@ -1,6 +1,6 @@
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { configService, Cors, Log, Websocket } from '@config/env.config';
import { Auth, configService, Cors, Log, Websocket } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { Server } from 'http';
import { Server as SocketIO } from 'socket.io';
@ -24,8 +24,40 @@ export class WebsocketController extends EventController implements EventControl
}
this.socket = new SocketIO(httpServer, {
cors: {
origin: this.cors,
cors: { origin: this.cors },
allowRequest: async (req, callback) => {
try {
const url = new URL(req.url || '', 'http://localhost');
const params = new URLSearchParams(url.search);
// Permite conexões internas do Socket.IO (EIO=4 é o Engine.IO v4)
if (params.has('EIO')) {
return callback(null, true);
}
const apiKey = params.get('apikey') || (req.headers.apikey as string);
if (!apiKey) {
this.logger.error('Connection rejected: apiKey not provided');
return callback('apiKey is required', false);
}
const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } });
if (!instance) {
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
if (apiKey !== globalToken) {
this.logger.error('Connection rejected: invalid global token');
return callback('Invalid global token', false);
}
}
callback(null, true);
} catch (error) {
this.logger.error('Authentication error:');
this.logger.error(error);
callback('Authentication error', false);
}
},
});
@ -101,10 +133,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-WebsocketGlobal`,
...message,
});
this.logger.log({ local: `${origin}.sendData-WebsocketGlobal`, ...message });
}
}
@ -119,10 +148,7 @@ export class WebsocketController extends EventController implements EventControl
this.socket.of(`/${instanceName}`).emit(event, message);
if (logEnabled) {
this.logger.log({
local: `${origin}.sendData-Websocket`,
...message,
});
this.logger.log({ local: `${origin}.sendData-Websocket`, ...message });
}
}
} catch (err) {

View File

@ -63,9 +63,9 @@ const createBucket = async () => {
if (!exists) {
await minioClient.makeBucket(bucketName);
}
await setBucketPolicy();
if (!BUCKET.SKIP_POLICY) {
await setBucketPolicy();
}
logger.info(`S3 Bucket ${bucketName} - ON`);
return true;
} catch (error) {

View File

@ -1,7 +1,7 @@
import { Auth, ConfigService, ProviderSession } from '@config/env.config';
import { Logger } from '@config/logger.config';
import axios from 'axios';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
type ResponseSuccess = { status: number; data?: any };
type ResponseProvider = Promise<[ResponseSuccess?, Error?]>;
@ -36,7 +36,7 @@ export class ProviderFiles {
} catch (error) {
this.logger.error(['Failed to connect to the file server', error?.message, error?.stack]);
const pid = process.pid;
execSync(`kill -9 ${pid}`);
execFileSync('kill', ['-9', `${pid}`]);
}
}
}

View File

@ -0,0 +1,37 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { NumberDto } from '@api/dto/chat.dto';
import { businessController } from '@api/server.module';
import { catalogSchema, collectionsSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class BusinessRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('getCatalog'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: catalogSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCatalog(instance, data),
});
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getCollections'), ...guards, async (req, res) => {
const response = await this.dataValidate<NumberDto>({
request: req,
schema: collectionsSchema,
ClassRef: NumberDto,
execute: (instance, data) => businessController.fetchCollections(instance, data),
});
return res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@ -207,7 +207,6 @@ export class ChatRouter extends RouterBroker {
return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileName'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileNameDto>({
request: req,

View File

@ -11,6 +11,7 @@ import fs from 'fs';
import mimeTypes from 'mime-types';
import path from 'path';
import { BusinessRouter } from './business.router';
import { CallRouter } from './call.router';
import { ChatRouter } from './chat.router';
import { GroupRouter } from './group.router';
@ -82,6 +83,7 @@ router
.use('/message', new MessageRouter(...guards).router)
.use('/call', new CallRouter(...guards).router)
.use('/chat', new ChatRouter(...guards).router)
.use('/business', new BusinessRouter(...guards).router)
.use('/group', new GroupRouter(...guards).router)
.use('/template', new TemplateRouter(configService, ...guards).router)
.use('/settings', new SettingsRouter(...guards).router)

View File

@ -15,7 +15,6 @@ export class InstanceRouter extends RouterBroker {
super();
this.router
.post('/create', ...guards, async (req, res) => {
console.log('create instance', req.body);
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,

View File

@ -3,6 +3,7 @@ import { Chatwoot, configService, ProviderSession } from '@config/env.config';
import { eventEmitter } from '@config/event.config';
import { Logger } from '@config/logger.config';
import { BusinessController } from './controllers/business.controller';
import { CallController } from './controllers/call.controller';
import { ChatController } from './controllers/chat.controller';
import { GroupController } from './controllers/group.controller';
@ -98,6 +99,7 @@ export const instanceController = new InstanceController(
export const sendMessageController = new SendMessageController(waMonitor);
export const callController = new CallController(waMonitor);
export const chatController = new ChatController(waMonitor);
export const businessController = new BusinessController(waMonitor);
export const groupController = new GroupController(waMonitor);
export const labelController = new LabelController(waMonitor);

View File

@ -7,7 +7,7 @@ import { CacheConf, Chatwoot, ConfigService, Database, DelInstance, ProviderSess
import { Logger } from '@config/logger.config';
import { INSTANCE_DIR, STORE_DIR } from '@config/path.config';
import { NotFoundException } from '@exceptions';
import { execSync } from 'child_process';
import { execFileSync } from 'child_process';
import EventEmitter2 from 'eventemitter2';
import { rmSync } from 'fs';
import { join } from 'path';
@ -91,6 +91,7 @@ export class WAMonitoringService {
Chatwoot: true,
Proxy: true,
Rabbitmq: true,
Nats: true,
Sqs: true,
Websocket: true,
Setting: true,
@ -168,7 +169,8 @@ export class WAMonitoringService {
public async cleaningStoreData(instanceName: string) {
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
execSync(`rm -rf ${join(STORE_DIR, 'chatwoot', instanceName + '*')}`);
const instancePath = join(STORE_DIR, 'chatwoot', instanceName);
execFileSync('rm', ['-rf', instancePath]);
}
const instance = await this.prismaRepository.instance.findFirst({
@ -190,6 +192,7 @@ export class WAMonitoringService {
await this.prismaRepository.chatwoot.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.proxy.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.rabbitmq.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.nats.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.sqs.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.integrationSession.deleteMany({ where: { instanceId: instance.id } });
await this.prismaRepository.typebot.deleteMany({ where: { instanceId: instance.id } });

View File

@ -15,6 +15,7 @@ export enum Events {
MESSAGES_UPDATE = 'messages.update',
MESSAGES_DELETE = 'messages.delete',
SEND_MESSAGE = 'send.message',
SEND_MESSAGE_UPDATE = 'send.message.update',
CONTACTS_SET = 'contacts.set',
CONTACTS_UPSERT = 'contacts.upsert',
CONTACTS_UPDATE = 'contacts.update',

View File

@ -72,6 +72,7 @@ export type EventsRabbitmq = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -97,7 +98,16 @@ export type Rabbitmq = {
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY: string;
PREFIX_KEY?: string;
};
export type Nats = {
ENABLED: boolean;
URI: string;
EXCHANGE_NAME: string;
GLOBAL_ENABLED: boolean;
EVENTS: EventsRabbitmq;
PREFIX_KEY?: string;
};
export type Sqs = {
@ -131,6 +141,7 @@ export type EventsWebhook = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -163,6 +174,7 @@ export type EventsPusher = {
MESSAGES_UPDATE: boolean;
MESSAGES_DELETE: boolean;
SEND_MESSAGE: boolean;
SEND_MESSAGE_UPDATE: boolean;
CONTACTS_SET: boolean;
CONTACTS_UPDATE: boolean;
CONTACTS_UPSERT: boolean;
@ -251,6 +263,7 @@ export type S3 = {
PORT?: number;
USE_SSL?: boolean;
REGION?: string;
SKIP_POLICY?: boolean;
};
export type CacheConf = { REDIS: CacheConfRedis; LOCAL: CacheConfLocal };
@ -263,6 +276,7 @@ export interface Env {
PROVIDER: ProviderSession;
DATABASE: Database;
RABBITMQ: Rabbitmq;
NATS: Nats;
SQS: Sqs;
WEBSOCKET: Websocket;
WA_BUSINESS: WaBusiness;
@ -356,7 +370,7 @@ export class ConfigService {
RABBITMQ: {
ENABLED: process.env?.RABBITMQ_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY || 'evolution',
PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY,
EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.RABBITMQ_URI || '',
EVENTS: {
@ -370,6 +384,7 @@ export class ConfigService {
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',
SEND_MESSAGE_UPDATE: process.env?.RABBITMQ_EVENTS_SEND_MESSAGE_UPDATE === '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',
@ -389,6 +404,43 @@ export class ConfigService {
TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
NATS: {
ENABLED: process.env?.NATS_ENABLED === 'true',
GLOBAL_ENABLED: process.env?.NATS_GLOBAL_ENABLED === 'true',
PREFIX_KEY: process.env?.NATS_PREFIX_KEY,
EXCHANGE_NAME: process.env?.NATS_EXCHANGE_NAME || 'evolution_exchange',
URI: process.env.NATS_URI || '',
EVENTS: {
APPLICATION_STARTUP: process.env?.NATS_EVENTS_APPLICATION_STARTUP === 'true',
INSTANCE_CREATE: process.env?.NATS_EVENTS_INSTANCE_CREATE === 'true',
INSTANCE_DELETE: process.env?.NATS_EVENTS_INSTANCE_DELETE === 'true',
QRCODE_UPDATED: process.env?.NATS_EVENTS_QRCODE_UPDATED === 'true',
MESSAGES_SET: process.env?.NATS_EVENTS_MESSAGES_SET === 'true',
MESSAGES_UPSERT: process.env?.NATS_EVENTS_MESSAGES_UPSERT === 'true',
MESSAGES_EDITED: process.env?.NATS_EVENTS_MESSAGES_EDITED === 'true',
MESSAGES_UPDATE: process.env?.NATS_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.NATS_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.NATS_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.NATS_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.NATS_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.NATS_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.NATS_EVENTS_CONTACTS_UPSERT === 'true',
PRESENCE_UPDATE: process.env?.NATS_EVENTS_PRESENCE_UPDATE === 'true',
CHATS_SET: process.env?.NATS_EVENTS_CHATS_SET === 'true',
CHATS_UPDATE: process.env?.NATS_EVENTS_CHATS_UPDATE === 'true',
CHATS_UPSERT: process.env?.NATS_EVENTS_CHATS_UPSERT === 'true',
CHATS_DELETE: process.env?.NATS_EVENTS_CHATS_DELETE === 'true',
CONNECTION_UPDATE: process.env?.NATS_EVENTS_CONNECTION_UPDATE === 'true',
LABELS_EDIT: process.env?.NATS_EVENTS_LABELS_EDIT === 'true',
LABELS_ASSOCIATION: process.env?.NATS_EVENTS_LABELS_ASSOCIATION === 'true',
GROUPS_UPSERT: process.env?.NATS_EVENTS_GROUPS_UPSERT === 'true',
GROUP_UPDATE: process.env?.NATS_EVENTS_GROUPS_UPDATE === 'true',
GROUP_PARTICIPANTS_UPDATE: process.env?.NATS_EVENTS_GROUP_PARTICIPANTS_UPDATE === 'true',
CALL: process.env?.NATS_EVENTS_CALL === 'true',
TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true',
TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true',
},
},
SQS: {
ENABLED: process.env?.SQS_ENABLED === 'true',
ACCESS_KEY_ID: process.env.SQS_ACCESS_KEY_ID || '',
@ -421,6 +473,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.PUSHER_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.PUSHER_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.PUSHER_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.PUSHER_EVENTS_SEND_MESSAGE_UPDATE === 'true',
CONTACTS_SET: process.env?.PUSHER_EVENTS_CONTACTS_SET === 'true',
CONTACTS_UPDATE: process.env?.PUSHER_EVENTS_CONTACTS_UPDATE === 'true',
CONTACTS_UPSERT: process.env?.PUSHER_EVENTS_CONTACTS_UPSERT === 'true',
@ -477,6 +530,7 @@ export class ConfigService {
MESSAGES_UPDATE: process.env?.WEBHOOK_EVENTS_MESSAGES_UPDATE === 'true',
MESSAGES_DELETE: process.env?.WEBHOOK_EVENTS_MESSAGES_DELETE === 'true',
SEND_MESSAGE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE === 'true',
SEND_MESSAGE_UPDATE: process.env?.WEBHOOK_EVENTS_SEND_MESSAGE_UPDATE === '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',
@ -555,6 +609,7 @@ export class ConfigService {
PORT: Number.parseInt(process.env?.S3_PORT || '9000'),
USE_SSL: process.env?.S3_USE_SSL === 'true',
REGION: process.env?.S3_REGION,
SKIP_POLICY: process.env?.S3_SKIP_POLICY === 'true',
},
AUTHENTICATION: {
API_KEY: {

View File

@ -3,19 +3,19 @@ import { configService, S3 } from '@config/env.config';
const getTypeMessage = (msg: any) => {
let mediaId: string;
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message.mediaUrl;
else mediaId = msg.key.id;
if (configService.get<S3>('S3').ENABLE) mediaId = msg.message?.mediaUrl;
else mediaId = msg.key?.id;
const types = {
conversation: msg?.message?.conversation,
extendedTextMessage: msg?.message?.extendedTextMessage?.text,
contactMessage: msg?.message?.contactMessage?.displayName,
locationMessage: msg?.message?.locationMessage?.degreesLatitude,
locationMessage: msg?.message?.locationMessage?.degreesLatitude.toString(),
viewOnceMessageV2:
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.title,
listResponseMessage: msg?.message?.listResponseMessage?.title || msg?.listResponseMessage?.title,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
templateButtonReplyMessage:
msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId,

View File

@ -0,0 +1,17 @@
import { JSONSchema7 } from 'json-schema';
export const catalogSchema: JSONSchema7 = {
type: 'object',
properties: {
number: { type: 'string' },
limit: { type: 'number' },
},
};
export const collectionsSchema: JSONSchema7 = {
type: 'object',
properties: {
number: { type: 'string' },
limit: { type: 'number' },
},
};

View File

@ -68,6 +68,7 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@ -104,6 +105,44 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
'PRESENCE_UPDATE',
'CHATS_SET',
'CHATS_UPSERT',
'CHATS_UPDATE',
'CHATS_DELETE',
'GROUPS_UPSERT',
'GROUP_UPDATE',
'GROUP_PARTICIPANTS_UPDATE',
'CONNECTION_UPDATE',
'LABELS_EDIT',
'LABELS_ASSOCIATION',
'CALL',
'TYPEBOT_START',
'TYPEBOT_CHANGE_STATUS',
],
},
},
// NATS
natsEnabled: { type: 'boolean' },
natsEvents: {
type: 'array',
minItems: 0,
items: {
type: 'string',
enum: [
'APPLICATION_STARTUP',
'QRCODE_UPDATED',
'MESSAGES_SET',
'MESSAGES_UPSERT',
'MESSAGES_EDITED',
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',
@ -140,6 +179,7 @@ export const instanceSchema: JSONSchema7 = {
'MESSAGES_UPDATE',
'MESSAGES_DELETE',
'SEND_MESSAGE',
'SEND_MESSAGE_UPDATE',
'CONTACTS_SET',
'CONTACTS_UPSERT',
'CONTACTS_UPDATE',

View File

@ -1,4 +1,5 @@
// Integrations Schema
export * from './business.schema';
export * from './chat.schema';
export * from './group.schema';
export * from './instance.schema';