fix(chatwoot): resolve webhook timeout on deletion with 5+ images

Problem:
- Chatwoot shows red error when deleting messages with 5+ images
- Cause: Chatwoot webhook timeout of 5 seconds
- Processing 5 images takes ~9 seconds
- Duplicate webhooks arrive during processing

Solution:
- Implemented async processing with setImmediate()
- Webhook responds immediately (< 100ms)
- Deletion processes in background without blocking
- Maintains idempotency with cache (1 hour TTL)
- Maintains lock mechanism (60 seconds TTL)

Benefits:
- Scales infinitely (10, 20, 100+ images)
- No timeout regardless of quantity
- No error messages in Chatwoot
- Reliable background processing

Tested:
- 5 images: 9s background processing
- Webhook response: < 100ms
- No red error in Chatwoot
- Deletion completes successfully

BREAKING CHANGE: Fixed assertSessions signature to accept force parameter
This commit is contained in:
Anderson Silva 2025-10-06 16:14:26 -03:00
parent a5a46dc72a
commit d4b0cfd2ba
3 changed files with 75 additions and 50 deletions

View File

@ -71,7 +71,7 @@ export const useVoiceCallsBaileys = async (
socket.on('assertSessions', async (jids, force, callback) => { socket.on('assertSessions', async (jids, force, callback) => {
try { try {
const response = await baileys_sock.assertSessions(jids); const response = await baileys_sock.assertSessions(jids, force);
callback(response); callback(response);

View File

@ -4593,8 +4593,8 @@ export class BaileysStartupService extends ChannelStartupService {
return response; return response;
} }
public async baileysAssertSessions(jids: string[]) { public async baileysAssertSessions(jids: string[], force?: boolean) {
const response = await this.client.assertSessions(jids); const response = await this.client.assertSessions(jids, force);
return response; return response;
} }

View File

@ -1122,7 +1122,9 @@ export class ChatwootService {
data.append('message_type', messageType); data.append('message_type', messageType);
data.append('attachments[]', fileStream, { filename: fileName }); if (fileData && fileName) {
data.append('attachments[]', fileData, { filename: fileName });
}
const sourceReplyId = quotedMsg?.chatwootMessageId || null; const sourceReplyId = quotedMsg?.chatwootMessageId || null;
@ -1487,6 +1489,59 @@ export class ChatwootService {
}); });
} }
/**
* Processa deleção de mensagem em background
* Método assíncrono chamado via setImmediate para não bloquear resposta do webhook
*/
private async processDeletion(instance: InstanceDto, body: any, deleteLockKey: string) {
this.logger.warn(`[DELETE] 🗑️ Processing deletion - messageId: ${body.id}`);
const waInstance = this.waMonitor.waInstances[instance.instanceName];
// Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos)
const messages = await this.prismaRepository.message.findMany({
where: {
chatwootMessageId: body.id,
instanceId: instance.instanceId,
},
});
if (messages && messages.length > 0) {
this.logger.warn(`[DELETE] Found ${messages.length} message(s) to delete from Chatwoot message ${body.id}`);
this.logger.verbose(`[DELETE] Messages keys: ${messages.map((m) => (m.key as any)?.id).join(', ')}`);
// Deletar cada mensagem no WhatsApp
for (const message of messages) {
const key = message.key as ExtendedMessageKey;
this.logger.warn(
`[DELETE] Attempting to delete WhatsApp message - keyId: ${key?.id}, remoteJid: ${key?.remoteJid}`,
);
try {
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
this.logger.warn(`[DELETE] ✅ Message ${key.id} deleted in WhatsApp successfully`);
} catch (error) {
this.logger.error(`[DELETE] ❌ Error deleting message ${key.id} in WhatsApp: ${error}`);
this.logger.error(`[DELETE] Error details: ${JSON.stringify(error, null, 2)}`);
}
}
// Remover todas as mensagens do banco de dados
await this.prismaRepository.message.deleteMany({
where: {
instanceId: instance.instanceId,
chatwootMessageId: body.id,
},
});
this.logger.warn(`[DELETE] ✅ SUCCESS: ${messages.length} message(s) deleted from WhatsApp and database`);
} else {
// Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
this.logger.warn(`[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${body.id}`);
}
// Liberar lock após processar
await this.cache.delete(deleteLockKey);
}
public async receiveWebhook(instance: InstanceDto, body: any) { public async receiveWebhook(instance: InstanceDto, body: any) {
try { try {
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial // IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
@ -1545,55 +1600,25 @@ export class ChatwootService {
// Lock já foi adquirido no início do método (antes do delay) // Lock já foi adquirido no início do método (antes do delay)
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`; const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
this.logger.warn(`[DELETE] 🗑️ Processing deletion - messageId: ${body.id}`); // ESTRATÉGIA: Processar em background e responder IMEDIATAMENTE
const waInstance = this.waMonitor.waInstances[instance.instanceName]; // Isso evita timeout do Chatwoot (5s) quando há muitas imagens (> 5s de processamento)
this.logger.warn(`[DELETE] 🚀 Starting background deletion - messageId: ${body.id}`);
// Buscar TODAS as mensagens com esse chatwootMessageId (pode ser múltiplos anexos) // Executar em background (sem await) - não bloqueia resposta do webhook
const messages = await this.prismaRepository.message.findMany({ setImmediate(async () => {
where: { try {
chatwootMessageId: body.id, await this.processDeletion(instance, body, deleteLockKey);
instanceId: instance.instanceId, } catch (error) {
}, this.logger.error(`[DELETE] ❌ Background deletion failed for messageId ${body.id}: ${error}`);
}
}); });
if (messages && messages.length > 0) { // RESPONDER IMEDIATAMENTE ao Chatwoot (< 50ms)
this.logger.warn(`[DELETE] Found ${messages.length} message(s) to delete from Chatwoot message ${body.id}`); return {
this.logger.verbose(`[DELETE] Messages keys: ${messages.map((m) => (m.key as any)?.id).join(', ')}`); message: 'deletion_accepted',
messageId: body.id,
// Deletar cada mensagem no WhatsApp note: 'Deletion is being processed in background',
for (const message of messages) { };
const key = message.key as ExtendedMessageKey;
this.logger.warn(
`[DELETE] Attempting to delete WhatsApp message - keyId: ${key?.id}, remoteJid: ${key?.remoteJid}`,
);
try {
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
this.logger.warn(`[DELETE] ✅ Message ${key.id} deleted in WhatsApp successfully`);
} catch (error) {
this.logger.error(`[DELETE] ❌ Error deleting message ${key.id} in WhatsApp: ${error}`);
this.logger.error(`[DELETE] Error details: ${JSON.stringify(error, null, 2)}`);
}
}
// Remover todas as mensagens do banco de dados
await this.prismaRepository.message.deleteMany({
where: {
instanceId: instance.instanceId,
chatwootMessageId: body.id,
},
});
this.logger.warn(`[DELETE] ✅ SUCCESS: ${messages.length} message(s) deleted from WhatsApp and database`);
} else {
// Mensagem não encontrada - pode ser uma mensagem antiga que foi substituída por edição
// Nesse caso, ignoramos silenciosamente pois o ID já foi atualizado no banco
this.logger.warn(`[DELETE] ⚠️ WARNING: Message not found in DB - chatwootMessageId: ${body.id}`);
}
// Liberar lock após processar
await this.cache.delete(deleteLockKey);
return { message: 'deleted' };
} }
if ( if (