With this node.js micro framework using Venom Bot under the hood, you can easily create a WhatsApp Chatbot 🤖 . You will only need to edit your conversation flow in a single file.
Getting Started
- Create a new repository from this template
- Install in your development environment
- Configure port(s), credentials, etc
- Write your conversation flow
- Start
Install
Docker
Requirements: docker
Build and Run with Dockerfile
$ docker build -t wchatbot .
$ docker run --name wchatbot -p 3000:3000 -v /your_project_absolute_path/src:/wchatbot/src wchatbot
or Build and Run with Docker Compose
$ docker-compose build
$ docker-compose up
Visit http://localhost:3000 and play with your chatbot!
Virtual Machine
Requirements: nodejs (Latest maintenance LTS version), yarn (or npm), pm2, chrome/chromium
Use an nginx reverse proxy to publicly expose the http control panel (configuration example).
$ yarn install
Launch the chatbot and the http control panel
$ yarn start
$ yarn http-ctrl:start
Visit http://localhost:3000 and play with your chatbot!
Local Machine
Requirements: nodejs (Latest maintenance LTS version), yarn (or npm), chrome/chromium
$ yarn install
Launch the chatbot and the http control panel
$ yarn http-ctrl:dev:detach
$ yarn dev
Visit http://localhost:3000 and play with your chatbot!
Configuration
Edit ./src/config.js
file
Basic
export const chatbotOptions = {
httpCtrl: {
port: 3000, // httpCtrl port (http://localhost:3000/)
username: "admin", // httpCtrl auth login
password: "chatbot",
},
};
Advanced
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
Chatbot Controls
$ docker exec wchatbot yarn start
$ docker exec wchatbot yarn stop
$ docker exec wchatbot yarn restart
$ docker exec wchatbot yarn reload
HTTP Control Panel Controls
$ 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
Virtual Machine
Chatbot Controls
$ yarn start
$ yarn stop
$ yarn restart
$ yarn reload
HTTP Control Panel Controls
$ yarn http-ctrl:start
$ yarn http-ctrl:stop
$ yarn http-ctrl:restart
$ yarn http-ctrl:reload
Local Machine
Direct in your OS without Docker
Chatbot
$ yarn dev
$ yarn dev:detach
Launch HTTP Control Panel
$ yarn http-ctrl:dev
$ yarn http-ctrl:dev:detach
Sessions
Sessions and auth tokens are write in ./tokens
folder.
Logs
Logs are write in ./logs
folder.
Attention: console.log
and http-ctrl-console.log
only write in ./logs
folder with yarn dev:detach
and yarn http-ctrl:dev:detach
otherwise managed by pm2
.
Docker
Chatbot
$ docker exec wchatbot yarn log
HTTP Control Panel
$ docker exec wchatbot yarn http-ctrl:log
Conversations
$ docker exec wchatbot yarn conversations
Virtual Machine
Chatbot
$ yarn log
HTTP Control Panel
$ yarn http-ctrl:log
Conversations
$ yarn conversations
Local Machine
Chatbot
$ yarn log:dev
HTTP Control Panel
$ yarn log:http-ctrl:dev
Conversations
$ yarn conversations
Conversation Flow
Edit ./src/conversations/conversation.js
file.
The conversation flow is an array of ordered reply objects.
A reply is only triggered if its parent
(can be an integer or an array)
is equal to the id
of the previous reply.
To indicate that a reply is the end of the conversation add the following property:
Property | Type | Description |
---|---|---|
end |
Boolean | The end of the conversation |
You can protect so that only one number or a list of numbers is answered with:
Property | Type | Description |
---|---|---|
from |
String / Array | Only answer this or these numbers |
A reply necessarily needs the following properties:
Replies Types
Send Text
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
message |
String | Reply text message |
Example
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match with all text
message: "Hi I am a Chatbot!",
}
]
Send Buttons
Attention: It is currently not working!.
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
message |
String | Reply text message |
description |
String | Reply text subtitle |
buttons |
Array | Button object, look at the example |
Example
[
{
id: 1,
parent: 0,
pattern: /.*/,
message: "Hello!",
description: "Can I help with something?",
buttons: buttons([
"Website",
"LinkedIn",
"Github",
]),
}
]
Send List
Attention: It is currently not working!.
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
message |
String | Reply text message |
description |
String | Reply text subtitle |
button |
String | List button text |
list |
Array | List object, look at the example |
Example
[
{
id: 1,
parent: 0,
pattern: /other country/,
message: "Choice one country",
description: "Choice one option!",
button: "Countries list",
list: list([
"Argentina",
"Belize",
"Bolivia",
]),
},
]
Send Link
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
message |
String | Reply text message |
link |
String | URL of generated link preview |
Example
[
{
id: 2,
parent: 1, // Relation with id: 1
pattern: /github/,
message: "Check my Github repositories!",
link: "https://github.com/jfadev",
}
]
Send Image
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
image |
Path / Object | Path or Object returned by remoteImg() funtion |
Example
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
image: remoteImg("https://remote-server.com/menu.jpg"),
// image: "./images/menu.jpg",
}
]
Send Audio
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
audio |
Path / Object | Path or Object returned by remoteAudio() funtion. |
Example
[
{
id: 1,
parent: 0,
pattern: /.*/, // Match all
audio: remoteAudio("https://remote-server.com/audio.mp3"),
// audio: "./audios/audio.mp3",
}
]
Forward Message
Property | Type | Description |
---|---|---|
id |
Integer | Reply id is used to link with parent |
parent |
Integer | Id of the reply parent or ids array [2, 3] . If it has no parent it is 0 by default |
pattern |
RegExp | Regular expression to match in lower case |
message |
String | Reply text message |
forward |
String | Number where the message is forwarded |
Example
[
{
id: 1,
parent: 0,
pattern: /forward/,
message: "Text to forward",
forward: "[email protected]", // forward this message to this number
}
]
Helpers
Helper | Return | Description |
---|---|---|
buttons(buttonTexts) |
Array | Generate buttons |
remoteTxt(url) |
String | Return a remote TXT file |
remoteJson(url) |
JSON | Return a remote JSON file |
remoteImg(url) |
Object | Return a remote Image file |
remoteAudio(url) |
Object | Return a remote Audio file |
list(listRows) |
Array | Generate list |
inp(id, parents) |
String | Return input string by reply id. Use in beforeReply, afterReply and beforeForward |
med(id, parents) |
Media / null | Return Media ({buffer, extension}) by reply id. Use in beforeReply, afterReply and beforeForward |
Hooks
Property | Type | Description |
---|---|---|
beforeReply(from, input, output, parents, media) |
Function | Inject custom code before a reply |
afterReply(from, input, parents, media) |
Function | Inject custom code after a reply |
beforeForward(from, forward, input, parents, media) |
Function | Inject custom code before a forward |
Loops
Property | Type | Description |
---|---|---|
goTo(from, input, output, parents, media) |
Function | Should return the reply id where to jump |
clearParents |
Boolean | Clear parents data, use with goTo() |
Http Control Panel
With the control panel you can log in, start, stop or restart the bot and monitor the logs.
Set your username
and password
to access your control panel in file ./src/config.js
export const chatbotOptions = {
httpCtrl: {
port: 3000, // httpCtrl port (http://localhost:3000/)
username: "admin",
password: "chatbot"
}
};
Use an nginx reverse proxy to publicly expose the http control panel (configuration example).
Examples
Edit your file ./src/conversations/conversation.js
and create your custom conversation workflow.
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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,
},
];
Example 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
},
];
Advanced
Multiple Conversation Flows
Edit ./src/main.js
file.
import { session } from "./core";
import info from "./conversations/info";
import delivery from "./conversations/delivery";
session("chatbotSession", info);
session("chatbotSession", delivery);
Multiple Accounts
Edit ./src/main.js
file.
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);
Edit ./src/httpCtrl.js
file.
import { httpCtrl } from "./core";
httpCtrl("commercial_1", 3000);
httpCtrl("commercial_2", 3001);
httpCtrl("delivery", 3002);
Access to Venom client
Edit ./src/main.js
file.
import { session } from "./core";
import conversation from "./conversations/conversation";
// Run conversation flow and return a Venom client
const chatbot = await session("chatbotSession", conversation);
Schedule Jobs
Edit ./src/main.js
file.
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");
}
);
Testing
Unit tests writes with jest
$ yarn test
Test you conversation flow array structure with conversation.test.js file as example.
$ yarn test src/conversations/conversation
Troubleshooting
Attention: Do not log in to whatsapp web with the same account that the chatbot uses. This will make the chatbot unable to hear the messages.
Attention: You need a whatsapp account for the chatbot and a different account to be able to talk to it.