mirror of
https://github.com/cheveguerra/bot-whatsapp.git
synced 2026-04-18 03:29:15 +00:00
Merge pull request #139 from leifermendez/feature/fallback
Feature/fallback
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -2,7 +2,7 @@ name: Test / Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [feature/monorepo]
|
||||
branches: [dev]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
|
||||
22
.github/workflows/contributors.yml
vendored
Normal file
22
.github/workflows/contributors.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
name: Add contributors
|
||||
on:
|
||||
schedule:
|
||||
- cron: '20 20 * * *'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
add-contributors:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: BobAnkh/add-contributors@master
|
||||
with:
|
||||
CONTRIBUTOR: '### Contributors'
|
||||
COLUMN_PER_ROW: '6'
|
||||
ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
IMG_WIDTH: '100'
|
||||
FONT_SIZE: '14'
|
||||
PATH: '/README.md'
|
||||
COMMIT_MESSAGE: 'docs(README): update contributors'
|
||||
AVATAR_SHAPE: 'round'
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"conventionalCommits.scopes": ["hook", "contributing", "cli"]
|
||||
"conventionalCommits.scopes": ["hook", "contributing", "cli", "bot"]
|
||||
}
|
||||
|
||||
2
.yarn/releases/yarn-3.3.0.cjs
vendored
2
.yarn/releases/yarn-3.3.0.cjs
vendored
@@ -804,4 +804,4 @@ ${a.map(l=>`
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
IN THE SOFTWARE.
|
||||
*/
|
||||
*/
|
||||
2
GLOSSARY.md
Normal file
2
GLOSSARY.md
Normal file
@@ -0,0 +1,2 @@
|
||||
CTX: Es el objeto que representa un mensaje, con opciones, id, ref
|
||||
messageInComming: Objeto entrante del provider {body, from,...}
|
||||
28
TODO.md
28
TODO.md
@@ -4,14 +4,15 @@
|
||||
- [ ] __(doc)__ Video explicacion de github action
|
||||
|
||||
### @bot-whatsapp/bot
|
||||
- [ ] agregar export package
|
||||
- [X] agregar export package
|
||||
- [X] Posibilidad de en el capture meter todo un nuevo CTX de FLOW .addAnswer('Marca la opcion',{capture:true, join:CTX})
|
||||
- [X] .addKeyword('1') no funciona con 1 caracter
|
||||
- [X] sensitivy viene activado por defecto
|
||||
- [ ] fallback respuesta en hijo: Se puede colocar en option el ref de la answer fallback
|
||||
- [X] fallback respuesta en hijo: Se puede colocar en option el ref de la answer fallback
|
||||
- [X] Cuando Envian Sticket devuelve mensaje raro
|
||||
- [ ] colocar mensaje esperando conectando whatsapp (provider)
|
||||
- [ ] Cuando Envian Sticket devuelve mensaje raro
|
||||
- [ ] createDatabase validar implementacion de funciones
|
||||
- [ ] limitar caracteres de mensajes
|
||||
|
||||
### @bot-whatsapp/database
|
||||
- [X] agregar export package
|
||||
@@ -23,10 +24,25 @@
|
||||
### @bot-whatsapp/provider
|
||||
- [X] agregar export package
|
||||
- [ ] __(doc):__ Video para explicar como implementar nuevos providers
|
||||
- [ ] WhatsappWeb provider enviar imagenes
|
||||
- [ ] WhatsappWeb provider enviar audio
|
||||
- [X] WhatsappWeb provider enviar imagenes
|
||||
- [X] WhatsappWeb provider enviar audio
|
||||
- [X] WhatsappWeb botones (Tiene truco) github:leifermendez/whatsapp-web.js
|
||||
- [ ] Twilio adapter
|
||||
- [ ] Meta adapter
|
||||
|
||||
### @bot-whatsapp/cli
|
||||
- [ ] Hacer comando para crear `example-app`
|
||||
- [X] Hacer comando para crear `example-app`
|
||||
|
||||
|
||||
### @bot-whatsapp/create-bot
|
||||
- [ ]
|
||||
|
||||
### Starters
|
||||
- [X] Base
|
||||
- [X] Basico
|
||||
- [ ] Enviando Imagen
|
||||
- [ ] Enviando Botones
|
||||
- [ ] Mezclando flujos hijos
|
||||
|
||||
### Extra
|
||||
- [X] Crear CI mantener fork update https://stackoverflow.com/questions/23793062/can-forks-be-synced-automatically-in-github
|
||||
|
||||
@@ -21,6 +21,9 @@ class CoreClass {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manejador de eventos
|
||||
*/
|
||||
listenerBusEvents = () => [
|
||||
{
|
||||
event: 'require_action',
|
||||
@@ -44,16 +47,18 @@ class CoreClass {
|
||||
]
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {*} ctxMessage
|
||||
*
|
||||
* @param {*} messageInComming
|
||||
* @returns
|
||||
*/
|
||||
handleMsg = async (messageInComming) => {
|
||||
const { body, from } = messageInComming
|
||||
let msgToSend = []
|
||||
let fallBackFlag = false
|
||||
|
||||
if (!body.length) return
|
||||
|
||||
//Consultamos mensaje previo en DB
|
||||
const prevMsg = await this.databaseClass.getPrevByNumber(from)
|
||||
//Consultamos for refSerializada en el flow actual
|
||||
const refToContinue = this.flowClass.findBySerialize(
|
||||
prevMsg?.refSerialize
|
||||
)
|
||||
@@ -67,14 +72,24 @@ class CoreClass {
|
||||
this.databaseClass.save(ctxByNumber)
|
||||
}
|
||||
|
||||
//Si se tiene un callback se ejecuta
|
||||
if (refToContinue && prevMsg?.options?.callback) {
|
||||
const indexFlow = this.flowClass.findIndexByRef(refToContinue?.ref)
|
||||
this.flowClass.allCallbacks[indexFlow].callback(messageInComming)
|
||||
// 📄 [options: fallback]: esta funcion se encarga de repetir el ultimo mensaje
|
||||
const fallBack = () => {
|
||||
fallBackFlag = true
|
||||
msgToSend = this.flowClass.find(refToContinue?.keyword, true) || []
|
||||
this.sendFlow(msgToSend, from)
|
||||
return refToContinue
|
||||
}
|
||||
|
||||
//Si se tiene anidaciones de flows, si tienes anidados obligatoriamente capture:true
|
||||
if (prevMsg?.options?.nested?.length) {
|
||||
// 📄 [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,
|
||||
})
|
||||
}
|
||||
|
||||
// 📄🤘(tiene return) [options: nested(array)]: Si se tiene flujos hijos los implementa
|
||||
if (!fallBackFlag && prevMsg?.options?.nested?.length) {
|
||||
const nestedRef = prevMsg.options.nested
|
||||
const flowStandalone = nestedRef.map((f) => ({
|
||||
...nestedRef.find((r) => r.refSerialize === f.refSerialize),
|
||||
@@ -85,20 +100,32 @@ class CoreClass {
|
||||
return
|
||||
}
|
||||
|
||||
//Consultamos si se espera respuesta por parte de cliente "Ejemplo: Dime tu nombre"
|
||||
if (!prevMsg?.options?.nested?.length && prevMsg?.options?.capture) {
|
||||
msgToSend = this.flowClass.find(refToContinue?.ref, true) || []
|
||||
} else {
|
||||
msgToSend = this.flowClass.find(body) || []
|
||||
// 📄🤘(tiene return) [options: capture (boolean)]: Si se tiene option boolean
|
||||
if (!fallBackFlag && !prevMsg?.options?.nested?.length) {
|
||||
const typeCapture = typeof prevMsg?.options?.capture
|
||||
const valueCapture = prevMsg?.options?.capture
|
||||
|
||||
if (['string', 'boolean'].includes(typeCapture) && valueCapture) {
|
||||
msgToSend = this.flowClass.find(refToContinue?.ref, true) || []
|
||||
this.sendFlow(msgToSend, from)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
msgToSend = this.flowClass.find(body) || []
|
||||
this.sendFlow(msgToSend, from)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar mensaje con contexto atraves del proveedor de whatsapp
|
||||
* @param {*} numberOrId
|
||||
* @param {*} ctxMessage ver más en GLOSSARY.md
|
||||
* @returns
|
||||
*/
|
||||
sendProviderAndSave = (numberOrId, ctxMessage) => {
|
||||
const { answer } = ctxMessage
|
||||
return Promise.all([
|
||||
this.providerClass.sendMessage(numberOrId, answer),
|
||||
this.providerClass.sendMessage(numberOrId, answer, ctxMessage),
|
||||
this.databaseClass.save({ ...ctxMessage, from: numberOrId }),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const { generateRef } = require('../../utils/hash')
|
||||
const { toJson } = require('./toJson')
|
||||
const { toSerialize } = require('./toSerialize')
|
||||
/**
|
||||
*
|
||||
* @param answer string
|
||||
* @param options {media:string, buttons:[], capture:true default false}
|
||||
* @param options {media:string, buttons:[{"body":"😎 Cursos"}], capture:true default false}
|
||||
* @returns
|
||||
*/
|
||||
const addAnswer =
|
||||
@@ -79,6 +78,7 @@ const addAnswer =
|
||||
}
|
||||
}
|
||||
|
||||
/// Retornar contexto no colocar nada más abajo de esto
|
||||
const ctx = ctxAnswer()
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,7 +19,7 @@ class ProviderClass extends EventEmitter {
|
||||
*
|
||||
*/
|
||||
|
||||
sendMessage = async (userId, message) => {
|
||||
sendMessage = async (userId, message, sendMessage) => {
|
||||
if (NODE_ENV !== 'production')
|
||||
console.log('[sendMessage]', { userId, message })
|
||||
return message
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3",
|
||||
"mongodb": "^4.11.0"
|
||||
"mongodb": "^4.11.0",
|
||||
"mysql2": "^2.3.3"
|
||||
},
|
||||
"exports": {
|
||||
"./mock": "./lib/mock/index.cjs",
|
||||
"./mongo": "./lib/mongo/index.cjs"
|
||||
"./mongo": "./lib/mongo/index.cjs",
|
||||
"./mysql": "./lib/mysql/index.cjs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,13 @@ module.exports = [
|
||||
},
|
||||
plugins: [commonjs()],
|
||||
},
|
||||
{
|
||||
input: join(__dirname, 'src', 'mysql', 'index.js'),
|
||||
output: {
|
||||
banner: banner['banner.output'].join(''),
|
||||
file: join(__dirname, 'lib', 'mysql', 'index.cjs'),
|
||||
format: 'cjs',
|
||||
},
|
||||
plugins: [commonjs()],
|
||||
},
|
||||
]
|
||||
|
||||
76
packages/database/src/mysql/index.js
Normal file
76
packages/database/src/mysql/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
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 = []
|
||||
|
||||
constructor() {
|
||||
this.init().then()
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.db = mysql.createConnection({
|
||||
host: DB_HOST,
|
||||
user: DB_USER,
|
||||
database: DB_NAME,
|
||||
})
|
||||
|
||||
await this.db.connect((error) => {
|
||||
if (!error) {
|
||||
console.log(`Solicitud de conexión a base de datos exitosa`)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(`Solicitud de conexión fallida ${error.stack}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getPrevByNumber = (from) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const sql = `SELECT * FROM history WHERE phone=${from} ORDER BY id DESC`
|
||||
this.db.query(sql, (error, rows) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
|
||||
if (rows.length) {
|
||||
const [row] = rows
|
||||
row.options = JSON.parse(row.options)
|
||||
resolve(row)
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
save = (ctx) => {
|
||||
const values = [
|
||||
[
|
||||
ctx.ref,
|
||||
ctx.keyword,
|
||||
ctx.answer,
|
||||
ctx.refSerialize,
|
||||
ctx.from,
|
||||
JSON.stringify(ctx.options),
|
||||
],
|
||||
]
|
||||
const sql =
|
||||
'INSERT INTO history (ref, keyword, answer, refSerialize, phone, options ) values ?'
|
||||
|
||||
this.db.query(sql, [values], (err) => {
|
||||
if (err) throw err
|
||||
console.log('Guardado en DB...', values)
|
||||
})
|
||||
this.listHistory.push(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MyslAdapter
|
||||
@@ -29,10 +29,13 @@
|
||||
"@types/node": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "5.43.0",
|
||||
"@typescript-eslint/parser": "5.43.0",
|
||||
"autoprefixer": "10.4.11",
|
||||
"eslint": "8.28.0",
|
||||
"eslint-plugin-qwik": "0.14.1",
|
||||
"node-fetch": "3.3.0",
|
||||
"postcss": "^8.4.16",
|
||||
"prettier": "2.7.1",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"typescript": "4.9.3",
|
||||
"vite": "3.2.4",
|
||||
"vite-tsconfig-paths": "3.5.0",
|
||||
|
||||
6
packages/docs/postcss.config.js
Normal file
6
packages/docs/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -63,4 +63,4 @@ code {
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
border-bottom: 2px solid #bfbfbf;
|
||||
}
|
||||
}
|
||||
21
packages/docs/tailwind.config.js
Normal file
21
packages/docs/tailwind.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
const defaultTheme = require("tailwindcss/defaultTheme");
|
||||
const colors = require("tailwindcss/colors");
|
||||
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: colors.purple,
|
||||
secondary: colors.sky,
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["'Inter'", ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
darkMode: "class",
|
||||
};
|
||||
@@ -1,13 +1,24 @@
|
||||
const { Client, LocalAuth } = require('whatsapp-web.js')
|
||||
const {
|
||||
Client,
|
||||
LocalAuth,
|
||||
MessageMedia,
|
||||
Buttons,
|
||||
List,
|
||||
} = require('whatsapp-web.js')
|
||||
const { ProviderClass } = require('@bot-whatsapp/bot')
|
||||
const { Console } = require('console')
|
||||
const { createWriteStream } = require('fs')
|
||||
const { createWriteStream, existsSync } = require('fs')
|
||||
const { cleanNumber, generateImage, isValidNumber } = require('./utils')
|
||||
|
||||
const logger = new Console({
|
||||
stdout: createWriteStream('./log'),
|
||||
})
|
||||
|
||||
/**
|
||||
* WebWhatsappProvider: Es una clase tipo adaptor
|
||||
* que extiende clases de ProviderClass (la cual es como interfaz para sber que funciones rqueridas)
|
||||
* https://github.com/pedroslopez/whatsapp-web.js
|
||||
*/
|
||||
class WebWhatsappProvider extends ProviderClass {
|
||||
vendor
|
||||
constructor() {
|
||||
@@ -26,8 +37,9 @@ class WebWhatsappProvider extends ProviderClass {
|
||||
logger.log(e)
|
||||
this.emit('require_action', {
|
||||
instructions: [
|
||||
`Debes eliminar la carpeta .wwebjs_auth`,
|
||||
`y reiniciar nuevamente el bot `,
|
||||
`(Opcion 1): Debes eliminar la carpeta .wwebjs_auth y reiniciar nuevamente el bot. `,
|
||||
`(Opcion 2): Intenta actualizar el paquete [npm install whatsapp-web.js] `,
|
||||
`(Opcion 3): Ir FORO de discord https://link.codigoencasa.com/DISCORD `,
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -80,10 +92,87 @@ class WebWhatsappProvider extends ProviderClass {
|
||||
},
|
||||
]
|
||||
|
||||
sendMessage = async (userId, message) => {
|
||||
const number = cleanNumber(userId)
|
||||
/**
|
||||
* Enviar un archivo multimedia
|
||||
* https://docs.wwebjs.dev/MessageMedia.html
|
||||
* @private
|
||||
* @param {*} number
|
||||
* @param {*} mediaInput
|
||||
* @returns
|
||||
*/
|
||||
sendMedia = async (number, mediaInput = null) => {
|
||||
if (!existsSync(mediaInput))
|
||||
throw new Error(`NO_SE_ENCONTRO: ${mediaInput}`)
|
||||
const media = MessageMedia.fromFilePath(mediaInput)
|
||||
return this.vendor.sendMessage(number, media, {
|
||||
sendAudioAsVoice: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar botones
|
||||
* https://docs.wwebjs.dev/Buttons.html
|
||||
* @private
|
||||
* @param {*} number
|
||||
* @param {*} message
|
||||
* @param {*} buttons []
|
||||
* @returns
|
||||
*/
|
||||
sendButtons = async (number, message, buttons = []) => {
|
||||
const buttonMessage = new Buttons(message, buttons, '', '')
|
||||
return this.vendor.sendMessage(number, buttonMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar lista
|
||||
* https://docs.wwebjs.dev/List.html
|
||||
* @private
|
||||
* @alpha No funciona en whatsapp bussines
|
||||
* @param {*} number
|
||||
* @param {*} message
|
||||
* @param {*} buttons []
|
||||
* @returns
|
||||
*/
|
||||
sendList = async (number, message, listInput = []) => {
|
||||
let sections = [
|
||||
{
|
||||
title: 'sectionTitle',
|
||||
rows: [
|
||||
{ title: 'ListItem1', description: 'desc' },
|
||||
{ title: 'ListItem2' },
|
||||
],
|
||||
},
|
||||
]
|
||||
let list = new List('List body', 'btnText', sections, 'Title', 'footer')
|
||||
return this.vendor.sendMessage(number, list)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar un mensaje solo texto
|
||||
* https://docs.wwebjs.dev/Message.html
|
||||
* @private
|
||||
* @param {*} number
|
||||
* @param {*} message
|
||||
* @returns
|
||||
*/
|
||||
sendText = async (number, message) => {
|
||||
return this.vendor.sendMessage(number, message)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} userId
|
||||
* @param {*} message
|
||||
* @param {*} param2
|
||||
* @returns
|
||||
*/
|
||||
sendMessage = async (userId, message, { options }) => {
|
||||
const number = cleanNumber(userId)
|
||||
if (options?.media) return this.sendMedia(number, options.media)
|
||||
if (options?.buttons?.length)
|
||||
return this.sendButtons(number, message, options.buttons)
|
||||
return this.sendText(number, message)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebWhatsappProvider
|
||||
|
||||
@@ -5,11 +5,6 @@ const {
|
||||
addKeyword,
|
||||
} = require('@bot-whatsapp/bot')
|
||||
|
||||
/**
|
||||
* ATENCION: Si vas a usar el provider whatsapp-web.js
|
||||
* recuerda ejecutar npm i whatsapp-web.js@latest
|
||||
*/
|
||||
|
||||
const WebWhatsappProvider = require('@bot-whatsapp/provider/web-whatsapp')
|
||||
const MockAdapter = require('@bot-whatsapp/database/mock')
|
||||
|
||||
|
||||
@@ -6,11 +6,6 @@ const {
|
||||
addChild,
|
||||
} = require('@bot-whatsapp/bot')
|
||||
|
||||
/**
|
||||
* ATENCION: Si vas a usar el provider whatsapp-web.js
|
||||
* recuerda ejecutar npm i whatsapp-web.js@latest
|
||||
*/
|
||||
|
||||
const WebWhatsappProvider = require('@bot-whatsapp/provider/web-whatsapp')
|
||||
const MockAdapter = require('@bot-whatsapp/database/mock')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user