From 480cc6792758817392edccd1be77a1c512a8fedc Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Fri, 12 Jul 2024 20:03:53 -0300 Subject: [PATCH] feat: Fixes and implementation of regex and fallback in typebot --- CHANGELOG.md | 5 + .../migration.sql | 8 + prisma/postgresql-schema.prisma | 30 +- .../integrations/typebot/dto/typebot.dto.ts | 1 + .../typebot/services/typebot.service.ts | 268 ++++++++++++------ .../typebot/validate/typebot.schema.ts | 3 +- 6 files changed, 222 insertions(+), 93 deletions(-) create mode 100644 prisma/migrations/20240712223655_column_fallback_typebot/migration.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc398757..213c70df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ * Organization configuration and logo in chatwoot bot contact * Added debounce time for typebot messages * Tagging in chatwoot contact by instance +* Add support for managing WhatsApp templates via official API +* Fixes and implementation of regex and fallback in typebot ### Fixed @@ -19,6 +21,8 @@ * Correction of audio sending, now we can speed it up and have the audio wireframe * Reply with media message on Chatwoot * improvements in sending status and groups +* Correction in response returns from buttons, lists and templates +* EvolutionAPI/Baileys implemented ### Break changes @@ -35,6 +39,7 @@ - Session search by typebot or remoteJid - KeepOpen configuration (keeps the session even when the bot ends, to run once per contact) - StopBotFromMe configuration, allows me to stop the bot if I send a chat message. +* Changed the way the goal webhook is configured # 1.8.2 (2024-07-03 13:50) diff --git a/prisma/migrations/20240712223655_column_fallback_typebot/migration.sql b/prisma/migrations/20240712223655_column_fallback_typebot/migration.sql new file mode 100644 index 000000000..b16e7d87b --- /dev/null +++ b/prisma/migrations/20240712223655_column_fallback_typebot/migration.sql @@ -0,0 +1,8 @@ +-- AlterEnum +ALTER TYPE "TriggerOperator" ADD VALUE 'regex'; + +-- AlterTable +ALTER TABLE "TypebotSetting" ADD COLUMN "typebotIdFallback" VARCHAR(100); + +-- AddForeignKey +ALTER TABLE "TypebotSetting" ADD CONSTRAINT "TypebotSetting_typebotIdFallback_fkey" FOREIGN KEY ("typebotIdFallback") REFERENCES "Typebot"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/postgresql-schema.prisma b/prisma/postgresql-schema.prisma index fdb73b246..a8806f89c 100644 --- a/prisma/postgresql-schema.prisma +++ b/prisma/postgresql-schema.prisma @@ -43,6 +43,7 @@ enum TriggerOperator { equals startsWith endsWith + regex } model Instance { @@ -271,6 +272,7 @@ model Typebot { Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) instanceId String sessions TypebotSession[] + TypebotSetting TypebotSetting[] } model TypebotSession { @@ -291,17 +293,19 @@ model TypebotSession { } 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 - 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()) + 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) + 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 } diff --git a/src/api/integrations/typebot/dto/typebot.dto.ts b/src/api/integrations/typebot/dto/typebot.dto.ts index bc968df3a..0fb50a922 100644 --- a/src/api/integrations/typebot/dto/typebot.dto.ts +++ b/src/api/integrations/typebot/dto/typebot.dto.ts @@ -42,4 +42,5 @@ export class TypebotSettingDto { stopBotFromMe?: boolean; keepOpen?: boolean; debounceTime?: number; + typebotIdFallback?: string; } diff --git a/src/api/integrations/typebot/services/typebot.service.ts b/src/api/integrations/typebot/services/typebot.service.ts index 3cb23c0f0..1b7d6be29 100644 --- a/src/api/integrations/typebot/services/typebot.service.ts +++ b/src/api/integrations/typebot/services/typebot.service.ts @@ -45,24 +45,34 @@ export class TypebotService { }, }); + 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 (!defaultSettingCheck) { - throw new Error('Default settings not found'); + await this.setDefaultSettings(instance, { + expire: data.expire, + keywordFinish: data.keywordFinish, + delayMessage: data.delayMessage, + unknownMessage: data.unknownMessage, + listeningFromMe: data.listeningFromMe, + stopBotFromMe: data.stopBotFromMe, + keepOpen: data.keepOpen, + debounceTime: data.debounceTime, + }); } - - if (!data.expire) data.expire = defaultSettingCheck.expire; - if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck.keywordFinish; - if (!data.delayMessage) data.delayMessage = defaultSettingCheck.delayMessage; - if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck.unknownMessage; - if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck.listeningFromMe; - if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck.stopBotFromMe; - if (!data.keepOpen) data.keepOpen = defaultSettingCheck.keepOpen; - if (!data.debounceTime) data.debounceTime = defaultSettingCheck.debounceTime; } const checkTriggerAll = await this.prismaRepository.typebot.findFirst({ where: { enabled: true, triggerType: 'all', + instanceId: instanceId, }, }); @@ -74,6 +84,7 @@ export class TypebotService { where: { url: data.url, typebot: data.typebot, + instanceId: instanceId, }, }); @@ -90,6 +101,7 @@ export class TypebotService { where: { triggerOperator: data.triggerOperator, triggerValue: data.triggerValue, + instanceId: instanceId, }, }); @@ -186,6 +198,7 @@ export class TypebotService { id: { not: typebotId, }, + instanceId: instanceId, }, }); @@ -203,6 +216,7 @@ export class TypebotService { id: { not: typebotId, }, + instanceId: instanceId, }, }); @@ -222,6 +236,7 @@ export class TypebotService { id: { not: typebotId, }, + instanceId: instanceId, }, }); @@ -358,6 +373,7 @@ export class TypebotService { stopBotFromMe: data.stopBotFromMe, keepOpen: data.keepOpen, debounceTime: data.debounceTime, + typebotIdFallback: data.typebotIdFallback, }, }); @@ -370,6 +386,7 @@ export class TypebotService { stopBotFromMe: updateSettings.stopBotFromMe, keepOpen: updateSettings.keepOpen, debounceTime: updateSettings.debounceTime, + typebotIdFallback: updateSettings.typebotIdFallback, }; } @@ -383,6 +400,7 @@ export class TypebotService { stopBotFromMe: data.stopBotFromMe, keepOpen: data.keepOpen, debounceTime: data.debounceTime, + typebotIdFallback: data.typebotIdFallback, instanceId: instanceId, }, }); @@ -396,6 +414,7 @@ export class TypebotService { stopBotFromMe: newSetttings.stopBotFromMe, keepOpen: newSetttings.keepOpen, debounceTime: newSetttings.debounceTime, + typebotIdFallback: newSetttings.typebotIdFallback, }; } catch (error) { this.logger.error(error); @@ -417,6 +436,9 @@ export class TypebotService { where: { instanceId: instanceId, }, + include: { + Fallback: true, + }, }); if (!settings) { @@ -431,6 +453,8 @@ export class TypebotService { listeningFromMe: settings.listeningFromMe, stopBotFromMe: settings.stopBotFromMe, keepOpen: settings.keepOpen, + typebotIdFallback: settings.typebotIdFallback, + fallback: settings.Fallback, }; } catch (error) { this.logger.error(error); @@ -573,17 +597,25 @@ export class TypebotService { }, }); + if (!expire) expire = defaultSettingCheck?.expire || 0; + if (!keywordFinish) keywordFinish = defaultSettingCheck?.keywordFinish || '#SAIR'; + if (!delayMessage) delayMessage = defaultSettingCheck?.delayMessage || 1000; + if (!unknownMessage) unknownMessage = defaultSettingCheck?.unknownMessage || 'Desculpe, não entendi'; + if (!listeningFromMe) listeningFromMe = defaultSettingCheck?.listeningFromMe || false; + if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck?.stopBotFromMe || false; + if (!keepOpen) keepOpen = defaultSettingCheck?.keepOpen || false; + if (!defaultSettingCheck) { - throw new Error('Default settings not found'); + await this.setDefaultSettings(instance, { + expire: expire, + keywordFinish: keywordFinish, + delayMessage: delayMessage, + unknownMessage: unknownMessage, + listeningFromMe: listeningFromMe, + stopBotFromMe: stopBotFromMe, + keepOpen: keepOpen, + }); } - - if (!expire) expire = defaultSettingCheck.expire; - if (!keywordFinish) keywordFinish = defaultSettingCheck.keywordFinish; - if (!delayMessage) delayMessage = defaultSettingCheck.delayMessage; - if (!unknownMessage) unknownMessage = defaultSettingCheck.unknownMessage; - if (!listeningFromMe) listeningFromMe = defaultSettingCheck.listeningFromMe; - if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck.stopBotFromMe; - if (!keepOpen) keepOpen = defaultSettingCheck.keepOpen; } const prefilledVariables = { @@ -1062,73 +1094,148 @@ export class TypebotService { } } - public async findTypebotByTrigger(content: string) { - let typebot = null; + public async findTypebotByTrigger(content: string, instanceId: string) { + console.log('Check for triggerType all'); + // Check for triggerType 'all' const findTriggerAll = await this.prismaRepository.typebot.findFirst({ where: { enabled: true, triggerType: 'all', + instanceId: instanceId, }, }); - if (findTriggerAll) { - typebot = findTriggerAll; - } else { - const findTriggerEquals = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'equals', - triggerValue: content, - }, - }); + console.log('findTriggerAll', findTriggerAll); - if (findTriggerEquals) { - typebot = findTriggerEquals; - } else { - const findTriggerStartsWith = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'startsWith', - triggerValue: { startsWith: content }, - }, - }); + if (findTriggerAll) return findTriggerAll; - if (findTriggerStartsWith) { - typebot = findTriggerStartsWith; - } else { - const findTriggerEndsWith = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'endsWith', - triggerValue: { endsWith: content }, - }, - }); + console.log('Check for exact match'); - if (findTriggerEndsWith) { - typebot = findTriggerEndsWith; - } else { - const findTriggerContains = await this.prismaRepository.typebot.findFirst({ - where: { - enabled: true, - triggerType: 'keyword', - triggerOperator: 'contains', - triggerValue: { contains: content }, - }, - }); + // Check for exact match + const findTriggerEquals = await this.prismaRepository.typebot.findFirst({ + where: { + enabled: true, + triggerType: 'keyword', + triggerOperator: 'equals', + triggerValue: content, + instanceId: instanceId, + }, + }); - if (findTriggerContains) { - typebot = findTriggerContains; - } - } - } + console.log('findTriggerEquals', findTriggerEquals); + + if (findTriggerEquals) return findTriggerEquals; + + console.log('Check for regex match'); + + // 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; } } - return typebot; + console.log('findTriggerRegex', findTriggerRegex); + + if (findTriggerRegex) return findTriggerRegex; + + console.log('Check for startsWith match'); + + // Check for startsWith match + const findTriggerStartsWith = await this.prismaRepository.typebot.findFirst({ + where: { + enabled: true, + triggerType: 'keyword', + triggerOperator: 'startsWith', + triggerValue: { + startsWith: content, + }, + instanceId: instanceId, + }, + }); + + console.log('findTriggerStartsWith', findTriggerStartsWith); + + if (findTriggerStartsWith) return findTriggerStartsWith; + + console.log('Check for endsWith match'); + + // Check for endsWith match + const findTriggerEndsWith = await this.prismaRepository.typebot.findFirst({ + where: { + enabled: true, + triggerType: 'keyword', + triggerOperator: 'endsWith', + triggerValue: { + endsWith: content, + }, + instanceId: instanceId, + }, + }); + + console.log('findTriggerEndsWith', findTriggerEndsWith); + + if (findTriggerEndsWith) return findTriggerEndsWith; + + console.log('Check for contains match'); + + // Check for contains match + const findTriggerContains = await this.prismaRepository.typebot.findFirst({ + where: { + enabled: true, + triggerType: 'keyword', + triggerOperator: 'contains', + triggerValue: { + contains: content, + }, + instanceId: instanceId, + }, + }); + + console.log('findTriggerContains', findTriggerContains); + + if (findTriggerContains) return findTriggerContains; + + console.log('Check for fallback'); + + const fallback = await this.prismaRepository.typebotSetting.findFirst({ + where: { + instanceId: instanceId, + }, + }); + + console.log('fallback', fallback); + + if (fallback?.typebotIdFallback) { + console.log('Check for fallback typebot'); + + const findFallback = await this.prismaRepository.typebot.findFirst({ + where: { + id: fallback.typebotIdFallback, + }, + }); + + console.log('findFallback', findFallback); + + if (findFallback) return findFallback; + } + + return null; } private processDebounce(content: string, remoteJid: string, debounceTime: number, callback: any) { @@ -1160,12 +1267,20 @@ export class TypebotService { }, }); + const settings = await this.prismaRepository.typebotSetting.findFirst({ + where: { + instanceId: instance.instanceId, + }, + }); + const content = this.getConversationMessage(msg); let findTypebot = null; + console.log('content', content); + if (!session) { - findTypebot = await this.findTypebotByTrigger(content); + findTypebot = await this.findTypebotByTrigger(content, instance.instanceId); if (!findTypebot) { return; @@ -1198,12 +1313,6 @@ export class TypebotService { !stopBotFromMe || !keepOpen ) { - const settings = await this.prismaRepository.typebotSetting.findFirst({ - where: { - instanceId: instance.instanceId, - }, - }); - if (!expire) expire = settings.expire; if (!keywordFinish) keywordFinish = settings.keywordFinish; @@ -1242,6 +1351,7 @@ export class TypebotService { return; } + console.log(debounceTime); if (debounceTime && debounceTime > 0) { this.processDebounce(content, remoteJid, debounceTime, async (debouncedContent) => { await this.processTypebot( @@ -1469,7 +1579,7 @@ export class TypebotService { typebotId: findTypebot.id, }); - if (data.session) { + if (data?.session) { session = data.session; } diff --git a/src/api/integrations/typebot/validate/typebot.schema.ts b/src/api/integrations/typebot/validate/typebot.schema.ts index e17eab8b3..e5750250d 100644 --- a/src/api/integrations/typebot/validate/typebot.schema.ts +++ b/src/api/integrations/typebot/validate/typebot.schema.ts @@ -28,7 +28,7 @@ export const typebotSchema: JSONSchema7 = { url: { type: 'string' }, typebot: { type: 'string' }, triggerType: { type: 'string', enum: ['all', 'keyword'] }, - triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith'] }, + triggerOperator: { type: 'string', enum: ['equals', 'contains', 'startsWith', 'endsWith', 'regex'] }, triggerValue: { type: 'string' }, expire: { type: 'integer' }, keywordFinish: { type: 'string' }, @@ -76,6 +76,7 @@ export const typebotSettingSchema: JSONSchema7 = { stopBotFromMe: { type: 'boolean' }, keepOpen: { type: 'boolean' }, debounceTime: { type: 'integer' }, + typebotIdFallback: { type: 'string' }, }, required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'], ...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'),