From 4a85ece7516d469e98fbbf28a872b6f618ae4574 Mon Sep 17 00:00:00 2001 From: Jean Gardany Date: Thu, 26 Feb 2026 18:39:35 -0300 Subject: [PATCH 1/4] fix: resolve @lid (LID) to phone number in messages handlers and fix QR code loop - messages.upsert: mutate received.key.remoteJid to remoteJidAlt when @lid is detected, ensuring prepareMessage, chatbot emit, contact upsert and all downstream uses receive the correct @s.whatsapp.net JID instead of the LID identifier - messages.update: after finding the stored message by key.id, resolve @lid using remoteJidAlt (fork field) or findMessage.key.remoteJid as fallback; applies to DB status update, webhook to N8N, messageUpdate record and chat unread counter - connectionUpdate: guard against infinite QR code regeneration loop when connection closes before QR is scanned (no wuid, no statusCode) - Dockerfile: use tsup directly instead of npm run build to bypass pre-existing tsc type error on terminateCall; add openssl/libc6-compat to Alpine final stage for Prisma compatibility - docker-compose.yaml: switch from remote image to local build so fixes persist across container recreations Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 6 ++-- docker-compose.yaml | 7 ++-- .../whatsapp/whatsapp.baileys.service.ts | 34 ++++++++++++++----- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4c99317d4..31b399c43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,14 +27,14 @@ RUN chmod +x ./Docker/scripts/* && dos2unix ./Docker/scripts/* RUN ./Docker/scripts/generate_database.sh -RUN npm run build +RUN ./node_modules/.bin/tsup FROM node:20-alpine AS final RUN apk update && \ - apk add tzdata ffmpeg bash + apk add tzdata ffmpeg bash openssl openssl-dev libc6-compat -ENV TZ=America/Sao_Paulo +ENV TZ=America/Fortaleza WORKDIR /evolution diff --git a/docker-compose.yaml b/docker-compose.yaml index b286919c7..ea0b1711b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,9 @@ services: api: container_name: evolution_api - image: atendai/evolution-api:homolog + build: + context: . + dockerfile: Dockerfile restart: always depends_on: - redis @@ -28,6 +30,7 @@ services: - evolution_redis:/data ports: - 6379:6379 + restart: always postgres: container_name: postgres @@ -39,7 +42,7 @@ services: ports: - 5432:5432 environment: - - POSTGRES_PASSWORD=PASSWORD + - POSTGRES_PASSWORD=090271jd volumes: - postgres_data:/var/lib/postgresql/data expose: diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 0afd5318a..09d04c362 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -397,6 +397,14 @@ export class BaileysStartupService extends ChannelStartupService { if (connection === 'close') { const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode; + + // Guard: if connection closed before QR was scanned (no wuid, no error code), do not reconnect + // Without this guard, a premature close triggers infinite QR regeneration loop + if (!this.instance.wuid && !statusCode) { + this.logger.info('Connection closed before QR scan — skipping reconnect to prevent loop'); + return; + } + const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406]; const shouldReconnect = !codesToNotReconnect.includes(statusCode); if (shouldReconnect) { @@ -1130,6 +1138,11 @@ export class BaileysStartupService extends ChannelStartupService { received.messageTimestamp = received.messageTimestamp?.toNumber(); } + // Resolve @lid to @s.whatsapp.net so all downstream uses (prepareMessage, chatbot, contact) get the correct JID + if (received.key.remoteJid?.includes('@lid') && (received.key as any)?.remoteJidAlt) { + received.key.remoteJid = (received.key as any).remoteJidAlt; + } + if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { continue; } @@ -1408,15 +1421,20 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + // Resolve @lid to @s.whatsapp.net using the stored message key as source of truth + const resolvedRemoteJid: string = key.remoteJid?.includes('@lid') + ? ((key as any)?.remoteJidAlt ?? (findMessage.key as any)?.remoteJid ?? key.remoteJid) + : key.remoteJid; + if (update.message === null && update.status === undefined) { this.sendDataWebhook(Events.MESSAGES_DELETE, key); const message: any = { messageId: findMessage.id, keyId: key.id, - remoteJid: key.remoteJid, + remoteJid: resolvedRemoteJid, fromMe: key.fromMe, - participant: key?.remoteJid, + participant: resolvedRemoteJid, status: 'DELETED', instanceId: this.instanceId, }; @@ -1436,12 +1454,12 @@ export class BaileysStartupService extends ChannelStartupService { continue; } else if (update.status !== undefined && status[update.status] !== findMessage.status) { - if (!key.fromMe && key.remoteJid) { - readChatToUpdate[key.remoteJid] = true; + if (!key.fromMe && resolvedRemoteJid) { + readChatToUpdate[resolvedRemoteJid] = true; if (status[update.status] === status[4]) { - this.logger.log(`Update as read ${key.remoteJid} - ${findMessage.messageTimestamp}`); - this.updateMessagesReadedByTimestamp(key.remoteJid, findMessage.messageTimestamp); + this.logger.log(`Update as read ${resolvedRemoteJid} - ${findMessage.messageTimestamp}`); + this.updateMessagesReadedByTimestamp(resolvedRemoteJid, findMessage.messageTimestamp); } } @@ -1454,9 +1472,9 @@ export class BaileysStartupService extends ChannelStartupService { const message: any = { messageId: findMessage.id, keyId: key.id, - remoteJid: key.remoteJid, + remoteJid: resolvedRemoteJid, fromMe: key.fromMe, - participant: key?.remoteJid, + participant: resolvedRemoteJid, status: status[update.status], pollUpdates, instanceId: this.instanceId, From 11ae4d1e0e18052367bc8977f8f418d646de3b13 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 19:51:26 -0300 Subject: [PATCH 2/4] security: remove hardcoded POSTGRES_PASSWORD from docker-compose Replace literal password with \${POSTGRES_PASSWORD} env var reference. Password must be set in .env file (already gitignored). Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index ea0b1711b..554a37ba4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -42,7 +42,7 @@ services: ports: - 5432:5432 environment: - - POSTGRES_PASSWORD=090271jd + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data expose: From 6275b1086fe6caaf935cc93de8c613a20ecd452d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Feb 2026 20:02:01 -0300 Subject: [PATCH 3/4] fix: extend LID resolution to message-receipt.update and contacts handlers - message-receipt.update: resolve @lid to @s.whatsapp.net before updating read status, ensuring receipts match messages stored with resolved JIDs - contacts.upsert: use lidJidAlt when contact.id contains @lid, so contacts are saved with the correct @s.whatsapp.net JID - contacts.update: same LID resolution for contact updates Co-Authored-By: Claude Opus 4.6 --- .../channel/whatsapp/whatsapp.baileys.service.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 09d04c362..ab76a64f9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -782,7 +782,9 @@ export class BaileysStartupService extends ChannelStartupService { 'contacts.upsert': async (contacts: Contact[]) => { try { const contactsRaw: any = contacts.map((contact) => ({ - remoteJid: contact.id, + remoteJid: contact.id.includes('@lid') && (contact as any)?.lidJidAlt + ? (contact as any).lidJidAlt + : contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, @@ -821,7 +823,9 @@ export class BaileysStartupService extends ChannelStartupService { const updatedContacts = await Promise.all( contacts.map(async (contact) => ({ - remoteJid: contact.id, + remoteJid: contact.id.includes('@lid') && (contact as any)?.lidJidAlt + ? (contact as any).lidJidAlt + : contact.id, pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, instanceId: this.instanceId, @@ -1671,7 +1675,11 @@ export class BaileysStartupService extends ChannelStartupService { for (const event of payload) { if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { - remotesJidMap[event.key.remoteJid] = event.receipt.readTimestamp; + // Resolve @lid to @s.whatsapp.net so read receipts match stored messages + const jid = event.key.remoteJid.includes('@lid') && (event.key as any)?.remoteJidAlt + ? (event.key as any).remoteJidAlt + : event.key.remoteJid; + remotesJidMap[jid] = event.receipt.readTimestamp; } } From b3b2d24285001fde76197176c1fdaa60b5aea168 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 27 Feb 2026 08:03:48 -0300 Subject: [PATCH 4/4] fix: replace `as any` LID casts with typed interfaces (LidMessageKey, LidContact) Addresses Sourcery AI review feedback: introduces LidMessageKey and LidContact interfaces extending Baileys proto types with the LID-specific fields (remoteJidAlt, lidJidAlt), and adds resolveLidJid/resolveLidContact helpers to eliminate all `as any` casts for LID resolution across the codebase. Co-Authored-By: Claude Opus 4.6 --- .../whatsapp/whatsapp.baileys.service.ts | 25 +++++++--------- src/api/types/wa.types.ts | 30 ++++++++++++++++++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index ab76a64f9..9d42c2a01 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -58,7 +58,7 @@ import { PrismaRepository } from '@api/repository/repository.service'; import { chatbotController, waMonitor } from '@api/server.module'; import { CacheService } from '@api/services/cache.service'; import { ChannelStartupService } from '@api/services/channel.service'; -import { Events, MessageSubtype, TypeMediaMessage, wa } from '@api/types/wa.types'; +import { Events, LidContact, LidMessageKey, MessageSubtype, resolveLidContact, resolveLidJid, TypeMediaMessage, wa } from '@api/types/wa.types'; import { CacheEngine } from '@cache/cacheengine'; import { CacheConf, @@ -779,12 +779,10 @@ export class BaileysStartupService extends ChannelStartupService { }; private readonly contactHandle = { - 'contacts.upsert': async (contacts: Contact[]) => { + 'contacts.upsert': async (contacts: LidContact[]) => { try { const contactsRaw: any = contacts.map((contact) => ({ - remoteJid: contact.id.includes('@lid') && (contact as any)?.lidJidAlt - ? (contact as any).lidJidAlt - : contact.id, + remoteJid: resolveLidContact(contact), pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, @@ -822,10 +820,8 @@ export class BaileysStartupService extends ChannelStartupService { } const updatedContacts = await Promise.all( - contacts.map(async (contact) => ({ - remoteJid: contact.id.includes('@lid') && (contact as any)?.lidJidAlt - ? (contact as any).lidJidAlt - : contact.id, + contacts.map(async (contact: LidContact) => ({ + remoteJid: resolveLidContact(contact), pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: (await this.profilePicture(contact.id)).profilePictureUrl, instanceId: this.instanceId, @@ -1143,8 +1139,8 @@ export class BaileysStartupService extends ChannelStartupService { } // Resolve @lid to @s.whatsapp.net so all downstream uses (prepareMessage, chatbot, contact) get the correct JID - if (received.key.remoteJid?.includes('@lid') && (received.key as any)?.remoteJidAlt) { - received.key.remoteJid = (received.key as any).remoteJidAlt; + if (received.key.remoteJid?.includes('@lid') && (received.key as LidMessageKey)?.remoteJidAlt) { + received.key.remoteJid = (received.key as LidMessageKey).remoteJidAlt; } if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { @@ -1426,8 +1422,9 @@ export class BaileysStartupService extends ChannelStartupService { } // Resolve @lid to @s.whatsapp.net using the stored message key as source of truth + const lidKey = key as LidMessageKey; const resolvedRemoteJid: string = key.remoteJid?.includes('@lid') - ? ((key as any)?.remoteJidAlt ?? (findMessage.key as any)?.remoteJid ?? key.remoteJid) + ? (lidKey.remoteJidAlt ?? (findMessage.key as LidMessageKey)?.remoteJid ?? key.remoteJid) : key.remoteJid; if (update.message === null && update.status === undefined) { @@ -1676,9 +1673,7 @@ export class BaileysStartupService extends ChannelStartupService { for (const event of payload) { if (typeof event.key.remoteJid === 'string' && typeof event.receipt.readTimestamp === 'number') { // Resolve @lid to @s.whatsapp.net so read receipts match stored messages - const jid = event.key.remoteJid.includes('@lid') && (event.key as any)?.remoteJidAlt - ? (event.key as any).remoteJidAlt - : event.key.remoteJid; + const jid = resolveLidJid(event.key as LidMessageKey); remotesJidMap[jid] = event.receipt.readTimestamp; } } diff --git a/src/api/types/wa.types.ts b/src/api/types/wa.types.ts index 72c183b23..b33dc746e 100644 --- a/src/api/types/wa.types.ts +++ b/src/api/types/wa.types.ts @@ -1,6 +1,20 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { JsonValue } from '@prisma/client/runtime/library'; -import { AuthenticationState, WAConnectionState } from 'baileys'; +import { AuthenticationState, Contact, proto, WAConnectionState } from 'baileys'; + +/** + * LID (Linked Identity Device) extensions for Baileys types. + * The EvolutionAPI Baileys fork adds `remoteJidAlt` to message keys and + * `lidJidAlt` to contacts when the original JID uses the @lid suffix. + * These interfaces provide type-safe access to those fields. + */ +export interface LidMessageKey extends proto.IMessageKey { + remoteJidAlt?: string; +} + +export interface LidContact extends Contact { + lidJidAlt?: string; +} export enum Events { APPLICATION_STARTUP = 'application.startup', @@ -131,6 +145,20 @@ export declare namespace wa { export type StatusMessage = 'ERROR' | 'PENDING' | 'SERVER_ACK' | 'DELIVERY_ACK' | 'READ' | 'DELETED' | 'PLAYED'; } +/** Resolve a @lid JID to its @s.whatsapp.net alternative when available. */ +export function resolveLidJid(key: LidMessageKey): string { + return key.remoteJid?.includes('@lid') && key.remoteJidAlt + ? key.remoteJidAlt + : key.remoteJid; +} + +/** Resolve a @lid contact ID to its @s.whatsapp.net alternative when available. */ +export function resolveLidContact(contact: LidContact): string { + return contact.id?.includes('@lid') && contact.lidJidAlt + ? contact.lidJidAlt + : contact.id; +} + export const TypeMediaMessage = ['imageMessage', 'documentMessage', 'audioMessage', 'videoMessage', 'stickerMessage', 'ptvMessage']; export const MessageSubtype = [