As mentioned previously, I really hate getting woken up at 3 AM in the morning. This happens fairly frequently for me, though, because I live in Japan and about half of the people who call me do not. I have not been effective at getting them to check what time it is here before they call, but I certainly want them to call, and even call me in the middle of the night if it is an emergency.

So I made myself a phone secretary with Twilio, their Ruby gem, and Sinatra (a lightweight Ruby web framework). I gave my friends and family a US number assigned to me by Twilio. Dialing it causes Twilio’s computer to talk to my server and figure out what I want to do with the call. The server runs a Sinatra app which checks the time in Japan and either forwards the call to the most appropriate phone or gently informs the user that it is 4:30 AM in the morning.

The code for this took 10 minutes. Reasoning my way through a deployment took, hmm, 3 hours or so. I am a programmer not a sysadmin, what can I say. I thought I’d write down what I did so that other folks can save themselves some pain.

Code (You’re probably not too interested in the exact logic, but feel free to use it as a springboard if you want to make a secretary/call forwarding app):

require 'rubygems' require 'sinatra' require 'twiliolib' require 'time' @@HOME = "81xxxxxxxxxx" #This is not actually my phone number. @@CELL = "81xxxxxxxxxxxx" #Neither is this. def pretty_time ( time ) time . strftime ( "%H:%M %p" ) end def time_in_japan () time = Time . now . utc time_in_japan = time + 9 * 3600 end def is_weekend? ( time ) ( time . wday == 0 ) || ( time . wday == 6 ) end def in_range? ( t , str ) time = Time . parse ( " #{ Date . today . to_s } #{ pretty_time ( t ) } " ) range_bounds = str . split ( /, */ ) start_time = Time . parse ( " #{ Date . today . to_s } #{ range_bounds [ 0 ] } " ) end_time = Time . parse ( " #{ Date . today . to_s } #{ range_bounds [ range_bounds . size - 1 ] } " ) ( time >= start_time ) && ( time "45" ) @r . append ( say ) @r . append ( call ) @r . respond end def redirect_twilio ( url ) @r = Twilio :: Response . new rd = Twilio :: Redirect . new ( "/ #{ url . sub ( /^\/*/ , "" ) } " ) @r . append ( rd ) @r . respond end post '/phone' do t = time = time_in_japan if ( is_weekend? ( time )) if in_range? ( time , "2:00 AM, 10:00 AM" ) redirect_twilio ( "wakeup" ) else forward_call ( @@CELL ) end else #Not a weekend. if in_range? ( time , "2:00 AM, 8:30 AM" ) redirect_twilio ( "wakeup" ) elsif in_range? ( time , "8:30 AM, 6:30 PM" ) redirect_twilio ( "working" ) elsif in_range? ( time , "6:30 PM, 9:00 PM" ) forward_call ( @@CELL ) else forward_call ( @@HOME ) end end end post '/wakeup' do if ( params [ :Digits ]. nil? || params [ :Digits ] == "" ) @r = Twilio :: Response . new say = Twilio :: Say . new ( "This is Patrick's computer secretary. He is asleep right now because it is #{ pretty_time ( time_in_japan ) } . If this is an emergency, hit any number to wake him up." ) g = Twilio :: Gather . new ( :numDigits => 10 ) g . append ( say ) @r . append ( g ) @r . respond else forward_call ( @@HOME , true ) end end post '/working' do if ( params [ :Digits ]. nil? || params [ :Digits ] == "" ) days_left = ( Date . parse ( "2010-04-01" ) - Date . today ). to_i @r = Twilio :: Response . new say = Twilio :: Say . new ( "This is Patrick's computer secretary. He is at work as it is #{ pretty_time ( time_in_japan ) } . Only #{ days_left } days left! If this is an emergency, hit any number call him at work." ) g = Twilio :: Gather . new ( :numDigits => 10 ) g . append ( say ) @r . append ( g ) @r . respond else forward_call ( @@CELL , true ) end end get '/' do 'Hello from Sinatra! What are you doing accessing this server anyway?' end

This script is a bit ugly but, hey, what do you want in ten minutes. (Memo to self: correct it after leaving my job.)

Sinatra Deployment On Ubuntu

A quick look around the Internet didn’t show any cookbook recipes for deploying Sinatra. I thought I’d write up what I’m using, which uses Apache reverse proxying to Sinatra. (Instructions included for Nginx as well.) It assumes you already have your webserver running and are familiar with basic Ruby usage and the Linux command line.

1) Install the daemons gem. We’re going to daemonize Sinatra so that it runs out of our console and starts and stops without our intervention, much like Apache does.

2) Create an /opt/pids/sinatra directory. (It seemed as good a place as any.) Let a non-privileged user write to that directory, for example by executing “sudo chown www-data /opt/pids/sinatra; sudo chmod 755 /opts/pids/sinatra”. Make a note of what non-privileged user you use. I am just reusing www-data because Apache has conveniently provided him for me and he is guaranteed to not to be able to screw up anything important if he is compromised.

2) Write a quick control script and put it in the same directory as your Sinatra app (called phone_sinatra.rb for the purposes of this demonstration). I threw these in /www/var/phone.example.com/ but you can put them anywhere. Make sure the scripts are readable, but not writable, by www-data. (sudo chmod 755 /www/var/phone.example.com/ will accomplish this: it makes only the owner able to write to it, but any user on the system — including www-data — can read from it.)

require 'rubygems' require 'daemons' pwd = Dir . pwd Daemons . run_proc ( 'phone_sinatra.rb' , { :dir_mode => :normal , :dir => "/opt/pids/sinatra}) do Dir.chdir(pwd) exec " ruby phone_sinatra . rb " end

3) (Optional) Add in a reverse proxy rule to Apache or Nginx to send requests to the subdomain of your choice to Sinatra instead. I ended up deploying this through Apache, so the rule is pretty quick:

ServerName phone . example . com ProxyPass / http :/ / phone . example . com : 4567 /

You could also do this on Nginx and it is similarly trivial.

server { listen 80 ; server_name phone . example . com ; proxy_pass http :/ / phone . example . com : 4567 / ; }

The main reason I do this is to not have to remember non-standard ports in my URLs. It also simplifies firewall management if you’re into that sort of thing.

4) Add a control script to /etc/init.d/sinatra so that we can start and stop Sinatra just like we do other services, like Apache.

#!/bin/bash # # Written by Patrick McKenzie, 2010. # I release this work unto the public domain. # # sinatra Startup script for Sinatra server. # description: Starts Sinatra as an unprivileged user. # sudo - u www - data ruby /var/ www / phone . example . com / control . rb $1 RETVAL = $? exit $RETVAL

5) Tell Ubuntu to start your daemon when the computer starts up and shut it off when the computer starts down: sudo update-rc.d sinatra defaults

6) Start the service manually for your first and only time: sudo /etc/init.d/sinatra start

There you have it: Sinatra is running the application you wrote, and it will start and stop with your Ubuntu server. If you were doing this for Twilio now you’d check your Twilio account settings to make sure it has the right URL set up for your phone number, and then try calling yourself. Preferably NOT from the phone you try to forward to.

All code in this blog post was written by Patrick McKenzie in early 2010. I release it unto the public domain. Feel free to use it as the basis for your own apps.

Twilio development makes me feel like a kid in a candy store — you can affect the real world through an API, how cool is that? I think next time I have a few hours to kill I’m going to make a similar secretary for my business. I don’t give folks my phone number because a) I live in Japan and b) they don’t pay me enough to do telephone support. However, quoting a telephone number on your website instantly says “There is a real business behind this!”

I think I’ll whip up a computer secretary for the business which handles the most common two support requests (“I didn’t get my Registration Key” and “I lost my password.”), and for anything else takes their message and emails it to me. That sort of thing costs megacorporations bazillions and can be whipped up these days by a single programmer on Saturday morning for under $5 a month in operating costs. Like I said, candy store.