mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-25 06:37:45 -06:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)}`,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -5,4 +5,5 @@ export class SettingsDto {
|
||||
always_online?: boolean;
|
||||
read_messages?: boolean;
|
||||
read_status?: boolean;
|
||||
sync_full_history?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user