mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-21 12:52:19 -06:00
feat(baileys,chatwoot,on-whatsapp-cache): implementações e correções na baileys e chatwoot
* corrige cache de números PN, LIDs e g.us para enviar o número correto * atualiza para os últimos commits da baileys * corrige envio de áudio e documentos via chatwoot no canal baileys * diversas correções na integração com chatwoot * corrige mensagens ignoradas no recebimento de leads
This commit is contained in:
@@ -23,7 +23,7 @@ import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageM
|
||||
import i18next from '@utils/i18n';
|
||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||
import axios from 'axios';
|
||||
import { proto, WAMessageKey } from 'baileys';
|
||||
import { WAMessageContent, WAMessageKey } from 'baileys';
|
||||
import dayjs from 'dayjs';
|
||||
import FormData from 'form-data';
|
||||
import { Jimp, JimpMime } from 'jimp';
|
||||
@@ -32,8 +32,6 @@ import mimeTypes from 'mime-types';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
const MIN_CONNECTION_NOTIFICATION_INTERVAL_MS = 30000; // 30 seconds
|
||||
|
||||
interface ChatwootMessage {
|
||||
messageId?: number;
|
||||
inboxId?: number;
|
||||
@@ -45,22 +43,6 @@ interface ChatwootMessage {
|
||||
export class ChatwootService {
|
||||
private readonly logger = new Logger('ChatwootService');
|
||||
|
||||
// HTTP timeout constants
|
||||
private readonly MEDIA_DOWNLOAD_TIMEOUT_MS = 60000; // 60 seconds for large files
|
||||
|
||||
// S3/MinIO retry configuration (external storage - longer delays, fewer retries)
|
||||
private readonly S3_MAX_RETRIES = 3;
|
||||
private readonly S3_BASE_DELAY_MS = 1000; // Base delay: 1 second
|
||||
private readonly S3_MAX_DELAY_MS = 8000; // Max delay: 8 seconds
|
||||
|
||||
// Database polling retry configuration (internal DB - shorter delays, more retries)
|
||||
private readonly DB_POLLING_MAX_RETRIES = 5;
|
||||
private readonly DB_POLLING_BASE_DELAY_MS = 100; // Base delay: 100ms
|
||||
private readonly DB_POLLING_MAX_DELAY_MS = 2000; // Max delay: 2 seconds
|
||||
|
||||
// Webhook processing delay
|
||||
private readonly WEBHOOK_INITIAL_DELAY_MS = 500; // Initial delay before processing webhook
|
||||
|
||||
// Lock polling delay
|
||||
private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks
|
||||
|
||||
@@ -588,8 +570,10 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
public async createConversation(instance: InstanceDto, body: any) {
|
||||
const isLid = body.key.addressingMode === 'lid' && body.key.remoteJidAlt;
|
||||
const remoteJid = isLid ? body.key.remoteJidAlt : body.key.remoteJid;
|
||||
const isLid = body.key.addressingMode === 'lid';
|
||||
const isGroup = body.key.remoteJid.endsWith('@g.us');
|
||||
const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid;
|
||||
const remoteJid = body.key.remoteJid;
|
||||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||||
const maxWaitTime = 5000; // 5 seconds
|
||||
@@ -598,19 +582,19 @@ export class ChatwootService {
|
||||
|
||||
try {
|
||||
// Processa atualização de contatos já criados @lid
|
||||
if (isLid && body.key.remoteJidAlt !== body.key.remoteJid) {
|
||||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
||||
if (contact && contact.identifier !== body.key.remoteJidAlt) {
|
||||
if (phoneNumber && remoteJid && !isGroup) {
|
||||
const contact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||
if (contact && contact.identifier !== remoteJid) {
|
||||
this.logger.verbose(
|
||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.remoteJidAlt: ${body.key.remoteJidAlt}`,
|
||||
`Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`,
|
||||
);
|
||||
const updateContact = await this.updateContact(instance, contact.id, {
|
||||
identifier: body.key.remoteJidAlt,
|
||||
phone_number: `+${body.key.remoteJidAlt.split('@')[0]}`,
|
||||
identifier: remoteJid,
|
||||
phone_number: `+${phoneNumber.split('@')[0]}`,
|
||||
});
|
||||
|
||||
if (updateContact === null) {
|
||||
const baseContact = await this.findContact(instance, body.key.remoteJidAlt.split('@')[0]);
|
||||
const baseContact = await this.findContact(instance, phoneNumber.split('@')[0]);
|
||||
if (baseContact) {
|
||||
await this.mergeContacts(baseContact.id, contact.id);
|
||||
this.logger.verbose(
|
||||
@@ -626,7 +610,7 @@ export class ChatwootService {
|
||||
// If it already exists in the cache, return conversationId
|
||||
if (await this.cache.has(cacheKey)) {
|
||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||
this.logger.verbose(`Found conversation to: ${phoneNumber}, conversation ID: ${conversationId}`);
|
||||
let conversationExists: conversation | boolean;
|
||||
try {
|
||||
conversationExists = await client.conversations.get({
|
||||
@@ -677,8 +661,7 @@ export class ChatwootService {
|
||||
return (await this.cache.get(cacheKey)) as number;
|
||||
}
|
||||
|
||||
const isGroup = remoteJid.includes('@g.us');
|
||||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0].split(':')[0];
|
||||
const chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].split(':')[0];
|
||||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||||
const filterInbox = await this.getInbox(instance);
|
||||
if (!filterInbox) return null;
|
||||
@@ -688,14 +671,15 @@ export class ChatwootService {
|
||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||||
|
||||
const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant;
|
||||
nameContact = `${group.subject} (GROUP)`;
|
||||
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
||||
body.key.participant.split('@')[0],
|
||||
participantJid.split('@')[0],
|
||||
);
|
||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
||||
const findParticipant = await this.findContact(instance, participantJid.split('@')[0]);
|
||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
||||
|
||||
if (findParticipant) {
|
||||
@@ -708,12 +692,12 @@ export class ChatwootService {
|
||||
} else {
|
||||
await this.createContact(
|
||||
instance,
|
||||
body.key.participant.split('@')[0],
|
||||
participantJid.split('@')[0],
|
||||
filterInbox.id,
|
||||
false,
|
||||
body.pushName,
|
||||
picture_url.profilePictureUrl || null,
|
||||
body.key.participant,
|
||||
participantJid,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -721,6 +705,7 @@ export class ChatwootService {
|
||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
||||
|
||||
this.logger.verbose(`Searching contact for: ${chatId}`);
|
||||
let contact = await this.findContact(instance, chatId);
|
||||
|
||||
if (contact) {
|
||||
@@ -1158,140 +1143,20 @@ export class ChatwootService {
|
||||
|
||||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||||
try {
|
||||
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
|
||||
// URLs presigned podem expirar, então convertemos para base64
|
||||
let mediaBuffer: Buffer;
|
||||
let mimeType: string;
|
||||
let fileName: string;
|
||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
||||
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
||||
|
||||
try {
|
||||
this.logger.verbose(`Downloading media from: ${media}`);
|
||||
if (!mimeType) {
|
||||
const parts = media.split('/');
|
||||
fileName = decodeURIComponent(parts[parts.length - 1]);
|
||||
|
||||
// Tentar fazer download do arquivo com autenticação do Chatwoot
|
||||
// maxRedirects: 0 para não seguir redirects automaticamente
|
||||
const response = await axios.get(media, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
||||
headers: {
|
||||
api_access_token: this.provider.token,
|
||||
},
|
||||
maxRedirects: 0, // Não seguir redirects automaticamente
|
||||
validateStatus: (status) => status < 500, // Aceitar redirects (301, 302, 307)
|
||||
});
|
||||
|
||||
this.logger.verbose(`Initial response status: ${response.status}`);
|
||||
|
||||
// Se for redirect, pegar a URL de destino e fazer novo request
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const redirectUrl = response.headers.location;
|
||||
this.logger.verbose(`Redirect to: ${redirectUrl}`);
|
||||
|
||||
if (redirectUrl) {
|
||||
// Fazer novo request para a URL do S3/MinIO (sem autenticação, pois é presigned URL)
|
||||
// IMPORTANTE: Chatwoot pode gerar a URL presigned ANTES de fazer upload
|
||||
// Vamos tentar com retry usando exponential backoff se receber 404 (arquivo ainda não disponível)
|
||||
this.logger.verbose('Downloading from S3/MinIO...');
|
||||
|
||||
let s3Response;
|
||||
let retryCount = 0;
|
||||
const maxRetries = this.S3_MAX_RETRIES;
|
||||
const baseDelay = this.S3_BASE_DELAY_MS;
|
||||
const maxDelay = this.S3_MAX_DELAY_MS;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
s3Response = await axios.get(redirectUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: this.MEDIA_DOWNLOAD_TIMEOUT_MS,
|
||||
validateStatus: (status) => status < 500,
|
||||
});
|
||||
|
||||
this.logger.verbose(
|
||||
`S3 response status: ${s3Response.status}, size: ${s3Response.data?.byteLength || 0} bytes (attempt ${retryCount + 1}/${maxRetries + 1})`,
|
||||
);
|
||||
|
||||
// Se não for 404, sair do loop
|
||||
if (s3Response.status !== 404) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Se for 404 e ainda tem tentativas, aguardar com exponential backoff e tentar novamente
|
||||
if (retryCount < maxRetries) {
|
||||
// Exponential backoff com max delay (seguindo padrão do webhook controller)
|
||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay);
|
||||
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
||||
this.logger.warn(
|
||||
`File not yet available in S3/MinIO (attempt ${retryCount + 1}/${maxRetries + 1}). Retrying in ${backoffDelay}ms with exponential backoff...`,
|
||||
);
|
||||
this.logger.verbose(`MinIO Response: ${errorBody}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
||||
retryCount++;
|
||||
} else {
|
||||
// Última tentativa falhou
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Após todas as tentativas, verificar o status final
|
||||
if (s3Response.status === 404) {
|
||||
const errorBody = s3Response.data?.toString ? s3Response.data.toString('utf-8') : s3Response.data;
|
||||
this.logger.error(`File not found in S3/MinIO after ${maxRetries + 1} attempts. URL: ${redirectUrl}`);
|
||||
this.logger.error(`MinIO Error Response: ${errorBody}`);
|
||||
throw new Error(
|
||||
'File not found in S3/MinIO (404). The file may have been deleted, the URL is incorrect, or Chatwoot has not finished uploading yet.',
|
||||
);
|
||||
}
|
||||
|
||||
if (s3Response.status === 403) {
|
||||
this.logger.error(`Access denied to S3/MinIO. URL may have expired: ${redirectUrl}`);
|
||||
throw new Error(
|
||||
'Access denied to S3/MinIO (403). Presigned URL may have expired. Check S3_PRESIGNED_EXPIRATION setting.',
|
||||
);
|
||||
}
|
||||
|
||||
if (s3Response.status >= 400) {
|
||||
this.logger.error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
||||
throw new Error(`S3/MinIO error ${s3Response.status}: ${s3Response.statusText}`);
|
||||
}
|
||||
|
||||
mediaBuffer = Buffer.from(s3Response.data);
|
||||
mimeType = s3Response.headers['content-type'] || 'application/octet-stream';
|
||||
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes from S3, type: ${mimeType}`);
|
||||
} else {
|
||||
this.logger.error('Redirect response without Location header');
|
||||
throw new Error('Redirect without Location header');
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
this.logger.error(`File not found (404) at: ${media}`);
|
||||
throw new Error('File not found (404). The attachment may not exist in Chatwoot storage.');
|
||||
} else if (response.status >= 400) {
|
||||
this.logger.error(`HTTP ${response.status}: ${response.statusText} for URL: ${media}`);
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
} else {
|
||||
// Download direto sem redirect
|
||||
mediaBuffer = Buffer.from(response.data);
|
||||
mimeType = response.headers['content-type'] || 'application/octet-stream';
|
||||
this.logger.verbose(`Downloaded ${mediaBuffer.length} bytes directly, type: ${mimeType}`);
|
||||
}
|
||||
|
||||
// Extrair nome do arquivo da URL ou usar o content-disposition
|
||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
||||
if (parsedMedia?.name && parsedMedia?.ext) {
|
||||
fileName = parsedMedia.name + parsedMedia.ext;
|
||||
} else {
|
||||
const parts = media.split('/');
|
||||
fileName = decodeURIComponent(parts[parts.length - 1].split('?')[0]);
|
||||
}
|
||||
|
||||
this.logger.verbose(`File name: ${fileName}, size: ${mediaBuffer.length} bytes`);
|
||||
} catch (downloadError) {
|
||||
this.logger.error('[MEDIA DOWNLOAD] ❌ Error downloading media from: ' + media);
|
||||
this.logger.error(`[MEDIA DOWNLOAD] Error message: ${downloadError.message}`);
|
||||
this.logger.error(`[MEDIA DOWNLOAD] Error stack: ${downloadError.stack}`);
|
||||
this.logger.error(`[MEDIA DOWNLOAD] Full error: ${JSON.stringify(downloadError, null, 2)}`);
|
||||
throw new Error(`Failed to download media: ${downloadError.message}`);
|
||||
mimeType = response.headers['content-type'];
|
||||
}
|
||||
|
||||
// Determinar o tipo de mídia pelo mimetype
|
||||
let type = 'document';
|
||||
|
||||
switch (mimeType.split('/')[0]) {
|
||||
@@ -1309,13 +1174,11 @@ export class ChatwootService {
|
||||
break;
|
||||
}
|
||||
|
||||
// Para áudio, usar base64 com data URI
|
||||
if (type === 'audio') {
|
||||
const base64Audio = `data:${mimeType};base64,${mediaBuffer.toString('base64')}`;
|
||||
const data: SendAudioDto = {
|
||||
number: number,
|
||||
audio: base64Audio,
|
||||
delay: 1200,
|
||||
audio: media,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
quoted: options?.quoted,
|
||||
};
|
||||
|
||||
@@ -1326,12 +1189,8 @@ export class ChatwootService {
|
||||
return messageSent;
|
||||
}
|
||||
|
||||
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
|
||||
const base64Media = mediaBuffer.toString('base64');
|
||||
|
||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg'];
|
||||
const parsedExt = path.parse(fileName)?.ext;
|
||||
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
|
||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||||
type = 'document';
|
||||
}
|
||||
|
||||
@@ -1339,7 +1198,7 @@ export class ChatwootService {
|
||||
number: number,
|
||||
mediatype: type as any,
|
||||
fileName: fileName,
|
||||
media: base64Media, // Base64 puro, sem prefixo
|
||||
media: media,
|
||||
delay: 1200,
|
||||
quoted: options?.quoted,
|
||||
};
|
||||
@@ -1395,87 +1254,9 @@ 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 WAMessageKey;
|
||||
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) {
|
||||
try {
|
||||
// IMPORTANTE: Verificar lock de deleção ANTES do delay inicial
|
||||
// para evitar race condition com webhooks duplicados
|
||||
let isDeletionEvent = false;
|
||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||
isDeletionEvent = true;
|
||||
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
||||
|
||||
// Verificar se já está processando esta deleção
|
||||
if (await this.cache.has(deleteLockKey)) {
|
||||
this.logger.warn(`[DELETE] ⏭️ SKIPPING: Deletion already in progress for messageId: ${body.id}`);
|
||||
return { message: 'already_processing' };
|
||||
}
|
||||
|
||||
// Adquirir lock IMEDIATAMENTE por 30 segundos
|
||||
await this.cache.set(deleteLockKey, true, 30);
|
||||
|
||||
this.logger.warn(
|
||||
`[WEBHOOK-DELETE] Event: ${body.event}, messageId: ${body.id}, conversation: ${body.conversation?.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Para deleções, processar IMEDIATAMENTE (sem delay)
|
||||
// Para outros eventos, aguardar delay inicial
|
||||
if (!isDeletionEvent) {
|
||||
await new Promise((resolve) => setTimeout(resolve, this.WEBHOOK_INITIAL_DELAY_MS));
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const client = await this.clientCw(instance);
|
||||
|
||||
@@ -1494,39 +1275,6 @@ export class ChatwootService {
|
||||
this.cache.delete(keyToDelete);
|
||||
}
|
||||
|
||||
// Log para debug de mensagens deletadas
|
||||
if (body.event === 'message_updated') {
|
||||
this.logger.verbose(
|
||||
`Message updated event - deleted: ${body.content_attributes?.deleted}, messageId: ${body.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Processar deleção de mensagem ANTES das outras validações
|
||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||
// Lock já foi adquirido no início do método (antes do delay)
|
||||
const deleteLockKey = `${instance.instanceName}:deleteMessage-${body.id}`;
|
||||
|
||||
// ESTRATÉGIA: Processar em background e responder IMEDIATAMENTE
|
||||
// Isso evita timeout do Chatwoot (5s) quando há muitas imagens (> 5s de processamento)
|
||||
this.logger.warn(`[DELETE] 🚀 Starting background deletion - messageId: ${body.id}`);
|
||||
|
||||
// Executar em background (sem await) - não bloqueia resposta do webhook
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await this.processDeletion(instance, body, deleteLockKey);
|
||||
} catch (error) {
|
||||
this.logger.error(`[DELETE] ❌ Background deletion failed for messageId ${body.id}: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// RESPONDER IMEDIATAMENTE ao Chatwoot (< 50ms)
|
||||
return {
|
||||
message: 'deletion_accepted',
|
||||
messageId: body.id,
|
||||
note: 'Deletion is being processed in background',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
!body?.conversation ||
|
||||
body.private ||
|
||||
@@ -1548,6 +1296,7 @@ export class ChatwootService {
|
||||
|
||||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||
instance.instanceId = waInstance.instanceId;
|
||||
|
||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||||
const message = await this.prismaRepository.message.findFirst({
|
||||
@@ -1670,63 +1419,44 @@ export class ChatwootService {
|
||||
|
||||
for (const message of body.conversation.messages) {
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
// Processa anexos de forma assíncrona para não bloquear o webhook
|
||||
const processAttachments = async () => {
|
||||
for (const attachment of message.attachments) {
|
||||
if (!messageReceived) {
|
||||
formatText = null;
|
||||
}
|
||||
|
||||
const options: Options = {
|
||||
quoted: await this.getQuotedMessage(body, instance),
|
||||
};
|
||||
|
||||
try {
|
||||
const messageSent = await this.sendAttachment(
|
||||
waInstance,
|
||||
chatId,
|
||||
attachment.data_url,
|
||||
formatText,
|
||||
options,
|
||||
);
|
||||
|
||||
if (!messageSent && body.conversation?.id) {
|
||||
this.onSendMessageError(instance, body.conversation?.id);
|
||||
}
|
||||
|
||||
if (messageSent) {
|
||||
await this.updateChatwootMessageId(
|
||||
{
|
||||
...messageSent,
|
||||
owner: instance.instanceName,
|
||||
},
|
||||
{
|
||||
messageId: body.id,
|
||||
inboxId: body.inbox?.id,
|
||||
conversationId: body.conversation?.id,
|
||||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
||||
},
|
||||
instance,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (body.conversation?.id) {
|
||||
this.onSendMessageError(instance, body.conversation?.id, error);
|
||||
}
|
||||
}
|
||||
for (const attachment of message.attachments) {
|
||||
if (!messageReceived) {
|
||||
formatText = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Executa em background sem bloquear
|
||||
processAttachments().catch((error) => {
|
||||
this.logger.error(error);
|
||||
});
|
||||
const options: Options = {
|
||||
quoted: await this.getQuotedMessage(body, instance),
|
||||
};
|
||||
|
||||
const messageSent = await this.sendAttachment(
|
||||
waInstance,
|
||||
chatId,
|
||||
attachment.data_url,
|
||||
formatText,
|
||||
options,
|
||||
);
|
||||
if (!messageSent && body.conversation?.id) {
|
||||
this.onSendMessageError(instance, body.conversation?.id);
|
||||
}
|
||||
|
||||
await this.updateChatwootMessageId(
|
||||
{
|
||||
...messageSent,
|
||||
},
|
||||
{
|
||||
messageId: body.id,
|
||||
inboxId: body.inbox?.id,
|
||||
conversationId: body.conversation?.id,
|
||||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
||||
},
|
||||
instance,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const data: SendTextDto = {
|
||||
number: chatId,
|
||||
text: formatText,
|
||||
delay: 1200,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
quoted: await this.getQuotedMessage(body, instance),
|
||||
};
|
||||
|
||||
@@ -1744,7 +1474,9 @@ export class ChatwootService {
|
||||
}
|
||||
|
||||
await this.updateChatwootMessageId(
|
||||
messageSent, // Já tem instanceId
|
||||
{
|
||||
...messageSent,
|
||||
},
|
||||
{
|
||||
messageId: body.id,
|
||||
inboxId: body.inbox?.id,
|
||||
@@ -1811,7 +1543,7 @@ export class ChatwootService {
|
||||
const data: SendTextDto = {
|
||||
number: chatId,
|
||||
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
||||
delay: 1200,
|
||||
delay: Math.floor(Math.random() * (2000 - 500 + 1)) + 500,
|
||||
};
|
||||
|
||||
sendTelemetry('/message/sendText');
|
||||
@@ -1835,55 +1567,6 @@ export class ChatwootService {
|
||||
const key = message.key as WAMessageKey;
|
||||
|
||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
||||
this.logger.verbose(
|
||||
`Skipping updateChatwootMessageId - messageId: ${chatwootMessageIds.messageId}, keyId: ${key?.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use instanceId from message or fallback to instance
|
||||
const instanceId = message.instanceId || instance.instanceId;
|
||||
|
||||
this.logger.verbose(
|
||||
`Updating message with chatwootMessageId: ${chatwootMessageIds.messageId}, keyId: ${key.id}, instanceId: ${instanceId}`,
|
||||
);
|
||||
|
||||
// Verifica se a mensagem existe antes de atualizar usando polling com exponential backoff
|
||||
let retries = 0;
|
||||
const maxRetries = this.DB_POLLING_MAX_RETRIES;
|
||||
const baseDelay = this.DB_POLLING_BASE_DELAY_MS;
|
||||
const maxDelay = this.DB_POLLING_MAX_DELAY_MS;
|
||||
let messageExists = false;
|
||||
|
||||
while (retries < maxRetries && !messageExists) {
|
||||
const existingMessage = await this.prismaRepository.message.findFirst({
|
||||
where: {
|
||||
instanceId: instanceId,
|
||||
key: {
|
||||
path: ['id'],
|
||||
equals: key.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMessage) {
|
||||
messageExists = true;
|
||||
this.logger.verbose(`Message found in database after ${retries} retries`);
|
||||
} else {
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
// Exponential backoff com max delay (seguindo padrão do sistema)
|
||||
const backoffDelay = Math.min(baseDelay * Math.pow(2, retries - 1), maxDelay);
|
||||
this.logger.verbose(`Message not found, retry ${retries}/${maxRetries} in ${backoffDelay}ms`);
|
||||
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
||||
} else {
|
||||
this.logger.verbose(`Message not found after ${retries} attempts`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageExists) {
|
||||
this.logger.warn(`Message not found in database after ${maxRetries} retries, keyId: ${key.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1896,7 +1579,7 @@ export class ChatwootService {
|
||||
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
||||
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
||||
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
||||
WHERE "instanceId" = ${instanceId}
|
||||
WHERE "instanceId" = ${instance.instanceId}
|
||||
AND "key"->>'id' = ${key.id}
|
||||
`;
|
||||
|
||||
@@ -1952,11 +1635,12 @@ export class ChatwootService {
|
||||
});
|
||||
|
||||
const key = message?.key as WAMessageKey;
|
||||
const messageContent = message?.message as WAMessageContent;
|
||||
|
||||
if (message && key?.id) {
|
||||
if (messageContent && key?.id) {
|
||||
return {
|
||||
key: message.key as proto.IMessageKey,
|
||||
message: message.message as proto.IMessage,
|
||||
key: key,
|
||||
message: messageContent,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2346,7 +2030,10 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const rawPhoneNumber =
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe
|
||||
? body.key.participantAlt.split('@')[0]
|
||||
: body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
@@ -2360,9 +2047,11 @@ export class ChatwootService {
|
||||
let content: string;
|
||||
|
||||
if (!body.key.fromMe) {
|
||||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||||
content = bodyMessage
|
||||
? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`
|
||||
: `**${formattedPhoneNumber} - ${participantName}:**`;
|
||||
} else {
|
||||
content = `${bodyMessage}`;
|
||||
content = bodyMessage ? bodyMessage : '';
|
||||
}
|
||||
|
||||
const send = await this.sendData(
|
||||
@@ -2487,7 +2176,10 @@ export class ChatwootService {
|
||||
|
||||
if (body.key.remoteJid.includes('@g.us')) {
|
||||
const participantName = body.pushName;
|
||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||
const rawPhoneNumber =
|
||||
body.key.addressingMode === 'lid' && !body.key.fromMe
|
||||
? body.key.participantAlt.split('@')[0]
|
||||
: body.key.participant.split('@')[0];
|
||||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||||
|
||||
let formattedPhoneNumber: string;
|
||||
@@ -2688,7 +2380,7 @@ export class ChatwootService {
|
||||
chatwootImport.clearAll(instance);
|
||||
}
|
||||
// Se não foi via QR code, verifica o throttling.
|
||||
else if (timeSinceLastNotification >= MIN_CONNECTION_NOTIFICATION_INTERVAL_MS) {
|
||||
else if (timeSinceLastNotification >= 30000) {
|
||||
const msgConnection = i18next.t('cw.inbox.connected');
|
||||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||||
waInstance.lastConnectionNotification = now;
|
||||
|
||||
Reference in New Issue
Block a user