Con este nodo.js micro marco usando Robot Venenoso bajo el capó, puedes crear fácilmente un Chatbot de WhatsApp 🤖 . Solo necesitarás editar tu flujo de conversación en un solo archivo.
Empezando
- Crear un nuevo repositorio a partir de esta plantilla
- Instalación en su entorno de desarrollo
- Configurar puerto(s), cartas credenciales, etc
- Escribe tu flujo de conversación
- Comenzar
Instalación
Docker
Requisitos: estibador
Cree y ejecute con Dockerfile
$ docker build -t wchatbot .
$ docker run --name wchatbot -p 3000:3000 -v /your_project_absolute_path/src:/wchatbot/src wchatbot
o Compile y ejecute con Docker Compose
$ docker-compose build
$ docker-compose up
Visita http://servidor local:3000 y juega con tu chatbot!
Máquina virtual
Requisitos: nodejs (El último maintenance LTS versión), yarn (o npm), pm2, chrome/chromium
Usa un nginx proxy inverso para exponer públicamente el panel de control http (ejemplo de configuración).
$ yarn install
Inicie el chatbot y el panel de control http
$ yarn start
$ yarn http-ctrl:start
Visita http://servidor local:3000 y juega con tu chatbot!
Máquina local
Requisitos: nodejs (El último maintenance LTS versión), yarn (o npm), chrome/chromium
$ yarn install
Inicie el chatbot y el panel de control http
$ yarn http-ctrl:dev:detach
$ yarn dev
Visita http://servidor local:3000 y juega con tu chatbot!
Configuración
Editar ./src/config.js
archivo
Basic
export const chatbotOptions = {
httpCtrl: {
port: 3000, // httpCtrl port (http://localhost:3000/)
username: "admin", // httpCtrl auth login
password: "chatbot",
},
};
Avanzado
export const venomOptions = {
...
browserArgs: [
"--no-sandbox", // Will be passed to browser. Use --no-sandbox with Docker
],
puppeteerOptions: { // Will be passed to puppeteer.launch.
args: ["--no-sandbox"] // Use --no-sandbox with Docker
},
...
};
Commands
Docker
Controles de chatbots
$ docker exec wchatbot yarn start
$ docker exec wchatbot yarn stop
$ docker exec wchatbot yarn restart
$ docker exec wchatbot yarn reload
Controles del panel de control HTTP
$ docker exec wchatbot yarn http-ctrl:start
$ docker exec wchatbot yarn http-ctrl:stop
$ docker exec wchatbot yarn http-ctrl:restart
$ docker exec wchatbot yarn http-ctrl:reload
Máquina virtual
Controles de chatbots
$ yarn start
$ yarn stop
$ yarn restart
$ yarn reload
Controles del panel de control HTTP
$ yarn http-ctrl:start
$ yarn http-ctrl:stop
$ yarn http-ctrl:restart
$ yarn http-ctrl:reload
Máquina local
Directo en su sistema operativo sin Docker
bot conversacional
$ yarn dev
$ yarn dev:detach
Inicie el panel de control HTTP
$ yarn http-ctrl:dev
$ yarn http-ctrl:dev:detach
Sesiones
Las sesiones y los tokens de autenticación se escriben ./tokens
carpeta.
Registros
Los registros se escriben ./logs
carpeta.
Atención: console.log
y http-ctrl-console.log
solo escribe ./logs
carpeta con yarn dev:detach
y yarn http-ctrl:dev:detach
administrado de otra manera por pm2
.
Docker
bot conversacional
$ docker exec wchatbot yarn log
Panel de control HTTP
$ docker exec wchatbot yarn http-ctrl:log
Conversaciones
$ docker exec wchatbot yarn conversations
Máquina virtual
bot conversacional
$ yarn log
Panel de control HTTP
$ yarn http-ctrl:log
Conversaciones
$ yarn conversations
Máquina local
bot conversacional
$ yarn log:dev
Panel de control HTTP
$ yarn log:http-ctrl:dev
Conversaciones
$ yarn conversations
Flujo de conversación
Editar ./src/conversations/conversation.js
archivo.
El flujo de conversación es una matriz de objetos de respuesta ordenados..
Una respuesta solo se activa si su parent
(puede ser un entero o una matriz)
es igual a la id
de la respuesta anterior.
Para indicar que una respuesta es el final de la conversación, agregue la siguiente propiedad:
Propiedad | Tipo | Descripción |
---|---|---|
end |
booleano | El final de la conversación |
Puedes proteger para que solo se responda un número o una lista de números con:
Propiedad | Tipo | Descripción |
---|---|---|
from |
Cadena / Array | Solo responde este o estos números |
Una respuesta necesita necesariamente las siguientes propiedades:
Tipos de respuestas
enviar texto
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
message |
Cadena | Responder mensaje de texto |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match with all text
message: "Hi I am a Chatbot!",
}
]
Enviar Botones
Atención: Actualmente no está funcionando.!.
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
message |
Cadena | Responder mensaje de texto |
description |
Cadena | Subtítulo del texto de respuesta |
buttons |
Array | Objeto de botón, mira el ejemplo |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /.*/,
message: "Hello!",
description: "Can I help with something?",
buttons: buttons([
"Website",
"LinkedIn",
"Github",
]),
}
]
Enviar lista
Atención: Actualmente no está funcionando.!.
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
message |
Cadena | Responder mensaje de texto |
description |
Cadena | Subtítulo del texto de respuesta |
button |
Cadena | Texto del botón de lista |
list |
Array | Objeto de lista, mira el ejemplo |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /other country/,
message: "Choice one country",
description: "Choice one option!",
button: "Countries list",
list: list([
"Argentina",
"Belize",
"Bolivia",
]),
},
]
Enviar enlace
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
message |
Cadena | Responder mensaje de texto |
link |
Cadena | URL de la vista previa del enlace generado |
Ejemplo
[
{
id: 2,
parent: 1, // Relation with id: 1
pattern: /github/,
message: "Check my Github repositories!",
link: "https://github.com/jfadev",
}
]
Enviar imagen
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
image |
Camino / Objeto | Ruta u objeto devuelto por remoteImg() funcion |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
image: remoteImg("https://remote-server.com/menu.jpg"),
// image: "./images/menu.jpg",
}
]
Enviar sonido
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
audio |
Camino / Objeto | Ruta u objeto devuelto por remoteAudio() funcion. |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
audio: remoteAudio("https://remote-server.com/audio.mp3"),
// audio: "./audios/audio.mp3",
}
]
Reenviar mensaje
Propiedad | Tipo | Descripción |
---|---|---|
id |
Entero | Responder id se utiliza para enlazar con parent |
parent |
Entero | Id del padre de respuesta o matriz de ids [2, 3] . Si no tiene padre es 0 por defecto |
pattern |
RegExp | Expresión regular para hacer coincidir en minúsculas |
message |
Cadena | Responder mensaje de texto |
forward |
Cadena | Número donde se reenvía el mensaje |
Ejemplo
[
{
id: 1,
parent: 0,
pattern: /forward/,
message: "Text to forward",
forward: "[email protected]", // forward this message to this number
}
]
ayudantes
Ayudante | Devolver | Descripción |
---|---|---|
buttons(buttonTexts) |
Array | Generar botones |
remoteTxt(url) |
Cadena | Devolver un archivo TXT remoto |
remoteJson(url) |
JSON | Devolver un archivo JSON remoto |
remoteImg(url) |
Objeto | Devolver un archivo de imagen remoto |
remoteAudio(url) |
Objeto | Devolver un archivo de audio remoto |
list(listRows) |
Array | Generar lista |
inp(id, parents) |
Cadena | Devolver cadena de entrada por ID de respuesta. Usar antes de Responder, afterReply y beforeForward |
med(id, parents) |
Medios de comunicación / nulo | Medios de devolución ({buffer, extensión}) por ID de respuesta. Usar antes de Responder, afterReply y beforeForward |
Manos
Propiedad | Tipo | Descripción |
---|---|---|
beforeReply(from, input, output, parents, media) |
Función | Inyectar código personalizado antes de una respuesta |
afterReply(from, input, parents, media) |
Función | Inyectar código personalizado después de una respuesta |
beforeForward(from, forward, input, parents, media) |
Función | Inyectar código personalizado antes de un reenvío |
Bucles
Propiedad | Tipo | Descripción |
---|---|---|
goTo(from, input, output, parents, media) |
Función | Debería devolver la identificación de respuesta a dónde saltar |
clearParents |
booleano | Borrar datos de padres, usar con ir a() |
Panel de control HTTP
Con el panel de control puedes iniciar sesión, comenzar, detener o reiniciar el bot y monitorear los registros.
Establecer su username
y password
para acceder a su panel de control en archivo ./src/config.js
export const chatbotOptions = {
httpCtrl: {
port: 3000, // httpCtrl port (http://localhost:3000/)
username: "admin",
password: "chatbot"
}
};
Use un proxy inverso nginx para exponer públicamente el panel de control http (ejemplo de configuración).
Ejemplos
Edita tu archivo ./src/conversations/conversation.js
y crea tu flujo de trabajo de conversación personalizado.
Ejemplo 1
import { buttons } from "../helpers";
/**
* Chatbot conversation flow
* Example 1
*/
export default [
{
id: 1,
parent: 0,
pattern: /hello|hi|howdy|good day|good morning|hey|hi-ya|how are you|how goes it|howdy\-do/,
message: "Hello! Thank you for contacting me, I am a Chatbot 🤖 , we will gladly assist you.",
description: "Can I help with something?",
buttons: buttons([
"Website",
"Linkedin",
"Github",
"Donate",
"Leave a Message",
]),
},
{
id: 2,
parent: 1, // Relation with id: 1
pattern: /website/,
message: "Visit my website and learn more about me!",
link: "https://jordifernandes.com/",
end: true,
},
{
id: 3,
parent: 1, // Relation with id: 1
pattern: /linkedin/,
message: "Visit my LinkedIn profile!",
link: "https://www.linkedin.com/in/jfadev",
end: true,
},
{
id: 4,
parent: 1, // Relation with id: 1
pattern: /github/,
message: "Check my Github repositories!",
link: "https://github.com/jfadev",
end: true,
},
{
id: 5,
parent: 1, // Relation with id: 1
pattern: /donate/,
message: "A tip is always good!",
link: "https://jordifernandes.com/donate/",
end: true,
},
{
id: 6,
parent: 1, // Relation with id: 1
pattern: /leave a message/,
message: "Write your message, I will contact you as soon as possible!",
},
{
id: 7,
parent: 6, // Relation with id: 6
pattern: /.*/, // Match with all text
message: "Thank you very much, your message will be sent to Jordi! Sincerely the Chatbot 🤖 !",
end: true,
},
];
Ejemplo 2
import { buttons, remoteTxt, remoteJson } from "../helpers";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 2
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/,
message: "Hello! I am a Delivery Chatbot.",
description: "Choice one option!",
buttons: buttons([
"See today's menu?",
"Order directly!",
"Talk to a human!",
]),
},
{
id: 2,
parent: 1, // Relation with id: 1
pattern: /menu/,
message: remoteTxt(`${customEndpoint}/menu.txt`),
// message: remoteJson(`${customEndpoint}/menu.json`)[0].message,
end: true,
},
{
id: 3,
parent: 1, // Relation with id: 1
pattern: /order/,
message: "Make a order!",
link: `${customEndpoint}/delivery-order.php`,
end: true,
},
{
id: 4,
parent: 1, // Relation with id: 1
pattern: /human/,
message: "Please call the following WhatsApp number: +1 206 555 0100",
end: true,
},
];
Ejemplo 3
import fetch from "sync-fetch";
import { remoteImg } from "../helpers";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 3
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "Hello! I am a Delivery Chatbot. Send a menu item number!",
},
{
id: 2,
parent: 0, // Same parent (send reply id=1 and id=2)
pattern: /.*/, // Match all
image: remoteImg(`${customEndpoint}/menu.jpg`),
},
{
id: 3,
parent: 1, // Relation with id: 1
pattern: /\d+/, // Match any number
message: "You are choice item number $input. How many units do you want?", // Inject input value ($input) in message
},
{
id: 4,
parent: 2, // Relation with id: 2
pattern: /\d+/, // Match any number
message: "You are choice $input units. How many units do you want?",
// Inject custom code or overwrite output 'message' property before reply
beforeReply(from, input, output, parents) {
// Example check external api and overwrite output 'message'
const response = fetch(
`${customEndpoint}/delivery-check-stock.php/?item=${input}&qty=${parents.pop()}`
).json();
return response.stock === 0
? "Item number $input is not available in this moment!"
: output;
},
end: true,
},
];
Ejemplo 4
import { remoteImg } from "../helpers";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 4
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "Image local and remote! Send [local] or [remote]",
},
{
id: 2,
parent: 1,
pattern: /local/,
image: "./images/image1.jpg",
end: true,
},
{
id: 3,
parent: 1,
pattern: /remote/,
image: remoteImg(`${customEndpoint}/image1.jpg`),
end: true,
},
];
Ejemplo 5
import { remoteImg } from "../helpers";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 5
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "Audio local and remote! Send [local] or [remote]",
},
{
id: 2,
parent: 1,
pattern: /local/,
audio: "./audios/audio1.mp3",
end: true,
},
{
id: 3,
parent: 1,
pattern: /remote/,
audio: remoteAudio(`${customEndpoint}/audio1.mp3`),
end: true,
},
];
Ejemplo 6
import fetch from "sync-fetch";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 6
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "",
// Inject custom code or overwrite output 'message' property before reply
beforeReply(from, input, output, parents) {
// Get reply from external api and overwrite output 'message'
const response = fetch(`${customEndpoint}/ai-reply.php/?input=${input}`).json();
return response.message;
},
end: true,
},
];
Ejemplo 7
import fetch from "sync-fetch";
const customEndpoint = "https://jordifernandes.com/examples/chatbot";
/**
* Chatbot conversation flow
* Example 7
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "Hello!",
// Inject custom code after reply
afterReply(from, input, parents) {
// Send WhatApp number to external api
const response = fetch(`${customEndpoint}/number-lead.php/`, {
method: "POST",
body: JSON.stringify({ number: from }),
headers: { "Content-Type": "application/json" },
}).json();
console.log('response:', response);
},
end: true,
},
];
Ejemplo 8
import { buttons, inp } from "../helpers";
/**
* Chatbot conversation flow
* Example 8
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/,
message: "Choice one option",
description: "choice option:",
buttons: buttons(["Option 1", "Option 2"]),
},
{
id: 2,
parent: 1,
pattern: /.*/,
message: "We have received your request. Thanks.\n\n",
beforeReply(from, input, output, parents) {
output += `Your option: ${inp(2, parents)}`;
return output;
},
forward: "[email protected]", // default number or empty
beforeForward(from, forward, input, parents) { // Overwrite forward number
switch (inp(2, parents)) { // Access to replies inputs by id
case "option 1":
forward = "[email protected]";
break;
case "option 2":
forward = "[email protected]";
break;
default:
forward = "[email protected]";
break;
}
return forward;
},
end: true,
},
];
Ejemplo 9
/**
* Chatbot conversation flow
* Example 9
*/
export default [
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
message: "",
// Inject custom code or overwrite output 'message' property before reply
beforeReply(from, input, output, parents, media) {
if (media) {
console.log("media buffer", media.buffer);
return `You send file with .${media.extension} extension!`;
} else {
return "Send a picture please!";
}
},
end: true,
},
];
Ejemplo 10
doc/examples/conversation10.js
import { promises as fs } from "fs";
/**
* Chatbot conversation flow
* Example 10
*/
export default [
{
id: 1,
parent: 0,
pattern: /\b(?!photo\b)\w+/, // different to photo
message: `Write "photo" for starting.`,
},
{
id: 2,
parent: [0, 1],
pattern: /photo/,
message: `Hi I'm a Chatbot, send a photo(s)`,
},
{
id: 3,
parent: 2,
pattern: /\b(?!finalize\b)\w+/, // different to finalize
message: "",
async beforeReply(from, input, output, parents, media) {
const uniqId = (new Date()).getTime();
// Download media
if (media) {
const dirName = "./downloads";
const fileName = `${uniqId}.${media.extension}`;
const filePath = `${dirName}/${fileName}`;
await fs.mkdir(dirName, { recursive: true });
await fs.writeFile(filePath, await media.buffer);
return `Photo download successfully! Send another or write "finalize".`;
} else {
return `Try send again or write "finalize".`;
}
},
goTo(from, input, output, parents, media) {
return 3; // return to id = 3
},
},
{
id: 4,
parent: 2,
pattern: /finalize/,
message: "Thank's you!",
end: true,
},
];
Ejemplo 11
doc/examples/conversation11.js
import { inp, med } from "../helpers";
import { promises as fs } from "fs";
const menu = "Menu:\n\n" +
"1. Send Text\n" +
"2. Send Image\n";
/**
* Chatbot conversation flow
* Example 11
*/
export default [
{
id: 1,
parent: 0,
pattern: /\/admin/,
from: "[email protected]", // only respond to this number
message: menu
},
{
id: 2,
parent: [1, 5],
pattern: /.*/,
message: "",
async beforeReply(from, input, output, parents, media) {
switch (input) {
case "1":
return `Write your text:`;
case "2":
return `Send your image:`;
}
},
},
{
id: 3,
parent: 2,
pattern: /.*/,
message: `Write "/save" to save or cancel with "/cancel".`,
},
{
id: 4,
parent: 3,
pattern: /\/save/,
message: "",
async beforeReply(from, input, output, parents, media) {
let txt = "";
let img = null;
let filePath = null;
const type = inp(2, parents);
if (type === "1") {
txt = inp(3, parents);
} else if (type === "2") {
img = med(3, parents); // media from parent replies
}
if (img) {
const uniqId = new Date().getTime();
const dirName = ".";
const fileName = `${uniqId}.${img.extension}`;
filePath = `${dirName}/${fileName}`;
await fs.writeFile(filePath, await img.buffer);
} else {
const uniqId = new Date().getTime();
const dirName = ".";
const fileName = `${uniqId}.txt`;
await fs.writeFile(filePath, txt);
}
return `Ok, text or image saved. Thank you very much!`;
},
end: true,
},
{
id: 5,
parent: 3,
pattern: /\/cancel/,
message: menu,
goTo(from, input, output, parents, media) {
return 2;
},
clearParents: true, // reset parents
},
];
Avanzado
Múltiples flujos de conversación
Editar ./src/main.js
archivo.
import { session } from "./core";
import info from "./conversations/info";
import delivery from "./conversations/delivery";
session("chatbotSession", info);
session("chatbotSession", delivery);
Multiples cuentas
Editar ./src/main.js
archivo.
import { session } from "./core";
import commercial from "./conversations/commercial";
import delivery from "./conversations/delivery";
session("commercial_1", commercial);
session("commercial_2", commercial);
session("delivery", delivery);
Editar ./src/httpCtrl.js
archivo.
import { httpCtrl } from "./core";
httpCtrl("commercial_1", 3000);
httpCtrl("commercial_2", 3001);
httpCtrl("delivery", 3002);
Acceso al cliente de Venom
Editar ./src/main.js
archivo.
import { session } from "./core";
import conversation from "./conversations/conversation";
// Run conversation flow and return a Venom client
const chatbot = await session("chatbotSession", conversation);
Programar trabajos
Editar ./src/main.js
archivo.
import schedule from "node-schedule"; // Add node-schedule in your project
import { session, log } from "./core";
import { jobsOptions } from "./config";
import conversation from "./conversations/conversation";
// Run conversation flow and return a Venom client
const chatbot = await session("chatbotSession", conversation);
const job1 = schedule.scheduleJob(
jobsOptions.job1.rule, // "*/15 * * * *"
async () => {
// custom logic example
await chatbot.sendText("[email protected]", "test");
}
);
Pruebas
Pruebas unitarias escribe con Es
$ yarn test
Pruebe su estructura de matriz de flujo de conversación con conversación.test.js archivo como ejemplo.
$ yarn test src/conversations/conversation
Solución de problemas
Atención: No inicie sesión en whatsapp web con la misma cuenta que usa el chatbot. Esto hará que el chatbot no pueda escuchar los mensajes..
Atención: Necesitas una cuenta de whatsapp para el chatbot y otra cuenta para poder hablar con él.