Merge branch 'codigoencasa:dev' into dev

This commit is contained in:
2023-01-23 21:58:16 -06:00
committed by GitHub
14 changed files with 376 additions and 33 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/node_modules
/packages/repl
/packages/*/starters
/packages/*/node_modules
/packages/*/dist

41
__test__/01-case.test.js Normal file
View File

@@ -0,0 +1,41 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const {
addKeyword,
createBot,
createFlow,
createProvider,
} = require('../packages/bot')
test(`[Caso - 01] Flow Basico`, async () => {
const [VALUE_A, VALUE_B] = ['hola', 'buenas']
const flow = addKeyword(VALUE_A).addAnswer(VALUE_B)
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
createBot({
database,
flow: createFlow([flow]),
provider,
})
provider.delaySendMessage(100, 'message', {
from: '000',
body: VALUE_A,
})
await delay(100)
const prevMsg = database.getPrevByNumber('000')
assert.is(prevMsg.answer, VALUE_B)
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

99
__test__/02-case.test.js Normal file
View File

@@ -0,0 +1,99 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const {
addKeyword,
createBot,
createFlow,
createProvider,
} = require('../packages/bot/index')
/**
* Falsear peticion async
* @param {*} fakeData
* @returns
*/
const fakeHTTP = async (fakeData = []) => {
console.log('⚡ Server request!')
await delay(50)
console.log('⚡ Server return!')
const data = fakeData.map((u, i) => ({ body: `${i + 1} ${u}` }))
console.log(data)
return Promise.resolve(data)
}
test(`[Caso - 02] Flow (flowDynamic)`, async () => {
const MOCK_VALUES = [
'Bienvenido te envio muchas marcas (5510)',
'Seleccione marca del auto a cotizar, con el *número* correspondiente',
'Seleccione la sub marca del auto a cotizar, con el *número* correspondiente:',
'Los precios rondan:',
]
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
const flujoPrincipal = addKeyword(['hola'])
.addAnswer(MOCK_VALUES[0], null, async (ctx, { flowDynamic }) => {
console.log('execute...')
const data = await fakeHTTP(['Ford', 'GM', 'BMW'])
return flowDynamic(data)
})
.addAnswer(MOCK_VALUES[1], null, async (ctx, { flowDynamic }) => {
const data = await fakeHTTP(['Ranger', 'Explorer'])
return flowDynamic(data)
})
.addAnswer(MOCK_VALUES[2], null, async (ctx, { flowDynamic }) => {
const data = await fakeHTTP(['Usado', 'Nuevos'])
return flowDynamic(data)
})
.addAnswer(MOCK_VALUES[3], null, async (ctx, { flowDynamic }) => {
const data = await fakeHTTP(['1000', '2000', '3000'])
return flowDynamic(data)
})
createBot({
database,
flow: createFlow([flujoPrincipal]),
provider,
})
provider.delaySendMessage(0, 'message', {
from: '000',
body: 'hola',
})
await delay(1200)
const getHistory = database.listHistory.map((i) => i.answer)
assert.is(MOCK_VALUES[0], getHistory[0])
//FlowDynamic
assert.is('1 Ford', getHistory[1])
assert.is('2 GM', getHistory[2])
assert.is('3 BMW', getHistory[3])
assert.is(MOCK_VALUES[1], getHistory[4])
//FlowDynamic
assert.is('1 Ranger', getHistory[5])
assert.is('2 Explorer', getHistory[6])
assert.is(MOCK_VALUES[2], getHistory[7])
//FlowDynamic
assert.is('1 Usado', getHistory[8])
assert.is('2 Nuevos', getHistory[9])
assert.is(MOCK_VALUES[3], getHistory[10])
//FlowDynamic
assert.is('1 1000', getHistory[11])
assert.is('2 2000', getHistory[12])
assert.is('3 3000', getHistory[13])
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

44
__test__/03-case.test.js Normal file
View File

@@ -0,0 +1,44 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const {
addKeyword,
createBot,
createFlow,
createProvider,
} = require('../packages/bot/index')
test(`[Caso - 03] Flow puro`, async () => {
const MOCK_VALUES = ['Bienvenido a mi tienda', 'Como estas?']
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
const flujoPrincipal = addKeyword(['hola'])
.addAnswer(MOCK_VALUES[0])
.addAnswer(MOCK_VALUES[1])
createBot({
database,
flow: createFlow([flujoPrincipal]),
provider,
})
provider.delaySendMessage(0, 'message', {
from: '000',
body: 'hola',
})
await delay(10)
const getHistory = database.listHistory.map((i) => i.answer)
assert.is(MOCK_VALUES[0], getHistory[0])
assert.is(MOCK_VALUES[1], getHistory[1])
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

82
__test__/04-case.test.js Normal file
View File

@@ -0,0 +1,82 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MOCK_DB = require('../packages/database/src/mock')
const PROVIDER_DB = require('../packages/provider/src/mock')
const {
addKeyword,
createBot,
createFlow,
createProvider,
} = require('../packages/bot/index')
/**
* Falsear peticion async
* @param {*} fakeData
* @returns
*/
const fakeHTTP = async (fakeData = []) => {
console.log('⚡ Server request!')
await delay(50)
console.log('⚡ Server return!')
const data = fakeData.map((u, i) => ({ body: `${i + 1} ${u}` }))
console.log(data)
return Promise.resolve(data)
}
test(`[Caso - 04] Romper flujo (endFlow)`, async () => {
const MOCK_VALUES = [
'Bienvenido te envio muchas marcas (5510)',
'Seleccione marca del auto a cotizar, con el *número* correspondiente',
'Seleccione la sub marca del auto a cotizar, con el *número* correspondiente:',
'Los precios rondan:',
]
const provider = createProvider(PROVIDER_DB)
const database = new MOCK_DB()
const flujoPrincipal = addKeyword(['hola'])
.addAnswer(MOCK_VALUES[0], null, async (ctx, { flowDynamic }) => {
console.log('execute...')
const data = await fakeHTTP(['Ford', 'GM', 'BMW'])
return flowDynamic(data)
})
.addAnswer(MOCK_VALUES[1], null, async (ctx, { endFlow }) => {
return endFlow()
})
.addAnswer(MOCK_VALUES[2], null, async (ctx, { flowDynamic }) => {
const data = await fakeHTTP(['Usado', 'Nuevos'])
return flowDynamic(data)
})
.addAnswer(MOCK_VALUES[3], null, async (ctx, { flowDynamic }) => {
const data = await fakeHTTP(['1000', '2000', '3000'])
return flowDynamic(data)
})
createBot({
database,
flow: createFlow([flujoPrincipal]),
provider,
})
provider.delaySendMessage(0, 'message', {
from: '000',
body: 'hola',
})
await delay(1200)
const getHistory = database.listHistory.map((i) => i.answer)
assert.is(MOCK_VALUES[0], getHistory[0])
//FlowDynamic
assert.is('1 Ford', getHistory[1])
assert.is('2 GM', getHistory[2])
assert.is('3 BMW', getHistory[3])
assert.is(MOCK_VALUES[1], getHistory[4])
assert.is(undefined, getHistory[5])
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

View File

@@ -24,7 +24,8 @@
"build": "yarn run cli:rollup && yarn run bot:rollup && yarn run provider:rollup && yarn run database:rollup && yarn run contexts:rollup && yarn run create-bot-whatsapp:rollup && yarn run portal:rollup",
"copy.lib": "node ./scripts/move.js",
"test.unit": "node ./node_modules/uvu/bin.js packages test",
"test.coverage": "node ./node_modules/c8/bin/c8.js npm run test.unit",
"test.e2e": "node ./node_modules/uvu/bin.js __test__",
"test.coverage": "node ./node_modules/c8/bin/c8.js npm run test.unit && npm run test.e2e",
"test": "npm run test.coverage",
"cli": "node ./packages/cli/bin/cli.js",
"create": "node ./packages/create-bot-whatsapp/bin/create.js",

View File

@@ -21,10 +21,12 @@ class CoreClass {
flowClass
databaseClass
providerClass
constructor(_flow, _database, _provider) {
generalArgs = { blackList: [] }
constructor(_flow, _database, _provider, _args) {
this.flowClass = _flow
this.databaseClass = _database
this.providerClass = _provider
this.generalArgs = { ...this.generalArgs, ..._args }
for (const { event, func } of this.listenerBusEvents()) {
this.providerClass.on(event, func)
@@ -70,10 +72,12 @@ class CoreClass {
const { body, from } = messageCtxInComming
let msgToSend = []
let fallBackFlag = false
let endFlowFlag = false
if (this.generalArgs.blackList.includes(from)) return
if (!body) return
if (!body.length) return
const prevMsg = await this.databaseClass.getPrevByNumber(from)
let prevMsg = await this.databaseClass.getPrevByNumber(from)
const refToContinue = this.flowClass.findBySerialize(
prevMsg?.refSerialize
)
@@ -87,16 +91,35 @@ class CoreClass {
this.databaseClass.save(ctxByNumber)
}
// 📄 Limpiar cola de procesos
const clearQueue = () => {
QueuePrincipal.pendingPromise = false
QueuePrincipal.queue = []
}
// 📄 Finalizar flujo
const endFlow = async () => {
prevMsg = null
endFlowFlag = true
clearQueue()
return
}
// 📄 Esta funcion se encarga de enviar un array de mensajes dentro de este ctx
const sendFlow = async (messageToSend, numberOrId) => {
// [1 Paso] esto esta bien!
if (prevMsg?.options?.capture) await cbEveryCtx(prevMsg?.ref)
const queue = []
for (const ctxMessage of messageToSend) {
if (endFlowFlag) return
const delayMs = ctxMessage?.options?.delay || 0
if (delayMs) await delay(delayMs)
QueuePrincipal.enqueue(() =>
Promise.all([
this.sendProviderAndSave(numberOrId, ctxMessage),
resolveCbEveryCtx(ctxMessage),
this.sendProviderAndSave(numberOrId, ctxMessage).then(
() => resolveCbEveryCtx(ctxMessage)
),
])
)
}
@@ -113,6 +136,7 @@ class CoreClass {
// 📄 [options: flowDynamic]: esta funcion se encarga de responder un array de respuesta esta limitado a 5 mensajes
// para evitar bloque de whatsapp
const flowDynamic = async (
listMsg = [],
optListMsg = { limit: 5, fallback: false }
@@ -122,15 +146,22 @@ class CoreClass {
fallBackFlag = optListMsg.fallback
const parseListMsg = listMsg
.map(({ body }, index) =>
toCtx({
.map((opt, index) => {
const body = typeof opt === 'string' ? opt : opt.body
const media = opt?.media ?? null
const buttons = opt?.buttons ?? []
return toCtx({
body,
from,
keyword: null,
index,
options: { media, buttons },
})
)
})
.slice(0, optListMsg.limit)
if (endFlowFlag) return
for (const msg of parseListMsg) {
await this.sendProviderAndSave(from, msg)
}
@@ -139,7 +170,6 @@ class CoreClass {
// 📄 Se encarga de revisar si el contexto del mensaje tiene callback o fallback
const resolveCbEveryCtx = async (ctxMessage) => {
if (prevMsg?.options?.capture) return cbEveryCtx(prevMsg?.ref)
if (!ctxMessage?.options?.capture)
return await cbEveryCtx(ctxMessage?.ref)
}
@@ -150,20 +180,10 @@ class CoreClass {
return this.flowClass.allCallbacks[inRef](messageCtxInComming, {
fallBack,
flowDynamic,
endFlow,
})
}
if (prevMsg?.ref) resolveCbEveryCtx(prevMsg)
// 📄 [options: callback]: Si se tiene un callback se ejecuta
//TODO AQUI
// if (!fallBackFlag) {
// if (prevMsg?.options?.capture) cbEveryCtx(prevMsg?.ref)
// for (const ite of this.flowClass.find(body)) {
// if (!ite?.options?.capture) cbEveryCtx(ite?.ref)
// }
// }
// 📄🤘(tiene return) [options: nested(array)]: Si se tiene flujos hijos los implementa
if (!fallBackFlag && prevMsg?.options?.nested?.length) {
const nestedRef = prevMsg.options.nested
@@ -173,11 +193,6 @@ class CoreClass {
msgToSend = this.flowClass.find(body, false, flowStandalone) || []
// //TODO AQUI
// for (const ite of msgToSend) {
// cbEveryCtx(ite?.ref)
// }
sendFlow(msgToSend, from)
return
}
@@ -225,5 +240,24 @@ class CoreClass {
this.continue(null, responde.ref)
}
}
/**
* Funcion dedicada a enviar el mensaje sin pasar por el flow
* (dialogflow)
* @param {*} messageToSend
* @param {*} numberOrId
* @returns
*/
sendFlowSimple = async (messageToSend, numberOrId) => {
const queue = []
for (const ctxMessage of messageToSend) {
const delayMs = ctxMessage?.options?.delay || 0
if (delayMs) await delay(delayMs)
QueuePrincipal.enqueue(() =>
this.sendProviderAndSave(numberOrId, ctxMessage)
)
}
return Promise.all(queue)
}
}
module.exports = CoreClass

View File

@@ -8,8 +8,8 @@ const { addKeyword, addAnswer, addChild, toSerialize } = require('./io/methods')
* @param {*} args
* @returns
*/
const createBot = async ({ flow, database, provider }) =>
new CoreClass(flow, database, provider)
const createBot = async ({ flow, database, provider }, args = {}) =>
new CoreClass(flow, database, provider, args)
/**
* Crear instancia de clase Io (Flow)

View File

@@ -5,12 +5,12 @@ const { generateRef, generateRefSerialize } = require('../../utils/hash')
* @param options {media:string, buttons:[], capture:true default false}
* @returns
*/
const toCtx = ({ body, from, prevRef, index }) => {
const toCtx = ({ body, from, prevRef, options = {}, index }) => {
return {
ref: generateRef(),
keyword: prevRef,
answer: body,
options: {},
options: options ?? {},
from,
refSerialize: generateRefSerialize({ index, answer: body }),
}

View File

@@ -1,6 +1,6 @@
{
"name": "@bot-whatsapp/bot",
"version": "0.0.66-alpha.0",
"version": "0.0.73-alpha.0",
"description": "",
"main": "./lib/bundle.bot.cjs",
"scripts": {

View File

@@ -0,0 +1,28 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const FlowClass = require('../io/flow.class')
const { addKeyword } = require('../index')
test(`[FlowClass] Probando instanciamiento de clase`, async () => {
const MOCK_FLOW = addKeyword('hola').addAnswer('Buenas!')
const flowClass = new FlowClass([MOCK_FLOW])
assert.is(flowClass instanceof FlowClass, true)
})
test(`[FlowClass] Probando find`, async () => {
const MOCK_FLOW = addKeyword('hola').addAnswer('Buenas!')
const flowClass = new FlowClass([MOCK_FLOW])
flowClass.find('hola')
assert.is(flowClass instanceof FlowClass, true)
})
test(`[FlowClass] Probando findBySerialize`, async () => {
const MOCK_FLOW = addKeyword('hola').addAnswer('Buenas!')
const flowClass = new FlowClass([MOCK_FLOW])
flowClass.findBySerialize('')
assert.is(flowClass instanceof FlowClass, true)
})
test.run()

View File

@@ -117,7 +117,7 @@ class DialogFlowCXContext extends CoreClass {
}
})
this.sendFlow(listMessages, from)
this.sendFlowSimple(listMessages, from)
}
}

View File

@@ -107,7 +107,7 @@ class DialogFlowContext extends CoreClass {
...customPayload,
answer: fields?.answer?.stringValue,
}
this.sendFlow([ctxFromDX], from)
this.sendFlowSimple([ctxFromDX], from)
return
}
@@ -115,7 +115,7 @@ class DialogFlowContext extends CoreClass {
answer: queryResult?.fulfillmentText,
}
this.sendFlow([ctxFromDX], from)
this.sendFlowSimple([ctxFromDX], from)
}
}

View File

@@ -159,6 +159,19 @@ const flowString = addKeyword('hola')
---
# QRPortalWeb
Argumento para asignar nombre y puerto al BOT
```js
QRPortalWeb({name:BOTNAME, port:3005 });
```
---
<Navigation
pages={[
{ name: 'Conceptos', link: '/docs/essential' },