mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-13 15:14:49 -06:00
2555 lines
77 KiB
TypeScript
2555 lines
77 KiB
TypeScript
import { InstanceDto } from '@api/dto/instance.dto';
|
||
import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '@api/dto/sendMessage.dto';
|
||
import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto';
|
||
import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client';
|
||
import { chatwootImport } from '@api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper';
|
||
import { PrismaRepository } from '@api/repository/repository.service';
|
||
import { CacheService } from '@api/services/cache.service';
|
||
import { WAMonitoringService } from '@api/services/monitor.service';
|
||
import { Events } from '@api/types/wa.types';
|
||
import { Chatwoot, ConfigService, Database, HttpServer } from '@config/env.config';
|
||
import { Logger } from '@config/logger.config';
|
||
import ChatwootClient, {
|
||
ChatwootAPIConfig,
|
||
contact,
|
||
contact_inboxes,
|
||
conversation,
|
||
conversation_show,
|
||
generic_id,
|
||
inbox,
|
||
} from '@figuro/chatwoot-sdk';
|
||
import { request as chatwootRequest } from '@figuro/chatwoot-sdk/dist/core/request';
|
||
import { Chatwoot as ChatwootModel, Contact as ContactModel, Message as MessageModel } from '@prisma/client';
|
||
import i18next from '@utils/i18n';
|
||
import { sendTelemetry } from '@utils/sendTelemetry';
|
||
import axios from 'axios';
|
||
import { proto } from 'baileys';
|
||
import dayjs from 'dayjs';
|
||
import FormData from 'form-data';
|
||
import Jimp from 'jimp';
|
||
import Long from 'long';
|
||
import mimeTypes from 'mime-types';
|
||
import path from 'path';
|
||
import { Readable } from 'stream';
|
||
|
||
interface ChatwootMessage {
|
||
messageId?: number;
|
||
inboxId?: number;
|
||
conversationId?: number;
|
||
contactInboxSourceId?: string;
|
||
isRead?: boolean;
|
||
}
|
||
|
||
export class ChatwootService {
|
||
private readonly logger = new Logger('ChatwootService');
|
||
|
||
private provider: any;
|
||
|
||
constructor(
|
||
private readonly waMonitor: WAMonitoringService,
|
||
private readonly configService: ConfigService,
|
||
private readonly prismaRepository: PrismaRepository,
|
||
private readonly cache: CacheService,
|
||
) {}
|
||
|
||
private pgClient = postgresClient.getChatwootConnection();
|
||
|
||
private async getProvider(instance: InstanceDto): Promise<ChatwootModel | null> {
|
||
const cacheKey = `${instance.instanceName}:getProvider`;
|
||
if (await this.cache.has(cacheKey)) {
|
||
const provider = (await this.cache.get(cacheKey)) as ChatwootModel;
|
||
|
||
return provider;
|
||
}
|
||
|
||
const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot();
|
||
|
||
if (!provider) {
|
||
this.logger.warn('provider not found');
|
||
return null;
|
||
}
|
||
|
||
this.cache.set(cacheKey, provider);
|
||
|
||
return provider;
|
||
}
|
||
|
||
private async clientCw(instance: InstanceDto) {
|
||
const provider = await this.getProvider(instance);
|
||
|
||
if (!provider) {
|
||
this.logger.error('provider not found');
|
||
return null;
|
||
}
|
||
|
||
this.provider = provider;
|
||
|
||
const client = new ChatwootClient({
|
||
config: this.getClientCwConfig(),
|
||
});
|
||
|
||
return client;
|
||
}
|
||
|
||
public getClientCwConfig(): ChatwootAPIConfig & { nameInbox: string; mergeBrazilContacts: boolean } {
|
||
return {
|
||
basePath: this.provider.url,
|
||
with_credentials: true,
|
||
credentials: 'include',
|
||
token: this.provider.token,
|
||
nameInbox: this.provider.nameInbox,
|
||
mergeBrazilContacts: this.provider.mergeBrazilContacts,
|
||
};
|
||
}
|
||
|
||
public getCache() {
|
||
return this.cache;
|
||
}
|
||
|
||
public async create(instance: InstanceDto, data: ChatwootDto) {
|
||
await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data);
|
||
|
||
if (data.autoCreate) {
|
||
this.logger.log('Auto create chatwoot instance');
|
||
const urlServer = this.configService.get<HttpServer>('SERVER').URL;
|
||
|
||
await this.initInstanceChatwoot(
|
||
instance,
|
||
data.nameInbox ?? instance.instanceName.split('-cwId-')[0],
|
||
`${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`,
|
||
true,
|
||
data.number,
|
||
data.organization,
|
||
data.logo,
|
||
);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
public async find(instance: InstanceDto): Promise<ChatwootDto> {
|
||
try {
|
||
return await this.waMonitor.waInstances[instance.instanceName].findChatwoot();
|
||
} catch (error) {
|
||
this.logger.error('chatwoot not found');
|
||
return { enabled: null, url: '' };
|
||
}
|
||
}
|
||
|
||
public async getContact(instance: InstanceDto, id: number) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
if (!id) {
|
||
this.logger.warn('id is required');
|
||
return null;
|
||
}
|
||
|
||
const contact = await client.contact.getContactable({
|
||
accountId: this.provider.accountId,
|
||
id,
|
||
});
|
||
|
||
if (!contact) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
return contact;
|
||
}
|
||
|
||
public async initInstanceChatwoot(
|
||
instance: InstanceDto,
|
||
inboxName: string,
|
||
webhookUrl: string,
|
||
qrcode: boolean,
|
||
number: string,
|
||
organization?: string,
|
||
logo?: string,
|
||
) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
const findInbox: any = await client.inboxes.list({
|
||
accountId: this.provider.accountId,
|
||
});
|
||
|
||
const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName);
|
||
|
||
let inboxId: number;
|
||
|
||
this.logger.log('Creating chatwoot inbox');
|
||
if (!checkDuplicate) {
|
||
const data = {
|
||
type: 'api',
|
||
webhook_url: webhookUrl,
|
||
};
|
||
|
||
const inbox = await client.inboxes.create({
|
||
accountId: this.provider.accountId,
|
||
data: {
|
||
name: inboxName,
|
||
channel: data as any,
|
||
},
|
||
});
|
||
|
||
if (!inbox) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
inboxId = inbox.id;
|
||
} else {
|
||
const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName);
|
||
|
||
if (!inbox) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
inboxId = inbox.id;
|
||
}
|
||
this.logger.log(`Inbox created - inboxId: ${inboxId}`);
|
||
|
||
if (!this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT) {
|
||
this.logger.log('Chatwoot bot contact is disabled');
|
||
|
||
return true;
|
||
}
|
||
|
||
this.logger.log('Creating chatwoot bot contact');
|
||
const contact =
|
||
(await this.findContact(instance, '123456')) ||
|
||
((await this.createContact(
|
||
instance,
|
||
'123456',
|
||
inboxId,
|
||
false,
|
||
organization ? organization : 'EvolutionAPI',
|
||
logo ? logo : 'https://evolution-api.com/files/evolution-api-favicon.png',
|
||
)) as any);
|
||
|
||
if (!contact) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
const contactId = contact.id || contact.payload.contact.id;
|
||
this.logger.log(`Contact created - contactId: ${contactId}`);
|
||
|
||
if (qrcode) {
|
||
this.logger.log('QR code enabled');
|
||
const data = {
|
||
contact_id: contactId.toString(),
|
||
inbox_id: inboxId.toString(),
|
||
};
|
||
|
||
const conversation = await client.conversations.create({
|
||
accountId: this.provider.accountId,
|
||
data,
|
||
});
|
||
|
||
if (!conversation) {
|
||
this.logger.warn('conversation not found');
|
||
return null;
|
||
}
|
||
|
||
let contentMsg = 'init';
|
||
|
||
if (number) {
|
||
contentMsg = `init:${number}`;
|
||
}
|
||
|
||
const message = await client.messages.create({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversation.id,
|
||
data: {
|
||
content: contentMsg,
|
||
message_type: 'outgoing',
|
||
},
|
||
});
|
||
|
||
if (!message) {
|
||
this.logger.warn('conversation not found');
|
||
return null;
|
||
}
|
||
this.logger.log('Init message sent');
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
public async createContact(
|
||
instance: InstanceDto,
|
||
phoneNumber: string,
|
||
inboxId: number,
|
||
isGroup: boolean,
|
||
name?: string,
|
||
avatar_url?: string,
|
||
jid?: string,
|
||
) {
|
||
try {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
let data: any = {};
|
||
if (!isGroup) {
|
||
data = {
|
||
inbox_id: inboxId,
|
||
name: name || phoneNumber,
|
||
identifier: jid,
|
||
avatar_url: avatar_url,
|
||
};
|
||
|
||
if ((jid && jid.includes('@')) || !jid) {
|
||
data['phone_number'] = `+${phoneNumber}`;
|
||
}
|
||
} else {
|
||
data = {
|
||
inbox_id: inboxId,
|
||
name: name || phoneNumber,
|
||
identifier: phoneNumber,
|
||
avatar_url: avatar_url,
|
||
};
|
||
}
|
||
|
||
const contact = await client.contacts.create({
|
||
accountId: this.provider.accountId,
|
||
data,
|
||
});
|
||
|
||
if (!contact) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
const findContact = await this.findContact(instance, phoneNumber);
|
||
|
||
const contactId = findContact?.id;
|
||
|
||
await this.addLabelToContact(this.provider.nameInbox, contactId);
|
||
|
||
return contact;
|
||
} catch (error) {
|
||
this.logger.error('Error creating contact');
|
||
console.log(error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async updateContact(instance: InstanceDto, id: number, data: any) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
if (!id) {
|
||
this.logger.warn('id is required');
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const contact = await client.contacts.update({
|
||
accountId: this.provider.accountId,
|
||
id,
|
||
data,
|
||
});
|
||
|
||
return contact;
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async addLabelToContact(nameInbox: string, contactId: number) {
|
||
try {
|
||
const uri = this.configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
|
||
|
||
if (!uri) return false;
|
||
|
||
const sqlTags = `SELECT id, taggings_count FROM tags WHERE name = $1 LIMIT 1`;
|
||
const tagData = (await this.pgClient.query(sqlTags, [nameInbox]))?.rows[0];
|
||
let tagId = tagData?.id;
|
||
const taggingsCount = tagData?.taggings_count || 0;
|
||
|
||
const sqlTag = `INSERT INTO tags (name, taggings_count)
|
||
VALUES ($1, $2)
|
||
ON CONFLICT (name)
|
||
DO UPDATE SET taggings_count = tags.taggings_count + 1
|
||
RETURNING id`;
|
||
|
||
tagId = (await this.pgClient.query(sqlTag, [nameInbox, taggingsCount + 1]))?.rows[0]?.id;
|
||
|
||
const sqlCheckTagging = `SELECT 1 FROM taggings
|
||
WHERE tag_id = $1 AND taggable_type = 'Contact' AND taggable_id = $2 AND context = 'labels' LIMIT 1`;
|
||
|
||
const taggingExists = (await this.pgClient.query(sqlCheckTagging, [tagId, contactId]))?.rowCount > 0;
|
||
|
||
if (!taggingExists) {
|
||
const sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at)
|
||
VALUES ($1, 'Contact', $2, 'labels', NOW())`;
|
||
|
||
await this.pgClient.query(sqlInsertLabel, [tagId, contactId]);
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public async findContact(instance: InstanceDto, phoneNumber: string) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
let query: any;
|
||
const isGroup = phoneNumber.includes('@g.us');
|
||
|
||
if (!isGroup) {
|
||
query = `+${phoneNumber}`;
|
||
} else {
|
||
query = phoneNumber;
|
||
}
|
||
|
||
let contact: any;
|
||
|
||
if (isGroup) {
|
||
contact = await client.contacts.search({
|
||
accountId: this.provider.accountId,
|
||
q: query,
|
||
});
|
||
} else {
|
||
contact = await chatwootRequest(this.getClientCwConfig(), {
|
||
method: 'POST',
|
||
url: `/api/v1/accounts/${this.provider.accountId}/contacts/filter`,
|
||
body: {
|
||
payload: this.getFilterPayload(query),
|
||
},
|
||
});
|
||
}
|
||
|
||
if (!contact && contact?.payload?.length === 0) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
if (!isGroup) {
|
||
return contact.payload.length > 1 ? this.findContactInContactList(contact.payload, query) : contact.payload[0];
|
||
} else {
|
||
return contact.payload.find((contact) => contact.identifier === query);
|
||
}
|
||
}
|
||
|
||
private async mergeBrazilianContacts(contacts: any[]) {
|
||
try {
|
||
const contact = await chatwootRequest(this.getClientCwConfig(), {
|
||
method: 'POST',
|
||
url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`,
|
||
body: {
|
||
base_contact_id: contacts.find((contact) => contact.phone_number.length === 14)?.id,
|
||
mergee_contact_id: contacts.find((contact) => contact.phone_number.length === 13)?.id,
|
||
},
|
||
});
|
||
|
||
return contact;
|
||
} catch {
|
||
this.logger.error('Error merging contacts');
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private findContactInContactList(contacts: any[], query: string) {
|
||
const phoneNumbers = this.getNumbers(query);
|
||
const searchableFields = this.getSearchableFields();
|
||
|
||
// eslint-disable-next-line prettier/prettier
|
||
if (contacts.length === 2 && this.getClientCwConfig().mergeBrazilContacts && query.startsWith('+55')) {
|
||
const contact = this.mergeBrazilianContacts(contacts);
|
||
if (contact) {
|
||
return contact;
|
||
}
|
||
}
|
||
|
||
const phone = phoneNumbers.reduce(
|
||
(savedNumber, number) => (number.length > savedNumber.length ? number : savedNumber),
|
||
'',
|
||
);
|
||
|
||
const contact_with9 = contacts.find((contact) => contact.phone_number === phone);
|
||
if (contact_with9) {
|
||
return contact_with9;
|
||
}
|
||
|
||
for (const contact of contacts) {
|
||
for (const field of searchableFields) {
|
||
if (contact[field] && phoneNumbers.includes(contact[field])) {
|
||
return contact;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private getNumbers(query: string) {
|
||
const numbers = [];
|
||
numbers.push(query);
|
||
|
||
if (query.startsWith('+55') && query.length === 14) {
|
||
const withoutNine = query.slice(0, 5) + query.slice(6);
|
||
numbers.push(withoutNine);
|
||
} else if (query.startsWith('+55') && query.length === 13) {
|
||
const withNine = query.slice(0, 5) + '9' + query.slice(5);
|
||
numbers.push(withNine);
|
||
}
|
||
|
||
return numbers;
|
||
}
|
||
|
||
private getSearchableFields() {
|
||
return ['phone_number'];
|
||
}
|
||
|
||
private getFilterPayload(query: string) {
|
||
const filterPayload = [];
|
||
|
||
const numbers = this.getNumbers(query);
|
||
const fieldsToSearch = this.getSearchableFields();
|
||
|
||
fieldsToSearch.forEach((field, index1) => {
|
||
numbers.forEach((number, index2) => {
|
||
const queryOperator = fieldsToSearch.length - 1 === index1 && numbers.length - 1 === index2 ? null : 'OR';
|
||
filterPayload.push({
|
||
attribute_key: field,
|
||
filter_operator: 'equal_to',
|
||
values: [number.replace('+', '')],
|
||
query_operator: queryOperator,
|
||
});
|
||
});
|
||
});
|
||
|
||
return filterPayload;
|
||
}
|
||
|
||
public async createConversation(instance: InstanceDto, body: any) {
|
||
const isLid = body.key.remoteJid.includes('@lid') && body.key.senderPn;
|
||
const remoteJid = isLid ? body.key.senderPn : body.key.remoteJid;
|
||
const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`;
|
||
const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`;
|
||
const maxWaitTime = 5000; // 5 secounds
|
||
|
||
try {
|
||
// Processa atualização de contatos já criados @lid
|
||
if (body.key.remoteJid.includes('@lid') && body.key.senderPn && body.key.senderPn !== body.key.remoteJid) {
|
||
const contact = await this.findContact(instance, body.key.remoteJid.split('@')[0]);
|
||
if (contact && contact.identifier !== body.key.senderPn) {
|
||
this.logger.verbose(
|
||
`Identifier needs update: (contact.identifier: ${contact.identifier}, body.key.remoteJid: ${body.key.remoteJid}, body.key.senderPn: ${body.key.senderPn})`,
|
||
);
|
||
await this.updateContact(instance, contact.id, {
|
||
identifier: body.key.senderPn,
|
||
phone_number: `+${body.key.senderPn.split('@')[0]}`,
|
||
});
|
||
}
|
||
}
|
||
this.logger.verbose(`--- Start createConversation ---`);
|
||
this.logger.verbose(`Instance: ${JSON.stringify(instance)}`);
|
||
|
||
// If it already exists in the cache, return conversationId
|
||
if (await this.cache.has(cacheKey)) {
|
||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||
this.logger.verbose(`Found conversation to: ${remoteJid}, conversation ID: ${conversationId}`);
|
||
return conversationId;
|
||
}
|
||
|
||
// If lock already exists, wait until release or timeout
|
||
if (await this.cache.has(lockKey)) {
|
||
this.logger.verbose(`Operação de criação já em andamento para ${remoteJid}, aguardando resultado...`);
|
||
const start = Date.now();
|
||
while (await this.cache.has(lockKey)) {
|
||
if (Date.now() - start > maxWaitTime) {
|
||
this.logger.warn(`Timeout aguardando lock para ${remoteJid}`);
|
||
break;
|
||
}
|
||
await new Promise((res) => setTimeout(res, 300));
|
||
if (await this.cache.has(cacheKey)) {
|
||
const conversationId = (await this.cache.get(cacheKey)) as number;
|
||
this.logger.verbose(`Resolves creation of: ${remoteJid}, conversation ID: ${conversationId}`);
|
||
return conversationId;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Adquire lock
|
||
await this.cache.set(lockKey, true, 30);
|
||
this.logger.verbose(`Bloqueio adquirido para: ${lockKey}`);
|
||
|
||
try {
|
||
/*
|
||
Double check after lock
|
||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||
*/
|
||
if (await this.cache.has(cacheKey)) {
|
||
return (await this.cache.get(cacheKey)) as number;
|
||
}
|
||
|
||
const client = await this.clientCw(instance);
|
||
if (!client) return null;
|
||
|
||
const isGroup = remoteJid.includes('@g.us');
|
||
const chatId = isGroup ? remoteJid : remoteJid.split('@')[0];
|
||
let nameContact = !body.key.fromMe ? body.pushName : chatId;
|
||
const filterInbox = await this.getInbox(instance);
|
||
if (!filterInbox) return null;
|
||
|
||
if (isGroup) {
|
||
this.logger.verbose(`Processing group conversation`);
|
||
const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId);
|
||
this.logger.verbose(`Group metadata: ${JSON.stringify(group)}`);
|
||
|
||
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,
|
||
isGroup,
|
||
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);
|
||
|
||
if (contact) {
|
||
this.logger.verbose(`Found contact: ${JSON.stringify(contact)}`);
|
||
if (!body.key.fromMe) {
|
||
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 {
|
||
const jid = isLid && body?.key?.senderPn ? body.key.senderPn : body.key.remoteJid;
|
||
contact = await this.createContact(
|
||
instance,
|
||
chatId,
|
||
filterInbox.id,
|
||
isGroup,
|
||
nameContact,
|
||
picture_url.profilePictureUrl || null,
|
||
jid,
|
||
);
|
||
}
|
||
|
||
if (!contact) {
|
||
this.logger.warn(`Contact not created or found`);
|
||
return null;
|
||
}
|
||
|
||
const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id;
|
||
this.logger.verbose(`Contact ID: ${contactId}`);
|
||
|
||
const contactConversations = (await client.contacts.listConversations({
|
||
accountId: this.provider.accountId,
|
||
id: contactId,
|
||
})) as any;
|
||
this.logger.verbose(`Contact conversations: ${JSON.stringify(contactConversations)}`);
|
||
|
||
if (!contactConversations || !contactConversations.payload) {
|
||
this.logger.error(`No conversations found or payload is undefined`);
|
||
return null;
|
||
}
|
||
|
||
let inboxConversation = contactConversations.payload.find(
|
||
(conversation) => conversation.inbox_id == filterInbox.id,
|
||
);
|
||
if (inboxConversation) {
|
||
if (this.provider.reopenConversation) {
|
||
this.logger.verbose(`Found conversation in reopenConversation mode: ${JSON.stringify(inboxConversation)}`);
|
||
if (inboxConversation && this.provider.conversationPending && inboxConversation.status !== 'open') {
|
||
await client.conversations.toggleStatus({
|
||
accountId: this.provider.accountId,
|
||
conversationId: inboxConversation.id,
|
||
data: {
|
||
status: 'pending',
|
||
},
|
||
});
|
||
}
|
||
} else {
|
||
inboxConversation = contactConversations.payload.find(
|
||
(conversation) =>
|
||
conversation && conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id,
|
||
);
|
||
this.logger.verbose(`Found conversation: ${JSON.stringify(inboxConversation)}`);
|
||
}
|
||
|
||
if (inboxConversation) {
|
||
this.logger.verbose(`Returning existing conversation ID: ${inboxConversation.id}`);
|
||
this.cache.set(cacheKey, inboxConversation.id);
|
||
return inboxConversation.id;
|
||
}
|
||
}
|
||
|
||
const data = {
|
||
contact_id: contactId.toString(),
|
||
inbox_id: filterInbox.id.toString(),
|
||
};
|
||
|
||
if (this.provider.conversationPending) {
|
||
data['status'] = 'pending';
|
||
}
|
||
|
||
/*
|
||
Triple check after lock
|
||
Utilizei uma nova verificação para evitar que outra thread execute entre o terminio do while e o set lock
|
||
*/
|
||
if (await this.cache.has(cacheKey)) {
|
||
return (await this.cache.get(cacheKey)) as number;
|
||
}
|
||
|
||
const conversation = await client.conversations.create({
|
||
accountId: this.provider.accountId,
|
||
data,
|
||
});
|
||
|
||
if (!conversation) {
|
||
this.logger.warn(`Conversation not created or found`);
|
||
return null;
|
||
}
|
||
|
||
this.logger.verbose(`New conversation created of ${remoteJid} with ID: ${conversation.id}`);
|
||
this.cache.set(cacheKey, conversation.id);
|
||
return conversation.id;
|
||
} finally {
|
||
await this.cache.delete(lockKey);
|
||
this.logger.verbose(`Block released for: ${lockKey}`);
|
||
}
|
||
} catch (error) {
|
||
this.logger.error(`Error in createConversation: ${error}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
public async getInbox(instance: InstanceDto): Promise<inbox | null> {
|
||
const cacheKey = `${instance.instanceName}:getInbox`;
|
||
if (await this.cache.has(cacheKey)) {
|
||
return (await this.cache.get(cacheKey)) as inbox;
|
||
}
|
||
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
const inbox = (await client.inboxes.list({
|
||
accountId: this.provider.accountId,
|
||
})) as any;
|
||
|
||
if (!inbox) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
const findByName = inbox.payload.find((inbox) => inbox.name === this.getClientCwConfig().nameInbox);
|
||
|
||
if (!findByName) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
this.cache.set(cacheKey, findByName);
|
||
return findByName;
|
||
}
|
||
|
||
public async createMessage(
|
||
instance: InstanceDto,
|
||
conversationId: number,
|
||
content: string,
|
||
messageType: 'incoming' | 'outgoing' | undefined,
|
||
privateMessage?: boolean,
|
||
attachments?: {
|
||
content: unknown;
|
||
encoding: string;
|
||
filename: string;
|
||
}[],
|
||
messageBody?: any,
|
||
sourceId?: string,
|
||
quotedMsg?: MessageModel,
|
||
) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
const replyToIds = await this.getReplyToIds(messageBody, instance);
|
||
|
||
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
||
|
||
const message = await client.messages.create({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversationId,
|
||
data: {
|
||
content: content,
|
||
message_type: messageType,
|
||
attachments: attachments,
|
||
private: privateMessage || false,
|
||
source_id: sourceId,
|
||
content_attributes: {
|
||
...replyToIds,
|
||
},
|
||
source_reply_id: sourceReplyId ? sourceReplyId.toString() : null,
|
||
},
|
||
});
|
||
|
||
if (!message) {
|
||
this.logger.warn('message not found');
|
||
return null;
|
||
}
|
||
|
||
return message;
|
||
}
|
||
|
||
public async getOpenConversationByContact(
|
||
instance: InstanceDto,
|
||
inbox: inbox,
|
||
contact: generic_id & contact,
|
||
): Promise<conversation> {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
const conversations = (await client.contacts.listConversations({
|
||
accountId: this.provider.accountId,
|
||
id: contact.id,
|
||
})) as any;
|
||
|
||
return (
|
||
conversations.payload.find(
|
||
(conversation) => conversation.inbox_id === inbox.id && conversation.status === 'open',
|
||
) || undefined
|
||
);
|
||
}
|
||
|
||
public async createBotMessage(
|
||
instance: InstanceDto,
|
||
content: string,
|
||
messageType: 'incoming' | 'outgoing' | undefined,
|
||
attachments?: {
|
||
content: unknown;
|
||
encoding: string;
|
||
filename: string;
|
||
}[],
|
||
) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
const contact = await this.findContact(instance, '123456');
|
||
|
||
if (!contact) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
const filterInbox = await this.getInbox(instance);
|
||
|
||
if (!filterInbox) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact);
|
||
|
||
if (!conversation) {
|
||
this.logger.warn('conversation not found');
|
||
return;
|
||
}
|
||
|
||
const message = await client.messages.create({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversation.id,
|
||
data: {
|
||
content: content,
|
||
message_type: messageType,
|
||
attachments: attachments,
|
||
},
|
||
});
|
||
|
||
if (!message) {
|
||
this.logger.warn('message not found');
|
||
return null;
|
||
}
|
||
|
||
return message;
|
||
}
|
||
|
||
private async sendData(
|
||
conversationId: number,
|
||
fileStream: Readable,
|
||
fileName: string,
|
||
messageType: 'incoming' | 'outgoing' | undefined,
|
||
content?: string,
|
||
instance?: InstanceDto,
|
||
messageBody?: any,
|
||
sourceId?: string,
|
||
quotedMsg?: MessageModel,
|
||
) {
|
||
if (sourceId && this.isImportHistoryAvailable()) {
|
||
const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId);
|
||
if (messageAlreadySaved) {
|
||
if (messageAlreadySaved.size > 0) {
|
||
this.logger.warn('Message already saved on chatwoot');
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
const data = new FormData();
|
||
|
||
if (content) {
|
||
data.append('content', content);
|
||
}
|
||
|
||
data.append('message_type', messageType);
|
||
|
||
data.append('attachments[]', fileStream, { filename: fileName });
|
||
|
||
const sourceReplyId = quotedMsg?.chatwootMessageId || null;
|
||
|
||
if (messageBody && instance) {
|
||
const replyToIds = await this.getReplyToIds(messageBody, instance);
|
||
|
||
if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) {
|
||
const content = JSON.stringify({
|
||
...replyToIds,
|
||
});
|
||
data.append('content_attributes', content);
|
||
}
|
||
}
|
||
|
||
if (sourceReplyId) {
|
||
data.append('source_reply_id', sourceReplyId.toString());
|
||
}
|
||
|
||
if (sourceId) {
|
||
data.append('source_id', sourceId);
|
||
}
|
||
|
||
const config = {
|
||
method: 'post',
|
||
maxBodyLength: Infinity,
|
||
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`,
|
||
headers: {
|
||
api_access_token: this.provider.token,
|
||
...data.getHeaders(),
|
||
},
|
||
data: data,
|
||
};
|
||
|
||
try {
|
||
const { data } = await axios.request(config);
|
||
|
||
return data;
|
||
} catch (error) {
|
||
this.logger.error(error);
|
||
}
|
||
}
|
||
|
||
public async createBotQr(
|
||
instance: InstanceDto,
|
||
content: string,
|
||
messageType: 'incoming' | 'outgoing' | undefined,
|
||
fileStream?: Readable,
|
||
fileName?: string,
|
||
) {
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
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');
|
||
|
||
if (!contact) {
|
||
this.logger.warn('contact not found');
|
||
return null;
|
||
}
|
||
|
||
const filterInbox = await this.getInbox(instance);
|
||
|
||
if (!filterInbox) {
|
||
this.logger.warn('inbox not found');
|
||
return null;
|
||
}
|
||
|
||
const conversation = await this.getOpenConversationByContact(instance, filterInbox, contact);
|
||
|
||
if (!conversation) {
|
||
this.logger.warn('conversation not found');
|
||
return;
|
||
}
|
||
|
||
const data = new FormData();
|
||
|
||
if (content) {
|
||
data.append('content', content);
|
||
}
|
||
|
||
data.append('message_type', messageType);
|
||
|
||
if (fileStream && fileName) {
|
||
data.append('attachments[]', fileStream, { filename: fileName });
|
||
}
|
||
|
||
const config = {
|
||
method: 'post',
|
||
maxBodyLength: Infinity,
|
||
url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversation.id}/messages`,
|
||
headers: {
|
||
api_access_token: this.provider.token,
|
||
...data.getHeaders(),
|
||
},
|
||
data: data,
|
||
};
|
||
|
||
try {
|
||
const { data } = await axios.request(config);
|
||
|
||
return data;
|
||
} catch (error) {
|
||
this.logger.error(error);
|
||
}
|
||
}
|
||
|
||
public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) {
|
||
try {
|
||
const parsedMedia = path.parse(decodeURIComponent(media));
|
||
let mimeType = mimeTypes.lookup(parsedMedia?.ext) || '';
|
||
let fileName = parsedMedia?.name + parsedMedia?.ext;
|
||
|
||
if (!mimeType) {
|
||
const parts = media.split('/');
|
||
fileName = decodeURIComponent(parts[parts.length - 1]);
|
||
|
||
const response = await axios.get(media, {
|
||
responseType: 'arraybuffer',
|
||
});
|
||
mimeType = response.headers['content-type'];
|
||
}
|
||
|
||
let type = 'document';
|
||
|
||
switch (mimeType.split('/')[0]) {
|
||
case 'image':
|
||
type = 'image';
|
||
break;
|
||
case 'video':
|
||
type = 'video';
|
||
break;
|
||
case 'audio':
|
||
type = 'audio';
|
||
break;
|
||
default:
|
||
type = 'document';
|
||
break;
|
||
}
|
||
|
||
if (type === 'audio') {
|
||
const data: SendAudioDto = {
|
||
number: number,
|
||
audio: media,
|
||
delay: 1200,
|
||
quoted: options?.quoted,
|
||
};
|
||
|
||
sendTelemetry('/message/sendWhatsAppAudio');
|
||
|
||
const messageSent = await waInstance?.audioWhatsapp(data, null, true);
|
||
|
||
return messageSent;
|
||
}
|
||
|
||
const documentExtensions = ['.gif', '.svg', '.tiff', '.tif'];
|
||
if (type === 'image' && parsedMedia && documentExtensions.includes(parsedMedia?.ext)) {
|
||
type = 'document';
|
||
}
|
||
|
||
const data: SendMediaDto = {
|
||
number: number,
|
||
mediatype: type as any,
|
||
fileName: fileName,
|
||
media: media,
|
||
delay: 1200,
|
||
quoted: options?.quoted,
|
||
};
|
||
|
||
sendTelemetry('/message/sendMedia');
|
||
|
||
if (caption) {
|
||
data.caption = caption;
|
||
}
|
||
|
||
const messageSent = await waInstance?.mediaMessage(data, null, true);
|
||
|
||
return messageSent;
|
||
} catch (error) {
|
||
this.logger.error(error);
|
||
}
|
||
}
|
||
|
||
public async onSendMessageError(instance: InstanceDto, conversation: number, error?: any) {
|
||
this.logger.verbose(`onSendMessageError ${JSON.stringify(error)}`);
|
||
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
return;
|
||
}
|
||
|
||
if (error && error?.status === 400 && error?.message[0]?.exists === false) {
|
||
client.messages.create({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversation,
|
||
data: {
|
||
content: `${i18next.t('cw.message.numbernotinwhatsapp')}`,
|
||
message_type: 'outgoing',
|
||
private: true,
|
||
},
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
client.messages.create({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversation,
|
||
data: {
|
||
content: i18next.t('cw.message.notsent', {
|
||
error: error ? `_${error.toString()}_` : '',
|
||
}),
|
||
message_type: 'outgoing',
|
||
private: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
public async receiveWebhook(instance: InstanceDto, body: any) {
|
||
try {
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
if (
|
||
this.provider.reopenConversation === false &&
|
||
body.event === 'conversation_status_changed' &&
|
||
body.status === 'resolved' &&
|
||
body.meta?.sender?.identifier
|
||
) {
|
||
const keyToDelete = `${instance.instanceName}:createConversation-${body.meta.sender.identifier}`;
|
||
this.cache.delete(keyToDelete);
|
||
}
|
||
|
||
if (
|
||
!body?.conversation ||
|
||
body.private ||
|
||
(body.event === 'message_updated' && !body.content_attributes?.deleted)
|
||
) {
|
||
return { message: 'bot' };
|
||
}
|
||
|
||
const chatId =
|
||
body.conversation.meta.sender?.identifier || body.conversation.meta.sender?.phone_number.replace('+', '');
|
||
// Chatwoot to Whatsapp
|
||
const messageReceived = body.content
|
||
? body.content
|
||
.replaceAll(/(?<!\*)\*((?!\s)([^\n*]+?)(?<!\s))\*(?!\*)/g, '_$1_') // Substitui * por _
|
||
.replaceAll(/\*{2}((?!\s)([^\n*]+?)(?<!\s))\*{2}/g, '*$1*') // Substitui ** por *
|
||
.replaceAll(/~{2}((?!\s)([^\n*]+?)(?<!\s))~{2}/g, '~$1~') // Substitui ~~ por ~
|
||
.replaceAll(/(?<!`)`((?!\s)([^`*]+?)(?<!\s))`(?!`)/g, '```$1```') // Substitui ` por ```
|
||
: body.content;
|
||
|
||
const senderName = body?.conversation?.messages[0]?.sender?.available_name || body?.sender?.name;
|
||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||
|
||
if (body.event === 'message_updated' && body.content_attributes?.deleted) {
|
||
const message = await this.prismaRepository.message.findFirst({
|
||
where: {
|
||
chatwootMessageId: body.id,
|
||
instanceId: instance.instanceId,
|
||
},
|
||
});
|
||
|
||
if (message) {
|
||
const key = message.key as {
|
||
id: string;
|
||
remoteJid: string;
|
||
fromMe: boolean;
|
||
participant: string;
|
||
};
|
||
|
||
await waInstance?.client.sendMessage(key.remoteJid, { delete: key });
|
||
|
||
await this.prismaRepository.message.deleteMany({
|
||
where: {
|
||
instanceId: instance.instanceId,
|
||
chatwootMessageId: body.id,
|
||
},
|
||
});
|
||
}
|
||
return { message: 'bot' };
|
||
}
|
||
|
||
const cwBotContact = this.configService.get<Chatwoot>('CHATWOOT').BOT_CONTACT;
|
||
|
||
if (chatId === '123456' && body.message_type === 'outgoing') {
|
||
const command = messageReceived.replace('/', '');
|
||
|
||
if (cwBotContact && (command.includes('init') || command.includes('iniciar'))) {
|
||
const state = waInstance?.connectionStatus?.state;
|
||
|
||
if (state !== 'open') {
|
||
const number = command.split(':')[1];
|
||
await waInstance.connectToWhatsapp(number);
|
||
} else {
|
||
await this.createBotMessage(
|
||
instance,
|
||
i18next.t('cw.inbox.alreadyConnected', {
|
||
inboxName: body.inbox.name,
|
||
}),
|
||
'incoming',
|
||
);
|
||
}
|
||
}
|
||
|
||
if (command === 'clearcache') {
|
||
waInstance.clearCacheChatwoot();
|
||
await this.createBotMessage(
|
||
instance,
|
||
i18next.t('cw.inbox.clearCache', {
|
||
inboxName: body.inbox.name,
|
||
}),
|
||
'incoming',
|
||
);
|
||
}
|
||
|
||
if (command === 'status') {
|
||
const state = waInstance?.connectionStatus?.state;
|
||
|
||
if (!state) {
|
||
await this.createBotMessage(
|
||
instance,
|
||
i18next.t('cw.inbox.notFound', {
|
||
inboxName: body.inbox.name,
|
||
}),
|
||
'incoming',
|
||
);
|
||
}
|
||
|
||
if (state) {
|
||
await this.createBotMessage(
|
||
instance,
|
||
i18next.t('cw.inbox.status', {
|
||
inboxName: body.inbox.name,
|
||
state: state,
|
||
}),
|
||
'incoming',
|
||
);
|
||
}
|
||
}
|
||
|
||
if (cwBotContact && (command === 'disconnect' || command === 'desconectar')) {
|
||
const msgLogout = i18next.t('cw.inbox.disconnect', {
|
||
inboxName: body.inbox.name,
|
||
});
|
||
|
||
await this.createBotMessage(instance, msgLogout, 'incoming');
|
||
|
||
await waInstance?.client?.logout('Log out instance: ' + instance.instanceName);
|
||
await waInstance?.client?.ws?.close();
|
||
}
|
||
}
|
||
|
||
if (body.message_type === 'outgoing' && body?.conversation?.messages?.length && chatId !== '123456') {
|
||
if (body?.conversation?.messages[0]?.source_id?.substring(0, 5) === 'WAID:') {
|
||
return { message: 'bot' };
|
||
}
|
||
|
||
if (!waInstance && body.conversation?.id) {
|
||
this.onSendMessageError(instance, body.conversation?.id, 'Instance not found');
|
||
return { message: 'bot' };
|
||
}
|
||
|
||
let formatText: string;
|
||
if (senderName === null || senderName === undefined) {
|
||
formatText = messageReceived;
|
||
} else {
|
||
const formattedDelimiter = this.provider.signDelimiter
|
||
? this.provider.signDelimiter.replaceAll('\\n', '\n')
|
||
: '\n';
|
||
const textToConcat = this.provider.signMsg ? [`*${senderName}:*`] : [];
|
||
textToConcat.push(messageReceived);
|
||
|
||
formatText = textToConcat.join(formattedDelimiter);
|
||
}
|
||
|
||
for (const message of body.conversation.messages) {
|
||
if (message.attachments && message.attachments.length > 0) {
|
||
for (const attachment of message.attachments) {
|
||
if (!messageReceived) {
|
||
formatText = null;
|
||
}
|
||
|
||
const options: Options = {
|
||
quoted: await this.getQuotedMessage(body, instance),
|
||
};
|
||
|
||
const messageSent = await this.sendAttachment(
|
||
waInstance,
|
||
chatId,
|
||
attachment.data_url,
|
||
formatText,
|
||
options,
|
||
);
|
||
if (!messageSent && body.conversation?.id) {
|
||
this.onSendMessageError(instance, body.conversation?.id);
|
||
}
|
||
|
||
await this.updateChatwootMessageId(
|
||
{
|
||
...messageSent,
|
||
owner: instance.instanceName,
|
||
},
|
||
{
|
||
messageId: body.id,
|
||
inboxId: body.inbox?.id,
|
||
conversationId: body.conversation?.id,
|
||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
||
},
|
||
instance,
|
||
);
|
||
}
|
||
} else {
|
||
const data: SendTextDto = {
|
||
number: chatId,
|
||
text: formatText,
|
||
delay: 1200,
|
||
quoted: await this.getQuotedMessage(body, instance),
|
||
};
|
||
|
||
sendTelemetry('/message/sendText');
|
||
|
||
let messageSent: any;
|
||
try {
|
||
messageSent = await waInstance?.textMessage(data, true);
|
||
if (!messageSent) {
|
||
throw new Error('Message not sent');
|
||
}
|
||
|
||
if (Long.isLong(messageSent?.messageTimestamp)) {
|
||
messageSent.messageTimestamp = messageSent.messageTimestamp?.toNumber();
|
||
}
|
||
|
||
await this.updateChatwootMessageId(
|
||
{
|
||
...messageSent,
|
||
instanceId: instance.instanceId,
|
||
},
|
||
{
|
||
messageId: body.id,
|
||
inboxId: body.inbox?.id,
|
||
conversationId: body.conversation?.id,
|
||
contactInboxSourceId: body.conversation?.contact_inbox?.source_id,
|
||
},
|
||
instance,
|
||
);
|
||
} catch (error) {
|
||
if (!messageSent && body.conversation?.id) {
|
||
this.onSendMessageError(instance, body.conversation?.id, error);
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
const chatwootRead = this.configService.get<Chatwoot>('CHATWOOT').MESSAGE_READ;
|
||
if (chatwootRead) {
|
||
const lastMessage = await this.prismaRepository.message.findFirst({
|
||
where: {
|
||
key: {
|
||
path: ['fromMe'],
|
||
equals: false,
|
||
},
|
||
instanceId: instance.instanceId,
|
||
},
|
||
});
|
||
if (lastMessage && !lastMessage.chatwootIsRead) {
|
||
const key = lastMessage.key as {
|
||
id: string;
|
||
fromMe: boolean;
|
||
remoteJid: string;
|
||
participant?: string;
|
||
};
|
||
|
||
waInstance?.markMessageAsRead({
|
||
readMessages: [
|
||
{
|
||
id: key.id,
|
||
fromMe: key.fromMe,
|
||
remoteJid: key.remoteJid,
|
||
},
|
||
],
|
||
});
|
||
const updateMessage = {
|
||
chatwootMessageId: lastMessage.chatwootMessageId,
|
||
chatwootConversationId: lastMessage.chatwootConversationId,
|
||
chatwootInboxId: lastMessage.chatwootInboxId,
|
||
chatwootContactInboxSourceId: lastMessage.chatwootContactInboxSourceId,
|
||
chatwootIsRead: true,
|
||
};
|
||
|
||
await this.prismaRepository.message.updateMany({
|
||
where: {
|
||
instanceId: instance.instanceId,
|
||
key: {
|
||
path: ['id'],
|
||
equals: key.id,
|
||
},
|
||
},
|
||
data: updateMessage,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (body.message_type === 'template' && body.event === 'message_created') {
|
||
const data: SendTextDto = {
|
||
number: chatId,
|
||
text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'),
|
||
delay: 1200,
|
||
};
|
||
|
||
sendTelemetry('/message/sendText');
|
||
|
||
await waInstance?.textMessage(data);
|
||
}
|
||
|
||
return { message: 'bot' };
|
||
} catch (error) {
|
||
this.logger.error(error);
|
||
|
||
return { message: 'bot' };
|
||
}
|
||
}
|
||
|
||
private async updateChatwootMessageId(
|
||
message: MessageModel,
|
||
chatwootMessageIds: ChatwootMessage,
|
||
instance: InstanceDto,
|
||
) {
|
||
const key = message.key as {
|
||
id: string;
|
||
fromMe: boolean;
|
||
remoteJid: string;
|
||
participant?: string;
|
||
};
|
||
|
||
if (!chatwootMessageIds.messageId || !key?.id) {
|
||
return;
|
||
}
|
||
|
||
await this.prismaRepository.message.updateMany({
|
||
where: {
|
||
key: {
|
||
path: ['id'],
|
||
equals: key.id,
|
||
},
|
||
instanceId: instance.instanceId,
|
||
},
|
||
data: {
|
||
chatwootMessageId: chatwootMessageIds.messageId,
|
||
chatwootConversationId: chatwootMessageIds.conversationId,
|
||
chatwootInboxId: chatwootMessageIds.inboxId,
|
||
chatwootContactInboxSourceId: chatwootMessageIds.contactInboxSourceId,
|
||
chatwootIsRead: chatwootMessageIds.isRead,
|
||
},
|
||
});
|
||
|
||
if (this.isImportHistoryAvailable()) {
|
||
chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id);
|
||
}
|
||
}
|
||
|
||
private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise<MessageModel> {
|
||
const messages = await this.prismaRepository.message.findFirst({
|
||
where: {
|
||
key: {
|
||
path: ['id'],
|
||
equals: keyId,
|
||
},
|
||
instanceId: instance.instanceId,
|
||
},
|
||
});
|
||
|
||
return messages || null;
|
||
}
|
||
|
||
private async getReplyToIds(
|
||
msg: any,
|
||
instance: InstanceDto,
|
||
): Promise<{ in_reply_to: string; in_reply_to_external_id: string }> {
|
||
let inReplyTo = null;
|
||
let inReplyToExternalId = null;
|
||
|
||
if (msg) {
|
||
inReplyToExternalId = msg.message?.extendedTextMessage?.contextInfo?.stanzaId ?? msg.contextInfo?.stanzaId;
|
||
if (inReplyToExternalId) {
|
||
const message = await this.getMessageByKeyId(instance, inReplyToExternalId);
|
||
if (message?.chatwootMessageId) {
|
||
inReplyTo = message.chatwootMessageId;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
in_reply_to: inReplyTo,
|
||
in_reply_to_external_id: inReplyToExternalId,
|
||
};
|
||
}
|
||
|
||
private async getQuotedMessage(msg: any, instance: InstanceDto): Promise<Quoted> {
|
||
if (msg?.content_attributes?.in_reply_to) {
|
||
const message = await this.prismaRepository.message.findFirst({
|
||
where: {
|
||
chatwootMessageId: msg?.content_attributes?.in_reply_to,
|
||
instanceId: instance.instanceId,
|
||
},
|
||
});
|
||
|
||
const key = message?.key as {
|
||
id: string;
|
||
fromMe: boolean;
|
||
remoteJid: string;
|
||
participant?: string;
|
||
};
|
||
|
||
if (message && key?.id) {
|
||
return {
|
||
key: message.key as proto.IMessageKey,
|
||
message: message.message as proto.IMessage,
|
||
};
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private isMediaMessage(message: any) {
|
||
const media = [
|
||
'imageMessage',
|
||
'documentMessage',
|
||
'documentWithCaptionMessage',
|
||
'audioMessage',
|
||
'videoMessage',
|
||
'stickerMessage',
|
||
'viewOnceMessageV2',
|
||
];
|
||
|
||
const messageKeys = Object.keys(message);
|
||
|
||
const result = messageKeys.some((key) => media.includes(key));
|
||
|
||
return result;
|
||
}
|
||
|
||
private getAdsMessage(msg: any) {
|
||
interface AdsMessage {
|
||
title: string;
|
||
body: string;
|
||
thumbnailUrl: string;
|
||
sourceUrl: string;
|
||
}
|
||
|
||
const adsMessage: AdsMessage | undefined = {
|
||
title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title || msg.contextInfo?.externalAdReply?.title,
|
||
body: msg.extendedTextMessage?.contextInfo?.externalAdReply?.body || msg.contextInfo?.externalAdReply?.body,
|
||
thumbnailUrl:
|
||
msg.extendedTextMessage?.contextInfo?.externalAdReply?.thumbnailUrl ||
|
||
msg.contextInfo?.externalAdReply?.thumbnailUrl,
|
||
sourceUrl:
|
||
msg.extendedTextMessage?.contextInfo?.externalAdReply?.sourceUrl || msg.contextInfo?.externalAdReply?.sourceUrl,
|
||
};
|
||
|
||
return adsMessage;
|
||
}
|
||
|
||
private getReactionMessage(msg: any) {
|
||
interface ReactionMessage {
|
||
key: {
|
||
id: string;
|
||
fromMe: boolean;
|
||
remoteJid: string;
|
||
participant?: string;
|
||
};
|
||
text: string;
|
||
}
|
||
const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage;
|
||
|
||
return reactionMessage;
|
||
}
|
||
|
||
private getTypeMessage(msg: any) {
|
||
const types = {
|
||
conversation: msg.conversation,
|
||
imageMessage: msg.imageMessage?.caption,
|
||
videoMessage: msg.videoMessage?.caption,
|
||
extendedTextMessage: msg.extendedTextMessage?.text,
|
||
messageContextInfo: msg.messageContextInfo?.stanzaId,
|
||
stickerMessage: undefined,
|
||
documentMessage: msg.documentMessage?.caption,
|
||
documentWithCaptionMessage: msg.documentWithCaptionMessage?.message?.documentMessage?.caption,
|
||
audioMessage: msg.audioMessage ? (msg.audioMessage.caption ?? '') : undefined,
|
||
contactMessage: msg.contactMessage?.vcard,
|
||
contactsArrayMessage: msg.contactsArrayMessage,
|
||
locationMessage: msg.locationMessage,
|
||
liveLocationMessage: msg.liveLocationMessage,
|
||
listMessage: msg.listMessage,
|
||
listResponseMessage: msg.listResponseMessage,
|
||
viewOnceMessageV2:
|
||
msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url ||
|
||
msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url ||
|
||
msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url,
|
||
};
|
||
|
||
return types;
|
||
}
|
||
|
||
private getMessageContent(types: any) {
|
||
const typeKey = Object.keys(types).find((key) => types[key] !== undefined);
|
||
|
||
let result = typeKey ? types[typeKey] : undefined;
|
||
|
||
// Remove externalAdReplyBody| in Chatwoot (Already Have)
|
||
if (result && typeof result === 'string' && result.includes('externalAdReplyBody|')) {
|
||
result = result.split('externalAdReplyBody|').filter(Boolean).join('');
|
||
}
|
||
|
||
if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') {
|
||
const latitude = result.degreesLatitude;
|
||
const longitude = result.degreesLongitude;
|
||
|
||
const locationName = result?.name;
|
||
const locationAddress = result?.address;
|
||
|
||
const formattedLocation =
|
||
`*${i18next.t('cw.locationMessage.location')}:*\n\n` +
|
||
`_${i18next.t('cw.locationMessage.latitude')}:_ ${latitude} \n` +
|
||
`_${i18next.t('cw.locationMessage.longitude')}:_ ${longitude} \n` +
|
||
(locationName ? `_${i18next.t('cw.locationMessage.locationName')}:_ ${locationName}\n` : '') +
|
||
(locationAddress ? `_${i18next.t('cw.locationMessage.locationAddress')}:_ ${locationAddress} \n` : '') +
|
||
`_${i18next.t('cw.locationMessage.locationUrl')}:_ ` +
|
||
`https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
|
||
|
||
return formattedLocation;
|
||
}
|
||
|
||
if (typeKey === 'contactMessage') {
|
||
const vCardData = result.split('\n');
|
||
const contactInfo = {};
|
||
|
||
vCardData.forEach((line) => {
|
||
const [key, value] = line.split(':');
|
||
if (key && value) {
|
||
contactInfo[key] = value;
|
||
}
|
||
});
|
||
|
||
let formattedContact =
|
||
`*${i18next.t('cw.contactMessage.contact')}:*\n\n` +
|
||
`_${i18next.t('cw.contactMessage.name')}:_ ${contactInfo['FN']}`;
|
||
|
||
let numberCount = 1;
|
||
Object.keys(contactInfo).forEach((key) => {
|
||
if (key.startsWith('item') && key.includes('TEL')) {
|
||
const phoneNumber = contactInfo[key];
|
||
formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`;
|
||
numberCount++;
|
||
} else if (key.includes('TEL')) {
|
||
const phoneNumber = contactInfo[key];
|
||
formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`;
|
||
numberCount++;
|
||
}
|
||
});
|
||
|
||
return formattedContact;
|
||
}
|
||
|
||
if (typeKey === 'contactsArrayMessage') {
|
||
const formattedContacts = result.contacts.map((contact) => {
|
||
const vCardData = contact.vcard.split('\n');
|
||
const contactInfo = {};
|
||
|
||
vCardData.forEach((line) => {
|
||
const [key, value] = line.split(':');
|
||
if (key && value) {
|
||
contactInfo[key] = value;
|
||
}
|
||
});
|
||
|
||
let formattedContact = `*${i18next.t('cw.contactMessage.contact')}:*\n\n_${i18next.t(
|
||
'cw.contactMessage.name',
|
||
)}:_ ${contact.displayName}`;
|
||
|
||
let numberCount = 1;
|
||
Object.keys(contactInfo).forEach((key) => {
|
||
if (key.startsWith('item') && key.includes('TEL')) {
|
||
const phoneNumber = contactInfo[key];
|
||
formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`;
|
||
numberCount++;
|
||
} else if (key.includes('TEL')) {
|
||
const phoneNumber = contactInfo[key];
|
||
formattedContact += `\n_${i18next.t('cw.contactMessage.number')} (${numberCount}):_ ${phoneNumber}`;
|
||
numberCount++;
|
||
}
|
||
});
|
||
|
||
return formattedContact;
|
||
});
|
||
|
||
const formattedContactsArray = formattedContacts.join('\n\n');
|
||
|
||
return formattedContactsArray;
|
||
}
|
||
|
||
if (typeKey === 'listMessage') {
|
||
const listTitle = result?.title || 'Unknown';
|
||
const listDescription = result?.description || 'Unknown';
|
||
const listFooter = result?.footerText || 'Unknown';
|
||
|
||
let formattedList =
|
||
'*List Menu:*\n\n' +
|
||
'_Title_: ' +
|
||
listTitle +
|
||
'\n' +
|
||
'_Description_: ' +
|
||
listDescription +
|
||
'\n' +
|
||
'_Footer_: ' +
|
||
listFooter;
|
||
|
||
if (result.sections && result.sections.length > 0) {
|
||
result.sections.forEach((section, sectionIndex) => {
|
||
formattedList += '\n\n*Section ' + (sectionIndex + 1) + ':* ' + section.title || 'Unknown\n';
|
||
|
||
if (section.rows && section.rows.length > 0) {
|
||
section.rows.forEach((row, rowIndex) => {
|
||
formattedList += '\n*Line ' + (rowIndex + 1) + ':*\n';
|
||
formattedList += '_▪️ Title:_ ' + (row.title || 'Unknown') + '\n';
|
||
formattedList += '_▪️ Description:_ ' + (row.description || 'Unknown') + '\n';
|
||
formattedList += '_▪️ ID:_ ' + (row.rowId || 'Unknown') + '\n';
|
||
});
|
||
} else {
|
||
formattedList += '\nNo lines found in this section.\n';
|
||
}
|
||
});
|
||
} else {
|
||
formattedList += '\nNo sections found.\n';
|
||
}
|
||
|
||
return formattedList;
|
||
}
|
||
|
||
if (typeKey === 'listResponseMessage') {
|
||
const responseTitle = result?.title || 'Unknown';
|
||
const responseDescription = result?.description || 'Unknown';
|
||
const responseRowId = result?.singleSelectReply?.selectedRowId || 'Unknown';
|
||
|
||
const formattedResponseList =
|
||
'*List Response:*\n\n' +
|
||
'_Title_: ' +
|
||
responseTitle +
|
||
'\n' +
|
||
'_Description_: ' +
|
||
responseDescription +
|
||
'\n' +
|
||
'_ID_: ' +
|
||
responseRowId;
|
||
return formattedResponseList;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
public getConversationMessage(msg: any) {
|
||
const types = this.getTypeMessage(msg);
|
||
|
||
const messageContent = this.getMessageContent(types);
|
||
|
||
return messageContent;
|
||
}
|
||
|
||
public async eventWhatsapp(event: string, instance: InstanceDto, body: any) {
|
||
try {
|
||
const waInstance = this.waMonitor.waInstances[instance.instanceName];
|
||
|
||
if (!waInstance) {
|
||
this.logger.warn('wa instance not found');
|
||
return null;
|
||
}
|
||
|
||
const client = await this.clientCw(instance);
|
||
|
||
if (!client) {
|
||
this.logger.warn('client not found');
|
||
return null;
|
||
}
|
||
|
||
if (this.provider?.ignoreJids && this.provider?.ignoreJids.length > 0) {
|
||
const ignoreJids: any = this.provider?.ignoreJids;
|
||
|
||
let ignoreGroups = false;
|
||
let ignoreContacts = false;
|
||
|
||
if (ignoreJids.includes('@g.us')) {
|
||
ignoreGroups = true;
|
||
}
|
||
|
||
if (ignoreJids.includes('@s.whatsapp.net')) {
|
||
ignoreContacts = true;
|
||
}
|
||
|
||
if (ignoreGroups && body?.key?.remoteJid.endsWith('@g.us')) {
|
||
this.logger.warn('Ignoring message from group: ' + body?.key?.remoteJid);
|
||
return;
|
||
}
|
||
|
||
if (ignoreContacts && body?.key?.remoteJid.endsWith('@s.whatsapp.net')) {
|
||
this.logger.warn('Ignoring message from contact: ' + body?.key?.remoteJid);
|
||
return;
|
||
}
|
||
|
||
if (ignoreJids.includes(body?.key?.remoteJid)) {
|
||
this.logger.warn('Ignoring message from jid: ' + body?.key?.remoteJid);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (event === 'messages.upsert' || event === 'send.message') {
|
||
if (body.key.remoteJid === 'status@broadcast') {
|
||
return;
|
||
}
|
||
|
||
if (body.message?.ephemeralMessage?.message) {
|
||
body.message = {
|
||
...body.message?.ephemeralMessage?.message,
|
||
};
|
||
}
|
||
|
||
const originalMessage = await this.getConversationMessage(body.message);
|
||
const bodyMessage = originalMessage
|
||
? originalMessage
|
||
.replaceAll(/\*((?!\s)([^\n*]+?)(?<!\s))\*/g, '**$1**')
|
||
.replaceAll(/_((?!\s)([^\n_]+?)(?<!\s))_/g, '*$1*')
|
||
.replaceAll(/~((?!\s)([^\n~]+?)(?<!\s))~/g, '~~$1~~')
|
||
: originalMessage;
|
||
|
||
if (bodyMessage && bodyMessage.includes('/survey/responses/') && bodyMessage.includes('http')) {
|
||
return;
|
||
}
|
||
|
||
const quotedId = body.contextInfo?.stanzaId || body.message?.contextInfo?.stanzaId;
|
||
|
||
let quotedMsg = null;
|
||
|
||
if (quotedId)
|
||
quotedMsg = await this.prismaRepository.message.findFirst({
|
||
where: {
|
||
key: {
|
||
path: ['id'],
|
||
equals: quotedId,
|
||
},
|
||
chatwootMessageId: {
|
||
not: null,
|
||
},
|
||
},
|
||
});
|
||
|
||
const isMedia = this.isMediaMessage(body.message);
|
||
|
||
const adsMessage = this.getAdsMessage(body);
|
||
|
||
const reactionMessage = this.getReactionMessage(body.message);
|
||
|
||
if (!bodyMessage && !isMedia && !reactionMessage) {
|
||
this.logger.warn('no body message found');
|
||
return;
|
||
}
|
||
|
||
const getConversation = await this.createConversation(instance, body);
|
||
|
||
if (!getConversation) {
|
||
this.logger.warn('conversation not found');
|
||
return;
|
||
}
|
||
|
||
const messageType = body.key.fromMe ? 'outgoing' : 'incoming';
|
||
|
||
if (isMedia) {
|
||
const downloadBase64 = await waInstance?.getBase64FromMediaMessage({
|
||
message: {
|
||
...body,
|
||
},
|
||
});
|
||
|
||
let nameFile: string;
|
||
const messageBody = body?.message[body?.messageType];
|
||
const originalFilename =
|
||
messageBody?.fileName || messageBody?.filename || messageBody?.message?.documentMessage?.fileName;
|
||
if (originalFilename) {
|
||
const parsedFile = path.parse(originalFilename);
|
||
if (parsedFile.name && parsedFile.ext) {
|
||
nameFile = `${parsedFile.name}-${Math.floor(Math.random() * (99 - 10 + 1) + 10)}${parsedFile.ext}`;
|
||
}
|
||
}
|
||
|
||
if (!nameFile) {
|
||
nameFile = `${Math.random().toString(36).substring(7)}.${mimeTypes.extension(downloadBase64.mimetype) || ''}`;
|
||
}
|
||
|
||
const fileData = Buffer.from(downloadBase64.base64, 'base64');
|
||
|
||
const fileStream = new Readable();
|
||
fileStream._read = () => {};
|
||
fileStream.push(fileData);
|
||
fileStream.push(null);
|
||
|
||
if (body.key.remoteJid.includes('@g.us')) {
|
||
const participantName = body.pushName;
|
||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||
|
||
let formattedPhoneNumber: string;
|
||
|
||
if (phoneMatch) {
|
||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||
} else {
|
||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||
}
|
||
|
||
let content: string;
|
||
|
||
if (!body.key.fromMe) {
|
||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||
} else {
|
||
content = `${bodyMessage}`;
|
||
}
|
||
|
||
const send = await this.sendData(
|
||
getConversation,
|
||
fileStream,
|
||
nameFile,
|
||
messageType,
|
||
content,
|
||
instance,
|
||
body,
|
||
'WAID:' + body.key.id,
|
||
quotedMsg,
|
||
);
|
||
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
|
||
return send;
|
||
} else {
|
||
const send = await this.sendData(
|
||
getConversation,
|
||
fileStream,
|
||
nameFile,
|
||
messageType,
|
||
bodyMessage,
|
||
instance,
|
||
body,
|
||
'WAID:' + body.key.id,
|
||
quotedMsg,
|
||
);
|
||
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
|
||
return send;
|
||
}
|
||
}
|
||
|
||
if (reactionMessage) {
|
||
if (reactionMessage.text) {
|
||
const send = await this.createMessage(
|
||
instance,
|
||
getConversation,
|
||
reactionMessage.text,
|
||
messageType,
|
||
false,
|
||
[],
|
||
{
|
||
message: { extendedTextMessage: { contextInfo: { stanzaId: reactionMessage.key.id } } },
|
||
},
|
||
'WAID:' + body.key.id,
|
||
quotedMsg,
|
||
);
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl;
|
||
if (isAdsMessage) {
|
||
const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' });
|
||
|
||
const extension = mimeTypes.extension(imgBuffer.headers['content-type']);
|
||
const mimeType = extension && mimeTypes.lookup(extension);
|
||
|
||
if (!mimeType) {
|
||
this.logger.warn('mimetype of Ads message not found');
|
||
return;
|
||
}
|
||
|
||
const random = Math.random().toString(36).substring(7);
|
||
const nameFile = `${random}.${mimeTypes.extension(mimeType)}`;
|
||
const fileData = Buffer.from(imgBuffer.data, 'binary');
|
||
|
||
const img = await Jimp.read(fileData);
|
||
await img.cover(320, 180);
|
||
|
||
const processedBuffer = await img.getBufferAsync(Jimp.MIME_PNG);
|
||
|
||
const fileStream = new Readable();
|
||
fileStream._read = () => {}; // _read is required but you can noop it
|
||
fileStream.push(processedBuffer);
|
||
fileStream.push(null);
|
||
|
||
const truncStr = (str: string, len: number) => {
|
||
if (!str) return '';
|
||
|
||
return str.length > len ? str.substring(0, len) + '...' : str;
|
||
};
|
||
|
||
const title = truncStr(adsMessage.title, 40);
|
||
const description = truncStr(adsMessage?.body, 75);
|
||
|
||
const send = await this.sendData(
|
||
getConversation,
|
||
fileStream,
|
||
nameFile,
|
||
messageType,
|
||
`${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`,
|
||
instance,
|
||
body,
|
||
'WAID:' + body.key.id,
|
||
);
|
||
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
|
||
return send;
|
||
}
|
||
|
||
if (body.key.remoteJid.includes('@g.us')) {
|
||
const participantName = body.pushName;
|
||
const rawPhoneNumber = body.key.participant.split('@')[0];
|
||
const phoneMatch = rawPhoneNumber.match(/^(\d{2})(\d{2})(\d{4})(\d{4})$/);
|
||
|
||
let formattedPhoneNumber: string;
|
||
|
||
if (phoneMatch) {
|
||
formattedPhoneNumber = `+${phoneMatch[1]} (${phoneMatch[2]}) ${phoneMatch[3]}-${phoneMatch[4]}`;
|
||
} else {
|
||
formattedPhoneNumber = `+${rawPhoneNumber}`;
|
||
}
|
||
|
||
let content: string;
|
||
|
||
if (!body.key.fromMe) {
|
||
content = `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}`;
|
||
} else {
|
||
content = `${bodyMessage}`;
|
||
}
|
||
|
||
const send = await this.createMessage(
|
||
instance,
|
||
getConversation,
|
||
content,
|
||
messageType,
|
||
false,
|
||
[],
|
||
body,
|
||
'WAID:' + body.key.id,
|
||
quotedMsg,
|
||
);
|
||
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
|
||
return send;
|
||
} else {
|
||
const send = await this.createMessage(
|
||
instance,
|
||
getConversation,
|
||
bodyMessage,
|
||
messageType,
|
||
false,
|
||
[],
|
||
body,
|
||
'WAID:' + body.key.id,
|
||
quotedMsg,
|
||
);
|
||
|
||
if (!send) {
|
||
this.logger.warn('message not sent');
|
||
return;
|
||
}
|
||
|
||
return send;
|
||
}
|
||
}
|
||
|
||
if (event === Events.MESSAGES_DELETE) {
|
||
const chatwootDelete = this.configService.get<Chatwoot>('CHATWOOT').MESSAGE_DELETE;
|
||
|
||
if (chatwootDelete === true) {
|
||
if (!body?.key?.id) {
|
||
this.logger.warn('message id not found');
|
||
return;
|
||
}
|
||
|
||
const message = await this.getMessageByKeyId(instance, body.key.id);
|
||
|
||
if (message?.chatwootMessageId && message?.chatwootConversationId) {
|
||
await this.prismaRepository.message.deleteMany({
|
||
where: {
|
||
key: {
|
||
path: ['id'],
|
||
equals: body.key.id,
|
||
},
|
||
instanceId: instance.instanceId,
|
||
},
|
||
});
|
||
|
||
return await client.messages.delete({
|
||
accountId: this.provider.accountId,
|
||
conversationId: message.chatwootConversationId,
|
||
messageId: message.chatwootMessageId,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (event === 'messages.edit' || event === 'send.message.update') {
|
||
const editedText = `${
|
||
body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text
|
||
}\n\n_\`${i18next.t('cw.message.edited')}.\`_`;
|
||
const message = await this.getMessageByKeyId(instance, body?.key?.id);
|
||
const key = message.key as {
|
||
id: string;
|
||
fromMe: boolean;
|
||
remoteJid: string;
|
||
participant?: string;
|
||
};
|
||
|
||
const messageType = key?.fromMe ? 'outgoing' : 'incoming';
|
||
|
||
if (message && message.chatwootConversationId) {
|
||
const send = await this.createMessage(
|
||
instance,
|
||
message.chatwootConversationId,
|
||
editedText,
|
||
messageType,
|
||
false,
|
||
[],
|
||
{
|
||
message: { extendedTextMessage: { contextInfo: { stanzaId: key.id } } },
|
||
},
|
||
'WAID:' + body.key.id,
|
||
null,
|
||
);
|
||
if (!send) {
|
||
this.logger.warn('edited message not sent');
|
||
return;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (event === 'messages.read') {
|
||
if (!body?.key?.id || !body?.key?.remoteJid) {
|
||
this.logger.warn('message id not found');
|
||
return;
|
||
}
|
||
|
||
const message = await this.getMessageByKeyId(instance, body.key.id);
|
||
const conversationId = message?.chatwootConversationId;
|
||
const contactInboxSourceId = message?.chatwootContactInboxSourceId;
|
||
|
||
if (conversationId) {
|
||
let sourceId = contactInboxSourceId;
|
||
const inbox = (await this.getInbox(instance)) as inbox & {
|
||
inbox_identifier?: string;
|
||
};
|
||
|
||
if (!sourceId && inbox) {
|
||
const conversation = (await client.conversations.get({
|
||
accountId: this.provider.accountId,
|
||
conversationId: conversationId,
|
||
})) as conversation_show & {
|
||
last_non_activity_message: { conversation: { contact_inbox: contact_inboxes } };
|
||
};
|
||
sourceId = conversation.last_non_activity_message?.conversation?.contact_inbox?.source_id;
|
||
}
|
||
|
||
if (sourceId && inbox?.inbox_identifier) {
|
||
const url =
|
||
`/public/api/v1/inboxes/${inbox.inbox_identifier}/contacts/${sourceId}` +
|
||
`/conversations/${conversationId}/update_last_seen`;
|
||
chatwootRequest(this.getClientCwConfig(), {
|
||
method: 'POST',
|
||
url: url,
|
||
});
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (event === 'status.instance') {
|
||
const data = body;
|
||
const inbox = await this.getInbox(instance);
|
||
|
||
if (!inbox) {
|
||
this.logger.warn('inbox not found');
|
||
return;
|
||
}
|
||
|
||
const msgStatus = i18next.t('cw.inbox.status', {
|
||
inboxName: inbox.name,
|
||
state: data.status,
|
||
});
|
||
|
||
await this.createBotMessage(instance, msgStatus, 'incoming');
|
||
}
|
||
|
||
if (event === 'connection.update') {
|
||
if (body.status === 'open') {
|
||
// if we have qrcode count then we understand that a new connection was established
|
||
if (this.waMonitor.waInstances[instance.instanceName].qrCode.count > 0) {
|
||
const msgConnection = i18next.t('cw.inbox.connected');
|
||
await this.createBotMessage(instance, msgConnection, 'incoming');
|
||
this.waMonitor.waInstances[instance.instanceName].qrCode.count = 0;
|
||
chatwootImport.clearAll(instance);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (event === 'qrcode.updated') {
|
||
if (body.statusCode === 500) {
|
||
const erroQRcode = `🚨 ${i18next.t('qrlimitreached')}`;
|
||
return await this.createBotMessage(instance, erroQRcode, 'incoming');
|
||
} else {
|
||
const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64');
|
||
|
||
const fileStream = new Readable();
|
||
fileStream._read = () => {};
|
||
fileStream.push(fileData);
|
||
fileStream.push(null);
|
||
|
||
await this.createBotQr(
|
||
instance,
|
||
i18next.t('qrgeneratedsuccesfully'),
|
||
'incoming',
|
||
fileStream,
|
||
`${instance.instanceName}.png`,
|
||
);
|
||
|
||
let msgQrCode = `⚡️${i18next.t('qrgeneratedsuccesfully')}\n\n${i18next.t('scanqr')}`;
|
||
|
||
if (body?.qrcode?.pairingCode) {
|
||
msgQrCode =
|
||
msgQrCode +
|
||
`\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring(
|
||
4,
|
||
8,
|
||
)}`;
|
||
}
|
||
|
||
await this.createBotMessage(instance, msgQrCode, 'incoming');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
this.logger.error(error);
|
||
}
|
||
}
|
||
|
||
public getNumberFromRemoteJid(remoteJid: string) {
|
||
return remoteJid.replace(/:\d+/, '').split('@')[0];
|
||
}
|
||
|
||
public startImportHistoryMessages(instance: InstanceDto) {
|
||
if (!this.isImportHistoryAvailable()) {
|
||
return;
|
||
}
|
||
|
||
this.createBotMessage(instance, i18next.t('cw.import.startImport'), 'incoming');
|
||
}
|
||
|
||
public isImportHistoryAvailable() {
|
||
const uri = this.configService.get<Chatwoot>('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI;
|
||
|
||
return uri && uri !== 'postgres://user:password@hostname:port/dbname';
|
||
}
|
||
|
||
public addHistoryMessages(instance: InstanceDto, messagesRaw: MessageModel[]) {
|
||
if (!this.isImportHistoryAvailable()) {
|
||
return;
|
||
}
|
||
|
||
chatwootImport.addHistoryMessages(instance, messagesRaw);
|
||
}
|
||
|
||
public addHistoryContacts(instance: InstanceDto, contactsRaw: ContactModel[]) {
|
||
if (!this.isImportHistoryAvailable()) {
|
||
return;
|
||
}
|
||
|
||
return chatwootImport.addHistoryContacts(instance, contactsRaw);
|
||
}
|
||
|
||
public async importHistoryMessages(instance: InstanceDto) {
|
||
if (!this.isImportHistoryAvailable()) {
|
||
return;
|
||
}
|
||
|
||
this.createBotMessage(instance, i18next.t('cw.import.importingMessages'), 'incoming');
|
||
|
||
const totalMessagesImported = await chatwootImport.importHistoryMessages(
|
||
instance,
|
||
this,
|
||
await this.getInbox(instance),
|
||
this.provider,
|
||
);
|
||
this.updateContactAvatarInRecentConversations(instance);
|
||
|
||
const msg = Number.isInteger(totalMessagesImported)
|
||
? i18next.t('cw.import.messagesImported', { totalMessagesImported })
|
||
: i18next.t('cw.import.messagesException');
|
||
|
||
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 contactIdentifiers = recentContacts
|
||
.map((contact) => contact.identifier)
|
||
.filter((identifier) => identifier !== null);
|
||
|
||
const contactsWithProfilePicture = (
|
||
await this.prismaRepository.contact.findMany({
|
||
where: {
|
||
instanceId: instance.instanceId,
|
||
id: {
|
||
in: contactIdentifiers,
|
||
},
|
||
profilePicUrl: {
|
||
not: null,
|
||
},
|
||
},
|
||
})
|
||
).reduce((acc: Map<string, ContactModel>, contact: ContactModel) => acc.set(contact.id, contact), new Map());
|
||
|
||
recentContacts.forEach(async (contact) => {
|
||
if (contactsWithProfilePicture.has(contact.identifier)) {
|
||
client.contacts.update({
|
||
accountId: this.provider.accountId,
|
||
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()}`);
|
||
}
|
||
}
|
||
|
||
public async syncLostMessages(
|
||
instance: InstanceDto,
|
||
chatwootConfig: ChatwootDto,
|
||
prepareMessage: (message: any) => any,
|
||
) {
|
||
try {
|
||
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();
|
||
} catch (error) {
|
||
return;
|
||
}
|
||
}
|
||
}
|