Merge branch 'dev' into fix/callback-01

This commit is contained in:
Leifer Mendez
2023-02-10 09:01:35 +01:00
committed by GitHub
27 changed files with 1226 additions and 1034 deletions

View File

@@ -110,13 +110,16 @@ class CoreClass {
}
// 📄 Finalizar flujo
const endFlow = async (message = null) => {
endFlowFlag = true
if (message) this.sendProviderAndSave(from, createCtxMessage(message))
clearQueue()
sendFlow([])
return
}
const endFlow =
(flag) =>
async (message = null) => {
flag.endFlow = true
endFlowFlag = true
if (message) this.sendProviderAndSave(from, createCtxMessage(message))
clearQueue()
sendFlow([])
return
}
// 📄 Esta funcion se encarga de enviar un array de mensajes dentro de este ctx
const sendFlow = async (messageToSend, numberOrId, options = { prev: prevMsg }) => {
@@ -127,68 +130,68 @@ class CoreClass {
if (endFlowFlag) return
const delayMs = ctxMessage?.options?.delay || 0
if (delayMs) await delay(delayMs)
QueuePrincipal.enqueue(() =>
Promise.all([
this.sendProviderAndSave(numberOrId, ctxMessage).then(() => resolveCbEveryCtx(ctxMessage)),
])
await QueuePrincipal.enqueue(() =>
this.sendProviderAndSave(numberOrId, ctxMessage).then(() => resolveCbEveryCtx(ctxMessage))
)
}
return Promise.all(queue)
}
// 📄 [options: fallBack]: esta funcion se encarga de repetir el ultimo mensaje
const fallBack = async (validation = false, message = null) => {
QueuePrincipal.queue = []
const continueFlow = async () => {
const currentPrev = await this.databaseClass.getPrevByNumber(from)
const nextFlow = (await this.flowClass.find(refToContinue?.ref, true)) ?? []
const filterNextFlow = nextFlow.filter((msg) => msg.refSerialize !== currentPrev?.refSerialize)
const isContinueFlow = filterNextFlow.map((i) => i.keyword).includes(currentPrev?.ref)
if (validation) {
const currentPrev = await this.databaseClass.getPrevByNumber(from)
const nextFlow = await this.flowClass.find(refToContinue?.ref, true)
const filterNextFlow = nextFlow.filter((msg) => msg.refSerialize !== currentPrev?.refSerialize)
return sendFlow(filterNextFlow, from, { prev: undefined })
if (!isContinueFlow) {
const refToContinueChild = this.flowClass.getRefToContinueChild(currentPrev?.keyword)
const flowStandaloneChild = this.flowClass.getFlowsChild()
const nextChildMessages =
(await this.flowClass.find(refToContinueChild?.ref, true, flowStandaloneChild)) || []
if (nextChildMessages?.length) return await sendFlow(nextChildMessages, from, { prev: undefined })
}
await this.sendProviderAndSave(from, {
...prevMsg,
answer: typeof message === 'string' ? message : message?.body ?? prevMsg.answer,
options: {
...prevMsg.options,
buttons: prevMsg.options?.buttons,
},
})
return
if (!isContinueFlow) {
await sendFlow(filterNextFlow, from, { prev: undefined })
return
}
}
// 📄 [options: fallBack]: esta funcion se encarga de repetir el ultimo mensaje
const fallBack =
(flag) =>
async (message = null) => {
QueuePrincipal.queue = []
flag.fallBack = true
await this.sendProviderAndSave(from, {
...prevMsg,
answer: typeof message === 'string' ? message : message?.body ?? prevMsg.answer,
options: {
...prevMsg.options,
buttons: prevMsg.options?.buttons,
},
})
return
}
// 📄 [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 = []) => {
if (!Array.isArray(listMsg)) listMsg = [listMsg]
const flowDynamic =
(flag) =>
async (listMsg = []) => {
flag.flowDynamic = true
if (!Array.isArray(listMsg)) listMsg = [listMsg]
const parseListMsg = listMsg.map((opt, index) => createCtxMessage(opt, index))
const currentPrev = await this.databaseClass.getPrevByNumber(from)
const parseListMsg = listMsg.map((opt, index) => createCtxMessage(opt, index))
const skipContinueFlow = async () => {
const nextFlow = await this.flowClass.find(refToContinue?.ref, true)
const filterNextFlow = nextFlow.filter((msg) => msg.refSerialize !== currentPrev?.refSerialize)
const isContinueFlow = filterNextFlow.map((i) => i.keyword).includes(currentPrev?.ref)
return {
continue: !isContinueFlow,
contexts: filterNextFlow,
if (endFlowFlag) return
for (const msg of parseListMsg) {
await this.sendProviderAndSave(from, msg)
}
await continueFlow()
return
}
if (endFlowFlag) return
for (const msg of parseListMsg) {
await this.sendProviderAndSave(from, msg)
}
const continueFlowData = await skipContinueFlow()
if (continueFlowData.continue) return sendFlow(continueFlowData.contexts, from, { prev: undefined })
return
}
// 📄 Se encarga de revisar si el contexto del mensaje tiene callback o fallback
const resolveCbEveryCtx = async (ctxMessage) => {
if (!ctxMessage?.options?.capture) return await cbEveryCtx(ctxMessage?.ref)
@@ -196,15 +199,29 @@ class CoreClass {
// 📄 Se encarga de revisar si el contexto del mensaje tiene callback y ejecutarlo
const cbEveryCtx = async (inRef) => {
let flags = {
endFlow: false,
fallBack: false,
flowDynamic: false,
wait: true,
}
const provider = this.providerClass
if (!this.flowClass.allCallbacks[inRef]) return Promise.resolve()
return this.flowClass.allCallbacks[inRef](messageCtxInComming, {
const argsCb = {
provider,
fallBack,
flowDynamic,
endFlow,
})
fallBack: fallBack(flags),
flowDynamic: flowDynamic(flags),
endFlow: endFlow(flags),
}
await this.flowClass.allCallbacks[inRef](messageCtxInComming, argsCb)
const wait = !(!flags.endFlow && !flags.fallBack && !flags.flowDynamic)
if (!wait) await continueFlow()
return
}
// 📄🤘(tiene return) [options: nested(array)]: Si se tiene flujos hijos los implementa
@@ -216,7 +233,7 @@ class CoreClass {
msgToSend = this.flowClass.find(body, false, flowStandalone) || []
sendFlow(msgToSend, from)
await sendFlow(msgToSend, from)
return
}
@@ -226,7 +243,7 @@ class CoreClass {
if (typeCapture === 'boolean' && fallBackFlag) {
msgToSend = this.flowClass.find(refToContinue?.ref, true) || []
sendFlow(msgToSend, from)
await sendFlow(msgToSend, from)
return
}
}
@@ -241,11 +258,11 @@ class CoreClass {
* @param {*} ctxMessage ver más en GLOSSARY.md
* @returns
*/
sendProviderAndSave = (numberOrId, ctxMessage) => {
sendProviderAndSave = async (numberOrId, ctxMessage) => {
const { answer } = ctxMessage
return this.providerClass
.sendMessage(numberOrId, answer, ctxMessage)
.then(() => this.databaseClass.save({ ...ctxMessage, from: numberOrId }))
await this.providerClass.sendMessage(numberOrId, answer, ctxMessage)
await this.databaseClass.save({ ...ctxMessage, from: numberOrId })
return
}
/**

View File

@@ -25,16 +25,8 @@ class FlowClass {
let refSymbol = null
overFlow = overFlow ?? this.flowSerialize
const customRegex = (str = null) => {
if (typeof str !== 'string') return
const instanceRegex = new RegExp(str)
return instanceRegex.test(str)
}
/** Retornar expresion regular para buscar coincidencia */
const mapSensitive = (str, mapOptions = { sensitive: false, regex: false }) => {
if (mapOptions.regex) return customRegex(str)
if (mapOptions.regex) return new RegExp(str)
const regexSensitive = mapOptions.sensitive ? 'g' : 'i'
if (Array.isArray(str)) {
return new RegExp(str.join('|'), regexSensitive)
@@ -43,10 +35,7 @@ class FlowClass {
}
const findIn = (keyOrWord, symbol = false, flow = overFlow) => {
const sensitive = refSymbol?.options?.sensitive || false
const regex = refSymbol?.options?.regex || false
capture = refSymbol?.options?.capture || false
if (capture) return messages
if (symbol) {
@@ -55,6 +44,8 @@ class FlowClass {
if (refSymbol?.ref) findIn(refSymbol.ref, true)
} else {
refSymbol = flow.find((c) => {
const sensitive = c?.options?.sensitive || false
const regex = c?.options?.regex || false
return mapSensitive(c.keyword, { sensitive, regex }).test(keyOrWord)
})
if (refSymbol?.ref) findIn(refSymbol.ref, true)
@@ -68,6 +59,37 @@ class FlowClass {
findBySerialize = (refSerialize) => this.flowSerialize.find((r) => r.refSerialize === refSerialize)
findIndexByRef = (ref) => this.flowSerialize.findIndex((r) => r.ref === ref)
getRefToContinueChild = (keyword) => {
try {
const flowChilds = this.flowSerialize
.reduce((acc, cur) => {
const merge = [...acc, cur?.options?.nested].flat(2)
return merge
}, [])
.filter((i) => !!i && i?.refSerialize === keyword)
.shift()
return flowChilds
} catch (e) {
return undefined
}
}
getFlowsChild = () => {
try {
const flowChilds = this.flowSerialize
.reduce((acc, cur) => {
const merge = [...acc, cur?.options?.nested].flat(2)
return merge
}, [])
.filter((i) => !!i)
return flowChilds
} catch (e) {
return []
}
}
}
module.exports = FlowClass

View File

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

View File

@@ -23,6 +23,8 @@ class ProviderClass extends EventEmitter {
if (NODE_ENV !== 'production') console.log('[sendMessage]', { userId, message })
return message
}
getInstance = () => this.vendor
}
module.exports = ProviderClass

View File

@@ -17,6 +17,8 @@ class MockFlow {
}
findBySerialize = () => ({})
findIndexByRef = () => 0
getRefToContinueChild = () => ({})
getFlowsChild = () => []
}
class MockDBA {

View File

@@ -30,7 +30,6 @@ class MongoAdapter {
save = async (ctx) => {
await this.db.collection('history').insert(ctx)
console.log('Guardando DB...', ctx)
this.listHistory.push(ctx)
}
}

View File

@@ -152,57 +152,65 @@ const flowString = addKeyword('hola')
## endFlow()
Esta funcion se utliza para finalizar un flujo con dos o más addAnswer. Un ejemplo de uso sería registrar 3 datos de un usuario en 3 preguntas distinas y
que el usuario pueda finalizar por él mismo el flujo.
Esta funcion se utliza para **finalizar un flujo con dos** o más addAnswer. Un ejemplo de uso sería registrar 3 datos de un usuario en 3 preguntas distinas y
que el usuario **pueda finalizar por él mismo el flujo**.
Como podrás comprobar en el ejemplo siguiente, se puede vincular flowDynamic y todas sus funciones; como por ejemplo botones.
```js
const flowFormulario = addKeyword(['Hola'])
let nombre;
let apellidos;
let telefono;
const flowFormulario = addKeyword(['Hola','⬅️ Volver al Inicio'])
.addAnswer(
['Hola!', 'Escriba su *Nombre* para generar su solicitud'],
['Hola!','Para enviar el formulario necesito unos datos...' ,'Escriba su *Nombre*'],
{ capture: true, buttons: [{ body: '❌ Cancelar solicitud' }] },
async (ctx, { flowDynamic, endFlow }) => {
if (ctx.body == '❌ Cancelar solicitud') {
await flowDynamic([
{
body: '❌ *Su solicitud de cita ha sido cancelada* ❌',
buttons: [{ body: '⬅️ Volver al Inicio' }],
},
])
return endFlow()
}
if (ctx.body == '❌ Cancelar solicitud')
return endFlow({body: '❌ Su solicitud ha sido cancelada ❌', // Aquí terminamos el flow si la condicion se comple
buttons:[{body:'⬅️ Volver al Inicio' }] // Y además, añadimos un botón por si necesitas derivarlo a otro flow
})
nombre = ctx.body
return flowDynamic(`Encantado *${nombre}*, continuamos...`)
}
)
.addAnswer(
['También necesito tus dos apellidos'],
{ capture: true, buttons: [{ body: '❌ Cancelar solicitud' }] },
async (ctx, { flowDynamic, endFlow }) => {
if (ctx.body == '❌ Cancelar solicitud') {
await flowDynamic([
{
body: '❌ *Su solicitud de cita ha sido cancelada* ❌',
buttons: [{ body: '⬅️ Volver al Inicio' }],
},
])
return endFlow()
}
if (ctx.body == '❌ Cancelar solicitud')
return endFlow({body: '❌ Su solicitud ha sido cancelada ❌',
buttons:[{body:'⬅️ Volver al Inicio' }]
})
apellidos = ctx.body
return flowDynamic(`Perfecto *${nombre}*, por último...`)
}
)
.addAnswer(
['Dejeme su número de teléfono y le llamaré lo antes posible.'],
{ capture: true, buttons: [{ body: '❌ Cancelar solicitud' }] },
async (ctx, { flowDynamic, endFlow }) => {
if (ctx.body == '❌ Cancelar solicitud') {
await flowDynamic([
{
body: '❌ *Su solicitud de cita ha sido cancelada* ❌',
buttons: [{ body: '⬅️ Volver al Inicio' }],
},
])
return endFlow()
}
if (ctx.body == '❌ Cancelar solicitud')
return endFlow({body: '❌ Su solicitud ha sido cancelada ❌',
buttons:[{body:'⬅️ Volver al Inicio' }]
})
telefono = ctx.body
await delay(2000)
return flowDynamic(`Estupendo *${nombre}*! te dejo el resumen de tu formulario
\n- Nombre y apellidos: *${nombre} ${apellidos}*
\n- Telefono: *${telefono}*`)
}
)
```
---

View File

@@ -157,6 +157,11 @@ class BaileysProvider extends ProviderClass {
}
}
/**
* Funcion SendRaw envia opciones directamente del proveedor
* @example await sendMessage('+XXXXXXXXXXX', 'Hello World')
*/
/**
* @alpha
* @param {string} number
@@ -213,10 +218,10 @@ class BaileysProvider extends ProviderClass {
* @example await sendMessage('+XXXXXXXXXXX', 'audio.mp3')
*/
sendAudio = async (number, audioUrl, voiceNote = false) => {
sendAudio = async (number, audioUrl) => {
return this.vendor.sendMessage(number, {
audio: { url: audioUrl },
ptt: voiceNote,
ptt: true,
})
}
@@ -282,6 +287,7 @@ class BaileysProvider extends ProviderClass {
* @param {string} message
* @example await sendMessage('+XXXXXXXXXXX', 'Hello World')
*/
sendMessage = async (numberIn, message, { options }) => {
const number = baileyCleanNumber(numberIn)

View File

@@ -1,20 +1,20 @@
const { ProviderClass } = require('@bot-whatsapp/bot')
const { ProviderClass } = require('../../../bot')
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}
class MockProvider extends ProviderClass {
constructor() {
super()
}
delaySendMessage = (miliseconds, eventName, payload) =>
new Promise((res) =>
setTimeout(() => {
this.emit(eventName, payload)
res
}, miliseconds)
)
delaySendMessage = async (miliseconds, eventName, payload) => {
await delay(miliseconds)
this.emit(eventName, payload)
}
sendMessage = async (userId, message) => {
console.log(`Enviando... ${userId}, ${message}`)
return Promise.resolve({ userId, message })
}
}

View File

@@ -230,6 +230,14 @@ class WebWhatsappProvider extends ProviderClass {
return this.sendFile(number, fileDownloaded)
}
/**
* Funcion SendRaw envia opciones directamente del proveedor
* @param {string} number
* @param {string} message
* @example await sendMessage('+XXXXXXXXXXX', 'Hello World')
*/
sendRaw = () => this.vendor.sendMessage
/**
*
* @param {*} userId

View File

@@ -1,6 +1,6 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const MockProvider = require('../../../__mocks__/mock.provider')
const MockProvider = require('../../provider/src/mock')
test(`ProviderClass`, async () => {
const provider = new MockProvider()