diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39adff09d..0a24cebea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,7 +93,7 @@ En la consola encontraras los pasos a seguir --> ![](https://i.imgur.com/dC6lEwy.png) -> __NOTA:__ [Eres libre de aportar informacion a este documento o arreglar ortografia 🤣]( +> __NOTA:__ [Eres libre de aportar informacion inexacta a este documento o arreglar horrores de ortografia que dificultan la comprensión. 🤣]( https://github.com/codigoencasa/bot-whatsapp/edit/dev/CONTRIBUTING.md) ------ diff --git a/__test__/0.1.7-case.test.js b/__test__/0.1.7-case.test.js index 1a29c9d85..de889a28d 100644 --- a/__test__/0.1.7-case.test.js +++ b/__test__/0.1.7-case.test.js @@ -18,8 +18,8 @@ suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => capture: true, }, async (ctx, { flowDynamic, state }) => { - state.update({ name: ctx.body }) - flowDynamic('Gracias por tu nombre!') + await state.update({ name: ctx.body }) + await flowDynamic('Gracias por tu nombre!') } ) .addAnswer( @@ -28,14 +28,14 @@ suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => capture: true, }, async (ctx, { flowDynamic, state }) => { - state.update({ age: ctx.body }) - const myState = state.getMyState() - await flowDynamic(`Gracias por tu edad! ${myState.name}`) + await state.update({ age: ctx.body }) + const name = state.get('name') + await flowDynamic(`Gracias por tu edad! ${name}`) } ) .addAnswer(MOCK_VALUES[2], null, async (_, { flowDynamic, state }) => { const myState = state.getMyState() - flowDynamic(`Nombre: ${myState.name} Edad: ${myState.age}`) + await flowDynamic(`Nombre: ${myState.name} Edad: ${myState.age}`) }) .addAnswer('🤖🤖 Gracias por tu participacion') @@ -98,4 +98,95 @@ suiteCase(`Debe retornar un mensaje resumen`, async ({ database, provider }) => assert.is(undefined, getHistory[18]) }) +suiteCase(`Manejando globalState`, async ({ database, provider }) => { + const MOCK_VALUES = ['¿Cual es tu nombre?', '¿Cual es tu edad?', 'Tu datos son:'] + + const flujoPrincipal = addKeyword(['hola']) + .addAnswer( + MOCK_VALUES[0], + { + capture: true, + }, + async (ctx, { flowDynamic, state, globalState }) => { + await state.update({ name: ctx.body }) + await globalState.update({ value: 'Soy el valor global' }) + await flowDynamic('Gracias por tu nombre!') + } + ) + .addAnswer( + MOCK_VALUES[1], + { + capture: true, + }, + async (ctx, { flowDynamic, state }) => { + await state.update({ age: ctx.body }) + const name = state.get('name') + await flowDynamic(`Gracias por tu edad! ${name}`) + } + ) + .addAnswer(MOCK_VALUES[2], null, async (_, { flowDynamic, state, globalState }) => { + const myState = state.getMyState() + const value = globalState.get('value') + await flowDynamic(`Nombre: ${myState.name} Edad: ${myState.age} Valor Global: ${value}`) + }) + .addAnswer('🤖🤖 Gracias por tu participacion') + + createBot({ + database, + flow: createFlow([flujoPrincipal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await provider.delaySendMessage(5, 'message', { + from: '001', + body: 'hola', + }) + + await provider.delaySendMessage(10, 'message', { + from: '000', + body: 'Leifer', + }) + + await provider.delaySendMessage(15, 'message', { + from: '000', + body: '90', + }) + + await provider.delaySendMessage(20, 'message', { + from: '001', + body: 'Maria', + }) + + await provider.delaySendMessage(25, 'message', { + from: '001', + body: '100', + }) + + await delay(1000) + const getHistory = database.listHistory.map((i) => i.answer) + assert.is(MOCK_VALUES[0], getHistory[0]) + assert.is('¿Cual es tu nombre?', getHistory[1]) + assert.is('Leifer', getHistory[2]) + assert.is('Gracias por tu nombre!', getHistory[3]) + assert.is('¿Cual es tu edad?', getHistory[4]) + assert.is('90', getHistory[5]) + assert.is('Gracias por tu edad! Leifer', getHistory[6]) + assert.is('Tu datos son:', getHistory[7]) + assert.is('Nombre: Leifer Edad: 90 Valor Global: Soy el valor global', getHistory[8]) + assert.is('🤖🤖 Gracias por tu participacion', getHistory[9]) + assert.is('Maria', getHistory[10]) + assert.is('Gracias por tu nombre!', getHistory[11]) + assert.is('¿Cual es tu edad?', getHistory[12]) + assert.is('100', getHistory[13]) + assert.is('Gracias por tu edad! Maria', getHistory[14]) + assert.is('Tu datos son:', getHistory[15]) + assert.is('Nombre: Maria Edad: 100 Valor Global: Soy el valor global', getHistory[16]) + assert.is('🤖🤖 Gracias por tu participacion', getHistory[17]) + assert.is(undefined, getHistory[18]) +}) suiteCase.run() diff --git a/__test__/0.1.8-case.test.js b/__test__/0.1.8-case.test.js index 91b9b107f..cd2451c10 100644 --- a/__test__/0.1.8-case.test.js +++ b/__test__/0.1.8-case.test.js @@ -37,10 +37,13 @@ suiteCase(`Debe ejecutar accion con captura`, async ({ database, provider }) => await delay(1000) const getHistory = database.listHistory.map((i) => i.answer) - assert.is('Buenas! ¿Cual es tu nombre?', getHistory[0]) - assert.is('Gracias por tu nombre!: Leifer', getHistory[3]) - assert.is('Chao!', getHistory[4]) - assert.is(undefined, getHistory[5]) + assert.is('__call_action__', getHistory[0]) + assert.is('Buenas! ¿Cual es tu nombre?', getHistory[1]) + assert.is('__capture_only_intended__', getHistory[2]) + assert.is('Leifer', getHistory[3]) + assert.is('Gracias por tu nombre!: Leifer', getHistory[4]) + assert.is('Chao!', getHistory[5]) + assert.is(undefined, getHistory[6]) }) suiteCase.run() diff --git a/__test__/0.1.9-case.test.js b/__test__/0.1.9-case.test.js new file mode 100644 index 000000000..2b364484d --- /dev/null +++ b/__test__/0.1.9-case.test.js @@ -0,0 +1,44 @@ +const { suite } = require('uvu') +const assert = require('uvu/assert') +const { addKeyword, createBot, createFlow } = require('../packages/bot/index') +const { setup, clear, delay } = require('../__mocks__/env') + +const suiteCase = suite('Flujo: flowDynamic (delay)') + +suiteCase.before.each(setup) +suiteCase.after.each(clear) + +suiteCase(`Delay en los flowDynamic`, async ({ database, provider }) => { + const flujoPrincipal = addKeyword(['hola']) + .addAction(async (_, { flowDynamic }) => { + await flowDynamic('Buenas! ¿Cual es tu nombre? este mensaje debe tener delay 1000', { delay: 1000 }) + await flowDynamic([{ body: 'Todo bien?', delay: 850 }]) + }) + .addAnswer('Bien!', null, async (_, { flowDynamic }) => { + await flowDynamic('si nada') + }) + .addAnswer('Chao!') + + await createBot({ + database, + flow: createFlow([flujoPrincipal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await delay(2000) + const getHistory = database.listHistory.map((i) => i.answer) + assert.is('__call_action__', getHistory[0]) + assert.is('Buenas! ¿Cual es tu nombre? este mensaje debe tener delay 1000', getHistory[1]) + assert.is('Todo bien?', getHistory[2]) + assert.is('Bien!', getHistory[3]) + assert.is('si nada', getHistory[4]) + assert.is('Chao!', getHistory[5]) + assert.is(undefined, getHistory[6]) +}) + +suiteCase.run() diff --git a/__test__/0.2.0-case.test.js b/__test__/0.2.0-case.test.js new file mode 100644 index 000000000..5957389ee --- /dev/null +++ b/__test__/0.2.0-case.test.js @@ -0,0 +1,61 @@ +const { suite } = require('uvu') +const assert = require('uvu/assert') +const { addKeyword, createBot, createFlow } = require('../packages/bot/index') +const { setup, clear, delay } = require('../__mocks__/env') + +const suiteCase = suite('Flujo: addAction (capture) encadenados') + +suiteCase.before.each(setup) +suiteCase.after.each(clear) + +suiteCase(`Encadenanos addAction con captures`, async ({ database, provider }) => { + const flujoPrincipal = addKeyword(['hola']) + .addAction(async (ctx, { flowDynamic }) => { + await flowDynamic(`Hola! primer flow dynamic. respondeme algo`) + }) + .addAction({ capture: true }, async (ctx, { flowDynamic, state }) => { + const reply = ctx.body + await state.update({ reply }) + await flowDynamic(`Esto me respondieste ${reply}`) + }) + .addAction(async (ctx, { flowDynamic }) => { + await flowDynamic(`Hola! segundo flow dynamic. respondeme algo`) + }) + .addAction({ capture: true }, async (ctx, { flowDynamic, state }) => { + const currentState = state.getMyState()?.reply + const reply = ctx.body + await state.update({ reply: currentState + ' ' + reply }) + await flowDynamic(`Esto me respondieste ${reply}`) + }) + .addAnswer('Chao') + + await createBot({ + database, + flow: createFlow([flujoPrincipal]), + provider, + }) + + await provider.delaySendMessage(0, 'message', { + from: '000', + body: 'hola', + }) + + await provider.delaySendMessage(10, 'message', { + from: '000', + body: 'ping', + }) + + await delay(2000) + const getHistory = database.listHistory.map((i) => i.answer) + assert.is('__call_action__', getHistory[0]) + assert.is('Hola! primer flow dynamic. respondeme algo', getHistory[1]) + assert.is('__capture_only_intended__', getHistory[2]) + assert.is('ping', getHistory[3]) + assert.is('Esto me respondieste ping', getHistory[4]) + assert.is('__call_action__', getHistory[5]) + assert.is('Hola! segundo flow dynamic. respondeme algo', getHistory[6]) + assert.is('__capture_only_intended__', getHistory[7]) + assert.is(undefined, getHistory[8]) +}) + +suiteCase.run() 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/package.json b/package.json index 79ab47f30..4b0e38799 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/root", - "version": "0.1.31", + "version": "0.1.32", "description": "Bot de wahtsapp open source para MVP o pequeños negocios", "main": "app.js", "private": true, @@ -24,7 +24,7 @@ "build": "pnpm run cli:rollup && pnpm run bot:rollup && pnpm run provider:rollup && pnpm run database:rollup && pnpm run contexts:rollup && pnpm run create-bot-whatsapp:rollup && pnpm run portal:rollup", "copy.lib": "node ./scripts/move.js", "test.unit": "node ./node_modules/uvu/bin.js packages test", - "test.e2e": "node ./node_modules/uvu/bin.js __test__ ", + "test.e2e": "node ./node_modules/uvu/bin.js __test__", "test.coverage": "node ./node_modules/c8/bin/c8.js npm run test.unit", "test": "npm run test.coverage", "cli": "node ./packages/cli/bin/cli.js", diff --git a/packages/bot/context/globalState.class.js b/packages/bot/context/globalState.class.js index 75bc4909c..df7dde293 100644 --- a/packages/bot/context/globalState.class.js +++ b/packages/bot/context/globalState.class.js @@ -21,6 +21,9 @@ class GlobalState { return () => this.STATE.get('__global__') } + get = () => { + return (prop) => this.STATE.get('__global__')[prop] + } /** * * @returns diff --git a/packages/bot/context/idleState.class.js b/packages/bot/context/idleState.class.js index 5678e2560..8b28e50cc 100755 --- a/packages/bot/context/idleState.class.js +++ b/packages/bot/context/idleState.class.js @@ -1,50 +1,42 @@ -const { EventEmitter } = require('node:events') +class IdleState { + index = new Map() + indexInterval = new Map() + indexEnd = new Map() -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 { - this.debugTime() - } - }, 1000) - } - - start = () => { - if (!this.timer) { - this.reset() - this.startTimer() - } - } - - reset = () => { + setIdleTime = (inRef, timeInSeconds) => { + this.stop(inRef) const currentTime = new Date().getTime() - this.endTime = currentTime + this.startTime * 1000 + const endTime = currentTime + timeInSeconds * 1000 + if (!this.index.has(inRef)) this.index.set(inRef, timeInSeconds) + if (!this.indexInterval.has(inRef)) this.indexInterval.set(inRef, null) + if (!this.indexEnd.has(inRef)) this.indexEnd.set(inRef, endTime) } - stop = () => { - if (this.timer) { - clearInterval(this.timer) - this.timer = null + start = (inRef, cb = () => null) => { + const refTimer = this.index.get(inRef) ?? undefined + if (refTimer) { + const interval = setInterval(() => { + const currentTime = new Date().getTime() + const endTime = this.indexEnd.get(inRef) + if (currentTime > endTime) { + this.stop(inRef) + cb() + } + }, 1000) + + this.indexInterval.set(inRef, interval) } } - debugTime = () => { - const currentTime = new Date().getTime() - return `Tiempo restante: ${((this.endTime - currentTime) / 1000).toFixed(0)} segundos` + stop = (inRef) => { + try { + clearInterval(this.indexInterval.get(inRef)) + this.index.delete(inRef) + this.indexInterval.delete(inRef) + this.indexEnd.delete(inRef) + } catch (err) { + return null + } } } diff --git a/packages/bot/context/state.class.js b/packages/bot/context/state.class.js index 23bbcd4b0..93490ffcb 100644 --- a/packages/bot/context/state.class.js +++ b/packages/bot/context/state.class.js @@ -8,8 +8,14 @@ class SingleState { * @returns */ updateState = (ctx = {}) => { - const currentStateByFrom = this.STATE.get(ctx.from) - return (keyValue) => this.STATE.set(ctx.from, { ...currentStateByFrom, ...keyValue }) + return (keyValue) => { + return new Promise((resolve) => { + const currentStateByFrom = this.STATE.get(ctx.from) + const updatedState = { ...currentStateByFrom, ...keyValue } + this.STATE.set(ctx.from, updatedState) + resolve() + }) + } } /** @@ -20,6 +26,15 @@ class SingleState { return () => this.STATE.get(from) } + /** + * + * @param {*} prop + * @returns + */ + get = (from) => { + return (prop) => this.STATE.get(from)[prop] + } + /** * * @returns diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index cef6fe74f..1d7a8d988 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -1,3 +1,4 @@ +const { EventEmitter } = require('node:events') const { toCtx } = require('../io/methods') const { printer } = require('../utils/interactive') const { delay } = require('../utils/delay') @@ -18,9 +19,7 @@ const loggerQueue = new Console({ stdout: createWriteStream(`${process.cwd()}/queue.class.log`), }) -const StateHandler = new SingleState() -const GlobalStateHandler = new GlobalState() -const idle = new IdleState() +const idleForCallback = new IdleState() /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos @@ -28,11 +27,13 @@ const idle = new IdleState() * [ ] Buscar mensaje en flow * */ -class CoreClass { +class CoreClass extends EventEmitter { flowClass databaseClass providerClass queuePrincipal + stateHandler = new SingleState() + globalStateHandler = new GlobalState() generalArgs = { blackList: [], listEvents: {}, @@ -45,6 +46,7 @@ class CoreClass { }, } constructor(_flow, _database, _provider, _args) { + super() this.flowClass = _flow this.databaseClass = _database this.providerClass = _provider @@ -56,9 +58,9 @@ class CoreClass { this.generalArgs.queue.timeout ) - GlobalStateHandler.updateState()(this.generalArgs.globalState) + this.globalStateHandler.updateState()(this.generalArgs.globalState) - if (this.generalArgs.extensions) GlobalStateHandler.RAW = this.generalArgs.extensions + if (this.generalArgs.extensions) this.globalStateHandler.RAW = this.generalArgs.extensions for (const { event, func } of this.listenerBusEvents()) { this.providerClass.on(event, func) @@ -124,21 +126,23 @@ class CoreClass { // 📄 Mantener estado de conversacion por numero const state = { - getMyState: StateHandler.getMyState(messageCtxInComming.from), - getAllState: StateHandler.getAllState, - update: StateHandler.updateState(messageCtxInComming), - clear: StateHandler.clear(messageCtxInComming.from), + getMyState: this.stateHandler.getMyState(messageCtxInComming.from), + get: this.stateHandler.get(messageCtxInComming.from), + getAllState: this.stateHandler.getAllState, + update: this.stateHandler.updateState(messageCtxInComming), + clear: this.stateHandler.clear(messageCtxInComming.from), } // 📄 Mantener estado global const globalState = { - getMyState: GlobalStateHandler.getMyState(), - getAllState: GlobalStateHandler.getAllState, - update: GlobalStateHandler.updateState(messageCtxInComming), - clear: GlobalStateHandler.clear(), + getMyState: this.globalStateHandler.getMyState(), + get: this.globalStateHandler.get(), + getAllState: this.globalStateHandler.getAllState, + update: this.globalStateHandler.updateState(messageCtxInComming), + clear: this.globalStateHandler.clear(), } - const extensions = GlobalStateHandler.RAW + const extensions = this.globalStateHandler.RAW // 📄 Crar CTX de mensaje (uso private) const createCtxMessage = (payload = {}, index = 0) => { @@ -220,7 +224,7 @@ class CoreClass { ctxMessage.ref ) } catch (error) { - logger.error(`Error al encolar: ${error.message}`) + logger.error(`Error al encolar (ID ${ctxMessage.ref}):`, error) return Promise.reject // Puedes considerar manejar el error aquí o rechazar la promesa // Pasada a resolveCbEveryCtx con el error correspondiente. @@ -285,9 +289,10 @@ class CoreClass { const flowDynamic = (flag) => async (listMsg = [], options = { continue: true }) => { - flag.flowDynamic = true - if (!Array.isArray(listMsg)) listMsg = [listMsg] + if (!options.hasOwnProperty('continue')) options = { ...options, continue: true } + flag.flowDynamic = true + if (!Array.isArray(listMsg)) listMsg = [{ body: listMsg, ...options }] const parseListMsg = listMsg.map((opt, index) => createCtxMessage(opt, index)) if (endFlowFlag) return @@ -302,13 +307,19 @@ class CoreClass { 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, @@ -320,25 +331,38 @@ class CoreClass { 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) + idleForCallback.stop(inRef) + const runContext = async (continueAfterIdle = true, overCtx = {}) => { + messageCtxInComming = { ...messageCtxInComming, ...overCtx } + 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 && continueAfterIdle) await continueFlow(prevMsg?.options?.nested?.length) + } + + if (startIdleMs > 0) { + idleForCallback.setIdleTime(inRef, startIdleMs / 1000) + idleForCallback.start(inRef, async () => { + await runContext(false, { idleFallBack: !!startIdleMs, from: null, body: null }) + }) + return + } + await runContext() return } @@ -420,13 +444,14 @@ class CoreClass { if (answer && answer.length && answer !== '__call_action__') { if (answer !== '__capture_only_intended__') { await this.providerClass.sendMessage(numberOrId, answer, ctxMessage) + this.emit('send_message', { numberOrId, answer, ctxMessage }) } - await this.databaseClass.save({ ...ctxMessage, from: numberOrId }) } + await this.databaseClass.save({ ...ctxMessage, from: numberOrId }) return Promise.resolve } catch (err) { - logger.log(`[ERROR.save]: `, ctxMessage) + logger.log(`[ERROR ID (${ctxMessage.ref})]: `, err) return Promise.reject } } @@ -460,7 +485,7 @@ class CoreClass { await this.queuePrincipal.enqueue( numberOrId, () => this.sendProviderAndSave(numberOrId, ctxMessage), - generateTime() + ctxMessage.ref ) // await queuePromises.dequeue() } diff --git a/packages/bot/io/flow.class.js b/packages/bot/io/flow.class.js index 1f94866f9..306d5b9cf 100644 --- a/packages/bot/io/flow.class.js +++ b/packages/bot/io/flow.class.js @@ -26,8 +26,7 @@ class FlowClass { overFlow = overFlow ?? this.flowSerialize const mapSensitive = (str, mapOptions = { sensitive: false, regex: false }) => { - - if (mapOptions.regex) return new Function(`return ${str}`)(); + if (mapOptions.regex) return new Function(`return ${str}`)() const regexSensitive = mapOptions.sensitive ? 'g' : 'i' if (Array.isArray(str)) { @@ -66,6 +65,8 @@ class FlowClass { findSerializeByRef = (ref) => this.flowSerialize.find((r) => r.ref === ref) + findSerializeByKeyword = (keyword) => this.flowSerialize.find((r) => r.keyword === keyword) + getRefToContinueChild = (keyword) => { try { const flowChilds = this.flowSerialize 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 = () => { diff --git a/packages/bot/package.json b/packages/bot/package.json index 7f59d959c..b5db8468c 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/bot", - "version": "0.0.167-alpha.0", + "version": "0.0.183-alpha.0", "description": "", "main": "./lib/bundle.bot.cjs", "scripts": { 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) { diff --git a/packages/bot/utils/queue.js b/packages/bot/utils/queue.js index ccabd2204..4532657fc 100644 --- a/packages/bot/utils/queue.js +++ b/packages/bot/utils/queue.js @@ -2,6 +2,7 @@ class Queue { constructor(logger, concurrencyLimit = 15, timeout = 20000) { this.queue = new Map() this.queueTime = new Map() + this.timers = new Map() this.idsCallbacks = new Map() this.workingOnPromise = new Map() this.logger = logger @@ -15,9 +16,13 @@ class Queue { * @param {*} promiseFunc * @returns */ - async enqueue(from, promiseFunc, fingerIdRef) { + async enqueue(from, promiseInFunc, fingerIdRef) { this.logger.log(`${from}:ENCOLADO ${fingerIdRef}`) + if (!this.timers.has(fingerIdRef)) { + this.timers.set(fingerIdRef, false) + } + if (!this.queue.has(from)) { this.queue.set(from, []) this.workingOnPromise.set(from, false) @@ -26,6 +31,34 @@ class Queue { const queueByFrom = this.queue.get(from) const workingByFrom = this.workingOnPromise.get(from) + const promiseFunc = (item) => { + const timer = ({ resolve }) => + setTimeout(() => { + console.log('no debe aparecer si la otra funcion del race se ejecuta primero 🙉🙉🙉🙉', fingerIdRef) + resolve('timeout') + }, this.timeout) + + const timerPromise = new Promise((resolve, reject) => { + if (item.cancelled) { + reject('cancelled') + } + if (!this.timers.has(fingerIdRef)) { + const refIdTimeOut = timer({ reject, resolve }) + clearTimeout(this.timers.get(fingerIdRef)) + this.timers.set(fingerIdRef, refIdTimeOut) + return refIdTimeOut + } + + return this.timers.get(fingerIdRef) + }) + + const cancel = () => { + clearTimeout(this.timers.get(fingerIdRef)) + this.timers.delete(fingerIdRef) + } + return { promiseInFunc, timer, timerPromise, cancel } + } + return new Promise((resolve, reject) => { queueByFrom.push({ promiseFunc, @@ -50,28 +83,16 @@ class Queue { async processQueue(from) { const queueByFrom = this.queue.get(from) - const timeOutFn = (item) => { - return new Promise((_, reject) => { - if (item.cancelled) { - reject('cancelled') - } - - // const fingerTimeByFrom = this.queueTime.get(from) - // if (fingerTimeByFrom > item.fingerTime) { - // console.log(`🚀🚀 ${fingerTimeByFrom}------${item.fingerTime}`) - // reject('overtime') - // } - - setTimeout(() => reject('timeout'), this.timeout) - }) - } - while (queueByFrom.length > 0) { const tasksToProcess = queueByFrom.splice(0, this.concurrencyLimit) const promises = tasksToProcess.map(async (item) => { try { - const value = await Promise.race([timeOutFn(item), item.promiseFunc()]) + const refToPromise = item.promiseFunc(item) + const value = await Promise.race([ + refToPromise.timerPromise, + refToPromise.promiseInFunc().then(() => refToPromise.cancel()), + ]) item.resolve(value) this.logger.log(`${from}:SUCCESS`) } catch (err) { diff --git a/packages/docs/public/videos/console.mp4 b/packages/docs/public/videos/console.mp4 new file mode 100644 index 000000000..f89dfba61 Binary files /dev/null and b/packages/docs/public/videos/console.mp4 differ diff --git a/packages/docs/public/videos/console.webm b/packages/docs/public/videos/console.webm new file mode 100644 index 000000000..4dc6acd40 Binary files /dev/null and b/packages/docs/public/videos/console.webm differ diff --git a/packages/docs/public/videos/xbmcc-kx99h.webm b/packages/docs/public/videos/xbmcc-kx99h.webm new file mode 100644 index 000000000..e73eb4496 Binary files /dev/null and b/packages/docs/public/videos/xbmcc-kx99h.webm differ diff --git a/packages/docs/src/components/widgets/CallToAction.tsx b/packages/docs/src/components/widgets/CallToAction.tsx index 1ca9b40a3..11a06b67f 100644 --- a/packages/docs/src/components/widgets/CallToAction.tsx +++ b/packages/docs/src/components/widgets/CallToAction.tsx @@ -21,12 +21,12 @@ export default component$(() => { playsInline > diff --git a/packages/docs/src/routes/docs/add-action/index.mdx b/packages/docs/src/routes/docs/add-action/index.mdx index bc5163693..212a5e261 100644 --- a/packages/docs/src/routes/docs/add-action/index.mdx +++ b/packages/docs/src/routes/docs/add-action/index.mdx @@ -2,10 +2,9 @@ import Navigation from '../../../components/widgets/Navigation' # addAction -Es una función útil para definir acciones que se activan cuando se recibe un mensaje de WhatsApp específico. -La función action puede realizar diversas tareas y acceder a los métodos y propiedades. En general, **addAction()** te permite crear Chat Bots de WhatsApp personalizados que pueden interactuar con los usuarios de una manera programática. -Puedes definir acciones específicas que se activan cuando se recibe un mensaje específico, lo que te permite crear una variedad de flujos de conversación y respuestas automatizadas. +El addAction es una función que se utiliza en la librería bot-whatsapp para definir acciones específicas en respuesta a los mensajes de WhatsApp recibidos. Esta función te permite crear flujos de conversación y definir cómo el bot debe responder a ciertos mensajes o palabras clave. +Aquí tienes un ejemplo de cómo se utiliza el addAction: ```js const { addKeyword } = require('@bot-whatsapp/bot') const flowPrincipal = addKeyword(['hola', 'alo']) @@ -27,10 +26,99 @@ const { addKeyword } = require('@bot-whatsapp/bot') return flowDynamic('Buenas! ¿Cual es tu nombre?') }) .addAction({ capture: true }, async (ctx, { flowDynamic, state }) => { - state.update({ name: ctx.body }) + await state.update({ name: ctx.body }) return flowDynamic(`Gracias por tu nombre!: ${ctx.body}`) }) .addAnswer('Chao!') +``` + +--- + +## Ejemplos + +En este ejemplo, hemos creado un flujo de conversación utilizando la palabra clave "hola" y "alo". + +Cuando el usuario envía un mensaje con alguna de estas palabras clave, el bot responde con un saludo. Luego, el bot captura el siguiente mensaje del usuario y lo muestra en la respuesta. + +Puedes agregar tantas acciones como desees en tu flujo de conversación, y cada acción puede tener su propia lógica y respuesta personalizada. + +Espero que esto te ayude a entender cómo utilizar el addAction en la creación de tu bot de WhatsApp. Si tienes alguna otra pregunta, no dudes en hacerla. + +```js +const { addKeyword } = require('@bot-whatsapp/bot') + +const flowPrincipal = addKeyword(['hola', 'alo']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('¡Hola! ¿En qué puedo ayudarte?') + }) + .addAction({ capture: true }, async (ctx, { flowDynamic }) => { + const mensaje = ctx.body + return flowDynamic(`Has dicho: ${mensaje}`) + }) +``` + +--- + +En este ejemplo, hemos creado varios flujos de conversación utilizando la función addKeyword y la función addAction. Cada flujo se activará cuando se envíe un mensaje que contenga una palabra clave específica. Dentro de cada flujo, definimos acciones que se ejecutarán en respuesta al mensaje recibido. + +El flujo principal se activará cuando se envíe un mensaje que contenga las palabras clave "hola" o "alo". La acción asociada simplemente devuelve un mensaje de bienvenida. + +Los flujos de consulta de producto, soporte técnico y otras consultas se activarán cuando se envíen mensajes que contengan las palabras clave "producto", "soporte" y "consultas", respectivamente. Cada acción asociada devuelve un mensaje solicitando más información sobre el tema específico. + +El flujo de ayuda se activará cuando se envíe un mensaje que contenga la palabra clave "ayuda". La primera acción asociada devuelve un mensaje que enumera los temas de ayuda disponibles. La segunda acción, con la opción capture: true, captura la respuesta del usuario y la utiliza para determinar qué acción ejecutar a continuación. + +Finalmente, creamos el bot utilizando la función createBot y agregamos todos los flujos creados. Luego, iniciamos el bot con bot.start(). + +Este ejemplo muestra cómo utilizar addAction para definir acciones específicas en respuesta a mensajes de WhatsApp y cómo crear flujos de conversación utilizando addKeyword. Puedes adaptar este ejemplo a tus necesidades y agregar más flujos y acciones según sea necesario. + +```js +const { addKeyword } = require('@bot-whatsapp/bot'); + +// Crear flujo principal +const flowPrincipal = addKeyword(['hola', 'alo']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('¡Hola! ¿En qué puedo ayudarte?'); + }); + +// Crear flujo de consulta de producto +const flowConsultaProducto = addKeyword(['producto']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('Por favor, especifica qué producto te interesa.'); + }); + +// Crear flujo de soporte técnico +const flowSoporteTecnico = addKeyword(['soporte']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('Por favor, describe el problema que estás experimentando.'); + }); + +// Crear flujo de otras consultas +const flowOtrasConsultas = addKeyword(['consultas']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('Por favor, proporciona más detalles sobre tu consulta.'); + }); + +// Crear flujo de ayuda +const flowAyuda = addKeyword(['ayuda']) + .addAction(async (_, { flowDynamic }) => { + return flowDynamic('¡Estoy aquí para ayudarte! ¿Necesitas ayuda con alguno de los siguientes temas?:\n 1. Información de producto.\n 2. Soporte técnico.\n 3. Otras consultas.'); + }) + .addAction({ capture: true }, async (ctx, { flowDynamic }) => { + const opcion = parseInt(ctx.body); + switch (opcion) { + case 1: return flowDynamic('Especifica qué producto te interesa.'); + case 2: return flowDynamic('Describe el problema que tienes.'); + case 3: return flowDynamic('Proporciona más detalles sobre tu consulta.'); + } + }); + +// Crear bot y agregar flujos +const bot = createBot({ + flows: [flowPrincipal, flowConsultaProducto, flowSoporteTecnico, flowOtrasConsultas, flowAyuda], +}); + + + ``` --- diff --git a/packages/docs/src/routes/docs/add-answers/index.mdx b/packages/docs/src/routes/docs/add-answers/index.mdx index 54f44f12b..05cab8ddf 100644 --- a/packages/docs/src/routes/docs/add-answers/index.mdx +++ b/packages/docs/src/routes/docs/add-answers/index.mdx @@ -88,7 +88,6 @@ Dependiendo de tus sistema operativo asegurate de colocar bien la ruta absoluta media: 'c:/ruta/imagen.png', //'c:\ruta\imagen.png' }) - ``` --- @@ -121,6 +120,48 @@ En algunas ocaciones necesitamos esperar por la respuesta del usuario para ello ``` +--- + +### Ejemplos + +En este ejemplo, se crean dos flujos de conversación: uno para saludar al usuario cuando escribe "hola" y otro para despedirse cuando escribe "adios". + +Cada flujo tiene una respuesta predefinida que se mostrará al usuario. Puedes personalizar las respuestas según tus necesidades. + +```js + const { addKeyword } = require('@bot-whatsapp/bot') + + const flowSaludo = addKeyword('hola') + .addAnswer('¡Hola! ¿En qué puedo ayudarte?') + + const flowDespedida = addKeyword('adios') + .addAnswer('¡Hasta luego! Espero haberte sido de ayuda. ¡Vuelve pronto!') + +``` + +--- + +En este ejemplo, se crean dos flujos de conversación: uno para el saludo y otro para la despedida. + +Además, se agrega un flujo adicional para enviar una imagen cuando se recibe el mensaje 'imagen'. + +```js +const flowSaludo = addKeyword('hola').addAnswer('¡Hola! ¿En qué puedo ayudarte?'); +const flowDespedida = addKeyword('adios').addAnswer('¡Hasta luego! Espero haberte sido de ayuda.'); + +const flowImagen = addKeyword('imagen').addAnswer('¡Aquí tienes una imagen!', { media: 'https://ruta/a/imagen.jpg' }); + +const adapterFlow = createFlow([flowSaludo, flowDespedida, flowImagen]); + +const adapterProvider = createProvider(WebWhatsappProvider); +createBot({ + flow: adapterFlow, + provider: adapterProvider, + database: adapterDB, +}); + +``` + --- diff --git a/packages/docs/src/routes/docs/add-keyword/index.mdx b/packages/docs/src/routes/docs/add-keyword/index.mdx index 94f9b3921..4a4da6c4d 100644 --- a/packages/docs/src/routes/docs/add-keyword/index.mdx +++ b/packages/docs/src/routes/docs/add-keyword/index.mdx @@ -2,13 +2,14 @@ import Navigation from '../../../components/widgets/Navigation' # addKeyword -Esta funcion se importa desde `@bot-whatsapp/bot` +Esta función, que se importa desde `@bot-whatsapp/bot`, inicia un [flujo](/docs/essential/) de chat en base a la configuración proporcionada. -Se encarga de iniciar un [flow](/docs/essential/) dependiendo de la configuracio que se le ha indicado. +La función `addKeyword` es como un punto de partida para la conversación. Le das a `addKeyword` una palabra o frase clave, y cuando alguien envía ese término al chatbot, la función desencadena un flujo de diálogo que has predefinido. ---- +--- + +En este ejemplo vemos cómo establecer un flujo que el bot seguirá al recibir un `hola` o `alo`. Las respuestas del bot se establecen con `.addAnswer()` -En este ejemplo puedes ver como definimos un conversacion que el bot va a responder cuando alguien escribe `hola` o `alo` . ```js const { addKeyword } = require('@bot-whatsapp/bot') @@ -19,7 +20,8 @@ const flowPrincipal = addKeyword(['hola', 'alo']) --- -Tambien puedes definir solo una palabra ejemplo si alguien escribe `comprar` +Podemos limitar el flujo a una sola palabra, como en el caso de `comprar`. + ```js const { addKeyword } = require('@bot-whatsapp/bot') @@ -30,10 +32,9 @@ const flowPrincipal = addKeyword('comprar') --- -### Regex - -En algunos casos necesitaras activar un flujo basado en una expression regular para esas situaciones tenemos. +## Regex +Para iniciarse en base a una expresión regular, utilice el código a continuación. En este caso detectará cuando el mensaje contiene algún número de tarjeta válido. ```js const { addKeyword } = require('@bot-whatsapp/bot') @@ -48,11 +49,9 @@ const flow = addKeyword(REGEX_CREDIT_NUMBER, { regex: true }) --- -### Sensitive - -El comportamiento del addKeyword por defecto detecta un palabra entre el mensaje ejemplo si alguien escribe `hola como estas` y tienes `addKeyword('hola')` el flow se activara. -Si no quieres este comportamiento y quieres afinar solo a que la persona escriba `hola` +## Sensitive +Por defecto, addKeyword detecta la palabra indicada en cualquier parte del mensaje, pero con `{ sensitive: true }` hará que solo se active el flujo si la persona escribe exactamente la palabra en cuestión. Vea el ejemplo de "hola" a continuación: ```js const { addKeyword } = require('@bot-whatsapp/bot') @@ -63,20 +62,20 @@ const flowBienvenida = addKeyword('hola', { sensitive: true }) ``` --- -### Eventos +## Eventos -En muchos casos necesitamos responder cuando no es una palabra exactamente. -Ejemplo que pasa si alguien envia una nota de voz, imagen, ubicacion, imagen o video. +Para los casos donde necesitamos responder ante acciones que no son palabras, podemos usar eventos. -En este caso puedes usar los **eventos** +Los eventos disponibles son: -- **WELCOME**: Se dispara cuando una persona escribe un palabra que no esta en ningun flow. Util para mensajes de bienvenida -- **MEDIA**: Util para detectar cuando la persona te envia imagen o video -- **LOCATION**: Util para detectar cuando la persona te envia coordenadas de ubicacion -- **VOICE_NOTE**: Util para detectar cuando la persona te envia nota de voz -- **DOCUMENT**: Util para detectar cuando la persona te envia alguno archivo como pdf, html o otro tipo que no sea los anteriores +- **WELCOME**: Activa el flujo cuando la persona escribe una palabra que no está en ningún flujo. Útil para mensajes de bienvenida +- **MEDIA**: Se activa cuando la persona envía una imagen o video +- **LOCATION**: Útil para detectar cuando la persona envía coordenadas de ubicación +- **VOICE_NOTE**: Activa el flujo cuando la persona envía una nota de voz +- **DOCUMENT**: Activa el flujo cuando la persona envía algún archivo como pdf, html u otro tipo además de los anteriores +- **ACTION**: Se utiliza como un "placebo" para no colocar una palabra clave. Esto es util cuando quiere un flujo pero que no se activa con ninguna palabra clave. (util para llamar desde gotoFlow) ---- +A continuación, vea algunos ejemplos de uso de estos eventos. ```js const { EVENTS } = require('@bot-whatsapp/bot') @@ -96,6 +95,9 @@ const flowNotaDeVoz = addKeyword(EVENTS.VOICE_NOTE) const flowDocumento = addKeyword(EVENTS.DOCUMENT) .addAnswer('Documento PDF recibido') +const flowAction = addKeyword(EVENTS.ACTION) + .addAnswer('Documento PDF recibido') + ``` --- @@ -103,6 +105,6 @@ const flowDocumento = addKeyword(EVENTS.DOCUMENT) +/> \ No newline at end of file diff --git a/packages/docs/src/routes/docs/flow-dynamic/index.mdx b/packages/docs/src/routes/docs/flow-dynamic/index.mdx index 4094c8271..08f390828 100644 --- a/packages/docs/src/routes/docs/flow-dynamic/index.mdx +++ b/packages/docs/src/routes/docs/flow-dynamic/index.mdx @@ -41,11 +41,18 @@ const flowString = addKeyword('ver categorias') await flowDynamic(mapeoDeLista) - await flowDynamic({body:'Tambien puedes enviar un mensaje de esta manera'}) + await flowDynamic([ + {body:'Tambien puedes enviar un mensaje de esta manera'} + ]) // Enviar una imagen o pdf o etc - await flowDynamic({media:'https://i.imgur.com/0HpzsEm.png'}) + await flowDynamic([ + { + body:"soy una imagen", + media:'https://i.imgur.com/0HpzsEm.png' + } + ]) }) ``` diff --git a/packages/docs/src/routes/docs/index.mdx b/packages/docs/src/routes/docs/index.mdx index 9813ca5c6..c69f10243 100644 --- a/packages/docs/src/routes/docs/index.mdx +++ b/packages/docs/src/routes/docs/index.mdx @@ -24,7 +24,7 @@ npm create bot-whatsapp@latest muted playsinline > - + diff --git a/packages/docs/src/routes/docs/install/index.mdx b/packages/docs/src/routes/docs/install/index.mdx index 6157c6d6c..5c7df049c 100644 --- a/packages/docs/src/routes/docs/install/index.mdx +++ b/packages/docs/src/routes/docs/install/index.mdx @@ -28,7 +28,7 @@ El **CLI** te hace una revisión previa, de versión de Node y sistema operativo muted playsinline > - + diff --git a/packages/docs/src/routes/docs/join/index.mdx b/packages/docs/src/routes/docs/join/index.mdx index 84698784b..1060a72a9 100644 --- a/packages/docs/src/routes/docs/join/index.mdx +++ b/packages/docs/src/routes/docs/join/index.mdx @@ -1,25 +1,27 @@ # Unirme -Bienvenido al proyecto! Estamos emocionados de tenerte a bordo y esperamos trabajar contigo. +Bienvenido al proyecto! Estamos ciertamente emocionados de tenerte a bordo, colaborando en lo que puedas, y esperamos trabajar y estrechar vínculos contigo. -Deseamos que te sientas cómodo y que puedas aportar tu valioso conocimiento y habilidades. +Sabemos que el camino para convertirte en un desarrollador profesional no es nada fácil, pero con una cuota de suerte y teniendo contactos puedes abrirte paso. -Recuerda que si tienes alguna inquietud, o simplemente deseas interactuar con los otros colaboradores puedes unirte a la comunidad. +Deseamos que te sientas cómodo, pero no tanto como para que te quedes dormido, y que puedas aportar tu valioso granito de arena, y también algún que otro aporte en conocimiento y habilidades. + +Recuerda que si tienes alguna inquietud, o simplemente deseas interactuar con los otros colaboradores, te recomendamos primero leer la documentación oficial. No queremos fomentar la vagancia. Después de hacer eso, puedes intentar unirte a la comunidad. Es un proceso complicado pero tampoco es imposible. Vamos, que el Leicester ganó la premier en 2016. --- -## Ventajas al unirme +## Ventajas super discretas al unirme -Participar en un proyecto de código abierto te permite aprender de manera práctica sobre tecnologías y metodologías de desarrollo de software. También puedes aprender de otros desarrolladores y contribuir a la comunidad de código abierto. +Participar abiertamente en un proyecto de código abierto, previa capacitación por supuesto, te permite aprender de manera práctica, aunque no tan didáctica, sobre tecnologías y metodologías que se van creando periódicamente en el mundo de desarrollo de software. También puedes, si así lo deseas, aprender de otros desarrolladores que programen en tu lenguaje favorito y contribuir con tu experiencia y dedicación a la comunidad de código abierto. -Al unirte te estará brindando la oportunidad de **adquirir experiencia en el desarrollo de software** y en el trabajo en equipo. Esto puede ser muy valioso para tu currículum y para tu carrera profesional. +Al unirte al canal de discord te estará brindando la oportunidad de **adquirir experiencia en el desarrollo integral y sostenido subsecuentemente en el tiempo de software** y en el trabajo en equipo. Esto puede ser realmente muy valioso para tu currículum y para tu carrera profesional, teniendo en cuenta que el mundo se consume en dinero. -La visibilidad es uno de los puntos que más se requieren hoy en día, al unirte se te brinda la oportunidad de **mayor visibilidad en la comunidad de desarrolladores y de demostrar tus habilidades**. Esto puede ayudarte a encontrar oportunidades de trabajo o a colaborar con otros proyectos. +La visibilidad es uno de los puntos que más se requieren a la hora de rentar un departamento hoy en día, al unirte se te brinda una sola oportunidad de **mayor visibilidad en días nublados en la comunidad de desarrolladores y de demostrar tus habilidades**. Esto puede ayudarte a encontrar oportunidades de trabajo, que de otra manera te serían inaccesibles, o a colaborar con otros proyectos, ya sea de código abierto, entreabierto o herméticamente cerrado. -Colaboración con otros desarrolladores de todo el mundo y **trabajar juntos para mejorar el proyecto** Al trabajar en equipo y colaborar con otros, podrás mejorar tu comunicación, resolución de problemas y liderazgo, habilidades que cada vez son más demandadas en el entorno laboral. +Colaboración con otros desarrolladores de todo el mundo conocido y por conocer, y **trabajar juntos para mejorar gradual y consecutivamente el susodicho proyecto** Al trabajar en equipo y colaborar con otros, participando en tareas de integración y desarrollo del lenguaje, así como también del idioma y la cultura, podrás mejorar tu comunicación verbal y escrita, resolución de problemas inmediatos generados por las pésimas condiciones de seguridad del estadio, y liderazgo, habilidades que cada vez son más demandadas en el entorno laboral actual. No sólo eso, diría que son casi fundamentales para el correcto desarrollo de la sociedad europea. Tal es así que el mismo Fernando Torres, apodado cariñosamente como "el niño", lo comentó cuando tuvo la oportunidad en La Resistencia. Este hecho generó graves impactos en la economía sociopolítica del siglo XXI, desembocando en la consecución de cierto equipo portugués en cierta máxima competición europea a nivel clubes. Lógicamente, todo apuntaba a una hazaña épica del gran, y me pongo de pie, José Mourinho. Pero lo cierto es que no se ha demostrado fehacientemente que sea responsabilidad del veterano croata que marcó el gol en la final, en tiempo de descuento. En fin, para gustos colores. -Contribución al bien común Participar en un proyecto de código abierto te permite contribuir a la comunidad y a la sociedad en general, ya que el código abierto es accesible y utilizable por cualquier persona. **Esto puede ser muy gratificante y sentirte parte de algo más grande**. +Contribución al bien común de la comunidad en edad escolar: Participar en un proyecto de código abierto tan prestigioso como este, no solo te permite contribuir a la comunidad y a la sociedad en general, sino que también trae aparejados ciertos cambios en la estructura política del pueblo camerunés, ya que el código abierto es accesible para unos pocos, pero utilizable por cualquier persona con dos dedos de frente. Esto, por supuesto, no aplica a los casos de trillizos o cuatrillizos, pero es un buen punto de partida. **Esto puede ser muy gratificante si se comparte a la medianoche y sentirte parte insignificante de algo un poco más grande de lo que estás acostumbrado**. -Es una excelente manera de incrementar tus habilidades tecnológicas y **estar al tanto de las últimas tendencias y desarrollos en el mundo del software**. Al trabajar con otros desarrolladores y contribuir a proyectos de código abierto, tendrás la oportunidad de \*\*aprender y practicar nuevas tecnologías y metodologías, lo que te ayudará a mejorar tus habilidades y a mantenerte actualizado en el mundo en constante cambio de la tecnología. +Es una excelente manera de aprender a incrementar tus habilidades socioeconómicas, sociales y económicas, y por sobre todas las cosas tecnológicas y **estar entre uno de los pocos seres humanos en la historia que estuvieron al tanto de las últimas tendencias en el mundo de la moda y de las ventajas de unir la experiencia y los desarrollos en el mundo del software**. Al trabajar con otros desarrolladores principiantes y contribuir a proyectos de código un tanto abierto, tendrás la inolvidable oportunidad de \*\*aprender, tanto como sea posible teniendo en cuenta las inclemencias del clima, y practicar, siempre y cuando estemos en año bisiesto, nuevas tecnologías y metodologías, lo que te ayudará a mejorar tus habilidades intrínsecas y a mantenerte actualizado en el universo en constante cambio de la ciencia que estudia las bondades y los atributos de la tecnología. -Esperamos que estés listo para **unirte a nosotros en esta emocionante aventura** +Esperamos que estés listo para **unirte a nosotros y nuestras mascotas en esta emocionante aventura** diff --git a/packages/docs/src/routes/docs/provider-meta/index.mdx b/packages/docs/src/routes/docs/provider-meta/index.mdx index 0112d3856..b0488bc5d 100644 --- a/packages/docs/src/routes/docs/provider-meta/index.mdx +++ b/packages/docs/src/routes/docs/provider-meta/index.mdx @@ -3,23 +3,44 @@ import Navigation from '../../../components/widgets/Navigation' # Meta - - ⚡ Para poder usar la API oficial de Meta de manera profesional debes tener en cuenta lo siguiente: - - - Deberas verificar tu negocio y tu aplicación con documentos oficiales dependiendo de tu pais. - - Deberas registrar un número de teléfono oficial de tu negocio que no este vinculado con ninguna cuenta de WhatsApp ya se normal o Bussines (Meta proporciona un número de pruebas gratuito). - - Meta brinda 1000 conversaciones gratis iniciadas por el cliente hacia el bot y 250 conversaciones gratis iniciadas por el bot hacia el cliente, después de eso deberás pagar por cada conversación (los costos varian según tu país o region). - +Más detalles de configuración puedes consultarlos [__aquí__](/docs/providers/meta). --- -## Meta: Configuración -En este apartado esta una guía de como iniciar la configuración de Meta. Puedes encontrar los [detalles aquí](/docs/providers/meta) +### Configuracion inicial + +Referencia completa: [aqui](https://github.com/codigoencasa/bot-whatsapp/blob/main/starters/apps/base-meta-memory/app.js) + +```js +const MetaProvider = require('@bot-whatsapp/provider/meta') + +const main = async () => { + const adapterDB = new MockAdapter() + const adapterFlow = createFlow([flowPrincipal]) + + const adapterProvider = createProvider(MetaProvider, { + jwtToken: 'jwtToken', + numberId: 'numberId', + verifyToken: 'verifyToken', + version: 'v16.0', + }) + + createBot({ + flow: adapterFlow, + provider: adapterProvider, + database: adapterDB, + }) +} + +main() +``` --- -## Funciones usando el provider -### Meta: Enviar mensaje +### Enviar texto + +Te presentamos aquí un ejemplo de cómo puedes enviar un mensaje con Meta. +Recuerda que ctx.from puede ser reemplazado por un número de teléfono en este formato: 59170000000, donde el 591 es el código de país y el 70000000 es el número de teléfono ```js const flowMensaje = addKeyword('hola') @@ -30,29 +51,40 @@ const flowMensaje = addKeyword('hola') }, async (ctx, {provider}) => { await provider.sendtext(ctx.from, 'mensaje') - //==> ctx.from puede ser reemplazado por un número de teléfono - //ej: 59170000000, donde el 591 es el código de país y el 70000000 es el número de teléfono } ) ``` -### Meta: Enviar media (imagen, pdf, audio, video) + +--- + +### Enviar medios (imagen, pdf, audio, video) + +Aquí te mostramos un ejemplo de cómo puedes enviar archivos multimedia con Meta ```js -const flowMensaje = addKeyword('hola') - .addAnswer( - 'Aqui va un mensaje', +const flujoFinal = addKeyword('hola').addAnswer('Hola!', null, async (ctx, { flowDynamic}) => { + await flowDynamic([ { - capture: true, - }, - async (ctx, {provider}) => { - await sendMedia = async (ctx.from, 'mensaje', 'url de la media') + body:'imagen', + media:"https://i.imgur.com/1XEiIGq.png" } - ) + ]) + await flowDynamic([ + { + body:'video', + media:"https://bot-whatsapp.netlify.app/videos/console.mp4" + } + ]) +}) ``` -### Meta: Enviar botones sin provider (maximo de 3 botones) + +--- + +### Enviar botones sin utilizar el proveedor (hasta 3 botones) + +Aquí puedes ver un ejemplo de cómo enviar botones sin necesidad de usar el proveedor. Ten en cuenta que puedes enviar hasta un máximo de 3 botones. ```js -// metodo sin uso del provider const flowMensaje = addKeyword('hola') .addAnswer( 'Aqui va un mensaje', @@ -66,9 +98,73 @@ const flowMensaje = addKeyword('hola') }, ) ``` -Para casos específicos donde se necesite enviar botones desde el provider revisar la ruta "node_modules/@bot-whatsapp/provider/meta/index.cjs", dentro el proyecto. +Si necesitas enviar botones de manera específica utilizando el proveedor, puedes revisar la ruta "node_modules/@bot-whatsapp/provider/meta/index.cjs" dentro de tu proyecto. + +### Enviar lista +A continuación, te mostramos un ejemplo de cómo enviar listas de opciones con Meta. Puedes adaptarlo a tus necesidades. + +```js +const flowServices = addKeyword('hola') + .addAnswer( + 'Aqui va un mensaje', + { + capture: true + }, + async (ctx, {provider}) => { + const headerText = 'HEADER_TEXT + const bodyText = 'BODY_TEXT' + const footerText = 'FOOTER_TEXT' + const buttonList = 'BUTTON_LIST' + const listParams = [ + { + title: 'TITLE_1', + rows: [ + { + id: 'ID_1', + title: 'TITLE_1', + description: 'DESCRIPTION_1' + }, + { + id: 'ID_2', + title: 'TITLE_2', + description: 'DESCRIPTION_2' + }, + { + id: 'ID_3', + title: 'TITLE_3', + description: 'DESCRIPTION_3' + } + ] + }, + { + title: 'TITLE_2', + rows: [ + { + id: 'ID_1', + title: 'TITLE_1', + description: 'DESCRIPTION_1' + }, + { + id: 'ID_2', + title: 'TITLE_2', + description: 'DESCRIPTION_2' + }, + { + id: 'ID_3', + title: 'TITLE_3', + description: 'DESCRIPTION_3' + } + ] + } + ] + await provider.sendList(ctx.from, headerText, bodyText, footerText, buttonList ,listParams) + } + ) +``` +### Enviar lista (otro metodo) + +Aquí te mostramos una variación de cómo enviar listas de opciones con Meta. -### Meta: Enviar lista metodo 1 ```js const flowMensaje = addKeyword('hola') .addAnswer( @@ -128,69 +224,10 @@ const flowMensaje = addKeyword('hola') } ) ``` -### Meta: Enviar lista metodo 2 -```js -const flowServices = addKeyword('hola') - .addAnswer( - 'Aqui va un mensaje', - { - capture: true - }, - async (ctx, {provider}) => { - const headerText = 'HEADER_TEXT - const bodyText = 'BODY_TEXT' - const footerText = 'FOOTER_TEXT' - const buttonList = 'BUTTON_LIST' - const listParams = [ - { - title: 'TITLE_1', - rows: [ - { - id: 'ID_1', - title: 'TITLE_1', - description: 'DESCRIPTION_1' - }, - { - id: 'ID_2', - title: 'TITLE_2', - description: 'DESCRIPTION_2' - }, - { - id: 'ID_3', - title: 'TITLE_3', - description: 'DESCRIPTION_3' - } - ] - }, - { - title: 'TITLE_2', - rows: [ - { - id: 'ID_1', - title: 'TITLE_1', - description: 'DESCRIPTION_1' - }, - { - id: 'ID_2', - title: 'TITLE_2', - description: 'DESCRIPTION_2' - }, - { - id: 'ID_3', - title: 'TITLE_3', - description: 'DESCRIPTION_3' - } - ] - } - ] - await provider.sendList(ctx.from, headerText, bodyText, footerText, buttonList ,listParams) - } - ) -``` --- -## Meta Deploy +## Ejecutar Meta -En este apartado esta una guía de como iniciar el deploy de Meta. Puedes encontrar los [detalles aquí](https://crhistianriverin.notion.site/Deploy-de-tu-chatbot-con-la-API-de-Meta-9ba0f1a800aa4df381c87a79dbf5a42a?pvs=4) +Te proporcionamos una guía para comenzar a ejecutar tu chatbot con la API de Meta. Puedes encontrar los [detalles aquí](https://crhistianriverin.notion.site/Deploy-de-tu-chatbot-con-la-API-de-Meta-9ba0f1a800aa4df381c87a79dbf5a42a?pvs=4). --- @@ -199,4 +236,4 @@ En este apartado esta una guía de como iniciar el deploy de Meta. Puedes encont { name: 'gotoFlow', link: '/docs/goto-flow' }, { name: 'Twilio', link: '/docs/provider-twilio' }, ]} -/> +/> \ No newline at end of file diff --git a/packages/docs/src/routes/docs/provider-twilio/index.mdx b/packages/docs/src/routes/docs/provider-twilio/index.mdx index f9e412d71..fa833f4a0 100644 --- a/packages/docs/src/routes/docs/provider-twilio/index.mdx +++ b/packages/docs/src/routes/docs/provider-twilio/index.mdx @@ -3,21 +3,11 @@ import Navigation from '../../../components/widgets/Navigation' # Twilio - - ⚡ Twilio te brinda un número de teléfono y funciona en base a créditos en su plataforma, ten en consideración que Twilio al ser un partner de Meta supondra costos un poco mas elevados en comparación. - +Más detalles de configuración puedes consultarlos [__aquí__](/docs/providers/twilio) --- -## Twilio: Configuración - -En este apartado esta una guía de como iniciar la configuración de Twilio. Puedes encontrar los [detalles aquí](/docs/providers/twilio) - ---- - -## Funciones usando el provider - -### Twilio: Enviar mensaje +### Enviar mensaje ```js const flowMensaje = addKeyword('hola') @@ -33,7 +23,10 @@ const flowMensaje = addKeyword('hola') } ) ``` -### Twilio: Enviar media (imagen, pdf, audio, video) + +--- + +### Enviar media (imagen, pdf, audio, video) ```js const flowMensaje = addKeyword('hola') @@ -47,7 +40,10 @@ const flowMensaje = addKeyword('hola') } ) ``` -### Twilio: Enviar botones sin provider (maximo de 3 botones) + +--- + +### Enviar botones (maximo de 3 botones) ```js // metodo sin uso del provider @@ -66,7 +62,8 @@ const flowMensaje = addKeyword('hola') ``` Para casos específicos donde se necesite enviar botones desde el provider revisar la ruta "node_modules/@bot-whatsapp/provider/meta/index.cjs", dentro el proyecto. -### Twilio: Enviar lista metodo 1 +### Enviar lista + ```js const flowMensaje = addKeyword('hola') .addAnswer( @@ -126,6 +123,7 @@ const flowMensaje = addKeyword('hola') } ) ``` + --- ## Funciones de la librería diff --git a/packages/docs/src/routes/docs/providers/meta/index.mdx b/packages/docs/src/routes/docs/providers/meta/index.mdx index 51714d2a0..197bc008f 100644 --- a/packages/docs/src/routes/docs/providers/meta/index.mdx +++ b/packages/docs/src/routes/docs/providers/meta/index.mdx @@ -9,7 +9,10 @@ prestarles ayuda a través del canal que ellos prefieran. ### Requerimientos -- Registrar una cuenta de [facebook developers](https://developers.facebook.com/apps) +- Registrar una cuenta de [facebook developers](https://developers.facebook.com/apps) +- Tendrás que verificar tu negocio y tu aplicación con documentos oficiales, cuyo tipo puede variar dependiendo de tu país. +- Deberás asociar a tu cuenta un número de teléfono oficial de tu negocio que no esté vinculado con ninguna otra cuenta de WhatsApp, ya sea regular o Business. Para las pruebas, Meta provee un número de teléfono gratuito. +- Meta ofrece 1000 conversaciones gratuitas iniciadas por el cliente hacia el bot, y 250 conversaciones gratuitas iniciadas por el bot hacia el cliente. Una vez alcanzado este límite, cada conversación adicional tendrá un costo que puede variar dependiendo de tu país o región. --- diff --git a/packages/docs/src/routes/docs/state/index.mdx b/packages/docs/src/routes/docs/state/index.mdx index fcc397de2..b453b3240 100644 --- a/packages/docs/src/routes/docs/state/index.mdx +++ b/packages/docs/src/routes/docs/state/index.mdx @@ -14,7 +14,7 @@ Esto tambien funcion si usas `gotoFlow` capture: true, }, async (ctx, { flowDynamic, state }) => { - state.update({ name: ctx.body }) + await state.update({ name: ctx.body }) flowDynamic('Gracias por tu nombre!') } ) @@ -24,7 +24,7 @@ Esto tambien funcion si usas `gotoFlow` capture: true, }, async (ctx, { flowDynamic, state }) => { - state.update({ age: ctx.body }) + await state.update({ age: ctx.body }) const myState = state.getMyState() await flowDynamic(`Gracias por tu edad! ${myState.name}`) } @@ -37,6 +37,23 @@ Esto tambien funcion si usas `gotoFlow` ``` +## state.get('prop') + +De esta manera puedes recuperar el valor de una propiedad del estado + +```js +... + .addAnswer( + 'Tu edad es', + null, + async (ctx, { flowDynamic, state }) => { + const age = state.get('age') + await flowDynamic(`Edad: ${age}`) + } + ) +... +``` + ## GlobalState De igual manera que el `state` anterior nosotros podemos usar `globalState` para tener un esta general de la app (NO es por usuario), esto es muy util si quieres ejemplo apagar o prener el bot @@ -48,9 +65,9 @@ De igual manera que el `state` anterior nosotros podemos usar `globalState` para .addAction(async (_, { flowDynamic, globalState }) => { const currentGlobalState = globalState.getMyState(); if(currentGlobalState.encendido){ - globalState.update({encendido:false}) + await globalState.update({encendido:false}) }else{ - globalState.update({encendido:true}) + await globalState.update({encendido:true}) } }) .addAnswer('🤖🤖 Gracias por tu participacion') @@ -84,6 +101,58 @@ De igual manera que el `state` anterior nosotros podemos usar `globalState` para ``` +## globalState.get('prop') + +De esta manera puedes recuperar el valor de una propiedad del estado global + +```js +... + .addAnswer( + 'La version del bot es', + null, + async (ctx, { flowDynamic, globalState }) => { + const version = globalState.get('version') + await flowDynamic(`Version: ${version}`) + } + ) +... +``` + +## Ejemplos + +En este ejemplo, se utiliza el contexto para capturar y almacenar información proporcionada por el usuario en diferentes etapas del flujo de conversación. El método addAnswer se utiliza para hacer preguntas al usuario y capturar sus respuestas utilizando `{capture: true}`. Luego, se utiliza state.update para actualizar el estado con la información capturada. + +En la última respuesta, se utiliza state.get para obtener los valores almacenados en el estado y mostrarlos en la consola. Esto demuestra cómo se puede acceder a la información almacenada en el estado en diferentes partes del flujo de conversación. + +Recuerda que estos son solo ejemplos básicos y puedes adaptarlos según tus necesidades específicas. + +```js +const flujo = addKeyword(['hola']) + .addAnswer('¿Cual es tu nombre?', {capture: true}, async (ctx, { state }) => { + await state.update({ name: ctx.body }) + }) + .addAnswer('¿Cual es tu edad?', {capture: true}, async (ctx, { state }) => { + await state.update({ age: ctx.body }) + }) + .addAnswer('¿Prefieres inglés o español?', {capture: true}, async (ctx, { state }) => { + await state.update({ language: ctx.body }) + }) + .addAnswer('¿Cuál es tu comida favorita?', {capture: true}, async (ctx, { state }) => { + await state.update({ favoriteFood: ctx.body }) + }) + .addAnswer('¡Gracias por la información!',null, async ({ state }) => { + const name = state.get('name'); + const age = state.get('age'); + const language = state.get('language'); + const favoriteFood = state.get('favoriteFood'); + + console.log(`Nombre: ${name}`); + console.log(`Edad: ${age}`); + console.log(`Lenguaje preferido: ${language}`); + console.log(`Comida favorita: ${favoriteFood}`); + }); +``` + --- { if (!mediaInput) throw new Error(`MEDIA_INPUT_NULL_: ${mediaInput}`) + + const formData = new FormData() + const mimeType = mime.lookup(mediaInput) + formData.append('file', createReadStream(mediaInput), { + contentType: mimeType, + }) + formData.append('messaging_product', 'whatsapp') + + const { + data: { id: mediaId }, + } = await axios.post(`${URL}/${this.version}/${this.numberId}/media`, formData, { + headers: { + Authorization: `Bearer ${this.jwtToken}`, + ...formData.getHeaders(), + }, + }) + const body = { messaging_product: 'whatsapp', to: number, type: 'image', image: { - link: mediaInput, + id: mediaId, }, } return this.sendMessageMeta(body) @@ -152,18 +169,18 @@ class MetaProvider extends ProviderClass { * @example await sendMessage('+XXXXXXXXXXX', 'https://dominio.com/imagen.jpg' | 'img/imagen.jpg') */ - sendMedia = async (number, mediaInput, text = '') => { + sendMedia = async (number, text = '', mediaInput) => { const fileDownloaded = await generalDownload(mediaInput) const mimeType = mime.lookup(fileDownloaded) - + mediaInput = fileDownloaded if (mimeType.includes('image')) return this.sendImage(number, mediaInput) if (mimeType.includes('video')) return this.sendVideo(number, fileDownloaded) if (mimeType.includes('audio')) { - const fileOpus = await convertAudio(fileDownloaded) + const fileOpus = await convertAudio(mediaInput) return this.sendAudio(number, fileOpus, text) } - return this.sendFile(number, fileDownloaded) + return this.sendFile(number, mediaInput) } /** diff --git a/packages/provider/src/meta/server.js b/packages/provider/src/meta/server.js index 9b72e2728..c940f97d4 100644 --- a/packages/provider/src/meta/server.js +++ b/packages/provider/src/meta/server.js @@ -7,14 +7,13 @@ const { getMediaUrl } = require('./utils') class MetaWebHookServer extends EventEmitter { constructor(jwtToken, numberId, version, token, metaPort = 3000) { super() - this.metaServer = polka() this.metaPort = metaPort this.token = token this.jwtToken = jwtToken this.numberId = numberId this.version = version - this.buildHTTPServer() + this.metaServer = this.buildHTTPServer() } /** @@ -26,6 +25,7 @@ class MetaWebHookServer extends EventEmitter { incomingMsg = async (req, res) => { const { body } = req const messages = body?.entry?.[0]?.changes?.[0]?.value?.messages + const contacts = req?.body?.entry?.[0]?.changes?.[0]?.value?.contacts if (!messages) { res.statusCode = 200 @@ -34,7 +34,9 @@ class MetaWebHookServer extends EventEmitter { } const [message] = messages + const [contact] = contacts const to = body.entry[0].changes[0].value?.metadata?.display_phone_number + const pushName = contact?.profile?.name if (message.type === 'text') { const body = message.text?.body @@ -43,6 +45,7 @@ class MetaWebHookServer extends EventEmitter { from: message.from, to, body, + pushName, } this.emit('message', responseObj) } @@ -56,6 +59,7 @@ class MetaWebHookServer extends EventEmitter { to, body, title_list_reply, + pushName, } this.emit('message', responseObj) } @@ -70,6 +74,7 @@ class MetaWebHookServer extends EventEmitter { url: resolvedUrl, to, body, + pushName, } this.emit('message', responseObj) @@ -85,6 +90,7 @@ class MetaWebHookServer extends EventEmitter { url: resolvedUrl, // Utilizar el valor resuelto de la promesa to, body, + pushName, } this.emit('message', responseObj) @@ -102,6 +108,7 @@ class MetaWebHookServer extends EventEmitter { url: resolvedUrl, // Utilizar el valor resuelto de la promesa to, body, + pushName, } this.emit('message', responseObj) @@ -117,6 +124,7 @@ class MetaWebHookServer extends EventEmitter { latitude: message.location.latitude, longitude: message.location.longitude, body, + pushName, } this.emit('message', responseObj) @@ -132,6 +140,7 @@ class MetaWebHookServer extends EventEmitter { url: resolvedUrl, // Utilizar el valor resuelto de la promesa to, body, + pushName, } this.emit('message', responseObj) @@ -146,6 +155,7 @@ class MetaWebHookServer extends EventEmitter { to, id: message.sticker.id, body, + pushName, } this.emit('message', responseObj) @@ -160,6 +170,7 @@ class MetaWebHookServer extends EventEmitter { contacts: [{ name: message.contacts[0].name, phones: message.contacts[0].phones }], to, body, + pushName, } this.emit('message', responseObj) @@ -177,6 +188,7 @@ class MetaWebHookServer extends EventEmitter { product_items: message.order.product_items, }, body, + pushName, } this.emit('message', responseObj) @@ -232,7 +244,7 @@ class MetaWebHookServer extends EventEmitter { * Contruir HTTP Server */ buildHTTPServer() { - this.metaServer + return polka() .use(urlencoded({ extended: true })) .use(json()) .get('/', this.emptyCtrl) diff --git a/packages/provider/src/twilio/index.js b/packages/provider/src/twilio/index.js index e19d2c932..b7c5940b9 100644 --- a/packages/provider/src/twilio/index.js +++ b/packages/provider/src/twilio/index.js @@ -17,7 +17,7 @@ const { parseNumber } = require('./utils') const PORT = process.env.PORT || 3000 class TwilioProvider extends ProviderClass { - twilioHook + twilioServer vendor vendorNumber publicUrl @@ -25,14 +25,14 @@ class TwilioProvider extends ProviderClass { super() this.publicUrl = publicUrl this.vendor = new twilio(accountSid, authToken) - this.twilioHook = new TwilioWebHookServer(port) + this.twilioServer = new TwilioWebHookServer(port) this.vendorNumber = parseNumber(vendorNumber) - this.twilioHook.start() + this.twilioServer.start() const listEvents = this.busEvents() for (const { event, func } of listEvents) { - this.twilioHook.on(event, func) + this.twilioServer.on(event, func) } } @@ -75,6 +75,7 @@ class TwilioProvider extends ProviderClass { `[NOTA]: Estas intentando enviar una fichero que esta en local.`, `[NOTA]: Para que esto funcione con Twilio necesitas que el fichero este en una URL publica`, `[NOTA]: más informacion aqui https://bot-whatsapp.netlify.app/docs/provider-twilio/`, + `[NOTA]: Esta es la url que se enviara a twilio (debe ser publica) ${urlEncode}`, ].join('\n') if ( @@ -117,12 +118,12 @@ class TwilioProvider extends ProviderClass { /** * - * @param {*} userId + * @param {*} number * @param {*} message * @param {*} param2 * @returns */ - sendMessage = async (number, message, { options }) => { + sendMessage = async (number, message, { options } = { options: {} }) => { number = parseNumber(number) if (options?.buttons?.length) this.sendButtons(number, message, options.buttons) if (options?.media) return this.sendMedia(number, message, options.media) diff --git a/packages/provider/src/twilio/server.js b/packages/provider/src/twilio/server.js index 2080bbe86..5906dafda 100644 --- a/packages/provider/src/twilio/server.js +++ b/packages/provider/src/twilio/server.js @@ -27,6 +27,7 @@ class TwilioWebHookServer extends EventEmitter { incomingMsg = (req, res) => { const { body } = req this.emit('message', { + ...body, from: parseNumber(body.From), to: parseNumber(body.To), body: body.Body, diff --git a/packages/provider/src/venom/index.js b/packages/provider/src/venom/index.js index 8b1f6aeae..5e20ca55a 100644 --- a/packages/provider/src/venom/index.js +++ b/packages/provider/src/venom/index.js @@ -47,6 +47,7 @@ class VenomProvider extends ProviderClass { undefined ) this.vendor = client + this.emit('ready', true) } catch (e) { logger.log(e) this.emit('auth_failure', { diff --git a/packages/provider/src/venom/package.json b/packages/provider/src/venom/package.json index 6735c5260..8a090da01 100644 --- a/packages/provider/src/venom/package.json +++ b/packages/provider/src/venom/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "venom-bot": "4.3.7", + "venom-bot": "5.0.21", "mime-types": "2.1.35" } } diff --git a/packages/provider/src/web-whatsapp/index.js b/packages/provider/src/web-whatsapp/index.js index 39838c8c0..6fe0d595c 100644 --- a/packages/provider/src/web-whatsapp/index.js +++ b/packages/provider/src/web-whatsapp/index.js @@ -79,7 +79,11 @@ class WebWhatsappProvider extends ProviderClass { }, { event: 'ready', - func: () => this.emit('ready', true), + func: () => { + const host = { ...this.vendor?.info?.wid, phone: this.vendor?.info?.wid?.user } + this.emit('ready', true) + this.emit('host', host) + }, }, { event: 'message', diff --git a/packages/provider/src/web-whatsapp/package.json b/packages/provider/src/web-whatsapp/package.json index 573c9adcd..673aa749c 100644 --- a/packages/provider/src/web-whatsapp/package.json +++ b/packages/provider/src/web-whatsapp/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "whatsapp-web.js": "^1.22.1" + "whatsapp-web.js": "^1.22.2-alpha.1" } } diff --git a/packages/provider/src/wppconnect/package.json b/packages/provider/src/wppconnect/package.json index ea4f1582a..a57b9ad46 100644 --- a/packages/provider/src/wppconnect/package.json +++ b/packages/provider/src/wppconnect/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@wppconnect-team/wppconnect": "^1.27.3" + "@wppconnect-team/wppconnect": "^1.28.0" } }