feat: Fixes and implementation of regex and fallback in typebot

This commit is contained in:
Davidson Gomes 2024-07-12 20:03:53 -03:00
parent a52a687493
commit 480cc67927
6 changed files with 224 additions and 95 deletions

View File

@ -10,6 +10,8 @@
* Organization configuration and logo in chatwoot bot contact * Organization configuration and logo in chatwoot bot contact
* Added debounce time for typebot messages * Added debounce time for typebot messages
* Tagging in chatwoot contact by instance * 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 ### Fixed
@ -19,6 +21,8 @@
* Correction of audio sending, now we can speed it up and have the audio wireframe * Correction of audio sending, now we can speed it up and have the audio wireframe
* Reply with media message on Chatwoot * Reply with media message on Chatwoot
* improvements in sending status and groups * improvements in sending status and groups
* Correction in response returns from buttons, lists and templates
* EvolutionAPI/Baileys implemented
### Break changes ### Break changes
@ -35,6 +39,7 @@
- Session search by typebot or remoteJid - Session search by typebot or remoteJid
- KeepOpen configuration (keeps the session even when the bot ends, to run once per contact) - 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. - 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) # 1.8.2 (2024-07-03 13:50)

View File

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

View File

@ -43,6 +43,7 @@ enum TriggerOperator {
equals equals
startsWith startsWith
endsWith endsWith
regex
} }
model Instance { model Instance {
@ -271,6 +272,7 @@ model Typebot {
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String instanceId String
sessions TypebotSession[] sessions TypebotSession[]
TypebotSetting TypebotSetting[]
} }
model TypebotSession { model TypebotSession {
@ -291,17 +293,19 @@ model TypebotSession {
} }
model TypebotSetting { model TypebotSetting {
id String @id @default(cuid()) id String @id @default(cuid())
expire Int? @default(0) @db.Integer expire Int? @default(0) @db.Integer
keywordFinish String? @db.VarChar(100) keywordFinish String? @db.VarChar(100)
delayMessage Int? @db.Integer delayMessage Int? @db.Integer
unknownMessage String? @db.VarChar(100) unknownMessage String? @db.VarChar(100)
listeningFromMe Boolean? @default(false) @db.Boolean listeningFromMe Boolean? @default(false) @db.Boolean
stopBotFromMe Boolean? @default(false) @db.Boolean stopBotFromMe Boolean? @default(false) @db.Boolean
keepOpen Boolean? @default(false) @db.Boolean keepOpen Boolean? @default(false) @db.Boolean
debounceTime Int? @db.Integer debounceTime Int? @db.Integer
createdAt DateTime? @default(now()) @db.Timestamp typebotIdFallback String? @db.VarChar(100)
updatedAt DateTime @updatedAt @db.Timestamp createdAt DateTime? @default(now()) @db.Timestamp
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) updatedAt DateTime @updatedAt @db.Timestamp
instanceId String @unique Fallback Typebot? @relation(fields: [typebotIdFallback], references: [id])
Instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String @unique
} }

View File

@ -42,4 +42,5 @@ export class TypebotSettingDto {
stopBotFromMe?: boolean; stopBotFromMe?: boolean;
keepOpen?: boolean; keepOpen?: boolean;
debounceTime?: number; debounceTime?: number;
typebotIdFallback?: string;
} }

View File

@ -45,24 +45,34 @@ export class TypebotService {
}, },
}); });
if (!defaultSettingCheck) { if (!data.expire) data.expire = defaultSettingCheck?.expire || 0;
throw new Error('Default settings not found'); 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.expire) data.expire = defaultSettingCheck.expire; if (!defaultSettingCheck) {
if (!data.keywordFinish) data.keywordFinish = defaultSettingCheck.keywordFinish; await this.setDefaultSettings(instance, {
if (!data.delayMessage) data.delayMessage = defaultSettingCheck.delayMessage; expire: data.expire,
if (!data.unknownMessage) data.unknownMessage = defaultSettingCheck.unknownMessage; keywordFinish: data.keywordFinish,
if (!data.listeningFromMe) data.listeningFromMe = defaultSettingCheck.listeningFromMe; delayMessage: data.delayMessage,
if (!data.stopBotFromMe) data.stopBotFromMe = defaultSettingCheck.stopBotFromMe; unknownMessage: data.unknownMessage,
if (!data.keepOpen) data.keepOpen = defaultSettingCheck.keepOpen; listeningFromMe: data.listeningFromMe,
if (!data.debounceTime) data.debounceTime = defaultSettingCheck.debounceTime; stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen,
debounceTime: data.debounceTime,
});
}
} }
const checkTriggerAll = await this.prismaRepository.typebot.findFirst({ const checkTriggerAll = await this.prismaRepository.typebot.findFirst({
where: { where: {
enabled: true, enabled: true,
triggerType: 'all', triggerType: 'all',
instanceId: instanceId,
}, },
}); });
@ -74,6 +84,7 @@ export class TypebotService {
where: { where: {
url: data.url, url: data.url,
typebot: data.typebot, typebot: data.typebot,
instanceId: instanceId,
}, },
}); });
@ -90,6 +101,7 @@ export class TypebotService {
where: { where: {
triggerOperator: data.triggerOperator, triggerOperator: data.triggerOperator,
triggerValue: data.triggerValue, triggerValue: data.triggerValue,
instanceId: instanceId,
}, },
}); });
@ -186,6 +198,7 @@ export class TypebotService {
id: { id: {
not: typebotId, not: typebotId,
}, },
instanceId: instanceId,
}, },
}); });
@ -203,6 +216,7 @@ export class TypebotService {
id: { id: {
not: typebotId, not: typebotId,
}, },
instanceId: instanceId,
}, },
}); });
@ -222,6 +236,7 @@ export class TypebotService {
id: { id: {
not: typebotId, not: typebotId,
}, },
instanceId: instanceId,
}, },
}); });
@ -358,6 +373,7 @@ export class TypebotService {
stopBotFromMe: data.stopBotFromMe, stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen, keepOpen: data.keepOpen,
debounceTime: data.debounceTime, debounceTime: data.debounceTime,
typebotIdFallback: data.typebotIdFallback,
}, },
}); });
@ -370,6 +386,7 @@ export class TypebotService {
stopBotFromMe: updateSettings.stopBotFromMe, stopBotFromMe: updateSettings.stopBotFromMe,
keepOpen: updateSettings.keepOpen, keepOpen: updateSettings.keepOpen,
debounceTime: updateSettings.debounceTime, debounceTime: updateSettings.debounceTime,
typebotIdFallback: updateSettings.typebotIdFallback,
}; };
} }
@ -383,6 +400,7 @@ export class TypebotService {
stopBotFromMe: data.stopBotFromMe, stopBotFromMe: data.stopBotFromMe,
keepOpen: data.keepOpen, keepOpen: data.keepOpen,
debounceTime: data.debounceTime, debounceTime: data.debounceTime,
typebotIdFallback: data.typebotIdFallback,
instanceId: instanceId, instanceId: instanceId,
}, },
}); });
@ -396,6 +414,7 @@ export class TypebotService {
stopBotFromMe: newSetttings.stopBotFromMe, stopBotFromMe: newSetttings.stopBotFromMe,
keepOpen: newSetttings.keepOpen, keepOpen: newSetttings.keepOpen,
debounceTime: newSetttings.debounceTime, debounceTime: newSetttings.debounceTime,
typebotIdFallback: newSetttings.typebotIdFallback,
}; };
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -417,6 +436,9 @@ export class TypebotService {
where: { where: {
instanceId: instanceId, instanceId: instanceId,
}, },
include: {
Fallback: true,
},
}); });
if (!settings) { if (!settings) {
@ -431,6 +453,8 @@ export class TypebotService {
listeningFromMe: settings.listeningFromMe, listeningFromMe: settings.listeningFromMe,
stopBotFromMe: settings.stopBotFromMe, stopBotFromMe: settings.stopBotFromMe,
keepOpen: settings.keepOpen, keepOpen: settings.keepOpen,
typebotIdFallback: settings.typebotIdFallback,
fallback: settings.Fallback,
}; };
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -573,17 +597,25 @@ export class TypebotService {
}, },
}); });
if (!defaultSettingCheck) { if (!expire) expire = defaultSettingCheck?.expire || 0;
throw new Error('Default settings not found'); 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 (!expire) expire = defaultSettingCheck.expire; if (!defaultSettingCheck) {
if (!keywordFinish) keywordFinish = defaultSettingCheck.keywordFinish; await this.setDefaultSettings(instance, {
if (!delayMessage) delayMessage = defaultSettingCheck.delayMessage; expire: expire,
if (!unknownMessage) unknownMessage = defaultSettingCheck.unknownMessage; keywordFinish: keywordFinish,
if (!listeningFromMe) listeningFromMe = defaultSettingCheck.listeningFromMe; delayMessage: delayMessage,
if (!stopBotFromMe) stopBotFromMe = defaultSettingCheck.stopBotFromMe; unknownMessage: unknownMessage,
if (!keepOpen) keepOpen = defaultSettingCheck.keepOpen; listeningFromMe: listeningFromMe,
stopBotFromMe: stopBotFromMe,
keepOpen: keepOpen,
});
}
} }
const prefilledVariables = { const prefilledVariables = {
@ -1062,73 +1094,148 @@ export class TypebotService {
} }
} }
public async findTypebotByTrigger(content: string) { public async findTypebotByTrigger(content: string, instanceId: string) {
let typebot = null; console.log('Check for triggerType all');
// Check for triggerType 'all'
const findTriggerAll = await this.prismaRepository.typebot.findFirst({ const findTriggerAll = await this.prismaRepository.typebot.findFirst({
where: { where: {
enabled: true, enabled: true,
triggerType: 'all', triggerType: 'all',
instanceId: instanceId,
}, },
}); });
if (findTriggerAll) { console.log('findTriggerAll', findTriggerAll);
typebot = findTriggerAll;
} else {
const findTriggerEquals = await this.prismaRepository.typebot.findFirst({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'equals',
triggerValue: content,
},
});
if (findTriggerEquals) { if (findTriggerAll) return findTriggerAll;
typebot = findTriggerEquals;
} else {
const findTriggerStartsWith = await this.prismaRepository.typebot.findFirst({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'startsWith',
triggerValue: { startsWith: content },
},
});
if (findTriggerStartsWith) { console.log('Check for exact match');
typebot = findTriggerStartsWith;
} else {
const findTriggerEndsWith = await this.prismaRepository.typebot.findFirst({
where: {
enabled: true,
triggerType: 'keyword',
triggerOperator: 'endsWith',
triggerValue: { endsWith: content },
},
});
if (findTriggerEndsWith) { // Check for exact match
typebot = findTriggerEndsWith; const findTriggerEquals = await this.prismaRepository.typebot.findFirst({
} else { where: {
const findTriggerContains = await this.prismaRepository.typebot.findFirst({ enabled: true,
where: { triggerType: 'keyword',
enabled: true, triggerOperator: 'equals',
triggerType: 'keyword', triggerValue: content,
triggerOperator: 'contains', instanceId: instanceId,
triggerValue: { contains: content }, },
}, });
});
if (findTriggerContains) { console.log('findTriggerEquals', findTriggerEquals);
typebot = findTriggerContains;
} 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) { 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); const content = this.getConversationMessage(msg);
let findTypebot = null; let findTypebot = null;
console.log('content', content);
if (!session) { if (!session) {
findTypebot = await this.findTypebotByTrigger(content); findTypebot = await this.findTypebotByTrigger(content, instance.instanceId);
if (!findTypebot) { if (!findTypebot) {
return; return;
@ -1198,12 +1313,6 @@ export class TypebotService {
!stopBotFromMe || !stopBotFromMe ||
!keepOpen !keepOpen
) { ) {
const settings = await this.prismaRepository.typebotSetting.findFirst({
where: {
instanceId: instance.instanceId,
},
});
if (!expire) expire = settings.expire; if (!expire) expire = settings.expire;
if (!keywordFinish) keywordFinish = settings.keywordFinish; if (!keywordFinish) keywordFinish = settings.keywordFinish;
@ -1242,6 +1351,7 @@ export class TypebotService {
return; return;
} }
console.log(debounceTime);
if (debounceTime && debounceTime > 0) { if (debounceTime && debounceTime > 0) {
this.processDebounce(content, remoteJid, debounceTime, async (debouncedContent) => { this.processDebounce(content, remoteJid, debounceTime, async (debouncedContent) => {
await this.processTypebot( await this.processTypebot(
@ -1469,7 +1579,7 @@ export class TypebotService {
typebotId: findTypebot.id, typebotId: findTypebot.id,
}); });
if (data.session) { if (data?.session) {
session = data.session; session = data.session;
} }

View File

@ -28,7 +28,7 @@ export const typebotSchema: JSONSchema7 = {
url: { type: 'string' }, url: { type: 'string' },
typebot: { type: 'string' }, typebot: { type: 'string' },
triggerType: { type: 'string', enum: ['all', 'keyword'] }, 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' }, triggerValue: { type: 'string' },
expire: { type: 'integer' }, expire: { type: 'integer' },
keywordFinish: { type: 'string' }, keywordFinish: { type: 'string' },
@ -76,6 +76,7 @@ export const typebotSettingSchema: JSONSchema7 = {
stopBotFromMe: { type: 'boolean' }, stopBotFromMe: { type: 'boolean' },
keepOpen: { type: 'boolean' }, keepOpen: { type: 'boolean' },
debounceTime: { type: 'integer' }, debounceTime: { type: 'integer' },
typebotIdFallback: { type: 'string' },
}, },
required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'], required: ['expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'],
...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'), ...isNotEmpty('expire', 'keywordFinish', 'delayMessage', 'unknownMessage', 'listeningFromMe', 'stopBotFromMe'),