Merge branch 'release/2.2.1'

This commit is contained in:
Davidson Gomes 2025-01-22 14:39:27 -03:00
commit 7f10a0eecd
65 changed files with 14361 additions and 525 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

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

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

@ -34,12 +34,15 @@ services:
image: postgres:15
networks:
- evolution-net
command: ["postgres", "-c", "max_connections=1000"]
command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"]
restart: always
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=PASSWORD
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=evolution
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- postgres_data:/var/lib/postgresql/data
expose:

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",
@ -11,10 +11,13 @@
"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,72 +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",
"mediainfo.js": "^0.3.2",
"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)

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);
@ -409,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

@ -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

@ -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

@ -22,13 +22,14 @@ 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 { 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';
export class BusinessStartupService extends ChannelStartupService {
@ -70,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;
@ -84,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,
@ -128,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());
@ -227,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 +=
@ -301,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,
@ -331,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]}`;
@ -352,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,
@ -371,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]);
}
@ -458,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,
},
},
() => {},
);
}
}
@ -497,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 },
@ -783,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',
@ -791,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'],
},
};
@ -895,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),
@ -997,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,
@ -1008,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';
@ -1055,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`,
@ -1064,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';
@ -1084,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');
@ -1251,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

@ -76,7 +76,9 @@ import {
import { BadRequestException, InternalServerErrorException, NotFoundException } from '@exceptions';
import ffmpegPath from '@ffmpeg-installer/ffmpeg';
import { Boom } from '@hapi/boom';
import { createId as cuid } from '@paralleldrive/cuid2';
import { Instance } from '@prisma/client';
import { createJid } from '@utils/createJid';
import { makeProxyAgent } from '@utils/makeProxyAgent';
import { getOnWhatsappCache, saveOnWhatsappCache } from '@utils/onWhatsappCache';
import { status } from '@utils/renderStatus';
@ -130,7 +132,7 @@ import ffmpeg from 'fluent-ffmpeg';
import FormData from 'form-data';
import { readFileSync } from 'fs';
import Long from 'long';
import mime from 'mime';
import mimeTypes from 'mime-types';
import NodeCache from 'node-cache';
import cron from 'node-cron';
import { release } from 'os';
@ -142,6 +144,8 @@ import sharp from 'sharp';
import { PassThrough, Readable } from 'stream';
import { v4 } from 'uuid';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
// Adicione a função getVideoDuration no início do arquivo
@ -270,7 +274,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async getProfileStatus() {
const status = await this.client.fetchStatus(this.instance.wuid);
return status?.status;
return status[0]?.status;
}
public get profilePictureUrl() {
@ -309,6 +313,9 @@ export class BaileysStartupService extends ChannelStartupService {
instance: this.instance.name,
state: 'refused',
statusReason: DisconnectReason.connectionClosed,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
});
this.endSession = true;
@ -388,11 +395,6 @@ export class BaileysStartupService extends ChannelStartupService {
state: connection,
statusReason: (lastDisconnect?.error as Boom)?.output?.statusCode ?? 200,
};
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
if (connection === 'close') {
@ -434,6 +436,11 @@ export class BaileysStartupService extends ChannelStartupService {
this.eventEmitter.emit('logout.instance', this.instance.name, 'inner');
this.client?.ws?.close();
this.client.end(new Error('Close connection'));
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
}
@ -481,6 +488,21 @@ export class BaileysStartupService extends ChannelStartupService {
);
this.syncChatwootLostMessages();
}
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
wuid: this.instance.wuid,
profileName: await this.getProfileName(),
profilePictureUrl: this.instance.profilePictureUrl,
...this.stateConnection,
});
}
if (connection === 'connecting') {
this.sendDataWebhook(Events.CONNECTION_UPDATE, {
instance: this.instance.name,
...this.stateConnection,
});
}
}
@ -672,8 +694,30 @@ export class BaileysStartupService extends ChannelStartupService {
this.client = makeWASocket(socketConfig);
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
useVoiceCallsBaileys(this.localSettings.wavoipToken, this.client, this.connectionStatus.state as any, true);
}
this.eventHandler();
this.client.ws.on('CB:call', (packet) => {
console.log('CB:call', packet);
const payload = {
event: 'CB:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.client.ws.on('CB:ack,class:call', (packet) => {
console.log('CB:ack,class:call', packet);
const payload = {
event: 'CB:ack,class:call',
packet: packet,
};
this.sendDataWebhook(Events.CALL, payload, true, ['websocket']);
});
this.phoneNumber = number;
return this.client;
@ -973,7 +1017,7 @@ export class BaileysStartupService extends ChannelStartupService {
const messagesRaw: any[] = [];
const messagesRepository = new Set(
const messagesRepository: Set<string> = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ??
(
await this.prismaRepository.message.findMany({
@ -1135,21 +1179,25 @@ export class BaileysStartupService extends ChannelStartupService {
}
const existingChat = await this.prismaRepository.chat.findFirst({
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
select: { id: true, name: true },
});
if (existingChat) {
const chatToInsert = {
remoteJid: received.key.remoteJid,
instanceId: this.instanceId,
name: received.pushName || '',
unreadMessages: 0,
};
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (
existingChat &&
received.pushName &&
existingChat.name !== received.pushName &&
received.pushName.trim().length > 0
) {
this.sendDataWebhook(Events.CHATS_UPSERT, [{ ...existingChat, name: received.pushName }]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
try {
await this.prismaRepository.chat.update({
where: { id: existingChat.id },
data: { name: received.pushName },
});
} catch (error) {
console.log(`Chat insert record ignored: ${received.key.remoteJid} - ${this.instanceId}`);
}
}
}
@ -1243,7 +1291,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
const { buffer, mediaType, fileName, size } = media;
const mimetype = mime.getType(fileName).toString();
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(`${this.instance.id}`, received.key.remoteJid, mediaType, fileName);
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
'Content-Type': mimetype,
@ -1276,17 +1324,21 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.localWebhook.enabled) {
if (isMedia && this.localWebhook.webhookBase64) {
const buffer = await downloadMediaMessage(
{ key: received.key, message: received?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
try {
const buffer = await downloadMediaMessage(
{ key: received.key, message: received?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
} catch (error) {
this.logger.error(['Error converting media to base64', error?.message]);
}
}
}
@ -1483,9 +1535,16 @@ export class BaileysStartupService extends ChannelStartupService {
this.sendDataWebhook(Events.CHATS_UPSERT, [chatToInsert]);
if (this.configService.get<Database>('DATABASE').SAVE_DATA.CHATS) {
await this.prismaRepository.chat.create({
data: chatToInsert,
});
try {
await this.prismaRepository.chat.update({
where: {
id: existingChat.id,
},
data: chatToInsert,
});
} catch (error) {
console.log(`Chat insert record ignored: ${chatToInsert.remoteJid} - ${chatToInsert.instanceId}`);
}
}
}
}
@ -1523,6 +1582,8 @@ export class BaileysStartupService extends ChannelStartupService {
private readonly labelHandle = {
[Events.LABELS_EDIT]: async (label: Label) => {
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
const labelsRepository = await this.prismaRepository.label.findMany({
where: { instanceId: this.instanceId },
});
@ -1557,7 +1618,6 @@ export class BaileysStartupService extends ChannelStartupService {
create: labelData,
});
}
this.sendDataWebhook(Events.LABELS_EDIT, { ...label, instance: this.instance.name });
}
},
@ -1565,26 +1625,18 @@ export class BaileysStartupService extends ChannelStartupService {
data: { association: LabelAssociation; type: 'remove' | 'add' },
database: Database,
) => {
this.logger.info(
`labels association - ${data?.association?.chatId} (${data.type}-${data?.association?.type}): ${data?.association?.labelId}`,
);
if (database.SAVE_DATA.CHATS) {
const chats = await this.prismaRepository.chat.findMany({
where: { instanceId: this.instanceId },
});
const chat = chats.find((c) => c.remoteJid === data.association.chatId);
if (chat) {
const labelsArray = Array.isArray(chat.labels) ? chat.labels.map((event) => String(event)) : [];
let labels = [...labelsArray];
const instanceId = this.instanceId;
const chatId = data.association.chatId;
const labelId = data.association.labelId;
if (data.type === 'remove') {
labels = labels.filter((label) => label !== data.association.labelId);
} else if (data.type === 'add') {
labels = [...labels, data.association.labelId];
}
await this.prismaRepository.chat.update({
where: { id: chat.id },
data: {
labels,
},
});
if (data.type === 'add') {
await this.addLabel(labelId, instanceId, chatId);
} else if (data.type === 'remove') {
await this.removeLabel(labelId, instanceId, chatId);
}
}
@ -1762,7 +1814,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async profilePicture(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
const profilePictureUrl = await this.client.profilePictureUrl(jid, 'image');
@ -1780,12 +1832,12 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async getStatus(number: string) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
return {
wuid: jid,
status: (await this.client.fetchStatus(jid))?.status,
status: (await this.client.fetchStatus(jid))[0]?.status,
};
} catch (error) {
return {
@ -1796,7 +1848,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async fetchProfile(instanceName: string, number?: string) {
const jid = number ? this.createJid(number) : this.client?.user?.id;
const jid = number ? createJid(number) : this.client?.user?.id;
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();
@ -1852,7 +1904,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async offerCall({ number, isVideo, callDuration }: OfferCallDto) {
const jid = this.createJid(number);
const jid = createJid(number);
try {
const call = await this.client.offerCall(jid, isVideo);
@ -2121,9 +2173,9 @@ export class BaileysStartupService extends ChannelStartupService {
if (options?.mentionsEveryOne) {
mentions = group.participants.map((participant) => participant.id);
} else if (options.mentioned?.length) {
} else if (options?.mentioned?.length) {
mentions = options.mentioned.map((mention) => {
const jid = this.createJid(mention);
const jid = createJid(mention);
if (isJidGroup(jid)) {
return null;
}
@ -2205,7 +2257,7 @@ export class BaileysStartupService extends ChannelStartupService {
const { buffer, mediaType, fileName, size } = media;
const mimetype = mime.getType(fileName).toString();
const mimetype = mimeTypes.lookup(fileName).toString();
const fullName = join(
`${this.instance.id}`,
@ -2245,17 +2297,21 @@ export class BaileysStartupService extends ChannelStartupService {
if (this.localWebhook.enabled) {
if (isMedia && this.localWebhook.webhookBase64) {
const buffer = await downloadMediaMessage(
{ key: messageRaw.key, message: messageRaw?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
try {
const buffer = await downloadMediaMessage(
{ key: messageRaw.key, message: messageRaw?.message },
'buffer',
{},
{
logger: P({ level: 'error' }) as any,
reuploadRequest: this.client.updateMediaMessage,
},
);
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
messageRaw.message.base64 = buffer ? buffer.toString('base64') : undefined;
} catch (error) {
this.logger.error(['Error converting media to base64', error?.message]);
}
}
}
@ -2527,12 +2583,12 @@ export class BaileysStartupService extends ChannelStartupService {
mediaMessage.fileName = 'video.mp4';
}
let mimetype: string;
let mimetype: string | false;
if (mediaMessage.mimetype) {
mimetype = mediaMessage.mimetype;
} else {
mimetype = mime.getType(mediaMessage.fileName);
mimetype = mimeTypes.lookup(mediaMessage.fileName);
if (!mimetype && isURL(mediaMessage.media)) {
let config: any = {
@ -3193,7 +3249,7 @@ export class BaileysStartupService 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';
@ -3243,7 +3299,7 @@ export class BaileysStartupService extends ChannelStartupService {
};
data.numbers.forEach((number) => {
const jid = this.createJid(number);
const jid = createJid(number);
if (isJidGroup(jid)) {
jids.groups.push({ number, jid });
@ -3436,7 +3492,7 @@ export class BaileysStartupService extends ChannelStartupService {
archive: data.archive,
lastMessages: [last_message],
},
this.createJid(number),
createJid(number),
);
return {
@ -3473,7 +3529,7 @@ export class BaileysStartupService extends ChannelStartupService {
markRead: false,
lastMessages: [last_message],
},
this.createJid(number),
createJid(number),
);
return {
@ -3585,7 +3641,7 @@ export class BaileysStartupService extends ChannelStartupService {
);
const typeMessage = getContentType(msg.message);
const ext = mime.getExtension(mediaMessage?.['mimetype']);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']);
const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`;
if (convertToMp4 && typeMessage === 'audioMessage') {
@ -3678,7 +3734,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async fetchBusinessProfile(number: string): Promise<NumberBusiness> {
try {
const jid = number ? this.createJid(number) : this.instance.wuid;
const jid = number ? createJid(number) : this.instance.wuid;
const profile = await this.client.getBusinessProfile(jid);
@ -3826,7 +3882,7 @@ export class BaileysStartupService extends ChannelStartupService {
}
public async updateMessage(data: UpdateMessageDto) {
const jid = this.createJid(data.number);
const jid = createJid(data.number);
const options = await this.formatUpdateMessage(data);
@ -3874,11 +3930,13 @@ export class BaileysStartupService extends ChannelStartupService {
try {
if (data.action === 'add') {
await this.client.addChatLabel(contact.jid, data.labelId);
await this.addLabel(data.labelId, this.instanceId, contact.jid);
return { numberJid: contact.jid, labelId: data.labelId, add: true };
}
if (data.action === 'remove') {
await this.client.removeChatLabel(contact.jid, data.labelId);
await this.removeLabel(data.labelId, this.instanceId, contact.jid);
return { numberJid: contact.jid, labelId: data.labelId, remove: true };
}
@ -4114,7 +4172,7 @@ export class BaileysStartupService extends ChannelStartupService {
const inviteUrl = inviteCode.inviteUrl;
const numbers = id.numbers.map((number) => this.createJid(number));
const numbers = id.numbers.map((number) => createJid(number));
const description = id.description ?? '';
const msg = `${description}\n\n${inviteUrl}`;
@ -4185,7 +4243,7 @@ export class BaileysStartupService extends ChannelStartupService {
public async updateGParticipant(update: GroupUpdateParticipantDto) {
try {
const participants = update.participants.map((p) => this.createJid(p));
const participants = update.participants.map((p) => createJid(p));
const updateParticipants = await this.client.groupParticipantsUpdate(
update.groupJid,
participants,
@ -4223,6 +4281,7 @@ export class BaileysStartupService extends ChannelStartupService {
throw new BadRequestException('Unable to leave the group', error.toString());
}
}
public async templateMessage() {
throw new Error('Method not available in the Baileys service');
}
@ -4259,6 +4318,19 @@ export class BaileysStartupService extends ChannelStartupService {
delete messageRaw.message.documentWithCaptionMessage;
}
const quotedMessage = messageRaw?.contextInfo?.quotedMessage;
if (quotedMessage) {
if (quotedMessage.extendedTextMessage) {
quotedMessage.conversation = quotedMessage.extendedTextMessage.text;
delete quotedMessage.extendedTextMessage;
}
if (quotedMessage.documentWithCaptionMessage) {
quotedMessage.documentMessage = quotedMessage.documentWithCaptionMessage.message.documentMessage;
delete quotedMessage.documentWithCaptionMessage;
}
}
return messageRaw;
}
@ -4326,4 +4398,133 @@ export class BaileysStartupService extends ChannelStartupService {
return unreadMessages;
}
private async addLabel(labelId: string, instanceId: string, chatId: string) {
const id = cuid();
await this.prismaRepository.$executeRawUnsafe(
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
VALUES ($4, $2, $3, to_jsonb(ARRAY[$1]::text[]), NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
DO
UPDATE
SET "labels" = (
SELECT to_jsonb(array_agg(DISTINCT elem))
FROM (
SELECT jsonb_array_elements_text("Chat"."labels") AS elem
UNION
SELECT $1::text AS elem
) sub
),
"updatedAt" = NOW();`,
labelId,
instanceId,
chatId,
id,
);
}
private async removeLabel(labelId: string, instanceId: string, chatId: string) {
const id = cuid();
await this.prismaRepository.$executeRawUnsafe(
`INSERT INTO "Chat" ("id", "instanceId", "remoteJid", "labels", "createdAt", "updatedAt")
VALUES ($4, $2, $3, '[]'::jsonb, NOW(), NOW()) ON CONFLICT ("instanceId", "remoteJid")
DO
UPDATE
SET "labels" = COALESCE (
(
SELECT jsonb_agg(elem)
FROM jsonb_array_elements_text("Chat"."labels") AS elem
WHERE elem <> $1
),
'[]'::jsonb
),
"updatedAt" = NOW();`,
labelId,
instanceId,
chatId,
id,
);
}
public async baileysOnWhatsapp(jid: string) {
const response = await this.client.onWhatsApp(jid);
return response;
}
public async baileysProfilePictureUrl(jid: string, type: 'image' | 'preview', timeoutMs: number) {
const response = await this.client.profilePictureUrl(jid, type, timeoutMs);
return response;
}
public async baileysAssertSessions(jids: string[], force: boolean) {
const response = await this.client.assertSessions(jids, force);
return response;
}
public async baileysCreateParticipantNodes(jids: string[], message: proto.IMessage, extraAttrs: any) {
const response = await this.client.createParticipantNodes(jids, message, extraAttrs);
const convertedResponse = {
...response,
nodes: response.nodes.map((node: any) => ({
...node,
content: node.content?.map((c: any) => ({
...c,
content: c.content instanceof Uint8Array ? Buffer.from(c.content).toString('base64') : c.content,
})),
})),
};
return convertedResponse;
}
public async baileysSendNode(stanza: any) {
console.log('stanza', JSON.stringify(stanza));
const response = await this.client.sendNode(stanza);
return response;
}
public async baileysGetUSyncDevices(jids: string[], useCache: boolean, ignoreZeroDevices: boolean) {
const response = await this.client.getUSyncDevices(jids, useCache, ignoreZeroDevices);
return response;
}
public async baileysGenerateMessageTag() {
const response = await this.client.generateMessageTag();
return response;
}
public async baileysSignalRepositoryDecryptMessage(jid: string, type: 'pkmsg' | 'msg', ciphertext: string) {
try {
const ciphertextBuffer = Buffer.from(ciphertext, 'base64');
const response = await this.client.signalRepository.decryptMessage({
jid,
type,
ciphertext: ciphertextBuffer,
});
return response instanceof Uint8Array ? Buffer.from(response).toString('base64') : response;
} catch (error) {
this.logger.error('Error decrypting message:');
this.logger.error(error);
throw error;
}
}
public async baileysGetAuthState() {
const response = {
me: this.client.authState.creds.me,
account: this.client.authState.creds.account,
};
return response;
}
}

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';
@ -704,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,
@ -1066,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) {
@ -1958,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');
@ -1970,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}`;
}
@ -2047,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');
@ -2056,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);
@ -2099,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}`;
}

View File

@ -756,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({

View File

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

View File

@ -728,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({

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

@ -728,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({

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

@ -965,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({

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

@ -943,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({

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

@ -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,20 +42,23 @@ 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,
);
}
}
@ -72,14 +75,15 @@ export class WAMonitoringService {
const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const where = instanceNames && instanceNames.length > 0
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const where =
instanceNames && instanceNames.length > 0
? {
name: {
in: instanceNames,
},
clientName,
}
: { clientName };
const instances = await this.prismaRepository.instance.findMany({
where,
@ -217,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', 'ptvMessage'];
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

@ -17,13 +17,14 @@ const getTypeMessage = (msg: any) => {
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
listResponseMessage: msg?.message?.listResponseMessage?.title,
responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId,
templateButtonReplyMessage: msg?.message?.templateButtonReplyMessage?.selectedId,
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

@ -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": [