@@ -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