diff --git a/src/api/integrations/kwik/controllers/kwik.controller.ts b/src/api/integrations/kwik/controllers/kwik.controller.ts new file mode 100644 index 00000000..b0e2d7ce --- /dev/null +++ b/src/api/integrations/kwik/controllers/kwik.controller.ts @@ -0,0 +1,38 @@ +// import { Logger } from '../../../../config/logger.config'; +import { WAMonitoringService } from '../../../../services/monitor.service'; +import { InstanceDto } from '../../../dto/instance.dto'; + +// const logger = new Logger('KwikController'); + +export class KwikController { + constructor(private readonly waMonitor: WAMonitoringService) {} + + public async fetchChats({ instanceName }: InstanceDto) { + const chats = await this.waMonitor.waInstances[instanceName].repository.chat.find({ + where: { owner: instanceName }, + }); + const mm = await Promise.all( + chats.map(async (chat) => { + const lastMsg = await this.waMonitor.waInstances[instanceName].repository.message.find({ + where: { + owner: instanceName, + key: { + remoteJid: chat.id, + }, + }, + limit: 1, + sort: { + messageTimestamp: -1, + }, + }); + + return { + ...chat._doc, + lastAllMsgTimestamp: lastMsg[0].messageTimestamp, + }; + }), + ); + + return mm; + } +} diff --git a/src/api/integrations/kwik/routes/kwik.router.ts b/src/api/integrations/kwik/routes/kwik.router.ts new file mode 100644 index 00000000..11bf4ad2 --- /dev/null +++ b/src/api/integrations/kwik/routes/kwik.router.ts @@ -0,0 +1,34 @@ +import { RequestHandler, Router } from 'express'; + +import { Logger } from '../../../../config/logger.config'; +import { RouterBroker } from '../../../abstract/abstract.router'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { HttpStatus } from '../../../routes/index.router'; +import { kwikController } from '../../../server.module'; + +const logger = new Logger('KwikRouter'); + +export class KwikRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router.get(this.routerPath('findChats'), ...guards, async (req, res) => { + logger.verbose('request received in findChats'); + logger.verbose('request body: '); + logger.verbose(req.body); + + logger.verbose('request query: '); + logger.verbose(req.query); + + const response = await this.dataValidate({ + request: req, + schema: null, + ClassRef: InstanceDto, + execute: (instance) => kwikController.fetchChats(instance), + }); + + return res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/api/integrations/kwik/services/kwik.service.ts b/src/api/integrations/kwik/services/kwik.service.ts new file mode 100644 index 00000000..911732a8 --- /dev/null +++ b/src/api/integrations/kwik/services/kwik.service.ts @@ -0,0 +1,230 @@ +import axios from 'axios'; +import { writeFileSync } from 'fs'; +import path from 'path'; + +import { ConfigService, HttpServer } from '../../../../config/env.config'; +import { Logger } from '../../../../config/logger.config'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { ChamaaiRaw } from '../../../models'; +import { WAMonitoringService } from '../../../services/monitor.service'; +import { Events } from '../../../types/wa.types'; +import { ChamaaiDto } from '../dto/chamaai.dto'; + +export class ChamaaiService { + constructor(private readonly waMonitor: WAMonitoringService, private readonly configService: ConfigService) {} + + private readonly logger = new Logger(ChamaaiService.name); + + public create(instance: InstanceDto, data: ChamaaiDto) { + this.logger.verbose('create chamaai: ' + instance.instanceName); + this.waMonitor.waInstances[instance.instanceName].setChamaai(data); + + return { chamaai: { ...instance, chamaai: data } }; + } + + public async find(instance: InstanceDto): Promise { + try { + this.logger.verbose('find chamaai: ' + instance.instanceName); + const result = await this.waMonitor.waInstances[instance.instanceName].findChamaai(); + + if (Object.keys(result).length === 0) { + throw new Error('Chamaai not found'); + } + + return result; + } catch (error) { + return { enabled: false, url: '', token: '', waNumber: '', answerByAudio: false }; + } + } + + private getTypeMessage(msg: any) { + this.logger.verbose('get type message'); + + const types = { + conversation: msg.conversation, + extendedTextMessage: msg.extendedTextMessage?.text, + }; + + 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; + + 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; + } + + private calculateTypingTime(text: string) { + const wordsPerMinute = 100; + + const wordCount = text.split(' ').length; + const typingTimeInMinutes = wordCount / wordsPerMinute; + const typingTimeInMilliseconds = typingTimeInMinutes * 60; + return typingTimeInMilliseconds; + } + + private convertToMilliseconds(count: number) { + const averageCharactersPerSecond = 15; + const characterCount = count; + const speakingTimeInSeconds = characterCount / averageCharactersPerSecond; + return speakingTimeInSeconds; + } + + private getRegexPatterns() { + const patternsToCheck = [ + '.*atend.*humano.*', + '.*falar.*com.*um.*humano.*', + '.*fala.*humano.*', + '.*atend.*humano.*', + '.*fala.*atend.*', + '.*preciso.*ajuda.*', + '.*quero.*suporte.*', + '.*preciso.*assiste.*', + '.*ajuda.*atend.*', + '.*chama.*atendente.*', + '.*suporte.*urgente.*', + '.*atend.*por.*favor.*', + '.*quero.*falar.*com.*alguém.*', + '.*falar.*com.*um.*humano.*', + '.*transfer.*humano.*', + '.*transfer.*atend.*', + '.*equipe.*humano.*', + '.*suporte.*humano.*', + ]; + + const regexPatterns = patternsToCheck.map((pattern) => new RegExp(pattern, 'iu')); + return regexPatterns; + } + + public async sendChamaai(instance: InstanceDto, remoteJid: string, msg: any) { + const content = this.getConversationMessage(msg.message); + const msgType = msg.messageType; + const find = await this.find(instance); + const url = find.url; + const token = find.token; + const waNumber = find.waNumber; + const answerByAudio = find.answerByAudio; + + if (!content && msgType !== 'audioMessage') { + return; + } + + let data; + let endpoint; + + if (msgType === 'audioMessage') { + const downloadBase64 = await this.waMonitor.waInstances[instance.instanceName].getBase64FromMediaMessage({ + message: { + ...msg, + }, + }); + + const random = Math.random().toString(36).substring(7); + const nameFile = `${random}.ogg`; + + const fileData = Buffer.from(downloadBase64.base64, 'base64'); + + const fileName = `${path.join( + this.waMonitor.waInstances[instance.instanceName].storePath, + 'temp', + `${nameFile}`, + )}`; + + writeFileSync(fileName, fileData, 'utf8'); + + const urlServer = this.configService.get('SERVER').URL; + + const url = `${urlServer}/store/temp/${nameFile}`; + + data = { + waNumber: waNumber, + audioUrl: url, + queryNumber: remoteJid.split('@')[0], + answerByAudio: answerByAudio, + }; + endpoint = 'processMessageAudio'; + } else { + data = { + waNumber: waNumber, + question: content, + queryNumber: remoteJid.split('@')[0], + answerByAudio: answerByAudio, + }; + endpoint = 'processMessageText'; + } + + const request = await axios.post(`${url}/${endpoint}`, data, { + headers: { + Authorization: `${token}`, + }, + }); + + const answer = request.data?.answer; + + const type = request.data?.type; + + const characterCount = request.data?.characterCount; + + if (answer) { + if (type === 'text') { + this.waMonitor.waInstances[instance.instanceName].textMessage({ + number: remoteJid.split('@')[0], + options: { + delay: this.calculateTypingTime(answer) * 1000 || 1000, + presence: 'composing', + linkPreview: false, + quoted: { + key: msg.key, + message: msg.message, + }, + }, + textMessage: { + text: answer, + }, + }); + } + + if (type === 'audio') { + this.waMonitor.waInstances[instance.instanceName].audioWhatsapp({ + number: remoteJid.split('@')[0], + options: { + delay: characterCount ? this.convertToMilliseconds(characterCount) * 1000 || 1000 : 1000, + presence: 'recording', + encoding: true, + }, + audioMessage: { + audio: answer, + }, + }); + } + + if (this.getRegexPatterns().some((pattern) => pattern.test(answer))) { + this.waMonitor.waInstances[instance.instanceName].sendDataWebhook(Events.CHAMA_AI_ACTION, { + remoteJid: remoteJid, + message: msg, + answer: answer, + action: 'transfer', + }); + } + } + } +} diff --git a/src/api/models/chat.model.ts b/src/api/models/chat.model.ts index 9e713a13..3c6a8a46 100644 --- a/src/api/models/chat.model.ts +++ b/src/api/models/chat.model.ts @@ -7,6 +7,7 @@ export class ChatRaw { id?: string; owner: string; lastMsgTimestamp?: number; + lastAllMsgTimestamp?: number | Long.Long; labels?: string[]; } diff --git a/src/api/repository/message.repository.ts b/src/api/repository/message.repository.ts index 9802bfae..d719de01 100644 --- a/src/api/repository/message.repository.ts +++ b/src/api/repository/message.repository.ts @@ -1,4 +1,5 @@ import { opendirSync, readFileSync, rmSync } from 'fs'; +import { SortOrder } from 'mongoose'; import { join } from 'path'; import { ConfigService, StoreConf } from '../../config/env.config'; @@ -10,6 +11,7 @@ export class MessageQuery { select?: MessageRawSelect; where: MessageRaw; limit?: number; + sort?: { [key: string]: SortOrder }; } export class MessageRepository extends Repository { @@ -22,6 +24,10 @@ export class MessageRepository extends Repository { public buildQuery(query: MessageQuery): MessageQuery { for (const [o, p] of Object.entries(query?.where || {})) { if (typeof p === 'object' && p !== null && !Array.isArray(p)) { + if (o === 'messageTimestamp') { + this.logger.verbose("Don't touch the messageTimestamp in where clause"); + continue; + } for (const [k, v] of Object.entries(p)) { query.where[`${o}.${k}`] = v; } @@ -119,7 +125,7 @@ export class MessageRepository extends Repository { return await this.messageModel .find({ ...query.where }) .select(query.select || {}) - .sort({ messageTimestamp: -1 }) + .sort(query?.sort ?? { messageTimestamp: -1 }) .limit(query?.limit ?? 0); } diff --git a/src/api/routes/index.router.ts b/src/api/routes/index.router.ts index 3b26671f..c4f48ab7 100644 --- a/src/api/routes/index.router.ts +++ b/src/api/routes/index.router.ts @@ -6,6 +6,7 @@ import { authGuard } from '../guards/auth.guard'; import { instanceExistsGuard, instanceLoggedGuard } from '../guards/instance.guard'; import { ChamaaiRouter } from '../integrations/chamaai/routes/chamaai.router'; import { ChatwootRouter } from '../integrations/chatwoot/routes/chatwoot.router'; +import { KwikRouter } from '../integrations/kwik/routes/kwik.router'; import { RabbitmqRouter } from '../integrations/rabbitmq/routes/rabbitmq.router'; import { SqsRouter } from '../integrations/sqs/routes/sqs.router'; import { TypebotRouter } from '../integrations/typebot/routes/typebot.router'; @@ -63,6 +64,7 @@ router .use('/typebot', new TypebotRouter(...guards).router) .use('/proxy', new ProxyRouter(...guards).router) .use('/chamaai', new ChamaaiRouter(...guards).router) - .use('/label', new LabelRouter(...guards).router); + .use('/label', new LabelRouter(...guards).router) + .use('/kwik', new KwikRouter(...guards).router); export { HttpStatus, router }; diff --git a/src/api/server.module.ts b/src/api/server.module.ts index 01560cb0..d6a335f3 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -17,6 +17,7 @@ import { ChamaaiService } from './integrations/chamaai/services/chamaai.service' import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller'; import { ChatwootRepository } from './integrations/chatwoot/repository/chatwoot.repository'; import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service'; +import { KwikController } from './integrations/kwik/controllers/kwik.controller'; import { RabbitmqController } from './integrations/rabbitmq/controllers/rabbitmq.controller'; import { RabbitmqRepository } from './integrations/rabbitmq/repository/rabbitmq.repository'; import { RabbitmqService } from './integrations/rabbitmq/services/rabbitmq.service'; @@ -182,5 +183,6 @@ export const sendMessageController = new SendMessageController(waMonitor); export const chatController = new ChatController(waMonitor); export const groupController = new GroupController(waMonitor); export const labelController = new LabelController(waMonitor); +export const kwikController = new KwikController(waMonitor); logger.info('Module - ON');