Spaces:
Runtime error
Runtime error
| 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, JimpMime } 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 mergeContacts(baseId: number, mergeId: number) { | |
| try { | |
| const contact = await chatwootRequest(this.getClientCwConfig(), { | |
| method: 'POST', | |
| url: `/api/v1/accounts/${this.provider.accountId}/actions/contact_merge`, | |
| body: { | |
| base_contact_id: baseId, | |
| mergee_contact_id: mergeId, | |
| }, | |
| }); | |
| return contact; | |
| } catch { | |
| this.logger.error('Error merging contacts'); | |
| return null; | |
| } | |
| } | |
| 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) { | |
| if (!body?.key) { | |
| this.logger.warn( | |
| `body.key is null or undefined in createConversation. Full body object: ${JSON.stringify(body)}`, | |
| ); | |
| return null; | |
| } | |
| const isLid = body.key.previousRemoteJid?.includes('@lid') && body.key.senderPn; | |
| const remoteJid = 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 (isLid && body.key.senderPn !== body.key.previousRemoteJid) { | |
| 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}`, | |
| ); | |
| const updateContact = await this.updateContact(instance, contact.id, { | |
| identifier: body.key.senderPn, | |
| phone_number: `+${body.key.senderPn.split('@')[0]}`, | |
| }); | |
| if (updateContact === null) { | |
| const baseContact = await this.findContact(instance, body.key.senderPn.split('@')[0]); | |
| if (baseContact) { | |
| await this.mergeContacts(baseContact.id, contact.id); | |
| this.logger.verbose( | |
| `Merge contacts: (${baseContact.id}) ${baseContact.phone_number} and (${contact.id}) ${contact.phone_number}`, | |
| ); | |
| } | |
| } | |
| } | |
| } | |
| 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, | |
| false, | |
| body.pushName, | |
| picture_url.profilePictureUrl || null, | |
| body.key.participant, | |
| ); | |
| } | |
| } | |
| const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); | |
| this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); | |
| let contact = await this.findContact(instance, chatId); | |
| 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 { | |
| contact = await this.createContact( | |
| instance, | |
| chatId, | |
| filterInbox.id, | |
| isGroup, | |
| nameContact, | |
| picture_url.profilePictureUrl || null, | |
| remoteJid, | |
| ); | |
| } | |
| 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 { | |
| // Ignore events that are not messages (like EPHEMERAL_SYNC_RESPONSE) | |
| if (body?.type && body.type !== 'message' && body.type !== 'conversation') { | |
| this.logger.verbose(`Ignoring non-message event type: ${body.type}`); | |
| return; | |
| } | |
| 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) { | |
| this.logger.warn(`body.key is null or undefined. Full body object: ${JSON.stringify(body)}`); | |
| return; | |
| } | |
| 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({ | |
| w: 320, | |
| h: 180, | |
| }); | |
| const processedBuffer = await img.getBuffer(JimpMime.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') { | |
| // Ignore events that are not messages (like EPHEMERAL_SYNC_RESPONSE) | |
| if (body?.type && body.type !== 'message') { | |
| this.logger.verbose(`Ignoring non-message event type: ${body.type}`); | |
| return; | |
| } | |
| if (!body?.key?.id) { | |
| this.logger.warn( | |
| `body.key.id is null or undefined in messages.edit. Full body object: ${JSON.stringify(body)}`, | |
| ); | |
| return; | |
| } | |
| 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; | |
| } | |
| } | |
| } | |