Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #868

Merged
merged 8 commits into from
Sep 30, 2023
Merged

Dev #868

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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