import ChatwootClient from '@figuro/chatwoot-sdk'; import axios from 'axios'; import FormData from 'form-data'; import { createReadStream, unlinkSync, writeFileSync } from 'fs'; import Jimp from 'jimp'; import mimeTypes from 'mime-types'; import path from 'path'; import { ConfigService, HttpServer } from '../../config/env.config'; import { Logger } from '../../config/logger.config'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { InstanceDto } from '../dto/instance.dto'; import { Options, Quoted, SendAudioDto, SendMediaDto, SendTextDto } from '../dto/sendMessage.dto'; import { MessageRaw } from '../models'; import { RepositoryBroker } from '../repository/repository.manager'; import { Events } from '../types/wa.types'; import { WAMonitoringService } from './monitor.service'; export class ChatwootService { private readonly logger = new Logger(ChatwootService.name); private provider: any; constructor( private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService, private readonly repository: RepositoryBroker, ) {} private async getProvider(instance: InstanceDto) { this.logger.verbose('get provider to instance: ' + instance.instanceName); const provider = await this.waMonitor.waInstances[instance.instanceName]?.findChatwoot(); if (!provider) { this.logger.warn('provider not found'); return null; } this.logger.verbose('provider found'); return provider; // try { // } catch (error) { // this.logger.error('provider not found'); // return null; // } } private async clientCw(instance: InstanceDto) { this.logger.verbose('get client to instance: ' + instance.instanceName); const provider = await this.getProvider(instance); if (!provider) { this.logger.error('provider not found'); return null; } this.logger.verbose('provider found'); this.provider = provider; this.logger.verbose('create client to instance: ' + instance.instanceName); const client = new ChatwootClient({ config: { basePath: provider.url, with_credentials: true, credentials: 'include', token: provider.token, }, }); this.logger.verbose('client created'); return client; } public async create(instance: InstanceDto, data: ChatwootDto) { this.logger.verbose('create chatwoot: ' + instance.instanceName); await this.waMonitor.waInstances[instance.instanceName].setChatwoot(data); this.logger.verbose('chatwoot created'); if (data.auto_create) { const urlServer = this.configService.get('SERVER').URL; await this.initInstanceChatwoot( instance, instance.instanceName.split('-cwId-')[0], `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, true, data.number, ); } return data; } public async find(instance: InstanceDto): Promise { this.logger.verbose('find chatwoot: ' + instance.instanceName); 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) { this.logger.verbose('get contact to instance: ' + instance.instanceName); 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; } this.logger.verbose('find contact in chatwoot'); const contact = await client.contact.getContactable({ accountId: this.provider.account_id, id, }); if (!contact) { this.logger.warn('contact not found'); return null; } this.logger.verbose('contact found'); return contact; } public async initInstanceChatwoot( instance: InstanceDto, inboxName: string, webhookUrl: string, qrcode: boolean, number: string, ) { this.logger.verbose('init instance chatwoot: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } this.logger.verbose('find inbox in chatwoot'); const findInbox: any = await client.inboxes.list({ accountId: this.provider.account_id, }); this.logger.verbose('check duplicate inbox'); const checkDuplicate = findInbox.payload.map((inbox) => inbox.name).includes(inboxName); let inboxId: number; if (!checkDuplicate) { this.logger.verbose('create inbox in chatwoot'); const data = { type: 'api', webhook_url: webhookUrl, }; const inbox = await client.inboxes.create({ accountId: this.provider.account_id, data: { name: inboxName, channel: data as any, }, }); if (!inbox) { this.logger.warn('inbox not found'); return null; } inboxId = inbox.id; } else { this.logger.verbose('find inbox in chatwoot'); const inbox = findInbox.payload.find((inbox) => inbox.name === inboxName); if (!inbox) { this.logger.warn('inbox not found'); return null; } inboxId = inbox.id; } this.logger.verbose('find contact in chatwoot and create if not exists'); const contact = (await this.findContact(instance, '123456')) || ((await this.createContact( instance, '123456', inboxId, false, 'EvolutionAPI', '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; if (qrcode) { this.logger.verbose('create conversation in chatwoot'); const data = { contact_id: contactId.toString(), inbox_id: inboxId.toString(), }; const conversation = await client.conversations.create({ accountId: this.provider.account_id, data, }); if (!conversation) { this.logger.warn('conversation not found'); return null; } this.logger.verbose('create message for init instance in chatwoot'); let contentMsg = 'init'; if (number) { contentMsg = `init:${number}`; } const message = await client.messages.create({ accountId: this.provider.account_id, conversationId: conversation.id, data: { content: contentMsg, message_type: 'outgoing', }, }); if (!message) { this.logger.warn('conversation not found'); return null; } } this.logger.verbose('instance chatwoot initialized'); return true; } public async createContact( instance: InstanceDto, phoneNumber: string, inboxId: number, isGroup: boolean, name?: string, avatar_url?: string, jid?: string, ) { this.logger.verbose('create contact to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } let data: any = {}; if (!isGroup) { this.logger.verbose('create contact in chatwoot'); data = { inbox_id: inboxId, name: name || phoneNumber, phone_number: `+${phoneNumber}`, identifier: jid, avatar_url: avatar_url, }; } else { this.logger.verbose('create contact group in chatwoot'); data = { inbox_id: inboxId, name: name || phoneNumber, identifier: phoneNumber, avatar_url: avatar_url, }; } this.logger.verbose('create contact in chatwoot'); const contact = await client.contacts.create({ accountId: this.provider.account_id, data, }); if (!contact) { this.logger.warn('contact not found'); return null; } this.logger.verbose('contact created'); return contact; } public async updateContact(instance: InstanceDto, id: number, data: any) { this.logger.verbose('update contact to instance: ' + instance.instanceName); 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; } this.logger.verbose('update contact in chatwoot'); try { const contact = await client.contacts.update({ accountId: this.provider.account_id, id, data, }); this.logger.verbose('contact updated'); return contact; } catch (error) { this.logger.error(error); } } public async findContact(instance: InstanceDto, phoneNumber: string) { this.logger.verbose('find contact to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } let query: any; if (!phoneNumber.includes('@g.us')) { this.logger.verbose('format phone number'); query = `+${phoneNumber}`; } else { this.logger.verbose('format group id'); query = phoneNumber; } this.logger.verbose('find contact in chatwoot'); const contact: any = await client.contacts.search({ accountId: this.provider.account_id, q: query, }); if (!contact) { this.logger.warn('contact not found'); return null; } if (!phoneNumber.includes('@g.us')) { this.logger.verbose('return contact'); return contact.payload.find((contact) => contact.phone_number === query); } else { this.logger.verbose('return group'); return contact.payload.find((contact) => contact.identifier === query); } } public async createConversation(instance: InstanceDto, body: any) { this.logger.verbose('create conversation to instance: ' + instance.instanceName); try { const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } const isGroup = body.key.remoteJid.includes('@g.us'); this.logger.verbose('is group: ' + isGroup); const chatId = isGroup ? body.key.remoteJid : body.key.remoteJid.split('@')[0]; this.logger.verbose('chat id: ' + chatId); let nameContact: string; nameContact = !body.key.fromMe ? body.pushName : chatId; this.logger.verbose('get inbox to instance: ' + instance.instanceName); const filterInbox = await this.getInbox(instance); if (!filterInbox) { this.logger.warn('inbox not found'); return null; } if (isGroup) { this.logger.verbose('get group name'); const group = await this.waMonitor.waInstances[instance.instanceName].client.groupMetadata(chatId); nameContact = `${group.subject} (GROUP)`; this.logger.verbose('find or create participant in chatwoot'); const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( body.key.participant.split('@')[0], ); const findParticipant = await this.findContact(instance, body.key.participant.split('@')[0]); 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, ); } } this.logger.verbose('find or create contact in chatwoot'); const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); const findContact = await this.findContact(instance, chatId); let contact: any; if (body.key.fromMe) { if (findContact) { contact = await this.updateContact(instance, findContact.id, { avatar_url: picture_url.profilePictureUrl || null, }); } else { const jid = isGroup ? null : body.key.remoteJid; contact = await this.createContact( instance, chatId, filterInbox.id, isGroup, nameContact, picture_url.profilePictureUrl || null, jid, ); } } else { if (findContact) { if (!findContact.name || findContact.name === chatId) { contact = await this.updateContact(instance, findContact.id, { name: nameContact, avatar_url: picture_url.profilePictureUrl || null, }); } else { contact = await this.updateContact(instance, findContact.id, { avatar_url: picture_url.profilePictureUrl || null, }); } if (!contact) { contact = await this.findContact(instance, chatId); } } else { const jid = isGroup ? null : 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 found'); return null; } const contactId = contact?.payload?.id || contact?.payload?.contact?.id || contact?.id; if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { this.logger.verbose('update contact name in chatwoot'); await this.updateContact(instance, contactId, { name: nameContact, }); } this.logger.verbose('get contact conversations in chatwoot'); const contactConversations = (await client.contacts.listConversations({ accountId: this.provider.account_id, id: contactId, })) as any; if (contactConversations) { let conversation: any; if (this.provider.reopen_conversation) { conversation = contactConversations.payload.find((conversation) => conversation.inbox_id == filterInbox.id); if (this.provider.conversation_pending) { await client.conversations.toggleStatus({ accountId: this.provider.account_id, conversationId: conversation.id, data: { status: 'pending', }, }); } } else { conversation = contactConversations.payload.find( (conversation) => conversation.status !== 'resolved' && conversation.inbox_id == filterInbox.id, ); } this.logger.verbose('return conversation if exists'); if (conversation) { this.logger.verbose('conversation found'); return conversation.id; } } this.logger.verbose('create conversation in chatwoot'); const data = { contact_id: contactId.toString(), inbox_id: filterInbox.id.toString(), }; if (this.provider.conversation_pending) { data['status'] = 'pending'; } const conversation = await client.conversations.create({ accountId: this.provider.account_id, data, }); if (!conversation) { this.logger.warn('conversation not found'); return null; } this.logger.verbose('conversation created'); return conversation.id; } catch (error) { this.logger.error(error); } } public async getInbox(instance: InstanceDto) { this.logger.verbose('get inbox to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } this.logger.verbose('find inboxes in chatwoot'); const inbox = (await client.inboxes.list({ accountId: this.provider.account_id, })) as any; if (!inbox) { this.logger.warn('inbox not found'); return null; } this.logger.verbose('find inbox by name'); const findByName = inbox.payload.find((inbox) => inbox.name === instance.instanceName.split('-cwId-')[0]); if (!findByName) { this.logger.warn('inbox not found'); return null; } this.logger.verbose('return inbox'); 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, ) { this.logger.verbose('create message to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } const replyToIds = await this.getReplyToIds(messageBody, instance); this.logger.verbose('create message in chatwoot'); const message = await client.messages.create({ accountId: this.provider.account_id, conversationId: conversationId, data: { content: content, message_type: messageType, attachments: attachments, private: privateMessage || false, source_id: sourceId, content_attributes: { ...replyToIds, }, }, }); if (!message) { this.logger.warn('message not found'); return null; } this.logger.verbose('message created'); return message; } public async createBotMessage( instance: InstanceDto, content: string, messageType: 'incoming' | 'outgoing' | undefined, attachments?: { content: unknown; encoding: string; filename: string; }[], ) { this.logger.verbose('create bot message to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } this.logger.verbose('find contact in chatwoot'); const contact = await this.findContact(instance, '123456'); if (!contact) { this.logger.warn('contact not found'); return null; } this.logger.verbose('get inbox to instance: ' + instance.instanceName); const filterInbox = await this.getInbox(instance); if (!filterInbox) { this.logger.warn('inbox not found'); return null; } this.logger.verbose('find conversation in chatwoot'); const findConversation = await client.conversations.list({ accountId: this.provider.account_id, inboxId: filterInbox.id, }); if (!findConversation) { this.logger.warn('conversation not found'); return null; } this.logger.verbose('find conversation by contact id'); const conversation = findConversation.data.payload.find( (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', ); if (!conversation) { this.logger.warn('conversation not found'); return; } this.logger.verbose('create message in chatwoot'); const message = await client.messages.create({ accountId: this.provider.account_id, conversationId: conversation.id, data: { content: content, message_type: messageType, attachments: attachments, }, }); if (!message) { this.logger.warn('message not found'); return null; } this.logger.verbose('bot message created'); return message; } private async sendData( conversationId: number, file: string, messageType: 'incoming' | 'outgoing' | undefined, content?: string, instance?: InstanceDto, messageBody?: any, sourceId?: string, ) { this.logger.verbose('send data to chatwoot'); const data = new FormData(); if (content) { this.logger.verbose('content found'); data.append('content', content); } this.logger.verbose('message type: ' + messageType); data.append('message_type', messageType); this.logger.verbose('temp file found'); data.append('attachments[]', createReadStream(file)); if (messageBody && instance) { const replyToIds = await this.getReplyToIds(messageBody, instance); if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { data.append('content_attributes', { ...replyToIds, }); } } if (sourceId) { data.append('source_id', sourceId); } this.logger.verbose('get client to instance: ' + this.provider.instanceName); const config = { method: 'post', maxBodyLength: Infinity, url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), }, data: data, }; this.logger.verbose('send data to chatwoot'); try { const { data } = await axios.request(config); this.logger.verbose('remove temp file'); unlinkSync(file); this.logger.verbose('data sent'); return data; } catch (error) { this.logger.error(error); unlinkSync(file); } } public async createBotQr( instance: InstanceDto, content: string, messageType: 'incoming' | 'outgoing' | undefined, file?: string, ) { this.logger.verbose('create bot qr to instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } this.logger.verbose('find contact in chatwoot'); const contact = await this.findContact(instance, '123456'); if (!contact) { this.logger.warn('contact not found'); return null; } this.logger.verbose('get inbox to instance: ' + instance.instanceName); const filterInbox = await this.getInbox(instance); if (!filterInbox) { this.logger.warn('inbox not found'); return null; } this.logger.verbose('find conversation in chatwoot'); const findConversation = await client.conversations.list({ accountId: this.provider.account_id, inboxId: filterInbox.id, }); if (!findConversation) { this.logger.warn('conversation not found'); return null; } this.logger.verbose('find conversation by contact id'); const conversation = findConversation.data.payload.find( (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', ); if (!conversation) { this.logger.warn('conversation not found'); return; } this.logger.verbose('send data to chatwoot'); const data = new FormData(); if (content) { this.logger.verbose('content found'); data.append('content', content); } this.logger.verbose('message type: ' + messageType); data.append('message_type', messageType); if (file) { this.logger.verbose('temp file found'); data.append('attachments[]', createReadStream(file)); } this.logger.verbose('get client to instance: ' + this.provider.instanceName); const config = { method: 'post', maxBodyLength: Infinity, url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), }, data: data, }; this.logger.verbose('send data to chatwoot'); try { const { data } = await axios.request(config); this.logger.verbose('remove temp file'); unlinkSync(file); this.logger.verbose('data sent'); return data; } catch (error) { this.logger.error(error); } } public async sendAttachment(waInstance: any, number: string, media: any, caption?: string, options?: Options) { this.logger.verbose('send attachment to instance: ' + waInstance.instanceName); try { this.logger.verbose('get media type'); const parts = media.split('/'); const fileName = decodeURIComponent(parts[parts.length - 1]); this.logger.verbose('file name: ' + fileName); const response = await axios.get(media, { responseType: 'arraybuffer', }); const mimeType = response.headers['content-type']; this.logger.verbose('mime type: ' + mimeType); 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; } this.logger.verbose('type: ' + type); if (type === 'audio') { this.logger.verbose('send audio to instance: ' + waInstance.instanceName); const data: SendAudioDto = { number: number, audioMessage: { audio: media, }, options: { delay: 1200, presence: 'recording', ...options, }, }; const messageSent = await waInstance?.audioWhatsapp(data, true); this.logger.verbose('audio sent'); return messageSent; } this.logger.verbose('send media to instance: ' + waInstance.instanceName); const data: SendMediaDto = { number: number, mediaMessage: { mediatype: type as any, fileName: fileName, media: media, }, options: { delay: 1200, presence: 'composing', ...options, }, }; if (caption) { this.logger.verbose('caption found'); data.mediaMessage.caption = caption; } const messageSent = await waInstance?.mediaMessage(data, true); this.logger.verbose('media sent'); return messageSent; } catch (error) { this.logger.error(error); } } public async receiveWebhook(instance: InstanceDto, body: any) { try { await new Promise((resolve) => setTimeout(resolve, 500)); this.logger.verbose('receive webhook to chatwoot instance: ' + instance.instanceName); const client = await this.clientCw(instance); if (!client) { this.logger.warn('client not found'); return null; } this.logger.verbose('check if is bot'); if ( !body?.conversation || body.private || (body.event === 'message_updated' && !body.content_attributes?.deleted) ) { return { message: 'bot' }; } this.logger.verbose('check if is group'); const chatId = body.conversation.meta.sender?.phone_number?.replace('+', '') || body.conversation.meta.sender?.identifier; // Chatwoot to Whatsapp const messageReceived = body.content ? body.content .replaceAll(/(? 0) { this.logger.verbose('message is media'); for (const attachment of message.attachments) { this.logger.verbose('send media to whatsapp'); if (!messageReceived) { this.logger.verbose('message do not have text'); formatText = null; } const options: Options = { quoted: await this.getQuotedMessage(body, instance), }; const messageSent = await this.sendAttachment( waInstance, chatId, attachment.data_url, formatText, options, ); this.updateChatwootMessageId( { ...messageSent, owner: instance.instanceName, }, { messageId: body.id, inboxId: body.inbox?.id, conversationId: body.conversation?.id, }, instance, ); } } else { this.logger.verbose('message is text'); this.logger.verbose('send text to whatsapp'); const data: SendTextDto = { number: chatId, textMessage: { text: formatText, }, options: { delay: 1200, presence: 'composing', quoted: await this.getQuotedMessage(body, instance), }, }; const messageSent = await waInstance?.textMessage(data, true); this.updateChatwootMessageId( { ...messageSent, owner: instance.instanceName, }, { messageId: body.id, inboxId: body.inbox?.id, conversationId: body.conversation?.id, }, instance, ); } } } if (body.message_type === 'template' && body.event === 'message_created') { this.logger.verbose('check if is template'); const data: SendTextDto = { number: chatId, textMessage: { text: body.content.replace(/\\\r\n|\\\n|\n/g, '\n'), }, options: { delay: 1200, presence: 'composing', }, }; this.logger.verbose('send text to whatsapp'); await waInstance?.textMessage(data); } return { message: 'bot' }; } catch (error) { this.logger.error(error); return { message: 'bot' }; } } private updateChatwootMessageId( message: MessageRaw, chatwootMessageIds: MessageRaw['chatwoot'], instance: InstanceDto, ) { if (!chatwootMessageIds.messageId || !message?.key?.id) { return; } message.chatwoot = chatwootMessageIds; this.repository.message.update([message], instance.instanceName, true); } private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { const messages = await this.repository.message.find({ where: { key: { id: keyId, }, owner: instance.instanceName, }, limit: 1, }); return messages.length ? messages[0] : 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; if (inReplyToExternalId) { const message = await this.getMessageByKeyId(instance, inReplyToExternalId); if (message?.chatwoot?.messageId) { inReplyTo = message.chatwoot.messageId; } } } return { in_reply_to: inReplyTo, in_reply_to_external_id: inReplyToExternalId, }; } private async getQuotedMessage(msg: any, instance: InstanceDto): Promise { if (msg?.content_attributes?.in_reply_to) { const message = await this.repository.message.find({ where: { chatwoot: { messageId: msg?.content_attributes?.in_reply_to, }, owner: instance.instanceName, }, limit: 1, }); if (message.length && message[0]?.key?.id) { return { key: message[0].key, message: message[0].message, }; } } return null; } private isMediaMessage(message: any) { this.logger.verbose('check if is media message'); const media = [ 'imageMessage', 'documentMessage', 'documentWithCaptionMessage', 'audioMessage', 'videoMessage', 'stickerMessage', ]; const messageKeys = Object.keys(message); const result = messageKeys.some((key) => media.includes(key)); this.logger.verbose('is media message: ' + result); return result; } private getAdsMessage(msg: any) { interface AdsMessage { title: string; body: string; thumbnailUrl: string; sourceUrl: string; } const adsMessage: AdsMessage | undefined = msg.extendedTextMessage?.contextInfo?.externalAdReply; this.logger.verbose('Get ads message if it exist'); adsMessage && this.logger.verbose('Ads message: ' + adsMessage); return adsMessage; } private getReactionMessage(msg: any) { interface ReactionMessage { key: MessageRaw['key']; text: string; } const reactionMessage: ReactionMessage | undefined = msg?.reactionMessage; this.logger.verbose('Get reaction message if it exists'); reactionMessage && this.logger.verbose('Reaction message: ' + reactionMessage); return reactionMessage; } private getTypeMessage(msg: any) { this.logger.verbose('get type message'); 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?.caption, contactMessage: msg.contactMessage?.vcard, contactsArrayMessage: msg.contactsArrayMessage, locationMessage: msg.locationMessage, liveLocationMessage: msg.liveLocationMessage, }; this.logger.verbose('type message: ' + types); return types; } private getMessageContent(types: any) { this.logger.verbose('get message content'); const typeKey = Object.keys(types).find((key) => types[key] !== undefined); const result = typeKey ? types[typeKey] : undefined; if (typeKey === 'locationMessage' || typeKey === 'liveLocationMessage') { const latitude = result.degreesLatitude; const longitude = result.degreesLongitude; const formattedLocation = `**Location:** **latitude:** ${latitude} **longitude:** ${longitude} https://www.google.com/maps/search/?api=1&query=${latitude},${longitude} `; this.logger.verbose('message content: ' + formattedLocation); 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 = `**Contact:** **name:** ${contactInfo['FN']}`; let numberCount = 1; Object.keys(contactInfo).forEach((key) => { if (key.startsWith('item') && key.includes('TEL')) { const phoneNumber = contactInfo[key]; formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; numberCount++; } if (key.includes('TEL')) { const phoneNumber = contactInfo[key]; formattedContact += `\n**number:** ${phoneNumber}`; numberCount++; } }); this.logger.verbose('message content: ' + formattedContact); 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 = `**Contact:** **name:** ${contact.displayName}`; let numberCount = 1; Object.keys(contactInfo).forEach((key) => { if (key.startsWith('item') && key.includes('TEL')) { const phoneNumber = contactInfo[key]; formattedContact += `\n**number ${numberCount}:** ${phoneNumber}`; numberCount++; } if (key.includes('TEL')) { const phoneNumber = contactInfo[key]; formattedContact += `\n**number:** ${phoneNumber}`; numberCount++; } }); return formattedContact; }); const formattedContactsArray = formattedContacts.join('\n\n'); this.logger.verbose('formatted contacts: ' + formattedContactsArray); return formattedContactsArray; } this.logger.verbose('message content: ' + result); return result; } private getConversationMessage(msg: any) { this.logger.verbose('get conversation message'); const types = this.getTypeMessage(msg); const messageContent = this.getMessageContent(types); this.logger.verbose('conversation message: ' + messageContent); return messageContent; } public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { this.logger.verbose('event whatsapp to instance: ' + instance.instanceName); 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 (event === 'messages.upsert' || event === 'send.message') { this.logger.verbose('event messages.upsert'); if (body.key.remoteJid === 'status@broadcast') { this.logger.verbose('status broadcast found'); return; } this.logger.verbose('get conversation message'); // Whatsapp to Chatwoot const originalMessage = await this.getConversationMessage(body.message); const bodyMessage = originalMessage ? originalMessage .replaceAll(/\*((?!\s)([^\n*]+?)(? { await img.cover(320, 180).writeAsync(fileName); }) .catch((err) => { this.logger.error(`image is not write: ${err}`); }); const truncStr = (str: string, len: number) => { return str.length > len ? str.substring(0, len) + '...' : str; }; const title = truncStr(adsMessage.title, 40); const description = truncStr(adsMessage.body, 75); this.logger.verbose('send data to chatwoot'); const send = await this.sendData( getConversation, fileName, 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; } this.logger.verbose('check if is group'); if (body.key.remoteJid.includes('@g.us')) { this.logger.verbose('message is group'); const participantName = body.pushName; let content: string; if (!body.key.fromMe) { this.logger.verbose('message is not from me'); content = `**${participantName}**\n\n${bodyMessage}`; } else { this.logger.verbose('message is from me'); content = `${bodyMessage}`; } this.logger.verbose('send data to chatwoot'); const send = await this.createMessage( instance, getConversation, content, messageType, false, [], body, 'WAID:' + body.key.id, ); if (!send) { this.logger.warn('message not sent'); return; } return send; } else { this.logger.verbose('message is not group'); this.logger.verbose('send data to chatwoot'); const send = await this.createMessage( instance, getConversation, bodyMessage, messageType, false, [], body, 'WAID:' + body.key.id, ); if (!send) { this.logger.warn('message not sent'); return; } return send; } } if (event === Events.MESSAGES_DELETE) { this.logger.verbose('deleting message from instance: ' + instance.instanceName); if (!body?.key?.id) { this.logger.warn('message id not found'); return; } const message = await this.getMessageByKeyId(instance, body.key.id); if (message?.chatwoot?.messageId && message?.chatwoot?.conversationId) { this.logger.verbose('deleting message in chatwoot. Message id: ' + body.key.id); return await client.messages.delete({ accountId: this.provider.account_id, conversationId: message.chatwoot.conversationId, messageId: message.chatwoot.messageId, }); } } if (event === 'status.instance') { this.logger.verbose('event status.instance'); const data = body; const inbox = await this.getInbox(instance); if (!inbox) { this.logger.warn('inbox not found'); return; } const msgStatus = `⚡️ Instance status ${inbox.name}: ${data.status}`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgStatus, 'incoming'); } if (event === 'connection.update') { this.logger.verbose('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 = `🚀 Connection successfully established!`; this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgConnection, 'incoming'); } } } if (event === 'qrcode.updated') { this.logger.verbose('event qrcode.updated'); if (body.statusCode === 500) { this.logger.verbose('qrcode error'); const erroQRcode = `🚨 QRCode generation limit reached, to generate a new QRCode, send the 'init' message again.`; this.logger.verbose('send message to chatwoot'); return await this.createBotMessage(instance, erroQRcode, 'incoming'); } else { this.logger.verbose('qrcode success'); const fileData = Buffer.from(body?.qrcode.base64.replace('data:image/png;base64,', ''), 'base64'); const fileName = `${path.join(waInstance?.storePath, 'temp', `${`${instance}.png`}`)}`; this.logger.verbose('temp file name: ' + fileName); this.logger.verbose('create temp file'); writeFileSync(fileName, fileData, 'utf8'); this.logger.verbose('send qrcode to chatwoot'); await this.createBotQr(instance, 'QRCode successfully generated!', 'incoming', fileName); let msgQrCode = `⚡️ QRCode successfully generated!\n\nScan this QR code within the next 40 seconds.`; if (body?.qrcode?.pairingCode) { msgQrCode = msgQrCode + `\n\n*Pairing Code:* ${body.qrcode.pairingCode.substring(0, 4)}-${body.qrcode.pairingCode.substring( 4, 8, )}`; } this.logger.verbose('send message to chatwoot'); await this.createBotMessage(instance, msgQrCode, 'incoming'); } } } catch (error) { this.logger.error(error); } } }