/jfa-whatsapp-chatbot

🤖 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.

Primary LanguageJavaScriptMIT LicenseMIT

Jfa WhatsApp Chatbot

Jfa WhatsApp Chatbot 💬

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

🛟 Introduction video

  1. Create a new repository from this template
  2. Install in your development environment
  3. Configure port(s), credentials, etc
  4. Write your conversation flow
  5. 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.

Replies Relations

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: "55368275082750726@c.us", // 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).

Http Control Panel

Examples

Edit your file ./src/conversations/conversation.js and create your custom conversation workflow.

Example 1

doc/examples/conversation1.js

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

doc/examples/conversation2.js

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

doc/examples/conversation3.js

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

doc/examples/conversation4.js

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

doc/examples/conversation5.js

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

doc/examples/conversation6.js

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

doc/examples/conversation7.js

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

doc/examples/conversation8.js

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,
  },
];

Example 9

doc/examples/conversation9.js

/**
 * 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: "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
  },
];

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("000000000000@c.us", "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.

Donate

https://jordifernandes.com/donate/

License

MIT License

Contributing

Pull requests are welcome :)

Contributors