diff --git a/__test__/0.2.1-case.test.js b/__test__/0.2.1-case.test.js new file mode 100644 index 000000000..d30e8b8e8 --- /dev/null +++ b/__test__/0.2.1-case.test.js @@ -0,0 +1,140 @@ +const { suite } = require('uvu') +const assert = require('uvu/assert') +const { addKeyword, createBot, createFlow, EVENTS } = require('../packages/bot/index') +const { setup, clear, delay } = require('../__mocks__/env') + +const suiteCase = suite('Flujo: idle state') + +suiteCase.before.each(setup) +suiteCase.after.each(clear) + +suiteCase(`Prevenir enviar mensaje luego de inactividad (2seg)`, async ({ database, provider }) => { + const flujoFinal = addKeyword(EVENTS.ACTION).addAnswer('Se cancelo por inactividad') + + const flujoPrincipal = addKeyword(['hola']) + .addAnswer( + 'debes de responder antes de que transcurran 2 segundos (2000)', + { capture: true, idle: 2000 }, + async (ctx, { gotoFlow, inRef }) => { + if (ctx?.idleFallBack) { + return gotoFlow(flujoFinal) + } + } + ) + .addAnswer('gracias!') + + await createBot({ + database, + flow: createFlow([flujoPrincipal, flujoFinal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await provider.delaySendMessage(50, 'message', { + from: '000', + body: 'mensaje al segundo', + }) + + await delay(3000) + const getHistory = database.listHistory.map((i) => i.answer) + assert.is('debes de responder antes de que transcurran 2 segundos (2000)', getHistory[0]) + assert.is('mensaje al segundo', getHistory[1]) + assert.is('gracias!', getHistory[2]) + assert.is(undefined, getHistory[3]) +}) + +suiteCase(`Enviar mensaje luego de inactividad (2seg)`, async ({ database, provider }) => { + const flujoFinal = addKeyword(EVENTS.ACTION).addAnswer('Se cancelo por inactividad') + + const flujoPrincipal = addKeyword(['hola']) + .addAnswer( + 'debes de responder antes de que transcurran 2 segundos (2000)', + { idle: 2000, capture: true }, + async (ctx, { gotoFlow }) => { + if (ctx?.idleFallBack) { + return gotoFlow(flujoFinal) + } + } + ) + .addAnswer('gracias!') + + await createBot({ + database, + flow: createFlow([flujoPrincipal, flujoFinal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await delay(3000) + const getHistory = database.listHistory.map((i) => i.answer) + assert.is('debes de responder antes de que transcurran 2 segundos (2000)', getHistory[0]) + assert.is('Se cancelo por inactividad', getHistory[1]) + assert.is(undefined, getHistory[2]) +}) + +suiteCase(`Enviar mensajes con ambos casos de idle`, async ({ database, provider }) => { + const flujoFinal = addKeyword(EVENTS.ACTION) + .addAnswer('Se cancelo por inactividad') + .addAction(async (_, { flowDynamic }) => { + await flowDynamic(`Empezemos de nuevo.`) + await flowDynamic(`Cual es el numero de orden? tienes dos segundos para responder...`) + }) + .addAction({ capture: true, idle: 2000 }, async (ctx, { flowDynamic }) => { + if (ctx?.idleFallBack) { + return flowDynamic(`BYE!`) + } + await flowDynamic(`Ok el numero que escribiste es ${ctx.body}`) + }) + .addAnswer('gracias!') + + const flujoPrincipal = addKeyword(['hola']).addAnswer( + 'Hola tienes 2 segundos para responder si no te pedire de nuevo otro dato', + { idle: 2000, capture: true }, + async (ctx, { gotoFlow }) => { + if (ctx?.idleFallBack) { + return gotoFlow(flujoFinal) + } + } + ) + + await createBot({ + database, + flow: createFlow([flujoPrincipal, flujoFinal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await delay(2100) + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'el numero es 444', + }) + + await delay(10000) + + const getHistory = database.listHistory.map((i) => i.answer) + assert.is('Hola tienes 2 segundos para responder si no te pedire de nuevo otro dato', getHistory[0]) + assert.is('Se cancelo por inactividad', getHistory[1]) + assert.is('__call_action__', getHistory[2]) + assert.is('Empezemos de nuevo.', getHistory[3]) + assert.is('Cual es el numero de orden? tienes dos segundos para responder...', getHistory[4]) + assert.is('__capture_only_intended__', getHistory[5]) + assert.is('el numero es 444', getHistory[6]) + assert.is('Ok el numero que escribiste es el numero es 444', getHistory[7]) + assert.is('gracias!', getHistory[8]) + assert.is(undefined, getHistory[9]) +}) + +suiteCase.run() diff --git a/packages/bot/context/idleState.class.js b/packages/bot/context/idleState.class.js new file mode 100755 index 000000000..d333fab74 --- /dev/null +++ b/packages/bot/context/idleState.class.js @@ -0,0 +1,47 @@ +const { EventEmitter } = require('node:events') + +class IdleState extends EventEmitter { + index = new Map() + indexInterval = new Map() + timer = null + startTime = 0 + endTime = 0 + + setIdleTime = (inRef, timeInSeconds) => { + if (!this.index.has(inRef)) { + this.index.set(inRef, timeInSeconds) + this.indexInterval.set(inRef, null) + } + } + + startTimer = (inRef) => { + const interval = setInterval(() => { + const currentTime = new Date().getTime() + if (currentTime > this.endTime) { + this.stop(inRef) + this.emit(`timeout_${inRef}`) + } + }, 1000) + + this.indexInterval.set(inRef, interval) + } + + start = (inRef) => { + const refTimer = this.index.get(inRef) ?? undefined + if (refTimer) { + this.startTimer(inRef) + } + } + + stop = (inRef) => { + try { + this.index.delete(inRef) + clearInterval(this.indexInterval.get(inRef)) + this.indexInterval.delete(inRef) + } catch (err) { + return null + } + } +} + +module.exports = IdleState diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index 7c0197f42..07b65bbe9 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -10,6 +10,7 @@ const { LIST_REGEX } = require('../io/events') const SingleState = require('../context/state.class') const GlobalState = require('../context/globalState.class') const { generateTime } = require('../utils/hash') +const IdleState = require('../context/idleState.class') const logger = new Console({ stdout: createWriteStream(`${process.cwd()}/core.class.log`), @@ -18,6 +19,8 @@ const loggerQueue = new Console({ stdout: createWriteStream(`${process.cwd()}/queue.class.log`), }) +const idleForCallback = new IdleState() + /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos * [ ] Guardar historial en db @@ -302,13 +305,19 @@ class CoreClass extends EventEmitter { return } - // 📄 Se encarga de revisar si el contexto del mensaje tiene callback o fallback + // 📄 Se encarga de revisar si el contexto del mensaje tiene callback o idle const resolveCbEveryCtx = async (ctxMessage) => { + if (!!ctxMessage?.options?.idle && !ctxMessage?.options?.capture) { + printer( + `[ATENCION IDLE]: La función "idle" no tendrá efecto a menos que habilites la opción "capture:true". Por favor, asegúrate de configurar "capture:true" o elimina la función "idle"` + ) + } + if (ctxMessage?.options?.idle) return await cbEveryCtx(ctxMessage?.ref, ctxMessage?.options?.idle) if (!ctxMessage?.options?.capture) return await cbEveryCtx(ctxMessage?.ref) } // 📄 Se encarga de revisar si el contexto del mensaje tiene callback y ejecutarlo - const cbEveryCtx = async (inRef) => { + const cbEveryCtx = async (inRef, startIdleMs = 0) => { let flags = { endFlow: false, fallBack: false, @@ -320,24 +329,39 @@ class CoreClass extends EventEmitter { const database = this.databaseClass if (!this.flowClass.allCallbacks[inRef]) return Promise.resolve() - const argsCb = { database, provider, state, globalState, extensions, + idle: idleForCallback, + inRef, fallBack: fallBack(flags), flowDynamic: flowDynamic(flags), endFlow: endFlow(flags), gotoFlow: gotoFlow(flags), } - await this.flowClass.allCallbacks[inRef](messageCtxInComming, argsCb) - //Si no hay llamado de fallaback y no hay llamado de flowDynamic y no hay llamado de enflow EL flujo continua - const ifContinue = !flags.endFlow && !flags.fallBack && !flags.flowDynamic - if (ifContinue) await continueFlow(prevMsg?.options?.nested?.length) + const runContext = async (continueAfterIdle = true, overCtx = {}) => { + messageCtxInComming = { ...messageCtxInComming, ...overCtx } + await this.flowClass.allCallbacks[inRef](messageCtxInComming, argsCb) + idleForCallback.stop(inRef) + //Si no hay llamado de fallaback y no hay llamado de flowDynamic y no hay llamado de enflow EL flujo continua + const ifContinue = !flags.endFlow && !flags.fallBack && !flags.flowDynamic + if (ifContinue && continueAfterIdle) await continueFlow(prevMsg?.options?.nested?.length) + } + + if (startIdleMs > 0) { + idleForCallback.setIdleTime(inRef, startIdleMs / 1000) + idleForCallback.start(inRef) + idleForCallback.on(`timeout_${inRef}`, async () => { + await runContext(false, { idleFallBack: !!startIdleMs, from: null, body: null }) + }) + return + } + await runContext() return } diff --git a/packages/bot/io/methods/addAnswer.js b/packages/bot/io/methods/addAnswer.js index 82451e2b3..706aa201c 100644 --- a/packages/bot/io/methods/addAnswer.js +++ b/packages/bot/io/methods/addAnswer.js @@ -22,6 +22,7 @@ const addAnswer = capture: typeof options?.capture === 'boolean' ? options?.capture : false, child: typeof options?.child === 'string' ? `${options?.child}` : null, delay: typeof options?.delay === 'number' ? options?.delay : 0, + idle: typeof options?.idle === 'number' ? options?.idle : null, }) const getNested = () => { diff --git a/packages/bot/package.json b/packages/bot/package.json index f9f29fabc..017c1489a 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/bot", - "version": "0.0.173-alpha.0", + "version": "0.0.174-alpha.0", "description": "", "main": "./lib/bundle.bot.cjs", "scripts": { diff --git a/packages/bot/tests/bot.class.test.js b/packages/bot/tests/bot.class.test.js index c89c43b6f..96d2c0e65 100644 --- a/packages/bot/tests/bot.class.test.js +++ b/packages/bot/tests/bot.class.test.js @@ -2,7 +2,7 @@ const { test } = require('uvu') const assert = require('uvu/assert') const FlowClass = require('../io/flow.class') const MockProvider = require('../../../__mocks__/mock.provider') -const { createBot, CoreClass, createFlow, createProvider, ProviderClass } = require('../index') +const { createBot, CoreClass, createFlow, createProvider, ProviderClass, addKeyword } = require('../index') class MockFlow { allCallbacks = { ref: () => 1 } @@ -291,6 +291,88 @@ test(`[Bot] Probando endFlow `, async () => { assert.is(Object.values(result).length, 0) }) +test(`[Bot] Probando sendFlow `, async () => { + const mockProvider = new MockProvider() + + const setting = { + flow: new MockFlow(), + database: new MockDBB(), + provider: mockProvider, + } + + const bot = await createBot(setting) + + const messageCtxInComming = { + body: 'Hola', + from: '123456789', + } + + const botHandler = await bot.handleMsg(messageCtxInComming) + const messages = [ + { + body: 'Hola', + from: '123456789', + }, + ] + const resultA = await botHandler.sendFlow(messages, '00000', {}) + const resultB = await botHandler.sendFlow(messages, '00000', { + prev: { + options: { + capture: true, + }, + }, + }) + const resultC = await botHandler.sendFlow(messages, '00000', { forceQueue: true }) + assert.is(undefined, resultA) + assert.is(undefined, resultB) + assert.is(undefined, resultC) +}) + +test(`[Bot] Probando fallBack `, async () => { + const mockProvider = new MockProvider() + + const setting = { + flow: new MockFlow(), + database: new MockDBB(), + provider: mockProvider, + } + + const bot = await createBot(setting) + + const messageCtxInComming = { + body: 'Hola', + from: '123456789', + } + + const botHandler = await bot.handleMsg(messageCtxInComming) + const result = botHandler.fallBack({ fallBack: true })('hola') + + assert.is(Object.values(result).length, 0) +}) + +test(`[Bot] Probando gotoFlow `, async () => { + const mockProvider = new MockProvider() + const flowWelcome = addKeyword('hola').addAnswer('chao') + const flow = createFlow([flowWelcome]) + const setting = { + flow, + database: new MockDBB(), + provider: mockProvider, + } + + const bot = await createBot(setting) + + const messageCtxInComming = { + body: 'Hola', + from: '123456789', + } + + const botHandler = await bot.handleMsg(messageCtxInComming) + const result = botHandler.gotoFlow({ gotoFlow: true })(flowWelcome) + + assert.is(Object.values(result).length, 0) +}) + test.run() function delay(ms) { diff --git a/packages/bot/utils/queue.js b/packages/bot/utils/queue.js index a7a840656..4532657fc 100644 --- a/packages/bot/utils/queue.js +++ b/packages/bot/utils/queue.js @@ -54,6 +54,7 @@ class Queue { const cancel = () => { clearTimeout(this.timers.get(fingerIdRef)) + this.timers.delete(fingerIdRef) } return { promiseInFunc, timer, timerPromise, cancel } }