mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-16 04:02:54 -06:00
feat: Sync lost messages on chatwoot
Runs the sync method every 30min
This commit is contained in:
parent
4ca141b4f4
commit
e241cf4ee0
@ -81,6 +81,7 @@
|
|||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"minio": "^8.0.1",
|
"minio": "^8.0.1",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"node-windows": "^1.0.0-beta.8",
|
"node-windows": "^1.0.0-beta.8",
|
||||||
"openai": "^4.52.7",
|
"openai": "^4.52.7",
|
||||||
"parse-bmfont-xml": "^1.1.4",
|
"parse-bmfont-xml": "^1.1.4",
|
||||||
@ -106,6 +107,7 @@
|
|||||||
"@types/json-schema": "^7.0.15",
|
"@types/json-schema": "^7.0.15",
|
||||||
"@types/mime": "3.0.0",
|
"@types/mime": "3.0.0",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
|
"@types/node-cron": "^3.0.11",
|
||||||
"@types/node-windows": "^0.1.2",
|
"@types/node-windows": "^0.1.2",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/qrcode-terminal": "^0.12.0",
|
"@types/qrcode-terminal": "^0.12.0",
|
||||||
|
@ -121,6 +121,7 @@ import { readFileSync } from 'fs';
|
|||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
import mime from 'mime';
|
import mime from 'mime';
|
||||||
import NodeCache from 'node-cache';
|
import NodeCache from 'node-cache';
|
||||||
|
import cron from 'node-cron';
|
||||||
import { release } from 'os';
|
import { release } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import P from 'pino';
|
import P from 'pino';
|
||||||
@ -367,7 +368,12 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
if (connection === 'open') {
|
if (connection === 'open') {
|
||||||
this.instance.wuid = this.client.user.id.replace(/:\d+/, '');
|
this.instance.wuid = this.client.user.id.replace(/:\d+/, '');
|
||||||
this.instance.profilePictureUrl = (await this.profilePicture(this.instance.wuid)).profilePictureUrl;
|
try {
|
||||||
|
const profilePic = await this.profilePicture(this.instance.wuid);
|
||||||
|
this.instance.profilePictureUrl = profilePic.profilePictureUrl;
|
||||||
|
} catch (error) {
|
||||||
|
this.instance.profilePictureUrl = null;
|
||||||
|
}
|
||||||
const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' ');
|
const formattedWuid = this.instance.wuid.split('@')[0].padEnd(30, ' ');
|
||||||
const formattedName = this.instance.name;
|
const formattedName = this.instance.name;
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
@ -402,6 +408,7 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
status: 'open',
|
status: 'open',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
this.syncChatwootLostMessages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3638,14 +3645,15 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private prepareMessage(message: proto.IWebMessageInfo): any {
|
private prepareMessage(message: proto.IWebMessageInfo): any {
|
||||||
const contentMsg = message?.message[getContentType(message.message)] as any;
|
const contentType = getContentType(message.message);
|
||||||
|
const contentMsg = message?.message[contentType] as any;
|
||||||
|
|
||||||
const messageRaw = {
|
const messageRaw = {
|
||||||
key: message.key,
|
key: message.key,
|
||||||
pushName: message.pushName,
|
pushName: message.pushName,
|
||||||
message: { ...message.message },
|
message: { ...message.message },
|
||||||
contextInfo: contentMsg?.contextInfo,
|
contextInfo: contentMsg?.contextInfo,
|
||||||
messageType: getContentType(message.message) || 'unknown',
|
messageType: contentType || 'unknown',
|
||||||
messageTimestamp: message.messageTimestamp as number,
|
messageTimestamp: message.messageTimestamp as number,
|
||||||
instanceId: this.instanceId,
|
instanceId: this.instanceId,
|
||||||
source: getDevice(message.key.id),
|
source: getDevice(message.key.id),
|
||||||
@ -3659,4 +3667,17 @@ export class BaileysStartupService extends ChannelStartupService {
|
|||||||
|
|
||||||
return messageRaw;
|
return messageRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async syncChatwootLostMessages() {
|
||||||
|
if (this.configService.get<Chatwoot>('CHATWOOT').ENABLED && this.localChatwoot?.enabled) {
|
||||||
|
const chatwootConfig = await this.findChatwoot();
|
||||||
|
const prepare = (message: any) => this.prepareMessage(message);
|
||||||
|
this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare);
|
||||||
|
|
||||||
|
const task = cron.schedule('0,30 * * * *', async () => {
|
||||||
|
this.chatwootService.syncLostMessages({ instanceName: this.instance.name }, chatwootConfig, prepare);
|
||||||
|
});
|
||||||
|
task.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export class ChatwootController {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findChatwoot(instance: InstanceDto) {
|
public async findChatwoot(instance: InstanceDto): Promise<ChatwootDto & { webhook_url: string }> {
|
||||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled');
|
||||||
|
|
||||||
const result = await this.chatwootService.find(instance);
|
const result = await this.chatwootService.find(instance);
|
||||||
|
@ -7,7 +7,7 @@ import { PrismaRepository } from '@api/repository/repository.service';
|
|||||||
import { CacheService } from '@api/services/cache.service';
|
import { CacheService } from '@api/services/cache.service';
|
||||||
import { WAMonitoringService } from '@api/services/monitor.service';
|
import { WAMonitoringService } from '@api/services/monitor.service';
|
||||||
import { Events } from '@api/types/wa.types';
|
import { Events } from '@api/types/wa.types';
|
||||||
import { Chatwoot, ConfigService, HttpServer } from '@config/env.config';
|
import { Chatwoot, ConfigService, Database, HttpServer } from '@config/env.config';
|
||||||
import { Logger } from '@config/logger.config';
|
import { Logger } from '@config/logger.config';
|
||||||
import ChatwootClient, {
|
import ChatwootClient, {
|
||||||
ChatwootAPIConfig,
|
ChatwootAPIConfig,
|
||||||
@ -24,6 +24,7 @@ import i18next from '@utils/i18n';
|
|||||||
import { sendTelemetry } from '@utils/sendTelemetry';
|
import { sendTelemetry } from '@utils/sendTelemetry';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { proto } from 'baileys';
|
import { proto } from 'baileys';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import Jimp from 'jimp';
|
import Jimp from 'jimp';
|
||||||
import Long from 'long';
|
import Long from 'long';
|
||||||
@ -53,7 +54,7 @@ export class ChatwootService {
|
|||||||
|
|
||||||
private pgClient = postgresClient.getChatwootConnection();
|
private pgClient = postgresClient.getChatwootConnection();
|
||||||
|
|
||||||
private async getProvider(instance: InstanceDto) {
|
private async getProvider(instance: InstanceDto): Promise<ChatwootModel | null> {
|
||||||
const cacheKey = `${instance.instanceName}:getProvider`;
|
const cacheKey = `${instance.instanceName}:getProvider`;
|
||||||
if (await this.cache.has(cacheKey)) {
|
if (await this.cache.has(cacheKey)) {
|
||||||
const provider = (await this.cache.get(cacheKey)) as ChatwootModel;
|
const provider = (await this.cache.get(cacheKey)) as ChatwootModel;
|
||||||
@ -715,7 +716,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getInbox(instance: InstanceDto) {
|
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)) {
|
||||||
return (await this.cache.get(cacheKey)) as inbox;
|
return (await this.cache.get(cacheKey)) as inbox;
|
||||||
@ -839,12 +840,6 @@ export class ChatwootService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT) {
|
|
||||||
this.logger.log('Chatwoot bot contact is disabled');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contact = await this.findContact(instance, '123456');
|
const contact = await this.findContact(instance, '123456');
|
||||||
|
|
||||||
if (!contact) {
|
if (!contact) {
|
||||||
@ -1186,10 +1181,10 @@ export class ChatwootService {
|
|||||||
|
|
||||||
const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
|
const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
|
||||||
|
|
||||||
if (cwBotContact && chatId === '123456' && body.message_type === 'outgoing') {
|
if (chatId === '123456' && body.message_type === 'outgoing') {
|
||||||
const command = messageReceived.replace('/', '');
|
const command = messageReceived.replace('/', '');
|
||||||
|
|
||||||
if (command.includes('init') || command.includes('iniciar')) {
|
if (cwBotContact && (command.includes('init') || command.includes('iniciar'))) {
|
||||||
const state = waInstance?.connectionStatus?.state;
|
const state = waInstance?.connectionStatus?.state;
|
||||||
|
|
||||||
if (state !== 'open') {
|
if (state !== 'open') {
|
||||||
@ -1242,7 +1237,7 @@ export class ChatwootService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command === 'disconnect' || command === 'desconectar') {
|
if (cwBotContact && (command === 'disconnect' || command === 'desconectar')) {
|
||||||
const msgLogout = i18next.t('cw.inbox.disconnect', {
|
const msgLogout = i18next.t('cw.inbox.disconnect', {
|
||||||
inboxName: body.inbox.name,
|
inboxName: body.inbox.name,
|
||||||
});
|
});
|
||||||
@ -1532,7 +1527,7 @@ export class ChatwootService {
|
|||||||
'audioMessage',
|
'audioMessage',
|
||||||
'videoMessage',
|
'videoMessage',
|
||||||
'stickerMessage',
|
'stickerMessage',
|
||||||
'viewOnceMessageV2'
|
'viewOnceMessageV2',
|
||||||
];
|
];
|
||||||
|
|
||||||
const messageKeys = Object.keys(message);
|
const messageKeys = Object.keys(message);
|
||||||
@ -1586,8 +1581,10 @@ export class ChatwootService {
|
|||||||
liveLocationMessage: msg.liveLocationMessage,
|
liveLocationMessage: msg.liveLocationMessage,
|
||||||
listMessage: msg.listMessage,
|
listMessage: msg.listMessage,
|
||||||
listResponseMessage: msg.listResponseMessage,
|
listResponseMessage: msg.listResponseMessage,
|
||||||
viewOnceMessageV2: msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
|
viewOnceMessageV2:
|
||||||
|
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
|
||||||
|
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
|
||||||
|
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
|
||||||
};
|
};
|
||||||
|
|
||||||
return types;
|
return types;
|
||||||
@ -2376,4 +2373,63 @@ export class ChatwootService {
|
|||||||
this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`);
|
this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async syncLostMessages(
|
||||||
|
instance: InstanceDto,
|
||||||
|
chatwootConfig: ChatwootDto,
|
||||||
|
prepareMessage: (message: any) => any,
|
||||||
|
) {
|
||||||
|
if (!this.isImportHistoryAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.configService.get<Database>('DATABASE').SAVE_DATA.MESSAGE_UPDATE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inbox = await this.getInbox(instance);
|
||||||
|
|
||||||
|
const sqlMessages = `select * from messages m
|
||||||
|
where account_id = ${chatwootConfig.accountId}
|
||||||
|
and inbox_id = ${inbox.id}
|
||||||
|
and created_at >= now() - interval '6h'
|
||||||
|
order by created_at desc`;
|
||||||
|
|
||||||
|
const messagesData = (await this.pgClient.query(sqlMessages))?.rows;
|
||||||
|
const ids: string[] = messagesData
|
||||||
|
.filter((message) => !!message.source_id)
|
||||||
|
.map((message) => message.source_id.replace('WAID:', ''));
|
||||||
|
|
||||||
|
const savedMessages = await this.prismaRepository.message.findMany({
|
||||||
|
where: {
|
||||||
|
Instance: { name: instance.instanceName },
|
||||||
|
messageTimestamp: { gte: dayjs().subtract(6, 'hours').unix() },
|
||||||
|
AND: ids.map((id) => ({ key: { path: ['id'], not: id } })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredMessages = savedMessages.filter(
|
||||||
|
(msg: any) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid),
|
||||||
|
);
|
||||||
|
const messagesRaw: any[] = [];
|
||||||
|
for (const m of filteredMessages) {
|
||||||
|
if (!m.message || !m.key || !m.messageTimestamp) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Long.isLong(m?.messageTimestamp)) {
|
||||||
|
m.messageTimestamp = m.messageTimestamp?.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
messagesRaw.push(prepareMessage(m as any));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addHistoryMessages(
|
||||||
|
instance,
|
||||||
|
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await chatwootImport.importHistoryMessages(instance, this, inbox, this.provider);
|
||||||
|
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||||||
|
waInstance.clearCacheChatwoot();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class ChatwootImport {
|
|||||||
const actualValue = this.historyMessages.has(instance.instanceName)
|
const actualValue = this.historyMessages.has(instance.instanceName)
|
||||||
? this.historyMessages.get(instance.instanceName)
|
? this.historyMessages.get(instance.instanceName)
|
||||||
: [];
|
: [];
|
||||||
this.historyMessages.set(instance.instanceName, actualValue.concat(messagesRaw));
|
this.historyMessages.set(instance.instanceName, [...actualValue, ...messagesRaw]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addHistoryContacts(instance: InstanceDto, contactsRaw: Contact[]) {
|
public addHistoryContacts(instance: InstanceDto, contactsRaw: Contact[]) {
|
||||||
@ -169,6 +169,24 @@ class ChatwootImport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getExistingSourceIds(sourceIds: string[]): Promise<Set<string>> {
|
||||||
|
const existingSourceIdsSet = new Set<string>();
|
||||||
|
|
||||||
|
if (sourceIds.length === 0) {
|
||||||
|
return existingSourceIdsSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = 'SELECT source_id FROM messages WHERE source_id = ANY($1)';
|
||||||
|
const pgClient = postgresClient.getChatwootConnection();
|
||||||
|
const result = await pgClient.query(query, [sourceIds]);
|
||||||
|
|
||||||
|
for (const row of result.rows) {
|
||||||
|
existingSourceIdsSet.add(row.source_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingSourceIdsSet;
|
||||||
|
}
|
||||||
|
|
||||||
public async importHistoryMessages(
|
public async importHistoryMessages(
|
||||||
instance: InstanceDto,
|
instance: InstanceDto,
|
||||||
chatwootService: ChatwootService,
|
chatwootService: ChatwootService,
|
||||||
@ -185,7 +203,7 @@ class ChatwootImport {
|
|||||||
|
|
||||||
let totalMessagesImported = 0;
|
let totalMessagesImported = 0;
|
||||||
|
|
||||||
const messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
let messagesOrdered = this.historyMessages.get(instance.instanceName) || [];
|
||||||
if (messagesOrdered.length === 0) {
|
if (messagesOrdered.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -216,6 +234,8 @@ class ChatwootImport {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const existingSourceIds = await this.getExistingSourceIds(messagesOrdered.map((message: any) => message.key.id));
|
||||||
|
messagesOrdered = messagesOrdered.filter((message: any) => !existingSourceIds.has(message.key.id));
|
||||||
// processing messages in batch
|
// processing messages in batch
|
||||||
const batchSize = 4000;
|
const batchSize = 4000;
|
||||||
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize);
|
||||||
@ -233,8 +253,8 @@ class ChatwootImport {
|
|||||||
|
|
||||||
// inserting messages in chatwoot db
|
// inserting messages in chatwoot db
|
||||||
let sqlInsertMsg = `INSERT INTO messages
|
let sqlInsertMsg = `INSERT INTO messages
|
||||||
(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, created_at, updated_at) VALUES `;
|
sender_type, sender_id, source_id, created_at, updated_at) VALUES `;
|
||||||
const bindInsertMsg = [provider.accountId, inbox.id];
|
const bindInsertMsg = [provider.accountId, inbox.id];
|
||||||
|
|
||||||
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
|
messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => {
|
||||||
@ -269,11 +289,14 @@ class ChatwootImport {
|
|||||||
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id);
|
bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id);
|
||||||
const bindSenderId = `$${bindInsertMsg.length}`;
|
const bindSenderId = `$${bindInsertMsg.length}`;
|
||||||
|
|
||||||
|
bindInsertMsg.push('WAID:' + message.key.id);
|
||||||
|
const bindSourceId = `$${bindInsertMsg.length}`;
|
||||||
|
|
||||||
bindInsertMsg.push(message.messageTimestamp as number);
|
bindInsertMsg.push(message.messageTimestamp as number);
|
||||||
const bindmessageTimestamp = `$${bindInsertMsg.length}`;
|
const bindmessageTimestamp = `$${bindInsertMsg.length}`;
|
||||||
|
|
||||||
sqlInsertMsg += `(${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0,
|
sqlInsertMsg += `(${bindContent}, ${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0,
|
||||||
${bindSenderType},${bindSenderId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
|
${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (bindInsertMsg.length > 2) {
|
if (bindInsertMsg.length > 2) {
|
||||||
|
@ -294,7 +294,7 @@ export class ChannelStartupService {
|
|||||||
this.clearCacheChatwoot();
|
this.clearCacheChatwoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findChatwoot(): Promise<ChatwootDto> {
|
public async findChatwoot(): Promise<ChatwootDto | null> {
|
||||||
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
if (!this.configService.get<Chatwoot>('CHATWOOT').ENABLED) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export function onUnexpectedError() {
|
|||||||
logger.error({
|
logger.error({
|
||||||
origin,
|
origin,
|
||||||
stderr: process.stderr.fd,
|
stderr: process.stderr.fd,
|
||||||
error,
|
|
||||||
});
|
});
|
||||||
|
logger.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user