├── flow ├── .gitkeep ├── api.json ├── vendor.json ├── steps.json ├── messages.json ├── products.bak └── products.json ├── chats └── .gitkeep ├── media └── .gitkeep ├── mediaSend ├── .gitkeep ├── pc-1.jpg ├── pc-2.jpg ├── pc-3.jpg ├── abrigo-1.jpg ├── abrigo-2.jpg ├── close-1.webp ├── close-2.webp ├── iphone-1.jpg ├── iphone-2.jpg ├── laptop-1.jpg ├── laptop-2.jpg └── abrigo-main.jpg ├── .gitignore ├── .env.example ├── .env ├── src ├── mail.js ├── api.js └── app.js ├── package.json └── README.md /flow/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chats/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mediaSend/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mediaSend/pc-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/pc-1.jpg -------------------------------------------------------------------------------- /mediaSend/pc-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/pc-2.jpg -------------------------------------------------------------------------------- /mediaSend/pc-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/pc-3.jpg -------------------------------------------------------------------------------- /mediaSend/abrigo-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/abrigo-1.jpg -------------------------------------------------------------------------------- /mediaSend/abrigo-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/abrigo-2.jpg -------------------------------------------------------------------------------- /mediaSend/close-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/close-1.webp -------------------------------------------------------------------------------- /mediaSend/close-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/close-2.webp -------------------------------------------------------------------------------- /mediaSend/iphone-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/iphone-1.jpg -------------------------------------------------------------------------------- /mediaSend/iphone-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/iphone-2.jpg -------------------------------------------------------------------------------- /mediaSend/laptop-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/laptop-1.jpg -------------------------------------------------------------------------------- /mediaSend/laptop-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/laptop-2.jpg -------------------------------------------------------------------------------- /mediaSend/abrigo-main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leifermendez/bot-ventas/HEAD/mediaSend/abrigo-main.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /node_modules/* 3 | session.json 4 | 5 | chats/* 6 | qr-code.svg 7 | !chats/.gitkeep 8 | !mediaSend/.gitkeep 9 | !media/.gitkeep 10 | !flow/.gitkeep 11 | .env -------------------------------------------------------------------------------- /flow/api.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "*Nombre*", 3 | "description": "*Descrpcion*", 4 | "precio_a": "*Precio A*", 5 | "precio_b": "*Precio B*", 6 | "precio_c": "*Precio C*", 7 | "precio_d": "*Precio D*" 8 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MAIL_PORT=2525 2 | MAIL_SMTP=smtp.mailtrap.io 3 | MAIL_USER=ssss 4 | MAIL_PASS=ss 5 | MAIL_CLIENT=le3ifer3@gmail.com 6 | MAIL_FROM=hola@hola.com 7 | MAIL_SUBJECT=Cliente interesado 8 | MAIL_TRANSPORT=sendgrid 9 | EXTERNAL_API= -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MAIL_PORT=465 2 | MAIL_SMTP=smtp.gmail.com 3 | MAIL_USER=juandatm07@gmail.com 4 | MAIL_PASS=CONTRASEÑA_GENERADA 5 | MAIL_CLIENT=juandatm07@gmail.com 6 | MAIL_FROM=juandatm07@gmail.com 7 | MAIL_SUBJECT=Cliente interesado 8 | MAIL_TRANSPORT=smtp 9 | EXTERNAL_API=http://f54b42d.online-server.cloud:5192/CreditosTorresAp/api/Products/ByCategoryId -------------------------------------------------------------------------------- /flow/vendor.json: -------------------------------------------------------------------------------- 1 | { 2 | "madrid": [ 3 | "Este es el contacto:", 4 | "LEIFER MENDEZ 3333333333333", 5 | "encargado del municipio de Madrid" 6 | ], 7 | "valencia": [ 8 | "Este es el contacto:", 9 | "LEIFER MENDEZ 3333333333333", 10 | "encargado del municipio de Valencia" 11 | ], 12 | "bogota": [ 13 | "Este es el contacto:", 14 | "LEIFER MENDEZ 3333333333333", 15 | "encargado del municipio de Valencia" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/mail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | const sendMail = async (dataMail) => { 4 | let transporter; 5 | 6 | const mailOptions = { 7 | from: process.env.MAIL_FROM, 8 | to: process.env.MAIL_CLIENT, 9 | subject: process.env.MAIL_SUBJECT, 10 | html: dataMail 11 | } 12 | 13 | transporter = nodemailer.createTransport({ 14 | host: process.env.MAIL_SMTP, 15 | port: process.env.MAIL_PORT, 16 | secure: true, 17 | auth: { 18 | user: process.env.MAIL_USER, 19 | pass: process.env.MAIL_PASS 20 | } 21 | }); 22 | 23 | 24 | const info = await transporter.sendMail(mailOptions); 25 | console.log("Message sent: %s", info.messageId, process.env.MAIL_FROM); 26 | 27 | } 28 | 29 | module.exports = { sendMail } -------------------------------------------------------------------------------- /flow/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "STEP_1": [ 3 | "hola", 4 | "ola", 5 | "ole", 6 | "inicio", 7 | "welcome", 8 | "buenos días", 9 | "buenas tardes", 10 | "buenas noches", 11 | "me dieron este número", 12 | "venden a crédito", 13 | "quisiera saber si venden", 14 | "necesito saber" 15 | ], 16 | "STEP_2": [ 17 | "catalogo", 18 | "productos", 19 | "categoría" 20 | ], 21 | "STEP_3": [ 22 | "asesor", 23 | "asesores", 24 | "Vendedor", 25 | "cobrador" 26 | ], 27 | "STEP_4": [ 28 | "muchas gracias", 29 | "ok", 30 | "gracias", 31 | "vale gracias" 32 | ], 33 | "STEP_5": [ 34 | "hacer pedido" 35 | ], 36 | "STEP_5_5": [ 37 | "si", 38 | "correcto" 39 | ] 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-ws-bot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./src/app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@sendgrid/mail": "^7.6.0", 15 | "axios": "^0.21.4", 16 | "chalk": "^4.1.2", 17 | "dotenv": "^8.6.0", 18 | "excel4node": "^1.7.2", 19 | "exceljs": "^4.3.0", 20 | "express": "^4.17.1", 21 | "file-type": "^16.5.3", 22 | "mime-db": "^1.51.0", 23 | "moment": "^2.29.1", 24 | "nodemailer": "^6.7.2", 25 | "nodemon": "^2.0.15", 26 | "ora": "^5.4.1", 27 | "qr-image": "^3.2.0", 28 | "qrcode-terminal": "^0.12.0", 29 | "whatsapp-web.js": "^1.15.2", 30 | "xlsx": "^0.16.9" 31 | } 32 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __Antes de iniciar__ 2 | 3 | [Ver Video](https://www.youtube.com/watch?v=iCSVcEq17rA) 4 | 5 | Lo primero que debes hacer es asegurarte de tener instalado NODE. 6 | 7 | > Debes de tener instalado NODE si no sabes como instalarlo te dejo un video en el cual explico como instalar node https://www.youtube.com/watch?v=6741ceWzsKQ&list=PL_WGMLcL4jzVY1y-SutA3N_PCNCAG7Y46&index=2&t=50s Minuto 0:50 8 | 9 | __Instalacion__ 10 | Debes instalar los paquetes necesarios 11 | ``` 12 | npm install 13 | ``` 14 | 15 | Luego debes de escanear el codigo QR puedes hacerlo escaneando en la terminal o en el navegador 16 | http://localhost:9000/qr 17 | 18 | Una vez escaneado y vinculada tu cuenta de Whatsapp puedes empezara probar el BOT 19 | 20 | __Steps__ 21 | En los archivos ` .json` donde se encuentran las palabras claves deben estar en __minuscula__ no importa si el usuario la escribe en otro 22 | formato ya que el script se encarga de interpretarlas en minusculas. 23 | 24 | __Envio de MAIL__ 25 | Debes te crear un archivo llamado `.env` el cual debe de tener las siguientes variables 26 | ``` 27 | MAIL_PORT=465 28 | MAIL_SMTP=smtp.gmail.com 29 | MAIL_USER=tumail@gmail.com 30 | MAIL_PASS=TU_CONTRASEÑA_GENERADA 31 | MAIL_CLIENT=email_donde_quieres_recibir@mail.com 32 | MAIL_FROM=tumail@gmail.com 33 | MAIL_SUBJECT=Cliente interesado 34 | MAIL_TRANSPORT=smtp 35 | ``` 36 | 37 | __Formatos de Mensaje__ 38 | `\n` Salto de linea 39 | `*PALABRA*` Negrito 40 | `_PALABRA_` Cursiva 41 | 42 | __Instalacion en Ubuntu__ 43 | ``` 44 | npm install pm2 -g 45 | sudo apt-get install -y libgbm-dev 46 | 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 47 | ``` -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const apiLabel = require('../flow/api.json') 3 | 4 | const url = process.env.EXTERNAL_API || null; 5 | 6 | const getProductFrom = async (id) => { 7 | try { 8 | /** 9 | * En este punto nos conectamos a la API externa y pedimos los productos 10 | * basados en el ID de categoria 11 | */ 12 | const response = await axios.get(`${url}/${id}`) 13 | console.log(response); 14 | return response.data 15 | } catch (e) { 16 | console.error(e); 17 | } 18 | } 19 | 20 | const checkApi = () => { 21 | return url 22 | } 23 | 24 | const parseData = async (findChild) => { 25 | try { 26 | if (checkApi() && findChild && findChild.external_category_id) { 27 | const idCategory = findChild.external_category_id; 28 | const productsById = await getProductFrom(idCategory); 29 | console.log('---->', productsById); 30 | 31 | 32 | /** 33 | * Esta parte deberia existir un endpint para pedir info del producto 34 | * individual pero como no esta hacemos esto 35 | */ 36 | 37 | const parseData = (Array.isArray(productsById)) ? productsById : []; 38 | 39 | const single = parseData.find(i => i.Id === findChild.external_product_id); 40 | 41 | return [ 42 | `${apiLabel.name} ${single.NombreProducto} \n`, 43 | `${apiLabel.description} ${single.Descripcion} \n`, 44 | [ 45 | `${[apiLabel.precio_a]}: ${single.PrecioInicial} `, 46 | `${[apiLabel.precio_b]}: ${single.PrecioCredito} `, 47 | `${[apiLabel.precio_c]}: ${single.PrecioVenta} `, 48 | `${[apiLabel.precio_d]}: ${single.PrecioCrediContado} ` 49 | ].join('\n') 50 | ] 51 | } else { 52 | return []; 53 | } 54 | } catch (e) { 55 | console.log(`ERROR`, e); 56 | return []; 57 | } 58 | } 59 | 60 | module.exports = { getProductFrom, checkApi, parseData } -------------------------------------------------------------------------------- /flow/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "STEP_1": [ 3 | "🅷🅾🅻🅰 ¡Bienvenid@ a mi chat de WhatsApp! 📱💻💰 \n", 4 | " *Te saluda CrediBot* � el asistente virtual 💻 de Créditos Torres J.L🖐️😃", 5 | " 👇 ", 6 | " ‼️ En qué puedo servirte? ‼️", 7 | " � Si necesitas saber sobre algún productos 🛋️🛒escribe la palabra ‼️ Catalogo y te mostrare nuestras categorías😉", 8 | "❓O si necesitas algún asesor escribe la palabra Asesor 👨🏻�" 9 | ], 10 | "STEP_2": [ 11 | "Perfecto estas son nuestras categorías. 🙂🤖�" 12 | ], 13 | "STEP_2_1": [ 14 | "Lo siento no tenemos esa categoria" 15 | ], 16 | "STEP_2_2": [ 17 | "Recuerda que puedes escribir Asesor" 18 | ], 19 | "STEP_2_3": [ 20 | "Si estas interesado en este producto escribe *hacer pedido* y si quieres", 21 | "ver otro articulo escribe la palabra *productos*" 22 | ], 23 | "STEP_2_4": [ 24 | "Buscando el mejor precio ...." 25 | ], 26 | "STEP_3": [ 27 | "De que municipio eres?" 28 | ], 29 | "STEP_3_1": [ 30 | "Lo siento no tenemos asesor intenta con otro municipio" 31 | ], 32 | "STEP_4": [ 33 | "Fue un placer hablar contigo ", 34 | "Si necesitas algo más no dudes en contactarme" 35 | ], 36 | "STEP_5": [ 37 | "Cual es tu nombre?" 38 | ], 39 | "STEP_5_1": [ 40 | "Que producto necesitas?" 41 | ], 42 | "STEP_5_2": [ 43 | "Tu forma de pago cual es: crédito? credicontado? contado?" 44 | ], 45 | "STEP_5_3": [ 46 | "¿De que municipio eres?" 47 | ], 48 | "STEP_5_4": [ 49 | "Tus datos son \n", 50 | "Nombre: *%NAME%* \n", 51 | "Municipio: *%LOCATION%* \n", 52 | "estas interesado en un *%PRODUCT%* \n", 53 | "y lo quieres comprar a *%METHOD%* \n", 54 | "Esta informacion es correct? " 55 | ], 56 | "STEP_5_5": [ 57 | "Esta informacion es correcta? De ser correcta responde *SI* de lo contrario *Hacer pedido*" 58 | ], 59 | "STEP_5_6": [ 60 | "Ok perfecto muy pronto un asesor lo estará contactando" 61 | ], 62 | "STEP_5_7": [ 63 | "Nuevo cliente sus datos son \n", 64 | "Nombre: %NAME% \n", 65 | "Municipio: %LOCATION% \n", 66 | "estas interesado en un %PRODUCT% \n", 67 | "y lo quieres comprar a %METHOD% \n \n", 68 | "Su numero de contacto es %USERPHONE%" 69 | ], 70 | "ERROR": [ 71 | "Ups lo siento no puedo comprenderte" 72 | ] 73 | } -------------------------------------------------------------------------------- /flow/products.bak: -------------------------------------------------------------------------------- 1 | { 2 | "laptop": { 3 | "label": " *Laptop* \n", 4 | "main_images": [ 5 | { 6 | "message": [ 7 | "*(1)* MSI Laptop Creator " 8 | ], 9 | "image": "laptop-1.jpg" 10 | }, 11 | { 12 | "message": [ 13 | "*(2)* ASUS Vivobook " 14 | ], 15 | "image": "laptop-2.jpg" 16 | } 17 | ], 18 | "main_message": [ 19 | "Marca el número para obtener más infocamación del producto" 20 | ], 21 | "list": [ 22 | { 23 | "message": [ 24 | "MSI Laptop Creator 17 A10SGS-252 \n", 25 | "Intel Core i7 10th Gen 10875H (2.30 GHz) 32 GB Memory 2 TB \n", 26 | " NVMe SSD NVIDIA GeForce RTX 2080 Super Max-Q 17.3 \n", 27 | "*Precio $3,468.00*" 28 | ], 29 | "image": "laptop-1.jpg", 30 | "opt": 1 31 | }, 32 | { 33 | "message": [ 34 | "ASUS Vivobook X712DA-202.MV \n", 35 | "Home and Business Laptop \n", 36 | "(AMD Ryzen 7 3700U 4-Core, 12GB RAM, 512GB SSD, 17.3 \n", 37 | "*Precio $719.97*" 38 | ], 39 | "image": "laptop-2.jpg", 40 | "opt": 2 41 | } 42 | ] 43 | }, 44 | "pc": { 45 | "label": " *pc* \n", 46 | "main_images": [ 47 | { 48 | "message": [ 49 | "*(1)* iBUYPOWER Slate 4 163A " 50 | ], 51 | "image": "pc-1.jpg" 52 | }, 53 | { 54 | "message": [ 55 | "*(2)* ASUS ROG Strix G15CK " 56 | ], 57 | "image": "pc-2.jpg" 58 | } 59 | ], 60 | "main_message": [ 61 | "Marca el número para obtener más infocamación del producto" 62 | ], 63 | "list": [ 64 | { 65 | "message": [ 66 | "iBUYPOWER Slate 4 163A - AMD Ryzen 5 3600 - Radeon RX 580 \n", 67 | "16 GB DDR4 - 1 TB HDD - 240 GB SATA SSD - Windows 10 \n", 68 | "Home - Gaming Desktop \n", 69 | "*Precio $1,099.99*" 70 | ], 71 | "image": "pc-1.jpg", 72 | "opt": 1 73 | }, 74 | { 75 | "message": [ 76 | "ASUS ROG Strix G15CK Gaming Desktop PC, Intel Core i5-10400F \n", 77 | "GeForce GTX 1660 SUPER, 8 GB DDR4 RAM, 512 GB SSD \n", 78 | "Wi-Fi 6, Windows 10 Home, Star Black, G15CK-BS562 \n", 79 | "*Precio $999.00*" 80 | ], 81 | "image": "pc-2.jpg", 82 | "opt": 2 83 | } 84 | ] 85 | } 86 | } -------------------------------------------------------------------------------- /flow/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "laptop": { 3 | "label": " *Laptop* \n", 4 | "main_images": [ 5 | { 6 | "message": [ 7 | "*(1)* MSI Laptop Creator " 8 | ], 9 | "image": "laptop-1.jpg" 10 | }, 11 | { 12 | "message": [ 13 | "*(2)* ASUS Vivobook " 14 | ], 15 | "image": "laptop-2.jpg" 16 | } 17 | ], 18 | "main_message": [ 19 | "Marca el número para obtener más infocamación del producto" 20 | ], 21 | "list": [ 22 | { 23 | "message": [ 24 | "MSI Laptop Creator 17 A10SGS-252 \n", 25 | "Intel Core i7 10th Gen 10875H (2.30 GHz) 32 GB Memory 2 TB \n", 26 | " NVMe SSD NVIDIA GeForce RTX 2080 Super Max-Q 17.3 \n" 27 | ], 28 | "image": [ 29 | "laptop-1.jpg", 30 | "pc-3.jpg" 31 | ], 32 | "opt": 1, 33 | "external_category_id": 9, 34 | "external_product_id": 15 35 | }, 36 | { 37 | "message": [ 38 | "ASUS Vivobook X712DA-202.MV \n", 39 | "Home and Business Laptop \n", 40 | "(AMD Ryzen 7 3700U 4-Core, 12GB RAM, 512GB SSD, 17.3 \n", 41 | "*Precio $719.97*" 42 | ], 43 | "image": [ 44 | "laptop-2.jpg" 45 | ], 46 | "opt": 2, 47 | "external_category_id": 10, 48 | "external_product_id": 16 49 | } 50 | ] 51 | }, 52 | "pc": { 53 | "label": " *pc* \n", 54 | "main_images": [ 55 | { 56 | "message": [ 57 | "*(1)* iBUYPOWER Slate 4 163A " 58 | ], 59 | "image": "pc-1.jpg" 60 | }, 61 | { 62 | "message": [ 63 | "*(2)* ASUS ROG Strix G15CK " 64 | ], 65 | "image": "pc-2.jpg" 66 | } 67 | ], 68 | "main_message": [ 69 | "Marca el número para obtener más infocamación del producto" 70 | ], 71 | "list": [ 72 | { 73 | "message": [ 74 | "iBUYPOWER Slate 4 163A - AMD Ryzen 5 3600 - Radeon RX 580 \n", 75 | "16 GB DDR4 - 1 TB HDD - 240 GB SATA SSD - Windows 10 \n", 76 | "Home - Gaming Desktop \n", 77 | "*Precio $1,099.99*" 78 | ], 79 | "image": [ 80 | "pc-1.jpg" 81 | ], 82 | "opt": 1 83 | }, 84 | { 85 | "message": [ 86 | "ASUS ROG Strix G15CK Gaming Desktop PC, Intel Core i5-10400F \n", 87 | "GeForce GTX 1660 SUPER, 8 GB DDR4 RAM, 512 GB SSD \n", 88 | "Wi-Fi 6, Windows 10 Home, Star Black, G15CK-BS562 \n", 89 | "*Precio $999.00*" 90 | ], 91 | "image": [ 92 | "pc-2.jpg" 93 | ], 94 | "opt": 2 95 | } 96 | ] 97 | } 98 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ⚡⚡⚡ DECLARAMOS LAS LIBRERIAS y CONSTANTES A USAR! ⚡⚡⚡ 3 | */ 4 | require('dotenv').config() 5 | const fs = require('fs'); 6 | const express = require('express'); 7 | const moment = require('moment'); 8 | const ExcelJS = require('exceljs'); 9 | const qrcode = require('qrcode-terminal'); 10 | const qr = require('qr-image'); 11 | const { Client, MessageMedia } = require('whatsapp-web.js'); 12 | const mail = require('./mail') 13 | const api = require('./api') 14 | const flow = require('../flow/steps.json') 15 | const messages = require('../flow/messages.json') 16 | const vendors = require('../flow/vendor.json') 17 | const products = require('../flow/products.json') 18 | const app = express(); 19 | app.use(express.urlencoded({ extended: true })) 20 | const SESSION_FILE_PATH = `${process.cwd()}/session.json`; 21 | let client; 22 | let sessionData; 23 | 24 | /** 25 | * Enviamos archivos multimedia a nuestro cliente 26 | * @param {*} number 27 | * @param {*} fileName 28 | */ 29 | const sendMedia = (number, fileName, text = null) => new Promise((resolve, reject) => { 30 | number = number.replace('@c.us', ''); 31 | number = `${number}@c.us` 32 | const media = MessageMedia.fromFilePath(`./mediaSend/${fileName}`); 33 | const msg = client.sendMessage(number, media, { caption: text || null }); 34 | resolve(msg) 35 | }) 36 | 37 | /** 38 | * Enviamos un mensaje simple (texto) a nuestro cliente 39 | * @param {*} number 40 | */ 41 | const sendMessage = (number = null, text = null) => new Promise((resolve, reject) => { 42 | number = number.replace('@c.us', ''); 43 | number = `${number}@c.us` 44 | const message = text; 45 | const msg = client.sendMessage(number, message); 46 | console.log(`⚡⚡⚡ Enviando mensajes....`); 47 | resolve(msg) 48 | }) 49 | 50 | /** 51 | * Clear number 52 | */ 53 | 54 | const clearNumber = (number) => { 55 | number = number.replace('@c.us', ''); 56 | number = `${number}` 57 | return number; 58 | } 59 | 60 | /** 61 | * Revisamos si tenemos credenciales guardadas para inciar sessio 62 | * este paso evita volver a escanear el QRCODE 63 | */ 64 | const withSession = () => { 65 | console.log(`Validando session con Whatsapp...`); 66 | sessionData = require(SESSION_FILE_PATH); 67 | client = new Client({ 68 | session: sessionData, 69 | puppeteer: { 70 | args: [ 71 | '--no-sandbox' 72 | ], 73 | } 74 | }); 75 | 76 | client.on('ready', () => { 77 | console.log('Client is ready!'); 78 | connectionReady(); 79 | 80 | }); 81 | 82 | 83 | 84 | client.on('auth_failure', () => { 85 | console.log('** Error de autentificacion vuelve a generar el QRCODE (Debes Borrar el archivo session.json) **'); 86 | }) 87 | 88 | 89 | client.initialize(); 90 | } 91 | 92 | /** 93 | * Generamos un QRCODE para iniciar sesion 94 | */ 95 | const withOutSession = () => { 96 | 97 | console.log(`🔴🔴 No tenemos session guardada, espera que se generar el QR CODE 🔴🔴`); 98 | 99 | client = new Client({ 100 | puppeteer: { 101 | args: [ 102 | '--no-sandbox' 103 | ], 104 | } 105 | }); 106 | client.on('qr', qr => { 107 | qrcode.generate(qr, { small: true }); 108 | generateImage(qr) 109 | }); 110 | 111 | client.on('ready', () => { 112 | console.log('Client is ready!'); 113 | connectionReady(); 114 | }); 115 | 116 | client.on('auth_failure', () => { 117 | console.log('** Error de autentificacion vuelve a generar el QRCODE **'); 118 | }) 119 | 120 | 121 | client.on('authenticated', (session) => { 122 | // Guardamos credenciales de de session para usar luego 123 | sessionData = session; 124 | fs.writeFile(SESSION_FILE_PATH, JSON.stringify(session), function(err) { 125 | if (err) { 126 | console.log(err); 127 | } 128 | }); 129 | }); 130 | 131 | client.initialize(); 132 | } 133 | 134 | const connectionReady = () => { 135 | 136 | /** Aqui escuchamos todos los mensajes que entran */ 137 | client.on('message', async msg => { 138 | let { body } = msg 139 | const { from, to } = msg; 140 | // handleExcel(from) 141 | let step = await readChat(from, body) 142 | body = body.toLowerCase(); 143 | /***************************** Preguntas ******************************** */ 144 | 145 | if (flow.STEP_1.includes(body)) { 146 | 147 | /** 148 | * Aqui damos la bienvenida 149 | */ 150 | 151 | console.log('STEP1', body); 152 | 153 | sendMessage(from, messages.STEP_1.join('')) 154 | return 155 | } 156 | 157 | if (flow.STEP_2.includes(body)) { 158 | 159 | /** 160 | * Aqui respondemos los prodcutos 161 | */ 162 | const step2 = messages.STEP_2.join('') 163 | 164 | const parseLabel = Object.keys(products).map(o => { 165 | return products[o]['label']; 166 | }).join('') 167 | 168 | sendMessage(from, step2) 169 | sendMessage(from, parseLabel) 170 | await readChat(from, body, 'STEP_2_1') 171 | return 172 | } 173 | 174 | if (flow.STEP_3.includes(body)) { 175 | /** 176 | * Aqui respondemos los asesores 177 | */ 178 | const step3 = messages.STEP_3.join('') 179 | console.log(step3) 180 | sendMessage(from, step3) 181 | await readChat(from, body, 'STEP_3_1') 182 | return 183 | } 184 | 185 | if (flow.STEP_4.includes(body)) { 186 | /** 187 | * Aqui respondemos gracias! 188 | */ 189 | const step4 = messages.STEP_4.join('') 190 | console.log(step4) 191 | sendMessage(from, step4) 192 | await readChat(from, body) 193 | return 194 | } 195 | 196 | if (flow.STEP_5.includes(body)) { 197 | /** 198 | * Aqui comenzamos a pedir datos al usuario 199 | */ 200 | const step5 = messages.STEP_5.join('') 201 | console.log(step5) 202 | sendMessage(from, step5) 203 | await readChat(from, body, 'STEP_5_1') 204 | return 205 | } 206 | 207 | 208 | /***************************** FLOW ******************************** */ 209 | 210 | /* Seguimos el flujo de los productos */ 211 | if (step && step.includes('STEP_2_1')) { 212 | 213 | /** 214 | * Buscar prodcuto en json 215 | */ 216 | const insideText = body.toLowerCase(); 217 | const productFind = products[insideText] || null; 218 | 219 | if (productFind) { 220 | 221 | const getAllitems = productFind.main_images; 222 | 223 | const listQueue = getAllitems.map(itemSend => { 224 | return sendMedia( 225 | from, 226 | itemSend.image, 227 | itemSend.message.join('') 228 | ) 229 | }) 230 | 231 | Promise.all(listQueue).then(() => { 232 | sendMessage(from, productFind.main_message.join('')) 233 | }) 234 | 235 | const stepProduct = `STEP_2_ITEM_${insideText}`.toUpperCase(); 236 | await readChat(from, body, stepProduct) 237 | 238 | } else { 239 | sendMessage(from, messages.STEP_2_1.join('')) 240 | await readChat(from, body, 'STEP_2_1') 241 | } 242 | return 243 | } 244 | 245 | /** Seguimos mostrandole mas imagenes del producto */ 246 | 247 | if (step && step.includes('STEP_2_ITEM_')) { 248 | 249 | /** 250 | * Buscar prodcuto en json pasado en Numero de opción 251 | */ 252 | 253 | let getItem = step.split('STEP_2_ITEM_') 254 | getItem = getItem.reverse()[0] || null 255 | 256 | const nameItem = getItem.toLowerCase(); 257 | const productFind = products[nameItem] || null; 258 | 259 | if (isNaN(parseInt(body))) { 260 | sendMessage(from, messages.STEP_2_1.join('')) 261 | await readChat(from, body) 262 | return 263 | } 264 | 265 | const findChild = productFind.list.find(a => parseInt(body) === a.opt) 266 | 267 | /** 268 | * Revisamos si estamos usando API externa o No 269 | */ 270 | if (api.checkApi()) { 271 | sendMessage(from, messages.STEP_2_4.join('')) 272 | } 273 | 274 | const dataExternal = await api.parseData(findChild); 275 | 276 | /** 277 | * Si no existe API continuamos con el flujo normal del JSON 278 | */ 279 | 280 | if (findChild) { 281 | 282 | const textProducto = findChild.message.concat(dataExternal) 283 | 284 | const lastImage = findChild.image.pop(); 285 | 286 | if (findChild.image.length) { 287 | 288 | findChild.image.forEach((child) => { 289 | sendMedia( 290 | from, 291 | child 292 | ) 293 | }) 294 | 295 | } 296 | 297 | await sendMedia( 298 | from, 299 | lastImage, 300 | textProducto.join('') 301 | ) 302 | 303 | 304 | sendMessage(from, messages.STEP_2_3.join('')) 305 | 306 | await readChat(from, body) 307 | } else { 308 | sendMessage(from, messages.STEP_2_1.join('')) 309 | await readChat(from, body) 310 | } 311 | 312 | 313 | return 314 | } 315 | 316 | /* Seguimos el flujo de los asesores */ 317 | if (step && step.includes('STEP_3_1')) { 318 | 319 | /** 320 | * Buscar asesor en json 321 | */ 322 | const insideText = body.toLowerCase(); 323 | const vendorFind = vendors[insideText] || null; 324 | 325 | if (vendorFind) { 326 | sendMessage(from, vendorFind.join('')) 327 | await readChat(from, body, 'STEP_4') 328 | } else { 329 | sendMessage(from, messages.STEP_3_1.join('')) 330 | await readChat(from, body) 331 | } 332 | return 333 | } 334 | 335 | /** Seguimos flujo de pedir datos */ 336 | if (step && step.includes('STEP_5_1')) { 337 | 338 | const step5_1 = messages.STEP_5_1.join('') 339 | console.log(step5_1) 340 | sendMessage(from, step5_1) 341 | await readChat(from, body, 'STEP_5_2') 342 | return 343 | } 344 | 345 | /** Seguimos flujo de pedir datos el municipio */ 346 | if (step && step.includes('STEP_5_2')) { 347 | 348 | const step5_2 = messages.STEP_5_2.join('') 349 | console.log(step5_2) 350 | sendMessage(from, step5_2) 351 | await readChat(from, body, 'STEP_5_3') 352 | return 353 | } 354 | 355 | /** Seguimos flujo de pedir asesor el municipio */ 356 | if (step && step.includes('STEP_5_3')) { 357 | 358 | const step5_3 = messages.STEP_5_3.join('') 359 | console.log(step5_3) 360 | sendMessage(from, step5_3) 361 | await readChat(from, body, 'STEP_5_4') 362 | return 363 | } 364 | 365 | /* Seguimos el flujo de los asesores */ 366 | if (step && step.includes('STEP_5_4')) { 367 | 368 | const step5_4 = messages.STEP_5_4.join('') 369 | const step5_5 = messages.STEP_5_5.join('') 370 | let messageStep5_4 = step5_4; 371 | const userName = await handleExcel(from, 'STEP_5_2'); 372 | const userProduct = await handleExcel(from, 'STEP_5_3'); 373 | const userMethodPay = await handleExcel(from, 'STEP_5_4'); 374 | 375 | messageStep5_4 = messageStep5_4.replace('%NAME%', userName.value || '') 376 | messageStep5_4 = messageStep5_4.replace('%LOCATION%', body || '') 377 | messageStep5_4 = messageStep5_4.replace('%PRODUCT%', userProduct.value || '') 378 | messageStep5_4 = messageStep5_4.replace('%METHOD%', userMethodPay.value || '') 379 | 380 | sendMessage(from, messageStep5_4) 381 | sendMessage(from, step5_5) 382 | await readChat(from, body, 'STEP_5_5') 383 | return 384 | } 385 | 386 | if (step && step.includes('STEP_5_5')) { 387 | if (flow.STEP_5_5.includes(body)) { 388 | const step5_6 = messages.STEP_5_6.join('') 389 | sendMessage(from, step5_6) 390 | 391 | const step5_7 = messages.STEP_5_7.join('') 392 | let messageStep5_7 = step5_7; 393 | const userName = await handleExcel(from, 'STEP_5_2'); 394 | const userLocation = await handleExcel(from, 'STEP_5_5'); 395 | const userProduct = await handleExcel(from, 'STEP_5_3'); 396 | const userMethodPay = await handleExcel(from, 'STEP_5_4'); 397 | 398 | messageStep5_7 = messageStep5_7.replace('%NAME%', userName.value) 399 | messageStep5_7 = messageStep5_7.replace('%LOCATION%', userLocation.value) 400 | messageStep5_7 = messageStep5_7.replace('%PRODUCT%', userProduct.value) 401 | messageStep5_7 = messageStep5_7.replace('%METHOD%', userMethodPay.value) 402 | messageStep5_7 = messageStep5_7.replace('%USERPHONE%', clearNumber(from)) 403 | 404 | mail.sendMail(messageStep5_7) 405 | await readChat(from, body) 406 | } else { 407 | sendMessage(from, messages.ERROR.join('')) 408 | await readChat(from, body) 409 | } 410 | return 411 | } 412 | 413 | /********************************** DEFAULT************************* */ 414 | sendMessage(from, messages.ERROR.join('')) 415 | return 416 | 417 | }); 418 | 419 | } 420 | 421 | /** 422 | * Guardar historial de conversacion 423 | * @param {*} number 424 | * @param {*} message 425 | */ 426 | const readChat = (number, message, step = null) => new Promise((resolve, reject) => { 427 | 428 | setTimeout(() => { 429 | number = number.replace('@c.us', ''); 430 | number = `${number}@c.us` 431 | const pathExcel = `./chats/${number}.xlsx`; 432 | const workbook = new ExcelJS.Workbook(); 433 | const today = moment().format('DD-MM-YYYY hh:mm') 434 | 435 | if (fs.existsSync(pathExcel)) { 436 | /** 437 | * Si existe el archivo de conversacion lo actualizamos 438 | */ 439 | const workbook = new ExcelJS.Workbook(); 440 | workbook.xlsx.readFile(pathExcel) 441 | .then(() => { 442 | const worksheet = workbook.getWorksheet(1); 443 | const lastRow = worksheet.lastRow; 444 | let getRowInsert = worksheet.getRow(++(lastRow.number)); 445 | getRowInsert.getCell('A').value = today; 446 | getRowInsert.getCell('B').value = message; 447 | 448 | if (step) { 449 | getRowInsert.getCell('C').value = step; 450 | } 451 | 452 | getRowInsert.commit(); 453 | workbook.xlsx.writeFile(pathExcel) 454 | .then(() => { 455 | const getRowPrevStep = worksheet.getRow(lastRow.number); 456 | const lastStep = getRowPrevStep.getCell('C').value 457 | resolve(lastStep) 458 | }) 459 | .catch((err) => { 460 | console.log('ERR', err); 461 | reject('error') 462 | }) 463 | 464 | 465 | }) 466 | .catch((err) => { 467 | console.log('ERR', err); 468 | reject('error') 469 | }) 470 | 471 | } else { 472 | /** 473 | * NO existe el archivo de conversacion lo creamos 474 | */ 475 | const worksheet = workbook.addWorksheet('Chats'); 476 | worksheet.columns = [ 477 | { header: 'Fecha', key: 'number_customer' }, 478 | { header: 'Mensajes', key: 'message' }, 479 | { header: 'Paso', key: 'step' }, 480 | ]; 481 | 482 | step = step || '' 483 | 484 | worksheet.addRow([today, message, step]); 485 | workbook.xlsx.writeFile(pathExcel) 486 | .then(() => { 487 | resolve('STEP_1') 488 | }) 489 | .catch((err) => { 490 | console.log('Error', err); 491 | reject('error') 492 | }); 493 | 494 | } 495 | }, 150) 496 | 497 | }); 498 | 499 | const generateImage = (base64) => { 500 | let qr_svg = qr.image(base64, { type: 'svg', margin: 4 }); 501 | qr_svg.pipe(require('fs').createWriteStream('qr-code.svg')); 502 | console.log(`⚡ Recuerda que el QR se actualiza cada minuto ⚡'`); 503 | console.log(`⚡ Actualiza F5 el navegador para mantener el mejor QR⚡`); 504 | console.log('http://localhost:9000/qr'); 505 | } 506 | 507 | const handleExcel = (number, step = null) => new Promise((resolve, reject) => { 508 | 509 | const proccessChild = (row) => new Promise((resolve) => { 510 | const stepFind = row.values[3] || null; 511 | resolve({ 512 | value: row.values[2] || null, 513 | step: stepFind 514 | }) 515 | }) 516 | 517 | let rowsList = []; 518 | setTimeout(() => { 519 | number = number.replace('@c.us', ''); 520 | number = `${number}@c.us` 521 | const pathExcel = `./chats/${number}.xlsx`; 522 | const workbook = new ExcelJS.Workbook(); 523 | if (fs.existsSync(pathExcel)) { 524 | /** 525 | * Si existe el archivo de conversacion lo actualizamos 526 | */ 527 | 528 | workbook.xlsx.readFile(pathExcel) 529 | .then(() => { 530 | const worksheet = workbook.getWorksheet(1); 531 | worksheet.eachRow((row) => rowsList.push(proccessChild(row))); 532 | Promise.all(rowsList).then((listPromise) => { 533 | const listRev = listPromise.reverse(); 534 | if (step) { 535 | const findStep = listRev.find((o) => o.step === step); 536 | resolve(findStep); 537 | } else { 538 | reject('error') 539 | } 540 | 541 | }) 542 | resolve; 543 | 544 | }) 545 | .catch((err) => { 546 | console.log('ERR', err); 547 | reject('error') 548 | }) 549 | } 550 | 551 | }, 150) 552 | }); 553 | 554 | 555 | /** 556 | * Revisamos si existe archivo con credenciales! 557 | */ 558 | (fs.existsSync(SESSION_FILE_PATH)) ? withSession(): withOutSession(); 559 | 560 | /** QR Link */ 561 | 562 | app.get('/qr', (req, res) => { 563 | res.writeHead(200, { 'content-type': 'image/svg+xml' }); 564 | fs.createReadStream(`./qr-code.svg`).pipe(res); 565 | }) 566 | 567 | app.listen(9000, () => { 568 | console.log('Server ready!'); 569 | }) --------------------------------------------------------------------------------