mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-22 20:12:02 -06:00
Ajustes da sincronizacao
This commit is contained in:
parent
427c994993
commit
a6304d3eec
11
BuildImage.ps1
Normal file
11
BuildImage.ps1
Normal 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
|
@ -3,7 +3,7 @@ FROM node:20-alpine AS builder
|
||||
RUN apk update && \
|
||||
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 contact="contato@atendai.com"
|
||||
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.3.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.3.3",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "evolution-api",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.3.3",
|
||||
"description": "Rest api for communication with WhatsApp",
|
||||
"main": "./dist/main.js",
|
||||
"type": "commonjs",
|
||||
|
@ -146,6 +146,12 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { useVoiceCallsBaileys } from './voiceCalls/useVoiceCallsBaileys';
|
||||
|
||||
|
||||
type DownloadMediaMessageContext = {
|
||||
reuploadRequest: (msg: WAMessage) => Promise<WAMessage>;
|
||||
logger: P.Logger;
|
||||
};
|
||||
|
||||
const groupMetadataCache = new CacheService(new CacheEngine(configService, 'groups').getEngine());
|
||||
|
||||
// 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 {
|
||||
const m = data?.message;
|
||||
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) {
|
||||
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) {
|
||||
if (msg.message[subtype]) {
|
||||
msg.message = msg.message[subtype].message;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Identifica o tipo de mídia contido na mensagem
|
||||
let mediaMessage: any;
|
||||
let mediaType: string;
|
||||
|
||||
let mediaType = '';
|
||||
for (const type of TypeMediaMessage) {
|
||||
mediaMessage = msg.message[type];
|
||||
if (mediaMessage) {
|
||||
if (msg.message[type]) {
|
||||
mediaMessage = msg.message[type];
|
||||
mediaType = type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const buffer = await downloadMediaMessage(
|
||||
{ key: msg?.key, message: msg?.message },
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: P({ level: 'error' }) as any,
|
||||
reuploadRequest: this.client.updateMediaMessage,
|
||||
|
||||
// Define um contexto completo conforme DownloadMediaMessageContext
|
||||
const downloadContext: DownloadMediaMessageContext = {
|
||||
logger: P({ level: 'error' }),
|
||||
reuploadRequest: async (message: WAMessage): Promise<WAMessage> => {
|
||||
// Aqui chamamos explicitamente o método que atualiza a mídia;
|
||||
// Se o método updateMediaMessage não retornar nada (void), retornamos a própria mensagem.
|
||||
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 ext = mimeTypes.extension(mediaMessage?.['mimetype']);
|
||||
const fileName = mediaMessage?.['fileName'] || `${msg.key.id}.${ext}` || `${v4()}.${ext}`;
|
||||
|
||||
const ext = mimeTypes.extension(mediaMessage?.mimetype);
|
||||
const fileName =
|
||||
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') {
|
||||
try {
|
||||
const convert = await this.processAudioMp4(buffer.toString('base64'));
|
||||
|
||||
if (Buffer.isBuffer(convert)) {
|
||||
const result = {
|
||||
const converted = await this.processAudioMp4(buffer.toString('base64'));
|
||||
if (Buffer.isBuffer(converted)) {
|
||||
return {
|
||||
mediaType,
|
||||
fileName,
|
||||
caption: mediaMessage['caption'],
|
||||
caption: mediaMessage.caption,
|
||||
size: {
|
||||
fileLength: mediaMessage['fileLength'],
|
||||
height: mediaMessage['height'],
|
||||
width: mediaMessage['width'],
|
||||
fileLength: mediaMessage.fileLength,
|
||||
height: mediaMessage.height,
|
||||
width: mediaMessage.width,
|
||||
},
|
||||
mimetype: 'audio/mp4',
|
||||
base64: convert.toString('base64'),
|
||||
buffer: getBuffer ? convert : null,
|
||||
base64: converted.toString('base64'),
|
||||
buffer: getBuffer ? converted : null,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (convertError) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Retorna os dados da mídia
|
||||
return {
|
||||
mediaType,
|
||||
fileName,
|
||||
caption: mediaMessage['caption'],
|
||||
caption: mediaMessage.caption,
|
||||
size: {
|
||||
fileLength: mediaMessage['fileLength'],
|
||||
height: mediaMessage['height'],
|
||||
width: mediaMessage['width'],
|
||||
fileLength: mediaMessage.fileLength,
|
||||
height: mediaMessage.height,
|
||||
width: mediaMessage.width,
|
||||
},
|
||||
mimetype: mediaMessage['mimetype'],
|
||||
mimetype: mediaMessage.mimetype,
|
||||
base64: buffer.toString('base64'),
|
||||
buffer: getBuffer ? buffer : null,
|
||||
};
|
||||
@ -3699,6 +3756,19 @@ export class BaileysStartupService extends ChannelStartupService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async fetchPrivacySettings() {
|
||||
const privacy = await this.client.fetchPrivacySettings();
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -199,6 +199,10 @@ class ChatwootImport {
|
||||
provider: ChatwootModel,
|
||||
) {
|
||||
try {
|
||||
this.logger.info(
|
||||
`[importHistoryMessages] Iniciando importação de mensagens para a instância "${instance.instanceName}".`
|
||||
);
|
||||
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
|
||||
const chatwootUser = await this.getChatwootUser(provider);
|
||||
@ -209,28 +213,32 @@ class ChatwootImport {
|
||||
let totalMessagesImported = 0;
|
||||
|
||||
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) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ordering messages by number and timestamp asc
|
||||
// Ordenando as mensagens por remoteJid e timestamp (ascendente)
|
||||
messagesOrdered.sort((a, b) => {
|
||||
const aKey = a.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
|
||||
const bKey = b.key as {
|
||||
remoteJid: string;
|
||||
};
|
||||
const aKey = a.key as { remoteJid: string };
|
||||
const bKey = b.key as { remoteJid: string };
|
||||
|
||||
const aMessageTimestamp = a.messageTimestamp as any as number;
|
||||
const bMessageTimestamp = b.messageTimestamp as any as number;
|
||||
|
||||
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);
|
||||
// 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>();
|
||||
allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => {
|
||||
phoneNumbersWithTimestamp.set(phoneNumber, {
|
||||
@ -238,15 +246,37 @@ class ChatwootImport {
|
||||
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));
|
||||
// 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;
|
||||
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||
let batchNumber = 1;
|
||||
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);
|
||||
this.logger.info(
|
||||
`[importHistoryMessages] Batch ${batchNumber}: ${messagesByPhoneNumber.size} números únicos encontrados.`
|
||||
);
|
||||
|
||||
if (messagesByPhoneNumber.size > 0) {
|
||||
const fksByNumber = await this.selectOrCreateFksFromChatwoot(
|
||||
@ -255,8 +285,11 @@ class ChatwootImport {
|
||||
phoneNumbersWithTimestamp,
|
||||
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
|
||||
(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 `;
|
||||
@ -264,16 +297,16 @@ class ChatwootImport {
|
||||
|
||||
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
|
||||
const fksChatwoot = fksByNumber.get(phoneNumber);
|
||||
|
||||
this.logger.info(
|
||||
`[importHistoryMessages] Número ${phoneNumber}: processando ${messages.length} mensagens.`
|
||||
);
|
||||
messages.forEach((message) => {
|
||||
if (!message.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentMessage = this.getContentMessage(chatwootService, message);
|
||||
if (!contentMessage) {
|
||||
return;
|
||||
@ -308,123 +341,237 @@ class ChatwootImport {
|
||||
if (sqlInsertMsg.slice(-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);
|
||||
}
|
||||
|
||||
this.deleteHistoryMessages(instance);
|
||||
this.deleteRepositoryMessagesCache(instance);
|
||||
this.logger.info(
|
||||
`[importHistoryMessages] Histórico e cache de mensagens da instância "${instance.instanceName}" foram limpos.`
|
||||
);
|
||||
|
||||
const providerData: ChatwootDto = {
|
||||
...provider,
|
||||
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.logger.info(
|
||||
`[importHistoryMessages] Concluída a importação de mensagens para a instância "${instance.instanceName}". Total importado: ${totalMessagesImported}.`
|
||||
);
|
||||
return totalMessagesImported;
|
||||
} catch (error) {
|
||||
this.logger.error(`Error on import history messages: ${error.toString()}`);
|
||||
|
||||
this.deleteHistoryMessages(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(
|
||||
provider: ChatwootModel,
|
||||
inbox: inbox,
|
||||
phoneNumbersWithTimestamp: Map<string, firstLastTimestamp>,
|
||||
messagesByPhoneNumber: Map<string, Message[]>,
|
||||
messagesByPhoneNumber: Map<string, Message[]>
|
||||
): Promise<Map<string, FksChatwoot>> {
|
||||
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];
|
||||
const phoneNumberBind = Array.from(messagesByPhoneNumber.keys())
|
||||
.map((phoneNumber) => {
|
||||
const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber);
|
||||
|
||||
if (phoneNumberTimestamp) {
|
||||
bindValues.push(phoneNumber);
|
||||
let bindStr = `($${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.first);
|
||||
bindStr += `$${bindValues.length},`;
|
||||
|
||||
bindValues.push(phoneNumberTimestamp.last);
|
||||
return `${bindStr}$${bindValues.length})`;
|
||||
// Obtém as duas versões normalizadas do número (com e sem nono dígito)
|
||||
const [normalizedWith, normalizedWithout] = this.normalizeBrazilianPhoneNumberOptions(rawPhoneNumber);
|
||||
const phoneTimestamp = phoneNumbersWithTimestamp.get(rawPhoneNumber);
|
||||
if (!phoneTimestamp) {
|
||||
this.logger.warn(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
|
||||
// Se preferir interromper, lance um erro:
|
||||
throw new Error(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
|
||||
}
|
||||
})
|
||||
.join(',');
|
||||
|
||||
// select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db
|
||||
const sqlFromChatwoot = `WITH
|
||||
phone_number AS (
|
||||
SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM (
|
||||
VALUES
|
||||
${phoneNumberBind}
|
||||
) as t (phone_number, created_at, last_activity_at)
|
||||
),
|
||||
// --- Etapa 1: Buscar ou Inserir o Contato ---
|
||||
let contact;
|
||||
try {
|
||||
this.logger.verbose(`Buscando contato para: ${normalizedWith} OU ${normalizedWithout}`);
|
||||
const selectContactQuery = `
|
||||
SELECT id, phone_number
|
||||
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 (
|
||||
SELECT * FROM phone_number
|
||||
WHERE phone_number NOT IN (
|
||||
SELECT phone_number
|
||||
FROM contacts
|
||||
JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2
|
||||
JOIN conversations con ON con.contact_inbox_id = ci.id
|
||||
AND con.account_id = $1
|
||||
AND con.inbox_id = $2
|
||||
AND con.contact_id = contacts.id
|
||||
WHERE contacts.account_id = $1
|
||||
)
|
||||
),
|
||||
|
||||
new_contact AS (
|
||||
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
|
||||
SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''),
|
||||
'@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at)
|
||||
FROM only_new_phone_number AS p
|
||||
ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
|
||||
RETURNING id, phone_number, created_at, updated_at
|
||||
),
|
||||
|
||||
new_contact_inbox AS (
|
||||
// --- Etapa 2: Buscar ou Inserir a Conversa (e o Contact_inboxes) ---
|
||||
let conversation;
|
||||
try {
|
||||
this.logger.verbose(`Buscando conversa para o contato (ID: ${contact.id}) na caixa ${inbox.id}`);
|
||||
const selectConversationQuery = `
|
||||
SELECT con.id AS conversation_id, con.contact_id
|
||||
FROM conversations con
|
||||
JOIN contact_inboxes ci ON ci.contact_id = con.contact_id AND ci.inbox_id = $2
|
||||
WHERE con.account_id = $1 AND con.inbox_id = $2 AND con.contact_id = $3
|
||||
LIMIT 1
|
||||
`;
|
||||
const convRes = await pgClient.query(selectConversationQuery, [provider.accountId, inbox.id, contact.id]);
|
||||
if (convRes.rowCount > 0) {
|
||||
conversation = convRes.rows[0];
|
||||
this.logger.verbose(`Conversa encontrada: ${JSON.stringify(conversation)}`);
|
||||
} else {
|
||||
this.logger.verbose(`Nenhuma conversa encontrada para o contato ${contact.id}. Verificando contact_inboxes.`);
|
||||
let contactInboxId: number;
|
||||
const selectContactInboxQuery = `
|
||||
SELECT id
|
||||
FROM contact_inboxes
|
||||
WHERE contact_id = $1 AND inbox_id = $2
|
||||
LIMIT 1
|
||||
`;
|
||||
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)
|
||||
SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at
|
||||
FROM new_contact
|
||||
RETURNING id, contact_id, created_at, updated_at
|
||||
),
|
||||
VALUES ($1, $2, gen_random_uuid(), NOW(), NOW())
|
||||
RETURNING id
|
||||
`;
|
||||
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 (
|
||||
INSERT INTO conversations (account_id, inbox_id, status, contact_id,
|
||||
contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
||||
SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(),
|
||||
new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at
|
||||
FROM new_contact_inbox
|
||||
RETURNING id, contact_id
|
||||
)
|
||||
this.logger.verbose(`Inserindo conversa para o contato ${contact.id} com contact_inbox ${contactInboxId}`);
|
||||
const insertConversationQuery = `
|
||||
INSERT INTO conversations
|
||||
(account_id, inbox_id, status, contact_id, contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
||||
VALUES
|
||||
($1, $2, 0, $3, $4, gen_random_uuid(), NOW(), NOW(), NOW())
|
||||
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
|
||||
FROM new_conversation
|
||||
JOIN new_contact ON new_conversation.contact_id = new_contact.id
|
||||
// --- Etapa 3: Mapeia o resultado para o Map ---
|
||||
const fks: FksChatwoot = {
|
||||
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
|
||||
|
||||
SELECT p.phone_number, c.id contact_id, con.id conversation_id
|
||||
FROM phone_number p
|
||||
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
|
||||
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]));
|
||||
} // fim for
|
||||
} catch (error) {
|
||||
this.logger.error(`Erro geral no processamento: ${error}`);
|
||||
throw error; // Propaga o erro para que o método pare
|
||||
}
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public async getChatwootUser(provider: ChatwootModel): Promise<ChatwootUser> {
|
||||
try {
|
||||
const pgClient = postgresClient.getChatwootConnection();
|
||||
@ -503,16 +650,14 @@ class ChatwootImport {
|
||||
|
||||
switch (typeKey) {
|
||||
case 'documentMessage':
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${
|
||||
msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
return `_<File: ${msg.message.documentMessage.fileName}${msg.message.documentMessage.caption ? ` ${msg.message.documentMessage.caption}` : ''
|
||||
}>_`;
|
||||
|
||||
case 'documentWithCaptionMessage':
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${
|
||||
msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
return `_<File: ${msg.message.documentWithCaptionMessage.message.documentMessage.fileName}${msg.message.documentWithCaptionMessage.message.documentMessage.caption
|
||||
? ` ${msg.message.documentWithCaptionMessage.message.documentMessage.caption}`
|
||||
: ''
|
||||
}>_`;
|
||||
}>_`;
|
||||
|
||||
case 'templateMessage':
|
||||
return msg.message.templateMessage.hydratedTemplate.hydratedTitleText
|
||||
|
Loading…
Reference in New Issue
Block a user