From 589f96efc26d03d622c85619656757f3dfb8c39e Mon Sep 17 00:00:00 2001 From: "Binho.dev" Date: Wed, 13 Sep 2023 11:58:49 -0300 Subject: [PATCH 1/5] feat(bot): :sparkles: feat IdleState - Set a idle time to wait a user answer, after this send a auto message of inactivity No have breaking changes --- packages/bot/context/idleState.class.js | 54 +++++++++++++++++++++++++ packages/bot/core/core.class.js | 3 ++ 2 files changed, 57 insertions(+) create mode 100755 packages/bot/context/idleState.class.js diff --git a/packages/bot/context/idleState.class.js b/packages/bot/context/idleState.class.js new file mode 100755 index 000000000..aa51c9fc4 --- /dev/null +++ b/packages/bot/context/idleState.class.js @@ -0,0 +1,54 @@ +const { EventEmitter } = require('node:events') + +class IdleState extends EventEmitter { + timer = null + startTime = 0 + endTime = 0 + + setIdleTime = (timeInSeconds) => { + this.startTime = timeInSeconds + return this.reset() + } + + startTimer = () => { + this.timer = setInterval(() => { + const currentTime = new Date().getTime() + + if (currentTime > this.endTime) { + this.stop() + this.emit('idle') + } else if (this.debug) { + return this.emit('debug', () => console.info(this.debugTime())) + } else { + return + } + }, 1000) + } + + start = () => { + this.stop() + if (!this.timer) { + this.reset() + this.startTimer() + } + } + + reset = () => { + const currentTime = new Date().getTime() + this.endTime = currentTime + this.startTime * 1000 + } + + stop = () => { + if (this.timer) { + clearInterval(this.timer) + this.timer = null + } + } + + debugTime = () => { + const currentTime = new Date().getTime() + return `Tiempo restante: ${((this.endTime - currentTime) / 1000).toFixed(0)} segundos` + } +} + +module.exports = IdleState diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index 7321c8978..cef6fe74f 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -9,6 +9,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`), @@ -19,6 +20,7 @@ const loggerQueue = new Console({ const StateHandler = new SingleState() const GlobalStateHandler = new GlobalState() +const idle = new IdleState() /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos @@ -325,6 +327,7 @@ class CoreClass { state, globalState, extensions, + idleState: idle, fallBack: fallBack(flags), flowDynamic: flowDynamic(flags), endFlow: endFlow(flags), From 894eddefe161e7c87f049faa40e91ec1bc649a3f Mon Sep 17 00:00:00 2001 From: "Binho.dev" Date: Wed, 13 Sep 2023 16:01:00 -0300 Subject: [PATCH 2/5] fix(bot): :fire: fix Changed a little code and removed unnecessary codes No breaking changes --- packages/bot/context/idleState.class.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/bot/context/idleState.class.js b/packages/bot/context/idleState.class.js index aa51c9fc4..5678e2560 100755 --- a/packages/bot/context/idleState.class.js +++ b/packages/bot/context/idleState.class.js @@ -17,16 +17,13 @@ class IdleState extends EventEmitter { if (currentTime > this.endTime) { this.stop() this.emit('idle') - } else if (this.debug) { - return this.emit('debug', () => console.info(this.debugTime())) } else { - return + this.debugTime() } }, 1000) } start = () => { - this.stop() if (!this.timer) { this.reset() this.startTimer() From e13f9127c5c016192f98ef6d8aa923ea08755e23 Mon Sep 17 00:00:00 2001 From: Leifer Mendez Date: Sat, 30 Sep 2023 10:52:51 +0200 Subject: [PATCH 3/5] fix: remove map --- packages/bot/package.json | 2 +- packages/bot/utils/queue.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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/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 } } From e2a6cbd7091544907a4c6e5911ea351c4d1d8a42 Mon Sep 17 00:00:00 2001 From: Leifer Mendez Date: Sat, 30 Sep 2023 13:00:59 +0200 Subject: [PATCH 4/5] feat(bot): :zap: idle feature --- __test__/0.2.1-case.test.js | 140 ++++++++++++++++++++++++ packages/bot/context/idleState.class.js | 52 ++++----- packages/bot/core/core.class.js | 38 +++++-- packages/bot/io/methods/addAnswer.js | 1 + 4 files changed, 194 insertions(+), 37 deletions(-) create mode 100644 __test__/0.2.1-case.test.js 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 index 5678e2560..d333fab74 100755 --- a/packages/bot/context/idleState.class.js +++ b/packages/bot/context/idleState.class.js @@ -1,50 +1,46 @@ const { EventEmitter } = require('node:events') class IdleState extends EventEmitter { + index = new Map() + indexInterval = new Map() timer = null startTime = 0 endTime = 0 - setIdleTime = (timeInSeconds) => { - this.startTime = timeInSeconds - return this.reset() + setIdleTime = (inRef, timeInSeconds) => { + if (!this.index.has(inRef)) { + this.index.set(inRef, timeInSeconds) + this.indexInterval.set(inRef, null) + } } - startTimer = () => { - this.timer = setInterval(() => { + startTimer = (inRef) => { + const interval = setInterval(() => { const currentTime = new Date().getTime() - if (currentTime > this.endTime) { - this.stop() - this.emit('idle') - } else { - this.debugTime() + this.stop(inRef) + this.emit(`timeout_${inRef}`) } }, 1000) - } - start = () => { - if (!this.timer) { - this.reset() - this.startTimer() - } + this.indexInterval.set(inRef, interval) } - reset = () => { - const currentTime = new Date().getTime() - this.endTime = currentTime + this.startTime * 1000 - } - - stop = () => { - if (this.timer) { - clearInterval(this.timer) - this.timer = null + start = (inRef) => { + const refTimer = this.index.get(inRef) ?? undefined + if (refTimer) { + this.startTimer(inRef) } } - debugTime = () => { - const currentTime = new Date().getTime() - return `Tiempo restante: ${((this.endTime - currentTime) / 1000).toFixed(0)} segundos` + stop = (inRef) => { + try { + this.index.delete(inRef) + clearInterval(this.indexInterval.get(inRef)) + this.indexInterval.delete(inRef) + } catch (err) { + return null + } } } diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index 91253af23..07b65bbe9 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -19,7 +19,7 @@ const loggerQueue = new Console({ stdout: createWriteStream(`${process.cwd()}/queue.class.log`), }) -const idle = new IdleState() +const idleForCallback = new IdleState() /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos @@ -305,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, @@ -323,25 +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, - idleState: idle, + 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 = () => { From 71146de27f8b9d9ed2ebbc1b317d3fc4e17f7684 Mon Sep 17 00:00:00 2001 From: Leifer Mendez Date: Sat, 30 Sep 2023 13:14:52 +0200 Subject: [PATCH 5/5] test: :art: more test coverage --- packages/bot/tests/bot.class.test.js | 84 +++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) 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) {