feat(chatwoot): import history messages to chatwoot on whatsapp connection

Messages are imported direct to chatwoot database. Media and group messages are ignored.

New env.yml variables:
CHATWOOT_IMPORT_DATABASE_CONNECTION_URI: URI to connect direct on chatwoot database.
CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE: Indicates to use a text placeholder on media messages.

New instance setting:
sync_full_history: Indicates to request a full history sync to baileys.

New chatwoot options:
import_contacts: Indicates to import contacts.
import_messages: Indicates to import messages.
days_limit_import_messages: Number of days to limit history messages to import.
This commit is contained in:
jaison-x
2024-01-31 11:20:29 -03:00
parent b09546577a
commit 8a5ebe83a3
23 changed files with 992 additions and 111 deletions

View File

@@ -51,6 +51,9 @@ export class ChatwootController {
data.sign_delimiter = null;
data.reopen_conversation = false;
data.conversation_pending = false;
data.import_contacts = false;
data.import_messages = false;
data.days_limit_import_messages = 0;
data.auto_create = false;
}

View File

@@ -57,12 +57,16 @@ export class InstanceController {
chatwoot_sign_msg,
chatwoot_reopen_conversation,
chatwoot_conversation_pending,
chatwoot_import_contacts,
chatwoot_import_messages,
chatwoot_days_limit_import_messages,
reject_call,
msg_call,
groups_ignore,
always_online,
read_messages,
read_status,
sync_full_history,
websocket_enabled,
websocket_events,
rabbitmq_enabled,
@@ -342,6 +346,7 @@ export class InstanceController {
always_online: always_online || false,
read_messages: read_messages || false,
read_status: read_status || false,
sync_full_history: sync_full_history ?? false,
};
this.logger.verbose('settings: ' + JSON.stringify(settings));
@@ -444,6 +449,9 @@ export class InstanceController {
number,
reopen_conversation: chatwoot_reopen_conversation || false,
conversation_pending: chatwoot_conversation_pending || false,
import_contacts: chatwoot_import_contacts ?? true,
import_messages: chatwoot_import_messages ?? true,
days_limit_import_messages: chatwoot_days_limit_import_messages ?? 60,
auto_create: true,
});
} catch (error) {
@@ -494,6 +502,9 @@ export class InstanceController {
sign_msg: chatwoot_sign_msg || false,
reopen_conversation: chatwoot_reopen_conversation || false,
conversation_pending: chatwoot_conversation_pending || false,
import_contacts: chatwoot_import_contacts ?? true,
import_messages: chatwoot_import_messages ?? true,
days_limit_import_messages: chatwoot_days_limit_import_messages || 60,
number,
name_inbox: instance.instanceName,
webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,

View File

@@ -9,5 +9,8 @@ export class ChatwootDto {
number?: string;
reopen_conversation?: boolean;
conversation_pending?: boolean;
import_contacts?: boolean;
import_messages?: boolean;
days_limit_import_messages?: number;
auto_create?: boolean;
}

View File

@@ -14,12 +14,16 @@ export class InstanceDto {
always_online?: boolean;
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
chatwoot_account_id?: string;
chatwoot_token?: string;
chatwoot_url?: string;
chatwoot_sign_msg?: boolean;
chatwoot_reopen_conversation?: boolean;
chatwoot_conversation_pending?: boolean;
chatwoot_import_contacts?: boolean;
chatwoot_import_messages?: boolean;
chatwoot_days_limit_import_messages?: number;
websocket_enabled?: boolean;
websocket_events?: string[];
rabbitmq_enabled?: boolean;

View File

@@ -5,4 +5,5 @@ export class SettingsDto {
always_online?: boolean;
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
}

View File

@@ -9,6 +9,11 @@ export class ChatRaw {
lastMsgTimestamp?: number;
}
type ChatRawBoolean<T> = {
[P in keyof T]?: 0 | 1;
};
export type ChatRawSelect = ChatRawBoolean<ChatRaw>;
const chatSchema = new Schema<ChatRaw>({
_id: { type: String, _id: true },
id: { type: String, required: true, minlength: 1 },

View File

@@ -14,6 +14,9 @@ export class ChatwootRaw {
number?: string;
reopen_conversation?: boolean;
conversation_pending?: boolean;
import_contacts?: boolean;
import_messages?: boolean;
days_limit_import_messages?: number;
}
const chatwootSchema = new Schema<ChatwootRaw>({
@@ -28,6 +31,9 @@ const chatwootSchema = new Schema<ChatwootRaw>({
number: { type: String, required: true },
reopen_conversation: { type: Boolean, required: true },
conversation_pending: { type: Boolean, required: true },
import_contacts: { type: Boolean, required: true },
import_messages: { type: Boolean, required: true },
days_limit_import_messages: { type: Number, required: true },
});
export const ChatwootModel = dbserver?.model(ChatwootRaw.name, chatwootSchema, 'chatwoot');

View File

@@ -10,6 +10,11 @@ export class ContactRaw {
owner: string;
}
type ContactRawBoolean<T> = {
[P in keyof T]?: 0 | 1;
};
export type ContactRawSelect = ContactRawBoolean<ContactRaw>;
const contactSchema = new Schema<ContactRaw>({
_id: { type: String, _id: true },
pushName: { type: String, minlength: 1 },

View File

@@ -33,6 +33,13 @@ export class MessageRaw {
contextInfo?: any;
}
type MessageRawBoolean<T> = {
[P in keyof T]?: 0 | 1;
};
export type MessageRawSelect = Omit<MessageRawBoolean<MessageRaw>, 'key'> & {
key?: MessageRawBoolean<Key>;
};
const messageSchema = new Schema<MessageRaw>({
_id: { type: String, _id: true },
key: {

View File

@@ -10,6 +10,7 @@ export class SettingsRaw {
always_online?: boolean;
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
}
const settingsSchema = new Schema<SettingsRaw>({
@@ -20,6 +21,7 @@ const settingsSchema = new Schema<SettingsRaw>({
always_online: { type: Boolean, required: true },
read_messages: { type: Boolean, required: true },
read_status: { type: Boolean, required: true },
sync_full_history: { type: Boolean, required: true },
});
export const SettingsModel = dbserver?.model(SettingsRaw.name, settingsSchema, 'settings');

View File

@@ -4,9 +4,10 @@ import { join } from 'path';
import { ConfigService, StoreConf } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { ChatRaw, IChatModel } from '../models';
import { ChatRaw, ChatRawSelect, IChatModel } from '../models';
export class ChatQuery {
select?: ChatRawSelect;
where: ChatRaw;
}
@@ -69,7 +70,7 @@ export class ChatRepository extends Repository {
this.logger.verbose('finding chats');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding chats in db');
return await this.chatModel.find({ owner: query.where.owner });
return await this.chatModel.find({ owner: query.where.owner }).select(query.select ?? {});
}
this.logger.verbose('finding chats in store');

View File

@@ -4,9 +4,10 @@ import { join } from 'path';
import { ConfigService, StoreConf } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { ContactRaw, IContactModel } from '../models';
import { ContactRaw, ContactRawSelect, IContactModel } from '../models';
export class ContactQuery {
select?: ContactRawSelect;
where: ContactRaw;
}
@@ -129,7 +130,7 @@ export class ContactRepository extends Repository {
this.logger.verbose('finding contacts');
if (this.dbSettings.ENABLED) {
this.logger.verbose('finding contacts in db');
return await this.contactModel.find({ ...query.where });
return await this.contactModel.find({ ...query.where }).select(query.select ?? {});
}
this.logger.verbose('finding contacts in store');

View File

@@ -4,9 +4,10 @@ import { join } from 'path';
import { ConfigService, StoreConf } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { IInsert, Repository } from '../abstract/abstract.repository';
import { IMessageModel, MessageRaw } from '../models';
import { IMessageModel, MessageRaw, MessageRawSelect } from '../models';
export class MessageQuery {
select?: MessageRawSelect;
where: MessageRaw;
limit?: number;
}
@@ -28,6 +29,15 @@ export class MessageRepository extends Repository {
}
}
for (const [o, p] of Object.entries(query?.select)) {
if (typeof p === 'object' && p !== null && !Array.isArray(p)) {
for (const [k, v] of Object.entries(p)) {
query.select[`${o}.${k}`] = v;
}
delete query.select[o];
}
}
return query;
}

View File

@@ -1,4 +1,4 @@
import ChatwootClient, { ChatwootAPIConfig, contact, conversation, inbox } from '@figuro/chatwoot-sdk';
import ChatwootClient, { ChatwootAPIConfig, contact, conversation, generic_id, inbox } from '@figuro/chatwoot-sdk';
import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request';
import axios from 'axios';
import FormData from 'form-data';
@@ -7,14 +7,15 @@ import Jimp from 'jimp';
import mimeTypes from 'mime-types';
import path from 'path';
import { ChatWoot, ConfigService, HttpServer } from '../../config/env.config';
import { Chatwoot, ConfigService, HttpServer } from '../../config/env.config';
import { Logger } from '../../config/logger.config';
import { chatwootImport } from '../../utils/chatwoot-import-helper';
import i18next from '../../utils/i18n';
import { ICache } from '../abstract/abstract.cache';
import { ChatwootDto } from '../dto/chatwoot.dto';
import { InstanceDto } from '../dto/instance.dto';
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto';
import { ChatwootRaw, MessageRaw } from '../models';
import { ChatwootRaw, ContactRaw, MessageRaw } from '../models';
import { RepositoryBroker } from '../repository/repository.manager';
import { Events } from '../types/wa.types';
import { WAMonitoringService } from './monitor.service';
@@ -770,6 +771,43 @@ export class ChatwootService {
return message;
}
public async getOpenConversationByContact(
instance: InstanceDto,
inbox: inbox,
contact: generic_id & contact,
): Promise<conversation> {
this.logger.verbose('find conversation in chatwoot');
const client = await this.clientCw(instance);
if (!client) {
this.logger.warn('client not found');
return null;
}
const payload = [
['inbox_id', inbox.id.toString()],
['contact_id', contact.id.toString()],
['status', 'open'],
];
return (
(
(await client.conversations.filter({
accountId: this.provider.account_id,
payload: payload.map((item, i, payload) => {
return {
attribute_key: item[0],
filter_operator: 'equal_to',
values: [item[1]],
query_operator: i < payload.length - 1 ? 'AND' : null,
};
}),
})) as { payload: conversation[] }
).payload[0] || undefined
);
}
public async createBotMessage(
instance: InstanceDto,
content: string,
@@ -805,21 +843,7 @@ export class ChatwootService {
return null;
}
this.logger.verbose('find conversation in chatwoot');
const findConversation = await client.conversations.list({
accountId: this.provider.account_id,
inboxId: filterInbox.id,
});
if (!findConversation) {
this.logger.warn('conversation not found');
return null;
}
this.logger.verbose('find conversation by contact id');
const conversation = findConversation.data.payload.find(
(conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open',
);
const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact);
if (!conversation) {
this.logger.warn('conversation not found');
@@ -942,21 +966,7 @@ export class ChatwootService {
return null;
}
this.logger.verbose('find conversation in chatwoot');
const findConversation = await client.conversations.list({
accountId: this.provider.account_id,
inboxId: filterInbox.id,
});
if (!findConversation) {
this.logger.warn('conversation not found');
return null;
}
this.logger.verbose('find conversation by contact id');
const conversation = findConversation.data.payload.find(
(conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open',
);
const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact);
if (!conversation) {
this.logger.warn('conversation not found');
@@ -1655,7 +1665,7 @@ export class ChatwootService {
return result;
}
private getConversationMessage(msg: any) {
public getConversationMessage(msg: any) {
this.logger.verbose('get conversation message');
const types = this.getTypeMessage(msg);
@@ -1965,7 +1975,7 @@ export class ChatwootService {
}
if (event === Events.MESSAGES_DELETE) {
const chatwootDelete = this.configService.get<ChatWoot>('CHATWOOT').MESSAGE_DELETE;
const chatwootDelete = this.configService.get<Chatwoot>('CHATWOOT').MESSAGE_DELETE;
if (chatwootDelete === true) {
this.logger.verbose('deleting message from instance: ' + instance.instanceName);
@@ -2060,6 +2070,7 @@ export class ChatwootService {
this.logger.verbose('send message to chatwoot');
await this.createBotMessage(instance, msgConnection, 'incoming');
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
chatwootImport.clearAll(instance);
}
}
}
@@ -2110,4 +2121,113 @@ export class ChatwootService {
public getNumberFromRemoteJid(remoteJid: string) {
return remoteJid.replace(/:\d+/, '').split('@')[0];
}
public startImportHistoryMessages(instance: InstanceDto) {
if (!this.isImportHistoryAvailable()) {
return;
}
this.createBotMessage(instance, `💬 Starting to import messages. Please wait...`, 'incoming');
}
public isImportHistoryAvailable() {
const uri = this.configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
return uri && uri !== 'postgres://user:password@hostname:port/dbname';
}
/* We can't proccess messages exactly in batch because Chatwoot use message id to order
messages in frontend and we are receiving the messages mixed between the batches.
Because this, we need to put all batches together and order after */
public addHistoryMessages(instance: InstanceDto, messagesRaw: MessageRaw[]) {
if (!this.isImportHistoryAvailable()) {
return;
}
chatwootImport.addHistoryMessages(instance, messagesRaw);
}
public addHistoryContacts(instance: InstanceDto, contactsRaw: ContactRaw[]) {
if (!this.isImportHistoryAvailable()) {
return;
}
return chatwootImport.addHistoryContacts(instance, contactsRaw);
}
public async importHistoryMessages(instance: InstanceDto) {
if (!this.isImportHistoryAvailable()) {
return;
}
this.createBotMessage(instance, '💬 Importing messages. More one moment...', 'incoming');
const totalMessagesImported = await chatwootImport.importHistoryMessages(
instance,
this,
await this.getInbox(instance),
this.provider,
);
this.updateContactAvatarInRecentConversations(instance);
const msg = Number.isInteger(totalMessagesImported)
? `${totalMessagesImported} messages imported. Refresh page to see the new messages`
: `Something went wrong in importing messages`;
this.createBotMessage(instance, `💬 ${msg}`, 'incoming');
return totalMessagesImported;
}
public async updateContactAvatarInRecentConversations(instance: InstanceDto, limitContacts = 100) {
try {
if (!this.isImportHistoryAvailable()) {
return;
}
const client = await this.clientCw(instance);
if (!client) {
this.logger.warn('client not found');
return null;
}
const inbox = await this.getInbox(instance);
if (!inbox) {
this.logger.warn('inbox not found');
return null;
}
const recentContacts = await chatwootImport.getContactsOrderByRecentConversations(
inbox,
this.provider,
limitContacts,
);
const contactsWithProfilePicture = (
await this.repository.contact.find({
where: {
owner: instance.instanceName,
id: {
$in: recentContacts.map((contact) => contact.identifier),
},
profilePictureUrl: { $ne: null },
},
} as any)
).reduce((acc: Map<string, ContactRaw>, contact: ContactRaw) => acc.set(contact.id, contact), new Map());
recentContacts.forEach(async (contact) => {
if (contactsWithProfilePicture.has(contact.identifier)) {
client.contacts.update({
accountId: this.provider.account_id,
id: contact.id,
data: {
avatar_url: contactsWithProfilePicture.get(contact.identifier).profilePictureUrl || null,
},
});
}
});
} catch (error) {
this.logger.error(`Error on update avatar in recent conversations: ${error.toString()}`);
}
}
}

View File

@@ -73,6 +73,7 @@ import { dbserver } from '../../libs/db.connect';
import { RedisCache } from '../../libs/redis.client';
import { getIO } from '../../libs/socket.server';
import { getSQS, removeQueues as removeQueuesSQS } from '../../libs/sqs.server';
import { chatwootImport } from '../../utils/chatwoot-import-helper';
import { makeProxyAgent } from '../../utils/makeProxyAgent';
import { useMultiFileAuthStateDb } from '../../utils/use-multi-file-auth-state-db';
import { useMultiFileAuthStateRedisDb } from '../../utils/use-multi-file-auth-state-redis-db';
@@ -103,6 +104,7 @@ import {
GroupUpdateParticipantDto,
GroupUpdateSettingDto,
} from '../dto/group.dto';
import { InstanceDto } from '../dto/instance.dto';
import {
ContactMessage,
MediaMessage,
@@ -355,6 +357,15 @@ export class WAStartupService {
this.localChatwoot.conversation_pending = data?.conversation_pending;
this.logger.verbose(`Chatwoot conversation pending: ${this.localChatwoot.conversation_pending}`);
this.localChatwoot.import_contacts = data?.import_contacts;
this.logger.verbose(`Chatwoot import contacts: ${this.localChatwoot.import_contacts}`);
this.localChatwoot.import_messages = data?.import_messages;
this.logger.verbose(`Chatwoot import messages: ${this.localChatwoot.import_messages}`);
this.localChatwoot.days_limit_import_messages = data?.days_limit_import_messages;
this.logger.verbose(`Chatwoot days limit import messages: ${this.localChatwoot.days_limit_import_messages}`);
this.logger.verbose('Chatwoot loaded');
}
@@ -369,6 +380,9 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`);
this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`);
this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`);
Object.assign(this.localChatwoot, { ...data, sign_delimiter: data.sign_msg ? data.sign_delimiter : null });
@@ -394,6 +408,9 @@ export class WAStartupService {
this.logger.verbose(`Chatwoot sign delimiter: ${data.sign_delimiter}`);
this.logger.verbose(`Chatwoot reopen conversation: ${data.reopen_conversation}`);
this.logger.verbose(`Chatwoot conversation pending: ${data.conversation_pending}`);
this.logger.verbose(`Chatwoot import contacts: ${data.import_contacts}`);
this.logger.verbose(`Chatwoot import messages: ${data.import_messages}`);
this.logger.verbose(`Chatwoot days limit import messages: ${data.days_limit_import_messages}`);
return {
enabled: data.enabled,
@@ -405,6 +422,9 @@ export class WAStartupService {
sign_delimiter: data.sign_delimiter || null,
reopen_conversation: data.reopen_conversation,
conversation_pending: data.conversation_pending,
import_contacts: data.import_contacts,
import_messages: data.import_messages,
days_limit_import_messages: data.days_limit_import_messages,
};
}
@@ -437,6 +457,9 @@ export class WAStartupService {
this.localSettings.read_status = data?.read_status;
this.logger.verbose(`Settings read_status: ${this.localSettings.read_status}`);
this.localSettings.sync_full_history = data?.sync_full_history;
this.logger.verbose(`Settings sync_full_history: ${this.localSettings.sync_full_history}`);
this.logger.verbose('Settings loaded');
}
@@ -449,6 +472,7 @@ export class WAStartupService {
this.logger.verbose(`Settings always_online: ${data.always_online}`);
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
Object.assign(this.localSettings, data);
this.logger.verbose('Settings set');
@@ -470,6 +494,7 @@ export class WAStartupService {
this.logger.verbose(`Settings always_online: ${data.always_online}`);
this.logger.verbose(`Settings read_messages: ${data.read_messages}`);
this.logger.verbose(`Settings read_status: ${data.read_status}`);
this.logger.verbose(`Settings sync_full_history: ${data.sync_full_history}`);
return {
reject_call: data.reject_call,
msg_call: data.msg_call,
@@ -477,6 +502,7 @@ export class WAStartupService {
always_online: data.always_online,
read_messages: data.read_messages,
read_status: data.read_status,
sync_full_history: data.sync_full_history,
};
}
@@ -1430,7 +1456,10 @@ export class WAStartupService {
msgRetryCounterCache: this.msgRetryCounterCache,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
generateHighQualityLinkPreview: true,
syncFullHistory: false,
syncFullHistory: this.localSettings.sync_full_history,
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
return this.historySyncNotification(msg);
},
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 },
patchMessageBeforeSending(message) {
@@ -1517,7 +1546,10 @@ export class WAStartupService {
msgRetryCounterCache: this.msgRetryCounterCache,
getMessage: async (key) => (await this.getMessage(key)) as Promise<proto.IMessage>,
generateHighQualityLinkPreview: true,
syncFullHistory: false,
syncFullHistory: this.localSettings.sync_full_history,
shouldSyncHistoryMessage: (msg: proto.Message.IHistorySyncNotification) => {
return this.historySyncNotification(msg);
},
userDevicesCache: this.userDevicesCache,
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 10 },
patchMessageBeforeSending(message) {
@@ -1611,33 +1643,48 @@ export class WAStartupService {
private readonly contactHandle = {
'contacts.upsert': async (contacts: Contact[], database: Database) => {
this.logger.verbose('Event received: contacts.upsert');
try {
this.logger.verbose('Event received: contacts.upsert');
this.logger.verbose('Finding contacts in database');
const contactsRepository = await this.repository.contact.find({
where: { owner: this.instance.name },
});
this.logger.verbose('Finding contacts in database');
const contactsRepository = new Set(
(
await this.repository.contact.find({
select: { id: 1, _id: 0 },
where: { owner: this.instance.name },
})
).map((contact) => contact.id),
);
this.logger.verbose('Verifying if contacts exists in database to insert');
const contactsRaw: ContactRaw[] = [];
for await (const contact of contacts) {
if (contactsRepository.find((cr) => cr.id === contact.id)) {
continue;
this.logger.verbose('Verifying if contacts exists in database to insert');
const contactsRaw: ContactRaw[] = [];
for (const contact of contacts) {
if (contactsRepository.has(contact.id)) {
continue;
}
contactsRaw.push({
id: contact.id,
pushName: contact?.name || contact?.verifiedName || contact.id.split('@')[0],
profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl,
owner: this.instance.name,
});
}
contactsRaw.push({
id: contact.id,
pushName: contact?.name || contact?.verifiedName,
profilePictureUrl: (await this.profilePicture(contact.id)).profilePictureUrl,
owner: this.instance.name,
});
this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT');
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw);
this.logger.verbose('Inserting contacts in database');
this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS);
if (this.localChatwoot.enabled && this.localChatwoot.import_contacts && contactsRaw.length) {
this.chatwootService.addHistoryContacts({ instanceName: this.instance.name }, contactsRaw);
chatwootImport.importHistoryContacts({ instanceName: this.instance.name }, this.localChatwoot);
}
} catch (error) {
this.logger.error(error);
}
this.logger.verbose('Sending data to webhook in event CONTACTS_UPSERT');
this.sendDataWebhook(Events.CONTACTS_UPSERT, contactsRaw);
this.logger.verbose('Inserting contacts in database');
this.repository.contact.insert(contactsRaw, this.instance.name, database.SAVE_DATA.CONTACTS);
},
'contacts.update': async (contacts: Partial<Contact>[], database: Database) => {
@@ -1667,7 +1714,6 @@ export class WAStartupService {
{
messages,
chats,
isLatest,
}: {
chats: Chat[];
contacts: Contact[];
@@ -1676,55 +1722,115 @@ export class WAStartupService {
},
database: Database,
) => {
this.logger.verbose('Event received: messaging-history.set');
if (isLatest) {
this.logger.verbose('isLatest defined as true');
const chatsRaw: ChatRaw[] = chats.map((chat) => {
return {
try {
this.logger.verbose('Event received: messaging-history.set');
const instance: InstanceDto = { instanceName: this.instance.name };
const daysLimitToImport = this.localChatwoot.enabled ? this.localChatwoot.days_limit_import_messages : 1000;
this.logger.verbose(`Param days limit import messages is: ${daysLimitToImport}`);
const date = new Date();
const timestampLimitToImport = new Date(date.setDate(date.getDate() - daysLimitToImport)).getTime() / 1000;
const maxBatchTimestamp = Math.max(...messages.map((message) => message.messageTimestamp as number));
const processBatch = maxBatchTimestamp >= timestampLimitToImport;
if (!processBatch) {
this.logger.verbose('Batch ignored by maxTimestamp in this batch');
return;
}
const chatsRaw: ChatRaw[] = [];
const chatsRepository = new Set(
(
await this.repository.chat.find({
select: { id: 1, _id: 0 },
where: { owner: this.instance.name },
})
).map((chat) => chat.id),
);
for (const chat of chats) {
if (chatsRepository.has(chat.id)) {
continue;
}
chatsRaw.push({
id: chat.id,
owner: this.instance.name,
lastMsgTimestamp: chat.lastMessageRecvTimestamp,
};
});
});
}
this.logger.verbose('Sending data to webhook in event CHATS_SET');
this.sendDataWebhook(Events.CHATS_SET, chatsRaw);
this.logger.verbose('Inserting chats in database');
this.repository.chat.insert(chatsRaw, this.instance.name, database.SAVE_DATA.CHATS);
const messagesRaw: MessageRaw[] = [];
const messagesRepository = new Set(
chatwootImport.getRepositoryMessagesCache(instance) ??
(
await this.repository.message.find({
select: { key: { id: 1 }, _id: 0 },
where: { owner: this.instance.name },
})
).map((message) => message.key.id),
);
if (chatwootImport.getRepositoryMessagesCache(instance) === null) {
chatwootImport.setRepositoryMessagesCache(instance, messagesRepository);
}
for (const m of messages) {
if (!m.message || !m.key || !m.messageTimestamp) {
continue;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
if (m.messageTimestamp <= timestampLimitToImport) {
continue;
}
if (messagesRepository.has(m.key.id)) {
continue;
}
messagesRaw.push({
key: m.key,
pushName: m.pushName || m.key.remoteJid.split('@')[0],
participant: m.participant,
message: { ...m.message },
messageType: getContentType(m.message),
messageTimestamp: m.messageTimestamp as number,
owner: this.instance.name,
});
}
this.logger.verbose('Sending data to webhook in event MESSAGES_SET');
this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]);
this.logger.verbose('Inserting messages in database');
await this.repository.message.insert(messagesRaw, this.instance.name, database.SAVE_DATA.NEW_MESSAGE);
if (this.localChatwoot.enabled && this.localChatwoot.import_messages && messagesRaw.length > 0) {
this.chatwootService.addHistoryMessages(
instance,
messagesRaw.filter((msg) => !chatwootImport.isIgnorePhoneNumber(msg.key?.remoteJid)),
);
}
messages = undefined;
chats = undefined;
} catch (error) {
this.logger.error(error);
}
const messagesRaw: MessageRaw[] = [];
const messagesRepository = await this.repository.message.find({
where: { owner: this.instance.name },
});
for await (const [, m] of Object.entries(messages)) {
if (!m.message) {
continue;
}
if (messagesRepository.find((mr) => mr.owner === this.instance.name && mr.key.id === m.key.id)) {
continue;
}
if (Long.isLong(m?.messageTimestamp)) {
m.messageTimestamp = m.messageTimestamp?.toNumber();
}
messagesRaw.push({
key: m.key,
pushName: m.pushName,
participant: m.participant,
message: { ...m.message },
messageType: getContentType(m.message),
messageTimestamp: m.messageTimestamp as number,
owner: this.instance.name,
});
}
this.logger.verbose('Sending data to webhook in event MESSAGES_SET');
this.sendDataWebhook(Events.MESSAGES_SET, [...messagesRaw]);
messages = undefined;
},
'messages.upsert': async (
@@ -2215,6 +2321,35 @@ export class WAStartupService {
}
}
private historySyncNotification(msg: proto.Message.IHistorySyncNotification) {
const instance: InstanceDto = { instanceName: this.instance.name };
if (
this.localChatwoot.enabled &&
this.localChatwoot.import_messages &&
this.isSyncNotificationFromUsedSyncType(msg)
) {
if (msg.chunkOrder === 1) {
this.chatwootService.startImportHistoryMessages(instance);
}
if (msg.progress === 100) {
setTimeout(() => {
this.chatwootService.importHistoryMessages(instance);
}, 10000);
}
}
return true;
}
private isSyncNotificationFromUsedSyncType(msg: proto.Message.IHistorySyncNotification) {
return (
(this.localSettings.sync_full_history && msg?.syncType === 2) ||
(!this.localSettings.sync_full_history && msg?.syncType === 3)
);
}
private createJid(number: string): string {
this.logger.verbose('Creating jid with number: ' + number);

View File

@@ -65,6 +65,9 @@ export declare namespace wa {
number?: string;
reopen_conversation?: boolean;
conversation_pending?: boolean;
import_contacts?: boolean;
import_messages?: boolean;
days_limit_import_messages?: number;
};
export type LocalSettings = {
@@ -74,6 +77,7 @@ export declare namespace wa {
always_online?: boolean;
read_messages?: boolean;
read_status?: boolean;
sync_full_history?: boolean;
};
export type LocalWebsocket = {