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.
- Jfa WhatsApp Chatbot 💬
- Create a new repository from this template
- Install in your development environment
- Configure port(s), credentials, etc
- Write your conversation flow
- Start
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!
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!
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!
Edit ./src/config.js
file
export const chatbotOptions = {
httpCtrl: {
port: 3000, // httpCtrl port (http://localhost:3000/)
username: "admin", // httpCtrl auth login
password: "chatbot",
},
};
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
},
...
};
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
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
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 and auth tokens are write in ./tokens
folder.
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
.
Chatbot
$ docker exec wchatbot yarn log
HTTP Control Panel
$ docker exec wchatbot yarn http-ctrl:log
Conversations
$ docker exec wchatbot yarn conversations
Chatbot
$ yarn log
HTTP Control Panel
$ yarn http-ctrl:log
Conversations
$ yarn conversations
Chatbot
$ yarn log:dev
HTTP Control Panel
$ yarn log:http-ctrl:dev
Conversations
$ yarn conversations
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:
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!",
}
]
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",
]),
}
]
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",
]),
},
]
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",
}
]
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",
}
]
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",
}
]
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: "55368275082750726@c.us", // forward this message to this number
}
]
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 |
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 |
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() |
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).
Edit your file ./src/conversations/conversation.js
and create your custom conversation workflow.
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,
},
];
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,
},
];
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,
},
];
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,
},
];
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,
},
];
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,
},
];
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,
},
];
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: "5512736862295@c.us", // 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 = "5511994751001@c.us";
break;
case "option 2":
forward = "5584384738389@c.us";
break;
default:
forward = "5512736862295@c.us";
break;
}
return forward;
},
end: true,
},
];
/**
* 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,
},
];
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,
},
];
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: "5584384738389@c.us", // 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
},
];
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);
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);
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);
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("000000000000@c.us", "test");
}
);
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
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.
https://jordifernandes.com/donate/
Pull requests are welcome :)