From 2606dbdac30d86a53eff63e761449c0f62d922d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 07:40:22 +0000 Subject: [PATCH] refactor: remove 6 chatbot integrations keeping only N8N Removed chatbot integrations: Chatwoot, Typebot, OpenAI, Dify, Flowise, and EvoAI. Only N8N integration remains active. Changes include: - Deleted all integration directories and files (controllers, services, DTOs, validators, routers) - Updated chatbot.controller.ts to only emit to N8N - Updated chatbot.router.ts and chatbot.schema.ts to export only N8N - Removed OpenAI dependency from N8nService (removed audio transcription) - Updated server.module.ts to remove all chatbot service instantiations - Cleaned monitor.service.ts and channel.service.ts from chatbot references - Removed chatbot properties from DTOs and validation schemas - Removed LocalChatwoot type and TYPEBOT events from wa.types - Cleaned PostgreSQL Prisma schema: removed 12 models and 2 enums - Removed chatbot relations from Instance model - Removed Chatwoot fields from Message model N8N remains as the only supported chatbot integration. --- prisma/mysql-schema.prisma | 12 - prisma/postgresql-schema.prisma | 318 -- src/api/dto/instance.dto.ts | 14 - .../chatbot/chatbot.controller.ts | 19 +- .../integrations/chatbot/chatbot.router.ts | 12 - .../integrations/chatbot/chatbot.schema.ts | 6 - .../controllers/chatwoot.controller.ts | 92 - .../chatbot/chatwoot/dto/chatwoot.dto.ts | 41 - .../chatbot/chatwoot/libs/postgres.client.ts | 47 - .../chatwoot/routes/chatwoot.router.ts | 46 - .../chatwoot/services/chatwoot.service.ts | 2611 ----------------- .../chatwoot/utils/chatwoot-import-helper.ts | 579 ---- .../chatwoot/validate/chatwoot.schema.ts | 45 - .../dify/controllers/dify.controller.ts | 126 - .../integrations/chatbot/dify/dto/dify.dto.ts | 13 - .../chatbot/dify/routes/dify.router.ts | 123 - .../chatbot/dify/services/dify.service.ts | 320 -- .../chatbot/dify/validate/dify.schema.ts | 116 - .../evoai/controllers/evoai.controller.ts | 122 - .../chatbot/evoai/dto/evoai.dto.ts | 10 - .../chatbot/evoai/routes/evoai.router.ts | 124 - .../chatbot/evoai/services/evoai.service.ts | 207 -- .../chatbot/evoai/validate/evoai.schema.ts | 115 - .../flowise/controllers/flowise.controller.ts | 118 - .../chatbot/flowise/dto/flowise.dto.ts | 10 - .../chatbot/flowise/routes/flowise.router.ts | 124 - .../flowise/services/flowise.service.ts | 150 - .../flowise/validate/flowise.schema.ts | 111 - .../chatbot/n8n/services/n8n.service.ts | 19 +- .../openai/controllers/openai.controller.ts | 482 --- .../chatbot/openai/dto/openai.dto.ts | 24 - .../chatbot/openai/routes/openai.router.ts | 164 -- .../chatbot/openai/services/openai.service.ts | 734 ----- .../chatbot/openai/validate/openai.schema.ts | 129 - .../typebot/controllers/typebot.controller.ts | 318 -- .../chatbot/typebot/dto/typebot.dto.ts | 17 - .../chatbot/typebot/routes/typebot.router.ts | 134 - .../typebot/services/typebot.service.ts | 1022 ------- .../typebot/validate/typebot.schema.ts | 97 - src/api/integrations/integration.dto.ts | 3 +- src/api/server.module.ts | 42 +- src/api/services/channel.service.ts | 177 +- src/api/services/monitor.service.ts | 16 +- src/api/types/wa.types.ts | 19 - src/validate/instance.schema.ts | 20 - 45 files changed, 9 insertions(+), 9039 deletions(-) delete mode 100644 src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/libs/postgres.client.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/routes/chatwoot.router.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts delete mode 100644 src/api/integrations/chatbot/chatwoot/validate/chatwoot.schema.ts delete mode 100644 src/api/integrations/chatbot/dify/controllers/dify.controller.ts delete mode 100644 src/api/integrations/chatbot/dify/dto/dify.dto.ts delete mode 100644 src/api/integrations/chatbot/dify/routes/dify.router.ts delete mode 100644 src/api/integrations/chatbot/dify/services/dify.service.ts delete mode 100644 src/api/integrations/chatbot/dify/validate/dify.schema.ts delete mode 100644 src/api/integrations/chatbot/evoai/controllers/evoai.controller.ts delete mode 100644 src/api/integrations/chatbot/evoai/dto/evoai.dto.ts delete mode 100644 src/api/integrations/chatbot/evoai/routes/evoai.router.ts delete mode 100644 src/api/integrations/chatbot/evoai/services/evoai.service.ts delete mode 100644 src/api/integrations/chatbot/evoai/validate/evoai.schema.ts delete mode 100644 src/api/integrations/chatbot/flowise/controllers/flowise.controller.ts delete mode 100644 src/api/integrations/chatbot/flowise/dto/flowise.dto.ts delete mode 100644 src/api/integrations/chatbot/flowise/routes/flowise.router.ts delete mode 100644 src/api/integrations/chatbot/flowise/services/flowise.service.ts delete mode 100644 src/api/integrations/chatbot/flowise/validate/flowise.schema.ts delete mode 100644 src/api/integrations/chatbot/openai/controllers/openai.controller.ts delete mode 100644 src/api/integrations/chatbot/openai/dto/openai.dto.ts delete mode 100644 src/api/integrations/chatbot/openai/routes/openai.router.ts delete mode 100644 src/api/integrations/chatbot/openai/services/openai.service.ts delete mode 100644 src/api/integrations/chatbot/openai/validate/openai.schema.ts delete mode 100644 src/api/integrations/chatbot/typebot/controllers/typebot.controller.ts delete mode 100644 src/api/integrations/chatbot/typebot/dto/typebot.dto.ts delete mode 100644 src/api/integrations/chatbot/typebot/routes/typebot.router.ts delete mode 100644 src/api/integrations/chatbot/typebot/services/typebot.service.ts delete mode 100644 src/api/integrations/chatbot/typebot/validate/typebot.schema.ts diff --git a/prisma/mysql-schema.prisma b/prisma/mysql-schema.prisma index 0f6e21af..fcaa36f9 100644 --- a/prisma/mysql-schema.prisma +++ b/prisma/mysql-schema.prisma @@ -48,18 +48,6 @@ enum TriggerOperator { regex } -enum OpenaiBotType { - assistant - chatCompletion -} - -enum DifyBotType { - chatBot - textGenerator - agent - workflow -} - model Instance { id String @id @default(cuid()) name String @unique @db.VarChar(255) diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 3acaa50f..711c7f2b 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -48,18 +48,6 @@ enum TriggerOperator { regex } -enum OpenaiBotType { - assistant - chatCompletion -} - -enum DifyBotType { - chatBot - textGenerator - agent - workflow -} - model Instance { id String @id @default(cuid()) name String @unique @db.VarChar(255) @@ -81,7 +69,6 @@ model Instance { Contact Contact[] Message Message[] Webhook Webhook? - Chatwoot Chatwoot? Label Label[] Proxy Proxy? Setting Setting? @@ -90,25 +77,14 @@ model Instance { Sqs Sqs? Kafka Kafka? Websocket Websocket? - Typebot Typebot[] Session Session? MessageUpdate MessageUpdate[] - TypebotSetting TypebotSetting? Media Media[] - OpenaiCreds OpenaiCreds[] - OpenaiBot OpenaiBot[] - OpenaiSetting OpenaiSetting? Template Template[] - Dify Dify[] - DifySetting DifySetting? IntegrationSession IntegrationSession[] - Flowise Flowise[] - FlowiseSetting FlowiseSetting? Pusher Pusher? N8n N8n[] N8nSetting N8nSetting[] - Evoai Evoai[] - EvoaiSetting EvoaiSetting? } model Session { @@ -159,11 +135,6 @@ model Message { contextInfo Json? @db.JsonB source DeviceMessage messageTimestamp Int @db.Integer - chatwootMessageId Int? @db.Integer - chatwootInboxId Int? @db.Integer - chatwootConversationId Int? @db.Integer - chatwootContactInboxSourceId String? @db.VarChar(100) - chatwootIsRead Boolean? @db.Boolean Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String MessageUpdate MessageUpdate[] @@ -210,31 +181,6 @@ model Webhook { @@index([instanceId]) } -model Chatwoot { - id String @id @default(cuid()) - enabled Boolean? @default(true) @db.Boolean - accountId String? @db.VarChar(100) - token String? @db.VarChar(100) - url String? @db.VarChar(500) - nameInbox String? @db.VarChar(100) - signMsg Boolean? @default(false) @db.Boolean - signDelimiter String? @db.VarChar(100) - number String? @db.VarChar(100) - reopenConversation Boolean? @default(false) @db.Boolean - conversationPending Boolean? @default(false) @db.Boolean - mergeBrazilContacts Boolean? @default(false) @db.Boolean - importContacts Boolean? @default(false) @db.Boolean - importMessages Boolean? @default(false) @db.Boolean - daysLimitImportMessages Int? @db.Integer - organization String? @db.VarChar(100) - logo String? @db.VarChar(500) - ignoreJids Json? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} - model Label { id String @id @default(cuid()) labelId String? @db.VarChar(100) @@ -346,54 +292,6 @@ model Pusher { instanceId String @unique } -model Typebot { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - url String @db.VarChar(500) - typebot String @db.VarChar(100) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime? @updatedAt @db.Timestamp - ignoreJids Json? - triggerType TriggerType? - triggerOperator TriggerOperator? - triggerValue String? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - TypebotSetting TypebotSetting[] -} - -model TypebotSetting { - id String @id @default(cuid()) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - typebotIdFallback String? @db.VarChar(100) - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id]) - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} - model Media { id String @id @default(cuid()) fileName String @db.VarChar(500) @@ -406,53 +304,6 @@ model Media { instanceId String } -model OpenaiCreds { - id String @id @default(cuid()) - name String? @unique @db.VarChar(255) - apiKey String? @unique @db.VarChar(255) - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - OpenaiAssistant OpenaiBot[] - OpenaiSetting OpenaiSetting? -} - -model OpenaiBot { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - botType OpenaiBotType - assistantId String? @db.VarChar(255) - functionUrl String? @db.VarChar(500) - model String? @db.VarChar(100) - systemMessages Json? @db.JsonB - assistantMessages Json? @db.JsonB - userMessages Json? @db.JsonB - maxTokens Int? @db.Integer - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - ignoreJids Json? - triggerType TriggerType? - triggerOperator TriggerOperator? - triggerValue String? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - OpenaiCreds OpenaiCreds @relation(fields: [openaiCredsId], references: [id], onDelete: Cascade) - openaiCredsId String - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - OpenaiSetting OpenaiSetting[] -} - model IntegrationSession { id String @id @default(cuid()) sessionId String @db.VarChar(255) @@ -472,30 +323,6 @@ model IntegrationSession { botId String? } -model OpenaiSetting { - id String @id @default(cuid()) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - speechToText Boolean? @default(false) @db.Boolean - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id]) - openaiCredsId String @unique - Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id]) - openaiIdFallback String? @db.VarChar(100) - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} - model Template { id String @id @default(cuid()) templateId String @unique @db.VarChar(255) @@ -508,103 +335,6 @@ model Template { instanceId String } -model Dify { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - botType DifyBotType - apiUrl String? @db.VarChar(255) - apiKey String? @db.VarChar(255) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - triggerType TriggerType? - triggerOperator TriggerOperator? - triggerValue String? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - DifySetting DifySetting[] -} - -model DifySetting { - id String @id @default(cuid()) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Fallback Dify? @relation(fields: [difyIdFallback], references: [id]) - difyIdFallback String? @db.VarChar(100) - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} - -model Flowise { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - apiUrl String? @db.VarChar(255) - apiKey String? @db.VarChar(255) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - triggerType TriggerType? - triggerOperator TriggerOperator? - triggerValue String? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - FlowiseSetting FlowiseSetting[] -} - -model FlowiseSetting { - id String @id @default(cuid()) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Fallback Flowise? @relation(fields: [flowiseIdFallback], references: [id]) - flowiseIdFallback String? @db.VarChar(100) - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} - model IsOnWhatsapp { id String @id @default(cuid()) remoteJid String @unique @db.VarChar(100) @@ -662,51 +392,3 @@ model N8nSetting { Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String @unique } - -model Evoai { - id String @id @default(cuid()) - enabled Boolean @default(true) @db.Boolean - description String? @db.VarChar(255) - agentUrl String? @db.VarChar(255) - apiKey String? @db.VarChar(255) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - triggerType TriggerType? - triggerOperator TriggerOperator? - triggerValue String? - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String - EvoaiSetting EvoaiSetting[] -} - -model EvoaiSetting { - id String @id @default(cuid()) - expire Int? @default(0) @db.Integer - keywordFinish String? @db.VarChar(100) - delayMessage Int? @db.Integer - unknownMessage String? @db.VarChar(100) - listeningFromMe Boolean? @default(false) @db.Boolean - stopBotFromMe Boolean? @default(false) @db.Boolean - keepOpen Boolean? @default(false) @db.Boolean - debounceTime Int? @db.Integer - ignoreJids Json? - splitMessages Boolean? @default(false) @db.Boolean - timePerChar Int? @default(50) @db.Integer - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - Fallback Evoai? @relation(fields: [evoaiIdFallback], references: [id]) - evoaiIdFallback String? @db.VarChar(100) - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId String @unique -} diff --git a/src/api/dto/instance.dto.ts b/src/api/dto/instance.dto.ts index 1da3bf1c..a4fb83f4 100644 --- a/src/api/dto/instance.dto.ts +++ b/src/api/dto/instance.dto.ts @@ -37,20 +37,6 @@ export class InstanceDto extends IntegrationDto { byEvents?: boolean; base64?: boolean; }; - chatwootAccountId?: string; - chatwootConversationPending?: boolean; - chatwootAutoCreate?: boolean; - chatwootDaysLimitImportMessages?: number; - chatwootImportContacts?: boolean; - chatwootImportMessages?: boolean; - chatwootLogo?: string; - chatwootMergeBrazilContacts?: boolean; - chatwootNameInbox?: string; - chatwootOrganization?: string; - chatwootReopenConversation?: boolean; - chatwootSignMsg?: boolean; - chatwootToken?: string; - chatwootUrl?: string; } export class SetPresenceDto { diff --git a/src/api/integrations/chatbot/chatbot.controller.ts b/src/api/integrations/chatbot/chatbot.controller.ts index 7890cfcd..5d9fab7c 100644 --- a/src/api/integrations/chatbot/chatbot.controller.ts +++ b/src/api/integrations/chatbot/chatbot.controller.ts @@ -1,13 +1,6 @@ import { InstanceDto } from '@api/dto/instance.dto'; import { PrismaRepository } from '@api/repository/repository.service'; -import { - difyController, - evoaiController, - flowiseController, - n8nController, - openaiController, - typebotController, -} from '@api/server.module'; +import { n8nController } from '@api/server.module'; import { WAMonitoringService } from '@api/services/monitor.service'; import { Logger } from '@config/logger.config'; import { IntegrationSession } from '@prisma/client'; @@ -91,17 +84,7 @@ export class ChatbotController { isIntegration, }; - typebotController.emit(emitData); - - openaiController.emit(emitData); - - difyController.emit(emitData); - n8nController.emit(emitData); - - evoaiController.emit(emitData); - - flowiseController.emit(emitData); } public processDebounce( diff --git a/src/api/integrations/chatbot/chatbot.router.ts b/src/api/integrations/chatbot/chatbot.router.ts index 50d6db4c..2e896873 100644 --- a/src/api/integrations/chatbot/chatbot.router.ts +++ b/src/api/integrations/chatbot/chatbot.router.ts @@ -1,11 +1,5 @@ -import { ChatwootRouter } from '@api/integrations/chatbot/chatwoot/routes/chatwoot.router'; -import { DifyRouter } from '@api/integrations/chatbot/dify/routes/dify.router'; -import { OpenaiRouter } from '@api/integrations/chatbot/openai/routes/openai.router'; -import { TypebotRouter } from '@api/integrations/chatbot/typebot/routes/typebot.router'; import { Router } from 'express'; -import { EvoaiRouter } from './evoai/routes/evoai.router'; -import { FlowiseRouter } from './flowise/routes/flowise.router'; import { N8nRouter } from './n8n/routes/n8n.router'; export class ChatbotRouter { @@ -14,12 +8,6 @@ export class ChatbotRouter { constructor(...guards: any[]) { this.router = Router(); - this.router.use('/chatwoot', new ChatwootRouter(...guards).router); - this.router.use('/typebot', new TypebotRouter(...guards).router); - this.router.use('/openai', new OpenaiRouter(...guards).router); - this.router.use('/dify', new DifyRouter(...guards).router); - this.router.use('/flowise', new FlowiseRouter(...guards).router); this.router.use('/n8n', new N8nRouter(...guards).router); - this.router.use('/evoai', new EvoaiRouter(...guards).router); } } diff --git a/src/api/integrations/chatbot/chatbot.schema.ts b/src/api/integrations/chatbot/chatbot.schema.ts index aa904d8e..ddbd0609 100644 --- a/src/api/integrations/chatbot/chatbot.schema.ts +++ b/src/api/integrations/chatbot/chatbot.schema.ts @@ -1,7 +1 @@ -export * from '@api/integrations/chatbot/chatwoot/validate/chatwoot.schema'; -export * from '@api/integrations/chatbot/dify/validate/dify.schema'; -export * from '@api/integrations/chatbot/evoai/validate/evoai.schema'; -export * from '@api/integrations/chatbot/flowise/validate/flowise.schema'; export * from '@api/integrations/chatbot/n8n/validate/n8n.schema'; -export * from '@api/integrations/chatbot/openai/validate/openai.schema'; -export * from '@api/integrations/chatbot/typebot/validate/typebot.schema'; diff --git a/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts b/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts deleted file mode 100644 index 17cdce01..00000000 --- a/src/api/integrations/chatbot/chatwoot/controllers/chatwoot.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { InstanceDto } from '@api/dto/instance.dto'; -import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; -import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service'; -import { PrismaRepository } from '@api/repository/repository.service'; -import { waMonitor } from '@api/server.module'; -import { CacheService } from '@api/services/cache.service'; -import { CacheEngine } from '@cache/cacheengine'; -import { Chatwoot, ConfigService, HttpServer } from '@config/env.config'; -import { BadRequestException } from '@exceptions'; -import { isURL } from 'class-validator'; - -export class ChatwootController { - constructor( - private readonly chatwootService: ChatwootService, - private readonly configService: ConfigService, - private readonly prismaRepository: PrismaRepository, - ) {} - - public async createChatwoot(instance: InstanceDto, data: ChatwootDto) { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); - - if (data?.enabled) { - if (!isURL(data.url, { require_tld: false })) { - throw new BadRequestException('url is not valid'); - } - - if (!data.accountId) { - throw new BadRequestException('accountId is required'); - } - - if (!data.token) { - throw new BadRequestException('token is required'); - } - - if (data.signMsg !== true && data.signMsg !== false) { - throw new BadRequestException('signMsg is required'); - } - if (data.signMsg === false) data.signDelimiter = null; - } - - if (!data.nameInbox || data.nameInbox === '') { - data.nameInbox = instance.instanceName; - } - - const result = await this.chatwootService.create(instance, data); - - const urlServer = this.configService.get('SERVER').URL; - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, - }; - - return response; - } - - public async findChatwoot(instance: InstanceDto): Promise { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); - - const result = await this.chatwootService.find(instance); - - const urlServer = this.configService.get('SERVER').URL; - - if (Object.keys(result || {}).length === 0) { - return { - enabled: false, - url: '', - accountId: '', - token: '', - signMsg: false, - nameInbox: '', - webhook_url: '', - }; - } - - const response = { - ...result, - webhook_url: `${urlServer}/chatwoot/webhook/${encodeURIComponent(instance.instanceName)}`, - }; - - return response; - } - - public async receiveWebhook(instance: InstanceDto, data: any) { - if (!this.configService.get('CHATWOOT').ENABLED) throw new BadRequestException('Chatwoot is disabled'); - - const chatwootCache = new CacheService(new CacheEngine(this.configService, ChatwootService.name).getEngine()); - const chatwootService = new ChatwootService(waMonitor, this.configService, this.prismaRepository, chatwootCache); - - return chatwootService.receiveWebhook(instance, data); - } -} diff --git a/src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts b/src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts deleted file mode 100644 index 4abf468f..00000000 --- a/src/api/integrations/chatbot/chatwoot/dto/chatwoot.dto.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Constructor } from '@api/integrations/integration.dto'; - -export class ChatwootDto { - enabled?: boolean; - accountId?: string; - token?: string; - url?: string; - nameInbox?: string; - signMsg?: boolean; - signDelimiter?: string; - number?: string; - reopenConversation?: boolean; - conversationPending?: boolean; - mergeBrazilContacts?: boolean; - importContacts?: boolean; - importMessages?: boolean; - daysLimitImportMessages?: number; - autoCreate?: boolean; - organization?: string; - logo?: string; - ignoreJids?: string[]; -} - -export function ChatwootInstanceMixin(Base: TBase) { - return class extends Base { - chatwootAccountId?: string; - chatwootToken?: string; - chatwootUrl?: string; - chatwootSignMsg?: boolean; - chatwootReopenConversation?: boolean; - chatwootConversationPending?: boolean; - chatwootMergeBrazilContacts?: boolean; - chatwootImportContacts?: boolean; - chatwootImportMessages?: boolean; - chatwootDaysLimitImportMessages?: number; - chatwootNameInbox?: string; - chatwootOrganization?: string; - chatwootLogo?: string; - chatwootAutoCreate?: boolean; - }; -} diff --git a/src/api/integrations/chatbot/chatwoot/libs/postgres.client.ts b/src/api/integrations/chatbot/chatwoot/libs/postgres.client.ts deleted file mode 100644 index 3e3e9685..00000000 --- a/src/api/integrations/chatbot/chatwoot/libs/postgres.client.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Chatwoot, configService } from '@config/env.config'; -import { Logger } from '@config/logger.config'; -import postgresql from 'pg'; - -const { Pool } = postgresql; - -class Postgres { - private logger = new Logger('Postgres'); - private pool; - private connected = false; - - getConnection(connectionString: string) { - if (this.connected) { - return this.pool; - } else { - this.pool = new Pool({ - connectionString, - ssl: { - rejectUnauthorized: false, - }, - }); - - this.pool.on('error', () => { - this.logger.error('postgres disconnected'); - this.connected = false; - }); - - try { - this.connected = true; - } catch (e) { - this.connected = false; - this.logger.error('postgres connect exception caught: ' + e); - return null; - } - - return this.pool; - } - } - - getChatwootConnection() { - const uri = configService.get('CHATWOOT').IMPORT.DATABASE.CONNECTION.URI; - - return this.getConnection(uri); - } -} - -export const postgresClient = new Postgres(); diff --git a/src/api/integrations/chatbot/chatwoot/routes/chatwoot.router.ts b/src/api/integrations/chatbot/chatwoot/routes/chatwoot.router.ts deleted file mode 100644 index 51b23ab5..00000000 --- a/src/api/integrations/chatbot/chatwoot/routes/chatwoot.router.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { RouterBroker } from '@api/abstract/abstract.router'; -import { InstanceDto } from '@api/dto/instance.dto'; -import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; -import { HttpStatus } from '@api/routes/index.router'; -import { chatwootController } from '@api/server.module'; -import { chatwootSchema, instanceSchema } from '@validate/validate.schema'; -import { RequestHandler, Router } from 'express'; - -export class ChatwootRouter extends RouterBroker { - constructor(...guards: RequestHandler[]) { - super(); - this.router - .post(this.routerPath('set'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: chatwootSchema, - ClassRef: ChatwootDto, - execute: (instance, data) => chatwootController.createChatwoot(instance, data), - }); - - res.status(HttpStatus.CREATED).json(response); - }) - .get(this.routerPath('find'), ...guards, async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: instanceSchema, - ClassRef: InstanceDto, - execute: (instance) => chatwootController.findChatwoot(instance), - }); - - res.status(HttpStatus.OK).json(response); - }) - .post(this.routerPath('webhook'), async (req, res) => { - const response = await this.dataValidate({ - request: req, - schema: instanceSchema, - ClassRef: InstanceDto, - execute: (instance, data) => chatwootController.receiveWebhook(instance, data), - }); - - res.status(HttpStatus.OK).json(response); - }); - } - - public readonly router: Router = Router(); -} diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts deleted file mode 100644 index cc2bd9e4..00000000 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ /dev/null @@ -1,2611 +0,0 @@ -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 { WAMessageContent, WAMessageKey } 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'); - - // Lock polling delay - private readonly LOCK_POLLING_DELAY_MS = 300; // Delay between lock status checks - - 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 { - 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('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 { - try { - return await this.waMonitor.waInstances[instance.instanceName].findChatwoot(); - } catch { - 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').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 { - return null; - } - } - - public async addLabelToContact(nameInbox: string, contactId: number) { - try { - const uri = this.configService.get('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 { - 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) { - const isLid = body.key.addressingMode === 'lid'; - const isGroup = body.key.remoteJid.endsWith('@g.us'); - const phoneNumber = isLid && !isGroup ? body.key.remoteJidAlt : body.key.remoteJid; - const { remoteJid } = body.key; - const cacheKey = `${instance.instanceName}:createConversation-${remoteJid}`; - const lockKey = `${instance.instanceName}:lock:createConversation-${remoteJid}`; - const maxWaitTime = 5000; // 5 seconds - const client = await this.clientCw(instance); - if (!client) return null; - - try { - // Processa atualização de contatos já criados @lid - if (phoneNumber && remoteJid && !isGroup) { - const contact = await this.findContact(instance, phoneNumber.split('@')[0]); - if (contact && contact.identifier !== remoteJid) { - this.logger.verbose( - `Identifier needs update: (contact.identifier: ${contact.identifier}, phoneNumber: ${phoneNumber}, body.key.remoteJidAlt: ${remoteJid}`, - ); - const updateContact = await this.updateContact(instance, contact.id, { - identifier: remoteJid, - phone_number: `+${phoneNumber.split('@')[0]}`, - }); - - if (updateContact === null) { - const baseContact = await this.findContact(instance, phoneNumber.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: ${phoneNumber}, conversation ID: ${conversationId}`); - let conversationExists: conversation | boolean; - try { - conversationExists = await client.conversations.get({ - accountId: this.provider.accountId, - conversationId: conversationId, - }); - this.logger.verbose(`Conversation exists: ${JSON.stringify(conversationExists)}`); - } catch (error) { - this.logger.error(`Error getting conversation: ${error}`); - conversationExists = false; - } - if (!conversationExists) { - this.logger.verbose('Conversation does not exist, re-calling createConversation'); - this.cache.delete(cacheKey); - return await this.createConversation(instance, body); - } - 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, this.LOCK_POLLING_DELAY_MS)); - 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 chatId = isGroup ? remoteJid : phoneNumber.split('@')[0].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)}`); - - const participantJid = isLid && !body.key.fromMe ? body.key.participantAlt : body.key.participant; - nameContact = `${group.subject} (GROUP)`; - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture( - participantJid.split('@')[0], - ); - this.logger.verbose(`Participant profile picture URL: ${JSON.stringify(picture_url)}`); - - const findParticipant = await this.findContact(instance, participantJid.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, - participantJid.split('@')[0], - filterInbox.id, - false, - body.pushName, - picture_url.profilePictureUrl || null, - participantJid, - ); - } - } - - const picture_url = await this.waMonitor.waInstances[instance.instanceName].profilePicture(chatId); - this.logger.verbose(`Contact profile picture URL: ${JSON.stringify(picture_url)}`); - - this.logger.verbose(`Searching contact for: ${chatId}`); - 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, 8 * 3600); - 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, 8 * 3600); - 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 { - 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 { - 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').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: Math.floor(Math.random() * (2000 - 500 + 1)) + 500, - quoted: options?.quoted, - }; - - sendTelemetry('/message/sendWhatsAppAudio'); - - const messageSent = await waInstance?.audioWhatsapp(data, null, true); - - return messageSent; - } - - const documentExtensions = ['.gif', '.svg', '.tiff', '.tif', '.dxf', '.dwg']; - 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); - throw error; // Re-throw para que o erro seja tratado pelo caller - } - } - - 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(/(?('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:' && - body?.conversation?.messages[0]?.id === body?.id - ) { - 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, - }, - { - 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: Math.floor(Math.random() * (2000 - 500 + 1)) + 500, - 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, - }, - { - 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').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 WAMessageKey; - - 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: Math.floor(Math.random() * (2000 - 500 + 1)) + 500, - }; - - 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 WAMessageKey; - - if (!chatwootMessageIds.messageId || !key?.id) { - return; - } - - // Use raw SQL to avoid JSON path issues - const result = await this.prismaRepository.$executeRaw` - UPDATE "Message" - SET - "chatwootMessageId" = ${chatwootMessageIds.messageId}, - "chatwootConversationId" = ${chatwootMessageIds.conversationId}, - "chatwootInboxId" = ${chatwootMessageIds.inboxId}, - "chatwootContactInboxSourceId" = ${chatwootMessageIds.contactInboxSourceId}, - "chatwootIsRead" = ${chatwootMessageIds.isRead || false} - WHERE "instanceId" = ${instance.instanceId} - AND "key"->>'id' = ${key.id} - `; - - this.logger.verbose(`Update result: ${result} rows affected`); - - if (this.isImportHistoryAvailable()) { - chatwootImport.updateMessageSourceID(chatwootMessageIds.messageId, key.id); - } - } - - private async getMessageByKeyId(instance: InstanceDto, keyId: string): Promise { - // Use raw SQL query to avoid JSON path issues with Prisma - const messages = await this.prismaRepository.$queryRaw` - SELECT * FROM "Message" - WHERE "instanceId" = ${instance.instanceId} - AND "key"->>'id' = ${keyId} - LIMIT 1 - `; - - return (messages as MessageModel[])[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 ?? 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 { - 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 WAMessageKey; - const messageContent = message?.message as WAMessageContent; - - if (messageContent && key?.id) { - return { - key: key, - message: messageContent, - }; - } - } - - 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 { - 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') { - this.logger.info(`[${event}] New message received - Instance: ${JSON.stringify(body, null, 2)}`); - 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*]+?)(? {}; - fileStream.push(fileData); - fileStream.push(null); - - if (body.key.remoteJid.includes('@g.us')) { - const participantName = body.pushName; - const rawPhoneNumber = - body.key.addressingMode === 'lid' && !body.key.fromMe - ? body.key.participantAlt.split('@')[0] - : 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 = bodyMessage - ? `**${formattedPhoneNumber} - ${participantName}:**\n\n${bodyMessage}` - : `**${formattedPhoneNumber} - ${participantName}:**`; - } 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.addressingMode === 'lid' && !body.key.fromMe - ? body.key.participantAlt.split('@')[0] - : 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').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') { - const editedMessageContent = - body?.editedMessage?.conversation || body?.editedMessage?.extendedTextMessage?.text; - const message = await this.getMessageByKeyId(instance, body?.key?.id); - - if (!message) { - this.logger.warn('Message not found for edit event'); - return; - } - - const key = message.key as WAMessageKey; - - const messageType = key?.fromMe ? 'outgoing' : 'incoming'; - - if (message && message.chatwootConversationId && message.chatwootMessageId) { - // Criar nova mensagem com formato: "Mensagem editada:\n\nteste1" - const editedText = `\n\n\`${i18next.t('cw.message.edited')}:\`\n\n${editedMessageContent}`; - - 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' && body.status === 'open') { - const waInstance = this.waMonitor.waInstances[instance.instanceName]; - if (!waInstance) return; - - const now = Date.now(); - const timeSinceLastNotification = now - (waInstance.lastConnectionNotification || 0); - - // Se a conexão foi estabelecida via QR code, notifica imediatamente. - if (waInstance.qrCode && waInstance.qrCode.count > 0) { - const msgConnection = i18next.t('cw.inbox.connected'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - waInstance.qrCode.count = 0; - waInstance.lastConnectionNotification = now; - chatwootImport.clearAll(instance); - } - // Se não foi via QR code, verifica o throttling. - else if (timeSinceLastNotification >= 30000) { - const msgConnection = i18next.t('cw.inbox.connected'); - await this.createBotMessage(instance, msgConnection, 'incoming'); - waInstance.lastConnectionNotification = now; - } else { - this.logger.warn( - `Connection notification skipped for ${instance.instanceName} - too frequent (${timeSinceLastNotification}ms since last)`, - ); - } - } - - 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').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, 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').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: Number(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 { - return; - } - } -} diff --git a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts b/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts deleted file mode 100644 index a4fd01df..00000000 --- a/src/api/integrations/chatbot/chatwoot/utils/chatwoot-import-helper.ts +++ /dev/null @@ -1,579 +0,0 @@ -import { InstanceDto } from '@api/dto/instance.dto'; -import { ChatwootDto } from '@api/integrations/chatbot/chatwoot/dto/chatwoot.dto'; -import { postgresClient } from '@api/integrations/chatbot/chatwoot/libs/postgres.client'; -import { ChatwootService } from '@api/integrations/chatbot/chatwoot/services/chatwoot.service'; -import { Chatwoot, configService } from '@config/env.config'; -import { Logger } from '@config/logger.config'; -import { inbox } from '@figuro/chatwoot-sdk'; -import { Chatwoot as ChatwootModel, Contact, Message } from '@prisma/client'; -import { proto } from 'baileys'; - -type ChatwootUser = { - user_type: string; - user_id: number; -}; - -type FksChatwoot = { - phone_number: string; - contact_id: string; - conversation_id: string; -}; - -type firstLastTimestamp = { - first: number; - last: number; -}; - -type IWebMessageInfo = Omit & Partial>; - -class ChatwootImport { - private logger = new Logger('ChatwootImport'); - private repositoryMessagesCache = new Map>(); - private historyMessages = new Map(); - private historyContacts = new Map(); - - public getRepositoryMessagesCache(instance: InstanceDto) { - return this.repositoryMessagesCache.has(instance.instanceName) - ? this.repositoryMessagesCache.get(instance.instanceName) - : null; - } - - public setRepositoryMessagesCache(instance: InstanceDto, repositoryMessagesCache: Set) { - this.repositoryMessagesCache.set(instance.instanceName, repositoryMessagesCache); - } - - public deleteRepositoryMessagesCache(instance: InstanceDto) { - this.repositoryMessagesCache.delete(instance.instanceName); - } - - public addHistoryMessages(instance: InstanceDto, messagesRaw: Message[]) { - const actualValue = this.historyMessages.has(instance.instanceName) - ? this.historyMessages.get(instance.instanceName) - : []; - this.historyMessages.set(instance.instanceName, [...actualValue, ...messagesRaw]); - } - - public addHistoryContacts(instance: InstanceDto, contactsRaw: Contact[]) { - const actualValue = this.historyContacts.has(instance.instanceName) - ? this.historyContacts.get(instance.instanceName) - : []; - this.historyContacts.set(instance.instanceName, actualValue.concat(contactsRaw)); - } - - public deleteHistoryMessages(instance: InstanceDto) { - this.historyMessages.delete(instance.instanceName); - } - - public deleteHistoryContacts(instance: InstanceDto) { - this.historyContacts.delete(instance.instanceName); - } - - public clearAll(instance: InstanceDto) { - this.deleteRepositoryMessagesCache(instance); - this.deleteHistoryMessages(instance); - this.deleteHistoryContacts(instance); - } - - public getHistoryMessagesLenght(instance: InstanceDto) { - return this.historyMessages.get(instance.instanceName)?.length ?? 0; - } - - public async importHistoryContacts(instance: InstanceDto, provider: ChatwootDto) { - try { - if (this.getHistoryMessagesLenght(instance) > 0) { - return; - } - - const pgClient = postgresClient.getChatwootConnection(); - - let totalContactsImported = 0; - - const contacts = this.historyContacts.get(instance.instanceName) || []; - if (contacts.length === 0) { - return 0; - } - - let contactsChunk: Contact[] = this.sliceIntoChunks(contacts, 3000); - while (contactsChunk.length > 0) { - const labelSql = `SELECT id FROM labels WHERE title = '${provider.nameInbox}' AND account_id = ${provider.accountId} LIMIT 1`; - - let labelId = (await pgClient.query(labelSql))?.rows[0]?.id; - - if (!labelId) { - // creating label in chatwoot db and getting the id - const sqlLabel = `INSERT INTO labels (title, color, show_on_sidebar, account_id, created_at, updated_at) VALUES ('${provider.nameInbox}', '#34039B', true, ${provider.accountId}, NOW(), NOW()) RETURNING id`; - - labelId = (await pgClient.query(sqlLabel))?.rows[0]?.id; - } - - // inserting contacts in chatwoot db - let sqlInsert = `INSERT INTO contacts - (name, phone_number, account_id, identifier, created_at, updated_at) VALUES `; - const bindInsert = [provider.accountId]; - - for (const contact of contactsChunk) { - const isGroup = this.isIgnorePhoneNumber(contact.remoteJid); - - const contactName = isGroup ? `${contact.pushName} (GROUP)` : contact.pushName; - bindInsert.push(contactName); - const bindName = `$${bindInsert.length}`; - - let bindPhoneNumber: string; - if (!isGroup) { - bindInsert.push(`+${contact.remoteJid.split('@')[0]}`); - bindPhoneNumber = `$${bindInsert.length}`; - } else { - bindPhoneNumber = 'NULL'; - } - bindInsert.push(contact.remoteJid); - const bindIdentifier = `$${bindInsert.length}`; - - sqlInsert += `(${bindName}, ${bindPhoneNumber}, $1, ${bindIdentifier}, NOW(), NOW()),`; - } - if (sqlInsert.slice(-1) === ',') { - sqlInsert = sqlInsert.slice(0, -1); - } - sqlInsert += ` ON CONFLICT (identifier, account_id) - DO UPDATE SET - name = EXCLUDED.name, - phone_number = EXCLUDED.phone_number, - identifier = EXCLUDED.identifier`; - - totalContactsImported += (await pgClient.query(sqlInsert, bindInsert))?.rowCount ?? 0; - - const sqlTags = `SELECT id FROM tags WHERE name = '${provider.nameInbox}' LIMIT 1`; - - const tagData = (await pgClient.query(sqlTags))?.rows[0]; - let tagId = tagData?.id; - - const sqlTag = `INSERT INTO tags (name, taggings_count) VALUES ('${provider.nameInbox}', ${totalContactsImported}) ON CONFLICT (name) DO UPDATE SET taggings_count = tags.taggings_count + ${totalContactsImported} RETURNING id`; - - tagId = (await pgClient.query(sqlTag))?.rows[0]?.id; - - await pgClient.query(sqlTag); - - let sqlInsertLabel = `INSERT INTO taggings (tag_id, taggable_type, taggable_id, context, created_at) VALUES `; - - contactsChunk.forEach((contact) => { - const bindTaggableId = `(SELECT id FROM contacts WHERE identifier = '${contact.remoteJid}' AND account_id = ${provider.accountId})`; - sqlInsertLabel += `($1, $2, ${bindTaggableId}, $3, NOW()),`; - }); - - if (sqlInsertLabel.slice(-1) === ',') { - sqlInsertLabel = sqlInsertLabel.slice(0, -1); - } - - await pgClient.query(sqlInsertLabel, [tagId, 'Contact', 'labels']); - - contactsChunk = this.sliceIntoChunks(contacts, 3000); - } - - this.deleteHistoryContacts(instance); - - return totalContactsImported; - } catch (error) { - this.logger.error(`Error on import history contacts: ${error.toString()}`); - } - } - - public async getExistingSourceIds(sourceIds: string[], conversationId?: number): Promise> { - try { - const existingSourceIdsSet = new Set(); - - if (sourceIds.length === 0) { - return existingSourceIdsSet; - } - - // Ensure all sourceIds are consistently prefixed with 'WAID:' as required by downstream systems and database queries. - const formattedSourceIds = sourceIds.map((sourceId) => `WAID:${sourceId.replace('WAID:', '')}`); - const pgClient = postgresClient.getChatwootConnection(); - - const params = conversationId ? [formattedSourceIds, conversationId] : [formattedSourceIds]; - - const query = conversationId - ? 'SELECT source_id FROM messages WHERE source_id = ANY($1) AND conversation_id = $2' - : 'SELECT source_id FROM messages WHERE source_id = ANY($1)'; - - const result = await pgClient.query(query, params); - for (const row of result.rows) { - existingSourceIdsSet.add(row.source_id); - } - - return existingSourceIdsSet; - } catch (error) { - this.logger.error(`Error on getExistingSourceIds: ${error.toString()}`); - return new Set(); - } - } - - public async importHistoryMessages( - instance: InstanceDto, - chatwootService: ChatwootService, - inbox: inbox, - provider: ChatwootModel, - ) { - try { - const pgClient = postgresClient.getChatwootConnection(); - - const chatwootUser = await this.getChatwootUser(provider); - if (!chatwootUser) { - throw new Error('User not found to import messages.'); - } - - let totalMessagesImported = 0; - - let messagesOrdered = this.historyMessages.get(instance.instanceName) || []; - if (messagesOrdered.length === 0) { - return 0; - } - - // ordering messages by number and timestamp asc - messagesOrdered.sort((a, b) => { - const aKey = a.key as { - remoteJid: string; - }; - - const bKey = b.key as { - remoteJid: string; - }; - - const aMessageTimestamp = a.messageTimestamp as any as number; - const bMessageTimestamp = b.messageTimestamp as any as number; - - return parseInt(aKey.remoteJid) - parseInt(bKey.remoteJid) || aMessageTimestamp - bMessageTimestamp; - }); - - const allMessagesMappedByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesOrdered); - // Map structure: +552199999999 => { first message timestamp from number, last message timestamp from number} - const phoneNumbersWithTimestamp = new Map(); - allMessagesMappedByPhoneNumber.forEach((messages: Message[], phoneNumber: string) => { - phoneNumbersWithTimestamp.set(phoneNumber, { - first: messages[0]?.messageTimestamp as any as number, - last: messages[messages.length - 1]?.messageTimestamp as any as number, - }); - }); - - const existingSourceIds = await this.getExistingSourceIds(messagesOrdered.map((message: any) => message.key.id)); - messagesOrdered = messagesOrdered.filter((message: any) => !existingSourceIds.has(message.key.id)); - // processing messages in batch - const batchSize = 4000; - let messagesChunk: Message[] = this.sliceIntoChunks(messagesOrdered, batchSize); - while (messagesChunk.length > 0) { - // Map structure: +552199999999 => Message[] - const messagesByPhoneNumber = this.createMessagesMapByPhoneNumber(messagesChunk); - - if (messagesByPhoneNumber.size > 0) { - const fksByNumber = await this.selectOrCreateFksFromChatwoot( - provider, - inbox, - phoneNumbersWithTimestamp, - messagesByPhoneNumber, - ); - - // inserting messages in chatwoot db - let sqlInsertMsg = `INSERT INTO messages - (content, processed_message_content, account_id, inbox_id, conversation_id, message_type, private, content_type, - sender_type, sender_id, source_id, created_at, updated_at) VALUES `; - const bindInsertMsg = [provider.accountId, inbox.id]; - - messagesByPhoneNumber.forEach((messages: any[], phoneNumber: string) => { - const fksChatwoot = fksByNumber.get(phoneNumber); - - messages.forEach((message) => { - if (!message.message) { - return; - } - - if (!fksChatwoot?.conversation_id || !fksChatwoot?.contact_id) { - return; - } - - const contentMessage = this.getContentMessage(chatwootService, message); - if (!contentMessage) { - return; - } - - bindInsertMsg.push(contentMessage); - const bindContent = `$${bindInsertMsg.length}`; - - bindInsertMsg.push(fksChatwoot.conversation_id); - const bindConversationId = `$${bindInsertMsg.length}`; - - bindInsertMsg.push(message.key.fromMe ? '1' : '0'); - const bindMessageType = `$${bindInsertMsg.length}`; - - bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_type : 'Contact'); - const bindSenderType = `$${bindInsertMsg.length}`; - - bindInsertMsg.push(message.key.fromMe ? chatwootUser.user_id : fksChatwoot.contact_id); - const bindSenderId = `$${bindInsertMsg.length}`; - - bindInsertMsg.push('WAID:' + message.key.id); - const bindSourceId = `$${bindInsertMsg.length}`; - - bindInsertMsg.push(message.messageTimestamp as number); - const bindmessageTimestamp = `$${bindInsertMsg.length}`; - - sqlInsertMsg += `(${bindContent}, ${bindContent}, $1, $2, ${bindConversationId}, ${bindMessageType}, FALSE, 0, - ${bindSenderType},${bindSenderId},${bindSourceId}, to_timestamp(${bindmessageTimestamp}), to_timestamp(${bindmessageTimestamp})),`; - }); - }); - if (bindInsertMsg.length > 2) { - if (sqlInsertMsg.slice(-1) === ',') { - sqlInsertMsg = sqlInsertMsg.slice(0, -1); - } - totalMessagesImported += (await pgClient.query(sqlInsertMsg, bindInsertMsg))?.rowCount ?? 0; - } - } - messagesChunk = this.sliceIntoChunks(messagesOrdered, batchSize); - } - - this.deleteHistoryMessages(instance); - this.deleteRepositoryMessagesCache(instance); - - const providerData: ChatwootDto = { - ...provider, - ignoreJids: Array.isArray(provider.ignoreJids) ? provider.ignoreJids.map((event) => String(event)) : [], - }; - - this.importHistoryContacts(instance, providerData); - - return totalMessagesImported; - } catch (error) { - this.logger.error(`Error on import history messages: ${error.toString()}`); - - this.deleteHistoryMessages(instance); - this.deleteRepositoryMessagesCache(instance); - } - } - - public async selectOrCreateFksFromChatwoot( - provider: ChatwootModel, - inbox: inbox, - phoneNumbersWithTimestamp: Map, - messagesByPhoneNumber: Map, - ): Promise> { - const pgClient = postgresClient.getChatwootConnection(); - - const bindValues = [provider.accountId, inbox.id]; - const phoneNumberBind = Array.from(messagesByPhoneNumber.keys()) - .map((phoneNumber) => { - const phoneNumberTimestamp = phoneNumbersWithTimestamp.get(phoneNumber); - - if (phoneNumberTimestamp) { - bindValues.push(phoneNumber); - let bindStr = `($${bindValues.length},`; - - bindValues.push(phoneNumberTimestamp.first); - bindStr += `$${bindValues.length},`; - - bindValues.push(phoneNumberTimestamp.last); - return `${bindStr}$${bindValues.length})`; - } - }) - .join(','); - - // select (or insert when necessary) data from tables contacts, contact_inboxes, conversations from chatwoot db - const sqlFromChatwoot = `WITH - phone_number AS ( - SELECT phone_number, created_at::INTEGER, last_activity_at::INTEGER FROM ( - VALUES - ${phoneNumberBind} - ) as t (phone_number, created_at, last_activity_at) - ), - - only_new_phone_number AS ( - SELECT * FROM phone_number - WHERE phone_number NOT IN ( - SELECT phone_number - FROM contacts - JOIN contact_inboxes ci ON ci.contact_id = contacts.id AND ci.inbox_id = $2 - JOIN conversations con ON con.contact_inbox_id = ci.id - AND con.account_id = $1 - AND con.inbox_id = $2 - AND con.contact_id = contacts.id - WHERE contacts.account_id = $1 - ) - ), - - new_contact AS ( - INSERT INTO contacts (name, phone_number, account_id, identifier, created_at, updated_at) - SELECT REPLACE(p.phone_number, '+', ''), p.phone_number, $1, CONCAT(REPLACE(p.phone_number, '+', ''), - '@s.whatsapp.net'), to_timestamp(p.created_at), to_timestamp(p.last_activity_at) - FROM only_new_phone_number AS p - ON CONFLICT(identifier, account_id) DO UPDATE SET updated_at = EXCLUDED.updated_at - RETURNING id, phone_number, created_at, updated_at - ), - - new_contact_inbox AS ( - INSERT INTO contact_inboxes (contact_id, inbox_id, source_id, created_at, updated_at) - SELECT new_contact.id, $2, gen_random_uuid(), new_contact.created_at, new_contact.updated_at - FROM new_contact - RETURNING id, contact_id, created_at, updated_at - ), - - new_conversation AS ( - INSERT INTO conversations (account_id, inbox_id, status, contact_id, - contact_inbox_id, uuid, last_activity_at, created_at, updated_at) - SELECT $1, $2, 0, new_contact_inbox.contact_id, new_contact_inbox.id, gen_random_uuid(), - new_contact_inbox.updated_at, new_contact_inbox.created_at, new_contact_inbox.updated_at - FROM new_contact_inbox - RETURNING id, contact_id - ) - - SELECT new_contact.phone_number, new_conversation.contact_id, new_conversation.id AS conversation_id - FROM new_conversation - JOIN new_contact ON new_conversation.contact_id = new_contact.id - - UNION - - SELECT p.phone_number, c.id contact_id, con.id conversation_id - FROM phone_number p - JOIN contacts c ON c.phone_number = p.phone_number - JOIN contact_inboxes ci ON ci.contact_id = c.id AND ci.inbox_id = $2 - JOIN conversations con ON con.contact_inbox_id = ci.id AND con.account_id = $1 - AND con.inbox_id = $2 AND con.contact_id = c.id`; - - const fksFromChatwoot = await pgClient.query(sqlFromChatwoot, bindValues); - - return new Map(fksFromChatwoot.rows.map((item: FksChatwoot) => [item.phone_number, item])); - } - - public async getChatwootUser(provider: ChatwootModel): Promise { - try { - const pgClient = postgresClient.getChatwootConnection(); - - const sqlUser = `SELECT owner_type AS user_type, owner_id AS user_id - FROM access_tokens - WHERE token = $1`; - - return (await pgClient.query(sqlUser, [provider.token]))?.rows[0] || false; - } catch (error) { - this.logger.error(`Error on getChatwootUser: ${error.toString()}`); - } - } - - public createMessagesMapByPhoneNumber(messages: Message[]): Map { - return messages.reduce((acc: Map, message: Message) => { - const key = message?.key as { - remoteJid: string; - }; - if (!this.isIgnorePhoneNumber(key?.remoteJid)) { - const phoneNumber = key?.remoteJid?.split('@')[0]; - if (phoneNumber) { - const phoneNumberPlus = `+${phoneNumber}`; - const messages = acc.has(phoneNumberPlus) ? acc.get(phoneNumberPlus) : []; - messages.push(message); - acc.set(phoneNumberPlus, messages); - } - } - - return acc; - }, new Map()); - } - - public async getContactsOrderByRecentConversations( - inbox: inbox, - provider: ChatwootModel, - limit = 50, - ): Promise<{ id: number; phone_number: string; identifier: string }[]> { - try { - const pgClient = postgresClient.getChatwootConnection(); - - const sql = `SELECT contacts.id, contacts.identifier, contacts.phone_number - FROM conversations - JOIN contacts ON contacts.id = conversations.contact_id - WHERE conversations.account_id = $1 - AND inbox_id = $2 - ORDER BY conversations.last_activity_at DESC - LIMIT $3`; - - return (await pgClient.query(sql, [provider.accountId, inbox.id, limit]))?.rows; - } catch (error) { - this.logger.error(`Error on get recent conversations: ${error.toString()}`); - } - } - - public getContentMessage(chatwootService: ChatwootService, msg: IWebMessageInfo) { - const contentMessage = chatwootService.getConversationMessage(msg.message); - if (contentMessage) { - return contentMessage; - } - - if (!configService.get('CHATWOOT').IMPORT.PLACEHOLDER_MEDIA_MESSAGE) { - return ''; - } - - const types = { - documentMessage: msg.message.documentMessage, - documentWithCaptionMessage: msg.message.documentWithCaptionMessage?.message?.documentMessage, - imageMessage: msg.message.imageMessage, - videoMessage: msg.message.videoMessage, - audioMessage: msg.message.audioMessage, - stickerMessage: msg.message.stickerMessage, - templateMessage: msg.message.templateMessage?.hydratedTemplate?.hydratedContentText, - }; - - const typeKey = Object.keys(types).find((key) => types[key] !== undefined && types[key] !== null); - switch (typeKey) { - case 'documentMessage': { - const doc = msg.message.documentMessage; - const fileName = doc?.fileName || 'document'; - const caption = doc?.caption ? ` ${doc.caption}` : ''; - return `__`; - } - - case 'documentWithCaptionMessage': { - const doc = msg.message.documentWithCaptionMessage?.message?.documentMessage; - const fileName = doc?.fileName || 'document'; - const caption = doc?.caption ? ` ${doc.caption}` : ''; - return `__`; - } - - case 'templateMessage': { - const template = msg.message.templateMessage?.hydratedTemplate; - return ( - (template?.hydratedTitleText ? `*${template.hydratedTitleText}*\n` : '') + - (template?.hydratedContentText || '') - ); - } - - case 'imageMessage': - return '__'; - - case 'videoMessage': - return '_