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 }} diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 60e857fcc..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 @@ -553,6 +558,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'); @@ -989,12 +998,14 @@ 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.historySyncChatCount += chatsRaw.length; + + this.sendDataWebhook(Events.CHATS_SET, chatsRaw); + const messagesRaw: any[] = []; const messagesRepository: Set = new Set( @@ -1046,15 +1057,17 @@ export class BaileysStartupService extends ChannelStartupService { messagesRaw.push(this.prepareMessage(m)); } - this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw], true, undefined, { - isLatest, - progress, - }); + this.historySyncMessageCount += messagesRaw.length; 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('CHATWOOT').ENABLED && this.localChatwoot?.enabled && @@ -1067,10 +1080,25 @@ 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: this.historySyncMessageCount, + chatCount: this.historySyncChatCount, + contactCount: this.historySyncContactCount, + }); + + this.historySyncMessageCount = 0; + this.historySyncChatCount = 0; + this.historySyncContactCount = 0; + } + contacts = undefined; messages = undefined; chats = undefined; @@ -1389,50 +1417,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]); @@ -1478,9 +1505,11 @@ 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); - this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + if (messageRaw.messageType === 'audioMessage' && !messageRaw.key.fromMe && messageRaw.key.id) { + await this.baileysCache.set(this.getUpsertEmittedCacheKey(messageRaw.key.id), true, 60 * 10); + } await chatbotController.emit({ instance: { instanceName: this.instance.name, instanceId: this.instanceId }, @@ -1649,6 +1678,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 = this.getUpsertEmittedCacheKey(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; } 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', ], }, },