Ajustes da sincronizacao

This commit is contained in:
Éder Costa 2025-04-10 18:44:57 -03:00
parent 427c994993
commit a6304d3eec
7 changed files with 2579 additions and 2337 deletions

11
BuildImage.ps1 Normal file
View File

@ -0,0 +1,11 @@
(Get-ECRLoginCommand).Password | docker login --username AWS --password-stdin 130811782740.dkr.ecr.us-east-2.amazonaws.com
#
$ErrorActionPreference = "Stop"
docker build -t evolution -f .\Dockerfile .
docker tag evolution:latest 130811782740.dkr.ecr.us-east-2.amazonaws.com/evolution
docker push 130811782740.dkr.ecr.us-east-2.amazonaws.com/evolution

View File

@ -3,7 +3,7 @@ FROM node:20-alpine AS builder
RUN apk update && \ RUN apk update && \
apk add git ffmpeg wget curl bash openssl apk add git ffmpeg wget curl bash openssl
LABEL version="2.2.3" description="Api to control whatsapp features through http requests." LABEL version="2.2.3.3" description="Api to control whatsapp features through http requests."
LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes" LABEL maintainer="Davidson Gomes" git="https://github.com/DavidsonGomes"
LABEL contact="contato@atendai.com" LABEL contact="contato@atendai.com"

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3", "version": "2.2.3.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3", "version": "2.2.3.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@adiwajshing/keyed-db": "^0.2.4", "@adiwajshing/keyed-db": "^0.2.4",

View File

@ -1,6 +1,6 @@
{ {
"name": "evolution-api", "name": "evolution-api",
"version": "2.2.3", "version": "2.2.3.3",
"description": "Rest api for communication with WhatsApp", "description": "Rest api for communication with WhatsApp",
"main": "./dist/main.js", "main": "./dist/main.js",
"type": "commonjs", "type": "commonjs",

View File

@ -146,6 +146,12 @@ import { v4 } from 'uuid';
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys'; import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
type DownloadMediaMessageContext = {
reuploadRequest: (msg: WAMessage) => Promise<WAMessage>;
logger: P.Logger;
};
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine()); const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
// Adicione a função getVideoDuration no início do arquivo // Adicione a função getVideoDuration no início do arquivo
@ -3601,94 +3607,145 @@ export class BaileysStartupService extends ChannelStartupService {
} }
} }
public async getBase64FromMediaMessage(data: getBase64FromMediaMessageDto, getBuffer = false) {
public async getBase64FromMediaMessage(
data: getBase64FromMediaMessageDto,
getBuffer = false
) {
try { try {
const m = data?.message; const m = data?.message;
const convertToMp4 = data?.convertToMp4 ?? false; const convertToMp4 = data?.convertToMp4 ?? false;
const msg = m?.message ? m : ((await this.getMessage(m.key, true)) as proto.IWebMessageInfo); // Se já houver propriedade "message", usa-o; senão, busca-o via key
const msg: proto.IWebMessageInfo = m?.message
? m
: (await this.getMessage(m.key, true)) as proto.IWebMessageInfo;
if (!msg) { if (!msg) {
throw 'Message not found'; throw new Error('Message not found');
} }
// Verifica se o conteúdo está aninhado em algum subtipo (ex.: extendedTextMessage)
for (const subtype of MessageSubtype) { for (const subtype of MessageSubtype) {
if (msg.message[subtype]) { if (msg.message[subtype]) {
msg.message = msg.message[subtype].message; msg.message = msg.message[subtype].message;
break;
} }
} }
// Identifica o tipo de mídia contido na mensagem
let mediaMessage: any; let mediaMessage: any;
let mediaType: string; let mediaType = '';
for (const type of TypeMediaMessage) { for (const type of TypeMediaMessage) {
mediaMessage = msg.message[type]; if (msg.message[type]) {
if (mediaMessage) { mediaMessage = msg.message[type];
mediaType = type; mediaType = type;
break; break;
} }
} }
if (!mediaMessage) { if (!mediaMessage) {
throw 'The message is not of the media type'; throw new Error('The message is not of the media type');
} }
if (typeof mediaMessage['mediaKey'] === 'object') { // Se o mediaKey for um objeto, forçamos a serialização para “descolar” possíveis problemas
if (typeof mediaMessage.mediaKey === 'object') {
msg.message = JSON.parse(JSON.stringify(msg.message)); msg.message = JSON.parse(JSON.stringify(msg.message));
} }
const buffer = await downloadMediaMessage( // Define um contexto completo conforme DownloadMediaMessageContext
{ key: msg?.key, message: msg?.message }, const downloadContext: DownloadMediaMessageContext = {
'buffer', logger: P({ level: 'error' }),
{}, reuploadRequest: async (message: WAMessage): Promise<WAMessage> => {
{ // Aqui chamamos explicitamente o método que atualiza a mídia;
logger: P({ level: 'error' }) as any, // Se o método updateMediaMessage não retornar nada (void), retornamos a própria mensagem.
reuploadRequest: this.client.updateMediaMessage, const updatedMsg = await this.client.updateMediaMessage(message);
return updatedMsg ? updatedMsg : message;
}, },
); };
let buffer: Buffer;
try {
// Tenta baixar a mídia usando o contexto com reuploadRequest
buffer = (await downloadMediaMessage(
{ key: msg.key, message: msg.message },
'buffer',
{},
downloadContext
)) as Buffer;
} catch (initialError) {
this.logger.warn(
'Initial downloadMediaMessage failed, updating media and retrying...'
);
// Se a tentativa falhar (possivelmente por URL expirada), atualiza a mídia e refaz o download
await this.client.updateMediaMessage(msg);
buffer = (await downloadMediaMessage(
{ key: msg.key, message: msg.message },
'buffer',
{},
{ logger: P({ level: 'error' }), reuploadRequest: async (m: WAMessage) => m } // Contexto “vazio”
)) as Buffer;
}
const typeMessage = getContentType(msg.message); const typeMessage = getContentType(msg.message);
const ext = mimeTypes.extension(mediaMessage?.mimetype);
const ext = mimeTypes.extension(mediaMessage?.['mimetype']); const fileName =
const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`; mediaMessage?.fileName || `${msg.key.id}.${ext}` || `${v4()}.${ext}`;
// Se for áudio e for pedido converter para mp4, processa a conversão
if (convertToMp4 && typeMessage === 'audioMessage') { if (convertToMp4 && typeMessage === 'audioMessage') {
try { try {
const convert = await this.processAudioMp4(buffer.toString('base64')); const converted = await this.processAudioMp4(buffer.toString('base64'));
if (Buffer.isBuffer(converted)) {
if (Buffer.isBuffer(convert)) { return {
const result = {
mediaType, mediaType,
fileName, fileName,
caption: mediaMessage['caption'], caption: mediaMessage.caption,
size: { size: {
fileLength: mediaMessage['fileLength'], fileLength: mediaMessage.fileLength,
height: mediaMessage['height'], height: mediaMessage.height,
width: mediaMessage['width'], width: mediaMessage.width,
}, },
mimetype: 'audio/mp4', mimetype: 'audio/mp4',
base64: convert.toString('base64'), base64: converted.toString('base64'),
buffer: getBuffer ? convert : null, buffer: getBuffer ? converted : null,
}; };
return result;
} }
} catch (error) { } catch (convertError) {
this.logger.error('Error converting audio to mp4:'); this.logger.error('Error converting audio to mp4:');
this.logger.error(error); this.logger.error(convertError);
throw new BadRequestException('Failed to convert audio to MP4'); throw new BadRequestException('Failed to convert audio to MP4');
} }
} }
// Retorna os dados da mídia
return { return {
mediaType, mediaType,
fileName, fileName,
caption: mediaMessage['caption'], caption: mediaMessage.caption,
size: { size: {
fileLength: mediaMessage['fileLength'], fileLength: mediaMessage.fileLength,
height: mediaMessage['height'], height: mediaMessage.height,
width: mediaMessage['width'], width: mediaMessage.width,
}, },
mimetype: mediaMessage['mimetype'], mimetype: mediaMessage.mimetype,
base64: buffer.toString('base64'), base64: buffer.toString('base64'),
buffer: getBuffer ? buffer : null, buffer: getBuffer ? buffer : null,
}; };
@ -3699,6 +3756,19 @@ export class BaileysStartupService extends ChannelStartupService {
} }
} }
public async fetchPrivacySettings() { public async fetchPrivacySettings() {
const privacy = await this.client.fetchPrivacySettings(); const privacy = await this.client.fetchPrivacySettings();

View File

@ -199,6 +199,10 @@ class ChatwootImport {
provider: ChatwootModel, provider: ChatwootModel,
) { ) {
try { try {
this.logger.info(
`[importHistoryMessages] Iniciando importação de mensagens para a instância "${instance.instanceName}".`
);
const pgClient = postgresClient.getChatwootConnection(); const pgClient = postgresClient.getChatwootConnection();
const chatwootUser = await this.getChatwootUser(provider); const chatwootUser = await this.getChatwootUser(provider);
@ -209,28 +213,32 @@ class ChatwootImport {
let totalMessagesImported = 0; let totalMessagesImported = 0;
let messagesOrdered = this.historyMessages.get(instance.instanceName) || []; let messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
this.logger.info(
`[importHistoryMessages] Número de mensagens recuperadas do histórico: ${messagesOrdered.length}.`
);
if (messagesOrdered.length === 0) { if (messagesOrdered.length === 0) {
return 0; return 0;
} }
// ordering messages by number and timestamp asc // Ordenando as mensagens por remoteJid e timestamp (ascendente)
messagesOrdered.sort((a, b) => { messagesOrdered.sort((a, b) => {
const aKey = a.key as { const aKey = a.key as { remoteJid: string };
remoteJid: string; const bKey = b.key as { remoteJid: string };
};
const bKey = b.key as {
remoteJid: string;
};
const aMessageTimestamp = a.messageTimestamp as any as number; const aMessageTimestamp = a.messageTimestamp as any as number;
const bMessageTimestamp = b.messageTimestamp as any as number; const bMessageTimestamp = b.messageTimestamp as any as number;
return parseInt(aKey.remoteJid) - parseInt(bKey.remoteJid) || aMessageTimestamp - bMessageTimestamp; return parseInt(aKey.remoteJid) - parseInt(bKey.remoteJid) || aMessageTimestamp - bMessageTimestamp;
}); });
this.logger.info('[importHistoryMessages] Mensagens ordenadas por remoteJid e messageTimestamp.');
// Mapeando mensagens por telefone
const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered); const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered);
// Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number} this.logger.info(
`[importHistoryMessages] Mensagens mapeadas para ${allMessagesMappedByPhoneNumber.size} números únicos.`
);
// Map: +numero => { first: timestamp, last: timestamp }
const phoneNumbersWithTimestamp = new Map<string, firstLastTimestamp>(); const phoneNumbersWithTimestamp = new Map<string, firstLastTimestamp>();
allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => { allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => {
phoneNumbersWithTimestamp.set(phoneNumber, { phoneNumbersWithTimestamp.set(phoneNumber, {
@ -238,15 +246,37 @@ class ChatwootImport {
last: messages[messages.length - 1]?.messageTimestamp as any as number, last: messages[messages.length - 1]?.messageTimestamp as any as number,
}); });
}); });
this.logger.info(
`[importHistoryMessages] Criado mapa de timestamps para ${phoneNumbersWithTimestamp.size} números.`
);
const existingSourceIds = await this.getExistingSourceIds(messagesOrdered.map((message: any) => message.key.id)); // Removendo mensagens que já existem no banco (verificação pelo source_id)
const existingSourceIds = await this.getExistingSourceIds(
messagesOrdered.map((message: any) => message.key.id)
);
this.logger.info(
`[importHistoryMessages] Quantidade de source_ids existentes no banco: ${existingSourceIds.size}.`
);
const initialCount = messagesOrdered.length;
messagesOrdered = messagesOrdered.filter((message: any) => !existingSourceIds.has(message.key.id)); messagesOrdered = messagesOrdered.filter((message: any) => !existingSourceIds.has(message.key.id));
// processing messages in batch this.logger.info(
`[importHistoryMessages] Mensagens filtradas: de ${initialCount} para ${messagesOrdered.length} após remoção de duplicados.`
);
// Processamento das mensagens em batches
const batchSize = 4000; const batchSize = 4000;
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize); let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
let batchNumber = 1;
while (messagesChunk.length > 0) { while (messagesChunk.length > 0) {
// Map structure: +552199999999 => Message[] this.logger.info(
`[importHistoryMessages] Processando batch ${batchNumber} com ${messagesChunk.length} mensagens.`
);
// Agrupando as mensagens deste batch por telefone
const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk); const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk);
this.logger.info(
`[importHistoryMessages] Batch ${batchNumber}: ${messagesByPhoneNumber.size} números únicos encontrados.`
);
if (messagesByPhoneNumber.size > 0) { if (messagesByPhoneNumber.size > 0) {
const fksByNumber = await this.selectOrCreateFksFromChatwoot( const fksByNumber = await this.selectOrCreateFksFromChatwoot(
@ -255,8 +285,11 @@ class ChatwootImport {
phoneNumbersWithTimestamp, phoneNumbersWithTimestamp,
messagesByPhoneNumber, messagesByPhoneNumber,
); );
this.logger.info(
`[importHistoryMessages] Batch ${batchNumber}: FKs recuperados para ${fksByNumber.size} números.`
);
// inserting messages in chatwoot db // Inserindo as mensagens no banco
let sqlInsertMsg = `INSERT INTO messages let sqlInsertMsg = `INSERT INTO messages
(content, processed_message_content, account_id, inbox_id, conversation_id, message_type, private, content_type, (content, processed_message_content, account_id, inbox_id, conversation_id, message_type, private, content_type,
sender_type, sender_id, source_id, created_at, updated_at) VALUES `; sender_type, sender_id, source_id, created_at, updated_at) VALUES `;
@ -264,16 +297,16 @@ class ChatwootImport {
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => { messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
const fksChatwoot = fksByNumber.get(phoneNumber); const fksChatwoot = fksByNumber.get(phoneNumber);
this.logger.info(
`[importHistoryMessages] Número ${phoneNumber}: processando ${messages.length} mensagens.`
);
messages.forEach((message) => { messages.forEach((message) => {
if (!message.message) { if (!message.message) {
return; return;
} }
if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) { if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) {
return; return;
} }
const contentMessage = this.getContentMessage(chatwootService, message); const contentMessage = this.getContentMessage(chatwootService, message);
if (!contentMessage) { if (!contentMessage) {
return; return;
@ -308,123 +341,237 @@ class ChatwootImport {
if (sqlInsertMsg.slice(-1) === ',') { if (sqlInsertMsg.slice(-1) === ',') {
sqlInsertMsg = sqlInsertMsg.slice(0, -1); sqlInsertMsg = sqlInsertMsg.slice(0, -1);
} }
totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0; const result = await pgClient.query(sqlInsertMsg, bindInsertMsg);
const rowCount = result?.rowCount ?? 0;
totalMessagesImported += rowCount;
this.logger.info(
`[importHistoryMessages] Batch ${batchNumber}: Inseridas ${rowCount} mensagens no banco.`
);
} }
} }
batchNumber++;
messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize); messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize);
} }
this.deleteHistoryMessages(instance); this.deleteHistoryMessages(instance);
this.deleteRepositoryMessagesCache(instance); this.deleteRepositoryMessagesCache(instance);
this.logger.info(
`[importHistoryMessages] Histórico e cache de mensagens da instância "${instance.instanceName}" foram limpos.`
);
const providerData: ChatwootDto = { const providerData: ChatwootDto = {
...provider, ...provider,
ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [], ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [],
}; };
this.logger.info(
`[importHistoryMessages] Iniciando importação de contatos do histórico para a instância "${instance.instanceName}".`
);
this.importHistoryContacts(instance, providerData); this.importHistoryContacts(instance, providerData);
this.logger.info(
`[importHistoryMessages] Concluída a importação de mensagens para a instância "${instance.instanceName}". Total importado: ${totalMessagesImported}.`
);
return totalMessagesImported; return totalMessagesImported;
} catch (error) { } catch (error) {
this.logger.error(`Error on import history messages: ${error.toString()}`); this.logger.error(`Error on import history messages: ${error.toString()}`);
this.deleteHistoryMessages(instance); this.deleteHistoryMessages(instance);
this.deleteRepositoryMessagesCache(instance); this.deleteRepositoryMessagesCache(instance);
} }
} }
private normalizeBrazilianPhoneNumberOptions(raw: string): [string, string] {
if (!raw.startsWith('+55')) {
return [raw, raw];
}
// Remove o prefixo "+55"
const digits = raw.slice(3); // pega tudo após os 3 primeiros caracteres
if (digits.length === 10) {
// Se tiver 10 dígitos, assume que é o formato antigo.
// Old: exatamente o valor recebido.
// New: insere o '9' após os dois primeiros dígitos.
const newDigits = digits.slice(0, 2) + '9' + digits.slice(2);
return [raw, `+55${newDigits}`];
} else if (digits.length === 11) {
// Se tiver 11 dígitos, assume que é o formato novo.
// New: exatamente o valor recebido.
// Old: remove o dígito extra na terceira posição.
const oldDigits = digits.slice(0, 2) + digits.slice(3);
return [`+55${oldDigits}`, raw];
} else {
// Se por algum motivo tiver outra quantidade de dígitos, retorna os mesmos valores.
return [raw, raw];
}
}
public async selectOrCreateFksFromChatwoot( public async selectOrCreateFksFromChatwoot(
provider: ChatwootModel, provider: ChatwootModel,
inbox: inbox, inbox: inbox,
phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>, phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>,
messagesByPhoneNumber: Map<string, Message[]>, messagesByPhoneNumber: Map<string, Message[]>
): Promise<Map<string, FksChatwoot>> { ): Promise<Map<string, FksChatwoot>> {
const pgClient = postgresClient.getChatwootConnection(); const pgClient = postgresClient.getChatwootConnection();
const resultMap = new Map<string, FksChatwoot>();
try {
// Para cada telefone presente
for (const rawPhoneNumber of messagesByPhoneNumber.keys()) {
const bindValues = [provider.accountId, inbox.id]; // Obtém as duas versões normalizadas do número (com e sem nono dígito)
const phoneNumberBind = Array.from(messagesByPhoneNumber.keys()) const [normalizedWith, normalizedWithout] = this.normalizeBrazilianPhoneNumberOptions(rawPhoneNumber);
.map((phoneNumber) => { const phoneTimestamp = phoneNumbersWithTimestamp.get(rawPhoneNumber);
const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber); if (!phoneTimestamp) {
this.logger.warn(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
if (phoneNumberTimestamp) { // Se preferir interromper, lance um erro:
bindValues.push(phoneNumber); throw new Error(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
let bindStr = `($${bindValues.length},`;
bindValues.push(phoneNumberTimestamp.first);
bindStr += `$${bindValues.length},`;
bindValues.push(phoneNumberTimestamp.last);
return `${bindStr}$${bindValues.length})`;
} }
})
.join(',');
// select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db // --- Etapa 1: Buscar ou Inserir o Contato ---
const sqlFromChatwoot = `WITH let contact;
phone_number AS ( try {
SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM ( this.logger.verbose(`Buscando contato para: ${normalizedWith} OU ${normalizedWithout}`);
VALUES const selectContactQuery = `
${phoneNumberBind} SELECT id, phone_number
) as t (phone_number, created_at, last_activity_at) FROM contacts
), WHERE account_id = $1
AND (phone_number = $2 OR phone_number = $3)
LIMIT 1
`;
const contactRes = await pgClient.query(selectContactQuery, [
provider.accountId,
normalizedWith,
normalizedWithout
]);
if (contactRes.rowCount > 0) {
contact = contactRes.rows[0];
this.logger.verbose(`Contato encontrado: ${JSON.stringify(contact)}`);
} else {
this.logger.verbose(`Contato não encontrado. Inserindo novo contato para ${normalizedWith}`);
const insertContactQuery = `
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
VALUES (REPLACE($2, '+', ''), $2, $1, CONCAT(REPLACE($2, '+', ''), '@s.whatsapp.net'),
to_timestamp($3), to_timestamp($4))
RETURNING id, phone_number
`;
const insertRes = await pgClient.query(insertContactQuery, [
provider.accountId,
normalizedWith,
phoneTimestamp.first,
phoneTimestamp.last,
]);
contact = insertRes.rows[0];
this.logger.verbose(`Novo contato inserido: ${JSON.stringify(contact)}`);
}
} catch (error) {
this.logger.error(`Erro ao recuperar/inserir contato para ${rawPhoneNumber}: ${error}`);
throw error;
}
only_new_phone_number AS ( // --- Etapa 2: Buscar ou Inserir a Conversa (e o Contact_inboxes) ---
SELECT * FROM phone_number let conversation;
WHERE phone_number NOT IN ( try {
SELECT phone_number this.logger.verbose(`Buscando conversa para o contato (ID: ${contact.id}) na caixa ${inbox.id}`);
FROM contacts const selectConversationQuery = `
JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2 SELECT con.id AS conversation_id, con.contact_id
JOIN conversations con ON con.contact_inbox_id = ci.id FROM conversations con
AND con.account_id = $1 JOIN contact_inboxes ci ON ci.contact_id = con.contact_id AND ci.inbox_id = $2
AND con.inbox_id = $2 WHERE con.account_id = $1 AND con.inbox_id = $2 AND con.contact_id = $3
AND con.contact_id = contacts.id LIMIT 1
WHERE contacts.account_id = $1 `;
) const convRes = await pgClient.query(selectConversationQuery, [provider.accountId, inbox.id, contact.id]);
), if (convRes.rowCount > 0) {
conversation = convRes.rows[0];
new_contact AS ( this.logger.verbose(`Conversa encontrada: ${JSON.stringify(conversation)}`);
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at) } else {
SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''), this.logger.verbose(`Nenhuma conversa encontrada para o contato ${contact.id}. Verificando contact_inboxes.`);
'@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at) let contactInboxId: number;
FROM only_new_phone_number AS p const selectContactInboxQuery = `
ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at SELECT id
RETURNING id, phone_number, created_at, updated_at FROM contact_inboxes
), WHERE contact_id = $1 AND inbox_id = $2
LIMIT 1
new_contact_inbox AS ( `;
const ciRes = await pgClient.query(selectContactInboxQuery, [contact.id, inbox.id]);
if (ciRes.rowCount > 0) {
contactInboxId = ciRes.rows[0].id;
this.logger.verbose(`contact_inbox encontrado: ${contactInboxId}`);
} else {
this.logger.verbose(`Contact_inbox não encontrado para o contato ${contact.id}. Inserindo novo contact_inbox.`);
const insertContactInboxQuery = `
INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at) INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at)
SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at VALUES ($1, $2, gen_random_uuid(), NOW(), NOW())
FROM new_contact RETURNING id
RETURNING id, contact_id, created_at, updated_at `;
), const ciInsertRes = await pgClient.query(insertContactInboxQuery, [contact.id, inbox.id]);
contactInboxId = ciInsertRes.rows[0].id;
this.logger.verbose(`Novo contact_inbox inserido com ID: ${contactInboxId}`);
}
new_conversation AS ( this.logger.verbose(`Inserindo conversa para o contato ${contact.id} com contact_inbox ${contactInboxId}`);
INSERT INTO conversations (account_id, inbox_id, status, contact_id, const insertConversationQuery = `
contact_inbox_id, uuid, last_activity_at, created_at, updated_at) INSERT INTO conversations
SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(), (account_id, inbox_id, status, contact_id, contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at VALUES
FROM new_contact_inbox ($1, $2, 0, $3, $4, gen_random_uuid(), NOW(), NOW(), NOW())
RETURNING id, contact_id RETURNING id AS conversation_id, contact_id
) `;
const convInsertRes = await pgClient.query(insertConversationQuery, [
provider.accountId,
inbox.id,
contact.id,
contactInboxId,
]);
conversation = convInsertRes.rows[0];
this.logger.verbose(`Nova conversa inserida: ${JSON.stringify(conversation)}`);
}
} catch (error) {
this.logger.error(`Erro ao recuperar/inserir conversa para o contato ${contact.id}: ${error}`);
throw error;
}
SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id // --- Etapa 3: Mapeia o resultado para o Map ---
FROM new_conversation const fks: FksChatwoot = {
JOIN new_contact ON new_conversation.contact_id = new_contact.id phone_number: normalizedWith,
contact_id: contact.id,
conversation_id: conversation.conversation_id || conversation.id
};
resultMap.set(normalizedWith, fks);
this.logger.verbose(`Resultado mapeado para ${normalizedWith}: ${JSON.stringify(fks)}`);
UNION } // fim for
} catch (error) {
SELECT p.phone_number, c.id contact_id, con.id conversation_id this.logger.error(`Erro geral no processamento: ${error}`);
FROM phone_number p throw error; // Propaga o erro para que o método pare
JOIN contacts c ON c.phone_number = p.phone_number }
JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2 return resultMap;
JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1
AND con.inbox_id = $2 AND con.contact_id = c.id`;
const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues);
return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item]));
} }
public async getChatwootUser(provider: ChatwootModel): Promise<ChatwootUser> { public async getChatwootUser(provider: ChatwootModel): Promise<ChatwootUser> {
try { try {
const pgClient = postgresClient.getChatwootConnection(); const pgClient = postgresClient.getChatwootConnection();
@ -503,16 +650,14 @@ class ChatwootImport {
switch (typeKey) { switch (typeKey) {
case 'documentMessage': case 'documentMessage':
return `_<File: ${msg.message.documentMessage.fileName}${ return `_<File: ${msg.message.documentMessage.fileName}${msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : '' }>_`;
}>_`;
case 'documentWithCaptionMessage': case 'documentWithCaptionMessage':
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${ return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${msg.message.documentWithCaptionMessage.message.documentMessage.caption
msg.message.documentWithCaptionMessage.message.documentMessage.caption
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}` ? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
: '' : ''
}>_`; }>_`;
case 'templateMessage': case 'templateMessage':
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText return msg.message.templateMessage.hydratedTemplate.hydratedTitleText