diff --git a/.env.example b/.env.example index 81b8bf8a..94daa156 100644 --- a/.env.example +++ b/.env.example @@ -119,6 +119,9 @@ CHATWOOT_MESSAGE_READ=false CHATWOOT_IMPORT_DATABASE_CONNECTION_URI=postgresql://user:pass@host:5432/dbname CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE=false +OPENAI_ENABLED=false +OPENAI_API_KEY_GLOBAL= + CACHE_REDIS_ENABLED=true CACHE_REDIS_URI=redis://localhost:6379/6 CACHE_REDIS_PREFIX_KEY=evolution diff --git a/prisma/migrations/20240718121437_add_openai_tables/migration.sql b/prisma/migrations/20240718121437_add_openai_tables/migration.sql new file mode 100644 index 00000000..37ee5c69 --- /dev/null +++ b/prisma/migrations/20240718121437_add_openai_tables/migration.sql @@ -0,0 +1,118 @@ +-- AlterTable +ALTER TABLE "Message" ADD COLUMN "openaiSessionId" TEXT; + +-- CreateTable +CREATE TABLE "OpenaiCreds" ( + "id" TEXT NOT NULL, + "apiKey" VARCHAR(255) NOT NULL, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL, + "instanceId" TEXT NOT NULL, + + CONSTRAINT "OpenaiCreds_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OpenaiBot" ( + "id" TEXT NOT NULL, + "botType" VARCHAR(100) NOT NULL, + "assistantId" VARCHAR(255), + "model" VARCHAR(100), + "systemMessages" JSONB, + "assistantMessages" JSONB, + "userMessages" JSONB, + "maxTokens" INTEGER, + "expire" INTEGER DEFAULT 0, + "keywordFinish" VARCHAR(100), + "delayMessage" INTEGER, + "unknownMessage" VARCHAR(100), + "listeningFromMe" BOOLEAN DEFAULT false, + "stopBotFromMe" BOOLEAN DEFAULT false, + "keepOpen" BOOLEAN DEFAULT false, + "debounceTime" INTEGER, + "ignoreJids" JSONB, + "triggerType" "TriggerType", + "triggerOperator" "TriggerOperator", + "triggerValue" TEXT, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL, + "openaiCredsId" TEXT NOT NULL, + "instanceId" TEXT NOT NULL, + + CONSTRAINT "OpenaiBot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OpenaiSession" ( + "id" TEXT NOT NULL, + "sessionId" VARCHAR(255) NOT NULL, + "remoteJid" VARCHAR(100) NOT NULL, + "status" "TypebotSessionStatus" NOT NULL, + "awaitUser" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL, + "openaiBotId" TEXT NOT NULL, + "instanceId" TEXT NOT NULL, + + CONSTRAINT "OpenaiSession_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OpenaiSetting" ( + "id" TEXT NOT NULL, + "expire" INTEGER DEFAULT 0, + "keywordFinish" VARCHAR(100), + "delayMessage" INTEGER, + "unknownMessage" VARCHAR(100), + "listeningFromMe" BOOLEAN DEFAULT false, + "stopBotFromMe" BOOLEAN DEFAULT false, + "keepOpen" BOOLEAN DEFAULT false, + "debounceTime" INTEGER, + "ignoreJids" JSONB, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP NOT NULL, + "openaiCredsId" TEXT NOT NULL, + "openaiIdFallback" VARCHAR(100), + "instanceId" TEXT NOT NULL, + + CONSTRAINT "OpenaiSetting_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "OpenaiCreds_apiKey_key" ON "OpenaiCreds"("apiKey"); + +-- CreateIndex +CREATE UNIQUE INDEX "OpenaiCreds_instanceId_key" ON "OpenaiCreds"("instanceId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OpenaiBot_assistantId_key" ON "OpenaiBot"("assistantId"); + +-- CreateIndex +CREATE UNIQUE INDEX "OpenaiSetting_instanceId_key" ON "OpenaiSetting"("instanceId"); + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_openaiSessionId_fkey" FOREIGN KEY ("openaiSessionId") REFERENCES "OpenaiSession"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiCreds" ADD CONSTRAINT "OpenaiCreds_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiBot" ADD CONSTRAINT "OpenaiBot_openaiCredsId_fkey" FOREIGN KEY ("openaiCredsId") REFERENCES "OpenaiCreds"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiBot" ADD CONSTRAINT "OpenaiBot_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiSession" ADD CONSTRAINT "OpenaiSession_openaiBotId_fkey" FOREIGN KEY ("openaiBotId") REFERENCES "OpenaiBot"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiSession" ADD CONSTRAINT "OpenaiSession_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiSetting" ADD CONSTRAINT "OpenaiSetting_openaiCredsId_fkey" FOREIGN KEY ("openaiCredsId") REFERENCES "OpenaiCreds"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiSetting" ADD CONSTRAINT "OpenaiSetting_openaiIdFallback_fkey" FOREIGN KEY ("openaiIdFallback") REFERENCES "OpenaiBot"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OpenaiSetting" ADD CONSTRAINT "OpenaiSetting_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240718123923_adjusts_openai_tables/migration.sql b/prisma/migrations/20240718123923_adjusts_openai_tables/migration.sql new file mode 100644 index 00000000..cea06fa4 --- /dev/null +++ b/prisma/migrations/20240718123923_adjusts_openai_tables/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "OpenaiBot" ADD COLUMN "enabled" BOOLEAN NOT NULL DEFAULT true; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index 62aab81f..3df3c3c6 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -47,41 +47,40 @@ enum TriggerOperator { } model Instance { - id String @id @default(cuid()) - name String @unique @db.VarChar(255) - connectionStatus InstanceConnectionStatus @default(open) - ownerJid String? @db.VarChar(100) - profileName String? @db.VarChar(100) - profilePicUrl String? @db.VarChar(500) - integration String? @db.VarChar(100) - number String? @db.VarChar(100) - businessId String? @db.VarChar(100) - token String? @unique @db.VarChar(255) - clientName String? @db.VarChar(100) - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime? @updatedAt @db.Timestamp - Chat Chat[] - Contact Contact[] - Message Message[] - Webhook Webhook? - Chatwoot Chatwoot? - Label Label[] - Proxy Proxy? - Setting Setting? - Rabbitmq Rabbitmq? - Sqs Sqs? - Websocket Websocket? - Typebot Typebot[] - Session Session? - MessageUpdate MessageUpdate[] - TypebotSession TypebotSession[] - TypebotSetting TypebotSetting? - Media Media[] - OpenaiCreds OpenaiCreds? - OpenaiAssistant OpenaiAssistant[] - OpenaiAssistantThread OpenaiAssistantThread[] - OpenaiChatCompletion OpenaiChatCompletion[] - OpenaiChatCompletionSession OpenaiChatCompletionSession[] + id String @id @default(cuid()) + name String @unique @db.VarChar(255) + connectionStatus InstanceConnectionStatus @default(open) + ownerJid String? @db.VarChar(100) + profileName String? @db.VarChar(100) + profilePicUrl String? @db.VarChar(500) + integration String? @db.VarChar(100) + number String? @db.VarChar(100) + businessId String? @db.VarChar(100) + token String? @unique @db.VarChar(255) + clientName String? @db.VarChar(100) + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime? @updatedAt @db.Timestamp + Chat Chat[] + Contact Contact[] + Message Message[] + Webhook Webhook? + Chatwoot Chatwoot? + Label Label[] + Proxy Proxy? + Setting Setting? + Rabbitmq Rabbitmq? + Sqs Sqs? + Websocket Websocket? + Typebot Typebot[] + Session Session? + MessageUpdate MessageUpdate[] + TypebotSession TypebotSession[] + TypebotSetting TypebotSetting? + Media Media[] + OpenaiCreds OpenaiCreds? + OpenaiBot OpenaiBot[] + OpenaiSession OpenaiSession[] + OpenaiSetting OpenaiSetting? } model Session { @@ -114,30 +113,28 @@ model Contact { } model Message { - id String @id @default(cuid()) - key Json @db.JsonB - pushName String? @db.VarChar(100) - participant String? @db.VarChar(100) - messageType String @db.VarChar(100) - message Json @db.JsonB - 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 - typebotSessionId String? - MessageUpdate MessageUpdate[] - TypebotSession TypebotSession? @relation(fields: [typebotSessionId], references: [id]) - Media Media? - OpenaiAssistantThread OpenaiAssistantThread? @relation(fields: [openaiAssistantThreadId], references: [id]) - openaiAssistantThreadId String? - OpenaiChatCompletionSession OpenaiChatCompletionSession? @relation(fields: [openaiChatCompletionSessionId], references: [id]) - openaiChatCompletionSessionId String? + id String @id @default(cuid()) + key Json @db.JsonB + pushName String? @db.VarChar(100) + participant String? @db.VarChar(100) + messageType String @db.VarChar(100) + message Json @db.JsonB + 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 + typebotSessionId String? + MessageUpdate MessageUpdate[] + TypebotSession TypebotSession? @relation(fields: [typebotSessionId], references: [id]) + Media Media? + OpenaiSession OpenaiSession? @relation(fields: [openaiSessionId], references: [id]) + openaiSessionId String? } model MessageUpdate { @@ -336,87 +333,80 @@ model Media { } model OpenaiCreds { - id String @id @default(cuid()) - 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 @unique + id String @id @default(cuid()) + 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 @unique + OpenaiAssistant OpenaiBot[] + OpenaiSetting OpenaiSetting[] } -model OpenaiAssistant { - id String @id @default(cuid()) - assistantId String @unique @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? - 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 - OpenaiAssistantThread OpenaiAssistantThread[] -} - -model OpenaiAssistantThread { - id String @id @default(cuid()) - threadId String @db.VarChar(255) - remoteJid String @db.VarChar(100) - status TypebotSessionStatus - awaitUser Boolean @default(false) @db.Boolean - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - OpenaiAssistant OpenaiAssistant @relation(fields: [openaiAssistantId], references: [id], onDelete: Cascade) - openaiAssistantId String - Message Message[] - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) +model OpenaiBot { + id String @id @default(cuid()) + enabled Boolean @default(true) @db.Boolean + botType String @db.VarChar(100) + assistantId String? @unique @db.VarChar(255) + 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 + 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 + OpenaiSession OpenaiSession[] + OpenaiSetting OpenaiSetting[] } -model OpenaiChatCompletion { - id String @id @default(cuid()) - 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 - ignoreJids Json? - 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 - OpenaiChatCompletionSession OpenaiChatCompletionSession[] +model OpenaiSession { + id String @id @default(cuid()) + sessionId String @db.VarChar(255) + remoteJid String @db.VarChar(100) + status TypebotSessionStatus + awaitUser Boolean @default(false) @db.Boolean + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + OpenaiBot OpenaiBot @relation(fields: [openaiBotId], references: [id], onDelete: Cascade) + openaiBotId String + Message Message[] + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String } -model OpenaiChatCompletionSession { - id String @id @default(cuid()) - remoteJid String @db.VarChar(100) - sessionId String @db.VarChar(100) - status TypebotSessionStatus - createdAt DateTime? @default(now()) @db.Timestamp - updatedAt DateTime @updatedAt @db.Timestamp - OpenaiChatCompletion OpenaiChatCompletion @relation(fields: [openaiChatCompletionId], references: [id], onDelete: Cascade) - openaiChatCompletionId String - Message Message[] - Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - instanceId 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? + createdAt DateTime? @default(now()) @db.Timestamp + updatedAt DateTime @updatedAt @db.Timestamp + OpenaiCreds OpenaiCreds? @relation(fields: [openaiCredsId], references: [id]) + openaiCredsId String + Fallback OpenaiBot? @relation(fields: [openaiIdFallback], references: [id]) + openaiIdFallback String? @db.VarChar(100) + Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) + instanceId String @unique } diff --git a/src/api/integrations/openai/controllers/openai.controller.ts b/src/api/integrations/openai/controllers/openai.controller.ts new file mode 100644 index 00000000..74675f23 --- /dev/null +++ b/src/api/integrations/openai/controllers/openai.controller.ts @@ -0,0 +1,81 @@ +import { configService, Openai } from '../../../../config/env.config'; +import { BadRequestException } from '../../../../exceptions'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { OpenaiCredsDto, OpenaiDto, OpenaiIgnoreJidDto } from '../dto/openai.dto'; +import { OpenaiService } from '../services/openai.service'; + +export class OpenaiController { + constructor(private readonly openaiService: OpenaiService) {} + + public async createOpenaiCreds(instance: InstanceDto, data: OpenaiCredsDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.createCreds(instance, data); + } + + public async findOpenaiCreds(instance: InstanceDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.findCreds(instance); + } + + public async createOpenai(instance: InstanceDto, data: OpenaiDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.create(instance, data); + } + + public async findOpenai(instance: InstanceDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.find(instance); + } + + public async fetchOpenai(instance: InstanceDto, openaiBotId: string) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.fetch(instance, openaiBotId); + } + + public async updateOpenai(instance: InstanceDto, openaiBotId: string, data: OpenaiDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.update(instance, openaiBotId, data); + } + + public async deleteOpenai(instance: InstanceDto, openaiBotId: string) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.delete(instance, openaiBotId); + } + + public async settings(instance: InstanceDto, data: any) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.setDefaultSettings(instance, data); + } + + public async fetchSettings(instance: InstanceDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.fetchDefaultSettings(instance); + } + + public async changeStatus(instance: InstanceDto, data: any) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.changeStatus(instance, data); + } + + public async fetchSessions(instance: InstanceDto, openaiBotId: string) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.fetchSessions(instance, openaiBotId); + } + + public async ignoreJid(instance: InstanceDto, data: OpenaiIgnoreJidDto) { + if (!configService.get('OPENAI').ENABLED) throw new BadRequestException('Openai is disabled'); + + return this.openaiService.ignoreJid(instance, data); + } +} diff --git a/src/api/integrations/openai/dto/openai.dto.ts b/src/api/integrations/openai/dto/openai.dto.ts new file mode 100644 index 00000000..7ce8caba --- /dev/null +++ b/src/api/integrations/openai/dto/openai.dto.ts @@ -0,0 +1,56 @@ +import { TriggerOperator, TriggerType } from '@prisma/client'; + +export class Session { + remoteJid?: string; + sessionId?: string; + status?: string; + createdAt?: number; + updateAt?: number; +} + +export class OpenaiCredsDto { + apiKey: string; +} + +export class OpenaiDto { + enabled?: boolean; + openaiCredsId: string; + botType?: string; + assistantId?: string; + model?: string; + systemMessages?: string[]; + assistantMessages?: string[]; + userMessages?: string[]; + maxTokens?: number; + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + triggerType?: TriggerType; + triggerOperator?: TriggerOperator; + triggerValue?: string; + ignoreJids?: any; +} + +export class OpenaiSettingDto { + openaiCredsId?: string; + expire?: number; + keywordFinish?: string; + delayMessage?: number; + unknownMessage?: string; + listeningFromMe?: boolean; + stopBotFromMe?: boolean; + keepOpen?: boolean; + debounceTime?: number; + openaiIdFallback?: string; + ignoreJids?: any; +} + +export class OpenaiIgnoreJidDto { + remoteJid?: string; + action?: string; +} diff --git a/src/api/integrations/openai/routes/openai.router.ts b/src/api/integrations/openai/routes/openai.router.ts new file mode 100644 index 00000000..2de473e3 --- /dev/null +++ b/src/api/integrations/openai/routes/openai.router.ts @@ -0,0 +1,144 @@ +import { RequestHandler, Router } from 'express'; + +import { + instanceSchema, + openaiCredsSchema, + openaiIgnoreJidSchema, + openaiSchema, + openaiSettingSchema, + openaiStatusSchema, +} from '../../../../validate/validate.schema'; +import { RouterBroker } from '../../../abstract/abstract.router'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { HttpStatus } from '../../../routes/index.router'; +import { openaiController } from '../../../server.module'; +import { OpenaiCredsDto, OpenaiDto, OpenaiIgnoreJidDto, OpenaiSettingDto } from '../dto/openai.dto'; + +export class OpenaiRouter extends RouterBroker { + constructor(...guards: RequestHandler[]) { + super(); + this.router + .post(this.routerPath('creds'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiCredsSchema, + ClassRef: OpenaiCredsDto, + execute: (instance, data) => openaiController.createOpenaiCreds(instance, data), + }); + + res.status(HttpStatus.CREATED).json(response); + }) + .get(this.routerPath('creds'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => openaiController.findOpenaiCreds(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('create'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiSchema, + ClassRef: OpenaiDto, + execute: (instance, data) => openaiController.createOpenai(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) => openaiController.findOpenai(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetch/:openaiBotId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => openaiController.fetchOpenai(instance, req.params.openaiBotId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .put(this.routerPath('update/:openaiBotId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiSchema, + ClassRef: OpenaiDto, + execute: (instance, data) => openaiController.updateOpenai(instance, req.params.openaiBotId, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .delete(this.routerPath('delete/:openaiBotId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => openaiController.deleteOpenai(instance, req.params.openaiBotId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('settings'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiSettingSchema, + ClassRef: OpenaiSettingDto, + execute: (instance, data) => openaiController.settings(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchSettings'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => openaiController.fetchSettings(instance), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('changeStatus'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiStatusSchema, + ClassRef: InstanceDto, + execute: (instance, data) => openaiController.changeStatus(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }) + .get(this.routerPath('fetchSessions/:openaiBotId'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: instanceSchema, + ClassRef: InstanceDto, + execute: (instance) => openaiController.fetchSessions(instance, req.params.openaiBotId), + }); + + res.status(HttpStatus.OK).json(response); + }) + .post(this.routerPath('ignoreJid'), ...guards, async (req, res) => { + const response = await this.dataValidate({ + request: req, + schema: openaiIgnoreJidSchema, + ClassRef: OpenaiIgnoreJidDto, + execute: (instance, data) => openaiController.ignoreJid(instance, data), + }); + + res.status(HttpStatus.OK).json(response); + }); + } + + public readonly router = Router(); +} diff --git a/src/api/integrations/openai/services/openai.service.ts b/src/api/integrations/openai/services/openai.service.ts new file mode 100644 index 00000000..273a213f --- /dev/null +++ b/src/api/integrations/openai/services/openai.service.ts @@ -0,0 +1,1751 @@ +import { ConfigService } from '../../../../config/env.config'; +import { Logger } from '../../../../config/logger.config'; +import { InstanceDto } from '../../../dto/instance.dto'; +import { PrismaRepository } from '../../../repository/repository.service'; +import { WAMonitoringService } from '../../../services/monitor.service'; +import { OpenaiCredsDto, OpenaiDto, OpenaiIgnoreJidDto, OpenaiSettingDto } from '../dto/openai.dto'; + +export class OpenaiService { + constructor( + private readonly waMonitor: WAMonitoringService, + private readonly configService: ConfigService, + private readonly prismaRepository: PrismaRepository, + ) {} + + private userMessageDebounce: { [key: string]: { message: string; timeoutId: NodeJS.Timeout } } = {}; + + private readonly logger = new Logger(OpenaiService.name); + + public async createCreds(instance: InstanceDto, data: OpenaiCredsDto) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if (!data.apiKey) throw new Error('API Key is required'); + + try { + const creds = await this.prismaRepository.openaiCreds.create({ + data: { + apiKey: data.apiKey, + instanceId: instanceId, + }, + }); + + return creds; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating openai creds'); + } + } + + public async findCreds(instance: InstanceDto) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const creds = await this.prismaRepository.openaiCreds.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiAssistant: true, + }, + }); + + return creds; + } + + public async create(instance: InstanceDto, data: OpenaiDto) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + if ( + !data.openaiCredsId || + !data.expire || + !data.keywordFinish || + !data.delayMessage || + !data.unknownMessage || + !data.listeningFromMe || + !data.stopBotFromMe || + !data.keepOpen || + !data.debounceTime || + !data.ignoreJids + ) { + const defaultSettingCheck = await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (!data.openaiCredsId) data.openaiCredsId = defaultSettingCheck?.openaiCredsId || null; + if (!data.expire) data.expire = defaultSettingCheck?.expire || 0; + if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; + if (!data.delayMessage) data.delayMessage = defaultSettingCheck?.delayMessage || 1000; + if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; + if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck?.listeningFromMe || false; + if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; + if (!data.keepOpen) data.keepOpen = defaultSettingCheck?.keepOpen || false; + if (!data.debounceTime) data.debounceTime = defaultSettingCheck?.debounceTime || 0; + if (!data.ignoreJids) data.ignoreJids = defaultSettingCheck?.ignoreJids || []; + + if (!data.openaiCredsId) { + throw new Error('Openai Creds Id is required'); + } + + if (!defaultSettingCheck) { + await this.setDefaultSettings(instance, { + openaiCredsId: data.openaiCredsId, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + ignoreJids: data.ignoreJids, + }); + } + } + + const checkTriggerAll = await this.prismaRepository.openaiBot.findFirst({ + where: { + enabled: true, + triggerType: 'all', + instanceId: instanceId, + }, + }); + + if (checkTriggerAll && data.triggerType === 'all') { + throw new Error('You already have a openai with an "All" trigger, you cannot have more bots while it is active'); + } + + let whereDuplication: any = { + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) throw new Error('Assistant ID is required'); + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) throw new Error('Model is required'); + if (!data.maxTokens) throw new Error('Max tokens is required'); + + if (!data.systemMessages) data.systemMessages = []; + if (!data.assistantMessages) data.assistantMessages = []; + if (!data.userMessages) data.userMessages = []; + + whereDuplication = { + ...whereDuplication, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + }; + } else { + throw new Error('Bot type is required'); + } + + const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + throw new Error('Openai Bot already exists'); + } + + if (data.triggerType !== 'all') { + if (!data.triggerOperator || !data.triggerValue) { + throw new Error('Trigger operator and value are required'); + } + + const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ + where: { + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + } + + try { + const openaiBot = await this.prismaRepository.openaiBot.create({ + data: { + enabled: data.enabled, + openaiCredsId: data.openaiCredsId, + botType: data.botType, + assistantId: data.assistantId, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + instanceId: instanceId, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + }, + }); + + return openaiBot; + } catch (error) { + this.logger.error(error); + throw new Error('Error creating openai bot'); + } + } + + public async fetch(instance: InstanceDto, openaiBotId: string) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.prismaRepository.openaiBot.findFirst({ + where: { + id: openaiBotId, + }, + include: { + OpenaiSession: true, + }, + }); + + if (!openaiBot) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai Bot not found'); + } + + return openaiBot; + } + + public async update(instance: InstanceDto, openaiBotId: string, data: OpenaiDto) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.prismaRepository.openaiBot.findFirst({ + where: { + id: openaiBotId, + }, + }); + + if (!openaiBot) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai Bot not found'); + } + + if (data.triggerType === 'all') { + const checkTriggerAll = await this.prismaRepository.openaiBot.findFirst({ + where: { + enabled: true, + triggerType: 'all', + id: { + not: openaiBotId, + }, + instanceId: instanceId, + }, + }); + + if (checkTriggerAll) { + throw new Error( + 'You already have a openai bot with an "All" trigger, you cannot have more bots while it is active', + ); + } + } + + let whereDuplication: any = { + id: { + not: openaiBotId, + }, + instanceId: instanceId, + }; + + if (data.botType === 'assistant') { + if (!data.assistantId) throw new Error('Assistant ID is required'); + + whereDuplication = { + ...whereDuplication, + assistantId: data.assistantId, + }; + } else if (data.botType === 'chatCompletion') { + if (!data.model) throw new Error('Model is required'); + if (!data.maxTokens) throw new Error('Max tokens is required'); + + if (!data.systemMessages) data.systemMessages = []; + if (!data.assistantMessages) data.assistantMessages = []; + if (!data.userMessages) data.userMessages = []; + + whereDuplication = { + ...whereDuplication, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + }; + } else { + throw new Error('Bot type is required'); + } + + const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ + where: whereDuplication, + }); + + if (checkDuplicate) { + throw new Error('Openai Bot already exists'); + } + + if (data.triggerType !== 'all') { + if (!data.triggerOperator || !data.triggerValue) { + throw new Error('Trigger operator and value are required'); + } + + const checkDuplicate = await this.prismaRepository.openaiBot.findFirst({ + where: { + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + id: { + not: openaiBotId, + }, + instanceId: instanceId, + }, + }); + + if (checkDuplicate) { + throw new Error('Trigger already exists'); + } + + try { + const openaiBot = await this.prismaRepository.openaiBot.update({ + where: { + id: openaiBotId, + }, + data: { + enabled: data.enabled, + openaiCredsId: data.openaiCredsId, + botType: data.botType, + assistantId: data.assistantId, + model: data.model, + systemMessages: data.systemMessages, + assistantMessages: data.assistantMessages, + userMessages: data.userMessages, + maxTokens: data.maxTokens, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + instanceId: instanceId, + triggerType: data.triggerType, + triggerOperator: data.triggerOperator, + triggerValue: data.triggerValue, + ignoreJids: data.ignoreJids, + }, + }); + + return openaiBot; + } catch (error) { + this.logger.error(error); + throw new Error('Error updating openai bot'); + } + } + } + + public async find(instance: InstanceDto): Promise { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBots = await this.prismaRepository.openaiBot.findMany({ + where: { + instanceId: instanceId, + }, + include: { + OpenaiSession: true, + }, + }); + + if (!openaiBots.length) { + this.logger.error('Openai bot not found'); + return null; + } + + return openaiBots; + } + + public async delete(instance: InstanceDto, openaiBotId: string) { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.prismaRepository.openaiBot.findFirst({ + where: { + id: openaiBotId, + }, + }); + + if (!openaiBot) { + throw new Error('Openai bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai bot not found'); + } + try { + await this.prismaRepository.openaiSession.deleteMany({ + where: { + openaiBotId: openaiBotId, + }, + }); + + await this.prismaRepository.openaiBot.delete({ + where: { + id: openaiBotId, + }, + }); + + return { openaiBot: { id: openaiBotId } }; + } catch (error) { + this.logger.error(error); + throw new Error('Error deleting openai bot'); + } + } + + public async setDefaultSettings(instance: InstanceDto, data: OpenaiSettingDto) { + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (settings) { + const updateSettings = await this.prismaRepository.openaiSetting.update({ + where: { + id: settings.id, + }, + data: { + openaiCredsId: data.openaiCredsId, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + openaiIdFallback: data.openaiIdFallback, + ignoreJids: data.ignoreJids, + }, + }); + + return { + openaiCredsId: updateSettings.openaiCredsId, + expire: updateSettings.expire, + keywordFinish: updateSettings.keywordFinish, + delayMessage: updateSettings.delayMessage, + unknownMessage: updateSettings.unknownMessage, + listeningFromMe: updateSettings.listeningFromMe, + stopBotFromMe: updateSettings.stopBotFromMe, + keepOpen: updateSettings.keepOpen, + debounceTime: updateSettings.debounceTime, + openaiIdFallback: updateSettings.openaiIdFallback, + ignoreJids: updateSettings.ignoreJids, + }; + } + + const newSetttings = await this.prismaRepository.openaiSetting.create({ + data: { + openaiCredsId: data.openaiCredsId, + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + openaiIdFallback: data.openaiIdFallback, + ignoreJids: data.ignoreJids, + instanceId: instanceId, + }, + }); + + return { + openaiCredsId: newSetttings.openaiCredsId, + expire: newSetttings.expire, + keywordFinish: newSetttings.keywordFinish, + delayMessage: newSetttings.delayMessage, + unknownMessage: newSetttings.unknownMessage, + listeningFromMe: newSetttings.listeningFromMe, + stopBotFromMe: newSetttings.stopBotFromMe, + keepOpen: newSetttings.keepOpen, + debounceTime: newSetttings.debounceTime, + openaiIdFallback: newSetttings.openaiIdFallback, + ignoreJids: newSetttings.ignoreJids, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } + } + + public async fetchDefaultSettings(instance: InstanceDto) { + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: instanceId, + }, + include: { + Fallback: true, + }, + }); + + if (!settings) { + throw new Error('Default settings not found'); + } + + return { + openaiCredsId: settings.openaiCredsId, + expire: settings.expire, + keywordFinish: settings.keywordFinish, + delayMessage: settings.delayMessage, + unknownMessage: settings.unknownMessage, + listeningFromMe: settings.listeningFromMe, + stopBotFromMe: settings.stopBotFromMe, + keepOpen: settings.keepOpen, + ignoreJids: settings.ignoreJids, + openaiIdFallback: settings.Fallback, + fallback: settings.Fallback, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching default settings'); + } + } + + public async ignoreJid(instance: InstanceDto, data: OpenaiIgnoreJidDto) { + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const settings = await this.prismaRepository.openaiSetting.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + if (!settings) { + throw new Error('Settings not found'); + } + + let ignoreJids: any = settings?.ignoreJids || []; + + if (data.action === 'add') { + if (ignoreJids.includes(data.remoteJid)) return { ignoreJids: ignoreJids }; + + ignoreJids.push(data.remoteJid); + } else { + ignoreJids = ignoreJids.filter((jid) => jid !== data.remoteJid); + } + + const updateSettings = await this.prismaRepository.openaiSetting.update({ + where: { + id: settings.id, + }, + data: { + ignoreJids: ignoreJids, + }, + }); + + return { + ignoreJids: updateSettings.ignoreJids, + }; + } catch (error) { + this.logger.error(error); + throw new Error('Error setting default settings'); + } + } + + public async fetchSessions(instance: InstanceDto, openaiBotId?: string, remoteJid?: string) { + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const openaiBot = await this.prismaRepository.openaiBot.findFirst({ + where: { + id: openaiBotId, + }, + }); + + if (!openaiBot) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot.instanceId !== instanceId) { + throw new Error('Openai Bot not found'); + } + + if (openaiBot) { + return await this.prismaRepository.openaiSession.findMany({ + where: { + openaiBotId: openaiBotId, + }, + }); + } + + if (remoteJid) { + return await this.prismaRepository.openaiSession.findMany({ + where: { + remoteJid: remoteJid, + openaiBotId: openaiBotId, + }, + }); + } + } catch (error) { + this.logger.error(error); + throw new Error('Error fetching sessions'); + } + } + + public async changeStatus(instance: InstanceDto, data: any) { + try { + const instanceId = await this.prismaRepository.instance + .findFirst({ + where: { + name: instance.instanceName, + }, + }) + .then((instance) => instance.id); + + const remoteJid = data.remoteJid; + const status = data.status; + + if (status === 'closed') { + await this.prismaRepository.openaiSession.deleteMany({ + where: { + remoteJid: remoteJid, + }, + }); + + return { openai: { ...instance, openai: { remoteJid: remoteJid, status: status } } }; + } else { + const session = await this.prismaRepository.openaiSession.updateMany({ + where: { + instanceId: instanceId, + remoteJid: remoteJid, + }, + data: { + status: status, + }, + }); + + const openaiData = { + remoteJid: remoteJid, + status: status, + session, + }; + + return { openai: { ...instance, openai: openaiData } }; + } + } catch (error) { + this.logger.error(error); + throw new Error('Error changing status'); + } + } + + // private getTypeMessage(msg: any) { + // let mediaId: string; + + // if (this.configService.get('S3').ENABLE) mediaId = msg.message.mediaUrl; + // else mediaId = msg.key.id; + + // const types = { + // conversation: msg?.message?.conversation, + // extendedTextMessage: msg?.message?.extendedTextMessage?.text, + // contactMessage: msg?.message?.contactMessage?.displayName, + // locationMessage: msg?.message?.locationMessage?.degreesLatitude, + // viewOnceMessageV2: + // msg?.message?.viewOnceMessageV2?.message?.imageMessage?.url || + // msg?.message?.viewOnceMessageV2?.message?.videoMessage?.url || + // msg?.message?.viewOnceMessageV2?.message?.audioMessage?.url, + // listResponseMessage: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId, + // responseRowId: msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId, + // // Medias + // audioMessage: msg?.message?.audioMessage ? `audioMessage|${mediaId}` : undefined, + // imageMessage: msg?.message?.imageMessage ? `imageMessage|${mediaId}` : undefined, + // videoMessage: msg?.message?.videoMessage ? `videoMessage|${mediaId}` : undefined, + // documentMessage: msg?.message?.documentMessage ? `documentMessage|${mediaId}` : undefined, + // documentWithCaptionMessage: msg?.message?.auddocumentWithCaptionMessageioMessage + // ? `documentWithCaptionMessage|${mediaId}` + // : undefined, + // }; + + // const messageType = Object.keys(types).find((key) => types[key] !== undefined) || 'unknown'; + + // return { ...types, messageType }; + // } + + // private getMessageContent(types: any) { + // const typeKey = Object.keys(types).find((key) => types[key] !== undefined); + + // const result = typeKey ? types[typeKey] : undefined; + + // return result; + // } + + // private getConversationMessage(msg: any) { + // const types = this.getTypeMessage(msg); + + // const messageContent = this.getMessageContent(types); + + // return messageContent; + // } + + // public async createNewSession(instance: InstanceDto, data: any) { + // if (data.remoteJid === 'status@broadcast') return; + // const id = Math.floor(Math.random() * 10000000000).toString(); + + // try { + // const version = this.configService.get('TYPEBOT').API_VERSION; + // let url: string; + // let reqData: {}; + // if (version === 'latest') { + // url = `${data.url}/api/v1/typebots/${data.typebot}/startChat`; + + // reqData = { + // prefilledVariables: { + // ...data.prefilledVariables, + // remoteJid: data.remoteJid, + // pushName: data.pushName || data.prefilledVariables?.pushName || '', + // instanceName: instance.instanceName, + // serverUrl: this.configService.get('SERVER').URL, + // apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + // }, + // }; + // } else { + // url = `${data.url}/api/v1/sendMessage`; + + // reqData = { + // startParams: { + // publicId: data.typebot, + // prefilledVariables: { + // ...data.prefilledVariables, + // remoteJid: data.remoteJid, + // pushName: data.pushName || data.prefilledVariables?.pushName || '', + // instanceName: instance.instanceName, + // serverUrl: this.configService.get('SERVER').URL, + // apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + // }, + // }, + // }; + // } + // const request = await axios.post(url, reqData); + + // let session = null; + // if (request?.data?.sessionId) { + // session = await this.prismaRepository.typebotSession.create({ + // data: { + // remoteJid: data.remoteJid, + // pushName: data.pushName || '', + // sessionId: `${id}-${request.data.sessionId}`, + // status: 'opened', + // prefilledVariables: { + // ...data.prefilledVariables, + // remoteJid: data.remoteJid, + // pushName: data.pushName || '', + // instanceName: instance.instanceName, + // serverUrl: this.configService.get('SERVER').URL, + // apiKey: this.configService.get('AUTHENTICATION').API_KEY.KEY, + // }, + // awaitUser: false, + // typebotId: data.typebotId, + // instanceId: instance.instanceId, + // }, + // }); + // } + // return { ...request.data, session }; + // } catch (error) { + // this.logger.error(error); + // return; + // } + // } + + // public async sendWAMessage( + // instance: InstanceDto, + // session: TypebotSession, + // settings: { + // expire: number; + // keywordFinish: string; + // delayMessage: number; + // unknownMessage: string; + // listeningFromMe: boolean; + // stopBotFromMe: boolean; + // keepOpen: boolean; + // }, + // remoteJid: string, + // messages: any, + // input: any, + // clientSideActions: any, + // ) { + // processMessages( + // this.waMonitor.waInstances[instance.instanceName], + // session, + // settings, + // messages, + // input, + // clientSideActions, + // applyFormatting, + // this.prismaRepository, + // ).catch((err) => { + // console.error('Erro ao processar mensagens:', err); + // }); + + // function findItemAndGetSecondsToWait(array, targetId) { + // if (!array) return null; + + // for (const item of array) { + // if (item.lastBubbleBlockId === targetId) { + // return item.wait?.secondsToWaitFor; + // } + // } + // return null; + // } + + // function applyFormatting(element) { + // let text = ''; + + // if (element.text) { + // text += element.text; + // } + + // if (element.children && element.type !== 'a') { + // for (const child of element.children) { + // text += applyFormatting(child); + // } + // } + + // if (element.type === 'p' && element.type !== 'inline-variable') { + // text = text.trim() + '\n'; + // } + + // if (element.type === 'inline-variable') { + // text = text.trim(); + // } + + // if (element.type === 'ol') { + // text = + // '\n' + + // text + // .split('\n') + // .map((line, index) => (line ? `${index + 1}. ${line}` : '')) + // .join('\n'); + // } + + // if (element.type === 'li') { + // text = text + // .split('\n') + // .map((line) => (line ? ` ${line}` : '')) + // .join('\n'); + // } + + // let formats = ''; + + // if (element.bold) { + // formats += '*'; + // } + + // if (element.italic) { + // formats += '_'; + // } + + // if (element.underline) { + // formats += '~'; + // } + + // let formattedText = `${formats}${text}${formats.split('').reverse().join('')}`; + + // if (element.url) { + // formattedText = element.children[0]?.text ? `[${formattedText}]\n(${element.url})` : `${element.url}`; + // } + + // return formattedText; + // } + + // async function processMessages( + // instance: any, + // session: TypebotSession, + // settings: { + // expire: number; + // keywordFinish: string; + // delayMessage: number; + // unknownMessage: string; + // listeningFromMe: boolean; + // stopBotFromMe: boolean; + // keepOpen: boolean; + // }, + // messages: any, + // input: any, + // clientSideActions: any, + // applyFormatting: any, + // prismaRepository: PrismaRepository, + // ) { + // for (const message of messages) { + // if (message.type === 'text') { + // let formattedText = ''; + + // for (const richText of message.content.richText) { + // for (const element of richText.children) { + // formattedText += applyFormatting(element); + // } + // formattedText += '\n'; + // } + + // formattedText = formattedText.replace(/\*\*/g, '').replace(/__/, '').replace(/~~/, '').replace(/\n$/, ''); + + // formattedText = formattedText.replace(/\n$/, ''); + + // await instance.textMessage( + // { + // number: remoteJid.split('@')[0], + // delay: settings?.delayMessage || 1000, + // text: formattedText, + // }, + // false, + // ); + + // sendTelemetry('/message/sendText'); + // } + + // if (message.type === 'image') { + // await instance.mediaMessage( + // { + // number: remoteJid.split('@')[0], + // delay: settings?.delayMessage || 1000, + // mediatype: 'image', + // media: message.content.url, + // }, + // false, + // ); + + // sendTelemetry('/message/sendMedia'); + // } + + // if (message.type === 'video') { + // await instance.mediaMessage( + // { + // number: remoteJid.split('@')[0], + // delay: settings?.delayMessage || 1000, + // mediatype: 'video', + // media: message.content.url, + // }, + // false, + // ); + + // sendTelemetry('/message/sendMedia'); + // } + + // if (message.type === 'audio') { + // await instance.audioWhatsapp( + // { + // number: remoteJid.split('@')[0], + // delay: settings?.delayMessage || 1000, + // encoding: true, + // audio: message.content.url, + // }, + // false, + // ); + + // sendTelemetry('/message/sendWhatsAppAudio'); + // } + + // const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); + + // if (wait) { + // await new Promise((resolve) => setTimeout(resolve, wait * 1000)); + // } + // } + + // if (input) { + // if (input.type === 'choice input') { + // let formattedText = ''; + + // const items = input.items; + + // for (const item of items) { + // formattedText += `▶️ ${item.content}\n`; + // } + + // formattedText = formattedText.replace(/\n$/, ''); + + // await instance.textMessage( + // { + // number: remoteJid.split('@')[0], + // delay: settings?.delayMessage || 1000, + // text: formattedText, + // }, + // false, + // ); + + // sendTelemetry('/message/sendText'); + // } + + // await prismaRepository.typebotSession.update({ + // where: { + // id: session.id, + // }, + // data: { + // awaitUser: true, + // }, + // }); + // } else { + // if (!settings?.keepOpen) { + // await prismaRepository.typebotSession.deleteMany({ + // where: { + // id: session.id, + // }, + // }); + // } else { + // await prismaRepository.typebotSession.update({ + // where: { + // id: session.id, + // }, + // data: { + // status: 'closed', + // }, + // }); + // } + // } + // } + // } + + // public async findTypebotByTrigger(content: string, instanceId: string) { + // // Check for triggerType 'all' + // const findTriggerAll = await this.prismaRepository.typebot.findFirst({ + // where: { + // enabled: true, + // triggerType: 'all', + // instanceId: instanceId, + // }, + // }); + + // if (findTriggerAll) return findTriggerAll; + + // // Check for exact match + // const findTriggerEquals = await this.prismaRepository.typebot.findFirst({ + // where: { + // enabled: true, + // triggerType: 'keyword', + // triggerOperator: 'equals', + // triggerValue: content, + // instanceId: instanceId, + // }, + // }); + + // if (findTriggerEquals) return findTriggerEquals; + + // // Check for regex match + // const findRegex = await this.prismaRepository.typebot.findMany({ + // where: { + // enabled: true, + // triggerType: 'keyword', + // triggerOperator: 'regex', + // instanceId: instanceId, + // }, + // }); + + // let findTriggerRegex = null; + + // for (const regex of findRegex) { + // const regexValue = new RegExp(regex.triggerValue); + + // if (regexValue.test(content)) { + // findTriggerRegex = regex; + // break; + // } + // } + + // if (findTriggerRegex) return findTriggerRegex; + + // // Check for startsWith match + // const findTriggerStartsWith = await this.prismaRepository.typebot.findFirst({ + // where: { + // enabled: true, + // triggerType: 'keyword', + // triggerOperator: 'startsWith', + // triggerValue: { + // startsWith: content, + // }, + // instanceId: instanceId, + // }, + // }); + + // if (findTriggerStartsWith) return findTriggerStartsWith; + + // // Check for endsWith match + // const findTriggerEndsWith = await this.prismaRepository.typebot.findFirst({ + // where: { + // enabled: true, + // triggerType: 'keyword', + // triggerOperator: 'endsWith', + // triggerValue: { + // endsWith: content, + // }, + // instanceId: instanceId, + // }, + // }); + + // if (findTriggerEndsWith) return findTriggerEndsWith; + + // // Check for contains match + // const findTriggerContains = await this.prismaRepository.typebot.findFirst({ + // where: { + // enabled: true, + // triggerType: 'keyword', + // triggerOperator: 'contains', + // triggerValue: { + // contains: content, + // }, + // instanceId: instanceId, + // }, + // }); + + // if (findTriggerContains) return findTriggerContains; + + // const fallback = await this.prismaRepository.typebotSetting.findFirst({ + // where: { + // instanceId: instanceId, + // }, + // }); + + // if (fallback?.typebotIdFallback) { + // const findFallback = await this.prismaRepository.typebot.findFirst({ + // where: { + // id: fallback.typebotIdFallback, + // }, + // }); + + // if (findFallback) return findFallback; + // } + + // return null; + // } + + // private processDebounce(content: string, remoteJid: string, debounceTime: number, callback: any) { + // if (this.userMessageDebounce[remoteJid]) { + // this.userMessageDebounce[remoteJid].message += ` ${content}`; + // this.logger.log('message debounced: ' + this.userMessageDebounce[remoteJid].message); + // clearTimeout(this.userMessageDebounce[remoteJid].timeoutId); + // } else { + // this.userMessageDebounce[remoteJid] = { + // message: content, + // timeoutId: null, + // }; + // } + + // this.userMessageDebounce[remoteJid].timeoutId = setTimeout(() => { + // const myQuestion = this.userMessageDebounce[remoteJid].message; + // this.logger.log('Debounce complete. Processing message: ' + myQuestion); + + // delete this.userMessageDebounce[remoteJid]; + // callback(myQuestion); + // }, debounceTime * 1000); + // } + + // public async sendTypebot(instance: InstanceDto, remoteJid: string, msg: Message) { + // try { + // const settings = await this.prismaRepository.typebotSetting.findFirst({ + // where: { + // instanceId: instance.instanceId, + // }, + // }); + + // if (settings?.ignoreJids) { + // const ignoreJids: any = settings.ignoreJids; + + // let ignoreGroups = false; + // let ignoreContacts = false; + + // if (ignoreJids.includes('@g.us')) { + // ignoreGroups = true; + // } + + // if (ignoreJids.includes('@s.whatsapp.net')) { + // ignoreContacts = true; + // } + + // if (ignoreGroups && remoteJid.endsWith('@g.us')) { + // this.logger.warn('Ignoring message from group: ' + remoteJid); + // return; + // } + + // if (ignoreContacts && remoteJid.endsWith('@s.whatsapp.net')) { + // this.logger.warn('Ignoring message from contact: ' + remoteJid); + // return; + // } + + // if (ignoreJids.includes(remoteJid)) { + // this.logger.warn('Ignoring message from jid: ' + remoteJid); + // return; + // } + // } + + // const session = await this.prismaRepository.typebotSession.findFirst({ + // where: { + // remoteJid: remoteJid, + // }, + // }); + + // const content = this.getConversationMessage(msg); + + // let findTypebot = null; + + // if (!session) { + // findTypebot = await this.findTypebotByTrigger(content, instance.instanceId); + + // if (!findTypebot) { + // return; + // } + // } else { + // findTypebot = await this.prismaRepository.typebot.findFirst({ + // where: { + // id: session.typebotId, + // }, + // }); + // } + + // const url = findTypebot?.url; + // const typebot = findTypebot?.typebot; + // let expire = findTypebot?.expire; + // let keywordFinish = findTypebot?.keywordFinish; + // let delayMessage = findTypebot?.delayMessage; + // let unknownMessage = findTypebot?.unknownMessage; + // let listeningFromMe = findTypebot?.listeningFromMe; + // let stopBotFromMe = findTypebot?.stopBotFromMe; + // let keepOpen = findTypebot?.keepOpen; + // let debounceTime = findTypebot?.debounceTime; + + // if ( + // !expire || + // !keywordFinish || + // !delayMessage || + // !unknownMessage || + // !listeningFromMe || + // !stopBotFromMe || + // !keepOpen + // ) { + // if (!expire) expire = settings.expire; + + // if (!keywordFinish) keywordFinish = settings.keywordFinish; + + // if (!delayMessage) delayMessage = settings.delayMessage; + + // if (!unknownMessage) unknownMessage = settings.unknownMessage; + + // if (!listeningFromMe) listeningFromMe = settings.listeningFromMe; + + // if (!stopBotFromMe) stopBotFromMe = settings.stopBotFromMe; + + // if (!keepOpen) keepOpen = settings.keepOpen; + + // if (!debounceTime) debounceTime = settings.debounceTime; + // } + + // const key = msg.key as { + // id: string; + // remoteJid: string; + // fromMe: boolean; + // participant: string; + // }; + + // if (!listeningFromMe && key.fromMe) { + // return; + // } + + // if (stopBotFromMe && listeningFromMe && key.fromMe && session) { + // await this.prismaRepository.typebotSession.deleteMany({ + // where: { + // typebotId: findTypebot.id, + // remoteJid: remoteJid, + // }, + // }); + // return; + // } + + // if (debounceTime && debounceTime > 0) { + // this.processDebounce(content, remoteJid, debounceTime, async (debouncedContent) => { + // await this.processTypebot( + // instance, + // remoteJid, + // msg, + // session, + // findTypebot, + // url, + // expire, + // typebot, + // keywordFinish, + // delayMessage, + // unknownMessage, + // listeningFromMe, + // stopBotFromMe, + // keepOpen, + // debouncedContent, + // ); + // }); + // } else { + // await this.processTypebot( + // instance, + // remoteJid, + // msg, + // session, + // findTypebot, + // url, + // expire, + // typebot, + // keywordFinish, + // delayMessage, + // unknownMessage, + // listeningFromMe, + // stopBotFromMe, + // keepOpen, + // content, + // ); + // } + + // // await this.processTypebot( + // // instance, + // // remoteJid, + // // msg, + // // session, + // // findTypebot, + // // url, + // // expire, + // // typebot, + // // keywordFinish, + // // delayMessage, + // // unknownMessage, + // // listeningFromMe, + // // stopBotFromMe, + // // keepOpen, + // // content, + // // ); + + // if (session && !session.awaitUser) return; + // } catch (error) { + // this.logger.error(error); + // return; + // } + // } + + // private async processTypebot( + // instance: InstanceDto, + // remoteJid: string, + // msg: Message, + // session: TypebotSession, + // findTypebot: TypebotModel, + // url: string, + // expire: number, + // typebot: string, + // keywordFinish: string, + // delayMessage: number, + // unknownMessage: string, + // listeningFromMe: boolean, + // stopBotFromMe: boolean, + // keepOpen: boolean, + // content: string, + // ) { + // if (session && expire && expire > 0) { + // const now = Date.now(); + + // const sessionUpdatedAt = new Date(session.updatedAt).getTime(); + + // const diff = now - sessionUpdatedAt; + + // const diffInMinutes = Math.floor(diff / 1000 / 60); + + // if (diffInMinutes > expire) { + // await this.prismaRepository.typebotSession.deleteMany({ + // where: { + // typebotId: findTypebot.id, + // remoteJid: remoteJid, + // }, + // }); + + // const data = await this.createNewSession(instance, { + // enabled: findTypebot.enabled, + // url: url, + // typebot: typebot, + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // remoteJid: remoteJid, + // pushName: msg.pushName, + // typebotId: findTypebot.id, + // }); + + // if (data.session) { + // session = data.session; + // } + + // await this.sendWAMessage( + // instance, + // session, + // { + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // stopBotFromMe: stopBotFromMe, + // keepOpen: keepOpen, + // }, + // remoteJid, + // data.messages, + // data.input, + // data.clientSideActions, + // ); + + // if (data.messages.length === 0) { + // const content = this.getConversationMessage(msg.message); + + // if (!content) { + // if (unknownMessage) { + // this.waMonitor.waInstances[instance.instanceName].textMessage( + // { + // number: remoteJid.split('@')[0], + // delay: delayMessage || 1000, + // text: unknownMessage, + // }, + // false, + // ); + + // sendTelemetry('/message/sendText'); + // } + // return; + // } + + // if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { + // await this.prismaRepository.typebotSession.deleteMany({ + // where: { + // typebotId: findTypebot.id, + // remoteJid: remoteJid, + // }, + // }); + // return; + // } + + // try { + // const version = this.configService.get('TYPEBOT').API_VERSION; + // let urlTypebot: string; + // let reqData: {}; + // if (version === 'latest') { + // urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`; + // reqData = { + // message: content, + // }; + // } else { + // urlTypebot = `${url}/api/v1/sendMessage`; + // reqData = { + // message: content, + // sessionId: data.sessionId, + // }; + // } + + // const request = await axios.post(urlTypebot, reqData); + + // await this.sendWAMessage( + // instance, + // session, + // { + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // stopBotFromMe: stopBotFromMe, + // keepOpen: keepOpen, + // }, + // remoteJid, + // request.data.messages, + // request.data.input, + // request.data.clientSideActions, + // ); + // } catch (error) { + // this.logger.error(error); + // return; + // } + // } + + // return; + // } + // } + + // if (session && session.status !== 'opened') { + // return; + // } + + // if (!session) { + // const data = await this.createNewSession(instance, { + // enabled: findTypebot?.enabled, + // url: url, + // typebot: typebot, + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // remoteJid: remoteJid, + // pushName: msg.pushName, + // typebotId: findTypebot.id, + // }); + + // if (data?.session) { + // session = data.session; + // } + + // await this.sendWAMessage( + // instance, + // session, + // { + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // stopBotFromMe: stopBotFromMe, + // keepOpen: keepOpen, + // }, + // remoteJid, + // data?.messages, + // data?.input, + // data?.clientSideActions, + // ); + + // if (data.messages.length === 0) { + // if (!content) { + // if (unknownMessage) { + // this.waMonitor.waInstances[instance.instanceName].textMessage( + // { + // number: remoteJid.split('@')[0], + // delay: delayMessage || 1000, + // text: unknownMessage, + // }, + // false, + // ); + + // sendTelemetry('/message/sendText'); + // } + // return; + // } + + // if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { + // await this.prismaRepository.typebotSession.deleteMany({ + // where: { + // typebotId: findTypebot.id, + // remoteJid: remoteJid, + // }, + // }); + + // return; + // } + + // let request: any; + // try { + // const version = this.configService.get('TYPEBOT').API_VERSION; + // let urlTypebot: string; + // let reqData: {}; + // if (version === 'latest') { + // urlTypebot = `${url}/api/v1/sessions/${data.sessionId}/continueChat`; + // reqData = { + // message: content, + // }; + // } else { + // urlTypebot = `${url}/api/v1/sendMessage`; + // reqData = { + // message: content, + // sessionId: data.sessionId, + // }; + // } + // request = await axios.post(urlTypebot, reqData); + + // await this.sendWAMessage( + // instance, + // session, + // { + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // stopBotFromMe: stopBotFromMe, + // keepOpen: keepOpen, + // }, + // remoteJid, + // request.data.messages, + // request.data.input, + // request.data.clientSideActions, + // ); + // } catch (error) { + // this.logger.error(error); + // return; + // } + // } + // return; + // } + + // await this.prismaRepository.typebotSession.update({ + // where: { + // id: session.id, + // }, + // data: { + // status: 'opened', + // awaitUser: false, + // }, + // }); + + // if (!content) { + // if (unknownMessage) { + // this.waMonitor.waInstances[instance.instanceName].textMessage( + // { + // number: remoteJid.split('@')[0], + // delay: delayMessage || 1000, + // text: unknownMessage, + // }, + // false, + // ); + + // sendTelemetry('/message/sendText'); + // } + // return; + // } + + // if (keywordFinish && content.toLowerCase() === keywordFinish.toLowerCase()) { + // await this.prismaRepository.typebotSession.deleteMany({ + // where: { + // typebotId: findTypebot.id, + // remoteJid: remoteJid, + // }, + // }); + // return; + // } + + // const version = this.configService.get('TYPEBOT').API_VERSION; + // let urlTypebot: string; + // let reqData: {}; + // if (version === 'latest') { + // urlTypebot = `${url}/api/v1/sessions/${session.sessionId.split('-')[1]}/continueChat`; + // reqData = { + // message: content, + // }; + // } else { + // urlTypebot = `${url}/api/v1/sendMessage`; + // reqData = { + // message: content, + // sessionId: session.sessionId.split('-')[1], + // }; + // } + // const request = await axios.post(urlTypebot, reqData); + + // await this.sendWAMessage( + // instance, + // session, + // { + // expire: expire, + // keywordFinish: keywordFinish, + // delayMessage: delayMessage, + // unknownMessage: unknownMessage, + // listeningFromMe: listeningFromMe, + // stopBotFromMe: stopBotFromMe, + // keepOpen: keepOpen, + // }, + // remoteJid, + // request?.data?.messages, + // request?.data?.input, + // request?.data?.clientSideActions, + // ); + + // return; + // } +} diff --git a/src/api/integrations/openai/validate/openai.schema.ts b/src/api/integrations/openai/validate/openai.schema.ts new file mode 100644 index 00000000..f3f76ead --- /dev/null +++ b/src/api/integrations/openai/validate/openai.schema.ts @@ -0,0 +1,125 @@ +import { JSONSchema7 } from 'json-schema'; +import { v4 } from 'uuid'; + +const isNotEmpty = (...propertyNames: string[]): JSONSchema7 => { + const properties = {}; + propertyNames.forEach( + (property) => + (properties[property] = { + minLength: 1, + description: `The "${property}" cannot be empty`, + }), + ); + return { + if: { + propertyNames: { + enum: [...propertyNames], + }, + }, + then: { properties }, + }; +}; + +export const openaiSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + enabled: { type: 'boolean' }, + openaiCredsId: { type: 'string' }, + botType: { type: 'string', enum: ['assistant', 'chatCompletion'] }, + assistantId: { type: 'string' }, + model: { type: 'string' }, + systemMessages: { type: 'array', items: { type: 'string' } }, + assistantMessages: { type: 'array', items: { type: 'string' } }, + userMessages: { type: 'array', items: { type: 'string' } }, + maxTokens: { type: 'integer' }, + triggerType: { type: 'string', enum: ['all', 'keyword'] }, + triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex', 'none'] }, + triggerValue: { type: 'string' }, + expire: { type: 'integer' }, + keywordFinish: { type: 'string' }, + delayMessage: { type: 'integer' }, + unknownMessage: { type: 'string' }, + listeningFromMe: { type: 'boolean' }, + stopBotFromMe: { type: 'boolean' }, + keepOpen: { type: 'boolean' }, + debounceTime: { type: 'integer' }, + ignoreJids: { type: 'array', items: { type: 'string' } }, + }, + required: ['enabled', 'openaiCredsId', 'botType', 'triggerType'], + ...isNotEmpty('enabled', 'openaiCredsId', 'botType', 'triggerType'), +}; + +export const openaiCredsSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + apiKey: { type: 'string' }, + }, + required: ['enabled', 'apiKey'], + ...isNotEmpty('enabled', 'apiKey'), +}; + +export const openaiStatusSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + remoteJid: { type: 'string' }, + status: { type: 'string', enum: ['opened', 'closed', 'paused'] }, + }, + required: ['remoteJid', 'status'], + ...isNotEmpty('remoteJid', 'status'), +}; + +export const openaiSettingSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + openaiCredsId: { type: 'string' }, + expire: { type: 'integer' }, + keywordFinish: { type: 'string' }, + delayMessage: { type: 'integer' }, + unknownMessage: { type: 'string' }, + listeningFromMe: { type: 'boolean' }, + stopBotFromMe: { type: 'boolean' }, + keepOpen: { type: 'boolean' }, + debounceTime: { type: 'integer' }, + ignoreJids: { type: 'array', items: { type: 'string' } }, + openaiIdFallback: { type: 'string' }, + }, + required: [ + 'openaiCredsId', + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + ], + ...isNotEmpty( + 'openaiCredsId', + 'expire', + 'keywordFinish', + 'delayMessage', + 'unknownMessage', + 'listeningFromMe', + 'stopBotFromMe', + 'keepOpen', + 'debounceTime', + 'ignoreJids', + ), +}; + +export const openaiIgnoreJidSchema: JSONSchema7 = { + $id: v4(), + type: 'object', + properties: { + remoteJid: { type: 'string' }, + action: { type: 'string', enum: ['add', 'remove'] }, + }, + required: ['remoteJid', 'action'], + ...isNotEmpty('remoteJid', 'action'), +}; diff --git a/src/api/server.module.ts b/src/api/server.module.ts index c99115fa..d26c1800 100644 --- a/src/api/server.module.ts +++ b/src/api/server.module.ts @@ -13,6 +13,8 @@ import { TemplateController } from './controllers/template.controller'; import { WebhookController } from './controllers/webhook.controller'; import { ChatwootController } from './integrations/chatwoot/controllers/chatwoot.controller'; import { ChatwootService } from './integrations/chatwoot/services/chatwoot.service'; +import { OpenaiController } from './integrations/openai/controllers/openai.controller'; +import { OpenaiService } from './integrations/openai/services/openai.service'; import { RabbitmqController } from './integrations/rabbitmq/controllers/rabbitmq.controller'; import { RabbitmqService } from './integrations/rabbitmq/services/rabbitmq.service'; import { S3Controller } from './integrations/s3/controllers/s3.controller'; @@ -65,6 +67,9 @@ const authService = new AuthService(prismaRepository); const typebotService = new TypebotService(waMonitor, configService, prismaRepository); export const typebotController = new TypebotController(typebotService); +const openaiService = new OpenaiService(waMonitor, configService, prismaRepository); +export const openaiController = new OpenaiController(openaiService); + const s3Service = new S3Service(prismaRepository); export const s3Controller = new S3Controller(s3Service); diff --git a/src/config/env.config.ts b/src/config/env.config.ts index dbe6f6e8..c6ec77ea 100644 --- a/src/config/env.config.ts +++ b/src/config/env.config.ts @@ -191,6 +191,7 @@ export type Chatwoot = { PLACEHOLDER_MEDIA_MESSAGE: boolean; }; }; +export type Openai = { ENABLED: boolean; API_KEY_GLOBAL?: string }; export type S3 = { ACCESS_KEY: string; @@ -224,6 +225,7 @@ export interface Env { QRCODE: QrCode; TYPEBOT: Typebot; CHATWOOT: Chatwoot; + OPENAI: Openai; CACHE: CacheConf; S3?: S3; AUTHENTICATION: Auth; @@ -431,6 +433,10 @@ export class ConfigService { PLACEHOLDER_MEDIA_MESSAGE: process.env?.CHATWOOT_IMPORT_PLACEHOLDER_MEDIA_MESSAGE === 'true', }, }, + OPENAI: { + ENABLED: process.env?.OPENAI_ENABLED === 'true', + API_KEY_GLOBAL: process.env?.OPENAI_API_KEY_GLOBAL || null, + }, CACHE: { REDIS: { ENABLED: process.env?.CACHE_REDIS_ENABLED === 'true', diff --git a/src/validate/validate.schema.ts b/src/validate/validate.schema.ts index 66f847a7..38967aa3 100644 --- a/src/validate/validate.schema.ts +++ b/src/validate/validate.schema.ts @@ -1,5 +1,6 @@ // Integrations Schema export * from '../api/integrations/chatwoot/validate/chatwoot.schema'; +export * from '../api/integrations/openai/validate/openai.schema'; export * from '../api/integrations/rabbitmq/validate/rabbitmq.schema'; export * from '../api/integrations/sqs/validate/sqs.schema'; export * from '../api/integrations/typebot/validate/typebot.schema';