mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-24 09:28:39 -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 && \
|
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
4
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user