Skip to content

Commit

Permalink
Merge pull request #868 from codigoencasa/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
leifermendez authored Sep 30, 2023
2 parents 74b0054 + 1291b63 commit 859d5ab
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 9 deletions.
140 changes: 140 additions & 0 deletions __test__/0.2.1-case.test.js
Original file line number Diff line number Diff line change
@@ -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()
47 changes: 47 additions & 0 deletions packages/bot/context/idleState.class.js
Original file line number Diff line number Diff line change
@@ -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
38 changes: 31 additions & 7 deletions packages/bot/core/core.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions packages/bot/io/methods/addAnswer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/bot/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
84 changes: 83 additions & 1 deletion packages/bot/tests/bot.class.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions packages/bot/utils/queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Queue {

const cancel = () => {
clearTimeout(this.timers.get(fingerIdRef))
this.timers.delete(fingerIdRef)
}
return { promiseInFunc, timer, timerPromise, cancel }
}
Expand Down

0 comments on commit 859d5ab

Please sign in to comment.