Skip to content

A Node bot server for the Facebook Messenger Platform

License

Notifications You must be signed in to change notification settings

aiteq/messenger-bot

Repository files navigation

@aiteq/messenger-bot

npm version  Dependency Status  Build Status  Coverage Status  Codacy Badge

A TypeScript coded Node.js package for effective building and managing Facebook Messenger Bots.

Features Technologies Getting started Usage Chat extensions CLI API Doc
new BotServer({
    verifyToken: "hasta-la-vista-baby",
    accessToken: "open-sesame",
    appSecret: "too-secret-to-say"
})
.hear("order", async (chat: Chat) => {
    await chat.say("Well, let's order some Botcoins. I'll just ask you a few details.");
    botcoinMachine.placeOrder({
        amount: await chat.ask("How many Botcoins you want to buy?"),
        wallet: await chat.ask("What's the address of your Botcoin wallet?"),
        email: await chat.ask("And finally, tell me your email where I should send instructions.")
    });
    chat.say("Thank you for your order!");
})
.start();

Major features

  • Express.js based, event-driven bot server handling both Webhook and Chat Extension requests.
  • Subscribing to incoming text messages using regular expresions or to events emitted by the webhook middleware.
  • Support for synchronous conversations between the bot and users.
  • Utilities for calling Messenger Platform API functions from code outside chatbot flow.
  • Standalone CLI for instant access to backing functions of the Messenger API.
  • Complete type definitions so the package is ready to be used in both JavaScript and TypeScript projects.

Technologies used

Node.js

Node is an open-source, cross-platform JavaScript run-time environment for executing JavaScript code server-side. Fot its event-driven architecture capable non-blocking I/O it is perfectly fitting platform for building chatbots.

TypeScript

A syntactical superset of JavaScript adding static typing, implementing object-oriented principles and adopting latest ES features like generics, decorators or reflection. For Node projects the TypeScript especially brings a higher level of maintainability.

Express

Express is a helpful framework built around Node.js for performing actions as a web server. The package uses the Express for handling webhook requests (incoming chat messages) as well as providing access to Chat Extensions.

Axios

Axios provides fully Promise based HTTP client functionality, so it was a clear choice for implementation of calling Facebook Graph APIs.

Embedded JavaScript Templates

EJS is a very simple templating language that helps to create HTML for the pages to be shown in a programmatic way with injecting values. The package uses the EJS for rendering Chat Extensions.

Jest

Jest is yet another unit testing framework by Facebook. It covers all needed unit testing areas: tests definition, assertion and code coverage measuring and reporting. In addition the Jest supports TypeScript and Promises. It's interface is just right balanced between descriptiveness and verbosity, so its using is very intuitive..

Grunt

As a task runner the Grunt helps to organize building, releasing and maintaining the package.

Getting started

Prerequisites

Install

npm install @aiteq/messenger-bot --save

Facebook application

Create and setup a Facebook application using Quick Start Guide and find out access token and app secret.

Bot code

Create index.ts and let's go to start coding:

import { BotServer, Chat } from "@aiteq/messenger-bot";

Create an instance of BotServer:

let bot: BotServer = new BotServer({
    name: "MyBot",
    port: process.env.PORT || 8080,
    verifyToken: "hasta-la-vista-baby",
    accessToken: "open-sesame",
    appSecret: "too-secret-to-say"
});

Subscribe for some text:

bot.hear("hello", (chat: Chat) => {
    chat.say("Hello! I'm Emil, the Bot. How are you?");
});

Start the server:

bot.start();

Build and start

Add some scripts to package.json:

"scripts": {
    "compile": "tsc -p .",
    "start": "node ./dist/index.js"
},

Create tsconfig.json:

{
    "compilerOptions":
    {
        "module": "commonjs",
        "target": "es6",
        "rootDir": "src",
        "outDir": "bin"
    },
    "include": [ "src/**/*" ]
}

Transpile the source:

npm run compile

Now the bot is ready and you can bring it to live:

npm run start

Start ngrok:

ngrok http 8080

and copy the provided https:// URL for the following step.

Setup the webhook

Follow the guide, paste the previously copied URL to Callback URL and add /webhook. So, if the URL provided by ngrok is e.g. https://54d4f722.ngrok.io, the Callback URL will be https://54d4f722.ngrok.io/webhook.

Set hasta-la-vista-baby (see creating the bot above) as Verify Token and click Verify and Save.

It's alive!

Now the bot is listening for messages sent to your page. Try to send message "hello".

Use cases

Hooking text

You can subscribe to specific content of incoming text messages in two ways: exact commands and regular expressions. The subscribing is done using the BotServer.hear() method.

Commands

Hooking exact words or phrases can be useful when your bot is supposed to listen for commands like a CLI. Commands are specified as strings or arrays of strings and are considered to be case-insensitive.

bot.hear("wait", (chat: Chat) => {
    chat.say("I'm waiting ...");
})
.hear(["sleep", "go sleep", "go to sleep"], (chat: Chat) => {
    chat.say("Well, good night");
});

Regular expressions

Subscribing to specific content using regular expressions is much more flexible than listing exact words. Like commands, regular expressions can be specified as an array or single. If the regular expression contains capturing groups they are passed to the callback as third argument.

bot.hear(/^good (night|morning)$/i, (chat: Chat, text: string, captured: string[]) => {
    chat.say(`Hi, good ${captured[0]}!`);
});

In addition, you can mix commands and regular expressions in the same array.

Note: The regular expressions are used exactly as given, so if you want to test the text in case-insensitive manner you must explictily indicate it (the i flag).

Hooking events

Besides searching for specific text you can also subscribe to a number of events emitted while the bot is receiving messages through webhook. The subscribing is done using the BotServer.on() method.

Check the Webhook.Event enum for complete set of available events.

Identified Postback events

When subscribing to Postback based events

you have two options:

  1. subscribe to the type of the event and receive all events of this type (e.g. Persistent Menu item selected):
bot.on(Webhook.Event.PERSISTENT_MENU, (chat: Chat) => {
    chat.say("You've selected some menu item, but I really don't know what you want to do...");
});
  1. subscribe to the specific ID in addition to the type, what is much more useful way:
bot.on(Webhook.Event.PERSISTENT_MENU, "menu-item-about", (chat: Chat) => {
    chat.say("What can I say about myself... I'm a bot.");
});

Conversation

Conversation is synchronous message exchange between users and the bot by setting a flow of questions and answers. It's a useful way to get conventional UI tasks, like form filling, closer to interpersonal communication.

In order to ensure execution of steps of the conversation synchronously, all methods of the Chat class return Promises. So you can call next step after resolving the current. And when you add a little bit of syntactic sugar using the async/await concept, the conversation flow will look much more readable, almost like a real dialog.

There are two methods for interaction with the user:

Note: No events are emitted and no hear handlers called when the bot receives an answer to the question asked.

bot.on(Webhook.Event.PERSISTENT_MENU, "menu-item-song", async (chat: Chat) => {
    profile.favSong = await chat.ask("What's your favourite song?");
});

Or more complex flow:

bot.on(Webhook.Event.PERSISTENT_MENU, "menu-item-order", async (chat: Chat) => {
    await chat.say("Well, let's order some Botcoins. I'll just ask you a few details.");
    order.amount = await chat.ask("How many Botcoins you want to buy?");
    order.wallet = await chat.ask("What's the address of your Botcoin wallet?");
    order.email = await chat.ask("And finally, tell me your email where I should send instructions for payment.");
    chat.say("Thank you for your order!");
});

Input validation

As with classic forms, even in the case of a conversation, we need to validate user inputs. Therefore, the interface offers the ability to call a validation function wich you can pass when calling the ask() method. As a validator you can conveniently use functions from validator.js package:

import * as validator from "validator";

bot.on(Webhook.Event.PERSISTENT_MENU, "menu-item-form", async (chat: Chat) => {
    //...
    let email: string = await chat.ask("Give me your email address, please", validator.isEmail)
    //...
});

The bot will automatically repeat the question until the user enters a valid email address.

Unanswered questions

The questions asked using the ask() or askWithMessage() method may remain unanswered by the user. In order to avoid pending Promises these questions are set to automatically expire. The expiration period is set to 5 minutes by default but you can override it using BotConfig.askTimeout parameter. The unanswered question is rejected after its expiration. If you want to react to this situation you may catch the rejection:

bot.on(Webhook.Event.PERSISTENT_MENU, "menu-item-name", async (chat: Chat) => {
    try {
        let name = await chat.ask("What's your name?");
        chat.say(`Hello, ${name}. My name is Emil.`);
    } catch (error) {
        chat.say("I'm so sorry you have no name.");
    }
});

If you won't catch the expiration the bot will swallow it without consequences. Don't worry about it.

Media reusing

When you're about to send a message with a media attached you can indicate wheather the media should be reused. The bot stores all reusable attachment ID's. When you try to send the same attachment (with the same URL and reusable set to true) twice or more times the bot replace media's URL with stored attachment ID.

BotUtils

The Facebook Messenger Platform API contains not only interactive functions for message exchange between the bot and users. There are a lot of services in the API backing the communication like activating Get Started button, installing Persistent Menu or generating Messenger Code.

Sometimes, we also want to send a push message - a message sent to the user proactively, not just as a response to some incoming message.

The above cases are not quite bot-aware functions. Thus, in order to keep BotServer's interface clean, these services are made available through the BotUtils class.

An instance of the BotUtils is initialized passing the the accessToken.

let utils: BotUtils = new BotUtils("open, sesame");

Example: send push message

utils.sendText("123450987643", "RATE ALERT: Botcoin price has reached $1,000");

See BotUtils.sendText()

Example: activate Get Started button

utils.setGetStartedButton();

See BotUtils.setGetStartedButton()

Example: generate Messenger Code

utils.generateMessengerCode("my-m-code.png");

See BotUtils.generateMessengerCode()

Server monitoring

The bot server supports responding for ping requests. By default, the ping service is attached to /ping path and may be overrided by BotConfig.pingPath configuration parameter. The ping request must use the GET method. If all goes well the "OK" string is returned with 200 HTTP code.

The ping feature is useful with conjunction with up-time monitoring services like Uptime Robot.

Chat extensions

The package supports Embedded JavaScript Templates (EJS) for rendering Chat Extension views. For creating a new extension follow these steps.

1. Implement ChatExtension interface

import { ChatExtension } from "@aiteq/messenger-bot";

export class MyExtension implements ChatExtension {

    constructor() {
        // name your extension - the name will be a part of extension's URL
        super("my");
    }

    // implement abstract method getModel()
    public getModel(): any {
        return {
            name: "Captain Nemo"
        };
    }
}

The chat extension class must implement abstract method getModel() that provides data to be used in the view.

The getModel is called every time an extension is requested.

2. Create view

In your project root create views folder (default for placing views in Express application) and my.ejs file within it.

<!DOCTYPE html>
<html>
    <head>
        <title>My Extension</title>
    </head>
    <body>
        <div>Greetings, <%= name %>!</div>
    </body>
</html>

3. Add the extension

At last you have to register the extension to the bot server using the addChatExtension method.

bot.addChatExtension(new MyExtension());

Now the extension is ready to use and you can test it pointing to <your-bot-url>/ext/my.

Note that the default path for extensions is /ext and you can chanage it by setting the extensionPath property of BotConfig.

CLI

The BotUtils class is useful if you need non-interactive functions of the Messenger API to be called within your application. More often, however, you will need to use these features one-time, operatively, or as a part of such automated workflow like shell script. There is a Command Line Interface ready for these cases.

General usage

mbutil <group> [command] [options]

A group represents a specific part of the Messenger API. Available groups are:

Group Functions
send Send text or attachment message
getstarted Manage Get Started button
greeting Manage page's localized greetings
menu Manage Persistent Menu
domains Manage Domain Whitelist
audience Manage Target Audience settings
accountlinking Manage Account Linking settings
chatext Manage Chat Extensions settings
code Generate Messenger Code

For each group, you can view help by:

mbutil <group> --help

Global options:

Option Function
--config <path> path to the config JSON file; must contain the accessToken property
--accessToken <token> access token (one of --config or --accessToken must be specified)
--help display help for the group

Group: send

Send plain text or attachment push message.

Usage:

mbutil send "<text>" --recipient <id> [options]
mbutil send image|audio|video|file --url <url> --recipient <id> [options]

Options:

Option Function
--recipient <id> ID of the recipient
--url <url> URL of the file to be attached

Group: getstarted

Manage Get Started button.

Display current setting:

mbutil getstarted get [options]

Activate the button with optional data:

mbutil getstarted set [--data "<data>"] [options]

Remove the button:

mbutil getstarted delete [options]

Options:

Option Function
--data "<data>" text or JSON to be send when the user tapped the button

Group: greeting

Manage page's localized Greeting.

Display current setting:

mbutil greeting get [options]

Add localized greeting text:

mbutil greeting add "<text>" [--locale <locale>] [options]

Remove greeting text:

mbutil greeting delete [options]

Options:

Option Function
--locale <locale> greeting's locale (supported locales); if omitted the text will be set as default

Group: menu

Manage Persistent Menu.

Display current setting:

mbutil menu get [options]

Set Persistent Menu according to definition in a JSON file:

mbutil menu set --file <path> [--locale <locale>] [options]

Remove Persistent Menu:

mbutil menu delete [options]

Options:

Option Function
--file <path> path to menu definition JSON file

Required structure of the JSON menu definition file is clear from the following example (object contains two variants of the menu for "default" and "cs_CZ" locales):

{
    "default": {
        "composerInputDisabled": false,
        "items": [
            {
                "title": "Show exchange rate",
                "id": "menu-rate"
            },
            {
                "title": "Buy Botcoins",
                "id": "menu-buy"
            },
            {
                "title": "Aiteq International, Ltd.",
                "url": "http://www.aiteq.international"
            }
        ]
    },
    "cs_CZ": {
        "composerInputDisabled": false,
        "items": [
            {
                "title": "Aktuální kurz",
                "id": "menu-rate"
            },
            {
                "title": "Koupit Botcoiny",
                "id": "menu-buy"
            },
            {
                "title": "Aiteq Reloaded, s.r.o.",
                "url": "http://www.aiteq.com"
            }
        ]
    }
}

Group: domains

Manage Domain Whitelist.

Display current whitelisted domains:

mbutil domains get [options]

Add one or more domains (space separated list) to the whitelist:

mbutil domains add <domain> [domains] [options]

Delete the domain whitelist:

mbutil domains delete [options]

Group: audience

Manage Target Audience settings. Countries are identified by ISO 3166 Alpha-2 codes.

Display current setting:

mbutil audience get [options]

Open Target Audience for all countries:

mbutil audience open [options]

Close Target Audience for all countries:

mbutil audience close [options]

Add one or more countries (space separated list) to the whitelist:

mbutil audience whitelist <country> [countries] [options]

Add one or more countries (space separated list) to the blacklist:

mbutil audience blacklist <country> [countries] [options]

Remove all Target Audience settings:

mbutil audience delete [options]

Group: accountlinking

Manage Account Linking URL.

Display currently set Account Linking URL:

mbutil accountlinking get [options]

Set Account Linking URL:

mbutil accountlinking set <url> [options]

Delete currently set Account Linking URL:

mbutil accountlinking delete [options]

Group: chatext

Manage Chat Extension URL.

Display currently set Chat Extension URL and settings:

mbutil chatext get [options]

Set Chat Extension URL:

mbutil chatext set <url> [options]

Delete currently set Chat Extension URL:

mbutil chatext delete [options]

Options:

Option Value Function
--inTest controls whether public users can see the Chat Extension
--shareButton controls whether the share button in the webview is enabled

Group: code

Generate Messenger Code.

Set Chat Extension URL:

mbutil code generate [options]

Options:

Option Value Function
--out path output file's path and name (default: ./code.png)
--size number between 100 - 2000 size of generated image, in pixels (default: 1000)
--ref text data to be received when user scans the code (optional)

API documentation

Package's reference API documentation is located in doc folder.

Credits

Tomáš Klíma, Aiteq & Aiteq

License

MIT