mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-25 18:08:40 -06:00
Ajustes da integracao com chatwoot e correcoes de erros na integracao com o s3
This commit is contained in:
parent
a6304d3eec
commit
a657b92c54
@ -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.3" description="Api to control whatsapp features through http requests."
|
LABEL version="2.2.3.17" 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"
|
||||||
|
|
||||||
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.2.3.3",
|
"version": "2.2.3.17",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.2.3.3",
|
"version": "2.2.3.17",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adiwajshing/keyed-db": "^0.2.4",
|
"@adiwajshing/keyed-db": "^0.2.4",
|
||||||
@ -55,6 +55,7 @@
|
|||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"tsup": "^8.3.5"
|
"tsup": "^8.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -10848,7 +10849,6 @@
|
|||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -10857,7 +10857,6 @@
|
|||||||
"version": "0.5.21",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.0"
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "evolution-api",
|
"name": "evolution-api",
|
||||||
"version": "2.2.3.3",
|
"version": "2.2.3.17",
|
||||||
"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",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit && tsup",
|
"build": "tsc --noEmit && tsup",
|
||||||
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
|
"start": "tsnd -r tsconfig-paths/register --files --transpile-only ./src/main.ts",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node --enable-source-maps -r source-map-support/register dist/main.js",
|
||||||
"dev:server": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
|
"dev:server": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./src/main.ts",
|
||||||
"test": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
|
"test": "tsnd -r tsconfig-paths/register --files --transpile-only --respawn --ignore-watch node_modules ./test/all.test.ts",
|
||||||
"lint": "eslint --fix --ext .ts src",
|
"lint": "eslint --fix --ext .ts src",
|
||||||
@ -95,6 +95,7 @@
|
|||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"tsup": "^8.3.5"
|
"tsup": "^8.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -295,53 +295,85 @@
|
|||||||
avatar_url?: string,
|
avatar_url?: string,
|
||||||
jid?: string,
|
jid?: string,
|
||||||
) {
|
) {
|
||||||
const client = await this.clientCw(instance);
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][createContact] start instance=${instance.instanceName} phone=${phoneNumber}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1) obter cliente
|
||||||
|
const client = await this.clientCw(instance);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
this.logger.warn('client not found');
|
this.logger.warn(
|
||||||
|
`[ChatwootService][createContact] client not found for instance=${instance.instanceName}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
this.logger.verbose(`[ChatwootService][createContact] client obtained`);
|
||||||
|
|
||||||
let data: any = {};
|
// 2) montar payload
|
||||||
|
const data: any = { inbox_id: inboxId, name: name || phoneNumber, avatar_url };
|
||||||
if (!isGroup) {
|
if (!isGroup) {
|
||||||
data = {
|
data.identifier = jid;
|
||||||
inbox_id: inboxId,
|
data.phone_number = `+${phoneNumber}`;
|
||||||
name: name || phoneNumber,
|
|
||||||
identifier: jid,
|
|
||||||
avatar_url: avatar_url,
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((jid && jid.includes('@')) || !jid) {
|
|
||||||
data['phone_number'] = `+${phoneNumber}`;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
data = {
|
data.identifier = phoneNumber;
|
||||||
inbox_id: inboxId,
|
|
||||||
name: name || phoneNumber,
|
|
||||||
identifier: phoneNumber,
|
|
||||||
avatar_url: avatar_url,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][createContact] payload=${JSON.stringify(data)}`
|
||||||
|
);
|
||||||
|
|
||||||
const contact = await client.contacts.create({
|
// 3) criar no Chatwoot
|
||||||
|
let rawResponse: any;
|
||||||
|
try {
|
||||||
|
rawResponse = await client.contacts.create({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][createContact] raw create response=${JSON.stringify(rawResponse)}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`[ChatwootService][createContact] error creating contact: ${err}`
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
if (!contact) {
|
// 4) extrair o contactId dos dois possíveis formatos
|
||||||
this.logger.warn('contact not found');
|
// - legacy: { id: number, ... }
|
||||||
|
// - nova versão: { payload: { contact: { id: number, ... } } }
|
||||||
|
const maybePayload = rawResponse.payload?.contact;
|
||||||
|
const contactObj = maybePayload ?? rawResponse;
|
||||||
|
const contactId = contactObj.id as number | undefined;
|
||||||
|
|
||||||
|
if (!contactId) {
|
||||||
|
this.logger.error(
|
||||||
|
`[ChatwootService][createContact] no id found in response; raw=${JSON.stringify(rawResponse)}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][createContact] created contact id=${contactId}`
|
||||||
|
);
|
||||||
|
|
||||||
const findContact = await this.findContact(instance, phoneNumber);
|
// 5) adicionar label
|
||||||
|
try {
|
||||||
const contactId = findContact?.id;
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][createContact] adding label=${this.provider.nameInbox} to contactId=${contactId}`
|
||||||
|
);
|
||||||
await this.addLabelToContact(this.provider.nameInbox, contactId);
|
await this.addLabelToContact(this.provider.nameInbox, contactId);
|
||||||
|
} catch (err) {
|
||||||
return contact;
|
this.logger.error(
|
||||||
|
`[ChatwootService][createContact] error addLabelToContact: ${err}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6) retornar objeto com .id para ser usado pelo createConversation
|
||||||
|
return { id: contactId, ...contactObj };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async updateContact(instance: InstanceDto, id: number, data: any) {
|
public async updateContact(instance: InstanceDto, id: number, data: any) {
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
@ -406,51 +438,78 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async findContact(instance: InstanceDto, phoneNumber: string) {
|
public async findContact(instance: InstanceDto, phoneNumber: string) {
|
||||||
const client = await this.clientCw(instance);
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][findContact] start for instance=${instance.instanceName}, phoneNumber=${phoneNumber}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const client = await this.clientCw(instance);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
this.logger.warn('client not found');
|
this.logger.warn(
|
||||||
|
`[ChatwootService][findContact] client not found for instance=${instance.instanceName}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let query: any;
|
|
||||||
const isGroup = phoneNumber.includes('@g.us');
|
const isGroup = phoneNumber.includes('@g.us');
|
||||||
|
const query = isGroup ? phoneNumber : `+${phoneNumber}`;
|
||||||
|
|
||||||
if (!isGroup) {
|
this.logger.verbose(
|
||||||
query = `+${phoneNumber}`;
|
`[ChatwootService][findContact] isGroup=${isGroup}, query=${query}`
|
||||||
} else {
|
);
|
||||||
query = phoneNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contact: any;
|
|
||||||
|
|
||||||
|
let response: any;
|
||||||
|
try {
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
contact = await client.contacts.search({
|
response = await client.contacts.search({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
q: query,
|
q: query,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
contact = await chatwootRequest(this.getClientCwConfig(), {
|
response = await chatwootRequest(this.getClientCwConfig(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: `/api/v1/accounts/${this.provider.accountId}/contacts/filter`,
|
url: `/api/v1/accounts/${this.provider.accountId}/contacts/filter`,
|
||||||
body: {
|
body: { payload: this.getFilterPayload(query) },
|
||||||
payload: this.getFilterPayload(query),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.logger.verbose(
|
||||||
if (!contact && contact?.payload?.length === 0) {
|
`[ChatwootService][findContact] raw response: ${JSON.stringify(response)}`
|
||||||
this.logger.warn('contact not found');
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`[ChatwootService][findContact] error during API call: ${error.message}`
|
||||||
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isGroup) {
|
const payload = response.payload || [];
|
||||||
return contact.payload.length > 1 ? this.findContactInContactList(contact.payload, query) : contact.payload[0];
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][findContact] payload length: ${payload.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (payload.length === 0) {
|
||||||
|
this.logger.warn(
|
||||||
|
`[ChatwootService][findContact] contact not found for query=${query}`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let found: any;
|
||||||
|
if (isGroup) {
|
||||||
|
found = payload.find((c: any) => c.identifier === query);
|
||||||
} else {
|
} else {
|
||||||
return contact.payload.find((contact) => contact.identifier === query);
|
found = payload.length > 1
|
||||||
|
? this.findContactInContactList(payload, query)
|
||||||
|
: payload[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.verbose(
|
||||||
|
`[ChatwootService][findContact] returning contact: ${JSON.stringify(found)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async mergeBrazilianContacts(contacts: any[]) {
|
private async mergeBrazilianContacts(contacts: any[]) {
|
||||||
try {
|
try {
|
||||||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||||||
@ -542,227 +601,215 @@
|
|||||||
return filterPayload;
|
return filterPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createConversation(instance: InstanceDto, body: any) {
|
|
||||||
try {
|
|
||||||
this.logger.verbose('--- Start createConversation ---');
|
|
||||||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
|
||||||
|
|
||||||
|
|
||||||
|
private pendingCreateConv = new Map<string, Promise<number>>();
|
||||||
|
|
||||||
|
public async createConversation(instance: InstanceDto, body: any): Promise<number> {
|
||||||
|
const remoteJid = body.key.remoteJid as string;
|
||||||
|
this.logger.verbose("[createConversation] Iniciando para remoteJid=" + remoteJid);
|
||||||
|
|
||||||
|
// 0) Se já está criando, reutiliza a promise
|
||||||
|
if (this.pendingCreateConv.has(remoteJid)) {
|
||||||
|
this.logger.verbose("[createConversation] Ja em criacao para " + remoteJid + ", retornando promise existente");
|
||||||
|
return this.pendingCreateConv.get(remoteJid)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
let triedRecovery = false;
|
||||||
|
const cacheKey = instance.instanceName + ":createConversation-" + remoteJid;
|
||||||
|
|
||||||
|
const p = (async (): Promise<number> => {
|
||||||
|
try {
|
||||||
|
this.logger.verbose("[createConversation] Chamando _createConversation pela primeira vez");
|
||||||
|
return await this._createConversation(instance, body);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error("[createConversation] Erro na primeira tentativa: " + err);
|
||||||
|
if (!triedRecovery) {
|
||||||
|
triedRecovery = true;
|
||||||
|
this.logger.warn("[createConversation] Tentando recuperacao: limpando cache e recriando conversa");
|
||||||
|
await this.cache.delete(cacheKey);
|
||||||
|
this.logger.verbose("[createConversation] Cache deletado para chave=" + cacheKey);
|
||||||
|
return await this._createConversation(instance, body);
|
||||||
|
}
|
||||||
|
this.logger.error("[createConversation] Ja tentei recuperacao, repassando erro");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.pendingCreateConv.set(remoteJid, p);
|
||||||
|
try {
|
||||||
|
const convId = await p;
|
||||||
|
this.logger.verbose("[createConversation] Concluido para " + remoteJid + ", convId=" + convId);
|
||||||
|
return convId;
|
||||||
|
} finally {
|
||||||
|
this.pendingCreateConv.delete(remoteJid);
|
||||||
|
this.logger.verbose("[createConversation] Removido pendingCreateConv para " + remoteJid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _createConversation(instance: InstanceDto, body: any): Promise<number> {
|
||||||
|
const remoteJid = body.key.remoteJid as string;
|
||||||
|
const cacheKey = instance.instanceName + ":createConversation-" + remoteJid;
|
||||||
|
this.logger.verbose("[_createConversation] Start para remoteJid=" + remoteJid);
|
||||||
|
|
||||||
|
// 1) Cliente Chatwoot
|
||||||
const client = await this.clientCw(instance);
|
const client = await this.clientCw(instance);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
this.logger.warn(`Client not found for instance: ${JSON.stringify(instance)}`);
|
this.logger.error("[_createConversation] Client Chatwoot nao encontrado para " + instance.instanceName);
|
||||||
return null;
|
throw new Error("Client not found for instance: " + instance.instanceName);
|
||||||
|
}
|
||||||
|
this.logger.verbose("[_createConversation] Client Chatwoot obtido");
|
||||||
|
|
||||||
|
// 2) Cache
|
||||||
|
const hasCache = await this.cache.has(cacheKey);
|
||||||
|
this.logger.verbose("[_createConversation] Cache check para key=" + cacheKey + ": " + hasCache);
|
||||||
|
if (hasCache) {
|
||||||
|
const cachedId = (await this.cache.get(cacheKey)) as number;
|
||||||
|
this.logger.verbose("[_createConversation] Usando ID em cache=" + cachedId);
|
||||||
|
return cachedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `${instance.instanceName}:createConversation-${body.key.remoteJid}`;
|
// 3) Inbox
|
||||||
this.logger.verbose(`Cache key: ${cacheKey}`);
|
|
||||||
|
|
||||||
if (await this.cache.has(cacheKey)) {
|
|
||||||
this.logger.verbose(`Cache hit for key: ${cacheKey}`);
|
|
||||||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
|
||||||
this.logger.verbose(`Cached conversation ID: ${conversationId}`);
|
|
||||||
let conversationExists: conversation | boolean;
|
|
||||||
try {
|
|
||||||
conversationExists = await client.conversations.get({
|
|
||||||
accountId: this.provider.accountId,
|
|
||||||
conversationId: conversationId,
|
|
||||||
});
|
|
||||||
this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error getting conversation: ${error}`);
|
|
||||||
conversationExists = false;
|
|
||||||
}
|
|
||||||
if (!conversationExists) {
|
|
||||||
this.logger.verbose('Conversation does not exist, re-calling createConversation');
|
|
||||||
this.cache.delete(cacheKey);
|
|
||||||
return await this.createConversation(instance, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGroup = body.key.remoteJid.includes('@g.us');
|
|
||||||
this.logger.verbose(`Is group: ${isGroup}`);
|
|
||||||
|
|
||||||
const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0];
|
|
||||||
this.logger.verbose(`Chat ID: ${chatId}`);
|
|
||||||
|
|
||||||
let nameContact: string;
|
|
||||||
|
|
||||||
nameContact = !body.key.fromMe ? body.pushName : chatId;
|
|
||||||
this.logger.verbose(`Name contact: ${nameContact}`);
|
|
||||||
|
|
||||||
const filterInbox = await this.getInbox(instance);
|
const filterInbox = await this.getInbox(instance);
|
||||||
|
|
||||||
if (!filterInbox) {
|
if (!filterInbox) {
|
||||||
this.logger.warn(`Inbox not found for instance: ${JSON.stringify(instance)}`);
|
this.logger.error("[_createConversation] Inbox nao encontrada para " + instance.instanceName);
|
||||||
return null;
|
throw new Error("Inbox not found for instance: " + instance.instanceName);
|
||||||
}
|
}
|
||||||
|
this.logger.verbose("[_createConversation] Inbox encontrada: id=" + filterInbox.id);
|
||||||
|
|
||||||
if (isGroup) {
|
// 4) Contato
|
||||||
this.logger.verbose('Processing group conversation');
|
const isGroup = remoteJid.includes("@g.us");
|
||||||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
const chatId = isGroup ? remoteJid : remoteJid.split("@")[0];
|
||||||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
this.logger.verbose("[_createConversation] isGroup=" + isGroup + ", chatId=" + chatId);
|
||||||
|
|
||||||
nameContact = `${group.subject} (GROUP)`;
|
|
||||||
|
|
||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(
|
|
||||||
body.key.participant.split('@')[0],
|
|
||||||
);
|
|
||||||
this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`);
|
|
||||||
|
|
||||||
const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]);
|
|
||||||
this.logger.verbose(`Found participant: ${JSON.stringify(findParticipant)}`);
|
|
||||||
|
|
||||||
if (findParticipant) {
|
|
||||||
if (!findParticipant.name || findParticipant.name === chatId) {
|
|
||||||
await this.updateContact(instance, findParticipant.id, {
|
|
||||||
name: body.pushName,
|
|
||||||
avatar_url: picture_url.profilePictureUrl || null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.createContact(
|
|
||||||
instance,
|
|
||||||
body.key.participant.split('@')[0],
|
|
||||||
filterInbox.id,
|
|
||||||
false,
|
|
||||||
body.pushName,
|
|
||||||
picture_url.profilePictureUrl || null,
|
|
||||||
body.key.participant,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId);
|
|
||||||
this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`);
|
|
||||||
|
|
||||||
let contact = await this.findContact(instance, chatId);
|
let contact = await this.findContact(instance, chatId);
|
||||||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
|
||||||
|
|
||||||
if (contact) {
|
if (contact) {
|
||||||
if (!body.key.fromMe) {
|
this.logger.verbose("[_createConversation] Contato encontrado: id=" + contact.id);
|
||||||
const waProfilePictureFile =
|
|
||||||
picture_url?.profilePictureUrl?.split('#')[0].split('?')[0].split('/').pop() || '';
|
|
||||||
const chatwootProfilePictureFile = contact?.thumbnail?.split('#')[0].split('?')[0].split('/').pop() || '';
|
|
||||||
const pictureNeedsUpdate = waProfilePictureFile !== chatwootProfilePictureFile;
|
|
||||||
const nameNeedsUpdate =
|
|
||||||
!contact.name ||
|
|
||||||
contact.name === chatId ||
|
|
||||||
(`+${chatId}`.startsWith('+55')
|
|
||||||
? this.getNumbers(`+${chatId}`).some(
|
|
||||||
(v) => contact.name === v || contact.name === v.substring(3) || contact.name === v.substring(1),
|
|
||||||
)
|
|
||||||
: false);
|
|
||||||
|
|
||||||
this.logger.verbose(`Picture needs update: ${pictureNeedsUpdate}`);
|
|
||||||
this.logger.verbose(`Name needs update: ${nameNeedsUpdate}`);
|
|
||||||
|
|
||||||
if (pictureNeedsUpdate || nameNeedsUpdate) {
|
|
||||||
contact = await this.updateContact(instance, contact.id, {
|
|
||||||
...(nameNeedsUpdate && { name: nameContact }),
|
|
||||||
...(waProfilePictureFile === '' && { avatar: null }),
|
|
||||||
...(pictureNeedsUpdate && { avatar_url: picture_url?.profilePictureUrl }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const jid = body.key.remoteJid;
|
this.logger.verbose("[_createConversation] Contato nao existe, criando...");
|
||||||
|
const name = isGroup
|
||||||
|
? (await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId)).subject + " (GROUP)"
|
||||||
|
: body.pushName || chatId;
|
||||||
|
const pictureUrl = (await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId)).profilePictureUrl;
|
||||||
contact = await this.createContact(
|
contact = await this.createContact(
|
||||||
instance,
|
instance,
|
||||||
chatId,
|
chatId,
|
||||||
filterInbox.id,
|
filterInbox.id,
|
||||||
isGroup,
|
isGroup,
|
||||||
nameContact,
|
name,
|
||||||
picture_url.profilePictureUrl || null,
|
pictureUrl || null,
|
||||||
jid,
|
isGroup ? remoteJid : undefined
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
this.logger.warn('Contact not created or found');
|
this.logger.error("[_createConversation] Falha ao criar contato para " + chatId);
|
||||||
return null;
|
throw new Error("Nao conseguiu criar contato para conversa");
|
||||||
}
|
}
|
||||||
|
this.logger.verbose("[_createConversation] Contato criado: id=" + contact.id);
|
||||||
|
}
|
||||||
|
const contactId = (contact.id ?? contact.payload?.contact?.id) as number;
|
||||||
|
|
||||||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
// 5) Listar conversas existentes
|
||||||
this.logger.verbose(`Contact ID: ${contactId}`);
|
this.logger.verbose("[_createConversation] Chamando listConversations para contactId=" + contactId);
|
||||||
|
const listResp: any = await client.contacts.listConversations({
|
||||||
const contactConversations = (await client.contacts.listConversations({
|
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
id: contactId,
|
id: contactId,
|
||||||
})) as any;
|
});
|
||||||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
this.logger.verbose("[_createConversation] listConversations raw: " + JSON.stringify(listResp));
|
||||||
|
|
||||||
if (!contactConversations || !contactConversations.payload) {
|
let conversations: any[] = [];
|
||||||
this.logger.error('No conversations found or payload is undefined');
|
if (Array.isArray(listResp)) conversations = listResp;
|
||||||
return null;
|
else if (Array.isArray(listResp.payload)) conversations = listResp.payload;
|
||||||
}
|
else if (Array.isArray(listResp.data?.payload)) conversations = listResp.data.payload;
|
||||||
|
else if (Array.isArray(listResp.data)) conversations = listResp.data;
|
||||||
|
this.logger.verbose("[_createConversation] Encontradas " + conversations.length + " conversas");
|
||||||
|
|
||||||
if (contactConversations.payload.length) {
|
// 6) Filtrar conversa aberta ou pendente
|
||||||
let conversation: any;
|
let conv = null;
|
||||||
if (this.provider.reopenConversation) {
|
if (this.provider.reopenConversation) {
|
||||||
conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id);
|
this.logger.verbose("[_createConversation] reopenConversation=true, buscando inbox_id=" + filterInbox.id);
|
||||||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(conversation)}`);
|
conv = conversations.find(c => c.inbox_id === filterInbox.id);
|
||||||
|
if (conv && this.provider.conversationPending && conv.status !== "pending") {
|
||||||
if (conversation && this.provider.conversationPending && conversation.status !== 'open') {
|
this.logger.verbose("[_createConversation] Reabrindo conversa " + conv.id + " para status=pending");
|
||||||
|
|
||||||
if (conversation) {
|
|
||||||
await client.conversations.toggleStatus({
|
await client.conversations.toggleStatus({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
conversationId: conversation.id,
|
conversationId: conv.id,
|
||||||
data: {
|
data: { status: "pending" },
|
||||||
status: 'pending',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (!conversation) {
|
|
||||||
this.logger.warn('Conversation not found, creating a new one');
|
|
||||||
this.cache.delete(cacheKey);
|
|
||||||
return await this.createConversation(instance, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
conversation = contactConversations.payload.find(
|
this.logger.verbose("[_createConversation] reopenConversation=false, buscando status!=resolved");
|
||||||
(conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
conv = conversations.find(c => c.status !== "resolved" && c.inbox_id === filterInbox.id);
|
||||||
);
|
|
||||||
this.logger.verbose(`Found conversation: ${JSON.stringify(conversation)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversation) {
|
if (conv) {
|
||||||
this.logger.verbose(`Returning existing conversation ID: ${conversation.id}`);
|
this.logger.verbose("[_createConversation] Usando conversa existente id=" + conv.id);
|
||||||
this.cache.set(cacheKey, conversation.id);
|
this.cache.set(cacheKey, conv.id, 5 * 60);
|
||||||
return conversation.id;
|
return conv.id;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
// 7) Criar nova conversa
|
||||||
|
this.logger.verbose("[_createConversation] Nenhuma conversa encontrada, criando nova...");
|
||||||
|
const payload: any = {
|
||||||
contact_id: contactId.toString(),
|
contact_id: contactId.toString(),
|
||||||
inbox_id: filterInbox.id.toString(),
|
inbox_id: filterInbox.id.toString(),
|
||||||
|
...(this.provider.conversationPending ? { status: "pending" } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.provider.conversationPending) {
|
try {
|
||||||
data['status'] = 'pending';
|
const newConv = await client.conversations.create({
|
||||||
}
|
|
||||||
|
|
||||||
const conversation = await client.conversations.create({
|
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
data,
|
data: payload,
|
||||||
});
|
});
|
||||||
|
if (!newConv?.id) {
|
||||||
|
this.logger.error("[_createConversation] create retornou sem ID");
|
||||||
|
throw new Error("Falha ao criar nova conversa: resposta sem ID");
|
||||||
|
}
|
||||||
|
this.logger.verbose("[_createConversation] Nova conversa criada id=" + newConv.id);
|
||||||
|
this.cache.set(cacheKey, newConv.id, 5 * 60);
|
||||||
|
return newConv.id;
|
||||||
|
|
||||||
if (!conversation) {
|
} catch (err: any) {
|
||||||
this.logger.warn('Conversation not created or found');
|
this.logger.error("[_createConversation] Erro ao criar conversa: " + err);
|
||||||
return null;
|
this.logger.warn("[_createConversation] Tentando recuperar conversa via listConversations novamente");
|
||||||
|
|
||||||
|
const retryList: any = await client.contacts.listConversations({
|
||||||
|
accountId: this.provider.accountId,
|
||||||
|
id: contactId,
|
||||||
|
});
|
||||||
|
this.logger.verbose("[_createConversation] retry listConversations raw: " + JSON.stringify(retryList));
|
||||||
|
|
||||||
|
let retryConvs: any[] = [];
|
||||||
|
if (Array.isArray(retryList)) retryConvs = retryList;
|
||||||
|
else if (Array.isArray(retryList.payload)) retryConvs = retryList.payload;
|
||||||
|
else if (Array.isArray(retryList.data?.payload)) retryConvs = retryList.data.payload;
|
||||||
|
else if (Array.isArray(retryList.data)) retryConvs = retryList.data;
|
||||||
|
this.logger.verbose("[_createConversation] retry encontrou " + retryConvs.length + " conversas");
|
||||||
|
|
||||||
|
const recovered = retryConvs.find(c => c.inbox_id === filterInbox.id);
|
||||||
|
if (recovered) {
|
||||||
|
this.logger.verbose("[_createConversation] Recuperou conversa existente id=" + recovered.id);
|
||||||
|
this.cache.set(cacheKey, recovered.id, 5 * 60);
|
||||||
|
return recovered.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`New conversation created with ID: ${conversation.id}`);
|
this.logger.error("[_createConversation] Nao recuperou conversa, repassando erro");
|
||||||
this.cache.set(cacheKey, conversation.id);
|
throw err;
|
||||||
return conversation.id;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error in createConversation: ${error}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async getInbox(instance: InstanceDto): Promise<inbox | null> {
|
public async getInbox(instance: InstanceDto): Promise<inbox | null> {
|
||||||
const cacheKey = `${instance.instanceName}:getInbox`;
|
const cacheKey = `${instance.instanceName}:getInbox`;
|
||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
@ -2216,8 +2263,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event === 'messages.edit') {
|
if (event === 'messages.edit') {
|
||||||
const editedText = `${
|
const editedText = `${body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||||||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
|
||||||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||||||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||||||
const key = message.key as {
|
const key = message.key as {
|
||||||
@ -2458,17 +2504,26 @@
|
|||||||
})
|
})
|
||||||
).reduce((acc: Map<string, ContactModel>, contact: ContactModel) => acc.set(contact.id, contact), new Map());
|
).reduce((acc: Map<string, ContactModel>, contact: ContactModel) => acc.set(contact.id, contact), new Map());
|
||||||
|
|
||||||
recentContacts.forEach(async (contact) => {
|
for (const c of recentContacts) {
|
||||||
if (contactsWithProfilePicture.has(contact.identifier)) {
|
const pic = contactsWithProfilePicture.get(c.identifier);
|
||||||
client.contacts.update({
|
if (!pic) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.contacts.update({
|
||||||
accountId: this.provider.accountId,
|
accountId: this.provider.accountId,
|
||||||
id: contact.id,
|
id: c.id,
|
||||||
data: {
|
data: {
|
||||||
avatar_url: contactsWithProfilePicture.get(contact.identifier).profilePictureUrl || null,
|
avatar_url: pic.profilePictureUrl || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.logger.verbose(`Avatar atualizado para o contato ${c.id}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`Falha ao atualizar avatar do contato ${c.id}: ${err}`);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`);
|
this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`);
|
||||||
}
|
}
|
||||||
|
@ -93,8 +93,8 @@ class ChatwootImport {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let contactsChunk: Contact[] = this.sliceIntoChunks(contacts, 3000);
|
const contactBatches = this.sliceIntoChunks(contacts, 3000);
|
||||||
while (contactsChunk.length > 0) {
|
for (const contactsChunk of contactBatches) {
|
||||||
const labelSql = `SELECT id FROM labels WHERE title = '${provider.nameInbox}' AND account_id = ${provider.accountId} LIMIT 1`;
|
const labelSql = `SELECT id FROM labels WHERE title = '${provider.nameInbox}' AND account_id = ${provider.accountId} LIMIT 1`;
|
||||||
|
|
||||||
let labelId = (await pgClient.query(labelSql))?.rows[0]?.id;
|
let labelId = (await pgClient.query(labelSql))?.rows[0]?.id;
|
||||||
@ -158,7 +158,6 @@ class ChatwootImport {
|
|||||||
|
|
||||||
await pgClient.query(sqlInsertLabel, [tagId, 'Contact', 'labels']);
|
await pgClient.query(sqlInsertLabel, [tagId, 'Contact', 'labels']);
|
||||||
|
|
||||||
contactsChunk = this.sliceIntoChunks(contacts, 3000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deleteHistoryContacts(instance);
|
this.deleteHistoryContacts(instance);
|
||||||
@ -265,9 +264,9 @@ class ChatwootImport {
|
|||||||
|
|
||||||
// Processamento das mensagens em batches
|
// Processamento das mensagens em batches
|
||||||
const batchSize = 4000;
|
const batchSize = 4000;
|
||||||
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
let messagesChunks = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||||
let batchNumber = 1;
|
let batchNumber = 1;
|
||||||
while (messagesChunk.length > 0) {
|
for (const messagesChunk of messagesChunks) {
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[importHistoryMessages] Processando batch ${batchNumber} com ${messagesChunk.length} mensagens.`
|
`[importHistoryMessages] Processando batch ${batchNumber} com ${messagesChunk.length} mensagens.`
|
||||||
);
|
);
|
||||||
@ -350,7 +349,7 @@ class ChatwootImport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
batchNumber++;
|
batchNumber++;
|
||||||
messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.deleteHistoryMessages(instance);
|
this.deleteHistoryMessages(instance);
|
||||||
@ -367,7 +366,7 @@ class ChatwootImport {
|
|||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[importHistoryMessages] Iniciando importação de contatos do histórico para a instância "${instance.instanceName}".`
|
`[importHistoryMessages] Iniciando importação de contatos do histórico para a instância "${instance.instanceName}".`
|
||||||
);
|
);
|
||||||
this.importHistoryContacts(instance, providerData);
|
await this.importHistoryContacts(instance, providerData);
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[importHistoryMessages] Concluída a importação de mensagens para a instância "${instance.instanceName}". Total importado: ${totalMessagesImported}.`
|
`[importHistoryMessages] Concluída a importação de mensagens para a instância "${instance.instanceName}". Total importado: ${totalMessagesImported}.`
|
||||||
@ -416,136 +415,154 @@ class ChatwootImport {
|
|||||||
): Promise<Map<string, FksChatwoot>> {
|
): Promise<Map<string, FksChatwoot>> {
|
||||||
const pgClient = postgresClient.getChatwootConnection();
|
const pgClient = postgresClient.getChatwootConnection();
|
||||||
const resultMap = new Map<string, FksChatwoot>();
|
const resultMap = new Map<string, FksChatwoot>();
|
||||||
try {
|
|
||||||
// Para cada telefone presente
|
|
||||||
for (const rawPhoneNumber of messagesByPhoneNumber.keys()) {
|
|
||||||
|
|
||||||
// Obtém as duas versões normalizadas do número (com e sem nono dígito)
|
for (const rawPhone of messagesByPhoneNumber.keys()) {
|
||||||
const [normalizedWith, normalizedWithout] = this.normalizeBrazilianPhoneNumberOptions(rawPhoneNumber);
|
// 1) Normalizar telefone e gerar JIDs
|
||||||
const phoneTimestamp = phoneNumbersWithTimestamp.get(rawPhoneNumber);
|
const [normalizedWith, normalizedWithout] =
|
||||||
if (!phoneTimestamp) {
|
this.normalizeBrazilianPhoneNumberOptions(rawPhone);
|
||||||
this.logger.warn(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
|
const jidWith = normalizedWith.replace(/^\+/, '') + '@s.whatsapp.net';
|
||||||
// Se preferir interromper, lance um erro:
|
const jidWithout = normalizedWithout.replace(/^\+/, '') + '@s.whatsapp.net';
|
||||||
throw new Error(`Timestamp não encontrado para o telefone ${rawPhoneNumber}`);
|
|
||||||
|
const ts = phoneNumbersWithTimestamp.get(rawPhone);
|
||||||
|
if (!ts) {
|
||||||
|
this.logger.warn(`Timestamp não encontrado para ${rawPhone}`);
|
||||||
|
throw new Error(`Timestamp não encontrado para ${rawPhone}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Etapa 1: Buscar ou Inserir o Contato ---
|
// 2) Buscar ou inserir Contact (agora incluindo identifier)
|
||||||
let contact;
|
let contact: { id: number; phone_number: string };
|
||||||
try {
|
{
|
||||||
this.logger.verbose(`Buscando contato para: ${normalizedWith} OU ${normalizedWithout}`);
|
const selectContact = `
|
||||||
const selectContactQuery = `
|
|
||||||
SELECT id, phone_number
|
SELECT id, phone_number
|
||||||
FROM contacts
|
FROM contacts
|
||||||
WHERE account_id = $1
|
WHERE account_id = $1
|
||||||
AND (phone_number = $2 OR phone_number = $3)
|
AND (
|
||||||
|
phone_number = $2
|
||||||
|
OR phone_number = $3
|
||||||
|
OR identifier = $4
|
||||||
|
OR identifier = $5
|
||||||
|
)
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
const contactRes = await pgClient.query(selectContactQuery, [
|
const res = await pgClient.query(selectContact, [
|
||||||
provider.accountId,
|
provider.accountId,
|
||||||
normalizedWith,
|
normalizedWith,
|
||||||
normalizedWithout
|
normalizedWithout,
|
||||||
|
jidWith,
|
||||||
|
jidWithout
|
||||||
]);
|
]);
|
||||||
if (contactRes.rowCount > 0) {
|
if (res.rowCount) {
|
||||||
contact = contactRes.rows[0];
|
contact = res.rows[0];
|
||||||
this.logger.verbose(`Contato encontrado: ${JSON.stringify(contact)}`);
|
this.logger.verbose(`Contato existente: ${JSON.stringify(contact)}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.verbose(`Contato não encontrado. Inserindo novo contato para ${normalizedWith}`);
|
const insertContact = `
|
||||||
const insertContactQuery = `
|
INSERT INTO contacts
|
||||||
INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at)
|
(name, phone_number, account_id, identifier, created_at, updated_at)
|
||||||
VALUES (REPLACE($2, '+', ''), $2, $1, CONCAT(REPLACE($2, '+', ''), '@s.whatsapp.net'),
|
VALUES
|
||||||
to_timestamp($3), to_timestamp($4))
|
(
|
||||||
|
REPLACE($2, '+', ''),
|
||||||
|
$2,
|
||||||
|
$1,
|
||||||
|
$5, -- agora é $5
|
||||||
|
to_timestamp($3),
|
||||||
|
to_timestamp($4)
|
||||||
|
)
|
||||||
RETURNING id, phone_number
|
RETURNING id, phone_number
|
||||||
`;
|
`;
|
||||||
const insertRes = await pgClient.query(insertContactQuery, [
|
const insertRes = await pgClient.query(insertContact, [
|
||||||
provider.accountId,
|
provider.accountId, // $1
|
||||||
normalizedWith,
|
normalizedWith, // $2
|
||||||
phoneTimestamp.first,
|
ts.first, // $3
|
||||||
phoneTimestamp.last,
|
ts.last, // $4
|
||||||
|
jidWith // $5
|
||||||
]);
|
]);
|
||||||
contact = insertRes.rows[0];
|
contact = insertRes.rows[0];
|
||||||
this.logger.verbose(`Novo contato inserido: ${JSON.stringify(contact)}`);
|
|
||||||
|
|
||||||
|
this.logger.verbose(`Contato inserido: ${JSON.stringify(contact)}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao recuperar/inserir contato para ${rawPhoneNumber}: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Etapa 2: Buscar ou Inserir a Conversa (e o Contact_inboxes) ---
|
// 3) Buscar ou inserir ContactInbox
|
||||||
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;
|
let contactInboxId: number;
|
||||||
const selectContactInboxQuery = `
|
{
|
||||||
|
const selectCi = `
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM contact_inboxes
|
FROM contact_inboxes
|
||||||
WHERE contact_id = $1 AND inbox_id = $2
|
WHERE contact_id = $1
|
||||||
|
AND inbox_id = $2
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`;
|
`;
|
||||||
const ciRes = await pgClient.query(selectContactInboxQuery, [contact.id, inbox.id]);
|
const ciRes = await pgClient.query(selectCi, [
|
||||||
if (ciRes.rowCount > 0) {
|
contact.id,
|
||||||
|
inbox.id
|
||||||
|
]);
|
||||||
|
if (ciRes.rowCount) {
|
||||||
contactInboxId = ciRes.rows[0].id;
|
contactInboxId = ciRes.rows[0].id;
|
||||||
this.logger.verbose(`contact_inbox encontrado: ${contactInboxId}`);
|
this.logger.verbose(`Contact_inbox existente: ${contactInboxId}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.verbose(`Contact_inbox não encontrado para o contato ${contact.id}. Inserindo novo contact_inbox.`);
|
const insertCi = `
|
||||||
const insertContactInboxQuery = `
|
INSERT INTO contact_inboxes
|
||||||
INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at)
|
(contact_id, inbox_id, source_id, created_at, updated_at)
|
||||||
VALUES ($1, $2, gen_random_uuid(), NOW(), NOW())
|
VALUES
|
||||||
|
($1, $2, gen_random_uuid(), NOW(), NOW())
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
const ciInsertRes = await pgClient.query(insertContactInboxQuery, [contact.id, inbox.id]);
|
const insertRes = await pgClient.query(insertCi, [
|
||||||
contactInboxId = ciInsertRes.rows[0].id;
|
contact.id,
|
||||||
this.logger.verbose(`Novo contact_inbox inserido com ID: ${contactInboxId}`);
|
inbox.id
|
||||||
|
]);
|
||||||
|
contactInboxId = insertRes.rows[0].id;
|
||||||
|
this.logger.verbose(`Contact_inbox inserido: ${contactInboxId}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.verbose(`Inserindo conversa para o contato ${contact.id} com contact_inbox ${contactInboxId}`);
|
// 4) Buscar ou inserir Conversation
|
||||||
const insertConversationQuery = `
|
let conversationId: number;
|
||||||
|
{
|
||||||
|
const selectConv = `
|
||||||
|
SELECT id
|
||||||
|
FROM conversations
|
||||||
|
WHERE account_id = $1
|
||||||
|
AND inbox_id = $2
|
||||||
|
AND contact_id = $3
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
const convRes = await pgClient.query(selectConv, [
|
||||||
|
provider.accountId,
|
||||||
|
inbox.id,
|
||||||
|
contact.id
|
||||||
|
]);
|
||||||
|
if (convRes.rowCount) {
|
||||||
|
conversationId = convRes.rows[0].id;
|
||||||
|
this.logger.verbose(`Conversa existente: ${conversationId}`);
|
||||||
|
} else {
|
||||||
|
const insertConv = `
|
||||||
INSERT INTO conversations
|
INSERT INTO conversations
|
||||||
(account_id, inbox_id, status, contact_id, contact_inbox_id, uuid, last_activity_at, created_at, updated_at)
|
(account_id, inbox_id, status, contact_id, contact_inbox_id, uuid,
|
||||||
|
last_activity_at, created_at, updated_at)
|
||||||
VALUES
|
VALUES
|
||||||
($1, $2, 0, $3, $4, gen_random_uuid(), NOW(), NOW(), NOW())
|
($1, $2, 0, $3, $4, gen_random_uuid(), NOW(), NOW(), NOW())
|
||||||
RETURNING id AS conversation_id, contact_id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
const convInsertRes = await pgClient.query(insertConversationQuery, [
|
const insertRes = await pgClient.query(insertConv, [
|
||||||
provider.accountId,
|
provider.accountId,
|
||||||
inbox.id,
|
inbox.id,
|
||||||
contact.id,
|
contact.id,
|
||||||
contactInboxId,
|
contactInboxId
|
||||||
]);
|
]);
|
||||||
conversation = convInsertRes.rows[0];
|
conversationId = insertRes.rows[0].id;
|
||||||
this.logger.verbose(`Nova conversa inserida: ${JSON.stringify(conversation)}`);
|
this.logger.verbose(`Conversa inserida: ${conversationId}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Erro ao recuperar/inserir conversa para o contato ${contact.id}: ${error}`);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Etapa 3: Mapeia o resultado para o Map ---
|
resultMap.set(rawPhone, {
|
||||||
const fks: FksChatwoot = {
|
|
||||||
phone_number: normalizedWith,
|
phone_number: normalizedWith,
|
||||||
contact_id: contact.id,
|
contact_id: String(contact.id),
|
||||||
conversation_id: conversation.conversation_id || conversation.id
|
conversation_id: String(conversationId)
|
||||||
};
|
});
|
||||||
resultMap.set(normalizedWith, fks);
|
|
||||||
this.logger.verbose(`Resultado mapeado para ${normalizedWith}: ${JSON.stringify(fks)}`);
|
|
||||||
|
|
||||||
} // 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;
|
return resultMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,8 +584,6 @@ class ChatwootImport {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -681,8 +696,12 @@ class ChatwootImport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sliceIntoChunks(arr: any[], chunkSize: number) {
|
public sliceIntoChunks<T>(arr: T[], chunkSize: number): T[][] {
|
||||||
return arr.splice(0, chunkSize);
|
const chunks: T[][] = [];
|
||||||
|
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||||
|
chunks.push(arr.slice(i, i + chunkSize));
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isGroup(remoteJid: string) {
|
public isGroup(remoteJid: string) {
|
||||||
|
@ -40,7 +40,8 @@ const bucketExists = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setBucketPolicy = async () => {
|
const setBucketPolicy = async () => {
|
||||||
if (minioClient) {
|
if (!minioClient) return;
|
||||||
|
|
||||||
const policy = {
|
const policy = {
|
||||||
Version: '2012-10-17',
|
Version: '2012-10-17',
|
||||||
Statement: [
|
Statement: [
|
||||||
@ -52,10 +53,25 @@ const setBucketPolicy = async () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
|
await minioClient.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||||
|
console.log(`[S3 Service] Bucket policy aplicada em ${bucketName}`);
|
||||||
|
} catch (err: any) {
|
||||||
|
// MinIO não implementa esse endpoint
|
||||||
|
if (err.code === 'NotImplemented') {
|
||||||
|
console.warn(
|
||||||
|
`[S3 Service] setBucketPolicy não suportado por este endpoint, ignorando (bucket=${bucketName})`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// qualquer outro erro real, relança
|
||||||
|
console.error('[S3 Service] Erro ao aplicar bucket policy', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const createBucket = async () => {
|
const createBucket = async () => {
|
||||||
if (minioClient) {
|
if (minioClient) {
|
||||||
try {
|
try {
|
||||||
|
@ -42,7 +42,10 @@ export class CacheService {
|
|||||||
if (!this.cache) {
|
if (!this.cache) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cache.set(key, value, ttl);
|
|
||||||
|
const effectiveTtl = ttl ?? (2 * 60 * 60);
|
||||||
|
|
||||||
|
this.cache.set(key, value, effectiveTtl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async hSet(key: string, field: string, value: any) {
|
public async hSet(key: string, field: string, value: any) {
|
||||||
@ -69,6 +72,20 @@ export class CacheService {
|
|||||||
if (!this.cache) {
|
if (!this.cache) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Verifica se a chave é realmente uma string
|
||||||
|
if (typeof key !== 'string') {
|
||||||
|
this.logger.error(
|
||||||
|
`Invalid cache key type: expected string but received ${typeof key}. Key content: ${JSON.stringify(key)}. Stack trace: ${new Error().stack}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Opcional: se a chave contiver quebras de linha, pode ser um sinal de que há um vCard em vez de um simples identificador
|
||||||
|
if (key.includes('\n')) {
|
||||||
|
this.logger.error(
|
||||||
|
`Invalid cache key format (contains newline characters): ${key}. Stack trace: ${new Error().stack}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Chama a implementação real do delete
|
||||||
return this.cache.delete(key);
|
return this.cache.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user