@@ -44,6 +44,25 @@ interface ChatwootMessage {
4444export class ChatwootService {
4545 private readonly logger = new Logger ( 'ChatwootService' ) ;
4646
47+ // HTTP timeout constants
48+ private readonly MEDIA_DOWNLOAD_TIMEOUT_MS = 60000 ; // 60 seconds for large files
49+
50+ // S3/MinIO retry configuration (external storage - longer delays, fewer retries)
51+ private readonly S3_MAX_RETRIES = 3 ;
52+ private readonly S3_BASE_DELAY_MS = 1000 ; // Base delay: 1 second
53+ private readonly S3_MAX_DELAY_MS = 8000 ; // Max delay: 8 seconds
54+
55+ // Database polling retry configuration (internal DB - shorter delays, more retries)
56+ private readonly DB_POLLING_MAX_RETRIES = 5 ;
57+ private readonly DB_POLLING_BASE_DELAY_MS = 100 ; // Base delay: 100ms
58+ private readonly DB_POLLING_MAX_DELAY_MS = 2000 ; // Max delay: 2 seconds
59+
60+ // Webhook processing delay
61+ private readonly WEBHOOK_INITIAL_DELAY_MS = 500 ; // Initial delay before processing webhook
62+
63+ // Lock polling delay
64+ private readonly LOCK_POLLING_DELAY_MS = 300 ; // Delay between lock status checks
65+
4766 private provider : any ;
4867
4968 constructor (
@@ -617,7 +636,7 @@ export class ChatwootService {
617636 this . logger . warn ( `Timeout aguardando lock para ${ remoteJid } ` ) ;
618637 break ;
619638 }
620- await new Promise ( ( res ) => setTimeout ( res , 300 ) ) ;
639+ await new Promise ( ( res ) => setTimeout ( res , this . LOCK_POLLING_DELAY_MS ) ) ;
621640 if ( await this . cache . has ( cacheKey ) ) {
622641 const conversationId = ( await this . cache . get ( cacheKey ) ) as number ;
623642 this . logger . verbose ( `Resolves creation of: ${ remoteJid } , conversation ID: ${ conversationId } ` ) ;
@@ -1136,7 +1155,7 @@ export class ChatwootService {
11361155 // maxRedirects: 0 para não seguir redirects automaticamente
11371156 const response = await axios . get ( media , {
11381157 responseType : 'arraybuffer' ,
1139- timeout : 60000 , // 60 segundos de timeout para arquivos grandes
1158+ timeout : this . MEDIA_DOWNLOAD_TIMEOUT_MS ,
11401159 headers : {
11411160 api_access_token : this . provider . token ,
11421161 } ,
@@ -1154,18 +1173,19 @@ export class ChatwootService {
11541173 if ( redirectUrl ) {
11551174 // Fazer novo request para a URL do S3/MinIO (sem autenticação, pois é presigned URL)
11561175 // IMPORTANTE: Chatwoot pode gerar a URL presigned ANTES de fazer upload
1157- // Vamos tentar com retry se receber 404 (arquivo ainda não disponível)
1176+ // Vamos tentar com retry usando exponential backoff se receber 404 (arquivo ainda não disponível)
11581177 this . logger . verbose ( 'Downloading from S3/MinIO...' ) ;
11591178
11601179 let s3Response ;
11611180 let retryCount = 0 ;
1162- const maxRetries = 3 ;
1163- const retryDelay = 2000 ; // 2 segundos entre tentativas
1181+ const maxRetries = this . S3_MAX_RETRIES ;
1182+ const baseDelay = this . S3_BASE_DELAY_MS ;
1183+ const maxDelay = this . S3_MAX_DELAY_MS ;
11641184
11651185 while ( retryCount <= maxRetries ) {
11661186 s3Response = await axios . get ( redirectUrl , {
11671187 responseType : 'arraybuffer' ,
1168- timeout : 60000 , // 60 segundos para arquivos grandes
1188+ timeout : this . MEDIA_DOWNLOAD_TIMEOUT_MS ,
11691189 validateStatus : ( status ) => status < 500 ,
11701190 } ) ;
11711191
@@ -1178,14 +1198,16 @@ export class ChatwootService {
11781198 break ;
11791199 }
11801200
1181- // Se for 404 e ainda tem tentativas, aguardar e tentar novamente
1201+ // Se for 404 e ainda tem tentativas, aguardar com exponential backoff e tentar novamente
11821202 if ( retryCount < maxRetries ) {
1203+ // Exponential backoff com max delay (seguindo padrão do webhook controller)
1204+ const backoffDelay = Math . min ( baseDelay * Math . pow ( 2 , retryCount ) , maxDelay ) ;
11831205 const errorBody = s3Response . data ?. toString ? s3Response . data . toString ( 'utf-8' ) : s3Response . data ;
11841206 this . logger . warn (
1185- `File not yet available in S3/MinIO (attempt ${ retryCount + 1 } /${ maxRetries + 1 } ). Retrying in ${ retryDelay } ms...` ,
1207+ `File not yet available in S3/MinIO (attempt ${ retryCount + 1 } /${ maxRetries + 1 } ). Retrying in ${ backoffDelay } ms with exponential backoff ...` ,
11861208 ) ;
11871209 this . logger . verbose ( `MinIO Response: ${ errorBody } ` ) ;
1188- await new Promise ( ( resolve ) => setTimeout ( resolve , retryDelay ) ) ;
1210+ await new Promise ( ( resolve ) => setTimeout ( resolve , backoffDelay ) ) ;
11891211 retryCount ++ ;
11901212 } else {
11911213 // Última tentativa falhou
@@ -1246,8 +1268,10 @@ export class ChatwootService {
12461268
12471269 this . logger . verbose ( `File name: ${ fileName } , size: ${ mediaBuffer . length } bytes` ) ;
12481270 } catch ( downloadError ) {
1249- this . logger . error ( 'Error downloading media from: ' + media ) ;
1250- this . logger . error ( downloadError ) ;
1271+ this . logger . error ( '[MEDIA DOWNLOAD] ❌ Error downloading media from: ' + media ) ;
1272+ this . logger . error ( `[MEDIA DOWNLOAD] Error message: ${ downloadError . message } ` ) ;
1273+ this . logger . error ( `[MEDIA DOWNLOAD] Error stack: ${ downloadError . stack } ` ) ;
1274+ this . logger . error ( `[MEDIA DOWNLOAD] Full error: ${ JSON . stringify ( downloadError , null , 2 ) } ` ) ;
12511275 throw new Error ( `Failed to download media: ${ downloadError . message } ` ) ;
12521276 }
12531277
@@ -1357,7 +1381,32 @@ export class ChatwootService {
13571381
13581382 public async receiveWebhook ( instance : InstanceDto , body : any ) {
13591383 try {
1360- await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
1384+ // IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
1385+ // para evitar race condition com webhooks duplicados
1386+ let isDeletionEvent = false ;
1387+ if ( body . event === 'message_updated' && body . content_attributes ?. deleted ) {
1388+ isDeletionEvent = true ;
1389+ const deleteLockKey = `${ instance . instanceName } :deleteMessage-${ body . id } ` ;
1390+
1391+ // Verificar se já está processando esta deleção
1392+ if ( await this . cache . has ( deleteLockKey ) ) {
1393+ this . logger . warn ( `[DELETE] ⏭️ SKIPPING: Deletion already in progress for messageId: ${ body . id } ` ) ;
1394+ return { message : 'already_processing' } ;
1395+ }
1396+
1397+ // Adquirir lock IMEDIATAMENTE por 30 segundos
1398+ await this . cache . set ( deleteLockKey , true , 30 ) ;
1399+
1400+ this . logger . warn (
1401+ `[WEBHOOK-DELETE] Event: ${ body . event } , messageId: ${ body . id } , conversation: ${ body . conversation ?. id } ` ,
1402+ ) ;
1403+ }
1404+
1405+ // Para deleções, processar IMEDIATAMENTE (sem delay)
1406+ // Para outros eventos, aguardar delay inicial
1407+ if ( ! isDeletionEvent ) {
1408+ await new Promise ( ( resolve ) => setTimeout ( resolve , this . WEBHOOK_INITIAL_DELAY_MS ) ) ;
1409+ }
13611410
13621411 const client = await this . clientCw ( instance ) ;
13631412
@@ -1385,7 +1434,10 @@ export class ChatwootService {
13851434
13861435 // Processar deleção de mensagem ANTES das outras validações
13871436 if ( body . event === 'message_updated' && body . content_attributes ?. deleted ) {
1388- this . logger . verbose ( `Processing message deletion from Chatwoot - messageId: ${ body . id } ` ) ;
1437+ // Lock já foi adquirido no início do método (antes do delay)
1438+ const deleteLockKey = `${ instance . instanceName } :deleteMessage-${ body . id } ` ;
1439+
1440+ this . logger . warn ( `[DELETE] 🗑️ Processing deletion - messageId: ${ body . id } ` ) ;
13891441 const waInstance = this . waMonitor . waInstances [ instance . instanceName ] ;
13901442
13911443 // Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos)
@@ -1397,18 +1449,22 @@ export class ChatwootService {
13971449 } ) ;
13981450
13991451 if ( messages && messages . length > 0 ) {
1400- this . logger . verbose ( `Found ${ messages . length } message(s) to delete from Chatwoot message ${ body . id } ` ) ;
1452+ this . logger . warn ( `[DELETE] Found ${ messages . length } message(s) to delete from Chatwoot message ${ body . id } ` ) ;
1453+ this . logger . verbose ( `[DELETE] Messages keys: ${ messages . map ( ( m ) => ( m . key as any ) ?. id ) . join ( ', ' ) } ` ) ;
14011454
14021455 // Deletar cada mensagem no WhatsApp
14031456 for ( const message of messages ) {
14041457 const key = message . key as ExtendedMessageKey ;
1405- this . logger . verbose ( `Deleting WhatsApp message - keyId: ${ key ?. id } ` ) ;
1458+ this . logger . warn (
1459+ `[DELETE] Attempting to delete WhatsApp message - keyId: ${ key ?. id } , remoteJid: ${ key ?. remoteJid } ` ,
1460+ ) ;
14061461
14071462 try {
14081463 await waInstance ?. client . sendMessage ( key . remoteJid , { delete : key } ) ;
1409- this . logger . verbose ( ` Message ${ key . id } deleted in WhatsApp successfully`) ;
1464+ this . logger . warn ( `[DELETE] ✅ Message ${ key . id } deleted in WhatsApp successfully`) ;
14101465 } catch ( error ) {
1411- this . logger . error ( `Error deleting message ${ key . id } in WhatsApp: ${ error } ` ) ;
1466+ this . logger . error ( `[DELETE] ❌ Error deleting message ${ key . id } in WhatsApp: ${ error } ` ) ;
1467+ this . logger . error ( `[DELETE] Error details: ${ JSON . stringify ( error , null , 2 ) } ` ) ;
14121468 }
14131469 }
14141470
@@ -1419,15 +1475,16 @@ export class ChatwootService {
14191475 chatwootMessageId : body . id ,
14201476 } ,
14211477 } ) ;
1422- this . logger . verbose ( ` ${ messages . length } message(s) removed from database`) ;
1478+ this . logger . warn ( `[DELETE] ✅ SUCCESS: ${ messages . length } message(s) deleted from WhatsApp and database`) ;
14231479 } else {
14241480 // Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
14251481 // Nesse caso, ignoramos silenciosamente pois o ID já foi atualizado no banco
1426- this . logger . verbose (
1427- `Message not found for chatwootMessageId: ${ body . id } - may have been replaced by an edited message` ,
1428- ) ;
1482+ this . logger . warn ( `[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${ body . id } ` ) ;
14291483 }
14301484
1485+ // Liberar lock após processar
1486+ await this . cache . delete ( deleteLockKey ) ;
1487+
14311488 return { message : 'deleted' } ;
14321489 }
14331490
@@ -1726,12 +1783,11 @@ export class ChatwootService {
17261783 `Updating message with chatwootMessageId: ${ chatwootMessageIds . messageId } , keyId: ${ key . id } , instanceId: ${ instanceId } ` ,
17271784 ) ;
17281785
1729- // Aguarda um pequeno delay para garantir que a mensagem foi criada no banco
1730- await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
1731-
1732- // Verifica se a mensagem existe antes de atualizar
1786+ // Verifica se a mensagem existe antes de atualizar usando polling com exponential backoff
17331787 let retries = 0 ;
1734- const maxRetries = 5 ;
1788+ const maxRetries = this . DB_POLLING_MAX_RETRIES ;
1789+ const baseDelay = this . DB_POLLING_BASE_DELAY_MS ;
1790+ const maxDelay = this . DB_POLLING_MAX_DELAY_MS ;
17351791 let messageExists = false ;
17361792
17371793 while ( retries < maxRetries && ! messageExists ) {
@@ -1750,8 +1806,14 @@ export class ChatwootService {
17501806 this . logger . verbose ( `Message found in database after ${ retries } retries` ) ;
17511807 } else {
17521808 retries ++ ;
1753- this . logger . verbose ( `Message not found, retry ${ retries } /${ maxRetries } ` ) ;
1754- await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) ) ;
1809+ if ( retries < maxRetries ) {
1810+ // Exponential backoff com max delay (seguindo padrão do sistema)
1811+ const backoffDelay = Math . min ( baseDelay * Math . pow ( 2 , retries - 1 ) , maxDelay ) ;
1812+ this . logger . verbose ( `Message not found, retry ${ retries } /${ maxRetries } in ${ backoffDelay } ms` ) ;
1813+ await new Promise ( ( resolve ) => setTimeout ( resolve , backoffDelay ) ) ;
1814+ } else {
1815+ this . logger . verbose ( `Message not found after ${ retries } attempts` ) ;
1816+ }
17551817 }
17561818 }
17571819
0 commit comments