Skip to content

Commit 8fbd7d1

Browse files
committed
feat: integrate stability improvements from upstream PRs
Integrates 3 key stability improvements: • PR EvolutionAPI#2420: Prevent Evolution instances from getting stuck in 'close' state - Auto-connect Evolution instances on startup - Force 'open' state for webhook-based integrations - Update monitor service to handle Evolution integration • PR EvolutionAPI#2372: Enhance contact and chat handling with improved JID mapping - Fix remoteLid vs remoteJid mapping in messaging-history.set - Add debug logs for better troubleshooting - Prevent orphaned chats without proper contact association - Clean remoteLid before database storage • PR EvolutionAPI#2427: Improve audio upsert reliability and GHCR workflow - Prevent early returns that skip message upsert emission - Add guarded fallback for inbound audio messages - Add GitHub Actions workflow for GHCR image publishing These improvements enhance: - Connection stability for Evolution instances - Contact/chat mapping accuracy - Audio message reliability - CI/CD with GHCR support All changes are non-breaking and backward compatible.
1 parent 0fa89a2 commit 8fbd7d1

5 files changed

Lines changed: 209 additions & 49 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Build and Publish GHCR image
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
tags:
8+
- "v*"
9+
workflow_dispatch:
10+
11+
env:
12+
REGISTRY: ghcr.io
13+
IMAGE_NAME: ${{ github.repository_owner }}/evolution-api
14+
15+
jobs:
16+
build-and-push:
17+
name: Build and Push GHCR
18+
runs-on: ubuntu-latest
19+
permissions:
20+
contents: read
21+
packages: write
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v5
26+
with:
27+
submodules: recursive
28+
29+
- name: Set up QEMU
30+
uses: docker/setup-qemu-action@v3
31+
32+
- name: Set up Docker Buildx
33+
uses: docker/setup-buildx-action@v3
34+
35+
- name: Log in to GHCR
36+
uses: docker/login-action@v3
37+
with:
38+
registry: ${{ env.REGISTRY }}
39+
username: ${{ github.actor }}
40+
password: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Extract metadata
43+
id: meta
44+
uses: docker/metadata-action@v5
45+
with:
46+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
47+
tags: |
48+
type=raw,value=latest,enable={{is_default_branch}}
49+
type=ref,event=branch
50+
type=ref,event=tag
51+
type=sha
52+
53+
- name: Build and push
54+
id: build-and-push
55+
uses: docker/build-push-action@v6
56+
with:
57+
context: .
58+
push: true
59+
platforms: linux/amd64,linux/arm64
60+
tags: ${{ steps.meta.outputs.tags }}
61+
labels: ${{ steps.meta.outputs.labels }}
62+
cache-from: type=gha
63+
cache-to: type=gha,mode=max
64+
65+
- name: Image digest
66+
run: echo ${{ steps.build-and-push.outputs.digest }}

src/api/controllers/instance.controller.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ export class InstanceController {
156156
getQrcode = instance.qrCode;
157157
}
158158

159+
if (instanceData.integration === Integration.EVOLUTION) {
160+
await instance.connectToWhatsapp();
161+
}
162+
159163
const result = {
160164
instance: {
161165
instanceName: instance.instanceName,

src/api/integrations/channel/evolution/evolution.channel.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ export class EvolutionStartupService extends ChannelStartupService {
115115

116116
public async connectToWhatsapp(data?: any): Promise<any> {
117117
if (!data) {
118+
this.stateConnection = { state: 'open' };
119+
120+
if (this.instanceId) {
121+
await this.prismaRepository.instance.update({
122+
where: { id: this.instanceId },
123+
data: { connectionStatus: 'open' },
124+
});
125+
}
126+
118127
this.loadChatwoot();
119128
return;
120129
}

src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Lines changed: 125 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,10 @@ export class BaileysStartupService extends ChannelStartupService {
553553
}
554554
}
555555

556+
private getUpsertEmittedCacheKey(messageId: string) {
557+
return `upsert_emitted_${this.instanceId}_${messageId}`;
558+
}
559+
556560
private async defineAuthState() {
557561
const db = this.configService.get<Database>('DATABASE');
558562
const cache = this.configService.get<CacheConf>('CACHE');
@@ -939,6 +943,14 @@ export class BaileysStartupService extends ChannelStartupService {
939943
progress?: number;
940944
syncType?: proto.HistorySync.HistorySyncType;
941945
}) => {
946+
//These logs are crucial; when something changes in Baileys/WhatsApp, we can more easily understand what changed!
947+
this.logger.debug('Messages abaixo');
948+
this.logger.debug(messages);
949+
this.logger.debug('Chats abaixo');
950+
this.logger.debug(chats);
951+
this.logger.debug('Contatos abaixo');
952+
this.logger.debug(contacts);
953+
942954
try {
943955
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
944956
console.log('received on-demand history sync, messages=', messages);
@@ -967,14 +979,29 @@ export class BaileysStartupService extends ChannelStartupService {
967979
}
968980

969981
const contactsMap = new Map();
982+
const contactsMapLidJid = new Map();
970983

971984
for (const contact of contacts) {
985+
let jid = null;
986+
987+
if (contact?.id?.search('@lid') !== -1) {
988+
if (contact.phoneNumber) {
989+
jid = contact.phoneNumber;
990+
}
991+
}
992+
993+
if (!jid) {
994+
jid = contact?.id;
995+
}
996+
972997
if (contact.id && (contact.notify || contact.name)) {
973-
contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid: contact.id });
998+
contactsMap.set(contact.id, { name: contact.name ?? contact.notify, jid });
974999
}
1000+
1001+
contactsMapLidJid.set(contact.id, { jid });
9751002
}
9761003

977-
const chatsRaw: { remoteJid: string; instanceId: string; name?: string }[] = [];
1004+
const chatsRaw: { remoteJid: string; remoteLid: string; instanceId: string; name?: string }[] = [];
9781005
const chatsRepository = new Set(
9791006
(await this.prismaRepository.chat.findMany({ where: { instanceId: this.instanceId } })).map(
9801007
(chat) => chat.remoteJid,
@@ -986,13 +1013,31 @@ export class BaileysStartupService extends ChannelStartupService {
9861013
continue;
9871014
}
9881015

989-
chatsRaw.push({ remoteJid: chat.id, instanceId: this.instanceId, name: chat.name });
1016+
let remoteJid = null;
1017+
let remoteLid = null;
1018+
1019+
if (chat.id.search('@lid') !== -1) {
1020+
remoteLid = chat.id;
1021+
if (contactsMapLidJid.has(chat.id)) {
1022+
remoteJid = contactsMapLidJid.get(chat.id).jid;
1023+
}
1024+
}
1025+
1026+
if (!remoteJid) {
1027+
remoteJid = chat.id;
1028+
}
1029+
1030+
chatsRaw.push({ remoteJid, remoteLid, instanceId: this.instanceId, name: chat.name });
9901031
}
9911032

9921033
this.sendDataWebhook(Events.CHATS_SET, chatsRaw);
9931034

9941035
if (this.configService.get<Database>('DATABASE').SAVE_DATA.HISTORIC) {
995-
await this.prismaRepository.chat.createMany({ data: chatsRaw, skipDuplicates: true });
1036+
const chatsToCreateMany = JSON.parse(JSON.stringify(chatsRaw)).map((chat) => {
1037+
delete chat.remoteLid;
1038+
return chat;
1039+
});
1040+
await this.prismaRepository.chat.createMany({ data: chatsToCreateMany, skipDuplicates: true });
9961041
}
9971042

9981043
const messagesRaw: any[] = [];
@@ -1389,50 +1434,49 @@ export class BaileysStartupService extends ChannelStartupService {
13891434
try {
13901435
if (isVideo && !this.configService.get<S3>('S3').SAVE_VIDEO) {
13911436
this.logger.warn('Video upload is disabled. Skipping video upload.');
1392-
// Skip video upload by returning early from this block
1393-
return;
1394-
}
1395-
1396-
const message: any = received;
1397-
1398-
// Verificação adicional para garantir que há conteúdo de mídia real
1399-
const hasRealMedia = this.hasValidMediaContent(message);
1400-
1401-
if (!hasRealMedia) {
1402-
this.logger.warn('Message detected as media but contains no valid media content');
14031437
} else {
1404-
const media = await this.getBase64FromMediaMessage({ message }, true);
1405-
1406-
if (!media) {
1407-
this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO');
1408-
return;
1438+
const message: any = received;
1439+
1440+
// Verificação adicional para garantir que há conteúdo de mídia real
1441+
const hasRealMedia = this.hasValidMediaContent(message);
1442+
1443+
if (!hasRealMedia) {
1444+
this.logger.warn('Message detected as media but contains no valid media content');
1445+
} else {
1446+
const media = await this.getBase64FromMediaMessage({ message }, true);
1447+
1448+
if (!media) {
1449+
this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO');
1450+
} else {
1451+
const { buffer, mediaType, fileName, size } = media;
1452+
const mimetype = mimeTypes.lookup(fileName).toString();
1453+
const fullName = join(
1454+
`${this.instance.id}`,
1455+
received.key.remoteJid,
1456+
mediaType,
1457+
`${Date.now()}_${fileName}`,
1458+
);
1459+
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, {
1460+
'Content-Type': mimetype,
1461+
});
1462+
1463+
await this.prismaRepository.media.create({
1464+
data: {
1465+
messageId: msg.id,
1466+
instanceId: this.instanceId,
1467+
type: mediaType,
1468+
fileName: fullName,
1469+
mimetype,
1470+
},
1471+
});
1472+
1473+
const mediaUrl = await s3Service.getObjectUrl(fullName);
1474+
1475+
messageRaw.message.mediaUrl = mediaUrl;
1476+
1477+
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
1478+
}
14091479
}
1410-
1411-
const { buffer, mediaType, fileName, size } = media;
1412-
const mimetype = mimeTypes.lookup(fileName).toString();
1413-
const fullName = join(
1414-
`${this.instance.id}`,
1415-
received.key.remoteJid,
1416-
mediaType,
1417-
`${Date.now()}_${fileName}`,
1418-
);
1419-
await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype });
1420-
1421-
await this.prismaRepository.media.create({
1422-
data: {
1423-
messageId: msg.id,
1424-
instanceId: this.instanceId,
1425-
type: mediaType,
1426-
fileName: fullName,
1427-
mimetype,
1428-
},
1429-
});
1430-
1431-
const mediaUrl = await s3Service.getObjectUrl(fullName);
1432-
1433-
messageRaw.message.mediaUrl = mediaUrl;
1434-
1435-
await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw });
14361480
}
14371481
} catch (error) {
14381482
this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);
@@ -1478,9 +1522,11 @@ export class BaileysStartupService extends ChannelStartupService {
14781522
if (messageRaw.key.remoteJid?.includes('@lid') && messageRaw.key.remoteJidAlt) {
14791523
messageRaw.key.remoteJid = messageRaw.key.remoteJidAlt;
14801524
}
1481-
console.log(messageRaw);
1525+
await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
14821526

1483-
this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw);
1527+
if (messageRaw.messageType === 'audioMessage' && !messageRaw.key.fromMe && messageRaw.key.id) {
1528+
await this.baileysCache.set(this.getUpsertEmittedCacheKey(messageRaw.key.id), true, 60 * 10);
1529+
}
14841530

14851531
await chatbotController.emit({
14861532
instance: { instanceName: this.instance.name, instanceId: this.instanceId },
@@ -1649,6 +1695,37 @@ export class BaileysStartupService extends ChannelStartupService {
16491695
this.logger.warn(`Original message not found for update. Skipping. Key: ${JSON.stringify(key)}`);
16501696
continue;
16511697
}
1698+
1699+
if (!key.fromMe && findMessage.messageType === 'audioMessage' && key.id) {
1700+
const upsertCacheKey = this.getUpsertEmittedCacheKey(key.id);
1701+
const alreadyEmitted = await this.baileysCache.get(upsertCacheKey);
1702+
1703+
if (!alreadyEmitted) {
1704+
const fallbackUpsertPayload = {
1705+
key: findMessage.key,
1706+
pushName: findMessage.pushName,
1707+
status: findMessage.status,
1708+
message: findMessage.message,
1709+
contextInfo: findMessage.contextInfo,
1710+
messageType: findMessage.messageType,
1711+
messageTimestamp: findMessage.messageTimestamp,
1712+
instanceId: findMessage.instanceId,
1713+
source: findMessage.source,
1714+
};
1715+
1716+
try {
1717+
await this.sendDataWebhook(Events.MESSAGES_UPSERT, fallbackUpsertPayload);
1718+
await this.baileysCache.set(upsertCacheKey, true, 60 * 10);
1719+
this.logger.warn(`Fallback messages.upsert emitted for audio message ${key.id}`);
1720+
} catch (error) {
1721+
this.logger.error([
1722+
`Failed to emit fallback messages.upsert for audio message ${key.id}`,
1723+
error?.message,
1724+
]);
1725+
}
1726+
}
1727+
}
1728+
16521729
message.messageId = findMessage.id;
16531730
}
16541731

src/api/services/monitor.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,11 @@ export class WAMonitoringService {
293293
ownerJid: instanceData.ownerJid,
294294
});
295295

296-
if (instanceData.connectionStatus === 'open' || instanceData.connectionStatus === 'connecting') {
296+
if (
297+
instanceData.connectionStatus === 'open' ||
298+
instanceData.connectionStatus === 'connecting' ||
299+
instanceData.integration === Integration.EVOLUTION
300+
) {
297301
this.logger.info(
298302
`Auto-connecting instance "${instanceData.instanceName}" (status: ${instanceData.connectionStatus})`,
299303
);

0 commit comments

Comments
 (0)