Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
services:
api:
container_name: evolution_api
image: atendai/evolution-api:homolog
build:
context: .
dockerfile: Dockerfile
restart: always
depends_on:
- redis
Expand All @@ -28,6 +30,7 @@ services:
- evolution_redis:/data
ports:
- 6379:6379
restart: always

postgres:
container_name: postgres
Expand All @@ -39,7 +42,7 @@ services:
ports:
- 5432:5432
environment:
- POSTGRES_PASSWORD=PASSWORD
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
expose:
Expand Down
49 changes: 35 additions & 14 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
};
Expand All @@ -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);
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
}

Expand Down
30 changes: 29 additions & 1 deletion src/api/types/wa.types.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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 = [
Expand Down