diff --git a/.gitignore b/.gitignore index 1cc3038..9f6dbbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules +/packages/repl /packages/*/starters /packages/*/node_modules /packages/*/dist diff --git a/__test__/01-case.test.js b/__test__/01-case.test.js new file mode 100644 index 0000000..bce4a42 --- /dev/null +++ b/__test__/01-case.test.js @@ -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)) +} diff --git a/__test__/02-case.test.js b/__test__/02-case.test.js new file mode 100644 index 0000000..a2a93ec --- /dev/null +++ b/__test__/02-case.test.js @@ -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)) +} diff --git a/__test__/03-case.test.js b/__test__/03-case.test.js new file mode 100644 index 0000000..45c399a --- /dev/null +++ b/__test__/03-case.test.js @@ -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)) +} diff --git a/__test__/04-case.test.js b/__test__/04-case.test.js new file mode 100644 index 0000000..2291a0c --- /dev/null +++ b/__test__/04-case.test.js @@ -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)) +} diff --git a/package.json b/package.json index 354bc44..2d1061e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index e3efc04..f7051b3 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -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 diff --git a/packages/bot/index.js b/packages/bot/index.js index eb9df24..ac96063 100644 --- a/packages/bot/index.js +++ b/packages/bot/index.js @@ -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) diff --git a/packages/bot/io/methods/toCtx.js b/packages/bot/io/methods/toCtx.js index d29295e..980cbfb 100644 --- a/packages/bot/io/methods/toCtx.js +++ b/packages/bot/io/methods/toCtx.js @@ -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 }), } diff --git a/packages/bot/package.json b/packages/bot/package.json index d3f004e..fe542c1 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -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": { diff --git a/packages/bot/tests/flow.class.test.js b/packages/bot/tests/flow.class.test.js new file mode 100644 index 0000000..e1c93d7 --- /dev/null +++ b/packages/bot/tests/flow.class.test.js @@ -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() diff --git a/packages/contexts/src/dialogflow-cx/dialogflow-cx.class.js b/packages/contexts/src/dialogflow-cx/dialogflow-cx.class.js index 25bd2a6..2d0ebd2 100644 --- a/packages/contexts/src/dialogflow-cx/dialogflow-cx.class.js +++ b/packages/contexts/src/dialogflow-cx/dialogflow-cx.class.js @@ -117,7 +117,7 @@ class DialogFlowCXContext extends CoreClass { } }) - this.sendFlow(listMessages, from) + this.sendFlowSimple(listMessages, from) } } diff --git a/packages/contexts/src/dialogflow/dialogflow.class.js b/packages/contexts/src/dialogflow/dialogflow.class.js index ebd92bc..6a7cda0 100644 --- a/packages/contexts/src/dialogflow/dialogflow.class.js +++ b/packages/contexts/src/dialogflow/dialogflow.class.js @@ -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) } } diff --git a/packages/docs/src/routes/docs/flows/index.mdx b/packages/docs/src/routes/docs/flows/index.mdx index 2e078d7..1a5e879 100644 --- a/packages/docs/src/routes/docs/flows/index.mdx +++ b/packages/docs/src/routes/docs/flows/index.mdx @@ -159,6 +159,19 @@ const flowString = addKeyword('hola') --- +# QRPortalWeb + +Argumento para asignar nombre y puerto al BOT + +```js +QRPortalWeb({name:BOTNAME, port:3005 }); + +``` + +--- + + +