Cyrus Stoller is Founder & CEO at Hazelnut. This post was originally published on his blog.
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:
- Users call a designated phone number
- Each user is then connected to another user who called the same phone number
- 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 application 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 application
run PhoneRoulette.new # running the Sinatra application
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.