Phil Nash
Posted on February 17, 2020
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:
What you'll need
To build your own WhatsApp bot along with this tutorial, you will need the following:
- Ruby and Bundler installed
- ngrok so we can expose our local webhook endpoints to the world in style
- A WhatsApp account
- A Twilio account (if you don't have one, sign up for a new Twilio account here and receive $10 credit when you upgrade)
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:
- Head to the WhatsApp sandbox area of the Twilio console, or navigate from the console to Programmable SMS and then WhatsApp
- The page will have the WhatsApp sandbox number on it. Open your WhatsApp application and start a new message to that number
- 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
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:
- Sinatra, a simple web framework
- The twilio-ruby gem so we can generate TwiML
- http.rb to help us make some HTTP requests later
- Envyable to manage environment variables in the 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.
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.
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.
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.
Posted on February 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.