mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
Merge branch 'release/2.2.1'
This commit is contained in:
commit
7f10a0eecd
@ -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
|
||||
|
@ -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'],
|
||||
|
28
.github/workflows/check_code_quality.yml
vendored
Normal file
28
.github/workflows/check_code_quality.yml
vendored
Normal 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
1
.gitignore
vendored
@ -21,7 +21,6 @@ lerna-debug.log*
|
||||
# Package
|
||||
/yarn.lock
|
||||
/pnpm-lock.yaml
|
||||
/package-lock.json
|
||||
|
||||
# IDEs
|
||||
.vscode/*
|
||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
150
local_install.sh
Executable 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
12227
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
90
package.json
90
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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`;
|
@ -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)
|
||||
|
@ -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 $$;
|
@ -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)
|
||||
|
@ -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' });
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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]) {
|
||||
|
@ -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 {
|
||||
|
@ -6,4 +6,5 @@ export class SettingsDto {
|
||||
readMessages?: boolean;
|
||||
readStatus?: boolean;
|
||||
syncFullHistory?: boolean;
|
||||
wavoipToken?: string;
|
||||
}
|
||||
|
@ -75,8 +75,6 @@ export class ChannelController {
|
||||
data.prismaRepository,
|
||||
data.cache,
|
||||
data.chatwootCache,
|
||||
data.baileysCache,
|
||||
data.providerFiles,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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';
|
||||
|
60
src/api/integrations/channel/whatsapp/baileys.controller.ts
Normal file
60
src/api/integrations/channel/whatsapp/baileys.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
105
src/api/integrations/channel/whatsapp/baileys.router.ts
Normal file
105
src/api/integrations/channel/whatsapp/baileys.router.ts
Normal 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();
|
||||
}
|
@ -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;
|
@ -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;
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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({
|
||||
|
@ -428,8 +428,8 @@ export class DifyService {
|
||||
},
|
||||
false,
|
||||
);
|
||||
textBuffer = '';
|
||||
}
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
if (mediaType === 'audio') {
|
||||
|
@ -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({
|
||||
|
@ -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');
|
||||
|
@ -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({
|
||||
|
@ -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');
|
||||
|
@ -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({
|
||||
|
@ -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');
|
||||
|
@ -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({
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
@ -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 [];
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
5
src/cache/cacheengine.ts
vendored
5
src/cache/cacheengine.ts
vendored
@ -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 !== '') {
|
||||
|
5
src/cache/localcache.ts
vendored
5
src/cache/localcache.ts
vendored
@ -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;
|
||||
}
|
||||
|
||||
|
5
src/cache/rediscache.ts
vendored
5
src/cache/rediscache.ts
vendored
@ -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();
|
||||
}
|
||||
|
25
src/main.ts
25
src/main.ts
@ -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
71
src/utils/createJid.ts
Normal 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`;
|
||||
}
|
@ -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: {
|
||||
|
@ -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,
|
||||
|
12
src/utils/instrumentSentry.ts
Normal file
12
src/utils/instrumentSentry.ts
Normal 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,
|
||||
});
|
||||
}
|
@ -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' },
|
||||
|
@ -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'),
|
||||
|
@ -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": [
|
||||
|
Loading…
Reference in New Issue
Block a user