diff --git a/.vscode/bot.code-snippets b/.vscode/bot.code-snippets new file mode 100644 index 0000000..062383f --- /dev/null +++ b/.vscode/bot.code-snippets @@ -0,0 +1,24 @@ +{ + "Flow Bot (simple)": { + "scope": "javascript", + "prefix": "bot:flow", + "description": "Crear un flujo simple", + "body": [ + "export const flow${1} = addKeyword(['hola', 'buenas'])", + " .addAnswer('Hola! 🚀 Bienvenido a este CHATBOT')", + " .addAnswer('¿Como puedo ayudarte?')" + ] + }, + "Flow Bot (completo)": { + "scope": "javascript", + "prefix": "bot:flow completo", + "description": "Crear un flujo completo", + "body": [ + "export const flow${1} = addKeyword(['categorias'])", + " .addAnswer('⚡ Tenemos las siguientes categorias')", + " .addAnswer(['🚀 Computadoras', '🚀 Celulares', '🚀 Otros'], {", + " delay: 1500, //Milisegundo 1500 = 1.5segundos", + "})" + ] + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b1954d..176cfc7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ __Requerimientos:__ __Clonar repo rama dev__ ``` -git clone --branch dev https://github.com/leifermendez/bot-whatsapp +git clone --branch dev https://github.com/codigoencasa/bot-whatsapp ``` __Instalar dependencias__ ``` @@ -34,24 +34,55 @@ Se ejecuta el CLI (Command Line Interface) para ayudarte a crear un app-bot de e yarn run cli ``` -Abrir carpeta __example-app-base__ y ejecutar +Selecionas (mediante las flechas arriba y abajo) el proveedor que quieras usar y cuando estes sobre el presiona la barra de espacio, igualmente selecciona la base de datos que quieras usar. + +Se creó un subdirecorio con el nombre del proveedor y base de datos que seleccionaste, ejemplo: `base-bailey-mysql` + +Dentro de ese directorio necesitas editar el archivo package.json y borrar las siguientes lineas: ``` -cd example-app-base -npm i + "@bot-whatsapp/bot": "latest", + "@bot-whatsapp/cli": "latest", + "@bot-whatsapp/database": "latest", + "@bot-whatsapp/provider": "latest", +``` + +Cambiate al directorio creado ejemplo: `base-bailey-mysql` +``` +cd base-baileys-mysql +``` +Ejecuta los comandos: +``` +npm install npm run pre-copy npm start ``` +En el caso de MySql y Mongo es necesario especificar en app.js los datos de la conexión, ejemplo de MySql: +``` +const BaileysProvider = require('@bot-whatsapp/provider/baileys') +const MySQLAdapter = require('@bot-whatsapp/database/mysql') -### __Commit y Push__ +/** + * Declaramos las conexiones de MySQL + */ +const MYSQL_DB_HOST = 'localhost' +const MYSQL_DB_USER = 'usr' +const MYSQL_DB_PASSWORD = 'pass' +const MYSQL_DB_NAME = 'bot' +``` + -El proyecto tiene implementado __[husky](https://typicode.github.io/husky/#/)__, es una herramienta que dispara unas acciones al momento de hacer commit y hacer push. - -__commit:__ Los commit son semánticos, esto quiere decir que deben cumplir un standar al momento de escribirlos ejemplo: ` feat(adapter): new adapter myqsl ` puede ver más info sobre esto __[aquí](https://github.com/conventional-changelog/commitlint/#what-is-commitlint)__ - -__push:__ Cada push ejecutar `yarn run test` el cual realiza los test internos que tienen que cumplir con __95% de cobertura__. +![](https://i.imgur.com/dC6lEwy.png) -> Documento en constante actualización.... +### 🤔 Preguntas frecuentes +- ¿Como puedo hacer aportaciones de código en el proyecto?: [Ver Video](https://youtu.be/Lxt8Acob6aU) +- ¿Como ejecutar el entorno de pruebas?: [Ver Video](https://youtu.be/Mf9V-dloBfk) +- ¿Como crear un nuevo proveedor?: [Ver Video](https://youtu.be/cahK9zH3SI8) +- ¿Que son los GithubActions?: [Ver Video](https://youtu.be/nYBEBFKLiqw) + + +> __NOTA:__ Documento en constante actualización.... ------ - [Discord](https://link.codigoencasa.com/DISCORD) diff --git a/GLOSSARY.md b/GLOSSARY.md index ec85e10..d054e6f 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -1,2 +1,2 @@ CTX: Es el objeto que representa un mensaje, con opciones, id, ref -messageInComming: Objeto entrante del provider {body, from,...} \ No newline at end of file +messageInComming: Objeto entrante del provider {body, from,to,...} \ No newline at end of file diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..5b5086e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,73 @@ +## MIGRANDO DE LA VERSIÓN 1 A LAS VERSIÓN 2 + +Pasar los flujos del bot de la versión 1 a la 2 es muy fácil, supongamos que en tu initial.json y response.json tienes un flujo como el siguiente: + +```js +//initial.json +[ + { + "keywords": [ + "hola", + "ola", + "alo" + ], + "key": "hola" + }, + { + "keywords": ["adios", "bye"], + "key": "adios" + } +] +``` +y + +```js +//response.json +{ + "hola":{ + "replyMessage":[ + "Gracias a ti! \n" + ], + "media":null, + "trigger":null + }, + "adios":{ + "replyMessage":[ + "Que te vaya bien!!" + ], + "media":null + } +} +``` +En la versión 2, no es necesario tener esos 2 archivos, los flujos se ponen directamente en app.js de la siguiente manera: + +```js +//app.js +/** + * Declarando flujos principales. + */ +const flowHola = addKeyword(['hola', 'ola', 'alo']) //Aqui van los "keywords" de initial.json + .addAnswer('Gracias a ti!') // Aquí va la respuesta del response.json, no es necesario especificar nuevamente los "keywords" + .addAnswer('Siempre un placer!!!') // Y se pueden agregar varias respuestas encadenadas ... TANTAS com sean necesarias. + +const flowAdios = addKeyword(['adios', 'bye']) //Aqui van los "keywords" de initial.json + .addAnswer('Que te vaya bien!!') // Aquí va la respuesta del response.json, no es necesario especificar nuevamente los "keywords" + .addAnswer('Hasta luego!', // Y se pueden agregar varias respuestas encadenadas ... TANTAS com sean necesarias. + null, null,[...addChild(flowHijo1)] // Y se pueden agregar flujos HIJOS (Sub Menus). Los flujos hijos se tienen que definir ANTES que los principales. + ) + + +##FALTAN EJEMPLOS DE ENVIOS DE IMAGENES! + + + const main = async () => { + const adapterDB = new MockAdapter() + const adapterFlow = createFlow([flowHola, flowAdios]) // Aqui se crean los flujos. + const adapterProvider = createProvider(BaileysProvider) + createBot({ + flow: adapterFlow, + provider: adapterProvider, + database: adapterDB, + }) +} + ``` \ No newline at end of file diff --git a/package.json b/package.json index 8a8ebe0..92f2f3d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "create-bot:rollup": "rollup --config ./packages/create-bot-whatsapp/rollup-create.config.js ", "bot:rollup": "rollup --config ./packages/bot/rollup-bot.config.js", "provider:rollup": "rollup --config ./packages/provider/rollup-provider.config.js ", + "contexts:rollup": "rollup --config ./packages/contexts/rollup-contexts.config.js", "database:rollup": "rollup --config ./packages/database/rollup-database.config.js", "create-bot-whatsapp:rollup": "rollup --config ./packages/create-bot-whatsapp/rollup-create.config.js", "format:check": "prettier --check ./packages", @@ -17,7 +18,7 @@ "fmt.staged": "pretty-quick --staged", "lint:check": "eslint ./packages", "lint:fix": "eslint --fix ./packages", - "build": "yarn run cli:rollup && yarn run bot:rollup && yarn run provider:rollup && yarn run database:rollup && yarn run create-bot-whatsapp:rollup", + "build": "yarn run cli:rollup && yarn run bot:rollup && yarn run provider:rollup && yarn run database:rollup && yarn run contexts:rollup && yarn run create-bot-whatsapp:rollup", "copy.lib": "node ./scripts/move.js", "test.unit": "node ./node_modules/uvu/bin.js packages test", "test.coverage": "node ./node_modules/c8/bin/c8.js npm run test.unit", @@ -37,6 +38,7 @@ "packages/cli", "packages/database", "packages/provider", + "packages/contexts", "packages/docs" ], "keywords": [ diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index e04bd13..e545a55 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -58,13 +58,13 @@ class CoreClass { ] /** - * - * @param {*} messageInComming + * GLOSSARY.md + * @param {*} messageCtxInComming * @returns */ - handleMsg = async (messageInComming) => { - logger.log(`[handleMsg]: `, messageInComming) - const { body, from } = messageInComming + handleMsg = async (messageCtxInComming) => { + logger.log(`[handleMsg]: `, messageCtxInComming) + const { body, from } = messageCtxInComming let msgToSend = [] let fallBackFlag = false @@ -92,12 +92,26 @@ class CoreClass { return refToContinue } + // 📄 Se encarga de revisar si el contexto del mensaje tiene callback y ejecutarlo + const cbEveryCtx = (inRef) => { + const indexFlow = this.flowClass.findIndexByRef(inRef) + this.flowClass.allCallbacks[indexFlow].callback( + messageCtxInComming, + { + fallBack, + } + ) + } + // 📄 [options: callback]: Si se tiene un callback se ejecuta - if (!fallBackFlag && refToContinue && prevMsg?.options?.callback) { - const indexFlow = this.flowClass.findIndexByRef(refToContinue?.ref) - this.flowClass.allCallbacks[indexFlow].callback(messageInComming, { - fallBack, - }) + if (!fallBackFlag) { + if (refToContinue && prevMsg?.options?.callback) { + cbEveryCtx(refToContinue?.ref) + } else { + for (const ite of this.flowClass.find(body)) { + cbEveryCtx(ite?.ref) + } + } } // 📄🤘(tiene return) [options: nested(array)]: Si se tiene flujos hijos los implementa diff --git a/packages/bot/io/methods/addAnswer.js b/packages/bot/io/methods/addAnswer.js index d5b53e6..a7db6d6 100644 --- a/packages/bot/io/methods/addAnswer.js +++ b/packages/bot/io/methods/addAnswer.js @@ -31,10 +31,7 @@ const addAnswer = nested: Array.isArray(nested) ? nested : [], }) - const callback = - typeof cb === 'function' - ? cb - : () => console.log('Callback no definida') + const callback = typeof cb === 'function' ? cb : () => null const lastCtx = inCtx.hasOwnProperty('ctx') ? inCtx.ctx : inCtx diff --git a/packages/bot/package.json b/packages/bot/package.json index 554f404..4079f55 100644 --- a/packages/bot/package.json +++ b/packages/bot/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/bot", - "version": "0.0.23-alpha.0", + "version": "0.0.24-alpha.0", "description": "", "main": "./lib/bundle.bot.cjs", "scripts": { diff --git a/packages/cli/interactive/index.js b/packages/cli/interactive/index.js index f7360ce..3dc16c4 100644 --- a/packages/cli/interactive/index.js +++ b/packages/cli/interactive/index.js @@ -12,7 +12,7 @@ const bannerDone = () => { [ `[Agradecimientos]: Este es un proyecto OpenSource, si tienes intenciones de colaborar puedes hacerlo:`, `[😉] Comprando un cafe https://www.buymeacoffee.com/leifermendez`, - `[⭐] Dar estrella https://github.com/leifermendez/bot-whatsapp`, + `[⭐] Dar estrella https://github.com/codigoencasa/bot-whatsapp`, `[🚀] Realizando mejoras en el codigo`, ].join('\n') ) @@ -85,7 +85,7 @@ const startInteractive = async () => { const indexOfPath = possiblesPath.find((a) => existsSync(a)) await copyBaseApp(indexOfPath, join(process.cwd(), templateName)) console.log(``) - console.log(bgMagenta(`⚡⚡⚡INSTRUCCIONES⚡⚡⚡`)) + console.log(bgMagenta(`⚡⚡⚡ INSTRUCCIONES ⚡⚡⚡`)) console.log(yellow(`cd ${templateName}`)) console.log(yellow(`npm install`)) console.log(yellow(`npm start`)) diff --git a/packages/cli/package.json b/packages/cli/package.json index 39a11d2..6e76ff6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/cli", - "version": "0.0.30-alpha.0", + "version": "0.0.31-alpha.0", "description": "", "main": "index.js", "devDependencies": { diff --git a/packages/contexts/package.json b/packages/contexts/package.json new file mode 100644 index 0000000..9d27195 --- /dev/null +++ b/packages/contexts/package.json @@ -0,0 +1,16 @@ +{ + "name": "@bot-whatsapp/contexts", + "version": "0.0.1", + "description": "", + "main": "./lib/bundle.contexts.cjs", + "files": [ + "./lib/" + ], + "exports": { + "./mock": "./lib/mock/index.cjs", + "./dialogflow": "./lib/dialogflow/index.cjs" + }, + "dependencies": { + "@bot-whatsapp/bot": "*" + } +} diff --git a/packages/contexts/rollup-contexts.config.js b/packages/contexts/rollup-contexts.config.js new file mode 100644 index 0000000..59a7624 --- /dev/null +++ b/packages/contexts/rollup-contexts.config.js @@ -0,0 +1,24 @@ +const banner = require('../../config/banner.rollup.json') +const commonjs = require('@rollup/plugin-commonjs') +const { join } = require('path') + +module.exports = [ + { + input: join(__dirname, 'src', 'mock', 'index.js'), + output: { + banner: banner['banner.output'].join(''), + file: join(__dirname, 'lib', 'mock', 'index.cjs'), + format: 'cjs', + }, + plugins: [commonjs()], + }, + { + input: join(__dirname, 'src', 'dialogflow', 'index.js'), + output: { + banner: banner['banner.output'].join(''), + file: join(__dirname, 'lib', 'dialogflow', 'index.cjs'), + format: 'cjs', + }, + plugins: [commonjs()], + }, +] diff --git a/packages/contexts/src/dialogflow/dialogflow.class.js b/packages/contexts/src/dialogflow/dialogflow.class.js new file mode 100644 index 0000000..db34e50 --- /dev/null +++ b/packages/contexts/src/dialogflow/dialogflow.class.js @@ -0,0 +1,113 @@ +const { CoreClass } = require('@bot-whatsapp/bot') +const dialogflow = require('@google-cloud/dialogflow') +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') + +/** + * Necesita extender de core.class + * handleMsg(messageInComming) // const { body, from } = messageInComming + */ + +const GOOGLE_ACCOUNT_PATH = join(process.cwd(), 'google-key.json') + +class DialogFlowContext extends CoreClass { + projectId = null + configuration = null + sessionClient = null + optionsDX = { + language: 'es', + } + + constructor(_database, _provider, _optionsDX = {}) { + super(null, _database, _provider) + this.optionsDX = { ...this.optionsDX, ..._optionsDX } + this.init() + } + + /** + * Verificar conexión con servicio de DialogFlow + */ + init = () => { + if (!existsSync(GOOGLE_ACCOUNT_PATH)) { + console.log(`[ERROR]: No se encontro ${GOOGLE_ACCOUNT_PATH}`) + /** + * Emitir evento de error para que se mueste por consola dicinedo que no tiene el json + * */ + } + + const rawJson = readFileSync(GOOGLE_ACCOUNT_PATH, 'utf-8') + const { project_id, private_key, client_email } = JSON.parse(rawJson) + + this.projectId = project_id + this.configuration = { + credentials: { + private_key, + client_email, + }, + } + + this.sessionClient = new dialogflow.SessionsClient(this.configuration) + } + + /** + * GLOSSARY.md + * @param {*} messageCtxInComming + * @returns + */ + handleMsg = async (messageCtxInComming) => { + const languageCode = this.optionsDX.language + const { from, body } = messageCtxInComming + + let customPayload = {} + + /** + * 📄 Creamos session de contexto basado en el numero de la persona + * para evitar este problema. + * https://github.com/codigoencasa/bot-whatsapp/pull/140 + */ + const session = this.sessionClient.projectAgentSessionPath( + this.projectId, + from + ) + const reqDialog = { + session, + queryInput: { + text: { + text: body, + languageCode, + }, + }, + } + + const [single] = (await this.sessionClient.detectIntent(reqDialog)) || [ + null, + ] + + const { queryResult } = single + + const msgPayload = queryResult?.fulfillmentMessages?.find( + (a) => a.message === 'payload' + ) + + // Revisamos si el dialogFlow tiene multimedia + if (msgPayload && msgPayload?.payload) { + const { fields } = msgPayload.payload + const mapButtons = fields?.buttons?.listValue?.values.map((m) => { + return m?.structValue?.fields?.body?.stringValue + }) + customPayload = { + media: fields?.media?.stringValue, + buttons: mapButtons, + } + } + + const ctxFromDX = { + ...customPayload, + answer: queryResult?.fulfillmentText, + } + + this.sendFlow([ctxFromDX], from) + } +} + +module.exports = DialogFlowContext diff --git a/packages/contexts/src/dialogflow/index.js b/packages/contexts/src/dialogflow/index.js new file mode 100644 index 0000000..e988f50 --- /dev/null +++ b/packages/contexts/src/dialogflow/index.js @@ -0,0 +1,14 @@ +const DialogFlowClass = require('./dialogflow.class') + +/** + * Crear instancia de clase Bot + * @param {*} args + * @returns + */ +const createBotDialog = async ({ database, provider }) => + new DialogFlowClass(database, provider) + +module.exports = { + createBotDialog, + DialogFlowClass, +} diff --git a/packages/contexts/src/mock/index.js b/packages/contexts/src/mock/index.js new file mode 100644 index 0000000..b609be5 --- /dev/null +++ b/packages/contexts/src/mock/index.js @@ -0,0 +1,14 @@ +const MockClass = require('./mock.class') + +/** + * Crear instancia de clase Bot + * @param {*} args + * @returns + */ +const createBotMock = async ({ database, provider }) => + new MockClass(database, provider) + +module.exports = { + createBotMock, + MockClass, +} diff --git a/packages/contexts/src/mock/mock.class.js b/packages/contexts/src/mock/mock.class.js new file mode 100644 index 0000000..6e5c9d2 --- /dev/null +++ b/packages/contexts/src/mock/mock.class.js @@ -0,0 +1,24 @@ +const { CoreClass } = require('@bot-whatsapp/bot') +/** + * Necesita extender de core.class + * handleMsg(messageInComming) // const { body, from } = messageInComming + */ + +class MockContext extends CoreClass { + constructor(_database, _provider) { + super(null, _database, _provider) + } + + init = () => {} + + /** + * GLOSSARY.md + * @param {*} messageCtxInComming + * @returns + */ + handleMsg = async () => { + console.log('DEBUG:') + } +} + +module.exports = MockContext diff --git a/packages/create-bot-whatsapp/package.json b/packages/create-bot-whatsapp/package.json index 678ba6c..7cfa3ea 100644 --- a/packages/create-bot-whatsapp/package.json +++ b/packages/create-bot-whatsapp/package.json @@ -1,6 +1,6 @@ { "name": "create-bot-whatsapp", - "version": "0.0.41-alpha.0", + "version": "0.0.42-alpha.0", "description": "", "main": "./lib/bundle.create-bot-whatsapp.cjs", "files": [ diff --git a/packages/database/package.json b/packages/database/package.json index f75a8cc..cfe467d 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -1,6 +1,6 @@ { "name": "@bot-whatsapp/database", - "version": "0.0.22-alpha.0", + "version": "0.0.23-alpha.0", "description": "Esto es el conector a mysql, pg, mongo", "main": "./lib/mock/index.cjs", "keywords": [], @@ -18,7 +18,7 @@ "exports": { "./mock": "./lib/mock/index.cjs", "./mongo": "./lib/mongo/index.cjs", - "./json-file": "./lib/json-file/index.cjs", + "./json": "./lib/json/index.cjs", "./mysql": "./lib/mysql/index.cjs" } } diff --git a/packages/database/rollup-database.config.js b/packages/database/rollup-database.config.js index 1eecdba..844d414 100644 --- a/packages/database/rollup-database.config.js +++ b/packages/database/rollup-database.config.js @@ -31,10 +31,10 @@ module.exports = [ plugins: [commonjs()], }, { - input: join(__dirname, 'src', 'json-file', 'index.js'), + input: join(__dirname, 'src', 'json', 'index.js'), output: { banner: banner['banner.output'].join(''), - file: join(__dirname, 'lib', 'json-file', 'index.cjs'), + file: join(__dirname, 'lib', 'json', 'index.cjs'), }, plugins: [commonjs()], }, diff --git a/packages/database/src/json-file/index.js b/packages/database/src/json/index.js similarity index 88% rename from packages/database/src/json-file/index.js rename to packages/database/src/json/index.js index af53df8..7aa8029 100644 --- a/packages/database/src/json-file/index.js +++ b/packages/database/src/json/index.js @@ -1,8 +1,7 @@ -const path = require('path') const StormDB = require('stormdb') -const engine = new StormDB.localFileEngine( - path.join(process.cwd(), './db.stormdb') -) +const { join } = require('path') + +const engine = new StormDB.localFileEngine(join(process.cwd(), './db.stormdb')) class JsonFileAdapter { db diff --git a/packages/database/src/mongo/index.js b/packages/database/src/mongo/index.js index 21b7cf1..6af68f0 100644 --- a/packages/database/src/mongo/index.js +++ b/packages/database/src/mongo/index.js @@ -1,23 +1,20 @@ -require('dotenv').config() const { MongoClient } = require('mongodb') -const DB_URI = process.env.DB_URI || 'mongodb://0.0.0.0:27017' -const DB_NAME = process.env.DB_NAME || 'db_bot' - class MongoAdapter { db listHistory = [] - - constructor() { + credentials = { dbUri: null, dbName: null } + constructor(_credentials) { + this.credentials = _credentials this.init().then() } init = async () => { try { - const client = new MongoClient(DB_URI, {}) + const client = new MongoClient(this.credentials.dbUri, {}) await client.connect() console.log('🆗 Conexión Correcta DB') - const db = client.db(DB_NAME) + const db = client.db(this.credentials.dbName) this.db = db return true } catch (e) { diff --git a/packages/database/src/mysql/index.js b/packages/database/src/mysql/index.js index a557f3e..e01b55a 100644 --- a/packages/database/src/mysql/index.js +++ b/packages/database/src/mysql/index.js @@ -3,7 +3,7 @@ const mysql = require('mysql2') class MyslAdapter { db listHistory = [] - credentials = { host: null, user: null, database: null } + credentials = { host: null, user: null, database: null, password: null } constructor(_credentials) { this.credentials = _credentials diff --git a/packages/docs/src/routes/index.tsx b/packages/docs/src/routes/index.tsx index 4ae7f96..af63475 100644 --- a/packages/docs/src/routes/index.tsx +++ b/packages/docs/src/routes/index.tsx @@ -13,7 +13,7 @@ export default component$(() => { clientes desde tu cuenta de Whatsapp automáticamente.

- Este bot esta programado en Javascript y usa NodeJS y es{' '} + Este bot esta hecho en Javascript y usa NodeJS y es{' '} Open Source @@ -25,7 +25,7 @@ export default component$(() => { más.

- Si se quere cambiar la librería que se está usando, esto se + Si se quiere cambiar la librería que se está usando, esto se puede hacer con solo cambiar unas lineas en el código.

@@ -183,18 +183,18 @@ export default component$(() => {

Instalación