Writing a bot for Programmable Chat in Node.js

August 29, 2016
Written by

header-chat-bot

It seems like bots are the new hot thing that every chat supports. They usually augment conversations or they can perform tasks for the user. We will add to an existing Programmable Chat chat a simple bot that will return us a GIF whenever we ask for it.

Getting Your Initial Chat Up And Running

We will be working off an existing chat application. Before we get started with our bot we need to get a couple of things ready.
First of all, make sure you have:

  • Node.js and npm which you can get here
  • A Twilio Account to set up Programmable Chat – Sign up for free
  • ngrok – You can read more about ngrok here

The existing chat application that we will base our work off is this quickstart project. It only takes 5 minutes to set this project up. Clone the repository by running:

git clone https://github.com/TwilioDevEd/ipm-quickstart-node.git
cd ipm-quickstart-node

Configure it according to the README.

Intercepting Messages

Twilio Programmable Chat will handle the sending of chat messages from client to client. The product also has webhook notifications for when new messages in our chat are sent. Using these webhooks we can control whether a message should be delivered or blocked. All this can be done simply by returning different HTTP status codes. Status code 200 (OK) means continue delivering this message and 403 (Forbidden) means block this message.
Before we decide whether to block or forward a message we will need to access the data that Twilio sends to our application. To do that we first need to parse it first by adding body-parser to our project using npm:

npm install body-parser --save

Once the package is installed add it to index.js:

require('dotenv').load();
var http = require('http');
var path = require('path');
var AccessToken = require('twilio').AccessToken;
var IpMessagingGrant = AccessToken.IpMessagingGrant;
var express = require('express');
var bodyParser = require('body-parser');
var randomUsername = require('./randos');

// Create Express webapp
var app = express();
app.use(express.static(path.join(__dirname, 'public')));

app.use(bodyParser.json({})) 
app.use(bodyParser.urlencoded({
  extended: true
}));

Add a very basic endpoint to index.js that will print the data received and return the status code 200 to tell Twilio to continue with delivering the message:

app.post('/message', function (req, res, next) {
  console.dir(req.body, { depth: 1 });
  res.sendStatus(200);
});

// Create http server and run it
var server = http.createServer(app);
var port = process.env.PORT || 3000;
server.listen(port, function() {
    console.log('Express server running on *:' + port);
});

Restart the server by running:

node .

We need to expose our application to the public so Twilio can deliver the webhook. For this execute in a different terminal:

ngrok http 3000

screenshot-ngrok.png

Afterwards go to your messaging service in the console and configure the webhook URL as: http://.ngrok.io/message.
Make sure that the HTTP method is POST and that OnMessageSend is activated.
ipm-screenshot-webhook.png

Navigate to http://localhost:3000 and start writing messages. You should see them start appearing on in your terminal with additional information for the message.
logging-messages.gif

Controlling The Conversation

Now that you are receiving all the necessary information, we can dive deeper into the control that these webhooks provide. If we return the status code 403 instead of 200 we can block the respective message. Let’s alter the route to intercept and not deliver messages that start with /gif:

app.post('/message', function (req, res, next) {
  if (req.body.Body.toLowerCase().indexOf('/gif') === 0) {
    res.sendStatus(403);
  } else {
    res.sendStatus(200);
  }
});

Restart your server and send a message with /gif. You should see that it’s not showing up. All other messages will normally appear.

blocked-gif-message.gif

Let The Bots Join!

So far we can intercept the content that is being sent but we want to actually send our own messages. We want our bot to actually participate in the chat. For this we will forward the message at all times and then use the information about the channel to place the bot in the channel and write a message to it using the Twilio helper library.
Modify the index.js file accordingly:

require('dotenv').load();
var http = require('http');
var path = require('path');
var Twilio = require('twilio');
var AccessToken = Twilio.AccessToken;
var IpMessagingGrant = AccessToken.IpMessagingGrant;
var express = require('express');
var bodyParser = require('body-parser');
var randomUsername = require('./randos');

var client = new Twilio.IpMessagingClient();
var service = client.services(process.env.TWILIO_IPM_SERVICE_SID);
var botName = 'gifinator';

// Create Express webapp
var app = express();
app.use(express.static(path.join(__dirname, 'public')));

app.use(bodyParser.json({})) 
app.use(bodyParser.urlencoded({
  extended: true
}));

app.get('/token', function(request, response) {
// ----------------------
// Hidden for conciseness
// ----------------------
});

app.post('/message', function (req, res, next) {
    res.sendStatus(200);

    if (req.body.Body.toLowerCase().indexOf('/gif') === 0) {
        var channel = service.channels(req.body.ChannelSid);
        channel.members.create({
            identity: botName
        }).then(function (response) {
            return channel.messages.create({
                from: botName,
                body: 'http://i.giphy.com/h8E7oT2FGEJkQ.gif'
            });
        }).then(function (response) {
            console.log('Bot message sent!');
        }).catch(function (err) {
            console.error('Failed to send message');
            console.error(err);
        });
    }
});

// Create http server and run it
var server = http.createServer(app);
var port = process.env.PORT || 3000;
server.listen(port, function() {
    console.log('Express server running on *:' + port);
});

Once you restarted the server try writing /gif in the chat again. Now you should see the bot write the actual message and your message staying preserved.

returned-gif-link.gif

Upgrade Your Bot

So far we are only sending text with our bot. We can use the new message attributes to add additional contextual information to our message, such as information that this is actually a picture.
For this send an object as attributes that specifies the type as 'image' when creating the message:

app.post('/message', function (req, res, next) {
// ----------------------
// Hidden for conciseness
// ----------------------
            return channel.messages.create({
                from: botName,
                body: 'http://i.giphy.com/h8E7oT2FGEJkQ.gif',
                attributes: JSON.stringify({
                    type: 'image'
                })
            })
// ----------------------
// Hidden for conciseness
// ----------------------
});

Now that we are sending this information we need to update our front-end to display the image. Open public/index.js and update the printMessage method as well as the event listener for messageAdded to wrap an image-tag around the content:

// ----------------------
// Hidden for conciseness
// ----------------------

    // Helper function to print chat message to the chat window
    function printMessage(fromUser, message, type) {
        var $user = $('<span class="username">').text(fromUser + ':');
        if (fromUser === username) {
            $user.addClass('me');
        }
        // replace original $message declaration with the following lines
        var $message = $('<span class="message">');
        if (type === 'image') {
            $message.html('<img width="500" src="' + message + '"/>');
        } else {
            $message = $message.text(message);
        }
        var $container = $('<div class="message-container">');
        $container.append($user).append($message);
        $chatWindow.append($container);
        $chatWindow.scrollTop($chatWindow[0].scrollHeight);
    }

// ----------------------
// Hidden for conciseness
// ----------------------
    
    // Set up channel after it has been found
    function setupChannel() {
        // Join the general channel
        generalChannel.join().then(function(channel) {
            print('Joined channel as ' +
                  '<span class="me">' + username + '</span>.', true);
        });

        // Listen for new messages sent to the channel
        generalChannel.on('messageAdded', function(message) {
            var type;
            if (message.attributes) {
                type = message.attributes.type;
            }
            printMessage(message.author, message.body, type);
        });
    }

Now restart the server and reload your browser and you should be able to see the GIF properly appear the next time you write /gif!

with-actual-gif.gif

Rise Of The Bots

mIZ9rPeMKefm0.gif

Amazing! You just finished your first bot!
But you don’t have to stop here. You could extend the bot to return a random GIF using the Giphy API or come up with your very own bots. How about a bot that recommends a restaurant whenever you talk with your colleagues about lunch? Now that you have the tools you are only limited by your imagination!
Let me know what you came up with by sending me a message or tell me in person at SIGNAL London. You can use DKUNDEL20 to get 20% off.

We can’t wait to see what you build!