Compare commits

..

116 Commits

Author SHA1 Message Date
Davidson Gomes
7f10a0eecd Merge branch 'release/2.2.1' 2025-01-22 14:39:27 -03:00
Davidson Gomes
a1085b4a4d version: 2.2.1 2025-01-22 14:38:50 -03:00
Davidson Gomes
b1b2d18e5d Remove redundant migration cleanup step for wavoip_token in PostgreSQL migration file. This change simplifies the migration process by eliminating unnecessary deletion of previous migration records, ensuring a cleaner migration history. 2025-01-22 12:01:17 -03:00
Davidson Gomes
37a9e316a8 Refactor database migrations and schema for Chat and Setting tables
- Removed the unique constraint on `remoteJid` and `instanceId` in the `Chat` model to prevent potential migration failures due to existing duplicates.
- Deleted the old migration file for adding `wavoipToken` to the `Setting` table and created a new migration that includes a check for the column's existence before adding it, ensuring smoother migration processes.
- Updated migration logic to clear previous migrations related to `wavoip_token` to maintain a clean migration history.
2025-01-22 11:58:34 -03:00
Davidson Gomes
f340f7716f Remove unused dependencies from ChannelController to streamline integration logic 2025-01-22 11:14:05 -03:00
Davidson Gomes
dab843f118 changelog 2025-01-22 11:09:29 -03:00
Davidson Gomes
f7db442a94 Refactor Instance Management with Additional Profile Information
- Added new fields `ownerJid`, `profileName`, and `profilePicUrl` to the Instance DTO for improved user identification and personalization.
- Updated InstanceController to include the new profile information in instance data handling.
- Enhanced WAMonitoringService to utilize the additional profile fields, improving the context of instance data during monitoring operations.
2025-01-22 11:09:05 -03:00
Davidson Gomes
3a04f7587e Refactor Evolution Channel with new instance management and media handling features
- Added `setInstance` method to manage instance details and trigger events for instance creation.
- Refactored `connectToWhatsapp` to improve error handling and streamline connection logic.
- Enhanced `sendMessageWithTyping` to support various message types including buttons and lists, and integrated file handling for media messages.
- Updated `processAudio` to handle audio conversion using an external API, improving audio processing capabilities.
- Introduced new methods for sending button messages and handling audio messages, enhancing user interaction features.
- Improved contact update logic to ensure accurate data synchronization with the database and external services.
2025-01-22 11:06:10 -03:00
Davidson Gomes
666c0b514d Chats filtering to support timestamp range queries
- Introduced a new `timestampFilter` to allow filtering messages based on `messageTimestamp` within specified `gte` and `lte` ranges.
- Updated SQL query logic in `ChannelStartupService` to incorporate the timestamp filtering, improving message retrieval accuracy.
2025-01-22 10:22:09 -03:00
Davidson Gomes
ab5eb80edd Refactor JID creation and fetch chats
- Introduced a new utility function `createJid` to standardize JID creation across the application, replacing the previous method in `ChannelStartupService`.
- Updated multiple services to utilize the new `createJid` function for improved consistency and maintainability.
- Added a `cleanMessageData` method in `ChannelStartupService` to sanitize message objects before processing.
- Updated CHANGELOG to reflect the refactor on chat fetching logic and the introduction of the new JID utility.
2025-01-22 10:16:48 -03:00
Davidson Gomes
b0219e5e5a Update version to 2.2.1, enhance database migration, and update dependencies
- Updated version labels in Dockerfile and package-lock.json to 2.2.1.
- Added a new column `wavoipToken` to the `Setting` table in the PostgreSQL migration.
- Implemented a cleanup step to remove duplicate records in the `Chat` table before creating a unique index.
- Upgraded various package dependencies to their latest versions for improved functionality and security.
2025-01-21 17:39:03 -03:00
Davidson Gomes
d7b4965d60 Update CHANGELOG for version 2.2.1
- Added features: retry system for sending webhooks and enhanced message filtering to support timestamp range queries.
- Fixed issues: corrected global webhook handling and resolved audio sending problems with WhatsApp Cloud API.
2025-01-21 17:32:24 -03:00
Davidson Gomes
cfe6bd9ae0 Refactor instance deletion logic and enhance WhatsApp connection updates
- Updated the `deleteInstance` method to allow logout for instances in 'connecting' or 'open' states, improving instance management.
- Enhanced the `BaileysStartupService` to include additional profile information (wuid, profileName, profilePictureUrl) in connection update webhooks.
- Removed redundant webhook data sending logic, streamlining connection state updates for better performance.
- Adjusted settings schema to ensure required fields are properly validated.
2025-01-17 17:54:18 -03:00
Davidson Gomes
ac58f58bbc Update package version to 2.2.1 in package.json 2025-01-17 16:07:03 -03:00
Davidson Gomes
cb08f6b152 Refactor WebhookController to implement retry logic for webhook requests
- Introduced a new `retryWebhookRequest` method to handle retries for failed webhook requests, allowing up to 10 attempts with a delay of 30 seconds between each.
- Updated error logging to provide detailed information on each retry attempt, including the attempt number and error details.
- Enhanced existing webhook request handling to utilize the new retry logic, improving reliability in sending webhook data.
- Modified error messages to be more informative, indicating when all retry attempts have failed.
2025-01-17 16:06:23 -03:00
Davidson Gomes
a817d62067 Enhance message filtering in ChannelStartupService to support timestamp range queries
- Added support for filtering messages based on a timestamp range in the `ChannelStartupService`.
- Introduced a `timestampFilter` object to handle `gte` and `lte` conditions for `messageTimestamp`.
- Updated message count queries to include the new timestamp filtering logic.
2025-01-16 17:40:19 -03:00
Davidson Gomes
540467293c Enhance settings and integrate Baileys controller for WhatsApp functionality
- Added `wavoipToken` field to `Setting` model in both MySQL and PostgreSQL schemas.
- Updated `package.json` and `package-lock.json` to include `mime-types` and `socket.io-client` dependencies.
- Introduced `BaileysController` and `BaileysRouter` for handling WhatsApp interactions.
- Refactored media type handling to use `mime-types` instead of `mime` across various services.
- Updated DTOs and validation schemas to accommodate the new `wavoipToken` field.
- Implemented voice call functionalities using the Wavoip service in the Baileys integration.
- Enhanced event handling in the WebSocket controller to support new features.
2025-01-16 11:58:33 -03:00
Davidson Gomes
616ae0a7eb Update ESLint configuration, Dockerfile, and package dependencies; add GitHub Actions workflow for code quality checks
- Changed ESLint parser options to use 'module' as source type.
- Updated Dockerfile to remove force flag from npm install command.
- Upgraded 'mime' package from version 3.0.0 to 4.0.0 in package.json.
- Added '@types/mime' as a development dependency.
- Updated TypeScript configuration to use 'CommonJS' module format.
- Introduced a new GitHub Actions workflow for checking code quality, including linting and build checks.
2025-01-09 17:04:33 -03:00
Davidson Gomes
d598c4ed0b Update ESLint configuration, Dockerfile, and package dependencies; refactor bot trigger logic
- Updated ESLint configuration to use TypeScript project references and adjusted parser options.
- Modified Dockerfile to include OpenSSL in both builder and final stages.
- Changed `mime` package version from `^4.0.6` to `^3.0.0` in `package.json` and updated TypeScript ESLint packages to `^6.21.0`.
- Refactored `findBotByTrigger` function to remove unnecessary settings repository parameter.
- Adjusted bot trigger logic in multiple controller files to streamline function calls.
2025-01-09 12:57:21 -03:00
Davidson Gomes
ca451bfacc Merge pull request #1126 from jesus-chacon/update_chats
Fix: cuid security deprecation, update libs, lint and improve chat DB update
2025-01-07 13:06:23 -03:00
Davidson Gomes
f971f388d5 Merge pull request #1134 from Allyson-Santana/bugfix/import-prisma-type
bugfix: import prisma types
2025-01-07 13:05:22 -03:00
Jesus
236b0f9b26 Fix import prisma types 2025-01-07 08:59:51 +01:00
Jesus
0f2498bbaa Fix prettier errors 2025-01-07 08:50:34 +01:00
Jesus
2816a16387 Added lock and update some libs 2025-01-07 08:50:24 +01:00
Jesus
a82669b6fa Improve chat update and fix unread messages 2025-01-07 08:25:37 +01:00
Jesus
72a33ae59f fix: lint and prettify 2025-01-07 08:25:37 +01:00
Jesus
425d340956 Fix: cuid security deprecacion 2025-01-07 08:25:37 +01:00
Jesus
f2872cf59a Update several libs 2025-01-07 08:25:37 +01:00
Allyson Santana
1773f2738e bugfix: import prisma types 2025-01-06 18:31:40 -03:00
Davidson Gomes
18626c9846 Merge pull request #1131 from FaelN1/develop
fix: include filename in media message payload for WhatsApp Business
2025-01-06 12:20:19 -03:00
Davidson Gomes
6212ee3eb0 Merge pull request #1125 from fmorett/patch-1
fix cannot read null of mentioned
2025-01-06 12:18:56 -03:00
Davidson Gomes
8c877029e5 Merge pull request #1124 from ScrashOff/fix/instance-creation-webhook-and-chatwoot-settings
fix: instance create w/webhook and chatwoot settings
2025-01-06 12:18:25 -03:00
Davidson Gomes
0000c1c05c Merge pull request #1121 from EdmilsonSantana/fix/upgrade_prima_to_6_1_0
fix: upgrade Prisma packages to 6.1.0 to resolve an issue in Alpine images
2025-01-06 12:17:29 -03:00
Davidson Gomes
16daf9be8f Merge pull request #1119 from MarksonSolutions/develop
send audio using sendWhatsAppAudio route, wabussines
2025-01-06 12:17:12 -03:00
Rafael Nicolas Barbosa Moreira
e0c960cc54 chore: remove redundant comment about filename condition 2025-01-03 16:35:14 -03:00
Rafael Nicolas Barbosa Moreira
08ceb803c8 fix: conditionally include filename for non-image media 2025-01-03 16:33:34 -03:00
Rafael Nicolas Barbosa Moreira
6b72550286 fix: include filename in media message payload for WhatsApp Business 2025-01-03 15:56:39 -03:00
Fabio Moretti
df0990df0f fix cannot read null of mentioned 2024-12-28 02:15:26 +01:00
Fellipe "ScrashOff
60c9e231be fix: instance create w/webhook and chatwoot settings 2024-12-27 16:58:15 -03:00
EdmilsonSantana
070159568e fix: upgrading prisma packages to 6.1.0 to fix a issue in alpine images 2024-12-26 07:59:54 -03:00
MarksonSolutions
bfba702fde send audio using sendWhatsAppAudio route, wabussines 2024-12-20 16:00:15 -04:00
Davidson Gomes
d5c2cfb4f9 Merge pull request #1103 from roberto0arruda/fix/sentry-auto-instrument-express
chore: improve instrumentSentry.ts import before another import
2024-12-11 15:38:24 -03:00
Roberto Arruda
cdab5e2ae8 chore: improve instrumentSentry.ts import before another import
Signed-off-by: Roberto Arruda <roberto0arruda@hotmail.com>
2024-12-10 12:13:42 -04:00
Davidson Gomes
682eaa995f Merge pull request #1088 from joaosantanadev/develop
[FIX]Correction of webhook global
2024-11-27 12:10:52 -03:00
Davidson Gomes
62ea22a06a Merge pull request #1076 from verbeux-ai/main
[FIX] Fixando label handler e chats
2024-11-27 12:10:31 -03:00
joaosantanadev
370025b8f9 Correction of webhook global 2024-11-25 12:59:48 -03:00
Pedro Ivo
5d13f7055b Merge remote-tracking branch 'refs/remotes/evo/develop'
# Conflicts:
#	prisma/mysql-schema.prisma
#	src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
2024-11-22 10:11:50 -03:00
Davidson Gomes
c4e87c160e Merge pull request #1078 from Richards0nd/feature/windows-commands-support
Adaptação de scripts e comandos para compatibilidade com Windows
2024-11-21 14:51:14 -03:00
Davidson Gomes
ee8e937fad Merge pull request #1073 from lucastononro/fix-prisma-p1001-dockercompose
Update docker-compose and .env.example
2024-11-21 14:49:10 -03:00
Davidson Gomes
d5a7e03ec2 Merge pull request #1012 from yousseefspires/fix/chats-messages
Add unique in Chat by instance/remotejid
2024-11-21 14:48:40 -03:00
Davidson Gomes
52b6b61ac9 Merge pull request #1071 from AlanCezarAraujo/fix/openai-audio
Correção de envio de áudio para OpenAI via Cloud API
2024-11-21 14:46:51 -03:00
Pedro Ivo
6e6711a5af fix: get instance id in right place on handle label 2024-11-19 10:26:01 -03:00
Pedro Ivo
013fa9dc08 fix: join is considering instance id 2024-11-19 10:17:49 -03:00
Pedro Ivo
a42bc988ec fix: add/remove saving on db and improve add query for startup case 2024-11-19 10:15:08 -03:00
Richards0n
3582cd38fb Add Windows support for database migration commands in package.json 2024-11-18 20:07:08 -03:00
Richards0n
bfd8c08987 Add Windows support for database deployment and improve error handling in runWithProvider.js 2024-11-18 20:01:10 -03:00
Pedro Ivo
ecbbc5b090 fix: avoid concurrency cases in label handler 2024-11-17 20:04:27 -03:00
Lucas Tonon
b7e15be418 Update docker-compose and .env.example
Error:

Database URL: 
Environment variables loaded from .env
Prisma schema loaded from prisma/postgresql-schema.prisma
Datasource "db": PostgreSQL database "evolution", schema "public" at "localhost:5432"
Error: P1001: Can't reach database server at localhost:5432

Fix:

Update `docker-compose.yaml` and `.env.example` files to configure PostgreSQL service and connection URI.

* **docker-compose.yaml**
  - Add `listen_addresses=*` command to the `postgres` service.
  - Add environment variables: `POSTGRES_USER=user`, `POSTGRES_PASSWORD=pass`, `POSTGRES_DB=evolution`, `POSTGRES_HOST_AUTH_METHOD=trust`.

* **.env.example**
  - Update `DATABASE_CONNECTION_URI` to `postgresql://user:pass@postgres:5432/evolution?schema=public`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/EvolutionAPI/evolution-api?shareId=XXXX-XXXX-XXXX-XXXX).
2024-11-17 11:36:12 -03:00
Alan Cezar
1d5508736e corrigido o envio de áudio para OpenAI 2024-11-15 17:57:38 -03:00
Pedro Ivo
cefe3ef6c3 fix: chats must be unique 2024-11-15 17:38:26 -03:00
Davidson Gomes
09a33a423e Merge pull request #1070 from help-corretora/main
Quando a conversa está aberta e o cliente manda mensagem, o status da conversa muda para pendente.
2024-11-15 10:59:10 -03:00
Help Corretora
e092a80aeb Merge pull request #1 from vitor-help/main
erro, colocando mensagens em pendente
2024-11-14 20:50:58 -03:00
Vitor
99a533afc1 o chat está colocando a conversa como pendente quando o cliente manda mensagem, esse código visa corrigir isso. 2024-11-14 20:48:47 -03:00
Davidson Gomes
b603021f56 Merge pull request #1067 from Alexandre-Prado/fix-chatbots-send-message
fix: esvazia textBuffer dos chatbots no sendMessageWhatsapp quando splitMessages é true
2024-11-14 18:37:07 -03:00
Davidson Gomes
7bf0fd1c36 Merge pull request #1058 from Richards0nd/newPhoneNumberContent
Correção: Número de telefone apresentado no nome do contato (CW Service)
2024-11-14 18:36:43 -03:00
Davidson Gomes
52cf4fa8b8 Merge pull request #1069 from AlanCezarAraujo/fix/whatsapp-images
Correção de Imagens no Cloud API
2024-11-14 18:36:24 -03:00
Alan Cezar
ef7574273c message recorded for no media messages 2024-11-14 09:11:21 -03:00
Alan Cezar
d797d8177c added isMediaMessage method 2024-11-14 09:10:11 -03:00
Alan Cezar
e7ed1446ee saved base64 file in memory 2024-11-14 09:09:27 -03:00
Alan Cezar
5eaabfb1eb fixed messageId 2024-11-14 09:08:52 -03:00
Alan Cezar
59383d5944 fixed remoteJid 2024-11-14 09:08:23 -03:00
Alan Cezar
d87d7c0775 fixed image mime type 2024-11-14 09:07:57 -03:00
Alexandre Prado
753f4ba141 fix: chatbots send message text buffer 2024-11-14 02:25:15 -03:00
Richards0n
7ef8afa9b3 Refactor phone number formatting in ChatwootService to improve parsing logic 2024-11-11 16:57:04 -03:00
Richardson Douglas
c5fd81ddbf Merge pull request #1 from Richards0nd/develop
Develop
2024-11-11 16:49:17 -03:00
Davidson Gomes
8f855b4bfd Merge pull request #1046 from rafwell/fixMessageStatus
Fix Message.Status as String
2024-11-11 15:44:25 -03:00
Davidson Gomes
084be1cee2 Merge pull request #1044 from Richards0nd/newPhoneNumberContent
Adiciona o número de telefone do remetente nas mensagens recebidas do WhatsApp antes do nome do contato (GRUPOS)
2024-11-11 15:44:05 -03:00
Davidson Gomes
997aceeebf Merge pull request #1038 from fmedeiros95/develop
Ajuste nos tipos de mensagem respondidas
2024-11-11 15:43:49 -03:00
Rafael Souza
c8410bd146 Fix Message.Status as String 2024-11-08 07:15:23 -03:00
Richards0n
3d51b45e2b feat: format participant phone number in messages 2024-11-08 00:25:00 -03:00
Felipe Medeiros
50d84d1a08 Merge branch 'EvolutionAPI:develop' into develop 2024-11-05 17:17:15 -03:00
Felipe Medeiros
fd1f08a41e feat: handle quoted messages in WhatsApp integration 2024-11-05 17:16:41 -03:00
Davidson Gomes
8dc27919b1 Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-11-02 08:13:04 -03:00
Davidson Gomes
6591a67ab6 feat: sh local install 2024-11-02 08:12:30 -03:00
Davidson Gomes
08192a03ca Merge pull request #1023 from adriel12319/main
Fix Ios Response Button
2024-11-01 16:10:07 -03:00
Adriel Santos Araujo
8f86c9d758 Update getConversationMessage.ts 2024-10-31 23:19:17 -03:00
Adriel Santos Araujo
0e5f9e3b77 Update getConversationMessage.ts 2024-10-31 22:52:45 -03:00
Davidson Gomes
1665654676 feat: typebot send list 2024-10-29 19:30:17 -03:00
Davidson Gomes
133eddd742 docker-compose 2024-10-29 18:22:45 -03:00
Davidson Gomes
c55312d206 feat: typebot send list 2024-10-29 17:51:06 -03:00
Davidson Gomes
52216ec08e fix: receive buttons and list response in integrations 2024-10-29 16:11:53 -03:00
Davidson Gomes
7a01cdf0ef fix: receive buttons and list response in integrations 2024-10-29 16:11:29 -03:00
Davidson Gomes
65a9c78d86 feat: typebot send buttons 2024-10-29 16:03:00 -03:00
yousseefs
f6ccd58dee some fixs 2024-10-29 18:43:31 +00:00
Davidson Gomes
fbccf2eb2a feat: send pix button 2024-10-29 10:00:32 -03:00
Davidson Gomes
23640a71b8 fix: fetch instances 2024-10-29 07:43:48 -03:00
Davidson Gomes
fce3e55e91 feat: send pix button 2024-10-29 07:36:24 -03:00
Davidson Gomes
9f39ec2110 feat: send ptv message 2024-10-28 18:02:42 -03:00
Davidson Gomes
89c4c194df Merge branch 'develop' of github.com:EvolutionAPI/evolution-api into develop 2024-10-28 18:02:24 -03:00
Davidson Gomes
a4e7baa41c feat: send ptv message 2024-10-28 18:02:17 -03:00
Davidson Gomes
e22ff6c0d9 Merge pull request #1009 from yousseefspires/fix/chats-messages
fix: received messages but chat doesnt exists
2024-10-28 17:42:44 -03:00
yousseefs
11d31123ac fix: received messages but chat doesnt exists 2024-10-28 20:23:20 +00:00
Davidson Gomes
0fdc47e8f0 Merge pull request #1001 from robjean9/patch-1
FIX: Update instance.controller.ts to filter by instanceName
2024-10-28 11:13:51 -03:00
Robson Jean Penteado
60db8081bd FIX: Update instance.controller.ts to filter by instanceName
This commit should fix the filter by instanceName
2024-10-24 09:49:43 -03:00
Davidson Gomes
37b003f169 Merge pull request #1000 from fmedeiros95/develop
remove o arquivo public/images/cover (1).png:Zone.Identifier
2024-10-24 09:05:10 -03:00
Felipe Medeiros
891c3eb5d3 remove o arquivo 2024-10-24 08:52:56 -03:00
Davidson Gomes
3b99699f1a fix: var API_AUDIO_CONVERTER with default value 2024-10-22 11:29:43 -03:00
Davidson Gomes
e1de70542b feat: convert audio with api 2024-10-22 09:43:58 -03:00
Davidson Gomes
c10680df41 brand 2024-10-21 18:53:21 -03:00
Davidson Gomes
171f460f3b fix: ignoreJids in integrations dont work 2024-10-21 13:52:08 -03:00
Davidson Gomes
6d0ad5f3db feat: convert audio with api 2024-10-21 12:04:38 -03:00
Davidson Gomes
f34115fdcb feat: convert audio with api 2024-10-21 11:59:45 -03:00
Davidson Gomes
f9705c07dc feat: convert audio with api 2024-10-21 11:59:20 -03:00
Davidson Gomes
e986768716 feat: convert audio with api 2024-10-21 11:39:21 -03:00
Davidson Gomes
34769e2293 fix: receive medias on chatwoot 2024-10-18 19:49:14 -03:00
Davidson Gomes
5401ecd2c4 fix: send buttons cloud api oficial 2024-10-18 19:23:15 -03:00
73 changed files with 15308 additions and 831 deletions

View File

@@ -26,7 +26,7 @@ DEL_INSTANCE=false
# Provider: postgresql | mysql
DATABASE_PROVIDER=postgresql
DATABASE_CONNECTION_URI='postgresql://user:pass@localhost:5432/evolution?schema=public'
DATABASE_CONNECTION_URI='postgresql://user:pass@postgres:5432/evolution?schema=public'
# Client name for the database connection
# It is used to separate an API installation from another that uses the same database.
DATABASE_CONNECTION_CLIENT_NAME=evolution_exchange
@@ -248,6 +248,10 @@ S3_USE_SSL=true
# S3_USE_SSL=true
# S3_REGION=eu-south
# Evolution Audio Converter - Environment variables - https://github.com/EvolutionAPI/evolution-audio-converter
# API_AUDIO_CONVERTER=http://localhost:4040/process-audio
# API_AUDIO_CONVERTER_KEY=429683C4C977415CAAFCCE10F7D57E11
# Define a global apikey to access all instances.
# OBS: This key must be inserted in the request header to create an instance.
AUTHENTICATION_API_KEY=429683C4C977415CAAFCCE10F7D57E11

View File

@@ -1,7 +1,11 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'CommonJS',
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
warnOnUnsupportedTypeScriptVersion: false,
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
plugins: ['@typescript-eslint', 'simple-import-sort', 'import'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],

View File

@@ -0,0 +1,28 @@
name: Check Code Quality
on: [pull_request]
jobs:
check-lint-and-build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v2
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 20.x
- name: Install packages
run: npm install
- name: Check linting
run: npm run lint:check
- name: Check build
run: npm run db:generate
- name: Check build
run: npm run build

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@ lerna-debug.log*
# Package
/yarn.lock
/pnpm-lock.yaml
/package-lock.json
# IDEs
.vscode/*

View File

@@ -1,4 +1,19 @@
# 2.2.0 (develop)
# 2.2.1 (2025-01-22 14:37)
### Features
* Retry system for send webhooks
* Message filtering to support timestamp range queries
* Chats filtering to support timestamp range queries
### Fixed
* Correction of webhook global
* Fixed send audio with whatsapp cloud api
* Refactor on fetch chats
* Refactor on Evolution Channel
# 2.2.0 (2024-10-18 10:00)
### Features
@@ -8,6 +23,8 @@
* Added unreadMessages to chats
* Pusher event integration
* Add support for splitMessages and timePerChar in Integrations
* Audio Converter via API
* Send PTV messages with Baileys
### Fixed
@@ -19,6 +36,8 @@
* Add indexes to improve performance in Evolution
* Add logical or permanent message deletion based on env config
* Add support for fetching multiple instances by key
* Update instance.controller.ts to filter by instanceName
* Receive template button reply message
# 2.1.2 (2024-10-06 10:09)

View File

@@ -1,9 +1,9 @@
FROM node:20-alpine AS builder
RUN apk update && \
apk add git ffmpeg wget curl bash
apk add git ffmpeg wget curl bash openssl
LABEL version="2.2.0" description="Api to control whatsapp features through http requests."
LABEL version="2.2.1" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@atendai.com"
@@ -11,7 +11,7 @@ WORKDIR /evolution
COPY ./package.json ./tsconfig.json ./
RUN npm install -f
RUN npm install
COPY ./src ./src
COPY ./public ./public
@@ -32,7 +32,7 @@ RUN npm run build
FROM node:20-alpine AS final
RUN apk update && \
apk add tzdata ffmpeg bash
apk add tzdata ffmpeg bash openssl
ENV TZ=America/Sao_Paulo

View File

@@ -75,10 +75,6 @@ To continuously improve our services, we have implemented telemetry that collect
Join our Evolution Pro community for expert support and a weekly call to answer questions. Visit the link below to learn more and subscribe:
[Click here to learn more](https://evolution-api.com/suporte-pro)
<br>
<a href="https://evolution-api.com/suporte-pro">
<img src="./public/images/evolution-pro.png" alt="Subscribe" width="600">
</a>
# Donate to the project.

View File

@@ -1,8 +1,11 @@
services:
api:
container_name: evolution_api
image: atendai/evolution-api:v2.0.9-rc
image: atendai/evolution-api:homolog
restart: always
depends_on:
- redis
- postgres
ports:
- 8080:8080
volumes:
@@ -14,8 +17,41 @@ services:
expose:
- 8080
redis:
image: redis:latest
networks:
- evolution-net
container_name: redis
command: >
redis-server --port 6379 --appendonly yes
volumes:
- evolution_redis:/data
ports:
- 6379:6379
postgres:
container_name: postgres
image: postgres:15
networks:
- evolution-net
command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"]
restart: always
ports:
- 5432:5432
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=evolution
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
- 5432
volumes:
evolution_instances:
evolution_redis:
postgres_data:
networks:

150
local_install.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# Definir cores para melhor legibilidade
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Função para log
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Verificar se está rodando como root
if [ "$(id -u)" = "0" ]; then
log_error "Este script não deve ser executado como root"
exit 1
fi
# Verificar sistema operacional
OS="$(uname -s)"
case "${OS}" in
Linux*)
if [ ! -x "$(command -v curl)" ]; then
log_warning "Curl não está instalado. Tentando instalar..."
if [ -x "$(command -v apt-get)" ]; then
sudo apt-get update && sudo apt-get install -y curl
elif [ -x "$(command -v yum)" ]; then
sudo yum install -y curl
else
log_error "Não foi possível instalar curl automaticamente. Por favor, instale manualmente."
exit 1
fi
fi
;;
Darwin*)
if [ ! -x "$(command -v curl)" ]; then
log_error "Curl não está instalado. Por favor, instale o Xcode Command Line Tools."
exit 1
fi
;;
*)
log_error "Sistema operacional não suportado: ${OS}"
exit 1
;;
esac
# Verificar conexão com a internet antes de prosseguir
if ! ping -c 1 8.8.8.8 &> /dev/null; then
log_error "Sem conexão com a internet. Por favor, verifique sua conexão."
exit 1
fi
# Adicionar verificação de espaço em disco
REQUIRED_SPACE=1000000 # 1GB em KB
AVAILABLE_SPACE=$(df -k . | awk 'NR==2 {print $4}')
if [ $AVAILABLE_SPACE -lt $REQUIRED_SPACE ]; then
log_error "Espaço em disco insuficiente. Necessário pelo menos 1GB livre."
exit 1
fi
# Adicionar tratamento de erro para comandos npm
npm_install_with_retry() {
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
log "Tentativa $attempt de $max_attempts para npm install"
if npm install; then
return 0
fi
attempt=$((attempt + 1))
[ $attempt -le $max_attempts ] && log_warning "Falha na instalação. Tentando novamente em 5 segundos..." && sleep 5
done
log_error "Falha ao executar npm install após $max_attempts tentativas"
return 1
}
# Adicionar timeout para comandos
execute_with_timeout() {
timeout 300 $@ || log_error "Comando excedeu o tempo limite de 5 minutos: $@"
}
# Verificar se o NVM já está instalado
if [ -d "$HOME/.nvm" ]; then
log "NVM já está instalado."
else
log "Instalando NVM..."
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
fi
# Carregar o NVM no ambiente atual
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
# Verificar se a versão do Node.js já está instalada
if command -v node >/dev/null 2>&1 && [ "$(node -v)" = "v20.10.0" ]; then
log "Node.js v20.10.0 já está instalado."
else
log "Instalando Node.js v20.10.0..."
nvm install v20.10.0
fi
nvm use v20.10.0
# Verificar as versões instaladas
log "Verificando as versões instaladas:"
log "Node.js: $(node -v)"
log "npm: $(npm -v)"
# Instala dependências do projeto
log "Instalando dependências do projeto..."
rm -rf node_modules
npm install
# Deploy do banco de dados
log "Deploy do banco de dados..."
npm run db:generate
npm run db:deploy
# Iniciar o projeto
log "Iniciando o projeto..."
if [ "$1" = "-dev" ]; then
npm run dev:server
else
npm run build
npm run start:prod
fi
log "Instalação concluída com sucesso!"
# Criar arquivo de log
LOGFILE="./installation_log_$(date +%Y%m%d_%H%M%S).log"
exec 1> >(tee -a "$LOGFILE")
exec 2>&1
# Adicionar trap para limpeza em caso de interrupção
cleanup() {
log "Limpando recursos temporários..."
# Adicione comandos de limpeza aqui
}
trap cleanup EXIT

12227
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "evolution-api",
"version": "2.2.0",
"version": "2.2.1",
"description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js",
"type": "commonjs",
@@ -8,13 +8,16 @@
"build": "tsc --noEmit && tsup",
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
"start:prod": "node dist/main",
"dev:server": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
"test": "clear && tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
"dev:server": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
"test": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
"lint": "eslint --fix --ext .ts src",
"lint:check": "eslint --ext .ts src",
"db:generate": "node runWithProvider.js \"npx prisma generate --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"db:deploy": "node runWithProvider.js \"rm -rf ./prisma/migrations && cp -r ./prisma/DATABASE_PROVIDER-migrations ./prisma/migrations && npx prisma migrate deploy --schema ./prisma/DATABASE_PROVIDER-schema.prisma\"",
"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": "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\""
},
"repository": {
"type": "git",
@@ -47,71 +50,75 @@
"homepage": "https://github.com/EvolutionAPI/evolution-api#readme",
"dependencies": {
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-sqs": "^3.569.0",
"@aws-sdk/client-sqs": "^3.723.0",
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@figuro/chatwoot-sdk": "^1.1.16",
"@hapi/boom": "^10.0.1",
"@prisma/client": "^5.15.0",
"@sentry/node": "^8.28.0",
"amqplib": "^0.10.3",
"axios": "^1.6.5",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "^6.1.0",
"@sentry/node": "^8.47.0",
"amqplib": "^0.10.5",
"axios": "^1.7.9",
"baileys": "github:EvolutionAPI/Baileys",
"class-validator": "^0.14.1",
"compression": "^1.7.4",
"compression": "^1.7.5",
"cors": "^2.8.5",
"dayjs": "^1.11.7",
"dotenv": "^16.4.5",
"dayjs": "^1.11.13",
"dotenv": "^16.4.7",
"eventemitter2": "^6.4.9",
"express": "^4.18.2",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"fluent-ffmpeg": "^2.1.2",
"form-data": "^4.0.0",
"https-proxy-agent": "^7.0.2",
"fluent-ffmpeg": "^2.1.3",
"form-data": "^4.0.1",
"https-proxy-agent": "^7.0.6",
"i18next": "^23.7.19",
"jimp": "^0.16.13",
"json-schema": "^0.4.0",
"jsonschema": "^1.4.1",
"link-preview-js": "^3.0.4",
"link-preview-js": "^3.0.13",
"long": "^5.2.3",
"mime": "^3.0.0",
"minio": "^8.0.1",
"mediainfo.js": "^0.3.4",
"mime": "^4.0.0",
"mime-types": "^2.1.35",
"minio": "^8.0.3",
"multer": "^1.4.5-lts.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",
"openai": "^4.52.7",
"pg": "^8.11.3",
"openai": "^4.77.3",
"pg": "^8.13.1",
"pino": "^8.11.0",
"prisma": "^5.15.0",
"prisma": "^6.1.0",
"pusher": "^5.2.0",
"qrcode": "^1.5.1",
"qrcode": "^1.5.4",
"qrcode-terminal": "^0.12.0",
"redis": "^4.6.5",
"sharp": "^0.32.2",
"socket.io": "^4.7.1",
"tsup": "^8.2.4",
"uuid": "^9.0.0"
"redis": "^4.7.0",
"sharp": "^0.32.6",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"tsup": "^8.3.5"
},
"devDependencies": {
"@types/compression": "^1.7.2",
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.18",
"@types/json-schema": "^7.0.15",
"@types/mime": "3.0.0",
"@types/node": "^18.15.11",
"@types/mime": "^4.0.0",
"@types/mime-types": "^2.1.4",
"@types/node": "^22.10.5",
"@types/node-cron": "^3.0.11",
"@types/qrcode": "^1.5.0",
"@types/qrcode-terminal": "^0.12.0",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@types/qrcode": "^1.5.5",
"@types/qrcode-terminal": "^0.12.2",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"prettier": "^2.8.8",
"prettier": "^3.4.2",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.5.4"
"typescript": "^5.7.2"
}
}

View File

@@ -0,0 +1,232 @@
/*
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 `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`.
*/
-- AlterTable
ALTER TABLE `Chat` ADD COLUMN `unreadMessages` INTEGER NOT NULL DEFAULT 0,
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` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `DifySetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `EvolutionBotSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `Flowise` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
MODIFY `createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
MODIFY `updatedAt` TIMESTAMP NOT NULL;
-- AlterTable
ALTER TABLE `FlowiseSetting` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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 `Message` MODIFY `status` VARCHAR(30) NULL;
-- AlterTable
ALTER TABLE `OpenaiBot` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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` ADD COLUMN `splitMessages` BOOLEAN NULL DEFAULT false,
ADD COLUMN `timePerChar` INTEGER NULL DEFAULT 50,
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 `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` 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;
-- CreateTable
CREATE TABLE `Pusher` (
`id` VARCHAR(191) NOT NULL,
`enabled` BOOLEAN NOT NULL DEFAULT false,
`appId` VARCHAR(100) NOT NULL,
`key` VARCHAR(100) NOT NULL,
`secret` VARCHAR(100) NOT NULL,
`cluster` VARCHAR(100) NOT NULL,
`useTLS` BOOLEAN NOT NULL DEFAULT false,
`events` JSON NOT NULL,
`createdAt` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
`updatedAt` TIMESTAMP NOT NULL,
`instanceId` VARCHAR(191) NOT NULL,
UNIQUE INDEX `Pusher_instanceId_key`(`instanceId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateIndex
CREATE INDEX `Chat_remoteJid_idx` ON `Chat`(`remoteJid`);
-- CreateIndex
CREATE INDEX `Contact_remoteJid_idx` ON `Contact`(`remoteJid`);
-- CreateIndex
CREATE INDEX `Setting_instanceId_idx` ON `Setting`(`instanceId`);
-- CreateIndex
CREATE INDEX `Webhook_instanceId_idx` ON `Webhook`(`instanceId`);
-- AddForeignKey
ALTER TABLE `Pusher` ADD CONSTRAINT `Pusher_instanceId_fkey` FOREIGN KEY (`instanceId`) REFERENCES `Instance`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- RenameIndex
ALTER TABLE `Chat` RENAME INDEX `Chat_instanceId_fkey` TO `Chat_instanceId_idx`;
-- RenameIndex
ALTER TABLE `Contact` RENAME INDEX `Contact_instanceId_fkey` TO `Contact_instanceId_idx`;
-- RenameIndex
ALTER TABLE `Message` RENAME INDEX `Message_instanceId_fkey` TO `Message_instanceId_idx`;
-- RenameIndex
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_instanceId_fkey` TO `MessageUpdate_instanceId_idx`;
-- RenameIndex
ALTER TABLE `MessageUpdate` RENAME INDEX `MessageUpdate_messageId_fkey` TO `MessageUpdate_messageId_idx`;

View File

@@ -127,6 +127,7 @@ model Chat {
unreadMessages Int @default(0)
@@index([instanceId])
@@index([remoteJid])
@@unique([instanceId, remoteJid])
}
model Contact {
@@ -263,6 +264,7 @@ model Setting {
readMessages Boolean @default(false)
readStatus Boolean @default(false)
syncFullHistory Boolean @default(false)
wavoipToken String? @db.VarChar(100)
createdAt DateTime? @default(dbgenerated("CURRENT_TIMESTAMP")) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)

View File

@@ -0,0 +1,19 @@
/*
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
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'Setting'
AND column_name = 'wavoipToken'
) THEN
ALTER TABLE "Setting" ADD COLUMN "wavoipToken" VARCHAR(100);
END IF;
END $$;

View File

@@ -264,6 +264,7 @@ model Setting {
readMessages Boolean @default(false) @db.Boolean
readStatus Boolean @default(false) @db.Boolean
syncFullHistory Boolean @default(false) @db.Boolean
wavoipToken String? @db.VarChar(100)
createdAt DateTime? @default(now()) @db.Timestamp
updatedAt DateTime @updatedAt @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,19 +1,31 @@
const dotenv = require('dotenv');
const { execSync } = require('child_process');
const { existsSync } = require('fs');
dotenv.config();
const { DATABASE_PROVIDER } = process.env;
const databaseProviderDefault = DATABASE_PROVIDER ?? "postgresql"
const databaseProviderDefault = DATABASE_PROVIDER ?? 'postgresql';
if (!DATABASE_PROVIDER) {
console.error(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
// process.exit(1);
console.warn(`DATABASE_PROVIDER is not set in the .env file, using default: ${databaseProviderDefault}`);
}
const command = process.argv
let command = process.argv
.slice(2)
.join(' ')
.replace(/\DATABASE_PROVIDER/g, databaseProviderDefault);
.replace(/DATABASE_PROVIDER/g, databaseProviderDefault);
if (command.includes('rmdir') && existsSync('prisma\\migrations')) {
try {
execSync('rmdir /S /Q prisma\\migrations', { stdio: 'inherit' });
} catch (error) {
console.error(`Error removing directory: prisma\\migrations`);
process.exit(1);
}
} else if (command.includes('rmdir')) {
console.warn(`Directory 'prisma\\migrations' does not exist, skipping removal.`);
}
try {
execSync(command, { stdio: 'inherit' });

View File

@@ -63,6 +63,9 @@ export class InstanceController {
instanceId,
integration: instanceData.integration,
instanceName: instanceData.instanceName,
ownerJid: instanceData.ownerJid,
profileName: instanceData.profileName,
profilePicUrl: instanceData.profilePicUrl,
hash,
number: instanceData.number,
businessId: instanceData.businessId,
@@ -119,6 +122,7 @@ export class InstanceController {
readMessages: instanceData.readMessages === true,
readStatus: instanceData.readStatus === true,
syncFullHistory: instanceData.syncFullHistory === true,
wavoipToken: instanceData.wavoipToken || '',
};
await this.settingsService.create(instance, settings);
@@ -382,7 +386,9 @@ export class InstanceController {
return this.waMonitor.instanceInfoById(instanceId, number);
}
return this.waMonitor.instanceInfo();
const instanceNames = instanceName ? [instanceName] : null;
return this.waMonitor.instanceInfo(instanceNames);
}
public async setPresence({ instanceName }: InstanceDto, data: SetPresenceDto) {
@@ -407,15 +413,11 @@ export class InstanceController {
public async deleteInstance({ instanceName }: InstanceDto) {
const { instance } = await this.connectionState({ instanceName });
if (instance.state === 'open') {
throw new BadRequestException('The "' + instanceName + '" instance needs to be disconnected');
}
try {
const waInstances = this.waMonitor.waInstances[instanceName];
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED) waInstances?.clearCacheChatwoot();
if (instance.state === 'connecting') {
if (instance.state === 'connecting' || instance.state === 'open') {
await this.logout({ instanceName });
}

View File

@@ -10,7 +10,10 @@ import axios from 'axios';
const logger = new Logger('ProxyController');
export class ProxyController {
constructor(private readonly proxyService: ProxyService, private readonly waMonitor: WAMonitoringService) {}
constructor(
private readonly proxyService: ProxyService,
private readonly waMonitor: WAMonitoringService,
) {}
public async createProxy(instance: InstanceDto, data: ProxyDto) {
if (!this.waMonitor.waInstances[instance.instanceName]) {

View File

@@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
SendStickerDto,
@@ -39,6 +40,13 @@ export class SendMessageController {
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendPtv({ instanceName }: InstanceDto, data: SendPtvDto, file?: any) {
if (file || isURL(data?.video) || isBase64(data?.video)) {
return await this.waMonitor.waInstances[instanceName].ptvMessage(data, file);
}
throw new BadRequestException('Owned media must be a url or base64');
}
public async sendSticker({ instanceName }: InstanceDto, data: SendStickerDto, file?: any) {
if (file || isURL(data.sticker) || isBase64(data.sticker)) {
return await this.waMonitor.waInstances[instanceName].mediaSticker(data, file);

View File

@@ -1,4 +1,5 @@
import { IntegrationDto } from '@api/integrations/integration.dto';
import { JsonValue } from '@prisma/client/runtime/library';
import { WAPresence } from 'baileys';
export class InstanceDto extends IntegrationDto {
@@ -10,6 +11,9 @@ export class InstanceDto extends IntegrationDto {
integration?: string;
token?: string;
status?: string;
ownerJid?: string;
profileName?: string;
profilePicUrl?: string;
// settings
rejectCall?: boolean;
msgCall?: string;
@@ -18,12 +22,35 @@ export class InstanceDto extends IntegrationDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
// proxy
proxyHost?: string;
proxyPort?: string;
proxyProtocol?: string;
proxyUsername?: string;
proxyPassword?: string;
webhook?: {
enabled?: boolean;
events?: string[];
headers?: JsonValue;
url?: string;
byEvents?: boolean;
base64?: boolean;
};
chatwootAccountId?: string;
chatwootConversationPending?: boolean;
chatwootAutoCreate?: boolean;
chatwootDaysLimitImportMessages?: number;
chatwootImportContacts?: boolean;
chatwootImportMessages?: boolean;
chatwootLogo?: string;
chatwootMergeBrazilContacts?: boolean;
chatwootNameInbox?: string;
chatwootOrganization?: string;
chatwootReopenConversation?: boolean;
chatwootSignMsg?: boolean;
chatwootToken?: string;
chatwootUrl?: string;
}
export class SetPresenceDto {

View File

@@ -70,7 +70,7 @@ export class SendPollDto extends Metadata {
messageSecret?: Uint8Array;
}
export type MediaType = 'image' | 'document' | 'video' | 'audio';
export type MediaType = 'image' | 'document' | 'video' | 'audio' | 'ptv';
export class SendMediaDto extends Metadata {
mediatype: MediaType;
@@ -82,6 +82,10 @@ export class SendMediaDto extends Metadata {
media: string;
}
export class SendPtvDto extends Metadata {
video: string;
}
export class SendStickerDto extends Metadata {
sticker: string;
}
@@ -90,15 +94,21 @@ export class SendAudioDto extends Metadata {
audio: string;
}
export type TypeButton = 'reply' | 'copy' | 'url' | 'call';
export type TypeButton = 'reply' | 'copy' | 'url' | 'call' | 'pix';
export type KeyType = 'phone' | 'email' | 'cpf' | 'cnpj' | 'random';
export class Button {
type: TypeButton;
displayText: string;
displayText?: string;
id?: string;
url?: string;
copyCode?: string;
phoneNumber?: string;
currency?: string;
name?: string;
keyType?: KeyType;
key?: string;
}
export class SendButtonsDto extends Metadata {

View File

@@ -6,4 +6,5 @@ export class SettingsDto {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
}

View File

@@ -75,8 +75,6 @@ export class ChannelController {
data.prismaRepository,
data.cache,
data.chatwootCache,
data.baileysCache,
data.providerFiles,
);
}

View File

@@ -2,14 +2,16 @@ import { Router } from 'express';
import { EvolutionRouter } from './evolution/evolution.router';
import { MetaRouter } from './meta/meta.router';
import { BaileysRouter } from './whatsapp/baileys.router';
export class ChannelRouter {
public readonly router: Router;
constructor(configService: any) {
constructor(configService: any, ...guards: any[]) {
this.router = Router();
this.router.use('/', new EvolutionRouter(configService).router);
this.router.use('/', new MetaRouter(configService).router);
this.router.use('/baileys', new BaileysRouter(...guards).router);
}
}

View File

@@ -1,16 +1,27 @@
import { MediaMessage, Options, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
import { ProviderFiles } from '@api/provider/sessions';
import { InstanceDto } from '@api/dto/instance.dto';
import {
MediaMessage,
Options,
SendAudioDto,
SendButtonsDto,
SendMediaDto,
SendTextDto,
} from '@api/dto/sendMessage.dto';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { PrismaRepository } from '@api/repository/repository.service';
import { chatbotController } from '@api/server.module';
import { CacheService } from '@api/services/cache.service';
import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Openai } from '@config/env.config';
import { Chatwoot, ConfigService, Openai, S3 } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { status } from '@utils/renderStatus';
import { isURL } from 'class-validator';
import { createJid } from '@utils/createJid';
import axios from 'axios';
import { isBase64, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import mime from 'mime';
import FormData from 'form-data';
import mimeTypes from 'mime-types';
import { join } from 'path';
import { v4 } from 'uuid';
export class EvolutionStartupService extends ChannelStartupService {
@@ -20,8 +31,6 @@ export class EvolutionStartupService extends ChannelStartupService {
public readonly prismaRepository: PrismaRepository,
public readonly cache: CacheService,
public readonly chatwootCache: CacheService,
public readonly baileysCache: CacheService,
private readonly providerFiles: ProviderFiles,
) {
super(configService, eventEmitter, prismaRepository, chatwootCache);
@@ -56,8 +65,34 @@ export class EvolutionStartupService extends ChannelStartupService {
await this.closeClient();
}
public setInstance(instance: InstanceDto) {
this.logger.setInstance(instance.instanceId);
this.instance.name = instance.instanceName;
this.instance.id = instance.instanceId;
this.instance.integration = instance.integration;
this.instance.number = instance.number;
this.instance.token = instance.token;
this.instance.businessId = instance.businessId;
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
this.chatwootService.eventWhatsapp(
Events.STATUS_INSTANCE,
{
instanceName: this.instance.name,
instanceId: this.instance.id,
integration: instance.integration,
},
{
instance: this.instance.name,
status: 'created',
},
);
}
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
return {
wuid: jid,
@@ -78,11 +113,12 @@ export class EvolutionStartupService extends ChannelStartupService {
}
public async connectToWhatsapp(data?: any): Promise<any> {
if (!data) return;
if (!data) {
this.loadChatwoot();
return;
}
try {
this.loadChatwoot();
this.eventHandler(data);
} catch (error) {
this.logger.error(error);
@@ -99,6 +135,7 @@ export class EvolutionStartupService extends ChannelStartupService {
id: received.key.id || v4(),
remoteJid: received.key.remoteJid,
fromMe: received.key.fromMe,
profilePicUrl: received.profilePicUrl,
};
messageRaw = {
key,
@@ -110,7 +147,9 @@ export class EvolutionStartupService extends ChannelStartupService {
instanceId: this.instanceId,
};
if (this.configService.get<Openai>('OPENAI').ENABLED) {
const isAudio = received?.message?.audioMessage;
if (this.configService.get<Openai>('OPENAI').ENABLED && isAudio) {
const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({
where: {
instanceId: this.instanceId,
@@ -165,7 +204,7 @@ export class EvolutionStartupService extends ChannelStartupService {
await this.updateContact({
remoteJid: messageRaw.key.remoteJid,
pushName: messageRaw.key.fromMe ? '' : messageRaw.key.fromMe == null ? '' : received.pushName,
pushName: messageRaw.pushName,
profilePicUrl: received.profilePicUrl,
});
}
@@ -175,35 +214,6 @@ export class EvolutionStartupService extends ChannelStartupService {
}
private async updateContact(data: { remoteJid: string; pushName?: string; profilePicUrl?: string }) {
const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
});
if (contact) {
const contactRaw: any = {
remoteJid: data.remoteJid,
pushName: data?.pushName,
instanceId: this.instanceId,
profilePicUrl: data?.profilePicUrl,
};
this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw);
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
await this.chatwootService.eventWhatsapp(
Events.CONTACTS_UPDATE,
{ instanceName: this.instance.name, instanceId: this.instanceId },
contactRaw,
);
}
await this.prismaRepository.contact.updateMany({
where: { remoteJid: contact.remoteJid, instanceId: this.instanceId },
data: contactRaw,
});
return;
}
const contactRaw: any = {
remoteJid: data.remoteJid,
pushName: data?.pushName,
@@ -211,11 +221,40 @@ export class EvolutionStartupService extends ChannelStartupService {
profilePicUrl: data?.profilePicUrl,
};
const existingContact = await this.prismaRepository.contact.findFirst({
where: {
remoteJid: data.remoteJid,
instanceId: this.instanceId,
},
});
if (existingContact) {
await this.prismaRepository.contact.updateMany({
where: {
remoteJid: data.remoteJid,
instanceId: this.instanceId,
},
data: contactRaw,
});
} else {
await this.prismaRepository.contact.create({
data: contactRaw,
});
}
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactRaw);
await this.prismaRepository.contact.create({
data: contactRaw,
});
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
await this.chatwootService.eventWhatsapp(
Events.CONTACTS_UPDATE,
{
instanceName: this.instance.name,
instanceId: this.instanceId,
integration: this.instance.integration,
},
contactRaw,
);
}
const chat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: data.remoteJid },
@@ -247,7 +286,13 @@ export class EvolutionStartupService extends ChannelStartupService {
});
}
protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) {
protected async sendMessageWithTyping(
number: string,
message: any,
options?: Options,
file?: any,
isIntegration = false,
) {
try {
let quoted: any;
let webhookUrl: any;
@@ -272,64 +317,187 @@ export class EvolutionStartupService extends ChannelStartupService {
webhookUrl = options.webhookUrl;
}
let audioFile;
const messageId = v4();
let messageRaw: any = {
key: { fromMe: true, id: messageId, remoteJid: number },
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
status: status[1],
};
let messageRaw: any;
if (message?.mediaType === 'image') {
messageRaw = {
...messageRaw,
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
mediaUrl: message.media,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'imageMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message?.mediaType === 'video') {
messageRaw = {
...messageRaw,
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
mediaUrl: message.media,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'videoMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message?.mediaType === 'audio') {
messageRaw = {
...messageRaw,
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
mediaUrl: message.media,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'audioMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
const buffer = Buffer.from(message.media, 'base64');
audioFile = {
buffer,
mimetype: 'audio/mp4',
originalname: `${messageId}.mp4`,
};
} else if (message?.mediaType === 'document') {
messageRaw = {
...messageRaw,
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
mediaUrl: message.media,
base64: isBase64(message.media) ? message.media : undefined,
mediaUrl: isURL(message.media) ? message.media : undefined,
quoted,
},
messageType: 'documentMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message.buttonMessage) {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
...message.buttonMessage,
buttons: message.buttonMessage.buttons,
footer: message.buttonMessage.footer,
body: message.buttonMessage.body,
quoted,
},
messageType: 'buttonMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else if (message.listMessage) {
messageRaw = {
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
...message.listMessage,
quoted,
},
messageType: 'listMessage',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
} else {
messageRaw = {
...messageRaw,
key: { fromMe: true, id: messageId, remoteJid: number },
message: {
...message,
quoted,
},
messageType: 'conversation',
messageTimestamp: Math.round(new Date().getTime() / 1000),
webhookUrl,
source: 'unknown',
instanceId: this.instanceId,
};
}
if (messageRaw.message.contextInfo) {
messageRaw.contextInfo = {
...messageRaw.message.contextInfo,
};
}
if (messageRaw.contextInfo?.stanzaId) {
const key: any = {
id: messageRaw.contextInfo.stanzaId,
};
const findMessage = await this.prismaRepository.message.findFirst({
where: {
instanceId: this.instanceId,
key,
},
});
if (findMessage) {
messageRaw.contextInfo.quotedMessage = findMessage.message;
}
}
const base64 = messageRaw.message.base64;
delete messageRaw.message.base64;
if (base64 || file || audioFile) {
if (this.configService.get<S3>('S3').ENABLE) {
try {
const fileBuffer = audioFile?.buffer || file?.buffer;
const buffer = base64 ? Buffer.from(base64, 'base64') : fileBuffer;
let mediaType: string;
let mimetype = audioFile?.mimetype || file.mimetype;
if (messageRaw.messageType === 'documentMessage') {
mediaType = 'document';
mimetype = !mimetype ? 'application/pdf' : mimetype;
} else if (messageRaw.messageType === 'imageMessage') {
mediaType = 'image';
mimetype = !mimetype ? 'image/png' : mimetype;
} else if (messageRaw.messageType === 'audioMessage') {
mediaType = 'audio';
mimetype = !mimetype ? 'audio/mp4' : mimetype;
} else if (messageRaw.messageType === 'videoMessage') {
mediaType = 'video';
mimetype = !mimetype ? 'video/mp4' : mimetype;
}
const fileName = `${messageRaw.key.id}.${mimetype.split('/')[1]}`;
const size = buffer.byteLength;
const fullName = join(`${this.instance.id}`, messageRaw.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size, {
'Content-Type': mimetype,
});
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
}
}
this.logger.log(messageRaw);
this.sendDataWebhook(Events.SEND_MESSAGE, messageRaw);
@@ -375,6 +543,7 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
null,
isIntegration,
);
return res;
@@ -396,7 +565,7 @@ export class EvolutionStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
caption: mediaMessage?.caption,
@@ -407,9 +576,9 @@ export class EvolutionStartupService extends ChannelStartupService {
};
if (isURL(mediaMessage.media)) {
mimetype = mime.getType(mediaMessage.media);
mimetype = mimeTypes.lookup(mediaMessage.media);
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
}
prepareMedia.mimetype = mimetype;
@@ -439,33 +608,78 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
file,
isIntegration,
);
return mediaSent;
}
public async processAudio(audio: string, number: string) {
public async processAudio(audio: string, number: string, file: any) {
number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`;
let mimetype: string;
if (process.env.API_AUDIO_CONVERTER) {
try {
this.logger.verbose('Using audio converter API');
const formData = new FormData();
const prepareMedia: any = {
fileName: `${hash}.mp4`,
mediaType: 'audio',
media: audio,
};
if (file) {
formData.append('file', file.buffer, {
filename: file.originalname,
contentType: file.mimetype,
});
} else if (isURL(audio)) {
formData.append('url', audio);
} else {
formData.append('base64', audio);
}
if (isURL(audio)) {
mimetype = mime.getType(audio);
formData.append('format', 'mp4');
const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, {
headers: {
...formData.getHeaders(),
apikey: process.env.API_AUDIO_CONVERTER_KEY,
},
});
if (!response?.data?.audio) {
throw new InternalServerErrorException('Failed to convert audio');
}
const prepareMedia: any = {
fileName: `${hash}.mp4`,
mediaType: 'audio',
media: response?.data?.audio,
mimetype: 'audio/mpeg',
};
return prepareMedia;
} catch (error) {
this.logger.error(error?.response?.data || error);
throw new InternalServerErrorException(error?.response?.data?.message || error?.toString() || error);
}
} else {
mimetype = mime.getType(prepareMedia.fileName);
let mimetype: string;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
mediaType: 'audio',
media: audio,
mimetype: 'audio/mpeg',
};
if (isURL(audio)) {
mimetype = mimeTypes.lookup(audio).toString();
} else {
mimetype = mimeTypes.lookup(prepareMedia.fileName).toString();
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
prepareMedia.mimetype = mimetype;
return prepareMedia;
}
public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) {
@@ -478,7 +692,7 @@ export class EvolutionStartupService extends ChannelStartupService {
throw new Error('File or buffer is undefined.');
}
const message = await this.processAudio(mediaData.audio, data.number);
const message = await this.processAudio(mediaData.audio, data.number, file);
const audioSent = await this.sendMessageWithTyping(
data.number,
@@ -491,14 +705,34 @@ export class EvolutionStartupService extends ChannelStartupService {
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
file,
isIntegration,
);
return audioSent;
}
public async buttonMessage() {
throw new BadRequestException('Method not available on Evolution Channel');
public async buttonMessage(data: SendButtonsDto, isIntegration = false) {
return await this.sendMessageWithTyping(
data.number,
{
buttonMessage: {
title: data.title,
description: data.description,
footer: data.footer,
buttons: data.buttons,
},
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
null,
isIntegration,
);
}
public async locationMessage() {
throw new BadRequestException('Method not available on Evolution Channel');

View File

@@ -1,6 +1,5 @@
import { NumberBusiness } from '@api/dto/chat.dto';
import {
Button,
ContactMessage,
MediaMessage,
Options,
@@ -13,7 +12,6 @@ import {
SendReactionDto,
SendTemplateDto,
SendTextDto,
TypeButton,
} from '@api/dto/sendMessage.dto';
import * as s3Service from '@api/integrations/storage/s3/libs/minio.server';
import { ProviderFiles } from '@api/provider/sessions';
@@ -24,16 +22,15 @@ import { ChannelStartupService } from '@api/services/channel.service';
import { Events, wa } from '@api/types/wa.types';
import { Chatwoot, ConfigService, Database, Openai, S3, WaBusiness } from '@config/env.config';
import { BadRequestException, InternalServerErrorException } from '@exceptions';
import { createJid } from '@utils/createJid';
import { status } from '@utils/renderStatus';
import axios from 'axios';
import { proto } from 'baileys';
import { arrayUnique, isURL } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
import FormData from 'form-data';
import { createReadStream } from 'fs';
import mime from 'mime';
import mimeTypes from 'mime-types';
import { join } from 'path';
import { v4 } from 'uuid';
export class BusinessStartupService extends ChannelStartupService {
constructor(
@@ -74,6 +71,10 @@ export class BusinessStartupService extends ChannelStartupService {
await this.closeClient();
}
private isMediaMessage(message: any) {
return message.document || message.image || message.audio || message.video;
}
private async post(message: any, params: string) {
try {
let urlServer = this.configService.get<WaBusiness>('WA_BUSINESS').URL;
@@ -88,7 +89,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
return {
wuid: jid,
@@ -132,9 +133,7 @@ export class BusinessStartupService extends ChannelStartupService {
this.eventHandler(content);
this.phoneNumber = this.createJid(
content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id,
);
this.phoneNumber = createJid(content.messages ? content.messages[0].from : content.statuses[0]?.recipient_id);
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(error?.toString());
@@ -231,7 +230,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (!contact.phones[0]?.wa_id) {
contact.phones[0].wa_id = this.createJid(contact.phones[0].phone);
contact.phones[0].wa_id = createJid(contact.phones[0].phone);
}
result +=
@@ -305,12 +304,7 @@ export class BusinessStartupService extends ChannelStartupService {
remoteJid: this.phoneNumber,
fromMe: received.messages[0].from === received.metadata.phone_number_id,
};
if (
received?.messages[0].document ||
received?.messages[0].image ||
received?.messages[0].audio ||
received?.messages[0].video
) {
if (this.isMediaMessage(received?.messages[0])) {
messageRaw = {
key,
pushName,
@@ -335,15 +329,19 @@ export class BusinessStartupService extends ChannelStartupService {
const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' });
const mediaType = message.messages[0].document
? 'document'
: message.messages[0].image
? 'image'
: message.messages[0].audio
? 'audio'
: 'video';
let mediaType;
const mimetype = result.headers['content-type'];
if (message.messages[0].document) {
mediaType = 'document';
} else if (message.messages[0].image) {
mediaType = 'image';
} else if (message.messages[0].audio) {
mediaType = 'audio';
} else {
mediaType = 'video';
}
const mimetype = result.data?.mime_type || result.headers['content-type'];
const contentDisposition = result.headers['content-disposition'];
let fileName = `${message.messages[0].id}.${mimetype.split('/')[1]}`;
@@ -356,15 +354,19 @@ export class BusinessStartupService extends ChannelStartupService {
const size = result.headers['content-length'] || buffer.data.byteLength;
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer.data, size, {
'Content-Type': mimetype,
});
const createdMessage = await this.prismaRepository.message.create({
data: messageRaw,
});
await this.prismaRepository.media.create({
data: {
messageId: received.messages[0].id,
messageId: createdMessage.id,
instanceId: this.instanceId,
type: mediaType,
fileName: fullName,
@@ -375,6 +377,7 @@ export class BusinessStartupService extends ChannelStartupService {
const mediaUrl = await s3Service.getObjectUrl(fullName);
messageRaw.message.mediaUrl = mediaUrl;
messageRaw.message.base64 = buffer.data.toString('base64');
} catch (error) {
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
}
@@ -462,16 +465,23 @@ export class BusinessStartupService extends ChannelStartupService {
},
});
const audioMessage = received?.messages[0]?.audio;
if (
openAiDefaultSettings &&
openAiDefaultSettings.openaiCredsId &&
openAiDefaultSettings.speechToText &&
received?.message?.audioMessage
audioMessage
) {
messageRaw.message.speechToText = await this.openaiService.speechToText(
openAiDefaultSettings.OpenaiCreds,
received,
this.client.updateMediaMessage,
{
message: {
mediaUrl: messageRaw.message.mediaUrl,
...messageRaw,
},
},
() => {},
);
}
}
@@ -501,9 +511,11 @@ export class BusinessStartupService extends ChannelStartupService {
}
}
await this.prismaRepository.message.create({
data: messageRaw,
});
if (!this.isMediaMessage(received?.messages[0])) {
await this.prismaRepository.message.create({
data: messageRaw,
});
}
const contact = await this.prismaRepository.contact.findFirst({
where: { instanceId: this.instanceId, remoteJid: key.remoteJid },
@@ -787,6 +799,8 @@ export class BusinessStartupService extends ChannelStartupService {
return await this.post(content, 'messages');
}
if (message['media']) {
const isImage = message['mimetype']?.startsWith('image/');
content = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
@@ -795,6 +809,7 @@ export class BusinessStartupService extends ChannelStartupService {
[message['mediaType']]: {
[message['type']]: message['id'],
preview_url: linkPreview,
...(message['fileName'] && !isImage && { filename: message['fileName'] }),
caption: message['caption'],
},
};
@@ -899,7 +914,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
const messageRaw: any = {
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: this.createJid(number) },
key: { fromMe: true, id: messageSent?.messages[0]?.id, remoteJid: createJid(number) },
message: this.convertMessageToRaw(message, content),
messageType: this.renderMessageType(content.type),
messageTimestamp: (messageSent?.messages[0]?.timestamp as number) || Math.round(new Date().getTime() / 1000),
@@ -1001,7 +1016,7 @@ export class BusinessStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
caption: mediaMessage?.caption,
@@ -1012,11 +1027,11 @@ export class BusinessStartupService extends ChannelStartupService {
};
if (isURL(mediaMessage.media)) {
mimetype = mime.getType(mediaMessage.media);
mimetype = mimeTypes.lookup(mediaMessage.media);
prepareMedia.id = mediaMessage.media;
prepareMedia.type = 'link';
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
@@ -1059,7 +1074,7 @@ export class BusinessStartupService extends ChannelStartupService {
number = number.replace(/\D/g, '');
const hash = `${number}-${new Date().getTime()}`;
let mimetype: string;
let mimetype: string | false;
const prepareMedia: any = {
fileName: `${hash}.mp3`,
@@ -1068,11 +1083,11 @@ export class BusinessStartupService extends ChannelStartupService {
};
if (isURL(audio)) {
mimetype = mime.getType(audio);
mimetype = mimeTypes.lookup(audio);
prepareMedia.id = audio;
prepareMedia.type = 'link';
} else {
mimetype = mime.getType(prepareMedia.fileName);
mimetype = mimeTypes.lookup(prepareMedia.fileName);
const id = await this.getIdMedia(prepareMedia);
prepareMedia.id = id;
prepareMedia.type = 'id';
@@ -1088,6 +1103,9 @@ export class BusinessStartupService extends ChannelStartupService {
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');
@@ -1112,97 +1130,42 @@ export class BusinessStartupService extends ChannelStartupService {
return audioSent;
}
private toJSONString(button: Button): string {
const toString = (obj: any) => JSON.stringify(obj);
const json = {
call: () => toString({ display_text: button.displayText, phone_number: button.phoneNumber }),
reply: () => toString({ display_text: button.displayText, id: button.id }),
copy: () => toString({ display_text: button.displayText, copy_code: button.copyCode }),
url: () =>
toString({
display_text: button.displayText,
url: button.url,
merchant_url: button.url,
}),
};
return json[button.type]?.() || '';
}
private readonly mapType = new Map<TypeButton, string>([
['reply', 'quick_reply'],
['copy', 'cta_copy'],
['url', 'cta_url'],
['call', 'cta_call'],
]);
public async buttonMessage(data: SendButtonsDto) {
const generate = await (async () => {
if (data?.thumbnailUrl) {
return await this.prepareMediaMessage({
mediatype: 'image',
media: data.thumbnailUrl,
});
}
})();
const embeddedMedia: any = {};
const buttons = data.buttons.map((value) => {
return {
name: this.mapType.get(value.type),
buttonParamsJson: this.toJSONString(value),
};
});
const message: proto.IMessage = {
viewOnceMessage: {
message: {
messageContextInfo: {
deviceListMetadata: {},
deviceListMetadataVersion: 2,
},
interactiveMessage: {
body: {
text: (() => {
let t = '*' + data.title + '*';
if (data?.description) {
t += '\n\n';
t += data.description;
t += '\n';
}
return t;
})(),
},
footer: {
text: data?.footer,
},
header: (() => {
if (generate?.message?.imageMessage) {
return {
hasMediaAttachment: !!generate.message.imageMessage,
imageMessage: generate.message.imageMessage,
};
}
})(),
nativeFlowMessage: {
buttons: buttons,
messageParamsJson: JSON.stringify({
from: 'api',
templateId: v4(),
}),
},
},
},
},
const btnItems = {
text: data.buttons.map((btn) => btn.displayText),
ids: data.buttons.map((btn) => btn.id),
};
return await this.sendMessageWithTyping(data.number, message, {
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
});
if (!arrayUnique(btnItems.text) || !arrayUnique(btnItems.ids)) {
throw new BadRequestException('Button texts cannot be repeated', 'Button IDs cannot be repeated.');
}
return await this.sendMessageWithTyping(
data.number,
{
text: !embeddedMedia?.mediaKey ? data.title : undefined,
buttons: data.buttons.map((button) => {
return {
type: 'reply',
reply: {
title: button.displayText,
id: button.id,
},
};
}),
[embeddedMedia?.mediaKey]: embeddedMedia?.message,
},
{
delay: data?.delay,
presence: 'composing',
quoted: data?.quoted,
linkPreview: data?.linkPreview,
mentionsEveryOne: data?.mentionsEveryOne,
mentioned: data?.mentioned,
},
);
}
public async locationMessage(data: SendLocationDto) {
@@ -1310,7 +1273,7 @@ export class BusinessStartupService extends ChannelStartupService {
}
if (!contact.wuid) {
contact.wuid = this.createJid(contact.phoneNumber);
contact.wuid = createJid(contact.phoneNumber);
}
result += `item1.TEL;waid=${contact.wuid}:${contact.phoneNumber}\n` + 'item1.X-ABLabel:Celular\n' + 'END:VCARD';

View File

@@ -0,0 +1,60 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { WAMonitoringService } from '@api/services/monitor.service';
export class BaileysController {
constructor(private readonly waMonitor: WAMonitoringService) {}
public async onWhatsapp({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysOnWhatsapp(body?.jid);
}
public async profilePictureUrl({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysProfilePictureUrl(body?.jid, body?.type, body?.timeoutMs);
}
public async assertSessions({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysAssertSessions(body?.jids, body?.force);
}
public async createParticipantNodes({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysCreateParticipantNodes(body?.jids, body?.message, body?.extraAttrs);
}
public async getUSyncDevices({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetUSyncDevices(body?.jids, body?.useCache, body?.ignoreZeroDevices);
}
public async generateMessageTag({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGenerateMessageTag();
}
public async sendNode({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSendNode(body?.stanza);
}
public async signalRepositoryDecryptMessage({ instanceName }: InstanceDto, body: any) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysSignalRepositoryDecryptMessage(body?.jid, body?.type, body?.ciphertext);
}
public async getAuthState({ instanceName }: InstanceDto) {
const instance = this.waMonitor.waInstances[instanceName];
return instance.baileysGetAuthState();
}
}

View File

@@ -0,0 +1,105 @@
import { RouterBroker } from '@api/abstract/abstract.router';
import { InstanceDto } from '@api/dto/instance.dto';
import { HttpStatus } from '@api/routes/index.router';
import { baileysController } from '@api/server.module';
import { instanceSchema } from '@validate/instance.schema';
import { RequestHandler, Router } from 'express';
export class BaileysRouter extends RouterBroker {
constructor(...guards: RequestHandler[]) {
super();
this.router
.post(this.routerPath('onWhatsapp'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.onWhatsapp(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('profilePictureUrl'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.profilePictureUrl(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('assertSessions'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.assertSessions(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('createParticipantNodes'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.createParticipantNodes(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getUSyncDevices'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getUSyncDevices(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('generateMessageTag'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.generateMessageTag(instance),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('sendNode'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.sendNode(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('signalRepositoryDecryptMessage'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.signalRepositoryDecryptMessage(instance, req.body),
});
res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('getAuthState'), ...guards, async (req, res) => {
const response = await this.dataValidate<InstanceDto>({
request: req,
schema: instanceSchema,
ClassRef: InstanceDto,
execute: (instance) => baileysController.getAuthState(instance),
});
res.status(HttpStatus.OK).json(response);
});
}
public readonly router: Router = Router();
}

View File

@@ -0,0 +1,78 @@
import { BinaryNode, Contact, JidWithDevice, proto, WAConnectionState } from 'baileys';
export interface ServerToClientEvents {
withAck: (d: string, callback: (e: number) => void) => void;
onWhatsApp: onWhatsAppType;
profilePictureUrl: ProfilePictureUrlType;
assertSessions: AssertSessionsType;
createParticipantNodes: CreateParticipantNodesType;
getUSyncDevices: GetUSyncDevicesType;
generateMessageTag: GenerateMessageTagType;
sendNode: SendNodeType;
'signalRepository:decryptMessage': SignalRepositoryDecryptMessageType;
}
export interface ClientToServerEvents {
init: (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'CB:call': (packet: any) => void;
'CB:ack,class:call': (packet: any) => void;
'connection.update:status': (
me: Contact | undefined,
account: proto.IADVSignedDeviceIdentity | undefined,
status: WAConnectionState,
) => void;
'connection.update:qr': (qr: string) => void;
}
export type onWhatsAppType = (jid: string, callback: onWhatsAppCallback) => void;
export type onWhatsAppCallback = (
response: {
exists: boolean;
jid: string;
}[],
) => void;
export type ProfilePictureUrlType = (
jid: string,
type: 'image' | 'preview',
timeoutMs: number | undefined,
callback: ProfilePictureUrlCallback,
) => void;
export type ProfilePictureUrlCallback = (response: string | undefined) => void;
export type AssertSessionsType = (jids: string[], force: boolean, callback: AssertSessionsCallback) => void;
export type AssertSessionsCallback = (response: boolean) => void;
export type CreateParticipantNodesType = (
jids: string[],
message: any,
extraAttrs: any,
callback: CreateParticipantNodesCallback,
) => void;
export type CreateParticipantNodesCallback = (nodes: any, shouldIncludeDeviceIdentity: boolean) => void;
export type GetUSyncDevicesType = (
jids: string[],
useCache: boolean,
ignoreZeroDevices: boolean,
callback: GetUSyncDevicesTypeCallback,
) => void;
export type GetUSyncDevicesTypeCallback = (jids: JidWithDevice[]) => void;
export type GenerateMessageTagType = (callback: GenerateMessageTagTypeCallback) => void;
export type GenerateMessageTagTypeCallback = (response: string) => void;
export type SendNodeType = (stanza: BinaryNode, callback: SendNodeTypeCallback) => void;
export type SendNodeTypeCallback = (response: boolean) => void;
export type SignalRepositoryDecryptMessageType = (
jid: string,
type: 'pkmsg' | 'msg',
ciphertext: Buffer,
callback: SignalRepositoryDecryptMessageCallback,
) => void;
export type SignalRepositoryDecryptMessageCallback = (response: any) => void;

View File

@@ -0,0 +1,181 @@
import { ConnectionState, WAConnectionState, WASocket } from 'baileys';
import { io, Socket } from 'socket.io-client';
import { ClientToServerEvents, ServerToClientEvents } from './transport.type';
let baileys_connection_state: WAConnectionState = 'close';
export const useVoiceCallsBaileys = async (
wavoip_token: string,
baileys_sock: WASocket,
status?: WAConnectionState,
logger?: boolean,
) => {
baileys_connection_state = status ?? 'close';
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io('https://devices.wavoip.com/baileys', {
transports: ['websocket'],
path: `/${wavoip_token}/websocket`,
});
socket.on('connect', () => {
if (logger) console.log('[*] - Wavoip connected', socket.id);
socket.emit(
'init',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
baileys_connection_state,
);
});
socket.on('disconnect', () => {
if (logger) console.log('[*] - Wavoip disconnect');
});
socket.on('connect_error', (error) => {
if (socket.active) {
if (logger)
console.log(
'[*] - Wavoip connection error temporary failure, the socket will automatically try to reconnect',
error,
);
} else {
if (logger) console.log('[*] - Wavoip connection error', error.message);
}
});
socket.on('onWhatsApp', async (jid, callback) => {
try {
const response: any = await baileys_sock.onWhatsApp(jid);
callback(response);
if (logger) console.log('[*] Success on call onWhatsApp function', response, jid);
} catch (error) {
if (logger) console.error('[*] Error on call onWhatsApp function', error);
}
});
socket.on('profilePictureUrl', async (jid, type, timeoutMs, callback) => {
try {
const response = await baileys_sock.profilePictureUrl(jid, type, timeoutMs);
callback(response);
if (logger) console.log('[*] Success on call profilePictureUrl function', response);
} catch (error) {
if (logger) console.error('[*] Error on call profilePictureUrl function', error);
}
});
socket.on('assertSessions', async (jids, force, callback) => {
try {
const response = await baileys_sock.assertSessions(jids, force);
callback(response);
if (logger) console.log('[*] Success on call assertSessions function', response);
} catch (error) {
if (logger) console.error('[*] Error on call assertSessions function', error);
}
});
socket.on('createParticipantNodes', async (jids, message, extraAttrs, callback) => {
try {
const response = await baileys_sock.createParticipantNodes(jids, message, extraAttrs);
callback(response, true);
if (logger) console.log('[*] Success on call createParticipantNodes function', response);
} catch (error) {
if (logger) console.error('[*] Error on call createParticipantNodes function', error);
}
});
socket.on('getUSyncDevices', async (jids, useCache, ignoreZeroDevices, callback) => {
try {
const response = await baileys_sock.getUSyncDevices(jids, useCache, ignoreZeroDevices);
callback(response);
if (logger) console.log('[*] Success on call getUSyncDevices function', response);
} catch (error) {
if (logger) console.error('[*] Error on call getUSyncDevices function', error);
}
});
socket.on('generateMessageTag', async (callback) => {
try {
const response = await baileys_sock.generateMessageTag();
callback(response);
if (logger) console.log('[*] Success on call generateMessageTag function', response);
} catch (error) {
if (logger) console.error('[*] Error on call generateMessageTag function', error);
}
});
socket.on('sendNode', async (stanza, callback) => {
try {
console.log('sendNode', JSON.stringify(stanza));
const response = await baileys_sock.sendNode(stanza);
callback(true);
if (logger) console.log('[*] Success on call sendNode function', response);
} catch (error) {
if (logger) console.error('[*] Error on call sendNode function', error);
}
});
socket.on('signalRepository:decryptMessage', async (jid, type, ciphertext, callback) => {
try {
const response = await baileys_sock.signalRepository.decryptMessage({
jid: jid,
type: type,
ciphertext: ciphertext,
});
callback(response);
if (logger) console.log('[*] Success on call signalRepository:decryptMessage function', response);
} catch (error) {
if (logger) console.error('[*] Error on call signalRepository:decryptMessage function', error);
}
});
// we only use this connection data to inform the webphone that the device is connected and creeds account to generate e2e whatsapp key for make call packets
baileys_sock.ev.on('connection.update', (update: Partial<ConnectionState>) => {
const { connection } = update;
if (connection) {
baileys_connection_state = connection;
socket
.timeout(1000)
.emit(
'connection.update:status',
baileys_sock.authState.creds.me,
baileys_sock.authState.creds.account,
connection,
);
}
if (update.qr) {
socket.timeout(1000).emit('connection.update:qr', update.qr);
}
});
baileys_sock.ws.on('CB:call', (packet) => {
if (logger) console.log('[*] Signling received');
socket.volatile.timeout(1000).emit('CB:call', packet);
});
baileys_sock.ws.on('CB:ack,class:call', (packet) => {
if (logger) console.log('[*] Signling ack received');
socket.volatile.timeout(1000).emit('CB:ack,class:call', packet);
});
return socket;
};

View File

@@ -184,7 +184,6 @@ export class ChatbotController {
public async findBotTrigger(
botRepository: any,
settingsRepository: any,
content: string,
instance: InstanceDto,
session?: IntegrationSession,
@@ -192,7 +191,7 @@ export class ChatbotController {
let findBot: null;
if (!session) {
findBot = await findBotByTrigger(botRepository, settingsRepository, content, instance.instanceId);
findBot = await findBotByTrigger(botRepository, content, instance.instanceId);
if (!findBot) {
return;

View File

@@ -28,7 +28,7 @@ import dayjs from 'dayjs';
import FormData from 'form-data';
import Jimp from 'jimp';
import Long from 'long';
import mime from 'mime';
import mimeTypes from 'mime-types';
import path from 'path';
import { Readable } from 'stream';
@@ -401,7 +401,6 @@ export class ChatwootService {
return true;
} catch (error) {
this.logger.error(error);
return false;
}
}
@@ -705,7 +704,7 @@ export class ChatwootService {
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
if (this.provider.conversationPending) {
if (this.provider.conversationPending && conversation.status !== 'open') {
if (conversation) {
await client.conversations.toggleStatus({
accountId: this.provider.accountId,
@@ -933,9 +932,11 @@ export class ChatwootService {
) {
if (sourceId && this.isImportHistoryAvailable()) {
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId]);
if (messageAlreadySaved.size > 0) {
this.logger.warn('Message already saved on chatwoot');
return null;
if (messageAlreadySaved) {
if (messageAlreadySaved.size > 0) {
this.logger.warn('Message already saved on chatwoot');
return null;
}
}
}
const data = new FormData();
@@ -1065,7 +1066,7 @@ export class ChatwootService {
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
try {
const parsedMedia = path.parse(decodeURIComponent(media));
let mimeType = mime.getType(parsedMedia?.ext) || '';
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
let fileName = parsedMedia?.name + parsedMedia?.ext;
if (!mimeType) {
@@ -1957,7 +1958,7 @@ export class ChatwootService {
}
if (!nameFile) {
nameFile = `${Math.random().toString(36).substring(7)}.${mime.getExtension(downloadBase64.mimetype) || ''}`;
nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`;
}
const fileData = Buffer.from(downloadBase64.base64, 'base64');
@@ -1969,11 +1970,21 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
if (!body.key.fromMe) {
content = `**${participantName}:**\n\n${bodyMessage}`;
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
} else {
content = `${bodyMessage}`;
}
@@ -2046,8 +2057,8 @@ export class ChatwootService {
if (isAdsMessage) {
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
const extension = mime.getExtension(imgBuffer.headers['content-type']);
const mimeType = extension && mime.getType(extension);
const extension = mimeTypes.extension(imgBuffer.headers['content-type']);
const mimeType = extension && mimeTypes.lookup(extension);
if (!mimeType) {
this.logger.warn('mimetype of Ads message not found');
@@ -2055,7 +2066,7 @@ export class ChatwootService {
}
const random = Math.random().toString(36).substring(7);
const nameFile = `${random}.${mime.getExtension(mimeType)}`;
const nameFile = `${random}.${mimeTypes.extension(mimeType)}`;
const fileData = Buffer.from(imgBuffer.data, 'binary');
const img = await Jimp.read(fileData);
@@ -2098,11 +2109,21 @@ export class ChatwootService {
if (body.key.remoteJid.includes('@g.us')) {
const participantName = body.pushName;
const rawPhoneNumber = body.key.participant.split('@')[0];
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
let formattedPhoneNumber: string;
if (phoneMatch) {
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
} else {
formattedPhoneNumber = `+${rawPhoneNumber}`;
}
let content: string;
if (!body.key.fromMe) {
content = `**${participantName}**\n\n${bodyMessage}`;
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
} else {
content = `${bodyMessage}`;
}
@@ -2442,57 +2463,61 @@ export class ChatwootService {
chatwootConfig: ChatwootDto,
prepareMessage: (message: any) => any,
) {
if (!this.isImportHistoryAvailable()) {
return;
}
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
return;
}
const inbox = await this.getInbox(instance);
const sqlMessages = `select * from messages m
where account_id = ${chatwootConfig.accountId}
and inbox_id = ${inbox.id}
and created_at >= now() - interval '6h'
order by created_at desc`;
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
const ids: string[] = messagesData
.filter((message) => !!message.source_id)
.map((message) => message.source_id.replace('WAID:', ''));
const savedMessages = await this.prismaRepository.message.findMany({
where: {
Instance: { name: instance.instanceName },
messageTimestamp: { gte: dayjs().subtract(6, 'hours').unix() },
AND: ids.map((id) => ({ key: { path: ['id'], not: id } })),
},
});
const filteredMessages = savedMessages.filter(
(msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid),
);
const messagesRaw: any[] = [];
for (const m of filteredMessages) {
if (!m.message || !m.key || !m.messageTimestamp) {
continue;
try {
if (!this.isImportHistoryAvailable()) {
return;
}
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
return;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
const inbox = await this.getInbox(instance);
const sqlMessages = `select * from messages m
where account_id = ${chatwootConfig.accountId}
and inbox_id = ${inbox.id}
and created_at >= now() - interval '6h'
order by created_at desc`;
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
const ids: string[] = messagesData
.filter((message) => !!message.source_id)
.map((message) => message.source_id.replace('WAID:', ''));
const savedMessages = await this.prismaRepository.message.findMany({
where: {
Instance: { name: instance.instanceName },
messageTimestamp: { gte: dayjs().subtract(6, 'hours').unix() },
AND: ids.map((id) => ({ key: { path: ['id'], not: id } })),
},
});
const filteredMessages = savedMessages.filter(
(msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid),
);
const messagesRaw: any[] = [];
for (const m of filteredMessages) {
if (!m.message || !m.key || !m.messageTimestamp) {
continue;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
messagesRaw.push(prepareMessage(m as any));
}
messagesRaw.push(prepareMessage(m as any));
this.addHistoryMessages(
instance,
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
);
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
} catch (error) {
return;
}
this.addHistoryMessages(
instance,
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
);
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
waInstance.clearCacheChatwoot();
}
}

View File

@@ -170,22 +170,26 @@ class ChatwootImport {
}
public async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
const existingSourceIdsSet = new Set<string>();
try {
const existingSourceIdsSet = new Set<string>();
if (sourceIds.length === 0) {
return existingSourceIdsSet;
}
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
const pgClient = postgresClient.getChatwootConnection();
const result = await pgClient.query(query, [formattedSourceIds]);
for (const row of result.rows) {
existingSourceIdsSet.add(row.source_id);
}
if (sourceIds.length === 0) {
return existingSourceIdsSet;
} catch (error) {
return null;
}
const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); // Make sure the sourceId is always formatted as WAID:1234567890
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
const pgClient = postgresClient.getChatwootConnection();
const result = await pgClient.query(query, [formattedSourceIds]);
for (const row of result.rows) {
existingSourceIdsSet.add(row.source_id);
}
return existingSourceIdsSet;
}
public async importHistoryMessages(

View File

@@ -64,17 +64,25 @@ export class DifyController extends ChatbotController implements ChatbotControll
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -748,13 +756,7 @@ export class DifyController extends ChatbotController implements ChatbotControll
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as DifyModel;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as DifyModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
@@ -788,15 +790,15 @@ export class DifyController extends ChatbotController implements ChatbotControll
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -428,8 +428,8 @@ export class DifyService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {

View File

@@ -60,17 +60,25 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -720,13 +728,7 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as EvolutionBot;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as EvolutionBot;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
@@ -760,15 +762,15 @@ export class EvolutionBotController extends ChatbotController implements Chatbot
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -190,8 +190,8 @@ export class EvolutionBotService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -274,8 +274,8 @@ export class EvolutionBotService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -60,17 +60,25 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
},
});
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -720,13 +728,7 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as Flowise;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as Flowise;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
@@ -760,15 +762,15 @@ export class FlowiseController extends ChatbotController implements ChatbotContr
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -189,8 +189,8 @@ export class FlowiseService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -273,8 +273,8 @@ export class FlowiseService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -201,18 +201,25 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
},
});
if (!data.openaiCredsId) data.openaiCredsId = defaultSettingCheck?.openaiCredsId || null;
if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '';
if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || '';
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false;
if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0;
if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || [];
if (!data.splitMessages) data.splitMessages = defaultSettingCheck?.splitMessages || false;
if (!data.timePerChar) data.timePerChar = defaultSettingCheck?.timePerChar || 0;
if (data.expire === undefined || data.expire === null) data.expire = defaultSettingCheck.expire;
if (data.keywordFinish === undefined || data.keywordFinish === null)
data.keywordFinish = defaultSettingCheck.keywordFinish;
if (data.delayMessage === undefined || data.delayMessage === null)
data.delayMessage = defaultSettingCheck.delayMessage;
if (data.unknownMessage === undefined || data.unknownMessage === null)
data.unknownMessage = defaultSettingCheck.unknownMessage;
if (data.listeningFromMe === undefined || data.listeningFromMe === null)
data.listeningFromMe = defaultSettingCheck.listeningFromMe;
if (data.stopBotFromMe === undefined || data.stopBotFromMe === null)
data.stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (data.keepOpen === undefined || data.keepOpen === null) data.keepOpen = defaultSettingCheck.keepOpen;
if (data.debounceTime === undefined || data.debounceTime === null)
data.debounceTime = defaultSettingCheck.debounceTime;
if (data.ignoreJids === undefined || data.ignoreJids === null) data.ignoreJids = defaultSettingCheck.ignoreJids;
if (data.splitMessages === undefined || data.splitMessages === null)
data.splitMessages = defaultSettingCheck?.splitMessages ?? false;
if (data.timePerChar === undefined || data.timePerChar === null)
data.timePerChar = defaultSettingCheck?.timePerChar ?? 0;
if (!data.openaiCredsId) {
throw new Error('Openai Creds Id is required');
@@ -958,13 +965,7 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as OpenaiBot;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as OpenaiBot;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
@@ -998,15 +999,15 @@ export class OpenaiController extends ChatbotController implements ChatbotContro
let splitMessages = findBot?.splitMessages;
let timePerChar = findBot?.timePerChar;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (splitMessages === undefined || splitMessages === null) splitMessages = settings?.splitMessages ?? false;
if (timePerChar === undefined || timePerChar === null) timePerChar = settings?.timePerChar ?? 0;

View File

@@ -234,8 +234,8 @@ export class OpenaiService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
if (mediaType === 'audio') {
@@ -318,8 +318,8 @@ export class OpenaiService {
},
false,
);
textBuffer = '';
}
textBuffer = '';
}
sendTelemetry('/message/sendText');

View File

@@ -570,6 +570,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
let listeningFromMe = data?.typebot?.listeningFromMe;
let stopBotFromMe = data?.typebot?.stopBotFromMe;
let keepOpen = data?.typebot?.keepOpen;
let debounceTime = data?.typebot?.debounceTime;
let ignoreJids = data?.typebot?.ignoreJids;
const defaultSettingCheck = await this.settingsRepository.findFirst({
where: {
@@ -586,15 +588,20 @@ export class TypebotController extends ChatbotController implements ChatbotContr
!unknownMessage ||
!listeningFromMe ||
!stopBotFromMe ||
!keepOpen
!keepOpen ||
!debounceTime ||
!ignoreJids
) {
if (!expire) expire = defaultSettingCheck?.expire || 0;
if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR';
if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000;
if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi';
if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false;
if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false;
if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false;
if (expire === undefined || expire === null) expire = defaultSettingCheck.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = defaultSettingCheck.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = defaultSettingCheck.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = defaultSettingCheck.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null)
listeningFromMe = defaultSettingCheck.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = defaultSettingCheck.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = defaultSettingCheck.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = defaultSettingCheck.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = defaultSettingCheck.ignoreJids;
if (!defaultSettingCheck) {
await this.settings(instance, {
@@ -605,6 +612,8 @@ export class TypebotController extends ChatbotController implements ChatbotContr
listeningFromMe: listeningFromMe,
stopBotFromMe: stopBotFromMe,
keepOpen: keepOpen,
debounceTime: debounceTime,
ignoreJids: ignoreJids,
});
}
}
@@ -934,13 +943,7 @@ export class TypebotController extends ChatbotController implements ChatbotContr
const content = getConversationMessage(msg);
let findBot = (await this.findBotTrigger(
this.botRepository,
this.settingsRepository,
content,
instance,
session,
)) as TypebotModel;
let findBot = (await this.findBotTrigger(this.botRepository, content, instance, session)) as TypebotModel;
if (!findBot) {
const fallback = await this.settingsRepository.findFirst({
@@ -980,15 +983,15 @@ export class TypebotController extends ChatbotController implements ChatbotContr
let debounceTime = findBot?.debounceTime;
let ignoreJids = findBot?.ignoreJids;
if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish;
if (!delayMessage) delayMessage = settings.delayMessage;
if (!unknownMessage) unknownMessage = settings.unknownMessage;
if (!listeningFromMe) listeningFromMe = settings.listeningFromMe;
if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe;
if (!keepOpen) keepOpen = settings.keepOpen;
if (!debounceTime) debounceTime = settings.debounceTime;
if (!ignoreJids) ignoreJids = settings.ignoreJids;
if (expire === undefined || expire === null) expire = settings.expire;
if (keywordFinish === undefined || keywordFinish === null) keywordFinish = settings.keywordFinish;
if (delayMessage === undefined || delayMessage === null) delayMessage = settings.delayMessage;
if (unknownMessage === undefined || unknownMessage === null) unknownMessage = settings.unknownMessage;
if (listeningFromMe === undefined || listeningFromMe === null) listeningFromMe = settings.listeningFromMe;
if (stopBotFromMe === undefined || stopBotFromMe === null) stopBotFromMe = settings.stopBotFromMe;
if (keepOpen === undefined || keepOpen === null) keepOpen = settings.keepOpen;
if (debounceTime === undefined || debounceTime === null) debounceTime = settings.debounceTime;
if (ignoreJids === undefined || ignoreJids === null) ignoreJids = settings.ignoreJids;
if (this.checkIgnoreJids(ignoreJids, remoteJid)) return;

View File

@@ -223,14 +223,130 @@ export class TypebotService {
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage(
{
if (formattedText.includes('[list]')) {
const listJson = {
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
title: '',
description: '',
buttonText: '',
footerText: '',
sections: [],
};
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/);
const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/);
const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/);
if (titleMatch) listJson.title = titleMatch[1].trim();
if (descriptionMatch) listJson.description = descriptionMatch[1].trim();
if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim();
if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim();
const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1];
if (menuContent) {
const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g);
if (sections) {
sections.forEach((section) => {
const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim();
const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g);
const sectionData = {
title: sectionTitle,
rows:
rows?.map((row) => ({
title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(),
description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(),
rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(),
})) || [],
};
listJson.sections.push(sectionData);
});
}
}
await instance.listMessage(listJson);
} else if (formattedText.includes('[buttons]')) {
const buttonJson = {
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
footer: '',
buttons: [],
};
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
}
}
}
await instance.buttonMessage(buttonJson);
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
}
sendTelemetry('/message/sendText');
}
@@ -299,14 +415,130 @@ export class TypebotService {
formattedText = formattedText.replace(/\n$/, '');
await instance.textMessage(
{
if (formattedText.includes('[list]')) {
const listJson = {
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
title: '',
description: '',
buttonText: '',
footerText: '',
sections: [],
};
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[buttonText\])/);
const buttonTextMatch = formattedText.match(/\[buttonText\]([\s\S]*?)(?=\[footerText\])/);
const footerTextMatch = formattedText.match(/\[footerText\]([\s\S]*?)(?=\[menu\])/);
if (titleMatch) listJson.title = titleMatch[1].trim();
if (descriptionMatch) listJson.description = descriptionMatch[1].trim();
if (buttonTextMatch) listJson.buttonText = buttonTextMatch[1].trim();
if (footerTextMatch) listJson.footerText = footerTextMatch[1].trim();
const menuContent = formattedText.match(/\[menu\]([\s\S]*?)\[\/menu\]/)?.[1];
if (menuContent) {
const sections = menuContent.match(/\[section\]([\s\S]*?)(?=\[section\]|\[\/section\]|\[\/menu\])/g);
if (sections) {
sections.forEach((section) => {
const sectionTitle = section.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim();
const rows = section.match(/\[row\]([\s\S]*?)(?=\[row\]|\[\/row\]|\[\/section\]|\[\/menu\])/g);
const sectionData = {
title: sectionTitle,
rows:
rows?.map((row) => ({
title: row.match(/title: (.*?)(?:\n|$)/)?.[1]?.trim(),
description: row.match(/description: (.*?)(?:\n|$)/)?.[1]?.trim(),
rowId: row.match(/rowId: (.*?)(?:\n|$)/)?.[1]?.trim(),
})) || [],
};
listJson.sections.push(sectionData);
});
}
}
await instance.listMessage(listJson);
} else if (formattedText.includes('[buttons]')) {
const buttonJson = {
number: remoteJid.split('@')[0],
thumbnailUrl: undefined,
title: '',
description: '',
footer: '',
buttons: [],
};
const thumbnailUrlMatch = formattedText.match(/\[thumbnailUrl\]([\s\S]*?)(?=\[title\])/);
const titleMatch = formattedText.match(/\[title\]([\s\S]*?)(?=\[description\])/);
const descriptionMatch = formattedText.match(/\[description\]([\s\S]*?)(?=\[footer\])/);
const footerMatch = formattedText.match(/\[footer\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url))/);
if (titleMatch) buttonJson.title = titleMatch[1].trim();
if (thumbnailUrlMatch) buttonJson.thumbnailUrl = thumbnailUrlMatch[1].trim();
if (descriptionMatch) buttonJson.description = descriptionMatch[1].trim();
if (footerMatch) buttonJson.footer = footerMatch[1].trim();
const buttonTypes = {
reply: /\[reply\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
pix: /\[pix\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
copy: /\[copy\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
call: /\[call\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
url: /\[url\]([\s\S]*?)(?=\[(?:reply|pix|copy|call|url)|$)/g,
};
for (const [type, pattern] of Object.entries(buttonTypes)) {
let match;
while ((match = pattern.exec(formattedText)) !== null) {
const content = match[1].trim();
const button: any = { type };
switch (type) {
case 'pix':
button.currency = content.match(/currency: (.*?)(?:\n|$)/)?.[1]?.trim();
button.name = content.match(/name: (.*?)(?:\n|$)/)?.[1]?.trim();
button.keyType = content.match(/keyType: (.*?)(?:\n|$)/)?.[1]?.trim();
button.key = content.match(/key: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'reply':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.id = content.match(/id: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'copy':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.copyCode = content.match(/copyCode: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'call':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.phoneNumber = content.match(/phone: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
case 'url':
button.displayText = content.match(/displayText: (.*?)(?:\n|$)/)?.[1]?.trim();
button.url = content.match(/url: (.*?)(?:\n|$)/)?.[1]?.trim();
break;
}
if (Object.keys(button).length > 1) {
buttonJson.buttons.push(button);
}
}
}
await instance.buttonMessage(buttonJson);
} else {
await instance.textMessage(
{
number: remoteJid.split('@')[0],
delay: settings?.delayMessage || 1000,
text: formattedText,
},
false,
);
}
sendTelemetry('/message/sendText');
}

View File

@@ -13,6 +13,7 @@ export type EmitData = {
sender: string;
apiKey?: string;
local?: boolean;
integration?: string[];
};
export interface EventControllerInterface {
@@ -23,7 +24,7 @@ export interface EventControllerInterface {
export class EventController {
public prismaRepository: PrismaRepository;
private waMonitor: WAMonitoringService;
protected waMonitor: WAMonitoringService;
private integrationStatus: boolean;
private integrationName: string;

View File

@@ -99,6 +99,7 @@ export class EventManager {
sender: string;
apiKey?: string;
local?: boolean;
integration?: string[];
}): Promise<void> {
await this.websocket.emit(eventData);
await this.rabbitmq.emit(eventData);

View File

@@ -120,7 +120,11 @@ export class PusherController extends EventController implements EventController
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('pusher')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -73,7 +73,12 @@ export class RabbitmqController extends EventController implements EventControll
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('rabbitmq')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -54,7 +54,12 @@ export class SqsController extends EventController implements EventControllerInt
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('sqs')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -5,7 +5,7 @@ import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import axios from 'axios';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
@@ -64,13 +64,14 @@ export class WebhookController extends EventController implements EventControlle
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
if (!instance || !instance?.enabled) {
if (integration && !integration.includes('webhook')) {
return;
}
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
@@ -82,14 +83,14 @@ export class WebhookController extends EventController implements EventControlle
event,
instance: instanceName,
data,
destination: instance?.url,
destination: instance?.url || `${webhookConfig.GLOBAL.URL}/${transformedWe}`,
date_time: dateTime,
sender,
server_url: serverUrl,
apikey: apiKey,
};
if (local) {
if (local && instance?.enabled) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string;
@@ -116,12 +117,12 @@ export class WebhookController extends EventController implements EventControlle
headers: webhookHeaders as Record<string, string> | undefined,
});
await httpService.post('', webhookData);
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook`,
message: error?.message,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -157,12 +158,18 @@ export class WebhookController extends EventController implements EventControlle
if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await httpService.post('', webhookData);
await this.retryWebhookRequest(
httpService,
webhookData,
`${origin}.sendData-Webhook-Global`,
globalURL,
serverUrl,
);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook-Global`,
message: error?.message,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
@@ -176,4 +183,51 @@ export class WebhookController extends EventController implements EventControlle
}
}
}
private async retryWebhookRequest(
httpService: AxiosInstance,
webhookData: any,
origin: string,
baseURL: string,
serverUrl: string,
maxRetries = 10,
delaySeconds = 30,
): Promise<void> {
let attempts = 0;
while (attempts < maxRetries) {
try {
await httpService.post('', webhookData);
if (attempts > 0) {
this.logger.log({
local: `${origin}`,
message: `Sucesso no envio após ${attempts + 1} tentativas`,
url: baseURL,
});
}
return;
} catch (error) {
attempts++;
this.logger.error({
local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: baseURL,
server_url: serverUrl,
});
if (attempts === maxRetries) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
}
}
}
}

View File

@@ -8,7 +8,10 @@ import { instanceSchema, webhookSchema } from '@validate/validate.schema';
import { RequestHandler, Router } from 'express';
export class WebhookRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
super();
this.router
.post(this.routerPath('set'), ...guards, async (req, res) => {

View File

@@ -35,6 +35,16 @@ export class WebsocketController extends EventController implements EventControl
socket.on('disconnect', () => {
this.logger.info('User disconnected');
});
socket.on('sendNode', async (data) => {
try {
await this.waMonitor.waInstances[data.instanceId].baileysSendNode(data.stanza);
this.logger.info('Node sent successfully');
} catch (error) {
this.logger.error('Error sending node:');
this.logger.error(error);
}
});
});
this.logger.info('Socket.io initialized');
@@ -65,7 +75,12 @@ export class WebsocketController extends EventController implements EventControl
dateTime,
sender,
apiKey,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('websocket')) {
return;
}
if (!this.status) {
return;
}

View File

@@ -8,7 +8,7 @@ import { StorageRouter } from '@api/integrations/storage/storage.router';
import { configService } from '@config/env.config';
import { Router } from 'express';
import fs from 'fs';
import mime from 'mime';
import mimeTypes from 'mime-types';
import path from 'path';
import { CallRouter } from './call.router';
@@ -49,7 +49,7 @@ router.get('/assets/*', (req, res) => {
const filePath = path.join(basePath, 'assets/', fileName);
if (fs.existsSync(filePath)) {
res.set('Content-Type', mime.getType(filePath) || 'text/css');
res.set('Content-Type', mimeTypes.lookup(filePath) || 'text/css');
res.send(fs.readFileSync(filePath));
} else {
res.status(404).send('File not found');
@@ -87,7 +87,7 @@ router
.use('/settings', new SettingsRouter(...guards).router)
.use('/proxy', new ProxyRouter(...guards).router)
.use('/label', new LabelRouter(...guards).router)
.use('', new ChannelRouter(configService).router)
.use('', new ChannelRouter(configService, ...guards).router)
.use('', new EventRouter(configService, ...guards).router)
.use('', new ChatbotRouter(...guards).router)
.use('', new StorageRouter(...guards).router);

View File

@@ -8,10 +8,14 @@ import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class InstanceRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
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

@@ -7,6 +7,7 @@ import {
SendLocationDto,
SendMediaDto,
SendPollDto,
SendPtvDto,
SendReactionDto,
SendStatusDto,
SendStickerDto,
@@ -22,6 +23,7 @@ import {
locationMessageSchema,
mediaMessageSchema,
pollMessageSchema,
ptvMessageSchema,
reactionMessageSchema,
statusMessageSchema,
stickerMessageSchema,
@@ -71,6 +73,18 @@ export class MessageRouter extends RouterBroker {
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendPtv'), ...guards, upload.single('file'), async (req, res) => {
const bodyData = req.body;
const response = await this.dataValidate<SendPtvDto>({
request: req,
schema: ptvMessageSchema,
ClassRef: SendPtvDto,
execute: (instance) => sendMessageController.sendPtv(instance, bodyData, req.file as any),
});
return res.status(HttpStatus.CREATED).json(response);
})
.post(this.routerPath('sendWhatsAppAudio'), ...guards, upload.single('file'), async (req, res) => {
const bodyData = req.body;

View File

@@ -9,7 +9,10 @@ import { RequestHandler, Router } from 'express';
import { HttpStatus } from './index.router';
export class TemplateRouter extends RouterBroker {
constructor(readonly configService: ConfigService, ...guards: RequestHandler[]) {
constructor(
readonly configService: ConfigService,
...guards: RequestHandler[]
) {
super();
this.router
.post(this.routerPath('create'), ...guards, async (req, res) => {

View File

@@ -15,6 +15,7 @@ import { TemplateController } from './controllers/template.controller';
import { ChannelController } from './integrations/channel/channel.controller';
import { EvolutionController } from './integrations/channel/evolution/evolution.controller';
import { MetaController } from './integrations/channel/meta/meta.controller';
import { BaileysController } from './integrations/channel/whatsapp/baileys.controller';
import { ChatbotController } from './integrations/chatbot/chatbot.controller';
import { ChatwootController } from './integrations/chatbot/chatwoot/controllers/chatwoot.controller';
import { ChatwootService } from './integrations/chatbot/chatwoot/services/chatwoot.service';
@@ -107,7 +108,7 @@ export const channelController = new ChannelController(prismaRepository, waMonit
// channels
export const evolutionController = new EvolutionController(prismaRepository, waMonitor);
export const metaController = new MetaController(prismaRepository, waMonitor);
export const baileysController = new BaileysController(waMonitor);
// chatbots
const typebotService = new TypebotService(waMonitor, configService, prismaRepository);
export const typebotController = new TypebotController(typebotService, prismaRepository, waMonitor);

View File

@@ -12,7 +12,8 @@ import { Events, wa } from '@api/types/wa.types';
import { Auth, Chatwoot, ConfigService, HttpServer } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { NotFoundException } from '@exceptions';
import { Contact, Message } from '@prisma/client';
import { Contact, Message, Prisma } from '@prisma/client';
import { createJid } from '@utils/createJid';
import { WASocket } from 'baileys';
import { isArray } from 'class-validator';
import EventEmitter2 from 'eventemitter2';
@@ -151,6 +152,7 @@ export class ChannelStartupService {
this.localSettings.readMessages = data?.readMessages;
this.localSettings.readStatus = data?.readStatus;
this.localSettings.syncFullHistory = data?.syncFullHistory;
this.localSettings.wavoipToken = data?.wavoipToken;
}
public async setSettings(data: SettingsDto) {
@@ -166,6 +168,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
},
create: {
rejectCall: data.rejectCall,
@@ -175,6 +178,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
instanceId: this.instanceId,
},
});
@@ -186,6 +190,12 @@ export class ChannelStartupService {
this.localSettings.readMessages = data?.readMessages;
this.localSettings.readStatus = data?.readStatus;
this.localSettings.syncFullHistory = data?.syncFullHistory;
this.localSettings.wavoipToken = data?.wavoipToken;
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
this.client.ws.close();
this.client.ws.connect();
}
}
public async findSettings() {
@@ -207,6 +217,7 @@ export class ChannelStartupService {
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
};
}
@@ -419,7 +430,7 @@ export class ChannelStartupService {
return data;
}
public async sendDataWebhook<T = any>(event: Events, data: T, local = true) {
public async sendDataWebhook<T = any>(event: Events, data: T, local = true, integration?: string[]) {
const serverUrl = this.configService.get<HttpServer>('SERVER').URL;
const tzoffset = new Date().getTimezoneOffset() * 60000; //offset in milliseconds
const localISOTime = new Date(Date.now() - tzoffset).toISOString();
@@ -439,6 +450,7 @@ export class ChannelStartupService {
sender: this.wuid,
apiKey: expose && instanceApikey ? instanceApikey : null,
local,
integration,
});
}
@@ -476,47 +488,11 @@ export class ChannelStartupService {
}
}
public createJid(number: string): string {
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
return number;
}
if (number.includes('@broadcast')) {
return number;
}
number = number
?.replace(/\s/g, '')
.replace(/\+/g, '')
.replace(/\(/g, '')
.replace(/\)/g, '')
.split(':')[0]
.split('@')[0];
if (number.includes('-') && number.length >= 24) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = number.replace(/\D/g, '');
if (number.length >= 18) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = this.formatMXOrARNumber(number);
number = this.formatBRNumber(number);
return `${number}@s.whatsapp.net`;
}
public async fetchContacts(query: Query<Contact>) {
const remoteJid = query?.where?.remoteJid
? query?.where?.remoteJid.includes('@')
? query.where?.remoteJid
: this.createJid(query.where?.remoteJid)
: createJid(query.where?.remoteJid)
: null;
const where = {
@@ -532,6 +508,64 @@ export class ChannelStartupService {
});
}
public cleanMessageData(message: any) {
if (!message) return message;
const cleanedMessage = { ...message };
const mediaUrl = cleanedMessage.message.mediaUrl;
delete cleanedMessage.message.base64;
if (cleanedMessage.message) {
// Limpa imageMessage
if (cleanedMessage.message.imageMessage) {
cleanedMessage.message.imageMessage = {
caption: cleanedMessage.message.imageMessage.caption,
};
}
// Limpa videoMessage
if (cleanedMessage.message.videoMessage) {
cleanedMessage.message.videoMessage = {
caption: cleanedMessage.message.videoMessage.caption,
};
}
// Limpa audioMessage
if (cleanedMessage.message.audioMessage) {
cleanedMessage.message.audioMessage = {
seconds: cleanedMessage.message.audioMessage.seconds,
};
}
// Limpa stickerMessage
if (cleanedMessage.message.stickerMessage) {
cleanedMessage.message.stickerMessage = {};
}
// Limpa documentMessage
if (cleanedMessage.message.documentMessage) {
cleanedMessage.message.documentMessage = {
caption: cleanedMessage.message.documentMessage.caption,
name: cleanedMessage.message.documentMessage.name,
};
}
// Limpa documentWithCaptionMessage
if (cleanedMessage.message.documentWithCaptionMessage) {
cleanedMessage.message.documentWithCaptionMessage = {
caption: cleanedMessage.message.documentWithCaptionMessage.caption,
name: cleanedMessage.message.documentWithCaptionMessage.name,
};
}
}
if (mediaUrl) cleanedMessage.message.mediaUrl = mediaUrl;
return cleanedMessage;
}
public async fetchMessages(query: Query<Message>) {
const keyFilters = query?.where?.key as {
id?: string;
@@ -540,12 +574,23 @@ export class ChannelStartupService {
participants?: string;
};
const timestampFilter = {};
if (query?.where?.messageTimestamp) {
if (query.where.messageTimestamp['gte'] && query.where.messageTimestamp['lte']) {
timestampFilter['messageTimestamp'] = {
gte: Math.floor(new Date(query.where.messageTimestamp['gte']).getTime() / 1000),
lte: Math.floor(new Date(query.where.messageTimestamp['lte']).getTime() / 1000),
};
}
}
const count = await this.prismaRepository.message.count({
where: {
instanceId: this.instanceId,
id: query?.where?.id,
source: query?.where?.source,
messageType: query?.where?.messageType,
...timestampFilter,
AND: [
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
@@ -569,6 +614,7 @@ export class ChannelStartupService {
id: query?.where?.id,
source: query?.where?.source,
messageType: query?.where?.messageType,
...timestampFilter,
AND: [
keyFilters?.id ? { key: { path: ['id'], equals: keyFilters?.id } } : {},
keyFilters?.fromMe ? { key: { path: ['fromMe'], equals: keyFilters?.fromMe } } : {},
@@ -625,113 +671,103 @@ export class ChannelStartupService {
const remoteJid = query?.where?.remoteJid
? query?.where?.remoteJid.includes('@')
? query.where?.remoteJid
: this.createJid(query.where?.remoteJid)
: createJid(query.where?.remoteJid)
: null;
let results = [];
const where = {
instanceId: this.instanceId,
};
if (!remoteJid) {
results = await this.prismaRepository.$queryRaw`
SELECT
"Chat"."id",
"Chat"."remoteJid",
"Chat"."name",
"Chat"."labels",
"Chat"."createdAt",
"Chat"."updatedAt",
"Contact"."pushName",
"Contact"."profilePicUrl",
"Chat"."unreadMessages",
(ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id,
(ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key,
(ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name,
(ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant,
(ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type,
(ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message,
(ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info,
(ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source,
(ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp,
(ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id,
(ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id,
(ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status
FROM "Chat"
LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid"
LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid"
WHERE
"Chat"."instanceId" = ${this.instanceId}
GROUP BY
"Chat"."id",
"Chat"."remoteJid",
"Contact"."id"
ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC;
`;
} else {
results = await this.prismaRepository.$queryRaw`
SELECT
"Chat"."id",
"Chat"."remoteJid",
"Chat"."name",
"Chat"."labels",
"Chat"."createdAt",
"Chat"."updatedAt",
"Contact"."pushName",
"Contact"."profilePicUrl",
"Chat"."unreadMessages",
(ARRAY_AGG("Message"."id" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_id,
(ARRAY_AGG("Message"."key" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_key,
(ARRAY_AGG("Message"."pushName" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_push_name,
(ARRAY_AGG("Message"."participant" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_participant,
(ARRAY_AGG("Message"."messageType" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_type,
(ARRAY_AGG("Message"."message" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message,
(ARRAY_AGG("Message"."contextInfo" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_context_info,
(ARRAY_AGG("Message"."source" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_source,
(ARRAY_AGG("Message"."messageTimestamp" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_message_timestamp,
(ARRAY_AGG("Message"."instanceId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_instance_id,
(ARRAY_AGG("Message"."sessionId" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_session_id,
(ARRAY_AGG("Message"."status" ORDER BY "Message"."messageTimestamp" DESC))[1] AS last_message_status
FROM "Chat"
LEFT JOIN "Message" ON "Message"."messageType" != 'reactionMessage' and "Message"."key"->>'remoteJid' = "Chat"."remoteJid"
LEFT JOIN "Contact" ON "Chat"."remoteJid" = "Contact"."remoteJid"
WHERE
"Chat"."instanceId" = ${this.instanceId} AND "Chat"."remoteJid" = ${remoteJid} and "Message"."messageType" != 'reactionMessage'
GROUP BY
"Chat"."id",
"Chat"."remoteJid",
"Contact"."id"
ORDER BY last_message_message_timestamp DESC NULLS LAST, "Chat"."updatedAt" DESC;
`;
if (remoteJid) {
where['remoteJid'] = remoteJid;
}
const timestampFilter =
query?.where?.messageTimestamp?.gte && query?.where?.messageTimestamp?.lte
? Prisma.sql`
AND "Message"."messageTimestamp" >= ${Math.floor(new Date(query.where.messageTimestamp.gte).getTime() / 1000)}
AND "Message"."messageTimestamp" <= ${Math.floor(new Date(query.where.messageTimestamp.lte).getTime() / 1000)}`
: Prisma.sql``;
const results = await this.prismaRepository.$queryRaw`
WITH rankedMessages AS (
SELECT DISTINCT ON ("Contact"."remoteJid")
"Contact"."id",
"Contact"."remoteJid",
"Contact"."pushName",
"Contact"."profilePicUrl",
COALESCE(
to_timestamp("Message"."messageTimestamp"::double precision),
"Contact"."updatedAt"
) as "updatedAt",
"Chat"."createdAt" as "windowStart",
"Chat"."createdAt" + INTERVAL '24 hours' as "windowExpires",
CASE
WHEN "Chat"."createdAt" + INTERVAL '24 hours' > NOW() THEN true
ELSE false
END as "windowActive",
"Message"."id" AS lastMessageId,
"Message"."key" AS lastMessage_key,
"Message"."pushName" AS lastMessagePushName,
"Message"."participant" AS lastMessageParticipant,
"Message"."messageType" AS lastMessageMessageType,
"Message"."message" AS lastMessageMessage,
"Message"."contextInfo" AS lastMessageContextInfo,
"Message"."source" AS lastMessageSource,
"Message"."messageTimestamp" AS lastMessageMessageTimestamp,
"Message"."instanceId" AS lastMessageInstanceId,
"Message"."sessionId" AS lastMessageSessionId,
"Message"."status" AS lastMessageStatus
FROM "Contact"
INNER JOIN "Message" ON "Message"."key"->>'remoteJid' = "Contact"."remoteJid"
LEFT JOIN "Chat" ON "Chat"."remoteJid" = "Contact"."remoteJid"
AND "Chat"."instanceId" = "Contact"."instanceId"
WHERE
"Contact"."instanceId" = ${this.instanceId}
AND "Message"."instanceId" = ${this.instanceId}
${remoteJid ? Prisma.sql`AND "Contact"."remoteJid" = ${remoteJid}` : Prisma.sql``}
${timestampFilter}
ORDER BY
"Contact"."remoteJid",
"Message"."messageTimestamp" DESC
)
SELECT * FROM rankedMessages
ORDER BY updatedAt DESC NULLS LAST;
`;
if (results && isArray(results) && results.length > 0) {
return results.map((chat) => {
const mappedResults = results.map((contact) => {
const lastMessage = contact.lastMessageId
? {
id: contact.lastMessageId,
key: contact.lastMessageKey,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
}
: undefined;
return {
id: chat.id,
remoteJid: chat.remoteJid,
name: chat.name,
labels: chat.labels,
createdAt: chat.createdAt,
updatedAt: chat.updatedAt,
pushName: chat.pushName,
profilePicUrl: chat.profilePicUrl,
unreadMessages: chat.unreadMessages,
lastMessage: chat.last_message_id
? {
id: chat.last_message_id,
key: chat.last_message_key,
pushName: chat.last_message_push_name,
participant: chat.last_message_participant,
messageType: chat.last_message_message_type,
message: chat.last_message_message,
contextInfo: chat.last_message_context_info,
source: chat.last_message_source,
messageTimestamp: chat.last_message_message_timestamp,
instanceId: chat.last_message_instance_id,
sessionId: chat.last_message_session_id,
status: chat.last_message_status,
}
: undefined,
id: contact.id,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
};
});
return mappedResults;
}
return [];

View File

@@ -42,42 +42,48 @@ export class WAMonitoringService {
public delInstanceTime(instance: string) {
const time = this.configService.get<DelInstance>('DEL_INSTANCE');
if (typeof time === 'number' && time > 0) {
setTimeout(async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
setTimeout(
async () => {
if (this.waInstances[instance]?.connectionStatus?.state !== 'open') {
if (this.waInstances[instance]?.connectionStatus?.state === 'connecting') {
if ((await this.waInstances[instance].integration) === Integration.WHATSAPP_BAILEYS) {
await this.waInstances[instance]?.client?.logout('Log out instance: ' + instance);
this.waInstances[instance]?.client?.ws?.close();
this.waInstances[instance]?.client?.end(undefined);
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
this.eventEmitter.emit('remove.instance', instance, 'inner');
} else {
this.eventEmitter.emit('remove.instance', instance, 'inner');
}
}
}, 1000 * 60 * time);
},
1000 * 60 * time,
);
}
}
public async instanceInfo(instanceNames?: string[]): Promise<any> {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
if (instanceNames && instanceNames.length > 0) {
const inexistentInstances = instanceNames ? instanceNames.filter((instance) => !this.waInstances[instance]) : [];
if (inexistentInstances.length > 0) {
throw new NotFoundException(
`Instance${inexistentInstances.length > 1 ? 's' : ''} "${inexistentInstances.join(', ')}" not found`,
);
if (inexistentInstances.length > 0) {
throw new NotFoundException(
`Instance${inexistentInstances.length > 1 ? 's' : ''} "${inexistentInstances.join(', ')}" not found`,
);
}
}
const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const where = instanceNames
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const where =
instanceNames && instanceNames.length > 0
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const instances = await this.prismaRepository.instance.findMany({
where,
@@ -123,7 +129,9 @@ export class WAMonitoringService {
throw new NotFoundException(`Instance "${instanceName}" not found`);
}
return this.instanceInfo([instanceName]);
const instanceNames = instanceName ? [instanceName] : null;
return this.instanceInfo(instanceNames);
}
public async cleaningUp(instanceName: string) {
@@ -213,8 +221,11 @@ export class WAMonitoringService {
data: {
id: data.instanceId,
name: data.instanceName,
ownerJid: data.ownerJid,
profileName: data.profileName,
profilePicUrl: data.profilePicUrl,
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : data.status ?? 'open',
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: data.hash,

View File

@@ -85,6 +85,7 @@ export declare namespace wa {
readMessages?: boolean;
readStatus?: boolean;
syncFullHistory?: boolean;
wavoipToken?: string;
};
export type LocalEvent = {
@@ -131,7 +132,14 @@ export declare namespace wa {
export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED';
}
export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage'];
export const TypeMediaMessage = [
'imageMessage',
'documentMessage',
'audioMessage',
'videoMessage',
'stickerMessage',
'ptvMessage',
];
export const MessageSubtype = [
'ephemeralMessage',

View File

@@ -10,7 +10,10 @@ const logger = new Logger('CacheEngine');
export class CacheEngine {
private engine: ICache;
constructor(private readonly configService: ConfigService, module: string) {
constructor(
private readonly configService: ConfigService,
module: string,
) {
const cacheConf = configService.get<CacheConf>('CACHE');
if (cacheConf?.REDIS?.ENABLED && cacheConf?.REDIS?.URI !== '') {

View File

@@ -9,7 +9,10 @@ export class LocalCache implements ICache {
private conf: CacheConfLocal;
static localCache = new NodeCache();
constructor(private readonly configService: ConfigService, private readonly module: string) {
constructor(
private readonly configService: ConfigService,
private readonly module: string,
) {
this.conf = this.configService.get<CacheConf>('CACHE')?.LOCAL;
}

View File

@@ -11,7 +11,10 @@ export class RedisCache implements ICache {
private client: RedisClientType;
private conf: CacheConfRedis;
constructor(private readonly configService: ConfigService, private readonly module: string) {
constructor(
private readonly configService: ConfigService,
private readonly module: string,
) {
this.conf = this.configService.get<CacheConf>('CACHE')?.REDIS;
this.client = redisClient.getConnection();
}

View File

@@ -1,3 +1,7 @@
// Import this first from sentry instrument!
import '@utils/instrumentSentry';
// Now import other modules
import { ProviderFiles } from '@api/provider/sessions';
import { PrismaRepository } from '@api/repository/repository.service';
import { HttpStatus, router } from '@api/routes/index.router';
@@ -21,19 +25,6 @@ function initWA() {
async function bootstrap() {
const logger = new Logger('SERVER');
const app = express();
const dsn = process.env.SENTRY_DSN;
if (dsn) {
logger.info('Sentry - ON');
Sentry.init({
dsn: dsn,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
Sentry.setupExpressErrorHandler(app);
}
let providerFiles: ProviderFiles = null;
if (configService.get<ProviderSession>('PROVIDER').ENABLED) {
@@ -141,6 +132,14 @@ async function bootstrap() {
eventManager.init(server);
if (process.env.SENTRY_DSN) {
logger.info('Sentry - ON');
// Add this after all routes,
// but before any and other error-handling middlewares are defined
Sentry.setupExpressErrorHandler(app);
}
server.listen(httpServer.PORT, () => logger.log(httpServer.TYPE.toUpperCase() + ' - ON: ' + httpServer.PORT));
initWA();

71
src/utils/createJid.ts Normal file
View File

@@ -0,0 +1,71 @@
// Check if the number is MX or AR
function formatMXOrARNumber(jid: string): string {
const countryCode = jid.substring(0, 2);
if (Number(countryCode) === 52 || Number(countryCode) === 54) {
if (jid.length === 13) {
const number = countryCode + jid.substring(3);
return number;
}
return jid;
}
return jid;
}
// Check if the number is br
function formatBRNumber(jid: string) {
const regexp = new RegExp(/^(\d{2})(\d{2})\d{1}(\d{8})$/);
if (regexp.test(jid)) {
const match = regexp.exec(jid);
if (match && match[1] === '55') {
const joker = Number.parseInt(match[3][0]);
const ddd = Number.parseInt(match[2]);
if (joker < 7 || ddd < 31) {
return match[0];
}
return match[1] + match[2] + match[3];
}
return jid;
} else {
return jid;
}
}
export function createJid(number: string): string {
number = number.replace(/:\d+/, '');
if (number.includes('@g.us') || number.includes('@s.whatsapp.net') || number.includes('@lid')) {
return number;
}
if (number.includes('@broadcast')) {
return number;
}
number = number
?.replace(/\s/g, '')
.replace(/\+/g, '')
.replace(/\(/g, '')
.replace(/\)/g, '')
.split(':')[0]
.split('@')[0];
if (number.includes('-') && number.length >= 24) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = number.replace(/\D/g, '');
if (number.length >= 18) {
number = number.replace(/[^\d-]/g, '');
return `${number}@g.us`;
}
number = formatMXOrARNumber(number);
number = formatBRNumber(number);
return `${number}@s.whatsapp.net`;
}

View File

@@ -1,11 +1,6 @@
import { advancedOperatorsSearch } from './advancedOperatorsSearch';
export const findBotByTrigger = async (
botRepository: any,
settingsRepository: any,
content: string,
instanceId: string,
) => {
export const findBotByTrigger = async (botRepository: any, content: string, instanceId: string) => {
// Check for triggerType 'all'
const findTriggerAll = await botRepository.findFirst({
where: {

View File

@@ -15,14 +15,16 @@ const getTypeMessage = (msg: any) => {
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
listResponseMessage: msg?.message?.listResponseMessage?.title,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
templateButtonReplyMessage:
msg?.message?.templateButtonReplyMessage?.selectedId || msg?.message?.buttonsResponseMessage?.selectedButtonId,
// Medias
audioMessage: msg?.message?.speechToText
? msg?.message?.speechToText
: msg?.message?.audioMessage
? `audioMessage|${mediaId}`
: undefined,
? `audioMessage|${mediaId}`
: undefined,
imageMessage: msg?.message?.imageMessage
? `imageMessage|${mediaId}${msg?.message?.imageMessage?.caption ? `|${msg?.message?.imageMessage?.caption}` : ''}`
: undefined,

View File

@@ -0,0 +1,12 @@
import * as Sentry from '@sentry/node';
const dsn = process.env.SENTRY_DSN;
if (dsn) {
Sentry.init({
dsn: dsn,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
});
}

View File

@@ -43,6 +43,7 @@ export const instanceSchema: JSONSchema7 = {
readMessages: { type: 'boolean' },
readStatus: { type: 'boolean' },
syncFullHistory: { type: 'boolean' },
wavoipToken: { type: 'string' },
// Proxy
proxyHost: { type: 'string' },
proxyPort: { type: 'string' },

View File

@@ -122,6 +122,32 @@ export const mediaMessageSchema: JSONSchema7 = {
required: ['number', 'mediatype'],
};
export const ptvMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
properties: {
number: { ...numberDefinition },
video: { type: 'string' },
delay: {
type: 'integer',
description: 'Enter a value in milliseconds',
},
quoted: { ...quotedOptionsSchema },
everyOne: { type: 'boolean', enum: [true, false] },
mentioned: {
type: 'array',
minItems: 1,
uniqueItems: true,
items: {
type: 'string',
pattern: '^\\d+',
description: '"mentioned" must be an array of numeric strings',
},
},
},
required: ['number'],
};
export const audioMessageSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
@@ -387,14 +413,18 @@ export const buttonsMessageSchema: JSONSchema7 = {
properties: {
type: {
type: 'string',
enum: ['reply', 'copy', 'url', 'call'],
enum: ['reply', 'copy', 'url', 'call', 'pix'],
},
displayText: { type: 'string' },
id: { type: 'string' },
url: { type: 'string' },
phoneNumber: { type: 'string' },
currency: { type: 'string' },
name: { type: 'string' },
keyType: { type: 'string', enum: ['phone', 'email', 'cpf', 'cnpj', 'random'] },
key: { type: 'string' },
},
required: ['type', 'displayText'],
required: ['type'],
...isNotEmpty('id', 'url', 'phoneNumber'),
},
},

View File

@@ -31,6 +31,7 @@ export const settingsSchema: JSONSchema7 = {
readMessages: { type: 'boolean' },
readStatus: { type: 'boolean' },
syncFullHistory: { type: 'boolean' },
wavoipToken: { type: 'string' },
},
required: ['rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'],
...isNotEmpty('rejectCall', 'groupsIgnore', 'alwaysOnline', 'readMessages', 'readStatus', 'syncFullHistory'),

View File

@@ -4,7 +4,7 @@
"emitDecoratorMetadata": true,
"declaration": true,
"target": "es2020",
"module": "commonjs",
"module": "CommonJS",
"rootDir": "./",
"resolveJsonModule": true,
"removeComments": true,
@@ -26,7 +26,8 @@
"@libs/*": ["./src/libs/*"],
"@utils/*": ["./src/utils/*"],
"@validate/*": ["./src/validate/*"]
}
},
"moduleResolution": "Node"
},
"exclude": ["node_modules", "./test", "./dist", "./prisma"],
"include": [