feat: chatwoot integration completed

This commit is contained in:
Davidson Gomes 2023-07-12 20:28:51 -03:00
parent 514fb56209
commit 052303cc93
5 changed files with 507 additions and 25 deletions

View File

@ -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);
}
}

View File

@ -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<ChatwootRaw>({
_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 },

View File

@ -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<InstanceDto>({
request: req,
schema: instanceNameSchema,
ClassRef: InstanceDto,
execute: (instance, data) => chatwootController.receiveWebhook(instance, data),
});
res.status(HttpStatus.OK).json(response);
});
}

View File

@ -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);
}
}
}

View File

@ -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>('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],