Cyrus Stoller home about consulting

Building 'Phone Roulette' with Twilio & Sinatra

This weekend, I made Phone Roulette for fun. I wanted to experiment with the Twilio API, and this seemed like a good bite size project for me to get my feet wet. I was surprised that over 200 people called the number I setup, (480) 878-7813.

Let’s get started

In this blog post I’ll explain how to write a simple Sinatra application called Phone Roulette (github repo). Here’s the minimal spec:

  1. Users call a designated phone number
  2. Each user is then connected to another user who called the same phone number
  3. Ideally, users will be able to press a button on their keypad to start talking to someone else

For our application, we’re using many of the components one would use when writing the business logic for a call center. In a call center, you have callers who wait in queue to be connected to customer support agents. In our application, we have incoming calls that we need to dispatch to other callers.

Installing Gems

Here is the basic directory structure we’ll need to deploy this application for free using Heroku.

phone_roulette
├── Gemfile
├── Gemfile.lock
├── README.md
├── config.ru
├── phone_roulette.rb
├── tmp
└── views
    ├── _welcome.erb
    ├── about_to_connect.erb
    ├── agent_call.erb
    └── customer_call.erb

The Gemfile is where you specify third party libraries, called gems, used in your ruby project. The Ruby Toolbox is great place to discover new gems. I like that you can easily browse by category and see whether a project is still being actively developed.

Put the following into your Gemfile:

## Gemfile ##
source 'https://rubygems.org' # where should bundler look for these gems
ruby '2.0.0' # version of ruby - not critical but I like to make it explicit
gem 'sinatra' # installing the Sinatra framework
gem 'thin' # a simple rack server that will receive web requests
group :development do
  gem 'shotgun' # to auto reload our app when we make changes in development
end

From inside your photo_roulette directory run

$ bundle install

If that doesn’t work, you may need to first run

$ [sudo] gem install bundler

Using sudo may be optional depending on how your system is setup. Once you have successfully run the bundle command you should see a Gemfile.lock file. Bundler records the specific versions of the gems you are using in the Gemfile.lock, this will prevent version incompatibilities as new versions of gems are released.

Rack

Now that we have the gems we need installed, let’s get Sinatra setup to accept requests.

Sinatra is DSL for creating Rack-based applications. To run Rack applications on Heroku we need to provide a rackup file called config.ru. The config.ru is just a ruby file, but it has an ru extension to mark that it is a rackup file. To learn more, check out the rack-wiki.

Put the following in your config.ru:

## config.ru ##
root = ::File.dirname(__FILE__) # defining the root directory
require ::File.join( root, 'phone_roulette' ) # requiring the Sinatra app
run PhoneRoulette.new # running the Sinatra app

Now that we have our rackup file setup, we need to write our Sinatra application.

Basic Sinatra App

To start we need to define the PhoneRoulette application we referenced in the config.ru.

## phone_roulette.rb ##
require "sinatra"
class PhoneRoulette < Sinatra::Base
  get "/" do
    "Hello World"
  end
end

To run our application we can just run rackup config.ru, but if we want it to auto-reload we can use the shotgun gem that we installed by running

$ shotgun -d config.ru

If you go to http://localhost:9393 in your browser you should see “Hello World”. Now change the “Hello World” line in your phone_roulette.rb to “Hello World 2” and save. If you reload http://localhost:9393 and you should see “Hello World 2”.

Good work! You’ve written a Hello World application using Sinatra.

Deploying with Heroku

To deploy this application, you first need to add everything into a git repository. If you need help with that, I wrote a blog post on how to get started with git.

Once you have a git repository for your Sinatra application, create a Cedar Heroku application and push your repository to that remote. For more details on how to do this click here.

Configuring Twilio

Sign up for a Twilio account and choose the phone number you want people to be able to call.

Once you’re signed in, click on the Numbers tab at the top of your screen and then click on the phone number that you picked when you signed up.

In my case I clicked on the +1 480-878-7813 link. I then set the Voice Request URL to point to my Heroku application.

I instructed Twilio to send a POST whenever anyone calls (480) 878-7813 to

http://pacific-stream-2006.herokuapp.com/roulette.xml

And that’s all we had to do to setup Twilio. Super easy. We now have to tell Twilio how to respond to phone calls using our Sinatra application.

Responding to Twilio

As you may have guessed, we need to write a new route to respond to /roulette.xml. I’ll start by defining the higher level code and then explain the methods that I defined that make this simple code work. First, add the following inside the PhoneRoulette class:

## phone_roulette.rb ##
get_or_post '/roulette.xml' do
  if flip_even_odd
    erb :agent_call, :content_type => :xml
  else
    erb :customer_call, :content_type => :xml
  end
end

In English, we’re telling Sinatra to alternate between rendering agent_call and customer_call erb templates with content-type xml when it receives either a GET or POST request to /roulette.xml. Essentially, we’re making a customer hotline of length one and then having the next caller act as agent to handle the caller on the queue.

I added a get_or_post method at the top of my phone_roulette.rb after requiring Sinatra. This tells our application to respond to either GET or POST requests for a given path. This makes it easier to test responses in my browser and there’s a chance that I may want Twilio to send GET requests instead of POST requests in the future.

## phone_roulette.rb ##
def get_or_post(path, opts={}, &block)
  get(path, opts, &block)
  post(path, opts, &block)
end

This won’t quite work yet because we haven’t defined flip_even_odd and we haven’t told our Sinatra application where to find the erb template files that will tell Twilio what to do. But as soon as we’ve done that, our PhoneRoulette application will be ready to go.

Defining flip_even_odd

I tried my best to see if there was a way to prevent this application from having to track any state, but alas I need to keep one bit of state to be sure that callers were being matched as soon as possible. Instead of using SQL or Redis to store one bit, I opted to just use a temp file.

## phone_roulette.rb ##
def flip_even_odd
  bool_file = File.join("/tmp", "even_odd")
  if File.exists?(bool_file)
    File.delete(bool_file)
    return true
  else
    File.open(bool_file, "w") do |w|
      w.puts 1
    end
    return false
  end
end

Defining erb templates

To tell Sinatra where to find our erb templates we need to add the following to our PhoneRoulette class:

## phone_roulette.rb ##
set :root, File.dirname(__FILE__)
set :views, Proc.new { File.join(root, "views") }

Next, we’ll define the templates in views/agent_call.erb and views/customer_call.erb that tell Twilio how to react to our “agents” and “customers”.

Once I figured out that I wanted to use the Twilio Queue Feature, writing these templates was pretty straight forward. My only gripe with the Twilio documentation is that it takes too long to navigate. (In the interest of full disclosure, my preference would be for an ASCII man page instead something glossy.)

For simplicity, I’ll call my queue roulette. You can call yours whatever you like. Just use the name of your queue instead wherever I put roulette in these templates.

We want our “agent” to be connected to any “callers” on our queue. To do this, we need to respond with the following TwiML.

<!-- views/agent_call.erb -->
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="woman">Welcome to Phone Roulette</Say>
  <Dial hangupOnStar="true" action="roulette.xml">
    <Queue url="about_to_connect.xml">roulette</Queue>
  </Dial>
</Response>

This tells Twilio to answer the call and say, “Welcome to Phone Roulette” with a woman’s robo voice. Then it should dial the roulette queue. If this user presses * while connected, Twilio will call roulette.xml to figure out what it should do next.

And here is how we place a customer on the roulette queue.

<!-- views/customer_call.erb -->
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Enqueue waitUrl="about_to_connect.xml">roulette</Enqueue>
</Response>

This tells Twilio to answer the call and enqueue the call on the roulette queue. The waitURL tells Twilio what to do while the caller is waiting for an agent to answer the call.

Now we’ll define how Sinatra responds to a request to about_to_connect.xml inside the PhoneRoulette class.

## phone_roulette.rb ##
get_or_post '/about_to_connect.xml' do
  erb :about_to_connect, :content_type => :xml
end

And in our last template we’ll tell Twilio what to do while a “caller” is waiting for an agent to become available.

<!-- views/about_to_connect.erb -->
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say voice="woman">Welcome to Phone Roulette</Say>
  <Pause length="1"/>
  <Say voice="woman">Connecting</Say>
  <Pause length="1"/>
</Response>

The pause command tells Twilio to pause for one second before proceeding.

Using Partials

It’s great that everything is working, but I don’t like that that salutation is repeated code in the agent_call and about_to_connect templates. Luckily, it’s easy to refactor this into a partial.

To do this, make a new views/_welcome.erb file and put the repeated salutation code:

<!-- views/_welcome.erb -->
<Say voice="woman">Welcome to Phone Roulette</Say>
<Pause length="1"/>
<Say voice="woman">Connecting</Say>
<Pause length="1"/>

And then change views/agent_call.erb to:

<!-- views/agent_call.erb -->
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <%= erb :_welcome, :layout => false %>
  <Dial hangupOnStar="true" action="roulette.xml">
    <Queue url="about_to_connect.xml">roulette</Queue>
  </Dial>
</Response>

And views/about_to_connect.erb to:

<!-- views/about_to_connect.erb -->
<?xml version="1.0" encoding="UTF-8" ?>
<Response>
  <%= erb :_welcome, :layout => false %>
</Response>

Conclusion

Thanks for reading this far. Commit your code and push it up to Heroku. You’re all set!

If you’re interested in seeing a complete repository, you can clone mine.

PS I worked on this weekend project with Alex Meliones, one of the co-founders of BitWall.

Category Tutorial