Build a WhatsApp chatbot with Ruby, Sinatra and Twilio

February 17, 2020
Written by
Phil Nash
Twilion

Build a WhatsApp chatbot with Ruby, Sinatra and Twilio

Chatbots are programs that communicate some way with humans. They can be very basic, responding to keywords or phrases, or use something like Twilio Autopilot to take advantage of natural language understanding (NLU) to provide a richer experience and build out more complicated conversations.

In this tutorial we are going to see how easy it is to get started building chatbots for WhatsApp using the Twilio API for WhatsApp and the Ruby web framework Sinatra. Here's an example of the conversation we're going to build:

Using the bot inside the WhatsApp application. First we ask for cats and get a cat picture and fact. Then we ask for dogs and get a dog picture and fact. Finally we ask the bot if it can do anything else and it says it only knows about dogs and cats.

What you'll need

To build your own WhatsApp bot along with this tutorial, you will need the following:

Configure your WhatsApp sandbox

To launch a bot on WhatsApp you must go through an approval process with WhatsApp, but Twilio allows you to build and test your WhatsApp apps using our sandbox. Let's start by configuring the sandbox to use with your WhatsApp account.

The Twilio console walks you through the process, but here's what you need to do:

  1. Head to the WhatsApp sandbox area of the Twilio console, or navigate from the console to Programmable SMS and then WhatsApp
  2. The page will have the WhatsApp sandbox number on it. Open your WhatsApp application and start a new message to that number
  3. The page also has the message you need to send, which is "join" plus two random words, like "join flagrant-pigeon". Send your message to the sandbox number

The Twilio Sandbox for WhatsApp first screen. It tells you to send a message to the WhatsApp number +1 415 523 8886 with a code that starts with "join".

When you receive a message back, you are all set up and ready to work with the sandbox.

Creating the Ruby application

Let's kick off a new Ruby application in which to build our bot. Start by creating a new directory to work in. Then initialise a new Gemfile in the app and create a couple of files we'll need:

mkdir whatsapp-bot
cd whatsapp-bot
bundle init
mkdir config
touch bot.rb config.ru config/env.yml

Add the gems we will use to build this application:

bundle add sinatra twilio-ruby http envyable

config/env.yml will store our application config and Envyable will load it into the environment for us. We only need to store one piece of config for this application: your Twilio auth token which you can find on your Twilio console dashboard. Add your auth token to config/env.yml:

TWILIO_AUTH_TOKEN: YOUR_TWILIO_AUTH_TOKEN

We'll use config.ru to load the application and the config, and to run it. Copy the following into config.ru:

require 'bundler'
Bundler.require

Envyable.load('./config/env.yml')

require './bot.rb'
run WhatsAppBot

Let's test that everything is working as expected by creating a "Hello World!" Sinatra application. Open bot.rb and enter the following code:

require "sinatra/base"

class WhatsAppBot < Sinatra::Base
  get '/' do
    "Hello World!"
  end
end

On the command line start the application with:

bundle exec rackup config.ru

The application will start on localhost:9292. Open that in your browser and you will see the text "Hello World!".

Building a chatbot

Now that our application is all set up we can start to build our bot. In this post we will build a simple bot that responds to two keywords when someone sends a message to our WhatsApp number. The words we're going to look for in the message are "dog" or "cat" and our bot will respond with a random picture and fact about either dogs or cats.

Webhooks

With the Twilio API for WhatsApp, when your number (or sandbox account) receives a message, Twilio makes a webhook request to a URL that you define. That request will include all the information about the message, including the body of the message.

Our application will need to define a route that we can set as the webhook request URL to receive those incoming messages, parse out whether the message contains the words we are looking for, and respond with the use of TwiML. TwiML is a set of XML elements that describe how your application communicates with Twilio.

The application we have built so far could respond to a webhook at the root path, but all it does is respond with "Hello World!" so let's get to work updating that.

Let's remove the "Hello World!" route and add a /bot route instead. Twilio webhooks are POST requests by default, so we'll set up the route to handle that too. To do so, we pass a block to the post method that Sinatra defines.

require "sinatra/base"

class WhatsAppBot < Sinatra::Base
  post '/bot' do

  end
end

Next let's extract the body of the message from the request parameters. Since we're going to try to match the contents of the message against the words "dog" and "cat" we'll also translate the body to lower case.

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
  end
end

We are going to respond to the message using TwiML and the twilio-ruby library gives us a useful class for building up our response: Twilio::TwiML::MessagingResponse. Initialise a new response on the next line:

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
    response = Twilio::TwiML::MessagingResponse.new
  end
end

The MessagingResponse object uses the builder pattern to generate the response. We're going to build a message and then add a body and media to it. We can pass a block to the Twilio::TwiML::MessagingResponse#message method and that will nest those elements within a <Message> element in the result.

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
    response = Twilio::TwiML::MessagingResponse.new
    response.message do |message|
      # nested in a <Message>
    end
  end
end

Now we need to start building our actual response. We'll check to see if the body includes the word "dog" or "cat" and add the relevant responses. If the body of the message contains neither word we should also add a default response to tell the user what the bot can respond to.

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
    response = Twilio::TwiML::MessagingResponse.new
    response.message do |message|
      if body.include?("dog")
        # add dog fact and picture to the message
      end
      if body.include?("cat")
        # add cat fact and picture to the message
      end
      if !(body.include?("dog") || body.include?("cat"))
        message.body("I only know about dogs or cats, sorry!")
      end
    end
  end
end

We currently have no way to get dog or cat facts. Luckily there are some APIs we can use for this. For dogs we will use the Dog CEO API for pictures and this dog API for facts. For cats there's TheCatAPI for pictures and the cat facts API for facts. We'll use the http.rb library we installed earlier to make requests to each of these APIs.

Each API works with GET requests. To make a get request with http.rb you call get on the HTTP module passing the URL as a string. The get method returns a response object whose contents you can read by calling to_s.

To make the application nice and tidy let's wrap up the API calls to each of these services into a Dog and Cat module, each with a fact and picture method.

Add these modules to the bottom bot.rb:

module Dog
  def self.fact
    response = HTTP.get("https://dog-api.kinduff.com/api/facts")
    JSON.parse(response.to_s)["facts"].first
  end

  def self.picture
    response = HTTP.get("https://dog.ceo/api/breeds/image/random")
    JSON.parse(response.to_s)["message"]
  end
end

module Cat
  def self.fact
    response = HTTP.get("https://catfact.ninja/fact")
    JSON.parse(response.to_s)["fact"]
  end

  def self.picture
    response = HTTP.get("https://api.thecatapi.com/v1/images/search")
    JSON.parse(response.to_s).first["url"]
  end
end

Now we can use these modules in the webhook response like so:

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
    response = Twilio::TwiML::MessagingResponse.new
    response.message do |message|
      if body.include?("dog")
        message.body(Dog.fact)
        message.media(Dog.picture)
      end
      if body.include?("cat")
        message.body(Cat.fact)
        message.media(Cat.picture)
      end
      if !(body.include?("dog") || body.include?("cat"))
        message.body("I only know about dogs or cats, sorry!")
      end
    end
  end
end

To return the message back to WhatsApp via Twilio we need to set the content type of the response to "text/xml" and return the XML string.

class WhatsAppBot < Sinatra::Base
  post '/bot' do
    body = params["Body"].downcase
    response = Twilio::TwiML::MessagingResponse.new
    response.message do |message|
      if body.include?("dog")
        message.body(Dog.fact)
        message.media(Dog.picture)
      end
      if body.include?("cat")
        message.body(Cat.fact)
        message.media(Cat.picture)
      end
      if !(body.include?("dog") || body.include?("cat"))
        message.body("I only know about dogs or cats, sorry!")
      end
    end
    content_type "text/xml"
    response.to_xml
  end
end

That's all the code for the webhook, but there's one more thing to consider.

Webhook security

This might not be the most mission critical data to be returning in a webhook request, but it is good practice to secure your webhooks to ensure you only respond to requests from the service you are expecting. Twilio signs all webhook requests using your auth token and you can validate that signature to validate the request.

The twilio-ruby library provides rack middleware to make validating requests from Twilio easy: let's add that to the application too. At the top of your WhatsAppBot class include the middleware with the use method. Pass the following three arguments to use: the middleware class Rack::TwilioWebhookAuthentication, the auth token, and the path to protect (in this case, "/bot".)

class WhatsAppBot < Sinatra::Base
  use Rack::TwilioWebhookAuthentication, ENV['TWILIO_AUTH_TOKEN'], '/bot'
 
  post '/bot' do

Hooking up the bot to WhatsApp

On the command line stop the application with ctrl/cmd + c and restart it with:

bundle exec rackup config.ru

We now need to make sure Twilio webhooks can reach our application. This is why I included ngrok in the requirements for this application. ngrok allows us to connect a public URL to the application running on our machine. If you don't already have ngrok installed, follow the instructions to download and install ngrok.

Start ngrok up to tunnel through to port 9292 with the following command:

ngrok http 9292

This will give you an ngrok URL that you can now add to your WhatsApp sandbox so that incoming messages will be directed to your application.

The ngrok dashboard. It shows the URL you can now use to tunnel traffic through to your locally running app.

Take that ngrok URL and add the path to the bot so it looks like this: https://YOUR_NGROK_SUBDOMAIN.ngrok.io/bot. Enter that URL in the WhatsApp sandbox admin in the input marked "When a message comes in" and save the configuration.

The Twilio Sandbox for WhatsApp config page. You should add your ngrok URL into the field labelled "When a message comes in".

Testing your bot

You can now send a message to the WhatsApp sandbox number and your application will swing into action to return you pictures and facts of dogs or cats.

Using the bot inside the WhatsApp application. First we ask for cats and get a cat picture and fact. Then we ask for dogs and get a dog picture and fact. Finally we ask the bot if it can do anything else and it says it only knows about dogs and cats.

 

Build more bots

In this post we've seen how to configure the Twilio API for WhatsApp and connect it up to a Ruby application to return pictures and facts of dogs or cats. You can get all the code for this bot on GitHub.

It's a simple bot, but provides a good basis to build more. You could look into receiving images from WhatsApp to make a visual bot or sending or receiving location as part of the message. We could also build on this to create even smarter bots using Twilio Autopilot.

Have you built any interesting bots? What other features would you like to see explored? Let me know in the comments or on Twitter at @philnash.