diff --git a/.env.example b/.env.example index a3f438ea..d057d187 100644 --- a/.env.example +++ b/.env.example @@ -49,6 +49,8 @@ RABBITMQ_URI=amqp://localhost RABBITMQ_EXCHANGE_NAME=evolution # Global events - By enabling this variable, events from all instances are sent in the same event queue. RABBITMQ_GLOBAL_ENABLED=false +# Prefix key to queue name +RABBITMQ_PREFIX_KEY=evolution # Choose the events you want to send to RabbitMQ RABBITMQ_EVENTS_APPLICATION_STARTUP=false RABBITMQ_EVENTS_INSTANCE_CREATE=false diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml new file mode 100644 index 00000000..d30615db --- /dev/null +++ b/.github/workflows/publish_docker_image.yml @@ -0,0 +1,49 @@ +name: Build Docker image + +on: + push: + tags: + - "*.*.*" + +jobs: + build_deploy: + name: Build and Deploy + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: atendai/evolution-api + images: evoapicloud/evolution-api + tags: type=semver,pattern=v{{version}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v5 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} \ No newline at end of file diff --git a/.github/workflows/publish_docker_image_homolog.yml b/.github/workflows/publish_docker_image_homolog.yml index 9bc638ee..c6397a9a 100644 --- a/.github/workflows/publish_docker_image_homolog.yml +++ b/.github/workflows/publish_docker_image_homolog.yml @@ -20,7 +20,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: atendai/evolution-api-lite + images: evoapicloud/evolution-api-lite tags: homolog - name: Set up QEMU diff --git a/.github/workflows/publish_docker_image_latest.yml b/.github/workflows/publish_docker_image_latest.yml index 0f262a16..79335ddc 100644 --- a/.github/workflows/publish_docker_image_latest.yml +++ b/.github/workflows/publish_docker_image_latest.yml @@ -20,7 +20,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: atendai/evolution-api-lite + images: evoapicloud/evolution-api-lite tags: latest - name: Set up QEMU diff --git a/.gitignore b/.gitignore index a8226ede..3a20ac21 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /dist /node_modules +.cursor* /Docker/.env .vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e95d1d..2d316041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +# 2.2.3 (2025-02-03 11:52) + +### Fixed + +* Fix cache in local file system +* Update Baileys Version + +# 2.2.2 (2025-01-31 06:55) + +### Features + +* Added prefix key to queue name in RabbitMQ + +### Fixed + +* Update Baileys Version + # 2.2.1 (2025-01-22 14:37) ### Features diff --git a/Docker/swarm/evolution_api_v2.yaml b/Docker/swarm/evolution_api_v2.yaml index e06cf9e1..ba677514 100644 --- a/Docker/swarm/evolution_api_v2.yaml +++ b/Docker/swarm/evolution_api_v2.yaml @@ -2,7 +2,7 @@ version: "3.7" services: evolution_v2: - image: atendai/evolution-api-lite:latest + image: evoapicloud/evolution-api:latest volumes: - evolution_instances:/evolution/instances networks: diff --git a/Dockerfile b/Dockerfile index b360e768..ad1641cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,9 @@ FROM node:20-alpine AS builder RUN apk update && \ apk add git wget curl bash openssl -LABEL version="2.2.1" description="Api to control whatsapp features through http requests." +LABEL version="2.2.3" description="Api to control whatsapp features through http requests." LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" -LABEL contact="contato@atendai.com" +LABEL contact="contato@evolution-api.com" WORKDIR /evolution diff --git a/LICENSE b/LICENSE index da01e779..ad430f14 100644 --- a/LICENSE +++ b/LICENSE @@ -8,7 +8,7 @@ a. LOGO and copyright information: In the process of using Evolution API's front b. Usage Notification Requirement: If Evolution API is used as part of any project, including closed-source systems (e.g., proprietary software), the user is required to display a clear notification within the system that Evolution API is being utilized. This notification should be visible to system administrators and accessible from the system's documentation or settings page. Failure to comply with this requirement may result in the necessity for a commercial license, as determined by the producer. -Please contact contato@atendai.com to inquire about licensing matters. +Please contact contato@evolution-api.com to inquire about licensing matters. 2. As a contributor, you should agree that: diff --git a/README.md b/README.md index e27851bb..efe25d62 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@
+[![Docker Image (https://img.shields.io/badge/Docker-Image-blue)](https://hub.docker.com/r/evoapicloud/evolution-api)] [![Whatsapp Group](https://img.shields.io/badge/Group-WhatsApp-%2322BC18)](https://evolution-api.com/whatsapp) [![Discord Community](https://img.shields.io/badge/Discord-Community-blue)](https://evolution-api.com/discord) [![Postman Collection](https://img.shields.io/badge/Postman-Collection-orange)](https://evolution-api.com/postman) diff --git a/docker-compose.yaml b/docker-compose.yaml index bd7325aa..33918c38 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: api: container_name: evolution_api - image: atendai/evolution-api-lite:latest + image: evoapicloud/evolution-api:latest restart: always depends_on: - redis diff --git a/package-lock.json b/package-lock.json index ada8fbce..9a1b8a56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "evolution-api", - "version": "2.2.1", + "version": "2.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "evolution-api", - "version": "2.2.1", + "version": "2.2.3", "license": "Apache-2.0", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", "@aws-sdk/client-sqs": "^3.723.0", "@hapi/boom": "^10.0.1", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^6.1.0", + "@prisma/client": "^5.22.0", "@sentry/node": "^8.47.0", "amqplib": "^0.10.5", "axios": "^1.7.9", @@ -41,7 +41,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "pino": "^8.11.0", - "prisma": "^6.1.0", + "prisma": "^5.22.0", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", @@ -2390,12 +2390,13 @@ } }, "node_modules/@prisma/client": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz", - "integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { - "node": ">=18.18" + "node": ">=16.13" }, "peerDependencies": { "prisma": "*" @@ -2407,43 +2408,48 @@ } }, "node_modules/@prisma/debug": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz", - "integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==" + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz", - "integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.2.1", - "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", - "@prisma/fetch-engine": "6.2.1", - "@prisma/get-platform": "6.2.1" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/engines-version": { - "version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz", - "integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==" + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz", - "integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.2.1", - "@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69", - "@prisma/get-platform": "6.2.1" + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" } }, "node_modules/@prisma/get-platform": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz", - "integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.2.1" + "@prisma/debug": "5.22.0" } }, "node_modules/@prisma/instrumentation": { @@ -4593,8 +4599,8 @@ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==" }, "node_modules/baileys": { - "version": "6.7.9", - "resolved": "git+ssh://git@github.com/EvolutionAPI/Baileys.git#d39c74f2f0ec463b5ad7bde7db7f1e2e40720b01", + "version": "6.7.12", + "resolved": "git+ssh://git@github.com/EvolutionAPI/Baileys.git#2c69f65d4b6c4e779d6e3d2c0c32689a5425df95", "dependencies": { "@adiwajshing/keyed-db": "^0.2.4", "@hapi/boom": "^9.1.3", @@ -4718,8 +4724,8 @@ } }, "node_modules/baileys/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "version": "6.7.12", + "resolved": "git+ssh://git@github.com/EvolutionAPI/Baileys.git#ce92d5d32f1174f050d2bba8fd637dc6e45faafa", "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "funding": [ "https://github.com/sponsors/broofa", @@ -9296,18 +9302,19 @@ } }, "node_modules/prisma": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz", - "integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "6.2.1" + "@prisma/engines": "5.22.0" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": ">=18.18" + "node": ">=16.13" }, "optionalDependencies": { "fsevents": "2.3.3" diff --git a/package.json b/package.json index 04ccf1ef..c7c84c99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "evolution-api", - "version": "2.2.1", + "version": "2.2.3", "description": "Rest api for communication with WhatsApp", "main": "./dist/main.js", "type": "commonjs", @@ -41,7 +41,7 @@ ], "author": { "name": "Davidson Gomes", - "email": "contato@atendai.com" + "email": "contato@evolution-api.com" }, "license": "Apache-2.0", "bugs": { @@ -53,7 +53,7 @@ "@aws-sdk/client-sqs": "^3.723.0", "@hapi/boom": "^10.0.1", "@paralleldrive/cuid2": "^2.2.2", - "@prisma/client": "^6.1.0", + "@prisma/client": "^5.22.0", "@sentry/node": "^8.47.0", "amqplib": "^0.10.5", "axios": "^1.7.9", @@ -81,7 +81,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "pino": "^8.11.0", - "prisma": "^6.1.0", + "prisma": "^5.22.0", "pusher": "^5.2.0", "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", diff --git a/src/api/controllers/chat.controller.ts b/src/api/controllers/chat.controller.ts index 207d8ba5..8c922f11 100644 --- a/src/api/controllers/chat.controller.ts +++ b/src/api/controllers/chat.controller.ts @@ -3,6 +3,8 @@ import { BlockUserDto, DeleteMessage, getBase64FromMediaMessageDto, + getCatalogDto, + getCollectionsDto, MarkChatUnreadDto, NumberDto, PrivacySettingDto, @@ -109,4 +111,12 @@ export class ChatController { public async blockUser({ instanceName }: InstanceDto, data: BlockUserDto) { return await this.waMonitor.waInstances[instanceName].blockUser(data); } + + public async fetchCatalog({ instanceName }: InstanceDto, data: getCatalogDto) { + return await this.waMonitor.waInstances[instanceName].fetchCatalog(instanceName, data); + } + + public async fetchCatalogCollections({ instanceName }: InstanceDto, data: getCollectionsDto) { + return await this.waMonitor.waInstances[instanceName].fetchCatalogCollections(instanceName, data); + } } diff --git a/src/api/dto/chat.dto.ts b/src/api/dto/chat.dto.ts index 00da7fdd..1693165e 100644 --- a/src/api/dto/chat.dto.ts +++ b/src/api/dto/chat.dto.ts @@ -126,3 +126,14 @@ export class BlockUserDto { number: string; status: 'block' | 'unblock'; } + +export class getCatalogDto { + number?: string; + limit?: number; + cursor?: string; +} + +export class getCollectionsDto { + number?: string; + limit?: number; +} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ddd93603..42695d8d 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -4,6 +4,8 @@ import { BlockUserDto, DeleteMessage, getBase64FromMediaMessageDto, + getCatalogDto, + getCollectionsDto, LastMessage, MarkChatUnreadDto, NumberBusiness, @@ -87,6 +89,7 @@ import makeWASocket, { BufferedEventData, BufferJSON, CacheStore, + CatalogCollection, Chat, ConnectionState, Contact, @@ -96,6 +99,7 @@ import makeWASocket, { fetchLatestBaileysVersion, generateWAMessageFromContent, getAggregateVotesInPollMessage, + GetCatalogOptions, getContentType, getDevice, GroupMetadata, @@ -109,6 +113,7 @@ import makeWASocket, { MiscMessageGenerationOptions, ParticipantAction, prepareWAMessageMedia, + Product, proto, UserFacingSocketConfig, WABrowserDescription, @@ -910,9 +915,10 @@ export class BaileysStartupService extends ChannelStartupService { } } + const editedMessage = + received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage; + if (received.message?.protocolMessage?.editedMessage || received.message?.editedMessage?.message) { - const editedMessage = - received.message?.protocolMessage || received.message?.editedMessage?.message?.protocolMessage; if (editedMessage) { await this.sendDataWebhook(Events.MESSAGES_EDITED, editedMessage); } @@ -936,6 +942,17 @@ export class BaileysStartupService extends ChannelStartupService { await this.baileysCache.delete(received.key.id); } + // Cache to avoid duplicate messages + const messageKey = `${this.instance.id}_${received.key.id}`; + const cached = await this.baileysCache.get(messageKey); + + if (cached && !editedMessage) { + this.logger.info(`Message duplicated ignored: ${received.key.id}`); + continue; + } + + await this.baileysCache.set(messageKey, true, 30 * 60); + if ( (type !== 'notify' && type !== 'append') || received.message?.protocolMessage || @@ -1145,6 +1162,17 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + const updateKey = `${this.instance.id}_${key.id}_${update.status}`; + + const cached = await this.baileysCache.get(updateKey); + + if (cached) { + this.logger.info(`Message duplicated ignored: ${key.id}`); + continue; + } + + await this.baileysCache.set(updateKey, true, 30 * 60); + if (key.remoteJid !== 'status@broadcast') { let pollUpdates: any; @@ -1833,6 +1861,7 @@ export class BaileysStartupService extends ChannelStartupService { const cache = this.configService.get('CACHE'); if (!cache.REDIS.ENABLED && !cache.LOCAL.ENABLED) group = await this.findGroup({ groupJid: sender }, 'inner'); else group = await this.getGroupMetadataCache(sender); + // group = await this.findGroup({ groupJid: sender }, 'inner'); } catch (error) { throw new NotFoundException('Group not found'); } @@ -2982,25 +3011,43 @@ export class BaileysStartupService extends ChannelStartupService { const messageId = response.message?.protocolMessage?.key?.id; if (messageId) { const isLogicalDeleted = configService.get('DATABASE').DELETE_DATA.LOGICAL_MESSAGE_DELETE; - let message = await this.prismaRepository.message.findUnique({ - where: { id: messageId }, + let message = await this.prismaRepository.message.findFirst({ + where: { + key: { + path: ['id'], + equals: messageId, + }, + }, }); if (isLogicalDeleted) { if (!message) return response; const existingKey = typeof message?.key === 'object' && message.key !== null ? message.key : {}; message = await this.prismaRepository.message.update({ - where: { id: messageId }, + where: { id: message.id }, data: { key: { ...existingKey, deleted: true, }, + status: 'DELETED', }, }); + const messageUpdate: any = { + messageId: message.id, + keyId: messageId, + remoteJid: response.key.remoteJid, + fromMe: response.key.fromMe, + participant: response.key?.remoteJid, + status: 'DELETED', + instanceId: this.instanceId, + }; + await this.prismaRepository.messageUpdate.create({ + data: messageUpdate, + }); } else { await this.prismaRepository.message.deleteMany({ where: { - id: messageId, + id: message.id, }, }); } @@ -3009,7 +3056,7 @@ export class BaileysStartupService extends ChannelStartupService { instanceId: message.instanceId, key: message.key, messageType: message.messageType, - status: message.status, + status: 'DELETED', source: message.source, messageTimestamp: message.messageTimestamp, pushName: message.pushName, @@ -3293,13 +3340,72 @@ export class BaileysStartupService extends ChannelStartupService { } try { - return await this.client.sendMessage(jid, { + const response = await this.client.sendMessage(jid, { ...(options as any), edit: data.key, }); + if (response) { + const messageId = response.message?.protocolMessage?.key?.id; + if (messageId) { + let message = await this.prismaRepository.message.findFirst({ + where: { + key: { + path: ['id'], + equals: messageId, + }, + }, + }); + if (!message) throw new NotFoundException('Message not found'); + + if (!(message.key.valueOf() as any).fromMe) { + new BadRequestException('You cannot edit others messages'); + } + if ((message.key.valueOf() as any)?.deleted) { + new BadRequestException('You cannot edit deleted messages'); + } + + const updateMessage = this.prepareMessage({ ...response }); + message = await this.prismaRepository.message.update({ + where: { id: message.id }, + data: { + message: { + ...updateMessage?.message?.[updateMessage.messageType]?.editedMessage, + }, + status: 'EDITED', + }, + }); + const messageUpdate: any = { + messageId: message.id, + keyId: messageId, + remoteJid: response.key.remoteJid, + fromMe: response.key.fromMe, + participant: response.key?.remoteJid, + status: 'EDITED', + instanceId: this.instanceId, + }; + await this.prismaRepository.messageUpdate.create({ + data: messageUpdate, + }); + + this.sendDataWebhook(Events.MESSAGES_EDITED, { + id: message.id, + instanceId: message.instanceId, + key: message.key, + messageType: message.messageType, + status: 'EDITED', + source: message.source, + messageTimestamp: message.messageTimestamp, + pushName: message.pushName, + participant: message.participant, + message: message.message, + }); + } + } + + return response; } catch (error) { this.logger.error(error); - throw new BadRequestException(error.toString()); + throw error; } } @@ -3915,4 +4021,118 @@ export class BaileysStartupService extends ChannelStartupService { return response; } + + //Catalogs and collections + public async fetchCatalog(instanceName: string, data: getCatalogDto) { + const jid = data.number ? createJid(data.number) : this.client?.user?.id; + const limit = data.limit || 10; + const cursor = data.cursor || null; + + const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + + if (!onWhatsapp.exists) { + throw new BadRequestException(onWhatsapp); + } + + try { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const business = await this.fetchBusinessProfile(info?.jid); + const catalog = await this.getCatalog({ jid: info?.jid, limit, cursor }); + + return { + wuid: info?.jid || jid, + name: info?.name, + numberExists: info?.exists, + isBusiness: business.isBusiness, + catalogLength: catalog?.products.length, + catalog: catalog?.products, + }; + } catch (error) { + console.log(error); + return { + wuid: jid, + name: null, + isBusiness: false, + }; + } + } + + public async getCatalog({ + jid, + limit, + cursor, + }: GetCatalogOptions): Promise<{ products: Product[]; nextPageCursor: string | undefined }> { + try { + jid = jid ? createJid(jid) : this.instance.wuid; + + const catalog = await this.client.getCatalog({ jid, limit: limit, cursor: cursor }); + + if (!catalog) { + return { + products: undefined, + nextPageCursor: undefined, + }; + } + + return catalog; + } catch (error) { + throw new InternalServerErrorException('Error getCatalog', error.toString()); + } + } + + public async fetchCatalogCollections(instanceName: string, data: getCollectionsDto) { + const jid = data.number ? createJid(data.number) : this.client?.user?.id; + const limit = data.limit || 10; + + const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + + if (!onWhatsapp.exists) { + throw new BadRequestException(onWhatsapp); + } + + try { + const info = (await this.whatsappNumber({ numbers: [jid] }))?.shift(); + const business = await this.fetchBusinessProfile(info?.jid); + const catalogCollections = await this.getCollections(info?.jid, limit); + + return { + wuid: info?.jid || jid, + name: info?.name, + numberExists: info?.exists, + isBusiness: business.isBusiness, + catalogLength: catalogCollections?.length, + catalogCollections: catalogCollections, + }; + } catch (error) { + console.log(error); + return { + wuid: jid, + name: null, + isBusiness: false, + }; + } + } + + public async getCollections(jid?: string | undefined, limit?: number): Promise { + try { + jid = jid ? createJid(jid) : this.instance.wuid; + + const result = await this.client.getCollections(jid, limit); + + if (!result) { + return [ + { + id: undefined, + name: undefined, + products: [], + status: undefined, + }, + ]; + } + + return result.collections; + } catch (error) { + throw new InternalServerErrorException('Error getCatalog', error.toString()); + } + } } diff --git a/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts b/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts index d7623441..22defde5 100644 --- a/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts +++ b/src/api/integrations/event/rabbitmq/rabbitmq.controller.ts @@ -87,6 +87,7 @@ export class RabbitmqController extends EventController implements EventControll const rabbitmqLocal = instanceRabbitmq?.events; const rabbitmqGlobal = configService.get('RABBITMQ').GLOBAL_ENABLED; const rabbitmqEvents = configService.get('RABBITMQ').EVENTS; + const prefixKey = configService.get('RABBITMQ').PREFIX_KEY; const rabbitmqExchangeName = configService.get('RABBITMQ').EXCHANGE_NAME; const we = event.replace(/[.-]/gm, '_').toUpperCase(); const logEnabled = configService.get('LOG').LEVEL.includes('WEBHOOKS'); @@ -159,7 +160,9 @@ export class RabbitmqController extends EventController implements EventControll autoDelete: false, }); - const queueName = event; + const queueName = prefixKey + ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` + : event.replace(/_/g, '.').toLowerCase(); await this.amqpChannel.assertQueue(queueName, { durable: true, @@ -195,6 +198,7 @@ export class RabbitmqController extends EventController implements EventControll const rabbitmqExchangeName = configService.get('RABBITMQ').EXCHANGE_NAME; const events = configService.get('RABBITMQ').EVENTS; + const prefixKey = configService.get('RABBITMQ').PREFIX_KEY; if (!events) { this.logger.warn('No events to initialize on AMQP'); @@ -207,7 +211,10 @@ export class RabbitmqController extends EventController implements EventControll eventKeys.forEach((event) => { if (events[event] === false) return; - const queueName = `${event.replace(/_/g, '.').toLowerCase()}`; + const queueName = + prefixKey !== '' + ? `${prefixKey}.${event.replace(/_/g, '.').toLowerCase()}` + : `${event.replace(/_/g, '.').toLowerCase()}`; const exchangeName = rabbitmqExchangeName; this.amqpChannel.assertExchange(exchangeName, 'topic', { diff --git a/src/api/routes/chat.router.ts b/src/api/routes/chat.router.ts index 20126c1a..aac9fe39 100644 --- a/src/api/routes/chat.router.ts +++ b/src/api/routes/chat.router.ts @@ -22,6 +22,8 @@ import { Contact, Message, MessageUpdate } from '@prisma/client'; import { archiveChatSchema, blockUserSchema, + catalogSchema, + collectionsSchema, contactValidateSchema, deleteMessageSchema, markChatUnreadSchema, @@ -267,6 +269,28 @@ export class ChatRouter extends RouterBroker { }); return res.status(HttpStatus.CREATED).json(response); + }) + + .post(this.routerPath('fetchCatalog'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: catalogSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchCatalog(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); + }) + + .post(this.routerPath('fetchCollections'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: collectionsSchema, + ClassRef: NumberDto, + execute: (instance, data) => chatController.fetchCatalogCollections(instance, data), + }); + + return res.status(HttpStatus.OK).json(response); }); } diff --git a/src/api/services/channel.service.ts b/src/api/services/channel.service.ts index 29bed428..66c59679 100644 --- a/src/api/services/channel.service.ts +++ b/src/api/services/channel.service.ts @@ -559,7 +559,7 @@ export class ChannelStartupService { "Message"."messageTimestamp" DESC ) SELECT * FROM rankedMessages - ORDER BY updatedAt DESC NULLS LAST; + ORDER BY "updatedAt" DESC NULLS LAST; `; if (results && isArray(results) && results.length > 0) { diff --git a/src/config/env.config.ts b/src/config/env.config.ts index a46fb2aa..8ba1fa83 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -97,6 +97,7 @@ export type Rabbitmq = { EXCHANGE_NAME: string; GLOBAL_ENABLED: boolean; EVENTS: EventsRabbitmq; + PREFIX_KEY?: string; }; export type Sqs = { @@ -355,6 +356,7 @@ export class ConfigService { RABBITMQ: { ENABLED: process.env?.RABBITMQ_ENABLED === 'true', GLOBAL_ENABLED: process.env?.RABBITMQ_GLOBAL_ENABLED === 'true', + PREFIX_KEY: process.env?.RABBITMQ_PREFIX_KEY, EXCHANGE_NAME: process.env?.RABBITMQ_EXCHANGE_NAME || 'evolution_exchange', URI: process.env.RABBITMQ_URI || '', EVENTS: { diff --git a/src/utils/use-multi-file-auth-state-prisma.ts b/src/utils/use-multi-file-auth-state-prisma.ts index 02f96f15..e16dc8b0 100644 --- a/src/utils/use-multi-file-auth-state-prisma.ts +++ b/src/utils/use-multi-file-auth-state-prisma.ts @@ -5,14 +5,14 @@ import { AuthenticationState, BufferJSON, initAuthCreds, WAProto as proto } from import fs from 'fs/promises'; import path from 'path'; -// const fixFileName = (file: string): string | undefined => { -// if (!file) { -// return undefined; -// } -// const replacedSlash = file.replace(/\//g, '__'); -// const replacedColon = replacedSlash.replace(/:/g, '-'); -// return replacedColon; -// }; +const fixFileName = (file: string): string | undefined => { + if (!file) { + return undefined; + } + const replacedSlash = file.replace(/\//g, '__'); + const replacedColon = replacedSlash.replace(/:/g, '-'); + return replacedColon; +}; export async function keyExists(sessionId: string): Promise { try { @@ -63,14 +63,14 @@ async function deleteAuthKey(sessionId: string): Promise { } } -// async function fileExists(file: string): Promise { -// try { -// const stat = await fs.stat(file); -// if (stat.isFile()) return true; -// } catch (error) { -// return; -// } -// } +async function fileExists(file: string): Promise { + try { + const stat = await fs.stat(file); + if (stat.isFile()) return true; + } catch (error) { + return; + } +} export default async function useMultiFileAuthStatePrisma( sessionId: string, @@ -80,16 +80,19 @@ export default async function useMultiFileAuthStatePrisma( saveCreds: () => Promise; }> { const localFolder = path.join(INSTANCE_DIR, sessionId); - // const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json'); + const localFile = (key: string) => path.join(localFolder, fixFileName(key) + '.json'); await fs.mkdir(localFolder, { recursive: true }); async function writeData(data: any, key: string): Promise { const dataString = JSON.stringify(data, BufferJSON.replacer); if (key != 'creds') { - return await cache.hSet(sessionId, key, data); - // await fs.writeFile(localFile(key), dataString); - // return; + if (process.env.CACHE_REDIS_ENABLED === 'true') { + return await cache.hSet(sessionId, key, data); + } else { + await fs.writeFile(localFile(key), dataString); + return; + } } await saveKey(sessionId, dataString); return; @@ -100,9 +103,13 @@ export default async function useMultiFileAuthStatePrisma( let rawData; if (key != 'creds') { - return await cache.hGet(sessionId, key); - // if (!(await fileExists(localFile(key)))) return null; - // rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' }); + if (process.env.CACHE_REDIS_ENABLED === 'true') { + return await cache.hGet(sessionId, key); + } else { + if (!(await fileExists(localFile(key)))) return null; + rawData = await fs.readFile(localFile(key), { encoding: 'utf-8' }); + return JSON.parse(rawData, BufferJSON.reviver); + } } else { rawData = await getAuthKey(sessionId); } @@ -117,8 +124,11 @@ export default async function useMultiFileAuthStatePrisma( async function removeData(key: string): Promise { try { if (key != 'creds') { - return await cache.hDelete(sessionId, key); - // await fs.unlink(localFile(key)); + if (process.env.CACHE_REDIS_ENABLED === 'true') { + return await cache.hDelete(sessionId, key); + } else { + await fs.unlink(localFile(key)); + } } else { await deleteAuthKey(sessionId); } diff --git a/src/validate/chat.schema.ts b/src/validate/chat.schema.ts index dba27995..fd324c10 100644 --- a/src/validate/chat.schema.ts +++ b/src/validate/chat.schema.ts @@ -315,3 +315,21 @@ export const profileSchema: JSONSchema7 = { isBusiness: { type: 'boolean' }, }, }; + +export const catalogSchema: JSONSchema7 = { + type: 'object', + properties: { + number: { type: 'string' }, + limit: { type: 'number' }, + cursor: { type: 'string' }, + }, +}; + +export const collectionsSchema: JSONSchema7 = { + type: 'object', + properties: { + number: { type: 'string' }, + limit: { type: 'number' }, + cursor: { type: 'string' }, + }, +};