diff --git a/.gitignore b/.gitignore index 60be892..d5e4ade 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ config.json coverage/ *.lcov log +log/* +*.log lib tmp/ .yarn/* diff --git a/.vscode/settings.json b/.vscode/settings.json index e286082..d35c0e4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "conventionalCommits.scopes": ["hook", "contributing", "cli", "bot"] + "conventionalCommits.scopes": [ + "hook", + "contributing", + "cli", + "bot", + "provider" + ] } diff --git a/TODO.md b/TODO.md index b5c0a4c..553235c 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ - [X] __(doc)__ Video de como colaborar PR - [ ] __(doc)__ Video implementación de test y cobertura - [ ] __(doc)__ Video explicacion de github action +- [ ] Crear packages list externas ### @bot-whatsapp/bot - [X] agregar export package @@ -10,15 +11,17 @@ - [X] sensitivy viene activado por defecto - [X] fallback respuesta en hijo: Se puede colocar en option el ref de la answer fallback - [X] Cuando Envian Sticket devuelve mensaje raro +- [x] addAnswer agregar delay - [ ] colocar mensaje esperando conectando whatsapp (provider) - [ ] createDatabase validar implementacion de funciones -- [ ] limitar caracteres de mensajes +- [ ] limitar caracteres de mensajes 4000 +- [X] cuando envias numeros (5 o 1) se dispara el flujo ### @bot-whatsapp/database - [X] agregar export package - [X] __(doc):__ Video para explicar como implementar nuevos database - [X] Mongo adapter -- [ ] MySQL adapter +- [X] MySQL adapter - [ ] JsonFile adapter ### @bot-whatsapp/provider diff --git a/package.json b/package.json index 062be61..9515320 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "commit": "git-cz", "cli:rollup": "rollup --config ./packages/cli/rollup-cli.config.js ", + "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 ", "database:rollup": "rollup --config ./packages/database/rollup-database.config.js", @@ -16,12 +17,12 @@ "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", - "link.dist": "cd packages/bot && npm link && cd ../provider && npm link && cd ../cli && npm link && cd ../database && npm link && cd ../provider && npm link", "copy.lib": "node ./scripts/move.js", "test.unit": "node ./node_modules/uvu/bin.js packages test", "test.coverage": "node ./node_modules/c8/bin/c8.js npm run test.unit", "test": "npm run test.coverage", "cli": "node ./packages/cli/bin/cli.js", + "create": "node ./packages/create-bot-whatsapp/bin/create.js", "dev:debug": "node --inspect ./example-app/app.js", "dev": "node ./example-app/app.js", "prepare": "npx husky install", @@ -30,6 +31,7 @@ "release": "standard-version" }, "workspaces": [ + "packages/create-bot-whatsapp", "packages/bot", "packages/cli", "packages/database", diff --git a/packages/bot/core/core.class.js b/packages/bot/core/core.class.js index b1e2fbc..f7726c8 100644 --- a/packages/bot/core/core.class.js +++ b/packages/bot/core/core.class.js @@ -1,6 +1,13 @@ const { toCtx } = require('../io/methods') const { printer } = require('../utils/interactive') +const { delay } = require('../utils/delay') +const Queue = require('../utils/queue') +const { Console } = require('console') +const { createWriteStream } = require('fs') +const logger = new Console({ + stdout: createWriteStream(`${process.cwd()}/core.class.log`), +}) /** * [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos * [ ] Guardar historial en db @@ -25,6 +32,10 @@ class CoreClass { * Manejador de eventos */ listenerBusEvents = () => [ + { + event: 'preinit', + func: () => printer('Iniciando provider espere...'), + }, { event: 'require_action', func: ({ instructions, title = '⚡⚡ ACCION REQUERIDA ⚡⚡' }) => @@ -52,6 +63,7 @@ class CoreClass { * @returns */ handleMsg = async (messageInComming) => { + logger.log(`[handleMsg]: `, messageInComming) const { body, from } = messageInComming let msgToSend = [] let fallBackFlag = false @@ -130,10 +142,14 @@ class CoreClass { ]) } - sendFlow = (messageToSend, numberOrId) => { + sendFlow = async (messageToSend, numberOrId) => { const queue = [] for (const ctxMessage of messageToSend) { - queue.push(this.sendProviderAndSave(numberOrId, ctxMessage)) + const delayMs = ctxMessage?.options?.delay || 0 + if (delayMs) await delay(delayMs) + Queue.enqueue(() => + this.sendProviderAndSave(numberOrId, ctxMessage) + ) } return Promise.all(queue) } diff --git a/packages/bot/index.js b/packages/bot/index.js index 5972d61..eb9df24 100644 --- a/packages/bot/index.js +++ b/packages/bot/index.js @@ -22,11 +22,13 @@ const createFlow = (args) => { /** * Crear instancia de clase Provider + * Depdendiendo del Provider puedes pasar argumentos + * Ver Documentacion * @param {*} args * @returns */ -const createProvider = (providerClass = class {}) => { - const providerInstance = new providerClass() +const createProvider = (providerClass = class {}, args = null) => { + const providerInstance = new providerClass(args) if (!providerClass.prototype instanceof ProviderClass) throw new Error('El provider no implementa ProviderClass') return providerInstance diff --git a/packages/bot/io/flow.class.js b/packages/bot/io/flow.class.js index 5ed8eb4..2c18268 100644 --- a/packages/bot/io/flow.class.js +++ b/packages/bot/io/flow.class.js @@ -21,29 +21,25 @@ class FlowClass { } find = (keyOrWord, symbol = false, overFlow = null) => { + keyOrWord = `${keyOrWord}` let capture = false let messages = [] let refSymbol = null overFlow = overFlow ?? this.flowSerialize - const mapSensitiveString = (str, flag = false) => { - if (!flag && Array.isArray(str)) { - return str.map((c) => c.toLowerCase()) + /** Retornar expresion regular para buscar coincidencia */ + const mapSensitive = (str, flag = false) => { + const regexSensitive = flag ? 'g' : 'i' + if (Array.isArray(str)) { + return new RegExp(str.join('|'), regexSensitive) } - - if (!flag && typeof str === 'string') { - return str.toLowerCase() - } - - return str + return new RegExp(str, regexSensitive) } const findIn = (keyOrWord, symbol = false, flow = overFlow) => { const sensitive = refSymbol?.options?.sensitive || false capture = refSymbol?.options?.capture || false - keyOrWord = mapSensitiveString(keyOrWord, sensitive) - if (capture) return messages if (symbol) { @@ -51,9 +47,9 @@ class FlowClass { if (refSymbol?.answer) messages.push(refSymbol) if (refSymbol?.ref) findIn(refSymbol.ref, true) } else { - refSymbol = flow.find((c) => - mapSensitiveString(c.keyword, sensitive).includes(keyOrWord) - ) + refSymbol = flow.find((c) => { + return mapSensitive(c.keyword, sensitive).test(keyOrWord) + }) if (refSymbol?.ref) findIn(refSymbol.ref, true) return messages } diff --git a/packages/bot/io/methods/addAnswer.js b/packages/bot/io/methods/addAnswer.js index 891a6a8..d5b53e6 100644 --- a/packages/bot/io/methods/addAnswer.js +++ b/packages/bot/io/methods/addAnswer.js @@ -3,7 +3,7 @@ const { toJson } = require('./toJson') /** * * @param answer string - * @param options {media:string, buttons:[{"body":"😎 Cursos"}], capture:true default false} + * @param options {media:string, buttons:[{"body":"😎 Cursos"}], delay:ms, capture:true default false} * @returns */ const addAnswer = @@ -24,6 +24,7 @@ const addAnswer = : false, child: typeof options?.child === 'string' ? `${options?.child}` : null, + delay: typeof options?.delay === 'number' ? options?.delay : 0, }) const getNested = () => ({ diff --git a/packages/bot/utils/delay.js b/packages/bot/utils/delay.js new file mode 100644 index 0000000..021fafe --- /dev/null +++ b/packages/bot/utils/delay.js @@ -0,0 +1,4 @@ +const delay = (miliseconds) => + new Promise((res) => setTimeout(res, miliseconds)) + +module.exports = { delay } diff --git a/packages/bot/utils/queue.js b/packages/bot/utils/queue.js new file mode 100644 index 0000000..1f610e9 --- /dev/null +++ b/packages/bot/utils/queue.js @@ -0,0 +1,46 @@ +class Queue { + static queue = [] + static pendingPromise = false + + static enqueue(promise) { + return new Promise((resolve, reject) => { + this.queue.push({ + promise, + resolve, + reject, + }) + this.dequeue() + }) + } + + static dequeue() { + if (this.workingOnPromise) { + return false + } + const item = this.queue.shift() + if (!item) { + return false + } + try { + this.workingOnPromise = true + item.promise() + .then((value) => { + this.workingOnPromise = false + item.resolve(value) + this.dequeue() + }) + .catch((err) => { + this.workingOnPromise = false + item.reject(err) + this.dequeue() + }) + } catch (err) { + this.workingOnPromise = false + item.reject(err) + this.dequeue() + } + return true + } +} + +module.exports = Queue diff --git a/packages/create-bot-whatsapp/bin/create.js b/packages/create-bot-whatsapp/bin/create.js new file mode 100644 index 0000000..546eca2 --- /dev/null +++ b/packages/create-bot-whatsapp/bin/create.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +const main = require('../lib/bin/bundle.create.cjs') +main() diff --git a/packages/create-bot-whatsapp/index.js b/packages/create-bot-whatsapp/index.js new file mode 100644 index 0000000..7df7d03 --- /dev/null +++ b/packages/create-bot-whatsapp/index.js @@ -0,0 +1,12 @@ +/** + * Main function + */ +const main = () => { + console.clear() + console.log(``) + console.log(`[PostInstall]: Este es el main function.`) + console.log(`[PostInstall]: 👌 Aqui podrias instalar cosas`) + console.log(``) +} + +module.exports = main diff --git a/packages/create-bot-whatsapp/package.json b/packages/create-bot-whatsapp/package.json new file mode 100644 index 0000000..65926f8 --- /dev/null +++ b/packages/create-bot-whatsapp/package.json @@ -0,0 +1,13 @@ +{ + "name": "create-bot-whatsapp", + "version": "0.0.1", + "description": "", + "main": "./lib/bin/bundle.create.cjs", + "private": true, + "dependencies": { + "@bot-whatsapp/cli": "*" + }, + "bin": { + "bot": "./lib/bin/bundle.create.cjs" + } +} diff --git a/packages/create-bot-whatsapp/rollup-create.config.js b/packages/create-bot-whatsapp/rollup-create.config.js new file mode 100644 index 0000000..54f93f1 --- /dev/null +++ b/packages/create-bot-whatsapp/rollup-create.config.js @@ -0,0 +1,16 @@ +const banner = require('../../config/banner.rollup.json') +const commonjs = require('@rollup/plugin-commonjs') +const { nodeResolve } = require('@rollup/plugin-node-resolve') +const { join } = require('path') + +const PATH = join(__dirname, 'lib', 'bin', 'bundle.create.cjs') + +module.exports = { + input: join(__dirname, 'index.js'), + output: { + banner: banner['banner.output'].join(''), + file: PATH, + format: 'cjs', + }, + plugins: [commonjs(), nodeResolve()], +} diff --git a/packages/database/src/mysql/index.js b/packages/database/src/mysql/index.js index dfeb22e..63e6e7d 100644 --- a/packages/database/src/mysql/index.js +++ b/packages/database/src/mysql/index.js @@ -1,24 +1,17 @@ -require('dotenv').config() const mysql = require('mysql2') -const DB_NAME = process.env.DB_NAME || 'db_bot' -const DB_HOST = process.env.DB_HOST || 'localhost' -const DB_USER = process.env.DB_USER || 'root' - class MyslAdapter { db listHistory = [] + credentials = { host: null, user: null, database: null } - constructor() { + constructor(_credentials) { + this.credentials = _credentials this.init().then() } async init() { - this.db = mysql.createConnection({ - host: DB_HOST, - user: DB_USER, - database: DB_NAME, - }) + this.db = mysql.createConnection(this.credentials) await this.db.connect((error) => { if (!error) { diff --git a/packages/provider/TODO.md b/packages/provider/TODO.md deleted file mode 100644 index 1e2b948..0000000 --- a/packages/provider/TODO.md +++ /dev/null @@ -1,13 +0,0 @@ -# @bot-whatsapp/provider - -```js -// bootstrap.js Como iniciar el provider -const { inout, provider, database } = require('@bot-whatsapp') - -provider.start() -provider.close() -``` - -- [ ] whatsapp-web.js _verificar update_ -- [ ] Meta _verificar tokens_ -- [ ] Twilio _verificar tokens_ diff --git a/packages/provider/src/twilio/index.js b/packages/provider/src/twilio/index.js index 41795b1..973cc48 100644 --- a/packages/provider/src/twilio/index.js +++ b/packages/provider/src/twilio/index.js @@ -1,19 +1,59 @@ const twilio = require('twilio') const { ProviderClass } = require('@bot-whatsapp/bot') -const TwilioVendor = new twilio(accountSid, authToken) +const TwilioWebHookServer = require('./server') +const { parseNumber } = require('./utils') +/** + * { accountSid, authToken, vendorNumber } + */ class TwilioProvider extends ProviderClass { - constructor() { - super(TwilioVendor) + twilioHook + vendor + vendorNumber + constructor({ accountSid, authToken, vendorNumber }, _port = 3000) { + super() + this.vendor = new twilio(accountSid, authToken) + this.twilioHook = new TwilioWebHookServer(_port) + this.vendorNumber = vendorNumber + + this.twilioHook.start() + const listEvents = this.busEvents() + + for (const { event, func } of listEvents) { + this.twilioHook.on(event, func) + } } - sendMessage = (message) => - this.vendor.messages.create({ + sendMessage = async (number, message) => { + return this.vendor.messages.create({ body: message, - to: '+12345678901', // Text this number - from: '+12345678901', // From a valid Twilio number + from: ['whatsapp:+', parseNumber(this.vendorNumber)].join(''), + to: ['whatsapp:+', parseNumber(number)].join(''), }) + } + + /** + * Mapeamos los eventos nativos de whatsapp-web.js a los que la clase Provider espera + * para tener un standar de eventos + * @returns + */ + busEvents = () => [ + { + event: 'auth_failure', + func: (payload) => this.emit('error', payload), + }, + { + event: 'ready', + func: () => this.emit('ready', true), + }, + { + event: 'message', + func: (payload) => { + this.emit('message', payload) + }, + }, + ] } module.exports = TwilioProvider diff --git a/packages/provider/src/twilio/server.js b/packages/provider/src/twilio/server.js new file mode 100644 index 0000000..d208232 --- /dev/null +++ b/packages/provider/src/twilio/server.js @@ -0,0 +1,63 @@ +const { EventEmitter } = require('node:events') +const polka = require('polka') +const { urlencoded } = require('body-parser') +const { parseNumber } = require('./utils') + +/** + * Encargado de levantar un servidor HTTP con una hook url + * [POST] /twilio-hook + */ +class TwilioWebHookServer extends EventEmitter { + twilioServer + twilioPort + constructor(_twilioPort) { + this.twilioServer = this.buildHTTPServer() + this.twilioPort = _twilioPort + } + + /** + * Mensaje entrante + * emit: 'message' + * @param {*} req + * @param {*} res + */ + incomingMsg = (req, res) => { + const { body } = req + this.emit('message', { + from: parseNumber(body.From), + to: parseNumber(body.To), + body: body.Body, + }) + const json = JSON.stringify({ body }) + res.end(json) + } + + /** + * Contruir HTTP Server + * @returns + */ + buildHTTPServer = () => { + return polka() + .use(urlencoded({ extended: true })) + .post('/twilio-hook', this.incomingMsg) + } + + /** + * Puerto del HTTP + * @param {*} port default 3000 + */ + start = () => { + this.twilioServer.listen(this.twilioPort, () => { + console.log(``) + console.log(`[Twilio]: Agregar esta url "WHEN A MESSAGE COMES IN"`) + console.log( + `[Twilio]: http://localhost:${this.twilioPort}/twilio-hook` + ) + console.log(`[Twilio]: Más información en la documentacion`) + console.log(``) + }) + this.emit('ready') + } +} + +module.exports = TwilioWebHookServer diff --git a/packages/provider/src/twilio/utils.js b/packages/provider/src/twilio/utils.js new file mode 100644 index 0000000..e620c4e --- /dev/null +++ b/packages/provider/src/twilio/utils.js @@ -0,0 +1,5 @@ +const parseNumber = (number) => { + return `${number}`.replace('whatsapp:', '').replace('+', '') +} + +module.exports = { parseNumber } diff --git a/packages/provider/COMMON_MISTAKE.md b/packages/provider/src/web-whatsapp/COMMON_MISTAKE.md similarity index 100% rename from packages/provider/COMMON_MISTAKE.md rename to packages/provider/src/web-whatsapp/COMMON_MISTAKE.md diff --git a/packages/provider/src/web-whatsapp/index.js b/packages/provider/src/web-whatsapp/index.js index a010da6..463987c 100644 --- a/packages/provider/src/web-whatsapp/index.js +++ b/packages/provider/src/web-whatsapp/index.js @@ -32,7 +32,7 @@ class WebWhatsappProvider extends ProviderClass { for (const { event, func } of listEvents) { this.vendor.on(event, func) } - + this.vendor.emit('preinit') this.vendor.initialize().catch((e) => { logger.log(e) this.emit('require_action', { @@ -72,10 +72,6 @@ class WebWhatsappProvider extends ProviderClass { event: 'ready', func: () => this.emit('ready', true), }, - { - event: 'authenticated', - func: () => this.emit('ready', true), - }, { event: 'message', func: (payload) => { diff --git a/scripts/move.js b/scripts/move.js index 46dd625..5a0d8b6 100644 --- a/scripts/move.js +++ b/scripts/move.js @@ -11,6 +11,7 @@ const copyLibPkg = async (pkgName, to) => { } Promise.all([ + copyLibPkg('create-bot-whatsapp', appDir), copyLibPkg('bot', appDir), copyLibPkg('database', appDir), copyLibPkg('provider', appDir), diff --git a/yarn.lock b/yarn.lock index 8d6a213..a62b2db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3136,6 +3136,16 @@ __metadata: languageName: node linkType: hard +"create-bot-whatsapp@workspace:packages/create-bot-whatsapp": + version: 0.0.0-use.local + resolution: "create-bot-whatsapp@workspace:packages/create-bot-whatsapp" + dependencies: + "@bot-whatsapp/cli": "*" + bin: + bot: ./lib/bin/bundle.create.cjs + languageName: unknown + linkType: soft + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1"