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..554a37ba4 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=${POSTGRES_PASSWORD} 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..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, @@ -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) { @@ -771,10 +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, + remoteJid: resolveLidContact(contact), pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0], profilePicUrl: null, instanceId: this.instanceId, @@ -812,8 +820,8 @@ export class BaileysStartupService extends ChannelStartupService { } const updatedContacts = await Promise.all( - contacts.map(async (contact) => ({ - remoteJid: 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, @@ -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 LidMessageKey)?.remoteJidAlt) { + received.key.remoteJid = (received.key as LidMessageKey).remoteJidAlt; + } + if (settings?.groupsIgnore && received.key.remoteJid.includes('@g.us')) { continue; } @@ -1408,15 +1421,21 @@ export class BaileysStartupService extends ChannelStartupService { continue; } + // 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') + ? (lidKey.remoteJidAlt ?? (findMessage.key as LidMessageKey)?.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 +1455,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 +1473,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, @@ -1653,7 +1672,9 @@ 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 = 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 = [