diff --git a/Dockerfile b/Dockerfile index fc535aa27..eff124291 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS builder +FROM node:20-alpine AS base RUN apk update && \ apk add --no-cache git ffmpeg wget curl bash openssl @@ -9,6 +9,8 @@ LABEL contact="contato@evolution-api.com" WORKDIR /evolution +FROM base AS builder + COPY ./package.json ./tsconfig.json ./ RUN npm install @@ -17,7 +19,7 @@ COPY ./src ./src COPY ./public ./public COPY ./prisma ./prisma COPY ./manager ./manager -COPY ./.env.example ./.env +#COPY ./.env.example ./.env COPY ./runWithProvider.js ./ COPY ./tsup.config.ts ./ @@ -29,6 +31,17 @@ RUN ./Docker/scripts/generate_database.sh RUN npm run build +FROM base AS dev + +RUN apk update && \ + apk add git ffmpeg wget curl bash openssl + +COPY . . + +RUN npm install + +ENTRYPOINT ["/bin/bash", "-c", "npm run dev:server" ] + FROM node:20-alpine AS final RUN apk update && \ @@ -46,7 +59,7 @@ COPY --from=builder /evolution/dist ./dist COPY --from=builder /evolution/prisma ./prisma COPY --from=builder /evolution/manager ./manager COPY --from=builder /evolution/public ./public -COPY --from=builder /evolution/.env ./.env +#COPY --from=builder /evolution/.env ./.env COPY --from=builder /evolution/Docker ./Docker COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 2ca3424e5..354517bb4 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -1,13 +1,13 @@ services: api: - container_name: evolution_api - image: evolution/api:local - build: . + build: + target: dev restart: always ports: - 8080:8080 volumes: - evolution_instances:/evolution/instances + - ./:/evolution networks: - evolution-net env_file: diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 000000000..aba84f46f --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,20 @@ +version: "3.7" +services: + api: + build: + context: . + dockerfile: Dockerfile + target: final + restart: always + volumes: + - evolution_instances:/evolution/instances + networks: + - Docker + +volumes: + evolution_instances: + +networks: + Docker: ## Nome da rede interna + external: true + name: Docker ## Nome da rede interna diff --git a/docker-compose.yaml b/docker-compose.yaml index 33918c383..907a65030 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,11 @@ +version: "3.7" services: api: + build: + context: . + dockerfile: Dockerfile + target: final container_name: evolution_api - image: evoapicloud/evolution-api:latest restart: always depends_on: - redis @@ -11,16 +15,14 @@ services: volumes: - evolution_instances:/evolution/instances networks: - - evolution-net - env_file: - - .env + - Docker expose: - 8080 redis: image: redis:latest networks: - - evolution-net + - Docker container_name: redis command: > redis-server --port 6379 --appendonly yes @@ -33,14 +35,14 @@ services: container_name: postgres image: postgres:15 networks: - - evolution-net - command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] + - Docker + #command: ["postgres", "-c", "max_connections=1000", "-c", "listen_addresses=*"] restart: always ports: - 5432:5432 environment: - - POSTGRES_USER=user - - POSTGRES_PASSWORD=pass + - POSTGRES_USER=evolution + - POSTGRES_PASSWORD=postgres - POSTGRES_DB=evolution - POSTGRES_HOST_AUTH_METHOD=trust volumes: @@ -55,6 +57,6 @@ volumes: networks: - evolution-net: - name: evolution-net - driver: bridge + Docker: ## Nome da rede interna + external: true + name: Docker ## Nome da rede interna diff --git a/package-lock.json b/package-lock.json index f07b4c910..71722c731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", + "@nestjs/event-emitter": "^3.0.1", "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^6.1.0", "@sentry/node": "^8.47.0", @@ -1939,6 +1940,170 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, +<<<<<<< Updated upstream +======= +<<<<<<< HEAD + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.3.tgz", + "integrity": "sha512-ogEK+GriWodIwCw6buQ1rpcH4Kx+G7YQ9EwuPySI3rS05pSdtQ++UhucjusSI9apNidv+QURBztJkRecwwJQXg==", + "license": "MIT", + "peer": true, + "dependencies": { + "file-type": "21.0.0", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": ">=0.4.1", + "class-validator": ">=0.13.2", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/common/node_modules/file-type": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz", + "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/inflate": "^0.2.7", + "strtok3": "^10.2.2", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/@nestjs/common/node_modules/strtok3": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.1.tgz", + "integrity": "sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@nestjs/common/node_modules/token-types": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", + "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@nestjs/core": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.3.tgz", + "integrity": "sha512-5lTni0TCh8x7bXETRD57pQFnKnEg1T6M+VLE7wAmyQRIecKQU+2inRGZD+A4v2DC1I04eA0WffP0GKLxjOKlzw==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@nuxt/opencollective": "0.4.1", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "8.2.0", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/core/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@nestjs/event-emitter": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", + "license": "MIT", + "dependencies": { + "eventemitter2": "6.4.9" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" +======= +>>>>>>> Stashed changes "node_modules/@keyv/serialize": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", @@ -1968,6 +2133,10 @@ "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" +<<<<<<< Updated upstream +======= +>>>>>>> 8d6e59598e993cbb039606733568ce94b6004375 +>>>>>>> Stashed changes } }, "node_modules/@noble/hashes": { @@ -2016,6 +2185,23 @@ "node": ">= 8" } }, + "node_modules/@nuxt/opencollective": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@nuxt/opencollective/-/opencollective-0.4.1.tgz", + "integrity": "sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "consola": "^3.2.3" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0", + "npm": ">=5.10.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -3743,6 +3929,43 @@ "node": ">=18" } }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/token-types": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz", + "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", @@ -6799,6 +7022,13 @@ "node": ">=6" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT", + "peer": true + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -6829,6 +7059,13 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT", + "peer": true + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8067,6 +8304,16 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -8344,6 +8591,26 @@ "node": ">= 8" } }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=13.2.0" + } + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -10179,6 +10446,13 @@ "@redis/time-series": "1.1.0" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10372,6 +10646,16 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -11882,6 +12166,32 @@ "node": ">=14.17" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", diff --git a/package.json b/package.json index a6eb24701..98789bd34 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@ffmpeg-installer/ffmpeg": "^1.1.0", "@figuro/chatwoot-sdk": "^1.1.16", "@hapi/boom": "^10.0.1", + "@nestjs/event-emitter": "^3.0.1", "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^6.1.0", "@sentry/node": "^8.47.0", diff --git a/src/api/dto/instance.dto.ts b/src/api/dto/instance.dto.ts index 1da3bf1c1..54d6bb5fd 100644 --- a/src/api/dto/instance.dto.ts +++ b/src/api/dto/instance.dto.ts @@ -51,6 +51,8 @@ export class InstanceDto extends IntegrationDto { chatwootSignMsg?: boolean; chatwootToken?: string; chatwootUrl?: string; + phoneNumberId?: string; + name?: string; } export class SetPresenceDto { diff --git a/src/api/integrations/channel/meta/serpro.controller.ts b/src/api/integrations/channel/meta/serpro.controller.ts new file mode 100644 index 000000000..29dde84b1 --- /dev/null +++ b/src/api/integrations/channel/meta/serpro.controller.ts @@ -0,0 +1,49 @@ +import { PrismaRepository } from '@api/repository/repository.service'; +import { WAMonitoringService } from '@api/services/monitor.service'; +import { Logger } from '@config/logger.config'; + +import { ChannelController, ChannelControllerInterface } from '../channel.controller'; + +export class SerproController extends ChannelController implements ChannelControllerInterface { + private readonly logger = new Logger('SerproController'); + + constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) { + super(prismaRepository, waMonitor); + } + + integrationEnabled: boolean; + + // OBRIGATÓRIO para a interface! + public async receiveWebhook(data: any) { + // Pode redirecionar para o SERPRO específico + return this.receiveWebhookSerpro(data); + } + + public async receiveWebhookSerpro(data: any) { + const numberId = data.metadata?.display_phone_number || data.display_phone_number || '552121996300'; + + if (!numberId) { + this.logger.error('WebhookService -> receiveWebhookSerpro -> numberId not found'); + return { + status: 'success', + }; + } + + const instance = await this.prismaRepository.instance.findFirst({ + where: { number: numberId }, + }); + + if (!instance) { + this.logger.error('WebhookService -> receiveWebhookSerpro -> instance not found'); + return { + status: 'success', + }; + } + + await this.waMonitor.waInstances[instance.name].connectToWhatsapp(data); + + return { + status: 'success', + }; + } +} diff --git a/src/api/integrations/channel/meta/serpro.router.ts b/src/api/integrations/channel/meta/serpro.router.ts new file mode 100644 index 000000000..9360e36e6 --- /dev/null +++ b/src/api/integrations/channel/meta/serpro.router.ts @@ -0,0 +1,12 @@ +import { serproController } from '@api/server.module'; +import { Router } from 'express'; + +const serproRouter = Router(); + +serproRouter.post('/webhook', async (req, res) => { + const { body } = req; + const response = await serproController.receiveWebhook(body); + return res.status(200).json(response); +}); + +export { serproRouter }; diff --git a/src/api/integrations/channel/meta/whatsapp.business.service.ts b/src/api/integrations/channel/meta/whatsapp.business.service.ts index 56533ff9c..be32b0a40 100644 --- a/src/api/integrations/channel/meta/whatsapp.business.service.ts +++ b/src/api/integrations/channel/meta/whatsapp.business.service.ts @@ -28,6 +28,7 @@ import axios from 'axios'; import { arrayUnique, isURL } from 'class-validator'; import EventEmitter2 from 'eventemitter2'; import FormData from 'form-data'; +import { createReadStream } from 'fs'; import mimeTypes from 'mime-types'; import { join } from 'path'; @@ -146,20 +147,11 @@ export class BusinessStartupService extends ChannelStartupService { const version = this.configService.get('WA_BUSINESS').VERSION; urlServer = `${urlServer}/${version}/${id}`; const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; - - // Primeiro, obtenha a URL do arquivo let result = await axios.get(urlServer, { headers }); - - // Depois, baixe o arquivo usando a URL retornada - result = await axios.get(result.data.url, { - headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download - responseType: 'arraybuffer', - }); - + result = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' }); return result.data; } catch (e) { - this.logger.error(`Error downloading media: ${e}`); - throw e; + this.logger.error(e); } } @@ -167,23 +159,7 @@ export class BusinessStartupService extends ChannelStartupService { const message = received.messages[0]; let content: any = message.type + 'Message'; content = { [content]: message[message.type] }; - if (message.context) { - content = { ...content, contextInfo: { stanzaId: message.context.id } }; - } - return content; - } - - private messageAudioJson(received: any) { - const message = received.messages[0]; - let content: any = { - audioMessage: { - ...message.audio, - ptt: message.audio.voice || false, // Define se é mensagem de voz - }, - }; - if (message.context) { - content = { ...content, contextInfo: { stanzaId: message.context.id } }; - } + message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; return content; } @@ -216,77 +192,17 @@ export class BusinessStartupService extends ChannelStartupService { } private messageTextJson(received: any) { - // Verificar que received y received.messages existen - if (!received || !received.messages || received.messages.length === 0) { - this.logger.error('Error: received object or messages array is undefined or empty'); - return null; - } - - const message = received.messages[0]; let content: any; - - // Verificar si es un mensaje de tipo sticker, location u otro tipo que no tiene text - if (!message.text) { - // Si no hay texto, manejamos diferente según el tipo de mensaje - if (message.type === 'sticker') { - content = { stickerMessage: {} }; - } else if (message.type === 'location') { - content = { - locationMessage: { - degreesLatitude: message.location?.latitude, - degreesLongitude: message.location?.longitude, - name: message.location?.name, - address: message.location?.address, - }, - }; - } else { - // Para otros tipos de mensajes sin texto, creamos un contenido genérico - this.logger.log(`Mensaje de tipo ${message.type} sin campo text`); - content = { [message.type + 'Message']: message[message.type] || {} }; - } - - // Añadir contexto si existe - if (message.context) { - content = { ...content, contextInfo: { stanzaId: message.context.id } }; - } - - return content; - } - - // Si el mensaje tiene texto, procesamos normalmente - if (!received.metadata || !received.metadata.phone_number_id) { - this.logger.error('Error: metadata or phone_number_id is undefined'); - return null; - } - + const message = received.messages[0]; if (message.from === received.metadata.phone_number_id) { content = { extendedTextMessage: { text: message.text.body }, }; - if (message.context) { - content = { ...content, contextInfo: { stanzaId: message.context.id } }; - } + message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; } else { content = { conversation: message.text.body }; - if (message.context) { - content = { ...content, contextInfo: { stanzaId: message.context.id } }; - } + message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; } - - return content; - } - - private messageLocationJson(received: any) { - const message = received.messages[0]; - let content: any = { - locationMessage: { - degreesLatitude: message.location.latitude, - degreesLongitude: message.location.longitude, - name: message.location?.name, - address: message.location?.address, - }, - }; - message.context ? (content = { ...content, contextInfo: { stanzaId: message.context.id } }) : content; return content; } @@ -367,12 +283,6 @@ export class BusinessStartupService extends ChannelStartupService { case 'template': messageType = 'conversation'; break; - case 'location': - messageType = 'locationMessage'; - break; - case 'sticker': - messageType = 'stickerMessage'; - break; default: messageType = 'conversation'; break; @@ -389,36 +299,17 @@ export class BusinessStartupService extends ChannelStartupService { if (received.contacts) pushName = received.contacts[0].profile.name; if (received.messages) { - const message = received.messages[0]; // Añadir esta línea para definir message - const key = { - id: message.id, + id: received.messages[0].id, remoteJid: this.phoneNumber, - fromMe: message.from === received.metadata.phone_number_id, + fromMe: received.messages[0].from === received.metadata.phone_number_id, }; - - if (message.type === 'sticker') { - this.logger.log('Procesando mensaje de tipo sticker'); - messageRaw = { - key, - pushName, - message: { - stickerMessage: message.sticker || {}, - }, - messageType: 'stickerMessage', - messageTimestamp: parseInt(message.timestamp) as number, - source: 'unknown', - instanceId: this.instanceId, - }; - } else if (this.isMediaMessage(message)) { - const messageContent = - message.type === 'audio' ? this.messageAudioJson(received) : this.messageMediaJson(received); - + if (this.isMediaMessage(received?.messages[0])) { messageRaw = { key, pushName, - message: messageContent, - contextInfo: messageContent?.contextInfo, + message: this.messageMediaJson(received), + contextInfo: this.messageMediaJson(received)?.contextInfo, messageType: this.renderMessageType(received.messages[0].type), messageTimestamp: parseInt(received.messages[0].timestamp) as number, source: 'unknown', @@ -436,10 +327,7 @@ export class BusinessStartupService extends ChannelStartupService { const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }; const result = await axios.get(urlServer, { headers }); - const buffer = await axios.get(result.data.url, { - headers: { Authorization: `Bearer ${this.token}` }, // Use apenas o token de autorização para download - responseType: 'arraybuffer', - }); + const buffer = await axios.get(result.data.url, { headers, responseType: 'arraybuffer' }); let mediaType; @@ -464,17 +352,6 @@ export class BusinessStartupService extends ChannelStartupService { } } - // Para áudio, garantir extensão correta baseada no mimetype - if (mediaType === 'audio') { - if (mimetype.includes('ogg')) { - fileName = `${message.messages[0].id}.ogg`; - } else if (mimetype.includes('mp3')) { - fileName = `${message.messages[0].id}.mp3`; - } else if (mimetype.includes('m4a')) { - fileName = `${message.messages[0].id}.m4a`; - } - } - const size = result.headers['content-length'] || buffer.data.byteLength; const fullName = join(`${this.instance.id}`, key.remoteJid, mediaType, fileName); @@ -501,72 +378,13 @@ export class BusinessStartupService extends ChannelStartupService { messageRaw.message.mediaUrl = mediaUrl; messageRaw.message.base64 = buffer.data.toString('base64'); - - // Processar OpenAI speech-to-text para áudio após o mediaUrl estar disponível - if (this.configService.get('OPENAI').ENABLED && mediaType === 'audio') { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: this.instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); - - if ( - openAiDefaultSettings && - openAiDefaultSettings.openaiCredsId && - openAiDefaultSettings.speechToText - ) { - try { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( - openAiDefaultSettings.OpenaiCreds, - { - message: { - mediaUrl: messageRaw.message.mediaUrl, - ...messageRaw, - }, - }, - )}`; - } catch (speechError) { - this.logger.error(`Error processing speech-to-text: ${speechError}`); - } - } - } } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); } } else { const buffer = await this.downloadMediaMessage(received?.messages[0]); - messageRaw.message.base64 = buffer.toString('base64'); - // Processar OpenAI speech-to-text para áudio mesmo sem S3 - if (this.configService.get('OPENAI').ENABLED && message.type === 'audio') { - const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ - where: { - instanceId: this.instanceId, - }, - include: { - OpenaiCreds: true, - }, - }); - - if (openAiDefaultSettings && openAiDefaultSettings.openaiCredsId && openAiDefaultSettings.speechToText) { - try { - messageRaw.message.speechToText = `[audio] ${await this.openaiService.speechToText( - openAiDefaultSettings.OpenaiCreds, - { - message: { - base64: messageRaw.message.base64, - ...messageRaw, - }, - }, - )}`; - } catch (speechError) { - this.logger.error(`Error processing speech-to-text: ${speechError}`); - } - } - } + messageRaw.message.base64 = buffer.toString('base64'); } } else if (received?.messages[0].interactive) { messageRaw = { @@ -637,6 +455,33 @@ export class BusinessStartupService extends ChannelStartupService { // await this.client.readMessages([received.key]); } + if (this.configService.get('OPENAI').ENABLED) { + const openAiDefaultSettings = await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: this.instanceId, + }, + include: { + OpenaiCreds: true, + }, + }); + + const audioMessage = received?.messages[0]?.audio; + + if ( + openAiDefaultSettings && + openAiDefaultSettings.openaiCredsId && + openAiDefaultSettings.speechToText && + audioMessage + ) { + messageRaw.message.speechToText = await this.openaiService.speechToText(openAiDefaultSettings.OpenaiCreds, { + message: { + mediaUrl: messageRaw.message.mediaUrl, + ...messageRaw, + }, + }); + } + } + this.logger.log(messageRaw); this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); @@ -662,7 +507,7 @@ export class BusinessStartupService extends ChannelStartupService { } } - if (!this.isMediaMessage(message) && message.type !== 'sticker') { + if (!this.isMediaMessage(received?.messages[0])) { await this.prismaRepository.message.create({ data: messageRaw, }); @@ -865,54 +710,17 @@ export class BusinessStartupService extends ChannelStartupService { } protected async eventHandler(content: any) { - try { - // Registro para depuración - this.logger.log('Contenido recibido en eventHandler:'); - this.logger.log(JSON.stringify(content, null, 2)); - - const database = this.configService.get('DATABASE'); - const settings = await this.findSettings(); - - // Si hay mensajes, verificar primero el tipo - if (content.messages && content.messages.length > 0) { - const message = content.messages[0]; - this.logger.log(`Tipo de mensaje recibido: ${message.type}`); - - // Verificamos el tipo de mensaje antes de procesarlo - if ( - message.type === 'text' || - message.type === 'image' || - message.type === 'video' || - message.type === 'audio' || - message.type === 'document' || - message.type === 'sticker' || - message.type === 'location' || - message.type === 'contacts' || - message.type === 'interactive' || - message.type === 'button' || - message.type === 'reaction' - ) { - // Procesar el mensaje normalmente - this.messageHandle(content, database, settings); - } else { - this.logger.warn(`Tipo de mensaje no reconocido: ${message.type}`); - } - } else if (content.statuses) { - // Procesar actualizaciones de estado - this.messageHandle(content, database, settings); - } else { - this.logger.warn('No se encontraron mensajes ni estados en el contenido recibido'); - } - } catch (error) { - this.logger.error('Error en eventHandler:'); - this.logger.error(error); - } + const database = this.configService.get('DATABASE'); + const settings = await this.findSettings(); + + this.messageHandle(content, database, settings); } protected async sendMessageWithTyping(number: string, message: any, options?: Options, isIntegration = false) { try { let quoted: any; let webhookUrl: any; + const linkPreview = options?.linkPreview != false ? undefined : false; if (options?.quoted) { const m = options?.quoted; @@ -980,7 +788,7 @@ export class BusinessStartupService extends ChannelStartupService { to: number.replace(/\D/g, ''), text: { body: message['conversation'], - preview_url: Boolean(options?.linkPreview), + preview_url: linkPreview, }, }; quoted ? (content.context = { message_id: quoted.id }) : content; @@ -996,10 +804,9 @@ export class BusinessStartupService extends ChannelStartupService { to: number.replace(/\D/g, ''), [message['mediaType']]: { [message['type']]: message['id'], - ...(message['mediaType'] !== 'audio' && - message['fileName'] && - !isImage && { filename: message['fileName'] }), - ...(message['mediaType'] !== 'audio' && message['caption'] && { caption: message['caption'] }), + preview_url: linkPreview, + ...(message['fileName'] && !isImage && { filename: message['fileName'] }), + caption: message['caption'], }, }; quoted ? (content.context = { message_id: quoted.id }) : content; @@ -1097,7 +904,7 @@ export class BusinessStartupService extends ChannelStartupService { } })(); - if (messageSent?.error_data || messageSent.message) { + if (messageSent?.error_data) { this.logger.error(messageSent); return messageSent; } @@ -1164,50 +971,29 @@ export class BusinessStartupService extends ChannelStartupService { return res; } - private async getIdMedia(mediaMessage: any, isFile = false) { - try { - const formData = new FormData(); - - if (isFile === false) { - if (isURL(mediaMessage.media)) { - const response = await axios.get(mediaMessage.media, { responseType: 'arraybuffer' }); - const buffer = Buffer.from(response.data, 'base64'); - formData.append('file', buffer, { - filename: mediaMessage.fileName || 'media', - contentType: mediaMessage.mimetype, - }); - } else { - const buffer = Buffer.from(mediaMessage.media, 'base64'); - formData.append('file', buffer, { - filename: mediaMessage.fileName || 'media', - contentType: mediaMessage.mimetype, - }); - } - } else { - formData.append('file', mediaMessage.media.buffer, { - filename: mediaMessage.media.originalname, - contentType: mediaMessage.media.mimetype, - }); - } + private async getIdMedia(mediaMessage: any) { + const formData = new FormData(); - const mimetype = mediaMessage.mimetype || mediaMessage.media.mimetype; + const fileStream = createReadStream(mediaMessage.media); - formData.append('typeFile', mimetype); - formData.append('messaging_product', 'whatsapp'); + formData.append('file', fileStream, { filename: 'media', contentType: mediaMessage.mimetype }); + formData.append('typeFile', mediaMessage.mimetype); + formData.append('messaging_product', 'whatsapp'); - const token = this.token; + // const fileBuffer = await fs.readFile(mediaMessage.media); - const headers = { Authorization: `Bearer ${token}` }; - const url = `${this.configService.get('WA_BUSINESS').URL}/${ - this.configService.get('WA_BUSINESS').VERSION - }/${this.number}/media`; + // const fileBlob = new Blob([fileBuffer], { type: mediaMessage.mimetype }); + // formData.append('file', fileBlob); + // formData.append('typeFile', mediaMessage.mimetype); + // formData.append('messaging_product', 'whatsapp'); - const res = await axios.post(url, formData, { headers }); - return res.data.id; - } catch (error) { - this.logger.error(error.response.data); - throw new InternalServerErrorException(error?.toString() || error); - } + const headers = { Authorization: `Bearer ${this.token}` }; + const res = await axios.post( + process.env.API_URL + '/' + process.env.VERSION + '/' + this.number + '/media', + formData, + { headers }, + ); + return res.data.id; } protected async prepareMediaMessage(mediaMessage: MediaMessage) { @@ -1280,87 +1066,48 @@ export class BusinessStartupService extends ChannelStartupService { return mediaSent; } - public async processAudio(audio: string, number: string, file: any) { + public async processAudio(audio: string, number: string) { number = number.replace(/\D/g, ''); const hash = `${number}-${new Date().getTime()}`; - if (process.env.API_AUDIO_CONVERTER) { - this.logger.verbose('Using audio converter API'); - const formData = new FormData(); - - 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); - } - - formData.append('format', 'mp3'); - - const response = await axios.post(process.env.API_AUDIO_CONVERTER, formData, { - headers: { - ...formData.getHeaders(), - apikey: process.env.API_AUDIO_CONVERTER_KEY, - }, - }); - - const audioConverter = response?.data?.audio || response?.data?.url; - - if (!audioConverter) { - throw new InternalServerErrorException('Failed to convert audio'); - } + let mimetype: string | false; - const prepareMedia: any = { - fileName: `${hash}.mp3`, - mediaType: 'audio', - media: audioConverter, - mimetype: 'audio/mpeg', - }; + const prepareMedia: any = { + fileName: `${hash}.mp3`, + mediaType: 'audio', + media: audio, + }; + if (isURL(audio)) { + mimetype = mimeTypes.lookup(audio); + prepareMedia.id = audio; + prepareMedia.type = 'link'; + } else { + mimetype = mimeTypes.lookup(prepareMedia.fileName); const id = await this.getIdMedia(prepareMedia); prepareMedia.id = id; prepareMedia.type = 'id'; + } - this.logger.verbose('Audio converted'); - return prepareMedia; - } else { - let mimetype: string | false; - - const prepareMedia: any = { - fileName: `${hash}.mp3`, - mediaType: 'audio', - media: audio, - }; + prepareMedia.mimetype = mimetype; - if (isURL(audio)) { - mimetype = mimeTypes.lookup(audio); - prepareMedia.id = audio; - prepareMedia.type = 'link'; - } else if (audio && !file) { - mimetype = mimeTypes.lookup(prepareMedia.fileName); - const id = await this.getIdMedia(prepareMedia); - prepareMedia.id = id; - prepareMedia.type = 'id'; - } else if (file) { - prepareMedia.media = file; - const id = await this.getIdMedia(prepareMedia, true); - prepareMedia.id = id; - prepareMedia.type = 'id'; - mimetype = file.mimetype; - } + return prepareMedia; + } - prepareMedia.mimetype = mimetype; + public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { + const mediaData: SendAudioDto = { ...data }; - return prepareMedia; + 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'); } - } - public async audioWhatsapp(data: SendAudioDto, file?: any, isIntegration = false) { - const message = await this.processAudio(data.audio, data.number, file); + const message = await this.processAudio(mediaData.audio, data.number); const audioSent = await this.sendMessageWithTyping( data.number, diff --git a/src/api/repository/repository.service.ts b/src/api/repository/repository.service.ts index 793bb0c82..d242a7fcc 100644 --- a/src/api/repository/repository.service.ts +++ b/src/api/repository/repository.service.ts @@ -10,6 +10,11 @@ export class Query { } export class PrismaRepository extends PrismaClient { + message: any; + media: any; + openaiSetting: any; + contact: any; + messageUpdate: any; constructor(private readonly configService: ConfigService) { super(); } diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index fd2372fa5..43928e657 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -2,6 +2,7 @@ import { authGuard } from '@api/guards/auth.guard'; import { instanceExistsGuard, instanceLoggedGuard } from '@api/guards/instance.guard'; import Telemetry from '@api/guards/telemetry.guard'; import { ChannelRouter } from '@api/integrations/channel/channel.router'; +import { serproRouter } from '@api/integrations/channel/meta/serpro.router'; import { ChatbotRouter } from '@api/integrations/chatbot/chatbot.router'; import { EventRouter } from '@api/integrations/event/event.router'; import { StorageRouter } from '@api/integrations/storage/storage.router'; @@ -95,6 +96,6 @@ router .use('', new ChannelRouter(configService, ...guards).router) .use('', new EventRouter(configService, ...guards).router) .use('', new ChatbotRouter(...guards).router) - .use('', new StorageRouter(...guards).router); - + .use('', new StorageRouter(...guards).router) + .use('/serpro', serproRouter); export { HttpStatus, router }; diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 385fe17b1..c60ac9d45 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -16,6 +16,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 { SerproController } from './integrations/channel/meta/serpro.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'; @@ -114,6 +115,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 serproController = new SerproController(prismaRepository, waMonitor); export const baileysController = new BaileysController(waMonitor); const openaiService = new OpenaiService(waMonitor, prismaRepository, configService); diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index 90962dcb2..24958fb1b 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -12,6 +12,7 @@ import EventEmitter2 from 'eventemitter2'; import { rmSync } from 'fs'; import { join } from 'path'; +import { ChatwootService } from '../../api/integrations/chatbot/chatwoot/services/chatwoot.service'; import { CacheService } from './cache.service'; export class WAMonitoringService { @@ -144,7 +145,7 @@ export class WAMonitoringService { if (findInstance) { const instance = await this.prismaRepository.instance.update({ - where: { name: instanceName }, + where: { id: findInstance.id }, data: { connectionStatus: 'close' }, }); @@ -220,9 +221,17 @@ export class WAMonitoringService { public async saveInstance(data: any) { try { const clientName = await this.configService.get('DATABASE').CONNECTION.CLIENT_NAME; - await this.prismaRepository.instance.create({ - data: { - id: data.instanceId, + + // Pegue o token do .env/config + const token = process.env.SERPRO_CLIENT_SECRET; + + // Garanta que nome está preenchido! + if (!data.instanceName) throw new Error('instanceName é obrigatório no saveInstance!'); + + await this.prismaRepository.instance.upsert({ + where: { name: data.instanceName }, + create: { + id: data.instanceId || data.instanceName, // sempre defina! name: data.instanceName, ownerJid: data.ownerJid, profileName: data.profileName, @@ -231,7 +240,19 @@ export class WAMonitoringService { data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'), number: data.number, integration: data.integration || Integration.WHATSAPP_BAILEYS, - token: data.hash, + token: token, // PEGA DO ENV! + clientName: clientName, + businessId: data.businessId, + }, + update: { + ownerJid: data.ownerJid, + profileName: data.profileName, + profilePicUrl: data.profilePicUrl, + connectionStatus: + data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'), + number: data.number, + integration: data.integration || Integration.WHATSAPP_BAILEYS, + token: token, // ATUALIZA DO ENV! clientName: clientName, businessId: data.businessId, }, diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 8c253163a..3090c98b8 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -317,6 +317,11 @@ export interface Env { S3?: S3; AUTHENTICATION: Auth; PRODUCTION?: Production; + SERPRO: { + API_BASE_URL: string; + CLIENT_ID: string; + CLIENT_SECRET: string; + }; } export type Key = keyof Env; @@ -661,6 +666,11 @@ export class ConfigService { }, EXPOSE_IN_FETCH_INSTANCES: process.env?.AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES === 'true', }, + SERPRO: { + API_BASE_URL: process.env.SERPRO_API_BASE_URL || '', + CLIENT_ID: process.env.SERPRO_CLIENT_ID || '', + CLIENT_SECRET: process.env.SERPRO_CLIENT_SECRET || '', + }, }; } } diff --git a/src/main.ts b/src/main.ts index cf787f32d..996163a04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -15,8 +15,10 @@ import { ServerUP } from '@utils/server-up'; import axios from 'axios'; import compression from 'compression'; import cors from 'cors'; +import dotenv from 'dotenv'; import express, { json, NextFunction, Request, Response, urlencoded } from 'express'; import { join } from 'path'; +dotenv.config(); function initWA() { waMonitor.loadInstance();