Compare commits

..

15 Commits

Author SHA1 Message Date
Leifer Mendez
4819dfebda Create config.yml 2022-12-14 10:23:24 +01:00
Leifer Mendez
29fd94a0e1 Update bug.yml 2022-12-14 10:18:07 +01:00
Leifer Mendez
851052b3d2 Update bug.yml 2022-12-14 10:12:21 +01:00
Leifer Mendez
a43444bb3e Create bug.yml 2022-12-14 08:02:08 +01:00
Leifer Mendez
c2a6c6872d Create PULL_REQUEST_TEMPLATE.md 2022-12-14 07:59:13 +01:00
Leifer Mendez
f0a0bacec9 Merge pull request #191 from leifermendez/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2022-12-12 16:42:40 +01:00
Leifer Mendez
ec32dc429f Create CODE_OF_CONDUCT.md 2022-12-12 16:42:12 +01:00
Leifer Mendez
5303dd7d25 Create FUNDING.yml 2022-12-12 00:44:58 +01:00
Leifer Mendez
0246fc0b91 Merge pull request #171 from leifermendez/add-license-1
Create LICENSE.md
2022-12-11 22:21:00 +01:00
Leifer Mendez
2858e910dc Create LICENSE.md 2022-12-11 22:20:49 +01:00
Leifer Mendez
ee87e9e875 Merge pull request #140 from jzvi12/main
Use WhatsApp Number as an Unique Dialogflow Session ID
2022-12-06 09:57:10 +01:00
jzvi12
1884832192 fixed dialogflow session id 2022-12-05 19:10:41 -05:00
jzvi12
f21a58b6ff Update README.md 2022-12-02 10:16:16 -05:00
Leifer Mendez
8a4f134327 Update latest 2022-11-19 11:08:51 +01:00
Leifer Mendez
9b6ce92612 Update diaglogflow.js 2022-10-31 09:51:18 +01:00
140 changed files with 10954 additions and 8526 deletions

View File

@@ -1,8 +0,0 @@
{
"src": "./src",
"exclude": ["**/bot/lib", "__mocks__", "**/mock"],
"reporter": ["html"],
"report-dir": "./coverage",
"check-coverage": true,
"lines": 95
}

13
.env.example Normal file
View File

@@ -0,0 +1,13 @@
######DATABASE: none, mysql, dialogflow
DEFAULT_MESSAGE=true
SAVE_MEDIA=true
PORT=3000
DATABASE=none
LANGUAGE=es
SQL_HOST=
SQL_USER=
SQL_PASS=
SQL_DATABASE=
KEEP_DIALOG_FLOW=false
MULTI_DEVICE=true

View File

@@ -1,14 +0,0 @@
module.exports = {
env: {
browser: true,
commonjs: true,
es2021: true,
node: true,
},
extends: 'eslint:recommended',
overrides: [],
parserOptions: {
ecmaVersion: 'latest',
},
rules: {},
}

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
patreon: leifermendez
custom: "https://www.buymeacoffee.com/leifermendez"

58
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: 🐛 Reporte Bug
description: Algo no va bien?. Hazlo saber
labels: [bug, triage]
title: '[🐛]'
body:
- type: markdown
attributes:
value: |
Gracias por tomarte el tiempo de reportar este problema
- type: dropdown
id: version
attributes:
label: ¿Que versión estas usando?
description: '__INFO:__ Recuerda que puedes consultar dudas directamente en [discord](https://link.codigoencasa.com/DISCORD)'
options:
- v2
- v1
validations:
required: true
- type: dropdown
id: component
attributes:
label: ¿Sobre que afecta?
options:
- Flujo de palabras (Flow)
- DialogFlow
- Base de datos
- Otro
validations:
required: true
- type: textarea
id: description
attributes:
description: 'Trata de ser lo más claro posible, de esa manera podemos entender el contexto de tu problema y darte una mejor solución'
label: Describe tu problema
placeholder: Yo tengo un problema....
validations:
required: true
- type: input
id: reproduction
attributes:
label: Reproducir error
description: __(Recomendación)__ trata de grabar un video puedes usar algunas de las siguientes herramientas [https://www.vidyard.com/](https://www.vidyard.com/) [https://www.loom.com/](https://www.loom.com/) y en lo posbile apoyate en [https://stackblitz.com/](https://stackblitz.com/) para compartir el código de ser necesario
placeholder: URL video o stackblitz
validations:
required: false
- type: textarea
id: additional_information
attributes:
label: Información Adicional
validations:
required: false

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
contact_links:
- name: 🤔 Core Team
url: https://link.codigoencasa.com/DISCORD
about: Si quieres formar parte del CoreTeam, patrocinar el proyecto o propuesta profesionales

23
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,23 @@
# Cual version del BOT es?
- [ ] v2
- [ ] v1
# Que tipo de ISSUE es?
- [ ] Mejoras
- [ ] Bug
- [ ] Docs / tests
# Description
Por favor agrega una descripción de tu problema para tener más contexto y poder ayudar más rápido. Si es de ayuda puedes usar plataformar como [https://www.loom.com/](https://www.loom.com/) para grabar un video.
> Forma parte de este proyecto.
- [Discord](https://link.codigoencasa.com/DISCORD)
- [Twitter](https://twitter.com/leifermendez)
- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR)
- [Telegram](https://t.me/leifermendez)

View File

@@ -1,26 +0,0 @@
name: Test / Coverage
on:
push:
branches: [feature/monorepo]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run build --if-present
- run: npm run test.unit
- run: npm run test.coverage

17
.gitignore vendored
View File

@@ -1,7 +1,5 @@
/node_modules /node_modules
/packages/*/node_modules /node_modules/*
/packages/*/dist
/packages/*/docs/dist
session.json session.json
chats/* chats/*
!chats/.gitkeep !chats/.gitkeep
@@ -11,15 +9,4 @@ mediaSend/*
!mediaSend/.gitkeep !mediaSend/.gitkeep
!mediaSend/nota-de-voz.mp3 !mediaSend/nota-de-voz.mp3
.env .env
.wwebjs_auth .wwebjs_auth
packages/cli/config.json
config.json
coverage/
*.lcov
log
lib
tmp/
.fleet/
example-app/
qr.svg
package-lock.json

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn run format:check && yarn run format:write && git add .

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run test

View File

@@ -1,9 +0,0 @@
packages/**/lib
packages/docs
**/.git
**/.svn
**/.hg
**/node_modules
*.mjs
*.cjs
*.md

View File

@@ -1,6 +1 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

15
.vscode/launch.json vendored
View File

@@ -1,15 +0,0 @@
{
// Use IntelliSense para saber los atributos posibles.
// Mantenga el puntero para ver las descripciones de los existentes atributos.
// Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Iniciar el programa",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}\\example-app\\app.js"
}
]
}

View File

@@ -1,15 +1,14 @@
#### Actualización 14 Ene 2022 #### Actualización 14 Ene 2022
- npm update
- remove ora and chalk
- add env
- add mysql
- add dialogflow
- add scan qr from webpage
- update route with middleware
- fix send message to story
- external download
- easy deploy heroku
- add support for ubuntu/linux
- npm update https://stackoverflow.com/questions/51855169/dialogflow-403-iam-permission-dialogflow-sessions-detectintent
- remove ora and chalk
- add env
- add mysql
- add dialogflow
- add scan qr from webpage
- update route with middleware
- fix send message to story
- external download
- easy deploy heroku
- add support for ubuntu/linux
https://stackoverflow.com/questions/51855169/dialogflow-403-iam-permission-dialogflow-sessions-detectintent

128
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
leifer.contacto@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,87 +0,0 @@
```js
const {
createBot,
createProvider,
createFlow,
addKeyword,
toSerialize,
} = require('@bot-whatsapp/bot')
const WebWhatsappProvider = require('@bot-whatsapp/provider/web-whatsapp')
const MongoAdapter = require('@bot-whatsapp/database/mongo')
const flowArepa1 = toSerialize(
addKeyword(['1', 'AREPA14'])
.addAnswer('Esta es una arepa calificada ⭐⭐⭐⭐⭐')
.addAnswer(['Ingredientes:', '10g Aguacate', '20g Huevo'].join('\n'))
.toJson()
)
const flowArepa2_2 = toSerialize(
addKeyword('SI').addAnswer('te pongo huevo de mentira!').toJson()
)
const flowArepa2 = toSerialize(
addKeyword(['arepa2'])
.addAnswer('Esta es una arepa calificada ⭐⭐⭐⭐')
.addAnswer(
['Ingredientes:', '10g perico', '20g huevo', '10g queso'].join('\n')
)
.addAnswer(
'Eres Vegano SI o NO',
{
capture: true,
},
null,
[...flowArepa2_2]
)
.toJson()
)
const flowArepa3 = toSerialize(
addKeyword(['arepa3'])
.addAnswer('Esta es una arepa calificada LAMEJOR ⭐⭐⭐⭐⭐')
.toJson()
)
//////////////--MENU--PRINCIPAL--//////////////////
const flujoMenuArepa = addKeyword(['hola', 'ola', 'buenos'])
.addAnswer('Bienvenido "Arepera Aji Picante 🤯🚀😅"')
.addAnswer(
[
'El menú de hoy es el siguiente:',
'👉 [1 -AREPA14] - Arepa tradicional con Aguacate y Huevo',
'👉 [arepa2] - Arepa rellena de perico y huevo con un toque de queso',
'👉 [arepa3] - Rellena de Jamon y Queso',
].join('\n')
)
.addAnswer(
'Esperando respuesta...',
{
capture: true,
},
() => {
console.log('Enviar un mail!')
},
[...flowArepa1, ...flowArepa2, ...flowArepa3]
)
.addAnswer('Gracias!')
const main = async () => {
const adapterDB = new MongoAdapter()
const adapterFlow = createFlow([flujoMenuArepa])
const adapterProvider = createProvider(WebWhatsappProvider)
createBot({
flow: adapterFlow,
provider: adapterProvider,
database: adapterDB,
})
}
main()
```

21
LICENSE.md Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Leifer Mendez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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.

136
README.md
View File

@@ -1,20 +1,116 @@
[![Test / Coverage](https://github.com/leifermendez/bot-whatsapp/actions/workflows/ci.yml/badge.svg)](https://github.com/leifermendez/bot-whatsapp/actions/workflows/ci.yml) ## Chatbot Whatsapp (OpenSource)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
#### Actualización
🦊 Documentación: [https://bot-whatsapp.pages.dev/](https://bot-whatsapp.pages.dev/)
Video como hacer PR: https://youtu.be/Lxt8Acob6aU | Feature | Status |
| ------------- | ------------- |
- [ ] Evitar dependencias | Dialogflow | ✅ |
| MySQL | ✅ |
| JSON File | ✅ |
**Comunidad** | QR Scan (route) | ✅ |
| Easy deploy heroku | ✅ |
> Forma parte de este proyecto. | Buttons | ✅ℹ️ (No funciona en multi-device)|
| Send Voice Note | ✅ |
- [Discord](https://link.codigoencasa.com/DISCORD) | Add support ubuntu/linux | ✅ |
- [Twitter](https://twitter.com/leifermendez)
- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR) ## Requisitos
- [Telegram](https://t.me/leifermendez) - node v14 o superior
- VSCode (Editor de codigo) [Descargar](https://code.visualstudio.com/download)
- MySql (opcional) solo aplica si vas a usar el modo 'mysql' [sql-bot.sql migración](https://github.com/leifermendez/bot-whatsapp/blob/main/sql-bot.sql)
- Dialogflow (opcional) solo aplica si vas a usar el modo 'dialogflow'
### (Nuevo) Botones
[![btn](https://i.imgur.com/W7oYlSu.png)](https://youtu.be/5lEMCeWEJ8o)
> Implementar los botones solo necesitas hacer uso del metodo __sendMessageButton__ que se encuentra dentro `./controllers/send` dejo un ejemplo de como usarlo.
[Ver implementación](https://github.com/leifermendez/bot-whatsapp/blob/main/app.js#L123)
``` javascript
const { sendMessageButton } = require('./controllers/send')
await sendMessageButton(
{
"title":"¿Que te interesa ver?",
"message":"Recuerda todo este contenido es gratis y estaria genial que me siguas!",
"footer":"Gracias",
"buttons":[
{"body":"😎 Cursos"},
{"body":"👉 Youtube"},
{"body":"😁 Telegram"}
]
}
)
```
## Notas de Voz
[![voice note](https://i.imgur.com/zq6xYDp.png)](https://i.imgur.com/zq6xYDp.png)
> Se pueden enviar notas de voz con formato nativo para que no se vea como reenviado. En este ejemplo enviare el archivo __PTT-20220223-WA0000.opus__ que se encuentra dentro de la carpeta de __/mediaSend__
``` javascript
const { sendMediaVoiceNote } = require('./controllers/send')
await sendMediaVoiceNote(client, from, 'PTT-20220223-WA0000.opus')
```
## Instruciones
__Descargar o Clonar repositorio__
__Usas ¿Ubuntu / Linux?__
> Asegurate de instalar los siguientes paquetes
```
sudo apt-get install -y libgbm-dev
sudo apt install -y gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
```
__Instalar dependencias (npm install)__
> Ubicate en le directorio que descargaste y via consola o terminal ejecuta el siguiente comando
```
npm i
```
__Configurar .env__
> Con el editor de texto crea un archivo `.env` el cual debes de guiarte del archivo `.env.example`
[Ver video explicando](https://youtu.be/5lEMCeWEJ8o?t=381)
```
######DATABASE: none, mysql, dialogflow
DEFAULT_MESSAGE=true
SAVE_MEDIA=true
PORT=3000
DATABASE=none
LANGUAGE=es
SQL_HOST=
SQL_USER=
SQL_PASS=
SQL_DATABASE=
```
__Ejecutar el script__
> Ubicate en le directorio que descargaste y via consola o terminal ejecuta el siguiente comando
`npm run start`
__Whatsapp en tu celular__
> Ahora abre la aplicación de Whatsapp en tu dispositivo y escanea el código QR
<img src="https://i.imgur.com/RSbPtat.png" width="500" />
Tambien puedes visitar la pagina http://127.0.0.1:3000/qr
__Listo 😎__
> Cuando sale este mensaje tu BOT está __listo__ para trabajar!
![](https://i.imgur.com/eoJ4Ruk.png)
# ¿Quieres ver como se creó? 🤖
- [Ver Video 1](https://www.youtube.com/watch?v=A_Xu0OR_HkE)
- [¿Como instalarlo? (Actulización)](https://youtu.be/5lEMCeWEJ8o)
## ¿Como usarlo el chatbot de whatsapp?
> Escribe un mensaje al whatsapp que vinculaste con tu BOT
![](https://i.imgur.com/OSUgljQ.png)
> Ahora deberías obtener un arespuesta por parte del BOT como la siguiente, ademas de esto tambien se crea un archivo excel
con el historial de conversación con el número de tu cliente

31
TODO.md
View File

@@ -1,31 +0,0 @@
### Genral
- [X] __(doc)__ Video de como colaborar PR
- [ ] __(doc)__ Video implementación de test y cobertura
- [ ] __(doc)__ Video explicacion de github action
### @bot-whatsapp/bot
- [ ] 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
- [ ] colocar mensaje esperando conectando whatsapp (provider)
- [ ] Cuando Envian Sticket devuelve mensaje raro
- [ ] createDatabase validar implementacion de funciones
### @bot-whatsapp/database
- [X] agregar export package
- [X] __(doc):__ Video para explicar como implementar nuevos database
- [X] Mongo adapter
- [ ] MySQL adapter
- [ ] JsonFile adapter
### @bot-whatsapp/provider
- [X] agregar export package
- [ ] __(doc):__ Video para explicar como implementar nuevos providers
- [ ] WhatsappWeb provider enviar imagenes
- [ ] WhatsappWeb provider enviar audio
- [ ] Twilio adapter
- [ ] Meta adapter
### @bot-whatsapp/cli

View File

@@ -1,6 +0,0 @@
const MOCK_MOBILE_WS = {
from: 'XXXXXX',
hasMedia: false,
}
module.exports = { MOCK_MOBILE_WS }

View File

@@ -1,21 +0,0 @@
const ProviderClass = require('../packages/bot/provider/provider.class')
class MockProvider extends ProviderClass {
constructor() {
super()
}
delaySendMessage = (miliseconds, eventName, payload) =>
new Promise((res) =>
setTimeout(() => {
this.emit(eventName, payload)
res
}, miliseconds)
)
sendMessage = async (userId, message) => {
console.log(`Enviando... ${userId}, ${message}`)
return Promise.resolve({ userId, message })
}
}
module.exports = MockProvider

77
adapter/diaglogflow.js Normal file
View File

@@ -0,0 +1,77 @@
const dialogflow = require('@google-cloud/dialogflow');
const fs = require('fs')
/**
* Debes de tener tu archivo con el nombre "chatbot-account.json" en la raíz del proyecto
*/
const KEEP_DIALOG_FLOW = (process.env.KEEP_DIALOG_FLOW === 'true')
let PROJECID;
let CONFIGURATION;
let sessionClient;
const checkFileCredentials = () => {
if(!fs.existsSync(`${__dirname}/../chatbot-account.json`)){
return false
}
const parseCredentials = JSON.parse(fs.readFileSync(`${__dirname}/../chatbot-account.json`));
PROJECID = parseCredentials.project_id;
CONFIGURATION = {
credentials: {
private_key: parseCredentials['private_key'],
client_email: parseCredentials['client_email']
}
}
sessionClient = new dialogflow.SessionsClient(CONFIGURATION);
}
// Create a new session
// Detect intent method
const detectIntent = async (queryText, waPhoneNumber) => {
let media = null;
const sessionId = KEEP_DIALOG_FLOW ? 1 : waPhoneNumber;
const sessionPath = sessionClient.projectAgentSessionPath(PROJECID, sessionId);
const languageCode = process.env.LANGUAGE
const request = {
session: sessionPath,
queryInput: {
text: {
text: queryText,
languageCode: languageCode,
},
},
};
const responses = await sessionClient.detectIntent(request);
const [singleResponse] = responses;
const { queryResult } = singleResponse
const { intent } = queryResult || { intent: {} }
const parseIntent = intent['displayName'] || null
const parsePayload = queryResult['fulfillmentMessages'].find((a) => a.message === 'payload');
// console.log(singleResponse)
if (parsePayload && parsePayload.payload) {
const { fields } = parsePayload.payload
media = fields.media.stringValue || null
}
const customPayload = parsePayload ? parsePayload['payload'] : null
const parseData = {
replyMessage: queryResult.fulfillmentText,
media,
trigger: null
}
return parseData
}
const getDataIa = (message = '', sessionId = '', cb = () => { }) => {
detectIntent(message, sessionId).then((res) => {
cb(res)
})
}
checkFileCredentials();
module.exports = { getDataIa }

90
adapter/index.js Normal file
View File

@@ -0,0 +1,90 @@
const { getData, getReply, saveMessageMysql } = require('./mysql')
const { saveMessageJson } = require('./jsonDb')
const { getDataIa } = require('./diaglogflow')
const stepsInitial = require('../flow/initial.json')
const stepsReponse = require('../flow/response.json')
const get = (message) => new Promise((resolve, reject) => {
/**
* Si no estas usando un gesto de base de datos
*/
if (process.env.DATABASE === 'none') {
const { key } = stepsInitial.find(k => k.keywords.includes(message)) || { key: null }
const response = key || null
resolve(response)
}
/**
* Si usas MYSQL
*/
if (process.env.DATABASE === 'mysql') {
getData(message, (dt) => {
resolve(dt)
});
}
})
const reply = (step) => new Promise((resolve, reject) => {
/**
* Si no estas usando un gesto de base de datos
*/
if (process.env.DATABASE === 'none') {
let resData = { replyMessage: '', media: null, trigger: null }
const responseFind = stepsReponse[step] || {};
resData = {
...resData,
...responseFind,
replyMessage:responseFind.replyMessage.join('')}
resolve(resData);
return
}
/**
* Si usas MYSQL
*/
if (process.env.DATABASE === 'mysql') {
let resData = { replyMessage: '', media: null, trigger: null }
getReply(step, (dt) => {
resData = { ...resData, ...dt }
resolve(resData)
});
}
})
const getIA = (message, sessionId) => new Promise((resolve, reject) => {
/**
* Si usas dialogflow
*/
if (process.env.DATABASE === 'dialogflow') {
let resData = { replyMessage: '', media: null, trigger: null }
getDataIa(message, sessionId, (dt) => {
resData = { ...resData, ...dt }
resolve(resData)
})
}
})
/**
*
* @param {*} message
* @param {*} date
* @param {*} trigger
* @param {*} number
* @returns
*/
const saveMessage = ( message, trigger, number ) => new Promise( async (resolve, reject) => {
switch ( process.env.DATABASE ) {
case 'mysql':
resolve( await saveMessageMysql( message, trigger, number ) )
break;
case 'none':
resolve( await saveMessageJson( message, trigger, number ) )
break;
default:
resolve(true)
break;
}
})
module.exports = { get, reply, getIA, saveMessage }

20
adapter/jsonDb.js Normal file
View File

@@ -0,0 +1,20 @@
const Path = require('path')
const StormDB = require("stormdb");
const date = new Date().toISOString();
const saveMessageJson = (message, trigger, number) => new Promise( async(resolve,reject) =>{
try {
const engine = new StormDB.localFileEngine( Path.join(__dirname, `/../chats/${number}.json`) );
const db = new StormDB(engine);
// set default db value if db is empty
db.default({ messages: [] });
// add new users entry
db.get("messages").push({ message, date, trigger });
db.save();
resolve('Saved')
} catch (error) {
console.log(error)
reject(error)
}
})
module.exports = { saveMessageJson }

74
adapter/mysql.js Normal file
View File

@@ -0,0 +1,74 @@
const {connection} = require('../config/mysql')
const DATABASE_NAME = process.env.SQL_DATABASE || 'db_test'
getData = (message = '', callback) => connection.query(
`SELECT * FROM ${DATABASE_NAME}.initial WHERE keywords LIKE '%${message}%' LIMIT 1`,
(error, results
) => {
const [response] = results
const key = response?.option_key || null
callback(key)
});
getReply = (option_key = '', callback) => connection.query(
`SELECT * FROM ${DATABASE_NAME}.response WHERE option_key = '${option_key}' LIMIT 1`,
(error, results
) => {
const [response] = results;
console.log(response)
const value = {
replyMessage:response?.replyMessage || '',
trigger:response?.trigger || '',
media:response?.media || ''
}
callback(value)
});
getMessages = ( number ) => new Promise((resolve,reejct) => {
try {
connection.query(
`SELECT * FROM ${DATABASE_NAME}.response WHERE number = '${number}'`, (error, results) => {
if(error) {
console.log(error)
}
const [response] = results;
console.log(response)
const value = {
replyMessage:response?.replyMessage || '',
trigger:response?.trigger || '',
media:response?.media || ''
}
resolve(value)
})
} catch (error) {
}
})
saveMessageMysql = ( message, date, trigger, number ) => new Promise((resolve,reejct) => {
try {
connection.query(
`INSERT INTO ${DATABASE_NAME}.messages `+"( `message`, `date`, `trigger`, `number`)"+` VALUES ('${message}','${date}','${trigger}', '${number}')` , (error, results) => {
if(error) {
//TODO esta parte es mejor incluirla directamente en el archivo .sql template
console.log('DEBES DE CREAR LA TABLA DE MESSAGE')
// if( error.code === 'ER_NO_SUCH_TABLE' ){
// connection.query( `CREATE TABLE ${DATABASE_NAME}.messages `+"( `date` DATE NOT NULL , `message` VARCHAR(450) NOT NULL , `trigger` VARCHAR(450) NOT NULL , `number` VARCHAR(50) NOT NULL ) ENGINE = InnoDB", async (error, results) => {
// setTimeout( async () => {
// return resolve( await this.saveMessageMysql( message, date, trigger, number ) )
// }, 150)
// })
// }
}
console.log('Saved')
console.log( results )
resolve(results)
})
} catch (error) {
}
})
module.exports = {getData, getReply, saveMessageMysql}

171
app.js Normal file
View File

@@ -0,0 +1,171 @@
/**
* ⚡⚡⚡ DECLARAMOS LAS LIBRERIAS y CONSTANTES A USAR! ⚡⚡⚡
*/
require('dotenv').config()
const fs = require('fs');
const express = require('express');
const cors = require('cors')
const qrcode = require('qrcode-terminal');
const { Client, LocalAuth } = require('whatsapp-web.js');
const mysqlConnection = require('./config/mysql')
const { middlewareClient } = require('./middleware/client')
const { generateImage, cleanNumber, checkEnvFile, createClient, isValidNumber } = require('./controllers/handle')
const { connectionReady, connectionLost } = require('./controllers/connection')
const { saveMedia } = require('./controllers/save')
const { getMessages, responseMessages, bothResponse } = require('./controllers/flows')
const { sendMedia, sendMessage, lastTrigger, sendMessageButton, readChat } = require('./controllers/send')
const app = express();
app.use(cors())
app.use(express.json())
const MULTI_DEVICE = process.env.MULTI_DEVICE || 'true';
const server = require('http').Server(app)
const port = process.env.PORT || 3000
var client;
app.use('/', require('./routes/web'))
/**
* Escuchamos cuando entre un mensaje
*/
const listenMessage = () => client.on('message', async msg => {
const { from, body, hasMedia } = msg;
if (!isValidNumber(from)) {
return
}
// Este bug lo reporto Lucas Aldeco Brescia para evitar que se publiquen estados
if (from === 'status@broadcast') {
return
}
message = body.toLowerCase();
console.log('BODY', message)
const number = cleanNumber(from)
await readChat(number, message)
/**
* Guardamos el archivo multimedia que envia
*/
if (process.env.SAVE_MEDIA && hasMedia) {
const media = await msg.downloadMedia();
saveMedia(media);
}
/**
* Si estas usando dialogflow solo manejamos una funcion todo es IA
*/
if (process.env.DATABASE === 'dialogflow') {
if (!message.length) return;
const response = await bothResponse(message, number);
await sendMessage(client, from, response.replyMessage);
if (response.media) {
sendMedia(client, from, response.media);
}
return
}
/**
* Ver si viene de un paso anterior
* Aqui podemos ir agregando más pasos
* a tu gusto!
*/
const lastStep = await lastTrigger(from) || null;
if (lastStep) {
const response = await responseMessages(lastStep)
await sendMessage(client, from, response.replyMessage);
}
/**
* Respondemos al primero paso si encuentra palabras clave
*/
const step = await getMessages(message);
if (step) {
const response = await responseMessages(step);
/**
* Si quieres enviar botones
*/
await sendMessage(client, from, response.replyMessage, response.trigger);
if (response.hasOwnProperty('actions')) {
const { actions } = response;
await sendMessageButton(client, from, null, actions);
return
}
if (!response.delay && response.media) {
sendMedia(client, from, response.media);
}
if (response.delay && response.media) {
setTimeout(() => {
sendMedia(client, from, response.media);
}, response.delay)
}
return
}
//Si quieres tener un mensaje por defecto
if (process.env.DEFAULT_MESSAGE === 'true') {
const response = await responseMessages('DEFAULT')
await sendMessage(client, from, response.replyMessage, response.trigger);
/**
* Si quieres enviar botones
*/
if (response.hasOwnProperty('actions')) {
const { actions } = response;
await sendMessageButton(client, from, null, actions);
}
return
}
});
client = new Client({
authStrategy: new LocalAuth(),
puppeteer: { headless: true }
});
client.on('qr', qr => generateImage(qr, () => {
qrcode.generate(qr, { small: true });
console.log(`Ver QR http://localhost:${port}/qr`)
socketEvents.sendQR(qr)
}))
client.on('ready', (a) => {
connectionReady()
listenMessage()
// socketEvents.sendStatus(client)
});
client.on('auth_failure', (e) => {
// console.log(e)
// connectionLost()
});
client.on('authenticated', () => {
console.log('AUTHENTICATED');
});
client.initialize();
/**
* Verificamos si tienes un gesto de db
*/
if (process.env.DATABASE === 'mysql') {
mysqlConnection.connect()
}
server.listen(port, () => {
console.log(`El server esta listo por el puerto ${port}`);
})
checkEnvFile();

35
app.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "Chatbot Whatsapp (Leifer Mendez)",
"description": "El siguiente proyecto se realizó con fines educativos para el canal de Youtube (Leifer Mendez) donde aprendemos como usando node.js podemos crear un chatbot increíble que además le agregamos inteligencia artificial gracias al servicio de dialogflow",
"repository": "https://github.com/leifermendez/bot-whatsapp",
"logo": "https://avatars0.githubusercontent.com/u/15802366?s=460&u=77ec7ef359e8ed842aef769693f1675c0ed460fd&v=4",
"keywords": [
"nodejs",
"whatsapp",
"bot",
"chatbot",
"dialogflow"
],
"addons": [
],
"buildpacks": [
{
"url": "heroku/nodejs"
},
{
"url": "https://github.com/jontewks/puppeteer-heroku-buildpack"
}
],
"env": {
"SAVE_MEDIA": "false",
"DATABASE": {
"description": "'none', 'mysql', 'dialogflow' por defecto 'none' Puedes usar alguna de los siguientes opciones. Pero antes debes de saber como funciona y eso lo explico en el video. Puedes obtener más información https://github.com/leifermendez/bot-whatsapp/blob/main/README.md",
"value": "none"
},
"LANGUAGE": "es",
"SQL_HOST":"your_host",
"SQL_USER":"your_user",
"SQL_PASS":"your_password",
"SQL_DATABASE":"your_database"
}
}

13
chatbot-account.json Normal file
View File

@@ -0,0 +1,13 @@
{
"type": "",
"project_id": "",
"private_key_id": "",
"private_key":"",
"client_email": "",
"client_id": "",
"auth_uri": "",
"token_uri": "",
"auth_provider_x509_cert_url": "",
"client_x509_cert_url":""
}

18
config/mysql.js Normal file
View File

@@ -0,0 +1,18 @@
const mysql = require('mysql');
const connection = mysql.createConnection({
host : process.env.SQL_HOST || 'localhost',
user : process.env.SQL_USER || 'root',
password : process.env.SQL_PASS || '',
database : process.env.SQL_DATABASE || 'pruebas'
});
const connect = () => connection.connect(function(err) {
if (err) {
console.error('error connecting: ' + err.stack);
return;
}
console.log('Conexion correcta con tu base de datos MySQL')
});
module.exports = {connect, connection}

14
controllers/connection.js Normal file
View File

@@ -0,0 +1,14 @@
const connectionReady = (cb = () =>{}) => {
console.log('Listo para escuchas mensajes')
console.log('Client is ready!');
console.log('🔴 escribe: hola');
cb()
}
const connectionLost = (cb = () =>{}) => {
console.log('** Error de autentificacion vuelve a generar el QRCODE (Borrar el archivo session.json) **');
cb()
}
module.exports = {connectionReady, connectionLost}

28
controllers/flows.js Normal file
View File

@@ -0,0 +1,28 @@
const {get, reply, getIA} = require('../adapter')
const {saveExternalFile, checkIsUrl} = require('./handle')
const getMessages = async (message) => {
const data = await get(message)
return data
}
const responseMessages = async (step) => {
const data = await reply(step)
if(data && data.media){
const file = checkIsUrl(data.media) ? await saveExternalFile(data.media) : data.media;
return {...data,...{media:file}}
}
return data
}
const bothResponse = async (message, sessionId) => {
const data = await getIA(message, sessionId)
if(data && data.media){
const file = await saveExternalFile(data.media)
return {...data,...{media:file}}
}
return data
}
module.exports = { getMessages, responseMessages, bothResponse }

83
controllers/handle.js Normal file
View File

@@ -0,0 +1,83 @@
const { Client, LegacySessionAuth, LocalAuth } = require('whatsapp-web.js');
const http = require('http'); // or 'https' for https:// URLs
const https = require('https'); // or 'https' for https:// URLs
const fs = require('fs');
const qr = require('qr-image')
const MULTI_DEVICE = process.env.MULTI_DEVICE || 'true';
const cleanNumber = (number) => {
number = number.replace('@c.us', '');
number = `${number}@c.us`;
return number
}
const saveExternalFile = (url) => new Promise((resolve, reject) => {
const ext = url.split('.').pop()
const checkProtocol = url.split('/').includes('https:');
const handleHttp = checkProtocol ? https : http;
const name = `${Date.now()}.${ext}`;
const file = fs.createWriteStream(`${__dirname}/../mediaSend/${name}`);
console.log(url)
handleHttp.get(url, function(response) {
response.pipe(file);
file.on('finish', function() {
file.close(); // close() is async, call cb after close completes.
resolve(name)
});
file.on('error', function() {
console.log('errro')
file.close(); // close() is async, call cb after close completes.
resolve(null)
});
});
})
const checkIsUrl = (path) => {
try{
regex = /^(http(s)?:\/\/)[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$/i;
match = path.match(regex);
return match[0]
}catch(e){
return null
}
}
const generateImage = (base64, cb = () => {}) => {
let qr_svg = qr.image(base64, { type: 'svg', margin: 4 });
qr_svg.pipe(require('fs').createWriteStream('./mediaSend/qr-code.svg'));
console.log(`⚡ Recuerda que el QR se actualiza cada minuto ⚡'`);
console.log(`⚡ Actualiza F5 el navegador para mantener el mejor QR⚡`);
cb()
}
const checkEnvFile = () => {
const pathEnv = `${__dirname}/../.env`;
const isExist = fs.existsSync(pathEnv);
if(!isExist){
console.log(`🆗 ATENCION! 🆗 te falta crear tu archivo .env de lo contrario no funcionara`)
}
}
/**
*
* @param {*} session
* @param {*} cb
*/
const createClient = () => {
client = new Client({
authStrategy: new LocalAuth(
{dataPath: './sessions/',
clientId: 'bot'}),
puppeteer: { headless: false }
});
}
const isValidNumber = (rawNumber) => {
const regexGroup = /\@g.us\b/gm;
const exist = rawNumber.match(regexGroup);
return !exist
}
module.exports = {cleanNumber, saveExternalFile, generateImage, checkIsUrl, checkEnvFile, createClient, isValidNumber}

18
controllers/save.js Normal file
View File

@@ -0,0 +1,18 @@
const mimeDb = require('mime-db')
const fs = require('fs')
/**
* Guardamos archivos multimedia que nuestro cliente nos envie!
* @param {*} media
*/
const saveMedia = (media) => {
const extensionProcess = mimeDb[media.mimetype]
const ext = extensionProcess.extensions[0]
fs.writeFile(`./media/${Date.now()}.${ext}`, media.data, { encoding: 'base64' }, function (err) {
console.log('** Archivo Media Guardado **');
});
}
module.exports = {saveMedia}

113
controllers/send.js Normal file
View File

@@ -0,0 +1,113 @@
const ExcelJS = require('exceljs');
const moment = require('moment');
const fs = require('fs');
const { MessageMedia, Buttons } = require('whatsapp-web.js');
const { cleanNumber } = require('./handle')
const DELAY_TIME = 170; //ms
const DIR_MEDIA = `${__dirname}/../mediaSend`;
// import { Low, JSONFile } from 'lowdb'
// import { join } from 'path'
const { saveMessage } = require('../adapter')
/**
* Enviamos archivos multimedia a nuestro cliente
* @param {*} number
* @param {*} fileName
*/
const sendMedia = (client, number = null, fileName = null) => {
if(!client) return cosnole.error("El objeto cliente no está definido.");
try {
number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`;
if (fs.existsSync(file)) {
const media = MessageMedia.fromFilePath(file);
client.sendMessage(number, media, { sendAudioAsVoice: true });
}
} catch(e) {
throw e;
}
}
/**
* Enviamos archivos como notas de voz
* @param {*} number
* @param {*} fileName
*/
const sendMediaVoiceNote = (client, number = null, fileName = null) => {
if(!client) return cosnole.error("El objeto cliente no está definido.");
try {
number = cleanNumber(number || 0)
const file = `${DIR_MEDIA}/${fileName}`;
if (fs.existsSync(file)) {
const media = MessageMedia.fromFilePath(file);
client.sendMessage(number, media ,{ sendAudioAsVoice: true });
}
}catch(e) {
throw e;
}
}
/**
* Enviamos un mensaje simple (texto) a nuestro cliente
* @param {*} number
*/
const sendMessage = async (client, number = null, text = null, trigger = null) => {
setTimeout(async () => {
number = cleanNumber(number)
const message = text
client.sendMessage(number, message);
await readChat(number, message, trigger)
console.log(`⚡⚡⚡ Enviando mensajes....`);
},DELAY_TIME)
}
/**
* Enviamos un mensaje con buttons a nuestro cliente
* @param {*} number
*/
const sendMessageButton = async (client, number = null, text = null, actionButtons) => {
number = cleanNumber(number)
const { title = null, message = null, footer = null, buttons = [] } = actionButtons;
let button = new Buttons(message,[...buttons], title, footer);
client.sendMessage(number, button);
console.log(`⚡⚡⚡ Enviando mensajes....`);
}
/**
* Opte
*/
const lastTrigger = (number) => new Promise((resolve, reject) => {
number = cleanNumber(number)
const pathExcel = `${__dirname}/../chats/${number}.xlsx`;
const workbook = new ExcelJS.Workbook();
if (fs.existsSync(pathExcel)) {
workbook.xlsx.readFile(pathExcel)
.then(() => {
const worksheet = workbook.getWorksheet(1);
const lastRow = worksheet.lastRow;
const getRowPrevStep = worksheet.getRow(lastRow.number);
const lastStep = getRowPrevStep.getCell('C').value;
resolve(lastStep)
});
} else {
resolve(null)
}
})
/**
* Guardar historial de conversacion
* @param {*} number
* @param {*} message
*/
const readChat = async (number, message, trigger = null) => {
number = cleanNumber(number)
await saveMessage( message, trigger, number )
console.log('Saved')
}
module.exports = { sendMessage, sendMedia, lastTrigger, sendMessageButton, readChat, sendMediaVoiceNote }

16
controllers/socket.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = (socket) => {
return {
sendQR:(qr) => {
socket.emit('connection_qr',{
qr
})
},
sendStatus:() => {
socket.emit('connection_status',{
a:1
})
}
}
}

17
controllers/web.js Normal file
View File

@@ -0,0 +1,17 @@
const fs = require('fs')
const { sendMessage } = require('../controllers/send')
const sendMessagePost = (req, res) => {
console.log('asdasdasdasdasd')
const { message, number } = req.body
const client = req.clientWs || null;
sendMessage(client, number, message)
res.send({ status: 'Enviado!' })
}
const getQr = (req, res) => {
res.writeHead(200, { 'content-type': 'image/svg+xml' });
fs.createReadStream(`${__dirname}/../mediaSend/qr-code.svg`).pipe(res);
}
module.exports = { sendMessagePost, getQr }

View File

@@ -1,25 +0,0 @@
version: '3.3'
services:
mongo:
image: mongo
container_name: app_enviroment
restart: always
ports:
- '27019:27017'
environment:
MONGO_INITDB_DATABASE: bot
expose:
- 27019
mysql:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: bot
container_name: app_mysql
ports:
- '3306:3306'
expose:
- 3306

93
flow/initial.json Normal file
View File

@@ -0,0 +1,93 @@
[
{
"keywords": [
"hola",
"hola!",
"ola",
"ole",
"inicio",
"welcome",
"buenos días",
"buenas tardes",
"buenas noches",
"me dieron este número",
"venden a crédito",
"quisiera saber si venden",
"necesito saber"
],
"key": "STEP_1"
},
{
"keywords": [
"cursos",
"info",
"curso" ],
"key": "STEP_2"
},
{
"keywords": [
"angular"
],
"key": "STEP_2_1"
},
{
"keywords": [
"node"
],
"key": "STEP_2_2"
},
{
"keywords": [
"ngrx"
],
"key": "STEP_2_3"
},
{
"keywords": [
"aws"
],
"key": "STEP_2_4"
},
{
"keywords": [
"asesor",
"asesores",
"Vendedor",
"cobrador"
],
"key": "STEP_3"
},
{
"keywords": [
"muchas gracias",
"ok",
"gracias",
"vale gracias"
],
"key": "STEP_4"
},
{
"keywords": [
"youtube"
],
"key": "STEP_5"
},
{
"keywords": [
"VER_CURSOS"
],
"key": "STEP_6"
},
{
"keywords": [
"telegram"
],
"key": "STEP_7"
},
{
"keywords": [
"audio"
],
"key": "STEP_8"
}
]

148
flow/response.json Normal file
View File

@@ -0,0 +1,148 @@
{
"DEFAULT":{
"replyMessage":[
"*Esta respuesta es un respuesta default* cuando no se consigue una palabra clave \n",
"la puedes desactivar en tu archivo .env DEFAULT_MESSAGE=false \n",
"tambien te quiero recordar que si presentas algun error pasarte por el repositorio \n",
"https://github.com/leifermendez/bot-whatsapp#chatbot-whatsapp-opensource \n",
"y recuerda tener la ultima versión del proyecto \n\n",
"Prueba escribiendo *hola* \n"
],
"media":null,
"trigger":null
},
"STEP_0":{
"replyMessage":[
"El flujo ha finalizado \n",
"pero puedes ver todo el codigo de este \n",
"repositorio en https://github.com/leifermendez/bot-whatsapp.git"
],
"media":null,
"trigger":null
},
"STEP_1":{
"replyMessage":[
"Hola! y✌ Bienvenido a este 🤖 CHATBOT de Whatsapp, lo primero \n",
"decirte que mi nombre es *Leifer Mendez*😎 \n",
"\n Si necesitas ver más info sobre las capacitacion tecnicas ",
"escribe *cursos* o *info* o escribe *audio*"
],
"media":"https://media2.giphy.com/media/VQJu0IeULuAmCwf5SL/giphy.gif",
"trigger":null,
"actions":{
"title":"¿Que te interesa ver?",
"message":"Recuerda todo este contenido es gratis y estaria genial que me siguas!",
"footer":"Gracias",
"buttons":[
{"body":"Cursos"},
{"body":"Youtube"},
{"body":"Telegram"}
]
}
},
"STEP_2":{
"replyMessage":[
"Perfecto, te voy a pasar la lista ",
"de los temas que tengo y un breve video 🙂🤖 \n\n",
"*Angular* Basico (Pago) \n",
"*Angular* Basico (Gratis) \n",
"*Node* Basico (Gratis) \n",
"*NGRX* Basico (Gratis) \n",
"*AWS* Basico (Pago) \n\n",
"Escribe la palabra del tema que te interese \n"
],
"media":"https://i.giphy.com/media/5J5gN0WUk0VToHaK2p/giphy-downsized.gif",
"trigger":null
},
"STEP_2_1":{
"replyMessage":[
"Si te interesa Angular tienes disponible \n",
"*(Gratis)* https://bit.ly/367tJ32 \n\n",
"*(Pago)* https://link.codigoencasa.com/PROMO-INICIAL \n\n",
"*(Pago)* https://link.codigoencasa.com/ANGULAR-BASICO-EDTEAM \n\n",
"😎😎😎"
],
"media":"https://i.imgur.com/Q0a5UQI.jpg",
"trigger":null
},
"STEP_2_2":{
"replyMessage":[
"Si te interesa NODE tienes disponible \n",
"*(Gratis)* https://bit.ly/3od1Bl6 \n\n",
"Espero pronto tener más material disponible",
"🤖"
],
"media":null,
"trigger":null
},
"STEP_2_3":{
"replyMessage":[
"NGRX para manejar estados en Angular \n",
"*(Gratis)* https://bit.ly/ngrx-desde-cero \n",
"A darle! 😮"
],
"media":null,
"trigger":null
},
"STEP_2_4":{
"replyMessage":[
"Muy bien AWS esta pronto a salir pre-registrate aquí \n",
"*(Pre-registro)* https://link.codigoencasa.com/AWS-BASICO-INVITACION \n",
"😮😮"
],
"media":null,
"trigger":null
},
"STEP_3":{
"replyMessage":[
"¿Ok cual curso de intereso? \n",
"*angular* , *node*, *ngrx*, *aws*"
],
"media":null,
"trigger":null
},
"STEP_4":{
"replyMessage":[
"Gracias a ti! \n"
],
"media":"https://media4.giphy.com/media/hur0SFIU5SH4mxNBWa/giphy.gif",
"trigger":null
},
"STEP_5":{
"replyMessage":[
"Muy bien te comparto el canal de Youtube \n",
"https://youtube.com/leifermendez \n"
],
"media":null,
"trigger":null
},
"STEP_6":{
"replyMessage":[
"Perfecto, te voy a pasar la lista ",
"de los temas que tengo y un breve video 🙂🤖 \n\n",
"*Angular* Basico (Pago) \n",
"*Angular* Basico (Gratis) \n",
"*Node* Basico (Gratis) \n",
"*NGRX* Basico (Gratis) \n",
"*AWS* Basico (Pago) \n\n",
"Escribe la palabra del tema que te interese \n"
],
"media":"https://i.giphy.com/media/5J5gN0WUk0VToHaK2p/giphy-downsized.gif",
"trigger":null
},
"STEP_7":{
"replyMessage":[
"Vente al telegram \n",
"https://t.me/leifermendez \n"
],
"media":null,
"trigger":null
},
"STEP_8":{
"replyMessage":[
"Esto es una nota de voz \n"
],
"media":"nota-de-voz.mp3",
"trigger":null
}
}

Binary file not shown.

BIN
mediaSend/nota-de-voz.mp3 Normal file

Binary file not shown.

21
middleware/client.js Normal file
View File

@@ -0,0 +1,21 @@
const middlewareClient = (client = null) => async (req, res, next) => {
try {
if(!client){
res.status(409)
console.log(client)
res.send({ error: 'Error de client.' })
}else{
req.clientWs = client;
next()
}
} catch (e) {
console.log(e)
res.status(409)
res.send({ error: 'Error de client' })
}
}
module.exports = { middlewareClient }

0
middleware/db.js Normal file
View File

9380
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,56 @@
{ {
"name": "@bot-whatsapp/root", "name": "bot-whatsapp",
"version": "0.0.1", "version": "1.0.0",
"description": "Bot de wahtsapp open source para MVP o pequeños negocios", "description": "Bot de wahtsapp open source para MVP o pequeños negocios",
"main": "app.js", "main": "app.js",
"private": true, "scripts": {
"scripts": { "start": "node ./app.js",
"commit": "git-cz", "test": "echo \"Error: no test specified\" && exit 1"
"cli:rollup": "rollup --config ./packages/cli/rollup-cli.config.js ", },
"bot:rollup": "rollup --config ./packages/bot/rollup-bot.config.js", "keywords": [
"provider:rollup": "rollup --config ./packages/provider/rollup-provider.config.js ", "whatsapp",
"database:rollup": "rollup --config ./packages/database/rollup-database.config.js", "bot-whatsapp",
"format:check": "prettier --check ./packages", "node-bot-whatsapp"
"format:write": "prettier --write ./packages", ],
"lint:check": "eslint ./packages", "contributors": [
"lint:fix": "eslint --fix ./packages", {
"build": "yarn run cli:rollup && yarn run bot:rollup && yarn run provider:rollup && yarn run database:rollup", "email": "leifer33@gmail.com",
"link.dist": "cd packages/bot && npm link && cd ../provider && npm link && cd ../cli && npm link", "name": "Leifer Mendez",
"test.unit": "node ./node_modules/uvu/bin.js packages test", "url": "https://leifermendez.github.io"
"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",
"dev:debug": "node --inspect ./example-app/app.js",
"dev": "node ./example-app/app.js",
"prepare": "npx husky install",
"preinstall": "npx only-allow yarn"
}, },
"workspaces": [ {
"packages/bot", "name": "aurik3",
"packages/cli", "email": "aurik3@aurik3.com",
"packages/database", "url": "https://github.com/aurik3"
"packages/provider",
"packages/docs"
],
"keywords": [
"whatsapp",
"bot-whatsapp",
"node-bot-whatsapp"
],
"contributors": [
{
"email": "leifer33@gmail.com",
"name": "Leifer Mendez",
"url": "https://leifermendez.github.io"
},
{
"name": "aurik3",
"email": "aurik3@aurik3.com",
"url": "https://github.com/aurik3"
}
],
"repository": "https://github.com/leifermendez/bot-whatsapp",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-replace": "^5.0.1",
"c8": "^7.12.0",
"commitizen": "^4.2.5",
"cross-env": "^7.0.3",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"husky": "^8.0.2",
"only-allow": "^1.1.1",
"prettier": "^2.7.1",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
"rollup": "^3.2.3",
"rollup-plugin-cleanup": "^3.2.1",
"rollup-plugin-copy": "^3.4.0",
"uvu": "^0.5.6"
},
"packageManager": "yarn@1.22.19",
"engines": {
"node": ">=16",
"npm": "please-use-yarn",
"yarn": ">=1"
},
"author": "Leifer Mendez <leifer33@gmail.com>",
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
} }
],
"repository": {
"type": "git",
"url": "https://github.com/leifermendez/bot-whatsapp"
},
"license": "ISC",
"dependencies": {
"@google-cloud/dialogflow": "^5.2.0",
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"exceljs": "^4.3.0",
"express": "^4.18.1",
"file-type": "^17.1.6",
"mime-db": "^1.52.0",
"moment": "^2.29.4",
"mysql": "^2.18.1",
"qr-image": "^3.2.0",
"qrcode-terminal": "^0.12.0",
"socket.io": "^4.5.1",
"stormdb": "^0.6.0",
"whatsapp-web.js": "^1.18.4",
"xlsx": "^0.18.5"
},
"devDependencies": {
"pm2": "^5.2.0",
"prettier": "2.7.1"
},
"engines": {
"node": "16.x"
}
} }

View File

@@ -1,110 +0,0 @@
# @bot-whatsapp/io
### Caso de uso
> Una persona escribe `hola`
**addKeyword** recibe `string | string[]`
> `sensitive` false _default_
- [x] addKeyword
- [x] addAnswer
- [x] addKeyword: Opciones
- [x] addAnswer: Opciones, media, buttons
- [x] Retornar JSON (options)
- [ ] Recibir JSON
```js
// bootstrap.js Como iniciar el provider
const { inout, provider, database } = require('@bot-whatsapp')
/**
* async whatsapp-web, twilio, meta
* */
const bootstrap = async () => {
console.log(`Iniciando....`)
const client = await provider.start()
/**
* - QR
* - Endpoint
* - Check Token Meta, Twilio
* - Return events? on message
* */
console.log(`Fin...`)
// Esto es opcional ? no deberia ser necesario
client.on('message', ({number, body,...}) => {
// Incoming message
})
}
```
```js
// flow.js Como agregar keywords y respuestas
const { inout, provider, database } = require('@bot-whatsapp')
await inout
.addKeyword('hola')
.addAnswer('Bienvenido a tu tienda 🥲')
.addAnswer('escribe *catalogo* o *ofertas*')
await inout
.addKeyword(['catalogo', 'ofertas'])
.addAnswer('Este es nuestro CATALOGO mas reciente!', {
buttons: [{ body: 'Xiaomi' }, { body: 'Samsung' }],
})
await inout
.addKeyword('Xiaomi')
.addAnswer('Estos son nuestro productos XIAOMI ....', {
media: 'https://....',
})
.addAnswer('Si quieres mas info escrbie *info*')
await inout
.addKeyword('chao!')
.addAnswer('bye!')
.addAnswer('Recuerda que tengo esta promo', {
media: 'https://media2.giphy.com/media/VQJu0IeULuAmCwf5SL/giphy.gif',
})
await inout
.addKeyword('Modelo C', { sensitive: false })
.addAnswer('100USD', { media: 'http//:...' })
await inout
.addKeyword('hola!', { sensitive: false })
.addAnswer('Bievenido Escribe *productos*')
await inout
.addKeyword('productos', { sensitive: false })
.addAnswer('Esto son los mas vendidos')
.addAnswer('*PC1* Precio 10USD', { media: 'https://....' })
.addAnswer('*PC2* Precio 10USD', { media: 'https://....' })
await inout
.addKeyword('PC1', { sensitive: false })
.addAnswer('Bievenido Escribe *productos*')
const answerOne = await inout.addAnswer({
message: 'Como estas!',
media: 'https://media2.giphy.com/media/VQJu0IeULuAmCwf5SL/giphy.gif',
})
const otherAnswer = await inout.addAnswer('Aprovecho para decirte!')
answerOne.push(otherAnswer)
inout.addKeywords(['hola', 'hi', 'ola'])
```
**Comunidad**
> Forma parte de este proyecto.
- [Discord](https://link.codigoencasa.com/DISCORD)
- [Twitter](https://twitter.com/leifermendez)
- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR)
- [Telegram](https://t.me/leifermendez)

View File

@@ -1,128 +0,0 @@
const { toCtx } = require('../io/methods')
const { printer } = require('../utils/interactive')
/**
* [ ] Escuchar eventos del provider asegurarte que los provider emitan eventos
* [ ] Guardar historial en db
* [ ] Buscar mensaje en flow
*
*/
class CoreClass {
flowClass
databaseClass
providerClass
constructor(_flow, _database, _provider) {
this.flowClass = _flow
this.databaseClass = _database
this.providerClass = _provider
for (const { event, func } of this.listenerBusEvents()) {
this.providerClass.on(event, func)
}
}
listenerBusEvents = () => [
{
event: 'require_action',
func: ({ instructions, title = '⚡⚡ ACCION REQUERIDA ⚡⚡' }) =>
printer(instructions, title),
},
{
event: 'ready',
func: () => printer('Provider conectado y listo'),
},
{
event: 'auth_failure',
func: ({ instructions }) =>
printer(instructions, '⚡⚡ ERROR AUTH ⚡⚡'),
},
{
event: 'message',
func: (msg) => this.handleMsg(msg),
},
]
/**
* @private
* @param {*} ctxMessage
*/
handleMsg = async (messageInComming) => {
const { body, from } = messageInComming
let msgToSend = []
//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
)
if (prevMsg?.ref) {
const ctxByNumber = toCtx({
body,
from,
prevRef: prevMsg.refSerialize,
})
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)
}
//Si se tiene anidaciones de flows, si tienes anidados obligatoriamente capture:true
if (prevMsg?.options?.nested?.length) {
const nestedRef = prevMsg.options.nested
const flowStandalone = nestedRef.map((f) => ({
...nestedRef.find((r) => r.refSerialize === f.refSerialize),
}))
msgToSend = this.flowClass.find(body, false, flowStandalone) || []
this.sendFlow(msgToSend, from)
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) || []
}
this.sendFlow(msgToSend, from)
}
sendProviderAndSave = (numberOrId, ctxMessage) => {
const { answer } = ctxMessage
return Promise.all([
this.providerClass.sendMessage(numberOrId, answer),
this.databaseClass.save({ ...ctxMessage, from: numberOrId }),
])
}
sendFlow = (messageToSend, numberOrId) => {
const queue = []
for (const ctxMessage of messageToSend) {
queue.push(this.sendProviderAndSave(numberOrId, ctxMessage))
}
return Promise.all(queue)
}
/**
* @private
* @param {*} message
* @param {*} ref
*/
continue = (message, ref = false) => {
const responde = this.flowClass.find(message, ref)
if (responde) {
this.providerClass.sendMessage(responde.answer)
this.databaseClass.saveLog(responde.answer)
this.continue(null, responde.ref)
}
}
}
module.exports = CoreClass

View File

@@ -1,44 +0,0 @@
const CoreClass = require('./core/core.class')
const ProviderClass = require('./provider/provider.class')
const FlowClass = require('./io/flow.class')
const { addKeyword, addAnswer, toSerialize } = require('./io/methods')
/**
* Crear instancia de clase Bot
* @param {*} args
* @returns
*/
const createBot = async ({ flow, database, provider }) =>
new CoreClass(flow, database, provider)
/**
* Crear instancia de clase Io (Flow)
* @param {*} args
* @returns
*/
const createFlow = (args) => {
return new FlowClass(args)
}
/**
* Crear instancia de clase Provider
* @param {*} args
* @returns
*/
const createProvider = (providerClass = class {}) => {
const providerInstance = new providerClass()
if (!providerClass.prototype instanceof ProviderClass)
throw new Error('El provider no implementa ProviderClass')
return providerInstance
}
module.exports = {
createBot,
createFlow,
createProvider,
addKeyword,
addAnswer,
toSerialize,
ProviderClass,
CoreClass,
}

View File

@@ -1,71 +0,0 @@
const { toSerialize } = require('./methods/toSerialize')
class FlowClass {
allCallbacks = []
flowSerialize = []
flowRaw = []
constructor(_flow) {
if (!Array.isArray(_flow)) throw new Error('Esto debe ser un ARRAY')
this.flowRaw = _flow
this.allCallbacks = _flow
.map((cbIn) => cbIn.ctx.callbacks)
.flat(2)
.map((c, i) => ({ callback: c?.callback, index: i }))
const mergeToJsonSerialize = Object.keys(_flow)
.map((indexObjectFlow) => _flow[indexObjectFlow].toJson())
.flat(2)
this.flowSerialize = toSerialize(mergeToJsonSerialize)
}
find = (keyOrWord, symbol = false, overFlow = null) => {
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())
}
if (!flag && typeof str === 'string') {
return str.toLowerCase()
}
return str
}
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) {
refSymbol = flow.find((c) => c.keyword === keyOrWord)
if (refSymbol?.answer) messages.push(refSymbol)
if (refSymbol?.ref) findIn(refSymbol.ref, true)
} else {
refSymbol = flow.find((c) =>
mapSensitiveString(c.keyword, sensitive).includes(keyOrWord)
)
if (refSymbol?.ref) findIn(refSymbol.ref, true)
return messages
}
}
findIn(keyOrWord, symbol)
return messages
}
findBySerialize = (refSerialize) =>
this.flowSerialize.find((r) => r.refSerialize === refSerialize)
findIndexByRef = (ref) => this.flowSerialize.findIndex((r) => r.ref === ref)
}
module.exports = FlowClass

View File

@@ -1,91 +0,0 @@
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}
* @returns
*/
const addAnswer =
(inCtx) =>
(answer, options, cb = null, nested = []) => {
/**
* Todas las opciones referentes a el mensaje en concreto options:{}
* @returns
*/
const getAnswerOptions = () => ({
media:
typeof options?.media === 'string' ? `${options?.media}` : null,
buttons: Array.isArray(options?.buttons) ? options.buttons : [],
capture:
typeof options?.capture === 'boolean'
? options?.capture
: false,
child:
typeof options?.child === 'string' ? `${options?.child}` : null,
})
const getNested = () => ({
nested: Array.isArray(nested) ? nested : [],
})
const callback =
typeof cb === 'function'
? cb
: () => console.log('Callback no definida')
const lastCtx = inCtx.hasOwnProperty('ctx') ? inCtx.ctx : inCtx
/**
* Esta funcion se encarga de mapear y transformar todo antes
* de retornar
* @returns
*/
const ctxAnswer = () => {
const ref = `ans_${generateRef()}`
const options = {
...getAnswerOptions(),
...getNested(),
keyword: {},
callback: !!cb,
}
const json = [].concat(inCtx.json).concat([
{
ref,
keyword: lastCtx.ref,
answer,
options,
},
])
const callbacks = [].concat(inCtx.callbacks).concat([
{
ref: lastCtx.ref,
callback,
},
])
return {
...lastCtx,
ref,
answer,
json,
options,
callbacks,
}
}
const ctx = ctxAnswer()
return {
ctx,
ref: ctx.ref,
addAnswer: addAnswer(ctx),
toJson: toJson(ctx),
}
}
module.exports = { addAnswer }

View File

@@ -1,49 +0,0 @@
const { generateRef } = require('../../utils/hash')
const { addAnswer } = require('./addAnswer')
const { toJson } = require('./toJson')
/**
*
* @param {*} message `string | string[]`
* @param {*} options {sensitive:boolean} default false
*/
const addKeyword = (keyword, options) => {
const parseOptions = () => {
const defaultProperties = {
sensitive:
typeof options?.sensitive === 'boolean'
? options?.sensitive
: false,
}
return defaultProperties
}
const ctxAddKeyword = () => {
const ref = `key_${generateRef()}`
const options = parseOptions()
const json = [
{
ref,
keyword,
options,
},
]
/**
* Se guarda en db
*/
return { ref, keyword, options, json }
}
const ctx = ctxAddKeyword()
return {
ctx,
ref: ctx.ref,
addAnswer: addAnswer(ctx),
toJson: toJson(ctx),
}
}
module.exports = { addKeyword }

View File

@@ -1,7 +0,0 @@
const { addAnswer } = require('./addAnswer')
const { addKeyword } = require('./addKeyword')
const { toSerialize } = require('./toSerialize')
const { toCtx } = require('./toCtx')
const { toJson } = require('./toJson')
module.exports = { addAnswer, addKeyword, toCtx, toJson, toSerialize }

View File

@@ -1,19 +0,0 @@
const { generateRef, generateRefSerialize } = require('../../utils/hash')
/**
* @deprecate
* @param answer string
* @param options {media:string, buttons:[], capture:true default false}
* @returns
*/
const toCtx = ({ body, from, prevRef, index }) => {
return {
ref: generateRef(),
keyword: prevRef,
answer: body,
options: {},
from,
refSerialize: generateRefSerialize({ index, answer: body }),
}
}
module.exports = { toCtx }

View File

@@ -1,6 +0,0 @@
const toJson = (inCtx) => () => {
const lastCtx = inCtx.hasOwnProperty('ctx') ? inCtx.ctx : inCtx
return lastCtx.json
}
module.exports = { toJson }

View File

@@ -1,23 +0,0 @@
const { generateRefSerialize } = require('../../utils/hash')
/**
* Crear referencia serializada
* @param {*} flowJson
* @returns array[]
*/
const toSerialize = (flowJson) => {
if (!Array.isArray(flowJson)) throw new Error('Esto debe ser un ARRAY')
const jsonToSerialize = flowJson.map((row, index) => ({
...row,
refSerialize: `${generateRefSerialize({
index,
keyword: row.keyword,
answer: row.answer,
})}`,
}))
return jsonToSerialize
}
module.exports = { toSerialize }

View File

@@ -1,14 +0,0 @@
const commonjs = require('@rollup/plugin-commonjs')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const { join } = require('path')
const PATH = join(__dirname, 'lib', 'io', 'bundle.io.cjs')
module.exports = {
input: 'index.js',
output: {
file: PATH,
format: 'cjs',
},
plugins: [commonjs(), nodeResolve()],
}

View File

@@ -1,33 +0,0 @@
{
"name": "@bot-whatsapp/bot",
"version": "0.0.1",
"description": "",
"main": "./lib/bundle.bot.cjs",
"private": true,
"scripts": {
"bot:rollup": "node ../../node_modules/.bin/rollup index.js --config ./rollup-cli.config.js",
"format:check": "prettier --check .",
"format:write": "prettier --write .",
"lint:check": "eslint .",
"lint:fix": "eslint --fix .",
"test.unit": "cross-env NODE_ENV=test node ../../node_modules/uvu/bin.js tests"
},
"keywords": [],
"files": [
"./lib/bundle.bot.cjs",
"./provider/*",
"./core/*",
"./io/*"
],
"author": "",
"license": "ISC",
"devDependencies": {
"@bot-whatsapp/cli": "*",
"@bot-whatsapp/database": "*",
"@bot-whatsapp/provider": "*",
"kleur": "^4.1.5"
},
"dependencies": {
"dotenv": "^16.0.3"
}
}

View File

@@ -1,29 +0,0 @@
const { EventEmitter } = require('node:events')
/**
* Esta clase debe siempre proporcionar los siguietes metodos
* sendMessage = Para enviar un mensaje
*
* @important
* Esta clase extiende de la clase del provider OJO
* Eventos
* - message
* - ready
* - error
* - require_action
*/
const NODE_ENV = process.env.NODE_ENV || 'dev'
class ProviderClass extends EventEmitter {
/**
* events: message | auth | auth_error | ...
*
*/
sendMessage = async (userId, message) => {
if (NODE_ENV !== 'production')
console.log('[sendMessage]', { userId, message })
return message
}
}
module.exports = ProviderClass

View File

@@ -1,14 +0,0 @@
const commonjs = require('@rollup/plugin-commonjs')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const { join } = require('path')
const PATH = join(__dirname, 'lib', 'bundle.bot.cjs')
module.exports = {
input: join(__dirname, 'index.js'),
output: {
file: PATH,
format: 'cjs',
},
plugins: [commonjs(), nodeResolve()],
}

View File

@@ -1,279 +0,0 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const FlowClass = require('../io/flow.class')
const MockProvider = require('../../../__mocks__/mock.provider')
const {
createBot,
CoreClass,
createFlow,
createProvider,
ProviderClass,
} = require('../index')
class MockFlow {
allCallbacks = [{ callback: () => console.log('') }]
flowSerialize = []
flowRaw = []
find = (arg) => {
if (arg) {
return [{ answer: 'answer', ref: 'ref' }]
} else {
return null
}
}
findBySerialize = () => ({})
findIndexByRef = () => 0
}
class MockDBA {
listHistory = []
save = () => {}
getPrevByNumber = () => {}
}
class MockDBB {
listHistory = []
save = () => {}
getPrevByNumber = () => ({
refSerialize: 'xxxxx',
ref: 'xxxx',
options: { callback: true },
})
}
class MockDBC {
listHistory = []
save = () => {}
getPrevByNumber = () => ({
refSerialize: 'xxxxx',
ref: 'xxxx',
options: { callback: true, nested: ['1', '2'] },
})
saveLog = () => {}
}
test(`[CoreClass] Probando instanciamiento de clase`, async () => {
const setting = {
flow: new MockFlow(),
database: new MockDBA(),
provider: new MockProvider(),
}
const bot = await createBot(setting)
assert.is(bot instanceof CoreClass, true)
})
test(`[CoreClass createFlow] Probando instanciamiento de clase`, async () => {
const mockCreateFlow = createFlow([])
assert.is(mockCreateFlow instanceof FlowClass, true)
})
test(`[CoreClass createProvider] Probando instanciamiento de clase`, async () => {
const mockCreateProvider = createProvider(MockProvider)
assert.is(mockCreateProvider instanceof ProviderClass, true)
})
test(`[Bot] Eventos 'require_action,ready,auth_failure,message '`, async () => {
let responseEvents = {}
const MOCK_EVENTS = {
require_action: {
instructions: 'Debes...',
},
ready: true,
auth_failure: {
instructions: 'Error...',
},
message: {
from: 'XXXXXX',
body: 'hola',
hasMedia: false,
},
}
const mockProvider = new MockProvider()
const setting = {
flow: new MockFlow(),
database: new MockDBA(),
provider: mockProvider,
}
await createBot(setting)
/// Escuchamos eventos
mockProvider.on(
'require_action',
(r) => (responseEvents['require_action'] = r)
)
mockProvider.on('ready', (r) => (responseEvents['ready'] = r))
mockProvider.on('auth_failure', (r) => (responseEvents['auth_failure'] = r))
mockProvider.on('message', (r) => (responseEvents['message'] = r))
/// Emitimos eventos
mockProvider.delaySendMessage(
0,
'require_action',
MOCK_EVENTS.require_action
)
mockProvider.delaySendMessage(0, 'ready', MOCK_EVENTS.ready)
mockProvider.delaySendMessage(0, 'auth_failure', MOCK_EVENTS.auth_failure)
mockProvider.delaySendMessage(0, 'message', MOCK_EVENTS.message)
await delay(0)
/// Testeamos eventos
assert.is(
JSON.stringify(responseEvents.require_action),
JSON.stringify(MOCK_EVENTS.require_action)
)
assert.is(responseEvents.ready, MOCK_EVENTS.ready)
assert.is(
JSON.stringify(responseEvents.auth_failure),
JSON.stringify(MOCK_EVENTS.auth_failure)
)
assert.is(
JSON.stringify(responseEvents.message),
JSON.stringify(MOCK_EVENTS.message)
)
})
test(`[Bot] Probando Flujos Internos`, async () => {
let responseEvents = {}
const MOCK_EVENTS = {
require_action: {
instructions: 'Debes...',
},
ready: true,
auth_failure: {
instructions: 'Error...',
},
message: {
from: 'XXXXXX',
body: 'hola',
hasMedia: false,
},
}
const mockProvider = new MockProvider()
const setting = {
flow: new MockFlow(),
database: new MockDBB(),
provider: mockProvider,
}
await createBot(setting)
/// Escuchamos eventos
mockProvider.on(
'require_action',
(r) => (responseEvents['require_action'] = r)
)
mockProvider.on('ready', (r) => (responseEvents['ready'] = r))
mockProvider.on('auth_failure', (r) => (responseEvents['auth_failure'] = r))
mockProvider.on('message', (r) => (responseEvents['message'] = r))
/// Emitimos eventos
mockProvider.delaySendMessage(
0,
'require_action',
MOCK_EVENTS.require_action
)
mockProvider.delaySendMessage(0, 'ready', MOCK_EVENTS.ready)
mockProvider.delaySendMessage(0, 'auth_failure', MOCK_EVENTS.auth_failure)
mockProvider.delaySendMessage(0, 'message', MOCK_EVENTS.message)
await delay(0)
/// Testeamos eventos
assert.is(
JSON.stringify(responseEvents.require_action),
JSON.stringify(MOCK_EVENTS.require_action)
)
assert.is(responseEvents.ready, MOCK_EVENTS.ready)
assert.is(
JSON.stringify(responseEvents.auth_failure),
JSON.stringify(MOCK_EVENTS.auth_failure)
)
assert.is(
JSON.stringify(responseEvents.message),
JSON.stringify(MOCK_EVENTS.message)
)
})
test(`[Bot] Probando Flujos Nested`, async () => {
let responseEvents = {}
const MOCK_EVENTS = {
require_action: {
instructions: 'Debes...',
},
ready: true,
auth_failure: {
instructions: 'Error...',
},
message: {
from: 'XXXXXX',
body: 'hola',
hasMedia: false,
},
}
const mockProvider = new MockProvider()
const setting = {
flow: new MockFlow(),
database: new MockDBC(),
provider: mockProvider,
}
const botInstance = await createBot(setting)
botInstance.sendProviderAndSave('xxxxx', 'xxxxx')
botInstance.continue('xxxxx', 'xxxxx')
/// Escuchamos eventos
mockProvider.on(
'require_action',
(r) => (responseEvents['require_action'] = r)
)
mockProvider.on('ready', (r) => (responseEvents['ready'] = r))
mockProvider.on('auth_failure', (r) => (responseEvents['auth_failure'] = r))
mockProvider.on('message', (r) => (responseEvents['message'] = r))
/// Emitimos eventos
mockProvider.delaySendMessage(
0,
'require_action',
MOCK_EVENTS.require_action
)
mockProvider.delaySendMessage(0, 'ready', MOCK_EVENTS.ready)
mockProvider.delaySendMessage(0, 'auth_failure', MOCK_EVENTS.auth_failure)
mockProvider.delaySendMessage(0, 'message', MOCK_EVENTS.message)
await delay(0)
/// Testeamos eventos
assert.is(
JSON.stringify(responseEvents.require_action),
JSON.stringify(MOCK_EVENTS.require_action)
)
assert.is(responseEvents.ready, MOCK_EVENTS.ready)
assert.is(
JSON.stringify(responseEvents.auth_failure),
JSON.stringify(MOCK_EVENTS.auth_failure)
)
assert.is(
JSON.stringify(responseEvents.message),
JSON.stringify(MOCK_EVENTS.message)
)
})
test.run()
function delay(ms) {
return new Promise((res) => setTimeout(res, ms))
}

View File

@@ -1,152 +0,0 @@
const { test } = require('uvu')
const assert = require('uvu/assert')
const { generateRefSerialize } = require('../utils/hash')
const { addKeyword, addAnswer, toSerialize } = require('../io/methods')
test('Debere probar las propeidades', () => {
const ARRANGE = {
keyword: 'hola!',
}
const MAIN_CTX = addKeyword(ARRANGE.keyword)
assert.type(MAIN_CTX.addAnswer, 'function')
assert.is(MAIN_CTX.ctx.keyword, ARRANGE.keyword)
})
test('Debere probar las propeidades array', () => {
const ARRANGE = {
keyword: ['hola!', 'ole'],
}
const MAIN_CTX = addKeyword(ARRANGE.keyword)
assert.is(MAIN_CTX.ctx.keyword, ARRANGE.keyword)
})
test('Debere probar toSerialize', () => {
const ARRANGE = {
keyword: ['hola!', 'ole'],
}
const MAIN_CTX = addKeyword(ARRANGE.keyword)
.addAnswer('Segundo!')
.addAnswer('Segundo!')
.toJson()
const [ANSWER_A] = MAIN_CTX
assert.is(
toSerialize(MAIN_CTX)[0].refSerialize,
generateRefSerialize({
index: 0,
answer: ANSWER_A.answer,
keyword: ANSWER_A.keyword,
})
)
})
test('Debere probar el paso de contexto', () => {
const ARRANGE = {
keyword: 'hola!',
answer: 'Bienvenido',
}
const CTX_A = addKeyword(ARRANGE.keyword)
const CTX_B = addAnswer(CTX_A)(ARRANGE.answer)
assert.is(CTX_A.ctx.keyword, ARRANGE.keyword)
assert.is(CTX_B.ctx.keyword, ARRANGE.keyword)
assert.is(CTX_B.ctx.answer, ARRANGE.answer)
})
test('Debere probar la anidación', () => {
const ARRANGE = {
keyword: 'hola!',
answer_A: 'Bienvenido',
answer_B: 'Continuar',
}
const MAIN_CTX = addKeyword(ARRANGE.keyword)
.addAnswer(ARRANGE.answer_A)
.addAnswer(ARRANGE.answer_B)
assert.is(MAIN_CTX.ctx.answer, ARRANGE.answer_B)
})
test('Debere probar las poptions', () => {
const MAIN_CTX = addKeyword('etc', { sensitive: false })
assert.is(MAIN_CTX.ctx.options.sensitive, false)
})
test('Debere probar las addAnswer', () => {
const MOCK_OPT = {
media: 'http://image.mock/mock.png',
buttons: [1],
}
const MAIN_CTX = addKeyword('hola').addAnswer('etc', MOCK_OPT)
assert.is(MAIN_CTX.ctx.options.media, MOCK_OPT.media)
assert.is(MAIN_CTX.ctx.options.buttons.length, 1)
})
test('Debere probar error las addAnswer', () => {
const MOCK_OPT = {
media: { a: 1, b: [] },
buttons: 'test',
}
const MAIN_CTX = addKeyword('hola').addAnswer('etc', MOCK_OPT)
assert.is(MAIN_CTX.ctx.options.media, null)
assert.is(MAIN_CTX.ctx.options.buttons.length, 0)
})
test('Obtener toJson', () => {
const [ctxA, ctxB, ctxC] = addKeyword('hola')
.addAnswer('pera!')
.addAnswer('chao')
.toJson()
assert.is(ctxA.keyword, 'hola')
assert.match(ctxA.ref, /^key_/)
assert.is(ctxB.answer, 'pera!')
assert.match(ctxB.ref, /^ans_/)
assert.is(ctxC.answer, 'chao')
assert.match(ctxC.ref, /^ans_/)
})
test('addKeyword toJson con sensitive', () => {
const [ctxA] = addKeyword('hola').toJson()
assert.is(ctxA.options.sensitive, false)
const [ctxB] = addKeyword('hola', { sensitive: true }).toJson()
assert.is(ctxB.options.sensitive, true)
})
test('addAnswer toJson con IMG', () => {
const [, ctxB, ctxC] = addKeyword('hola')
.addAnswer('bye!', {
media: 'http://mock.img/file-a.png',
})
.addAnswer('otro!', {
media: 'http://mock.img/file-b.png',
})
.toJson()
assert.is(ctxB.options.media, 'http://mock.img/file-a.png')
assert.is(ctxC.options.media, 'http://mock.img/file-b.png')
})
test('addAnswer toJson con BUTTONS', () => {
const [, ctxB] = addKeyword('hola')
.addAnswer('mis opciones!', {
buttons: [{ body: 'BTN_1' }, { body: 'BTN_2' }],
})
.toJson()
assert.is(ctxB.options.buttons.length, 2)
const [btnA, btnB] = ctxB.options.buttons
assert.is(btnA.body, 'BTN_1')
assert.is(btnB.body, 'BTN_2')
})
test.run()

View File

@@ -1,24 +0,0 @@
const crypto = require('crypto')
/**
* Generamos un UUID unico con posibilidad de tener un prefijo
* @param {*} prefix
* @returns
*/
const generateRef = (prefix = false) => {
const id = crypto.randomUUID()
return prefix ? `${prefix}_${id}` : id
}
/**
* Genera un HASH MD5
* @param {*} param0
* @returns
*/
const generateRefSerialize = ({ index, answer, keyword }) =>
crypto
.createHash('md5')
.update(JSON.stringify({ index, answer, keyword }))
.digest('hex')
module.exports = { generateRef, generateRefSerialize }

View File

@@ -1,14 +0,0 @@
const { yellow, bgRed } = require('kleur')
const NODE_ENV = process.env.NODE_ENV || 'dev'
const printer = (message, title) => {
if (NODE_ENV !== 'test') {
// console.clear()
if (title) console.log(bgRed(`${title}`))
console.log(
yellow(Array.isArray(message) ? message.join('\n') : message)
)
console.log(``)
}
}
module.exports = { printer }

View File

@@ -1,20 +0,0 @@
# @bot-whatsapp/cli
- [x] Revisar version de NODE
- [x] Revisar OS
- [x] Obtener Package Manager
- [x] Revisar las libreria de WhatsappWeb para obtener version reciente
- [x] Opcion interactiva de limpiar session
- [x] Opcion de generar `json` con la configuracion
- [x] Agregar `rollup` para limpiar el codigo
---
**Comunidad**
> Forma parte de este proyecto.
- [Discord](https://link.codigoencasa.com/DISCORD)
- [Twitter](https://twitter.com/leifermendez)
- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR)
- [Telegram](https://t.me/leifermendez)

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env node
const index = require('../lib/cli/bundle.cli.cjs')
index.startInteractive()

View File

@@ -1,38 +0,0 @@
const { red, yellow, green, bgCyan } = require('kleur')
const checkNodeVersion = () => {
console.log(bgCyan('🚀 Revisando tu Node.js'))
const version = process.version
const majorVersion = parseInt(version.replace('v', '').split('.').shift())
if (majorVersion < 16) {
console.error(
red(
`🔴 Se require Node.js 16 o superior. Actualmente esta ejecutando Node.js ${version}`
)
)
process.exit(1)
}
console.log(green(`Node.js combatible ${version}`))
console.log(``)
}
const checkOs = () => {
console.log(bgCyan('🙂 Revisando tu Sistema Operativo'))
const os = process.platform
if (!os.includes('win32')) {
const messages = [
`El sistema operativo actual (${os}) posiblemente requiera`,
`una confiuración adicional referente al puppeter`,
``,
`Recuerda pasar por el WIKI`,
`🔗 https://github.com/leifermendez/bot-whatsapp/wiki/Instalaci%C3%B3n`,
``,
]
console.log(yellow(messages.join(' \n')))
}
console.log(``)
}
module.exports = { checkNodeVersion, checkOs }

View File

@@ -1,19 +0,0 @@
const rimraf = require('rimraf')
const { yellow } = require('kleur')
const { join } = require('path')
const PATH_WW = [
join(process.cwd(), '.wwebjs_auth'),
join(process.cwd(), 'session.json'),
]
const cleanSession = () => {
const queue = []
for (const PATH of PATH_WW) {
console.log(yellow(`😬 Eliminando: ${PATH}`))
queue.push(rimraf(PATH, () => Promise.resolve()))
}
return Promise.all(queue)
}
module.exports = { cleanSession }

View File

@@ -1,33 +0,0 @@
const { writeFile } = require('fs').promises
const { join } = require('path')
/**
* JSON_TEMPLATE = {[key:string]{...pros}}
*/
const JSON_TEMPLATE = {
provider: {
vendor: '',
},
database: {
host: '',
password: '',
port: '',
username: '',
db: '',
},
io: {
vendor: '',
},
}
const PATH_CONFIG = join(process.cwd(), 'config.json')
const jsonConfig = () => {
return writeFile(
PATH_CONFIG,
JSON.stringify(JSON_TEMPLATE, null, 2),
'utf-8'
)
}
module.exports = { jsonConfig }

View File

@@ -1,3 +0,0 @@
const { startInteractive } = require('./interactive')
if (process.env.NODE_ENV === 'dev') startInteractive()
module.exports = { startInteractive }

View File

@@ -1,24 +0,0 @@
const { readFileSync, existsSync } = require('fs')
const { join } = require('path')
const { installDeps, getPkgManage } = require('./tool')
const PATHS_DIR = [
join(__dirname, 'pkg-to-update.json'),
join(__dirname, '..', 'pkg-to-update.json'),
join(__dirname, '..', '..', 'pkg-to-update.json'),
]
const PKG_TO_UPDATE = () => {
const PATH_INDEX = PATHS_DIR.findIndex((a) => existsSync(a))
const data = readFileSync(PATHS_DIR[PATH_INDEX], 'utf-8')
const dataParse = JSON.parse(data)
const pkg = Object.keys(dataParse).map((n) => `${n}@${dataParse[n]}`)
return pkg
}
const installAll = async () => {
const pkg = await getPkgManage()
installDeps(pkg, PKG_TO_UPDATE()).runInstall()
}
module.exports = { installAll }

View File

@@ -1,68 +0,0 @@
const { red } = require('kleur')
const spawn = require('cross-spawn')
// const { detect } = require('detect-package-manager')
const PKG_OPTION = {
npm: 'install',
yarn: 'add',
pnpm: 'add',
}
const getPkgManage = async () => {
// const pkg = await detect()
// return pkg
return 'npm'
}
const installDeps = (pkgManager, packageList) => {
const errorMessage = `Ocurrio un error instalando ${packageList}`
let childProcess = []
const installSingle = (pkgInstall) => () => {
new Promise((resolve) => {
try {
childProcess = spawn(
pkgManager,
[PKG_OPTION[pkgManager], pkgInstall],
{
stdio: 'inherit',
}
)
childProcess.on('error', (e) => {
console.error(e)
console.error(red(errorMessage))
resolve()
})
childProcess.on('close', (code) => {
if (code === 0) {
resolve()
} else {
console.error(code)
console.error(red(errorMessage))
}
})
resolve()
} catch (e) {
console.error(e)
console.error(red(errorMessage))
}
})
}
if (typeof packageList === 'string') {
childProcess.push(installSingle(packageList))
} else {
for (const pkg of packageList) {
childProcess.push(installSingle(pkg))
}
}
const runInstall = () => {
return Promise.all(childProcess.map((i) => i()))
}
return { runInstall }
}
module.exports = { getPkgManage, installDeps }

View File

@@ -1,127 +0,0 @@
const prompts = require('prompts')
const { yellow, red } = require('kleur')
const { installAll } = require('../install')
const { cleanSession } = require('../clean')
const { checkNodeVersion, checkOs } = require('../check')
const { jsonConfig } = require('../configuration')
const startInteractive = async () => {
const questions = [
{
type: 'text',
name: 'dependencies',
message:
'Quieres actualizar las librerias "whatsapp-web.js"? (Y/n)',
},
{
type: 'text',
name: 'cleanTmp',
message: 'Quieres limpiar la session del bot? (Y/n)',
},
{
type: 'multiselect',
name: 'providerWs',
message: 'Proveedor de Whatsapp',
choices: [
{ title: 'whatsapp-web.js', value: 'whatsapp-web.js' },
{ title: 'API Oficial (Meta)', value: 'meta', disabled: true },
{ title: 'Twilio', value: 'twilio', disabled: true },
],
max: 1,
hint: 'Espacio para selecionar',
instructions: '↑/↓',
},
{
type: 'multiselect',
name: 'providerDb',
message: 'Cual base de datos quieres usar',
choices: [
{ title: 'JSONFile', value: 'json' },
{ title: 'MySQL', value: 'mysql', disabled: true },
{ title: 'Mongo', value: 'mongo', disabled: true },
],
max: 1,
hint: 'Espacio para selecionar',
instructions: '↑/↓',
},
]
console.clear()
checkNodeVersion()
checkOs()
const onCancel = () => {
console.log('Proceso cancelado!')
return true
}
const response = await prompts(questions, { onCancel })
const {
dependencies = '',
cleanTmp = '',
providerDb = [],
providerWs = [],
} = response
/**
* Question #1
* @returns
*/
const installOrUdpateDep = async () => {
const answer = dependencies.toLowerCase() || 'n'
if (answer.includes('n')) return true
if (answer.includes('y')) {
await installAll()
return true
}
}
/**
* Question #2
* @returns
*/
const cleanAllSession = async () => {
const answer = cleanTmp.toLowerCase() || 'n'
if (answer.includes('n')) return true
if (answer.includes('y')) {
await cleanSession()
return true
}
}
const vendorProvider = async () => {
if (!providerWs.length) {
console.log(
red(
`Debes de seleccionar una WS Provider. Tecla [Space] para seleccionar`
)
)
process.exit(1)
}
console.log(yellow(`'Deberia crer una carpeta en root/provider'`))
return true
}
const dbProvider = async () => {
const answer = providerDb
if (!providerDb.length) {
console.log(
red(
`Debes de seleccionar una DB Provider. Tecla [Space] para seleccionar`
)
)
process.exit(1)
}
if (answer === 'json') {
console.log('Deberia crer una carpeta en root/data')
return 1
}
}
await installOrUdpateDep()
await cleanAllSession()
await vendorProvider()
await dbProvider()
await jsonConfig()
}
module.exports = { startInteractive }

View File

@@ -1,19 +0,0 @@
{
"name": "@bot-whatsapp/cli",
"version": "0.0.1",
"description": "",
"main": "index.js",
"private": true,
"devDependencies": {
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.3",
"detect-package-manager": "^2.0.1",
"kleur": "^4.1.5"
},
"files": [
"./lib/cli/bundle.cli.cjs"
],
"bin": {
"bot": "./bin/cli.js"
}
}

View File

@@ -1,3 +0,0 @@
{
"whatsapp-web.js": "latest"
}

View File

@@ -1,14 +0,0 @@
const commonjs = require('@rollup/plugin-commonjs')
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const { join } = require('path')
const PATH = join(__dirname, 'lib', 'cli', 'bundle.cli.cjs')
module.exports = {
input: join(__dirname, 'index.js'),
output: {
file: PATH,
format: 'cjs',
},
plugins: [commonjs(), nodeResolve()],
}

View File

@@ -1,41 +0,0 @@
### 🚀 Package (@bot-whatsapp/database)
Este package tiene como reponsabilidad proveer de diferentes adaptadores para la capa de datos.
La idea es brindar multiples opciones como un adaptador de MySQL, Mongo, entre otros.
Ejemplo de como se implementaria:
```js
const MongoAdapter = require('@bot-whatsapp/database/mongo')
/// o
const MySQLAdapter = require('@bot-whatsapp/database/mysql')
const main = async () => {
const adapterDB = new MongoAdapter()
const adapterFlow = createFlow([flujoBot])
const adapterProvider = createProvider(WebWhatsappProvider)
createBot({
flow: adapterFlow,
provider: adapterProvider,
database: adapterDB,
})
}
```
#### Video
> Video explicando como debes de agregar nuevos adaptadores
[![Video](https://i.imgur.com/DlxJIKV.gif)](https://youtu.be/Sjzkpg1OJuY)
---
**Comunidad**
> Forma parte de este proyecto.
- [Discord](https://link.codigoencasa.com/DISCORD)
- [Twitter](https://twitter.com/leifermendez)
- [Youtube](https://www.youtube.com/watch?v=5lEMCeWEJ8o&list=PL_WGMLcL4jzWPhdhcUyhbFU6bC0oJd2BR)
- [Telegram](https://t.me/leifermendez)

View File

@@ -1,19 +0,0 @@
{
"name": "@bot-whatsapp/database",
"version": "0.0.1",
"description": "Esto es el conector a mysql, pg, mongo",
"main": "./lib/mock/index.cjs",
"private": true,
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {},
"dependencies": {
"dotenv": "^16.0.3",
"mongodb": "^4.11.0"
},
"exports": {
"./mock": "./lib/mock/index.cjs",
"./mongo": "./lib/mongo/index.cjs"
}
}

View File

@@ -1,21 +0,0 @@
const commonjs = require('@rollup/plugin-commonjs')
const { join } = require('path')
module.exports = [
{
input: join(__dirname, 'src', 'mock', 'index.js'),
output: {
file: join(__dirname, 'lib', 'mock', 'index.cjs'),
format: 'cjs',
},
plugins: [commonjs()],
},
{
input: join(__dirname, 'src', 'mongo', 'index.js'),
output: {
file: join(__dirname, 'lib', 'mongo', 'index.cjs'),
format: 'cjs',
},
plugins: [commonjs()],
},
]

View File

@@ -1,17 +0,0 @@
class MockDatabase {
listHistory = []
constructor() {
/**
* Se debe cargar listHistory con historial de mensajes
* para que se pueda continuar el flow
*/
}
save = (ctx) => {
console.log('Guardando DB...', ctx)
this.listHistory.push(ctx)
}
}
module.exports = MockDatabase

View File

@@ -1,46 +0,0 @@
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() {
this.init().then()
}
init = async () => {
try {
const client = new MongoClient(DB_URI, {})
await client.connect()
console.log('🆗 Conexión Correcta DB')
const db = client.db(DB_NAME)
this.db = db
return true
} catch (e) {
console.log('Error', e)
return
}
}
getPrevByNumber = async (from) => {
const result = await this.db
.collection('history')
.find({ from })
.sort({ _id: -1 })
.limit(1)
.toArray()
return result[0]
}
save = async (ctx) => {
await this.db.collection('history').insert(ctx)
console.log('Guardando DB...', ctx)
this.listHistory.push(ctx)
}
}
module.exports = MongoAdapter

View File

@@ -1,33 +0,0 @@
**/*.log
**/.DS_Store
*.
.vscode/settings.json
.history
.yarn
bazel-*
bazel-bin
bazel-out
bazel-qwik
bazel-testlogs
dist
dist-dev
lib
lib-types
etc
external
node_modules
temp
tsc-out
tsdoc-metadata.json
target
output
rollup.config.js
build
.cache
.vscode
.rollup.cache
dist
tsconfig.tsbuildinfo
vite.config.ts
*.spec.tsx
*.spec.ts

View File

@@ -1,40 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:qwik/recommended',
],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-inferrable-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-empty-interface': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'prefer-spread': 'off',
'no-case-declarations': 'off',
'no-console': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
},
};

View File

@@ -1,41 +0,0 @@
# Build
/dist
/lib
/lib-types
/server
# Development
node_modules
# Cache
.cache
.mf
.vscode
.rollup.cache
tsconfig.tsbuildinfo
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Editor
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Yarn
.yarn/*
!.yarn/releases
# Cloudflare
functions/**/*.js

View File

@@ -1 +0,0 @@
16

View File

@@ -1,6 +0,0 @@
# Files Prettier should not format
**/*.log
**/.DS_Store
*.
dist
node_modules

View File

@@ -1,11 +0,0 @@
### 😎 Documentación Bot-Whatsapp
👉 [https://bot-whatsapp.pages.dev/](https://bot-whatsapp.pages.dev/)
Se esta iniciando una documentación oficial sobre como usar e implementar los diferentes funcionalidades del bot-wahtsapp
La idea es cada usuario pueda ir aportando a la documentacion y formar parte de este proyecto.
##### ¿Como agregar documentación? [Video]

View File

@@ -1,19 +0,0 @@
import { cloudflarePagesAdaptor } from '@builder.io/qwik-city/adaptors/cloudflare-pages/vite';
import { extendConfig } from '@builder.io/qwik-city/vite';
import baseConfig from '../../vite.config';
export default extendConfig(baseConfig, () => {
return {
build: {
ssr: true,
rollupOptions: {
input: ['src/entry.cloudflare-pages.tsx', '@qwik-city-plan'],
},
},
plugins: [
cloudflarePagesAdaptor({
staticGenerate: true,
}),
],
};
});

View File

@@ -1,5 +0,0 @@
// @ts-ignore
// Cloudflare Pages Functions
// https://developers.cloudflare.com/pages/platform/functions/
export { onRequest } from '../server/entry.cloudflare-pages';

View File

@@ -1,41 +0,0 @@
{
"name": "bot-whatsapp-docs",
"version": "0.0.1",
"description": "Basic start point to build a docs site with Qwik",
"engines": {
"node": ">=15.0.0"
},
"private": true,
"scripts": {
"build": "qwik build",
"build.client": "vite build",
"build.preview": "vite build --ssr src/entry.preview.tsx",
"build.server": "vite build -c adaptors/cloudflare-pages/vite.config.ts",
"build.types": "tsc --incremental --noEmit",
"deploy": "wrangler pages dev ./dist",
"dev": "vite --mode ssr",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
"lint": "eslint \"src/**/*.ts*\"",
"preview": "qwik build preview && vite preview --open",
"start": "vite --open --mode ssr",
"qwik": "qwik"
},
"devDependencies": {
"@builder.io/qwik": "0.14.1",
"@builder.io/qwik-city": "0.0.127",
"@types/eslint": "8.4.10",
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"eslint": "8.28.0",
"eslint-plugin-qwik": "0.14.1",
"node-fetch": "3.3.0",
"prettier": "2.7.1",
"typescript": "4.9.3",
"vite": "3.2.4",
"vite-tsconfig-paths": "3.5.0",
"wrangler": "latest"
}
}

View File

@@ -1,4 +0,0 @@
# https://developers.cloudflare.com/pages/platform/headers/
/build/*
Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable

View File

@@ -1 +0,0 @@
# https://developers.cloudflare.com/pages/platform/redirects/

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 500 500"><g clip-path="url(#a)"><circle cx="250" cy="250" r="250" fill="#fff"/><path fill="#18B6F6" d="m367.87 418.45-61.17-61.18-.94.13v-.67L175.7 227.53l32.05-31.13L188.9 87.73 99.56 199.09c-15.22 15.42-18.03 40.51-7.08 59.03l55.83 93.11a46.82 46.82 0 0 0 40.73 22.81l27.65-.27 151.18 44.68Z"/><path fill="#AC7EF4" d="m401.25 196.94-12.29-22.81-6.41-11.67-2.54-4.56-.26.26-33.66-58.63a47.07 47.07 0 0 0-41.27-23.75l-29.51.8-88.01.28a47.07 47.07 0 0 0-40.33 23.34L93.4 207l95.76-119.54L314.7 226.19l-22.3 22.67 13.35 108.54.13-.26v.26h-.26l.26.27 10.42 10.2 50.62 49.78c2.13 2 5.6-.4 4.13-2.96l-31.25-61.85 54.5-101.3 1.73-2c.67-.81 1.33-1.62 1.87-2.42a46.8 46.8 0 0 0 3.34-50.18Z"/><path fill="#fff" d="M315.1 225.65 189.18 87.6l17.9 108.14L175 227l130.5 130.27-11.75-108.14 21.37-23.48Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h500v500H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 947 B

View File

@@ -1,9 +0,0 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "qwik-project-name",
"short_name": "Welcome to Qwik",
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "A Qwik project app."
}

View File

@@ -1,25 +0,0 @@
nav.breadcrumbs {
padding: 5px;
border-bottom: 1px solid #ddd;
}
nav.breadcrumbs > span {
display: inline-block;
padding: 5px 0;
font-size: 12px;
}
nav.breadcrumbs > span a {
text-decoration: none;
color: inherit;
}
nav.breadcrumbs > span::after {
content: '>';
padding: 0 5px;
opacity: 0.4;
}
nav.breadcrumbs > span:last-child::after {
display: none;
}

View File

@@ -1,74 +0,0 @@
import { component$, useStyles$ } from '@builder.io/qwik';
import { useContent, useLocation, ContentMenu } from '@builder.io/qwik-city';
import styles from './breadcrumbs.css?inline';
export const Breadcrumbs = component$(() => {
useStyles$(styles);
const { menu } = useContent();
const loc = useLocation();
const breadcrumbs = createBreadcrumbs(menu, loc.pathname);
if (breadcrumbs.length === 0) {
return null;
}
return (
<nav class="breadcrumbs">
{breadcrumbs.map((b) => (
<span>{b.href ? <a href={b.href}>{b.text}</a> : b.text}</span>
))}
</nav>
);
});
export function createBreadcrumbs(menu: ContentMenu | undefined, pathname: string) {
if (menu?.items) {
for (const indexA of menu.items) {
const breadcrumbA: ContentBreadcrumb = {
text: indexA.text,
};
if (typeof indexA.href === 'string') {
breadcrumbA.href = indexA.href;
}
if (indexA.href === pathname) {
return [breadcrumbA];
}
if (indexA.items) {
for (const indexB of indexA.items) {
const breadcrumbB: ContentBreadcrumb = {
text: indexB.text,
};
if (typeof indexB.href === 'string') {
breadcrumbB.href = indexB.href;
}
if (indexB.href === pathname) {
return [breadcrumbA, breadcrumbB];
}
if (indexB.items) {
for (const indexC of indexB.items) {
const breadcrumbC: ContentBreadcrumb = {
text: indexC.text,
};
if (typeof indexC.href === 'string') {
breadcrumbC.href = indexC.href;
}
if (indexC.href === pathname) {
return [breadcrumbA, breadcrumbB, breadcrumbC];
}
}
}
}
}
}
}
return [];
}
interface ContentBreadcrumb {
text: string;
href?: string;
}

View File

@@ -1,22 +0,0 @@
footer {
border-top: 0.5px solid #ddd;
margin-top: 40px;
padding: 20px;
text-align: center;
}
footer a {
color: #9e9e9e;
font-size: 12px;
}
footer ul {
list-style: none;
margin: 0;
padding: 0;
}
footer li {
display: inline-block;
padding: 6px 12px;
}

Some files were not shown because too many files have changed in this diff Show More