From 64e276dab938025f6e58eb88a4cd7ed780e55267 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Wed, 11 Feb 2026 21:21:16 +0000 Subject: [PATCH 1/5] fix(whatsapp): ensure audio messages always emit upsert events --- .../whatsapp/whatsapp.baileys.service.ts | 120 +++++++++++------- 1 file changed, 77 insertions(+), 43 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..85baedcdc 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1389,50 +1389,49 @@ export class BaileysStartupService extends ChannelStartupService { try { if (isVideo && !this.configService.get('S3').SAVE_VIDEO) { this.logger.warn('Video upload is disabled. Skipping video upload.'); - // Skip video upload by returning early from this block - return; - } - - const message: any = received; - - // Verificação adicional para garantir que há conteúdo de mídia real - const hasRealMedia = this.hasValidMediaContent(message); - - if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); } else { - const media = await this.getBase64FromMediaMessage({ message }, true); - - if (!media) { - this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); - return; + const message: any = received; + + // Verificação adicional para garantir que há conteúdo de mídia real + const hasRealMedia = this.hasValidMediaContent(message); + + if (!hasRealMedia) { + this.logger.warn('Message detected as media but contains no valid media content'); + } else { + const media = await this.getBase64FromMediaMessage({ message }, true); + + if (!media) { + this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); + } else { + const { buffer, mediaType, fileName, size } = media; + const mimetype = mimeTypes.lookup(fileName).toString(); + const fullName = join( + `${this.instance.id}`, + received.key.remoteJid, + mediaType, + `${Date.now()}_${fileName}`, + ); + await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { + 'Content-Type': mimetype, + }); + + await this.prismaRepository.media.create({ + data: { + messageId: msg.id, + instanceId: this.instanceId, + type: mediaType, + fileName: fullName, + mimetype, + }, + }); + + const mediaUrl = await s3Service.getObjectUrl(fullName); + + messageRaw.message.mediaUrl = mediaUrl; + + await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); + } } - - const { buffer, mediaType, fileName, size } = media; - const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join( - `${this.instance.id}`, - received.key.remoteJid, - mediaType, - `${Date.now()}_${fileName}`, - ); - await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype }); - - await this.prismaRepository.media.create({ - data: { - messageId: msg.id, - instanceId: this.instanceId, - type: mediaType, - fileName: fullName, - mimetype, - }, - }); - - const mediaUrl = await s3Service.getObjectUrl(fullName); - - messageRaw.message.mediaUrl = mediaUrl; - - await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]); @@ -1480,7 +1479,11 @@ export class BaileysStartupService extends ChannelStartupService { } console.log(messageRaw); - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + + if (messageRaw.messageType === 'audioMessage' && !messageRaw.key.fromMe && messageRaw.key.id) { + await this.baileysCache.set(`upsert_emitted_${this.instanceId}_${messageRaw.key.id}`, true, 60 * 10); + } await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, @@ -1649,6 +1652,37 @@ export class BaileysStartupService extends ChannelStartupService { this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`); continue; } + + if (!key.fromMe && findMessage.messageType === 'audioMessage' && key.id) { + const upsertCacheKey = `upsert_emitted_${this.instanceId}_${key.id}`; + const alreadyEmitted = await this.baileysCache.get(upsertCacheKey); + + if (!alreadyEmitted) { + const fallbackUpsertPayload = { + key: findMessage.key, + pushName: findMessage.pushName, + status: findMessage.status, + message: findMessage.message, + contextInfo: findMessage.contextInfo, + messageType: findMessage.messageType, + messageTimestamp: findMessage.messageTimestamp, + instanceId: findMessage.instanceId, + source: findMessage.source, + }; + + try { + await this.sendDataWebhook(Events.MESSAGES_UPSERT, fallbackUpsertPayload); + await this.baileysCache.set(upsertCacheKey, true, 60 * 10); + this.logger.warn(`Fallback messages.upsert emitted for audio message ${key.id}`); + } catch (error) { + this.logger.error([ + `Failed to emit fallback messages.upsert for audio message ${key.id}`, + error?.message, + ]); + } + } + } + message.messageId = findMessage.id; } From 24e6695e7765ed539adac62a0a8f0abeab6bcaf5 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Wed, 11 Feb 2026 21:21:41 +0000 Subject: [PATCH 2/5] ci: publish Docker image to GHCR on fork pushes --- .github/workflows/publish_ghcr_image.yml | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/publish_ghcr_image.yml diff --git a/.github/workflows/publish_ghcr_image.yml b/.github/workflows/publish_ghcr_image.yml new file mode 100644 index 000000000..5dd7fab7b --- /dev/null +++ b/.github/workflows/publish_ghcr_image.yml @@ -0,0 +1,66 @@ +name: Build and Publish GHCR image + +on: + push: + branches: + - main + tags: + - "v*" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/evolution-api + +jobs: + build-and-push: + name: Build and Push GHCR + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=tag + type=sha + + - name: Build and push + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Image digest + run: echo ${{ steps.build-and-push.outputs.digest }} From bb0ff85942b6d031b1f9d83183bfaa4ba20c6f32 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Thu, 12 Feb 2026 08:07:52 +0000 Subject: [PATCH 3/5] chore(whatsapp): remove raw upsert logging and centralize cache key --- .../channel/whatsapp/whatsapp.baileys.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 85baedcdc..42862420f 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -553,6 +553,10 @@ export class BaileysStartupService extends ChannelStartupService { } } + private getUpsertEmittedCacheKey(messageId: string) { + return `upsert_emitted_${this.instanceId}_${messageId}`; + } + private async defineAuthState() { const db = this.configService.get('DATABASE'); const cache = this.configService.get('CACHE'); @@ -1477,12 +1481,10 @@ export class BaileysStartupService extends ChannelStartupService { if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) { messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt; } - console.log(messageRaw); - await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); if (messageRaw.messageType === 'audioMessage' && !messageRaw.key.fromMe && messageRaw.key.id) { - await this.baileysCache.set(`upsert_emitted_${this.instanceId}_${messageRaw.key.id}`, true, 60 * 10); + await this.baileysCache.set(this.getUpsertEmittedCacheKey(messageRaw.key.id), true, 60 * 10); } await chatbotController.emit({ @@ -1654,7 +1656,7 @@ export class BaileysStartupService extends ChannelStartupService { } if (!key.fromMe && findMessage.messageType === 'audioMessage' && key.id) { - const upsertCacheKey = `upsert_emitted_${this.instanceId}_${key.id}`; + const upsertCacheKey = this.getUpsertEmittedCacheKey(key.id); const alreadyEmitted = await this.baileysCache.get(upsertCacheKey); if (!alreadyEmitted) { From 13447e06bb805c92461a0a7131e636c5058e1064 Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Mon, 23 Feb 2026 21:31:20 +0000 Subject: [PATCH 4/5] feat(history-sync): emit messaging-history.set event on sync completion and fix race condition Reorder webhook emissions (CHATS_SET, MESSAGES_SET) to fire after database persistence, fixing a race condition where consumers received the event before data was queryable. Emit a new MESSAGING_HISTORY_SET event when progress reaches 100%, allowing consumers to know exactly when history sync is complete and messages are available in the database. Register the new event across all transport types (Webhook, WebSocket, RabbitMQ, NATS, SQS, Kafka, Pusher) and validation schemas. --- .../whatsapp/whatsapp.baileys.service.ts | 20 +++++++++++++------ .../integrations/event/event.controller.ts | 1 + src/config/env.config.ts | 10 ++++++++++ src/validate/instance.schema.ts | 4 ++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 42862420f..6227aa3d5 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -993,12 +993,12 @@ export class BaileysStartupService extends ChannelStartupService { chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name }); } - this.sendDataWebhook(Events.CHATS_SET, chatsRaw); - if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); } + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + const messagesRaw: any[] = []; const messagesRepository: Set = new Set( @@ -1050,15 +1050,15 @@ export class BaileysStartupService extends ChannelStartupService { messagesRaw.push(this.prepareMessage(m)); } + if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { + await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); + } + this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { isLatest, progress, }); - if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { - await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); - } - if ( this.configService.get('CHATWOOT').ENABLED && this.localChatwoot?.enabled && @@ -1075,6 +1075,14 @@ export class BaileysStartupService extends ChannelStartupService { contacts.filter((c) => !!c.notify || !!c.name).map((c) => ({ id: c.id, name: c.name ?? c.notify })), ); + if (progress === 100) { + this.sendDataWebhook(Events.MESSAGING_HISTORY_SET, { + messageCount: messagesRaw.length, + chatCount: chatsRaw.length, + contactCount: contacts?.length ?? 0, + }); + } + contacts = undefined; messages = undefined; chats = undefined; diff --git a/src/api/integrations/event/event.controller.ts b/src/api/integrations/event/event.controller.ts index 39b52184b..63061ea10 100644 --- a/src/api/integrations/event/event.controller.ts +++ b/src/api/integrations/event/event.controller.ts @@ -162,6 +162,7 @@ export class EventController { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', 'REMOVE_INSTANCE', 'LOGOUT_INSTANCE', 'INSTANCE_CREATE', diff --git a/src/config/env.config.ts b/src/config/env.config.ts index 7c4e382e7..772ae9279 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -91,6 +91,7 @@ export type EventsRabbitmq = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; }; export type Rabbitmq = { @@ -150,6 +151,7 @@ export type Sqs = { SEND_MESSAGE: boolean; TYPEBOT_CHANGE_STATUS: boolean; TYPEBOT_START: boolean; + MESSAGING_HISTORY_SET: boolean; }; }; @@ -223,6 +225,7 @@ export type EventsWebhook = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; ERRORS: boolean; ERRORS_WEBHOOK: string; }; @@ -256,6 +259,7 @@ export type EventsPusher = { CALL: boolean; TYPEBOT_START: boolean; TYPEBOT_CHANGE_STATUS: boolean; + MESSAGING_HISTORY_SET: boolean; }; export type ApiKey = { KEY: string }; @@ -537,6 +541,7 @@ export class ConfigService { CALL: process.env?.RABBITMQ_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.RABBITMQ_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.RABBITMQ_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.RABBITMQ_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, NATS: { @@ -574,6 +579,7 @@ export class ConfigService { CALL: process.env?.NATS_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.NATS_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.NATS_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.NATS_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, SQS: { @@ -614,6 +620,7 @@ export class ConfigService { SEND_MESSAGE: process.env?.SQS_GLOBAL_SEND_MESSAGE === 'true', TYPEBOT_CHANGE_STATUS: process.env?.SQS_GLOBAL_TYPEBOT_CHANGE_STATUS === 'true', TYPEBOT_START: process.env?.SQS_GLOBAL_TYPEBOT_START === 'true', + MESSAGING_HISTORY_SET: process.env?.SQS_GLOBAL_MESSAGING_HISTORY_SET === 'true', }, }, KAFKA: { @@ -657,6 +664,7 @@ export class ConfigService { CALL: process.env?.KAFKA_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.KAFKA_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.KAFKA_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.KAFKA_EVENTS_MESSAGING_HISTORY_SET === 'true', }, SASL: process.env?.KAFKA_SASL_ENABLED === 'true' @@ -722,6 +730,7 @@ export class ConfigService { CALL: process.env?.PUSHER_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.PUSHER_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.PUSHER_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.PUSHER_EVENTS_MESSAGING_HISTORY_SET === 'true', }, }, WA_BUSINESS: { @@ -779,6 +788,7 @@ export class ConfigService { CALL: process.env?.WEBHOOK_EVENTS_CALL === 'true', TYPEBOT_START: process.env?.WEBHOOK_EVENTS_TYPEBOT_START === 'true', TYPEBOT_CHANGE_STATUS: process.env?.WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS === 'true', + MESSAGING_HISTORY_SET: process.env?.WEBHOOK_EVENTS_MESSAGING_HISTORY_SET === 'true', ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true', ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '', }, diff --git a/src/validate/instance.schema.ts b/src/validate/instance.schema.ts index a0553b666..16fd4fe80 100644 --- a/src/validate/instance.schema.ts +++ b/src/validate/instance.schema.ts @@ -86,6 +86,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -123,6 +124,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -160,6 +162,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, @@ -197,6 +200,7 @@ export const instanceSchema: JSONSchema7 = { 'CALL', 'TYPEBOT_START', 'TYPEBOT_CHANGE_STATUS', + 'MESSAGING_HISTORY_SET', ], }, }, From 58eeaf67a80b79a5d2e476e9fb914a8f74a9112d Mon Sep 17 00:00:00 2001 From: Alexandre Reyes Martins Date: Mon, 23 Feb 2026 21:48:30 +0000 Subject: [PATCH 5/5] fix(history-sync): use cumulative counts in MESSAGING_HISTORY_SET event Track message, chat and contact counts across all history sync batches using instance-level counters, so the final event reports accurate totals instead of only the last batch counts. Addresses Sourcery review feedback on PR #2440. --- .../whatsapp/whatsapp.baileys.service.ts | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 6227aa3d5..8f12718fb 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -252,6 +252,11 @@ export class BaileysStartupService extends ChannelStartupService { private logBaileys = this.configService.get('LOG').BAILEYS; private eventProcessingQueue: Promise = Promise.resolve(); + // Cumulative history sync counters (reset on sync completion) + private historySyncMessageCount = 0; + private historySyncChatCount = 0; + private historySyncContactCount = 0; + // Cache TTL constants (in seconds) private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates @@ -997,6 +1002,8 @@ export class BaileysStartupService extends ChannelStartupService { await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true }); } + this.historySyncChatCount += chatsRaw.length; + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); const messagesRaw: any[] = []; @@ -1050,6 +1057,8 @@ export class BaileysStartupService extends ChannelStartupService { messagesRaw.push(this.prepareMessage(m)); } + this.historySyncMessageCount += messagesRaw.length; + if (this.configService.get('DATABASE').SAVE_DATA.HISTORIC) { await this.prismaRepository.message.createMany({ data: messagesRaw, skipDuplicates: true }); } @@ -1071,16 +1080,23 @@ export class BaileysStartupService extends ChannelStartupService { ); } + const filteredContacts = contacts.filter((c) => !!c.notify || !!c.name); + this.historySyncContactCount += filteredContacts.length; + await this.contactHandle['contacts.upsert']( - contacts.filter((c) => !!c.notify || !!c.name).map((c) => ({ id: c.id, name: c.name ?? c.notify })), + filteredContacts.map((c) => ({ id: c.id, name: c.name ?? c.notify })), ); if (progress === 100) { this.sendDataWebhook(Events.MESSAGING_HISTORY_SET, { - messageCount: messagesRaw.length, - chatCount: chatsRaw.length, - contactCount: contacts?.length ?? 0, + messageCount: this.historySyncMessageCount, + chatCount: this.historySyncChatCount, + contactCount: this.historySyncContactCount, }); + + this.historySyncMessageCount = 0; + this.historySyncChatCount = 0; + this.historySyncContactCount = 0; } contacts = undefined;