mirror of
https://github.com/cheveguerra/bot-whatsapp.git
synced 2026-04-17 19:26:23 +00:00
Merge branch 'fix/delay-message' into dev
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,6 +18,8 @@ config.json
|
||||
coverage/
|
||||
*.lcov
|
||||
log
|
||||
log/*
|
||||
*.log
|
||||
lib
|
||||
tmp/
|
||||
.yarn/*
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -1,3 +1,9 @@
|
||||
{
|
||||
"conventionalCommits.scopes": ["hook", "contributing", "cli", "bot"]
|
||||
"conventionalCommits.scopes": [
|
||||
"hook",
|
||||
"contributing",
|
||||
"cli",
|
||||
"bot",
|
||||
"provider"
|
||||
]
|
||||
}
|
||||
|
||||
7
TODO.md
7
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = () => ({
|
||||
|
||||
4
packages/bot/utils/delay.js
Normal file
4
packages/bot/utils/delay.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const delay = (miliseconds) =>
|
||||
new Promise((res) => setTimeout(res, miliseconds))
|
||||
|
||||
module.exports = { delay }
|
||||
46
packages/bot/utils/queue.js
Normal file
46
packages/bot/utils/queue.js
Normal file
@@ -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
|
||||
3
packages/create-bot-whatsapp/bin/create.js
Normal file
3
packages/create-bot-whatsapp/bin/create.js
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
const main = require('../lib/bin/bundle.create.cjs')
|
||||
main()
|
||||
12
packages/create-bot-whatsapp/index.js
Normal file
12
packages/create-bot-whatsapp/index.js
Normal file
@@ -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
|
||||
13
packages/create-bot-whatsapp/package.json
Normal file
13
packages/create-bot-whatsapp/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
16
packages/create-bot-whatsapp/rollup-create.config.js
Normal file
16
packages/create-bot-whatsapp/rollup-create.config.js
Normal file
@@ -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()],
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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_
|
||||
@@ -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
|
||||
|
||||
63
packages/provider/src/twilio/server.js
Normal file
63
packages/provider/src/twilio/server.js
Normal file
@@ -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
|
||||
5
packages/provider/src/twilio/utils.js
Normal file
5
packages/provider/src/twilio/utils.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const parseNumber = (number) => {
|
||||
return `${number}`.replace('whatsapp:', '').replace('+', '')
|
||||
}
|
||||
|
||||
module.exports = { parseNumber }
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
10
yarn.lock
10
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"
|
||||
|
||||
Reference in New Issue
Block a user