Skip to content

Commit 523d7bb

Browse files
author
Guilherme Vidal
committed
fix: stabilize baileys message sync
1 parent 8426e87 commit 523d7bb

1 file changed

Lines changed: 154 additions & 3 deletions

File tree

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

Lines changed: 154 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,16 @@ export class BaileysStartupService extends ChannelStartupService {
252252
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
253253
private eventProcessingQueue: Promise<void> = Promise.resolve();
254254
private groupMetadataRateLimitUntil = 0;
255+
private reconnectAttempts = 0;
255256

256257
// Cache TTL constants (in seconds)
257258
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
258259
private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates
260+
private readonly HISTORY_GAP_SECONDS = 10 * 60; // 10 minutes - request recent history when a chat jumps ahead
261+
private readonly HISTORY_GAP_SYNC_TTL_SECONDS = 15 * 60; // 15 minutes - avoid Baileys/WhatsApp rate limits
262+
private readonly HISTORY_GAP_GLOBAL_TTL_SECONDS = 10; // Pace cross-chat history requests
263+
private readonly HISTORY_GAP_SYNC_MESSAGE_COUNT = 50;
264+
private readonly MAX_RECONNECT_DELAY_MS = 30_000;
259265

260266
public stateConnection: wa.StateConnection = { state: 'close' };
261267

@@ -429,6 +435,15 @@ export class BaileysStartupService extends ChannelStartupService {
429435
const codesToNotReconnect = [DisconnectReason.loggedOut, DisconnectReason.forbidden, 402, 406];
430436
const shouldReconnect = !codesToNotReconnect.includes(statusCode);
431437
if (shouldReconnect) {
438+
this.reconnectAttempts += 1;
439+
const reconnectDelayMs = Math.min(
440+
1000 * 2 ** Math.min(this.reconnectAttempts - 1, 5),
441+
this.MAX_RECONNECT_DELAY_MS,
442+
);
443+
this.logger.warn(
444+
`WhatsApp connection closed for ${this.instance.name}. Reconnecting in ${reconnectDelayMs}ms. Reason: ${statusCode}`,
445+
);
446+
await delay(reconnectDelayMs);
432447
await this.connectToWhatsapp(this.phoneNumber);
433448
} else {
434449
this.sendDataWebhook(Events.STATUS_INSTANCE, {
@@ -466,6 +481,7 @@ export class BaileysStartupService extends ChannelStartupService {
466481
}
467482

468483
if (connection === 'open') {
484+
this.reconnectAttempts = 0;
469485
this.instance.wuid = this.client.user.id.replace(/:\d+/, '');
470486
try {
471487
const profilePic = await this.profilePicture(this.instance.wuid);
@@ -1176,6 +1192,8 @@ export class BaileysStartupService extends ChannelStartupService {
11761192
continue;
11771193
}
11781194

1195+
await this.requestRecentHistoryOnGap(received);
1196+
11791197
const existingChat = await this.prismaRepository.chat.findFirst({
11801198
where: { instanceId: this.instanceId, remoteJid: received.key.remoteJid },
11811199
select: { id: true, name: true },
@@ -1315,6 +1333,13 @@ export class BaileysStartupService extends ChannelStartupService {
13151333
received?.message?.audioMessage;
13161334

13171335
const isVideo = received?.message?.videoMessage;
1336+
const storedMessage = await this.findStoredMessageByKeyId(received.key.id);
1337+
const shouldSendToChatwoot = !storedMessage?.chatwootMessageId;
1338+
1339+
if (storedMessage?.chatwootMessageId) {
1340+
this.logger.info(`Message duplicated ignored before integrations: ${received.key.id}`);
1341+
continue;
1342+
}
13181343

13191344
if (this.localSettings.readMessages && received.key.id !== 'status@broadcast') {
13201345
await this.client.readMessages([received.key]);
@@ -1327,7 +1352,8 @@ export class BaileysStartupService extends ChannelStartupService {
13271352
if (
13281353
this.configService.get<Chatwoot>('CHATWOOT').ENABLED &&
13291354
this.localChatwoot?.enabled &&
1330-
!received.key.id.includes('@broadcast')
1355+
!received.key.id.includes('@broadcast') &&
1356+
shouldSendToChatwoot
13311357
) {
13321358
const chatwootSentMessage = await this.chatwootService.eventWhatsapp(
13331359
Events.MESSAGES_UPSERT,
@@ -1356,7 +1382,27 @@ export class BaileysStartupService extends ChannelStartupService {
13561382
if (this.configService.get<Database>('DATABASE').SAVE_DATA.NEW_MESSAGE) {
13571383
// eslint-disable-next-line @typescript-eslint/no-unused-vars
13581384
const { pollUpdates, ...messageData } = messageRaw;
1359-
const msg = await this.prismaRepository.message.create({ data: messageData });
1385+
const msg =
1386+
storedMessage ??
1387+
(await this.prismaRepository.message.create({
1388+
data: messageData,
1389+
}));
1390+
1391+
if (storedMessage) {
1392+
const chatwootFields = {
1393+
chatwootMessageId: messageRaw.chatwootMessageId,
1394+
chatwootInboxId: messageRaw.chatwootInboxId,
1395+
chatwootConversationId: messageRaw.chatwootConversationId,
1396+
};
1397+
const hasChatwootUpdate = Object.values(chatwootFields).some((value) => value != null);
1398+
if (hasChatwootUpdate) {
1399+
await this.prismaRepository.message.update({
1400+
where: { id: storedMessage.id },
1401+
data: chatwootFields,
1402+
});
1403+
}
1404+
this.logger.info(`Message duplicated ignored in database insert: ${received.key.id}`);
1405+
}
13601406

13611407
const { remoteJid } = received.key;
13621408
const timestamp = msg.messageTimestamp;
@@ -1385,7 +1431,7 @@ export class BaileysStartupService extends ChannelStartupService {
13851431
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
13861432
}
13871433

1388-
if (isMedia) {
1434+
if (isMedia && !storedMessage) {
13891435
if (this.configService.get<S3>('S3').ENABLE) {
13901436
try {
13911437
if (isVideo && !this.configService.get<S3>('S3').SAVE_VIDEO) {
@@ -1442,6 +1488,10 @@ export class BaileysStartupService extends ChannelStartupService {
14421488
}
14431489
}
14441490

1491+
if (storedMessage) {
1492+
continue;
1493+
}
1494+
14451495
if (this.localWebhook.enabled) {
14461496
if (isMedia && this.localWebhook.webhookBase64) {
14471497
try {
@@ -4663,6 +4713,107 @@ export class BaileysStartupService extends ChannelStartupService {
46634713
return obj;
46644714
}
46654715

4716+
private getMessageTimestampAsNumber(message: proto.IWebMessageInfo): number | null {
4717+
const timestamp = message?.messageTimestamp;
4718+
4719+
if (Long.isLong(timestamp)) {
4720+
return Math.floor(timestamp.toNumber());
4721+
}
4722+
4723+
if (typeof timestamp === 'number' && Number.isFinite(timestamp)) {
4724+
return Math.floor(timestamp);
4725+
}
4726+
4727+
if (typeof timestamp === 'string') {
4728+
const parsed = Number(timestamp);
4729+
return Number.isFinite(parsed) ? Math.floor(parsed) : null;
4730+
}
4731+
4732+
return null;
4733+
}
4734+
4735+
private async findStoredMessageByKeyId(keyId?: string): Promise<Message | null> {
4736+
if (!keyId) {
4737+
return null;
4738+
}
4739+
4740+
const rows = (await this.prismaRepository.$queryRaw`
4741+
SELECT * FROM "Message"
4742+
WHERE "instanceId" = ${this.instanceId}
4743+
AND "key"->>'id' = ${keyId}
4744+
LIMIT 1
4745+
`) as Message[];
4746+
4747+
return rows[0] ?? null;
4748+
}
4749+
4750+
private async requestRecentHistoryOnGap(message: WAMessage): Promise<void> {
4751+
const remoteJid = message?.key?.remoteJid;
4752+
const timestamp = this.getMessageTimestampAsNumber(message);
4753+
4754+
if (
4755+
!this.client ||
4756+
!remoteJid ||
4757+
!message?.key?.id ||
4758+
!timestamp ||
4759+
remoteJid === 'status@broadcast' ||
4760+
isJidBroadcast(remoteJid) ||
4761+
isJidNewsletter(remoteJid)
4762+
) {
4763+
return;
4764+
}
4765+
4766+
const cacheKey = `history_gap_sync:${this.instanceId}:${remoteJid}`;
4767+
const syncRequested = await this.baileysCache.get(cacheKey);
4768+
if (syncRequested) {
4769+
return;
4770+
}
4771+
4772+
const globalCacheKey = `history_gap_sync:${this.instanceId}:global`;
4773+
const globalSyncRequested = await this.baileysCache.get(globalCacheKey);
4774+
if (globalSyncRequested) {
4775+
return;
4776+
}
4777+
4778+
const rows = (await this.prismaRepository.$queryRaw`
4779+
SELECT "id", "messageTimestamp"
4780+
FROM "Message"
4781+
WHERE "instanceId" = ${this.instanceId}
4782+
AND "key"->>'remoteJid' = ${remoteJid}
4783+
AND "key"->>'id' <> ${message.key.id}
4784+
ORDER BY "messageTimestamp" DESC
4785+
LIMIT 1
4786+
`) as Pick<Message, 'id' | 'messageTimestamp'>[];
4787+
4788+
const lastStoredMessage = rows[0];
4789+
const secondsSinceLastMessage = lastStoredMessage ? timestamp - Number(lastStoredMessage.messageTimestamp) : null;
4790+
const shouldSyncHistory =
4791+
!lastStoredMessage || (secondsSinceLastMessage !== null && secondsSinceLastMessage > this.HISTORY_GAP_SECONDS);
4792+
4793+
if (!shouldSyncHistory) {
4794+
return;
4795+
}
4796+
4797+
await this.baileysCache.set(cacheKey, true, this.HISTORY_GAP_SYNC_TTL_SECONDS);
4798+
await this.baileysCache.set(globalCacheKey, true, this.HISTORY_GAP_GLOBAL_TTL_SECONDS);
4799+
4800+
try {
4801+
const requestId = await this.client.fetchMessageHistory(
4802+
this.HISTORY_GAP_SYNC_MESSAGE_COUNT,
4803+
message.key,
4804+
timestamp,
4805+
);
4806+
const gapDescription = lastStoredMessage ? `${secondsSinceLastMessage}s` : 'no local messages';
4807+
this.logger.warn(
4808+
`Requested WhatsApp history sync for ${remoteJid} after gap (${gapDescription}). RequestId: ${requestId}`,
4809+
);
4810+
} catch (error) {
4811+
this.logger.warn(
4812+
`Unable to request WhatsApp history sync for ${remoteJid}: ${error?.message ?? JSON.stringify(error)}`,
4813+
);
4814+
}
4815+
}
4816+
46664817
private prepareMessage(message: proto.IWebMessageInfo): any {
46674818
const contentType = getContentType(message.message);
46684819
const contentMsg = message?.message[contentType] as any;

0 commit comments

Comments
 (0)