mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-09 01:49:37 -06:00
Merge pull request #2048 from dersonbsb2022/main
feat(chatwoot): comprehensive improvements to message handling, editing, deletion and i18n (translate messages)
This commit is contained in:
commit
b808dda33b
4133
package-lock.json
generated
4133
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -254,6 +254,10 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
private endSession = false;
|
private endSession = false;
|
||||||
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
private logBaileys = this.configService.get<Log>('LOG').BAILEYS;
|
||||||
|
|
||||||
|
// Cache TTL constants (in seconds)
|
||||||
|
private readonly MESSAGE_CACHE_TTL_SECONDS = 5 * 60; // 5 minutes - avoid duplicate message processing
|
||||||
|
private readonly UPDATE_CACHE_TTL_SECONDS = 30 * 60; // 30 minutes - avoid duplicate status updates
|
||||||
|
|
||||||
public stateConnection: wa.StateConnection = { state: 'close' };
|
public stateConnection: wa.StateConnection = { state: 'close' };
|
||||||
|
|
||||||
public phoneNumber: string;
|
public phoneNumber: string;
|
||||||
@ -1065,6 +1069,11 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
settings: any,
|
settings: any,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
// Garantir que localChatwoot está carregado antes de processar mensagens
|
||||||
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && !this.localChatwoot?.enabled) {
|
||||||
|
await this.loadChatwoot();
|
||||||
|
}
|
||||||
|
|
||||||
for (const received of messages) {
|
for (const received of messages) {
|
||||||
if (received.key.remoteJid?.includes('@lid') && (received.key as ExtendedMessageKey).senderPn) {
|
if (received.key.remoteJid?.includes('@lid') && (received.key as ExtendedMessageKey).senderPn) {
|
||||||
(received.key as ExtendedMessageKey).previousRemoteJid = received.key.remoteJid;
|
(received.key as ExtendedMessageKey).previousRemoteJid = received.key.remoteJid;
|
||||||
@ -1150,7 +1159,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(type !== 'notify' && type !== 'append') ||
|
(type !== 'notify' && type !== 'append') ||
|
||||||
@ -1270,7 +1279,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
} else {
|
} else {
|
||||||
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
this.logger.info(`Update readed messages duplicated ignored [avoid deadlock]: ${messageKey}`);
|
||||||
}
|
}
|
||||||
@ -1445,12 +1454,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
const cached = await this.baileysCache.get(updateKey);
|
const cached = await this.baileysCache.get(updateKey);
|
||||||
|
|
||||||
if (cached) {
|
// Não ignorar mensagens deletadas (messageStubType === 1) mesmo que estejam em cache
|
||||||
|
const isDeletedMessage = update.messageStubType === 1;
|
||||||
|
|
||||||
|
if (cached && !isDeletedMessage) {
|
||||||
this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
this.logger.info(`Message duplicated ignored [avoid deadlock]: ${updateKey}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baileysCache.set(updateKey, true, 30 * 60);
|
if (!isDeletedMessage) {
|
||||||
|
await this.baileysCache.set(updateKey, true, this.UPDATE_CACHE_TTL_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
if (status[update.status] === 'READ' && key.fromMe) {
|
if (status[update.status] === 'READ' && key.fromMe) {
|
||||||
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||||
@ -1537,7 +1551,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
if (status[update.status] === status[4]) {
|
if (status[update.status] === status[4]) {
|
||||||
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
|
this.logger.log(`Update as read in message.update ${remoteJid} - ${timestamp}`);
|
||||||
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
await this.updateMessagesReadedByTimestamp(remoteJid, timestamp);
|
||||||
await this.baileysCache.set(messageKey, true, 5 * 60);
|
await this.baileysCache.set(messageKey, true, this.MESSAGE_CACHE_TTL_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.prismaRepository.message.update({
|
await this.prismaRepository.message.update({
|
||||||
@ -1554,8 +1568,22 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
this.sendDataWebhook(Events.MESSAGES_UPDATE, message);
|
||||||
|
|
||||||
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE)
|
if (this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
|
||||||
await this.prismaRepository.messageUpdate.create({ data: message });
|
// Verificar se a mensagem ainda existe antes de criar o update
|
||||||
|
const messageExists = await this.prismaRepository.message.findFirst({
|
||||||
|
where: {
|
||||||
|
instanceId: message.instanceId,
|
||||||
|
key: {
|
||||||
|
path: ['id'],
|
||||||
|
equals: message.keyId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (messageExists) {
|
||||||
|
await this.prismaRepository.messageUpdate.create({ data: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const existingChat = await this.prismaRepository.chat.findFirst({
|
const existingChat = await this.prismaRepository.chat.findFirst({
|
||||||
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
where: { instanceId: this.instanceId, remoteJid: message.remoteJid },
|
||||||
@ -4565,7 +4593,9 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async baileysAssertSessions(jids: string[]) {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public async baileysAssertSessions(jids: string[], _force?: boolean) {
|
||||||
|
// Note: _force parameter kept for API compatibility but not used in Baileys 7.0.0-rc.5+
|
||||||
const response = await this.client.assertSessions(jids);
|
const response = await this.client.assertSessions(jids);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@ -46,6 +46,25 @@ interface ChatwootMessage {
|
|||||||
export class ChatwootService {
|
export class ChatwootService {
|
||||||
private readonly logger = new Logger('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
|
||||||
|
|
||||||
private provider: any;
|
private provider: any;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -617,8 +636,12 @@ export class ChatwootService {
|
|||||||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await new Promise((res) => setTimeout(res, 300));
|
await new Promise((res) => setTimeout(res, this.LOCK_POLLING_DELAY_MS));
|
||||||
// Removed cache check here to ensure we always check Chatwoot
|
if (await this.cache.has(cacheKey)) {
|
||||||
|
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||||||
|
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||||||
|
return conversationId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1071,7 +1094,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
private async sendData(
|
private async sendData(
|
||||||
conversationId: number,
|
conversationId: number,
|
||||||
fileStream: Readable,
|
fileData: Buffer | Readable,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
messageType: 'incoming' | 'outgoing' | undefined,
|
messageType: 'incoming' | 'outgoing' | undefined,
|
||||||
content?: string,
|
content?: string,
|
||||||
@ -1099,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;
|
||||||
|
|
||||||
@ -1227,20 +1252,140 @@ export class ChatwootService {
|
|||||||
|
|
||||||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||||||
try {
|
try {
|
||||||
const parsedMedia = path.parse(decodeURIComponent(media));
|
// Sempre baixar o arquivo do MinIO/S3 antes de enviar
|
||||||
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
// URLs presigned podem expirar, então convertemos para base64
|
||||||
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
let mediaBuffer: Buffer;
|
||||||
|
let mimeType: string;
|
||||||
|
let fileName: string;
|
||||||
|
|
||||||
if (!mimeType) {
|
try {
|
||||||
const parts = media.split('/');
|
this.logger.verbose(`Downloading media from: ${media}`);
|
||||||
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, {
|
const response = await axios.get(media, {
|
||||||
responseType: 'arraybuffer',
|
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)
|
||||||
});
|
});
|
||||||
mimeType = response.headers['content-type'];
|
|
||||||
|
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determinar o tipo de mídia pelo mimetype
|
||||||
let type = 'document';
|
let type = 'document';
|
||||||
|
|
||||||
switch (mimeType.split('/')[0]) {
|
switch (mimeType.split('/')[0]) {
|
||||||
@ -1258,10 +1403,12 @@ export class ChatwootService {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Para áudio, usar base64 com data URI
|
||||||
if (type === 'audio') {
|
if (type === 'audio') {
|
||||||
|
const base64Audio = `data:${mimeType};base64,${mediaBuffer.toString('base64')}`;
|
||||||
const data: SendAudioDto = {
|
const data: SendAudioDto = {
|
||||||
number: number,
|
number: number,
|
||||||
audio: media,
|
audio: base64Audio,
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@ -1273,8 +1420,12 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Para outros tipos, converter para base64 puro (sem prefixo data URI)
|
||||||
|
const base64Media = mediaBuffer.toString('base64');
|
||||||
|
|
||||||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||||||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
const parsedExt = path.parse(fileName)?.ext;
|
||||||
|
if (type === 'image' && parsedExt && documentExtensions.includes(parsedExt)) {
|
||||||
type = 'document';
|
type = 'document';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1282,7 +1433,7 @@ export class ChatwootService {
|
|||||||
number: number,
|
number: number,
|
||||||
mediatype: type as any,
|
mediatype: type as any,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
media: media,
|
media: base64Media, // Base64 puro, sem prefixo
|
||||||
delay: 1200,
|
delay: 1200,
|
||||||
quoted: options?.quoted,
|
quoted: options?.quoted,
|
||||||
};
|
};
|
||||||
@ -1298,6 +1449,7 @@ export class ChatwootService {
|
|||||||
return messageSent;
|
return messageSent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
|
throw error; // Re-throw para que o erro seja tratado pelo caller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1337,9 +1489,87 @@ 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 {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@ -1358,6 +1588,39 @@ export class ChatwootService {
|
|||||||
this.cache.delete(keyToDelete);
|
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 (
|
if (
|
||||||
!body?.conversation ||
|
!body?.conversation ||
|
||||||
body.private ||
|
body.private ||
|
||||||
@ -1380,29 +1643,6 @@ export class ChatwootService {
|
|||||||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||||||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
|
|
||||||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
|
||||||
const message = await this.prismaRepository.message.findFirst({
|
|
||||||
where: {
|
|
||||||
chatwootMessageId: body.id,
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
const key = message.key as ExtendedMessageKey;
|
|
||||||
|
|
||||||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
|
||||||
|
|
||||||
await this.prismaRepository.message.deleteMany({
|
|
||||||
where: {
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
chatwootMessageId: body.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return { message: 'bot' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
|
const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
|
||||||
|
|
||||||
if (chatId === '123456' && body.message_type === 'outgoing') {
|
if (chatId === '123456' && body.message_type === 'outgoing') {
|
||||||
@ -1498,40 +1738,58 @@ export class ChatwootService {
|
|||||||
|
|
||||||
for (const message of body.conversation.messages) {
|
for (const message of body.conversation.messages) {
|
||||||
if (message.attachments && message.attachments.length > 0) {
|
if (message.attachments && message.attachments.length > 0) {
|
||||||
for (const attachment of message.attachments) {
|
// Processa anexos de forma assíncrona para não bloquear o webhook
|
||||||
if (!messageReceived) {
|
const processAttachments = async () => {
|
||||||
formatText = null;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const options: Options = {
|
// Executa em background sem bloquear
|
||||||
quoted: await this.getQuotedMessage(body, instance),
|
processAttachments().catch((error) => {
|
||||||
};
|
this.logger.error(error);
|
||||||
|
});
|
||||||
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,
|
|
||||||
owner: instance.instanceName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: body.id,
|
|
||||||
inboxId: body.inbox?.id,
|
|
||||||
conversationId: body.conversation?.id,
|
|
||||||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
|
||||||
},
|
|
||||||
instance,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const data: SendTextDto = {
|
const data: SendTextDto = {
|
||||||
number: chatId,
|
number: chatId,
|
||||||
@ -1554,10 +1812,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.updateChatwootMessageId(
|
await this.updateChatwootMessageId(
|
||||||
{
|
messageSent, // Já tem instanceId
|
||||||
...messageSent,
|
|
||||||
instanceId: instance.instanceId,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
messageId: body.id,
|
messageId: body.id,
|
||||||
inboxId: body.inbox?.id,
|
inboxId: body.inbox?.id,
|
||||||
@ -1648,11 +1903,60 @@ export class ChatwootService {
|
|||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as ExtendedMessageKey;
|
||||||
|
|
||||||
if (!chatwootMessageIds.messageId || !key?.id) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use raw SQL to avoid JSON path issues
|
// Use raw SQL to avoid JSON path issues
|
||||||
await this.prismaRepository.$executeRaw`
|
const result = await this.prismaRepository.$executeRaw`
|
||||||
UPDATE "Message"
|
UPDATE "Message"
|
||||||
SET
|
SET
|
||||||
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
"chatwootMessageId" = ${chatwootMessageIds.messageId},
|
||||||
@ -1660,10 +1964,12 @@ export class ChatwootService {
|
|||||||
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
"chatwootInboxId" = ${chatwootMessageIds.inboxId},
|
||||||
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
"chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId},
|
||||||
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
"chatwootIsRead" = ${chatwootMessageIds.isRead || false}
|
||||||
WHERE "instanceId" = ${instance.instanceId}
|
WHERE "instanceId" = ${instanceId}
|
||||||
AND "key"->>'id' = ${key.id}
|
AND "key"->>'id' = ${key.id}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
this.logger.verbose(`Update result: ${result} rows affected`);
|
||||||
|
|
||||||
if (this.isImportHistoryAvailable()) {
|
if (this.isImportHistoryAvailable()) {
|
||||||
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||||||
}
|
}
|
||||||
@ -2100,11 +2406,6 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const fileData = Buffer.from(downloadBase64.base64, 'base64');
|
const fileData = Buffer.from(downloadBase64.base64, 'base64');
|
||||||
|
|
||||||
const fileStream = new Readable();
|
|
||||||
fileStream._read = () => {};
|
|
||||||
fileStream.push(fileData);
|
|
||||||
fileStream.push(null);
|
|
||||||
|
|
||||||
if (body.key.remoteJid.includes('@g.us')) {
|
if (body.key.remoteJid.includes('@g.us')) {
|
||||||
const participantName = body.pushName;
|
const participantName = body.pushName;
|
||||||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||||||
@ -2128,7 +2429,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const send = await this.sendData(
|
const send = await this.sendData(
|
||||||
getConversation,
|
getConversation,
|
||||||
fileStream,
|
fileData,
|
||||||
nameFile,
|
nameFile,
|
||||||
messageType,
|
messageType,
|
||||||
content,
|
content,
|
||||||
@ -2148,7 +2449,7 @@ export class ChatwootService {
|
|||||||
} else {
|
} else {
|
||||||
const send = await this.sendData(
|
const send = await this.sendData(
|
||||||
getConversation,
|
getConversation,
|
||||||
fileStream,
|
fileData,
|
||||||
nameFile,
|
nameFile,
|
||||||
messageType,
|
messageType,
|
||||||
bodyMessage,
|
bodyMessage,
|
||||||
@ -2216,11 +2517,6 @@ export class ChatwootService {
|
|||||||
});
|
});
|
||||||
const processedBuffer = await img.getBuffer(JimpMime.png);
|
const processedBuffer = await img.getBuffer(JimpMime.png);
|
||||||
|
|
||||||
const fileStream = new Readable();
|
|
||||||
fileStream._read = () => {}; // _read is required but you can noop it
|
|
||||||
fileStream.push(processedBuffer);
|
|
||||||
fileStream.push(null);
|
|
||||||
|
|
||||||
const truncStr = (str: string, len: number) => {
|
const truncStr = (str: string, len: number) => {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
|
|
||||||
@ -2232,7 +2528,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const send = await this.sendData(
|
const send = await this.sendData(
|
||||||
getConversation,
|
getConversation,
|
||||||
fileStream,
|
processedBuffer,
|
||||||
nameFile,
|
nameFile,
|
||||||
messageType,
|
messageType,
|
||||||
`${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`,
|
`${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`,
|
||||||
@ -2346,9 +2642,53 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.edit' || event === 'send.message.update') {
|
if (event === 'messages.edit' || event === 'send.message.update') {
|
||||||
const editedText = `${
|
const editedMessageContent =
|
||||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text;
|
||||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
|
||||||
|
// Se não houver conteúdo editado, verificar se é uma deleção
|
||||||
|
if (!editedMessageContent || editedMessageContent.trim() === '') {
|
||||||
|
// Verificar se é uma mensagem revogada (messageStubType: 1)
|
||||||
|
const messageStubType = body?.update?.messageStubType || body?.messageStubType;
|
||||||
|
this.logger.verbose(
|
||||||
|
`No edited content found - messageStubType: ${messageStubType}, body.update: ${JSON.stringify(body?.update)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (messageStubType === 1) {
|
||||||
|
// É uma mensagem deletada - processar exclusão no Chatwoot
|
||||||
|
this.logger.verbose('Message revoked detected, processing deletion in Chatwoot');
|
||||||
|
|
||||||
|
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||||
|
|
||||||
|
if (message?.chatwootMessageId && message?.chatwootConversationId) {
|
||||||
|
try {
|
||||||
|
await client.messages.delete({
|
||||||
|
accountId: this.provider.accountId,
|
||||||
|
conversationId: message.chatwootConversationId,
|
||||||
|
messageId: message.chatwootMessageId,
|
||||||
|
});
|
||||||
|
this.logger.verbose(`Deleted revoked message ${message.chatwootMessageId} in Chatwoot`);
|
||||||
|
|
||||||
|
// Remover do banco de dados
|
||||||
|
await this.prismaRepository.message.deleteMany({
|
||||||
|
where: {
|
||||||
|
key: {
|
||||||
|
path: ['id'],
|
||||||
|
equals: body.key.id,
|
||||||
|
},
|
||||||
|
instanceId: instance.instanceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.verbose(`Removed revoked message from database`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error deleting revoked message: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.verbose('Message deleted, skipping edit notification');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@ -2357,10 +2697,24 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const key = message.key as ExtendedMessageKey;
|
const key = message.key as ExtendedMessageKey;
|
||||||
|
|
||||||
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
||||||
|
|
||||||
if (message && message.chatwootConversationId) {
|
if (message && message.chatwootConversationId && message.chatwootMessageId) {
|
||||||
|
// Deletar a mensagem original no Chatwoot
|
||||||
|
try {
|
||||||
|
await client.messages.delete({
|
||||||
|
accountId: this.provider.accountId,
|
||||||
|
conversationId: message.chatwootConversationId,
|
||||||
|
messageId: message.chatwootMessageId,
|
||||||
|
});
|
||||||
|
this.logger.verbose(`Deleted original message ${message.chatwootMessageId} for edit`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error deleting original message for edit: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Criar nova mensagem com formato: "Mensagem editada:\n\nteste1"
|
||||||
|
const editedText = `${i18next.t('cw.message.edited')}:\n\n${editedMessageContent}`;
|
||||||
|
|
||||||
const send = await this.createMessage(
|
const send = await this.createMessage(
|
||||||
instance,
|
instance,
|
||||||
message.chatwootConversationId,
|
message.chatwootConversationId,
|
||||||
@ -2375,10 +2729,31 @@ export class ChatwootService {
|
|||||||
null,
|
null,
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!send) {
|
if (!send) {
|
||||||
this.logger.warn('edited message not sent');
|
this.logger.warn('edited message not sent');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(`Created edited message in Chatwoot with ID: ${send.id}`);
|
||||||
|
|
||||||
|
// Atualizar o chatwootMessageId no banco para apontar para a nova mensagem
|
||||||
|
// Isso permite que a exclusão funcione após a edição
|
||||||
|
try {
|
||||||
|
await this.prismaRepository.message.update({
|
||||||
|
where: {
|
||||||
|
id: message.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
chatwootMessageId: send.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.logger.verbose(
|
||||||
|
`Updated chatwootMessageId from ${message.chatwootMessageId} to ${send.id} for message ${body.key.id}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error updating chatwootMessageId after edit: ${error}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,21 @@ import fs from 'fs';
|
|||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
const distPath = path.resolve(process.cwd(), 'dist', 'translations');
|
// Make translations base directory configurable via environment variable
|
||||||
const srcPath = path.resolve(process.cwd(), 'src', 'utils', 'translations');
|
const envBaseDir = process.env.TRANSLATIONS_BASE_DIR;
|
||||||
|
let baseDir: string;
|
||||||
|
|
||||||
let translationsPath;
|
if (envBaseDir) {
|
||||||
|
// Use explicitly configured base directory
|
||||||
if (fs.existsSync(distPath)) {
|
baseDir = envBaseDir;
|
||||||
translationsPath = distPath;
|
|
||||||
} else if (fs.existsSync(srcPath)) {
|
|
||||||
translationsPath = srcPath;
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Translations directory not found in dist or src.');
|
// Fallback to auto-detection if env variable is not set
|
||||||
// Fallback to a non-existent path or handle error appropriately
|
const isProduction = fs.existsSync(path.join(process.cwd(), 'dist'));
|
||||||
translationsPath = '';
|
baseDir = isProduction ? 'dist' : 'src/utils';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const translationsPath = path.join(process.cwd(), baseDir, 'translations');
|
||||||
|
|
||||||
const languages = ['en', 'pt-BR', 'es'];
|
const languages = ['en', 'pt-BR', 'es'];
|
||||||
const configService: ConfigService = new ConfigService();
|
const configService: ConfigService = new ConfigService();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user