diff --git a/src/whatsapp/controllers/chatwoot.controller.ts b/src/whatsapp/controllers/chatwoot.controller.ts index c48e9285..e5887d26 100644 --- a/src/whatsapp/controllers/chatwoot.controller.ts +++ b/src/whatsapp/controllers/chatwoot.controller.ts @@ -45,4 +45,11 @@ export class ChatwootController { logger.verbose('requested findChatwoot from ' + instance.instanceName + ' instance'); return this.chatwootService.find(instance); } + + public async receiveWebhook(instance: InstanceDto, data: any) { + logger.verbose( + 'requested receiveWebhook from ' + instance.instanceName + ' instance', + ); + return this.chatwootService.receiveWebhook(instance, data); + } } diff --git a/src/whatsapp/models/chatwoot.model.ts b/src/whatsapp/models/chatwoot.model.ts index cb298c91..1ecdcf82 100644 --- a/src/whatsapp/models/chatwoot.model.ts +++ b/src/whatsapp/models/chatwoot.model.ts @@ -3,6 +3,7 @@ import { dbserver } from '../../db/db.connect'; export class ChatwootRaw { _id?: string; + enabled?: boolean; account_id?: string; token?: string; url?: string; @@ -11,6 +12,7 @@ export class ChatwootRaw { const chatwootSchema = new Schema({ _id: { type: String, _id: true }, + enabled: { type: Boolean, required: true }, account_id: { type: String, required: true }, token: { type: String, required: true }, url: { type: String, required: true }, diff --git a/src/whatsapp/routers/chatwoot.router.ts b/src/whatsapp/routers/chatwoot.router.ts index a31c42b1..3d87f137 100644 --- a/src/whatsapp/routers/chatwoot.router.ts +++ b/src/whatsapp/routers/chatwoot.router.ts @@ -46,11 +46,21 @@ export class ChatwootRouter extends RouterBroker { res.status(HttpStatus.OK).json(response); }) - .post(this.routerPath('webhook'), ...guards, async (req, res) => { - const { body } = req; - const { instance } = req.query; + .post(this.routerPath('webhook'), async (req, res) => { + logger.verbose('request received in findChatwoot'); + logger.verbose('request body: '); + logger.verbose(req.body); - res.status(HttpStatus.OK).json({ message: 'bot' }); + logger.verbose('request query: '); + logger.verbose(req.query); + const response = await this.dataValidate({ + request: req, + schema: instanceNameSchema, + ClassRef: InstanceDto, + execute: (instance, data) => chatwootController.receiveWebhook(instance, data), + }); + + res.status(HttpStatus.OK).json(response); }); } diff --git a/src/whatsapp/services/chatwoot.service.ts b/src/whatsapp/services/chatwoot.service.ts index 42d17d56..6cfc3aab 100644 --- a/src/whatsapp/services/chatwoot.service.ts +++ b/src/whatsapp/services/chatwoot.service.ts @@ -1,11 +1,16 @@ import { InstanceDto } from '../dto/instance.dto'; +import path from 'path'; import { ChatwootDto } from '../dto/chatwoot.dto'; import { WAMonitoringService } from './monitor.service'; import { Logger } from '../../config/logger.config'; import ChatwootClient from '@figuro/chatwoot-sdk'; -import { createReadStream, unlinkSync } from 'fs'; +import { createReadStream, unlinkSync, writeFileSync } from 'fs'; import axios from 'axios'; import FormData from 'form-data'; +import { SendTextDto } from '../dto/sendMessage.dto'; +import mimeTypes from 'mime-types'; +import { SendAudioDto } from '../dto/sendMessage.dto'; +import { SendMediaDto } from '../dto/sendMessage.dto'; export class ChatwootService { constructor(private readonly waMonitor: WAMonitoringService) {} @@ -71,7 +76,7 @@ export class ChatwootService { } const contact = await client.contact.getContactable({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id, }); @@ -92,7 +97,7 @@ export class ChatwootService { } const contact = await client.contacts.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, data: { inbox_id: inboxId, name: name || phoneNumber, @@ -115,7 +120,7 @@ export class ChatwootService { } const contact = await client.contacts.update({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id, data, }); @@ -131,7 +136,7 @@ export class ChatwootService { } const contact = await client.contacts.search({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, q: `+${phoneNumber}`, }); @@ -145,8 +150,8 @@ export class ChatwootService { throw new Error('client not found'); } - const chatId = body.data.key.remoteJid.split('@')[0]; - const nameContact = !body.data.key.fromMe ? body.data.pushName : chatId; + const chatId = body.key.remoteJid.split('@')[0]; + const nameContact = !body.key.fromMe ? body.pushName : chatId; const filterInbox = await this.getInbox(instance); @@ -156,14 +161,14 @@ export class ChatwootService { const contactId = contact.id || contact.payload.contact.id; - if (!body.data.key.fromMe && contact.name === chatId && nameContact !== chatId) { + if (!body.key.fromMe && contact.name === chatId && nameContact !== chatId) { await this.updateContact(instance, contactId, { name: nameContact, }); } const contactConversations = (await client.contacts.listConversations({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, id: contactId, })) as any; @@ -178,7 +183,7 @@ export class ChatwootService { } const conversation = await client.conversations.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, data: { contact_id: `${contactId}`, inbox_id: `${filterInbox.id}`, @@ -196,9 +201,12 @@ export class ChatwootService { } const inbox = (await client.inboxes.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, })) as any; - const findByName = inbox.payload.find((inbox) => inbox.name === instance); + + const findByName = inbox.payload.find( + (inbox) => inbox.name === instance.instanceName, + ); return findByName; } @@ -216,7 +224,7 @@ export class ChatwootService { const client = await this.clientCw(instance); const message = await client.messages.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, conversationId: conversationId, data: { content: content, @@ -245,16 +253,17 @@ export class ChatwootService { const filterInbox = await this.getInbox(instance); const findConversation = await client.conversations.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, inboxId: filterInbox.id, }); + const conversation = findConversation.data.payload.find( (conversation) => conversation?.meta?.sender?.id === contact.id && conversation.status === 'open', ); const message = await client.messages.create({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, conversationId: conversation.id, data: { content: content, @@ -285,7 +294,7 @@ export class ChatwootService { const config = { method: 'post', maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversationId}/messages`, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversationId}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), @@ -315,7 +324,7 @@ export class ChatwootService { const filterInbox = await this.getInbox(instance); const findConversation = await client.conversations.list({ - accountId: this.provider.accountId, + accountId: this.provider.account_id, inboxId: filterInbox.id, }); const conversation = findConversation.data.payload.find( @@ -338,7 +347,7 @@ export class ChatwootService { const config = { method: 'post', maxBodyLength: Infinity, - url: `${this.provider.url}/api/v1/accounts/${this.provider.accountId}/conversations/${conversation.id}/messages`, + url: `${this.provider.url}/api/v1/accounts/${this.provider.account_id}/conversations/${conversation.id}/messages`, headers: { api_access_token: this.provider.token, ...data.getHeaders(), @@ -355,7 +364,360 @@ export class ChatwootService { } } - public async chatwootWebhook(instance: InstanceDto, body: any) { - return true; + public async sendAttachment( + waInstance: any, + number: string, + media: any, + caption?: string, + ) { + try { + const parts = media.split('/'); + const fileName = decodeURIComponent(parts[parts.length - 1]); + + const mimeType = mimeTypes.lookup(fileName).toString(); + + 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, + audioMessage: { + audio: media, + }, + options: { + delay: 1200, + presence: 'recording', + }, + }; + + await waInstance?.audioWhatsapp(data); + + return; + } + + const data: SendMediaDto = { + number: number, + mediaMessage: { + mediatype: type as any, + fileName: fileName, + media: media, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + if (caption && type !== 'audio') { + data.mediaMessage.caption = caption; + } + + await waInstance?.mediaMessage(data); + + return; + } catch (error) { + throw new Error(error); + } + } + + public async receiveWebhook(instance: InstanceDto, body: any) { + try { + if (!body?.conversation || body.private) return { message: 'bot' }; + + const chatId = body.conversation.meta.sender.phone_number.replace('+', ''); + const messageReceived = body.content; + const senderName = body?.sender?.name; + const accountId = body.account.id as number; + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (chatId === '123456' && body.message_type === 'outgoing') { + const command = messageReceived.replace('/', ''); + + if (command === 'iniciar') { + const state = waInstance?.connectionStatus?.state; + + if (state !== 'open') { + await waInstance.connectToWhatsapp(); + } else { + await this.createBotMessage( + instance, + `🚨 Instância ${body.inbox.name} já está conectada.`, + 'incoming', + ); + } + } + + if (command === 'status') { + const state = waInstance?.connectionStatus?.state; + + if (!state) { + await this.createBotMessage( + instance, + `⚠️ Instância ${body.inbox.name} não existe.`, + 'incoming', + ); + } + + if (state) { + await this.createBotMessage( + instance, + `⚠️ Status da instância ${body.inbox.name}: *${state}*`, + 'incoming', + ); + } + } + + if (command === 'desconectar') { + const msgLogout = `🚨 Desconectando Whatsapp da caixa de entrada *${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 (IMPORT_MESSAGES_SENT && messages_sent.includes(body.id)) { + // console.log(`🚨 Não importar mensagens enviadas, ficaria duplicado.`); + + // const indexMessage = messages_sent.indexOf(body.id); + // messages_sent.splice(indexMessage, 1); + + // return { message: 'bot' }; + // } + + let formatText: string; + if (senderName === null || senderName === undefined) { + formatText = messageReceived; + } else { + // formatText = TOSIGN ? `*${senderName}*: ${messageReceived}` : messageReceived; + formatText = `*${senderName}*: ${messageReceived}`; + } + + for (const message of body.conversation.messages) { + if (message.attachments && message.attachments.length > 0) { + for (const attachment of message.attachments) { + console.log(attachment); + if (!messageReceived) { + formatText = null; + } + + await this.sendAttachment( + waInstance, + chatId, + attachment.data_url, + formatText, + ); + } + } else { + const data: SendTextDto = { + number: chatId, + textMessage: { + text: formatText, + }, + options: { + delay: 1200, + presence: 'composing', + }, + }; + + await waInstance?.textMessage(data); + } + } + } + + return { message: 'bot' }; + } catch (error) { + console.log(error); + + return { message: 'bot' }; + } + } + + private isMediaMessage(message: any) { + const media = [ + 'imageMessage', + 'documentMessage', + 'audioMessage', + 'videoMessage', + 'stickerMessage', + ]; + + const messageKeys = Object.keys(message); + return messageKeys.some((key) => media.includes(key)); + } + + 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: msg.stickerMessage?.fileSha256.toString('base64'), + documentMessage: msg.documentMessage?.caption, + audioMessage: msg.audioMessage?.caption, + }; + + return types; + } + + private getMessageContent(types: any) { + const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + return typeKey ? types[typeKey] : undefined; + } + + private getConversationMessage(msg: any) { + const types = this.getTypeMessage(msg); + + const messageContent = this.getMessageContent(types); + + return messageContent; + } + + public async eventWhatsapp(event: string, instance: InstanceDto, body: any) { + try { + const client = await this.clientCw(instance); + + if (!client) { + throw new Error('client not found'); + } + + const waInstance = this.waMonitor.waInstances[instance.instanceName]; + + if (event === 'messages.upsert') { + // if (body.key.fromMe && !IMPORT_MESSAGES_SENT) { + // return; + // } + + if (body.key.remoteJid === 'status@broadcast') { + console.log(`🚨 Ignorando status do whatsapp.`); + return; + } + + const getConversion = await this.createConversation(instance, body); + const messageType = body.key.fromMe ? 'outgoing' : 'incoming'; + + if (!getConversion) { + console.log('🚨 Erro ao criar conversa'); + return; + } + + const isMedia = this.isMediaMessage(body.message); + + const bodyMessage = await this.getConversationMessage(body.message); + + if (isMedia) { + const downloadBase64 = await waInstance?.getBase64FromMediaMessage({ + message: { + ...body, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.${mimeTypes.extension(downloadBase64.mimetype)}`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join(waInstance?.storePath, 'temp', `${nameFile}`)}`; + + writeFileSync(fileName, fileData, 'utf8'); + + return await this.sendData(getConversion, fileName, messageType, bodyMessage); + } + + const send = await this.createMessage( + instance, + getConversion, + bodyMessage, + messageType, + ); + + return send; + } + + if (event === 'status.instance') { + const data = body; + const inbox = await this.getInbox(instance); + const msgStatus = `⚡️ Status da instância ${inbox.name}: ${data.status}`; + await this.createBotMessage(instance, msgStatus, 'incoming'); + } + + if (event === 'connection.update') { + if (body.state === 'open') { + const msgConnection = `🚀 Conexão realizada com sucesso!`; + await this.createBotMessage(instance, msgConnection, 'incoming'); + } + } + + if (event === 'contacts.update') { + const data = body; + + if (data.length) { + for (const item of data) { + const number = item.id.split('@')[0]; + const photo = item.profilePictureUrl || null; + const find = await this.findContact(instance, number); + + if (find) { + await this.updateContact(instance, find.id, { + avatar_url: photo, + }); + } + } + } + } + + if (event === 'qrcode.updated') { + if (body.statusCode === 500) { + const erroQRcode = `🚨 Limite de geração de QRCode atingido, para gerar um novo QRCode, envie a mensagem /iniciar novamente.`; + return await this.createBotMessage(instance, erroQRcode, 'incoming'); + } else { + const fileData = Buffer.from( + body?.qrcode.base64.replace('data:image/png;base64,', ''), + 'base64', + ); + + const fileName = `${path.join( + waInstance?.storePath, + 'temp', + `${`${instance}.png`}`, + )}`; + + writeFileSync(fileName, fileData, 'utf8'); + + await this.createBotQr( + instance, + 'QRCode gerado com sucesso!', + 'incoming', + fileName, + ); + + const msgQrCode = `⚡️ QRCode gerado com sucesso!\n\nDigitalize este código QR nos próximos 40 segundos:`; + await this.createBotMessage(instance, msgQrCode, 'incoming'); + } + } + } catch (error) { + console.log(error); + } } } diff --git a/src/whatsapp/services/whatsapp.service.ts b/src/whatsapp/services/whatsapp.service.ts index f7da72ef..0f756d18 100644 --- a/src/whatsapp/services/whatsapp.service.ts +++ b/src/whatsapp/services/whatsapp.service.ts @@ -122,6 +122,8 @@ import sharp from 'sharp'; import { RedisCache } from '../../db/redis.client'; import { Log } from '../../config/env.config'; import ProxyAgent from 'proxy-agent'; +import { ChatwootService } from './chatwoot.service'; +import { waMonitor } from '../whatsapp.module'; export class WAStartupService { constructor( @@ -141,12 +143,14 @@ export class WAStartupService { private readonly localWebhook: wa.LocalWebHook = {}; private readonly localChatwoot: wa.LocalChatwoot = {}; private stateConnection: wa.StateConnection = { state: 'close' }; - private readonly storePath = join(ROOT_DIR, 'store'); + public readonly storePath = join(ROOT_DIR, 'store'); private readonly msgRetryCounterCache: CacheStore = new NodeCache(); private readonly userDevicesCache: CacheStore = new NodeCache(); private endSession = false; private logBaileys = this.configService.get('LOG').BAILEYS; + private chatwootService = new ChatwootService(waMonitor); + public set instanceName(name: string) { this.logger.verbose(`Initializing instance '${name}'`); if (!name) { @@ -161,6 +165,17 @@ export class WAStartupService { instance: this.instance.name, status: 'created', }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'created', + }, + ); + } } public get instanceName() { @@ -270,6 +285,24 @@ export class WAStartupService { return data; } + private async loadChatwoot() { + this.logger.verbose('Loading chatwoot'); + const data = await this.repository.chatwoot.find(this.instanceName); + this.localChatwoot.enabled = data?.enabled; + this.logger.verbose(`Chatwoot enabled: ${this.localChatwoot.enabled}`); + + this.localChatwoot.account_id = data?.account_id; + this.logger.verbose(`Chatwoot account id: ${this.localChatwoot.account_id}`); + + this.localChatwoot.token = data?.token; + this.logger.verbose(`Chatwoot token: ${this.localChatwoot.token}`); + + this.localChatwoot.url = data?.url; + this.logger.verbose(`Chatwoot url: ${this.localChatwoot.url}`); + + this.logger.verbose('Chatwoot loaded'); + } + public async setChatwoot(data: ChatwootRaw) { this.logger.verbose('Setting chatwoot'); await this.repository.chatwoot.create(data, this.instanceName); @@ -429,6 +462,17 @@ export class WAStartupService { statusCode: DisconnectReason.badSession, }); + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + message: 'QR code limit reached, please login again', + statusCode: DisconnectReason.badSession, + }, + ); + } + this.logger.verbose('Sending data to webhook in event CONNECTION_UPDATE'); this.sendDataWebhook(Events.CONNECTION_UPDATE, { instance: this.instance.name, @@ -442,6 +486,17 @@ export class WAStartupService { status: 'removed', }); + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + this.logger.verbose('endSession defined as true'); this.endSession = true; @@ -472,6 +527,16 @@ export class WAStartupService { this.sendDataWebhook(Events.QRCODE_UPDATED, { qrcode: { instance: this.instance.name, code: qr, base64 }, }); + + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.QRCODE_UPDATED, + { instanceName: this.instance.name }, + { + qrcode: { instance: this.instance.name, code: qr, base64 }, + }, + ); + } }); this.logger.verbose('Generating QR code in terminal'); @@ -512,6 +577,17 @@ export class WAStartupService { status: 'removed', }); + if (this.localChatwoot.enabled) { + this.chatwootService.eventWhatsapp( + Events.STATUS_INSTANCE, + { instanceName: this.instance.name }, + { + instance: this.instance.name, + status: 'removed', + }, + ); + } + this.logger.verbose('Emittin event logout.instance'); this.eventEmitter.emit('logout.instance', this.instance.name, 'inner'); this.client?.ws?.close(); @@ -626,6 +702,7 @@ export class WAStartupService { this.logger.verbose('Connecting to whatsapp'); try { this.loadWebhook(); + this.loadChatwoot(); this.instance.authState = await this.defineAuthState(); @@ -817,6 +894,14 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactsRaw); + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactsRaw, + ); + } + this.logger.verbose('Updating contacts in database'); await this.repository.contact.update( contactsRaw, @@ -940,6 +1025,14 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event MESSAGES_UPSERT'); await this.sendDataWebhook(Events.MESSAGES_UPSERT, messageRaw); + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.MESSAGES_UPSERT, + { instanceName: this.instance.name }, + messageRaw, + ); + } + this.logger.verbose('Inserting message in database'); await this.repository.message.insert( [messageRaw], @@ -978,6 +1071,14 @@ export class WAStartupService { this.logger.verbose('Sending data to webhook in event CONTACTS_UPDATE'); await this.sendDataWebhook(Events.CONTACTS_UPDATE, contactRaw); + if (this.localChatwoot.enabled) { + await this.chatwootService.eventWhatsapp( + Events.CONTACTS_UPDATE, + { instanceName: this.instance.name }, + contactRaw, + ); + } + this.logger.verbose('Updating contact in database'); await this.repository.contact.update( [contactRaw],