12 Hacks of Christmas – Day 8: Christmas Carol Lyrics by Text Message

December 19, 2014
Written by

day8

My wife and I took our newborn daughter out shopping on Michigan Ave last weekend. Walking out of Macy’s, I saw a bunch of carolers holding sheets of paper with song lyrics. I thought, “Who has printers and copiers anymore?”

But most everyone’s got a cellphone these days. Maybe they don’t want to install an app. Maybe they don’t have a data plan. But most folks these days can get a text.

And so I spent last night building the Christmas Carol Lyric Line. If you’d like to see how it works, text “carols” to 907.312.1412.

lyrics-menu-new

I wrote this app using Ruby and Sinatra. It’s quite straight forward and below I’ll show you how I did it. Before we get to that though, I want to get something off my chest.

The finished code I’m about to walk you through is fairly clean and modular, but as you can see from the Github history, it didn’t start off that way. Lots of stuff hardcoded, lots of long methods and messy code. It feels a bit disingenuous to write a post saying, “Here’s how I would build this thing” when, in reality, they way I would build this thing is to throw together a bunch of ugly code until I got it working, then refactor to make it easier to maintain, easier to extend and easier to talk about.

With that said…

The idea was that someone could text in, get a menu of songs, reply back with their menu selection and get the lyrics for that song.

I decided to build a Sinatra app. I didn’t do a lot of Sinatra before starting at Twilio but it turns out Sinatra’s pretty useful for building quick Twilio apps, since it’s so stripped down — you can often fit all your code in a single file.

Well, almost one file. In my new project directory, I did create a Gemfile which included:

source 'https://rubygems.org'
ruby '2.1.2'
gem 'rack'
gem 'sinatra'

Then I created twilo.rb where the rest of the code in this post goes. I started it off by requiring Sinatra:

require ‘sinatra’

Getting the Lyrics

Next step was to get some song lyrics. That was pretty easy as there are 3.5MM results on Google for “Christmas carol lyrics.” I created a lyrics directory and dropped the songs in there, one text file per song.

On my first iteration I hardcoded the song menu. But then I wanted to add some songs and found it to be a pain in the ass to change the hardcoded menu every tine. So, I figured why not just automatically create the menu based off of the files in directory?

I renamed the files in such a way that it’d be easy to extract a pretty name — e.g, Away-In-A-Manger.txt instead of away-in-a-manger.txt. Then I wrote a method to do the transformation:

def song_name(filename)
  filename.gsub(/\-/, ' ').gsub('.txt', '')
end

Then I needed a method that would get all the filenames in the lyrics directory, delete the system filenames that start with a period, and sort the rest in alphabetical order:

def filenames
  names = Dir.entries('lyrics')
  names.delete_if { |name| name[0] == '.' }
  names.sort
end

Then I used those two methods to create the text for a menu:

def menu_text
  string = "Welcome to the Christmas Carol Lyric Line! What song number would you like lyrics for?"
  filenames.each_with_index do |filename, i| 
    string << "\n#{i + 1} #{song_name(filename)}"
  end
  string
end

Once I had my menu to display when someone texted in, I needed to deal with their input. First step is to figure out the list of valid menu options (‘1’, ‘2’, etc.):

def valid_options
  (1..filenames.size).collect { |i| i.to_s }
end

Then I needed to convert a valid menu option into a filename. For instance, the user replies with ‘1’ and I return the first filename from the directory array. You’ll notice that I subtracted one from whatever the user texted — muggles don’t do zero indexing:

def filename(input)
  filenames[input.to_i - 1]
end

Once I had a filename, I needed to pull the lyrics:

def lyrics(filename)
  File.read("lyrics/#{filename}")
end

Alright! So now I have my output for each state: the user texted in a valid menu option (I return the lyrics) or the user texts in something else (I return the menu). Now I just needed to deal with the whole texting thing.

Texting the North Pole

First I did a quick Google search to find the area code for the North Pole. Very important.

North Pole area code
North Pole area code

Then I opened up my Twilio dashboard and searched for a number in the 907 area code (Fun fact, the North Pole and Anchorage share area codes):

phone-numbers

And wouldn’t you know it, not only do we have North Pole area codes but we’ve got North Pole area code numbers that include 312 — the area code for Chicago! It really is Christmas.

I snagged that third phone number as it seemed easiest to remember. Then I set the messaging webhook to http://baugues.ngrok.com/messaging. (If you haven’t used ngrok before, you should. It lets you expose your local development server to the Internet. Makes development cycles a lot faster. Kevin Whinnery wrote about it here.)

When someone texts my Twilio number, Twilio’s makes a POST request to that URL and passes along information in the params, just like if someone submitted a form. The bit that I’m interested in, the message body, is found in params[‘Body’].

I wrote a route to handle the POST request:

post '/message' do
  if valid_options.include?(params['Body'])
    message = lyrics(filename(params['Body']))
  else
    message = menu_text
  end

  content_type 'text/xml'
  twiml(message)
end

The if statement checks if the body of the incoming text is included in the list of valid menu options. If it is, then we use the lyrics from the filename pulled from the files array. If it’s not, then we generate the menu.

Then I needed to convert that message into the the kind of specialized XML Twilio expects called TwiML.

def twiml(message)
  %Q{
<Response>
  <Message>
    #{message}
    \n--\nPowered by Twilio.com
  </Message>
</Response> }
end

And that’s it. I fired up Sinatra app by running ruby twilio.rb from the terminal. To make sure things were working right, I tested my webhook with Postman:

postman

Then I sent off a text to my shiny new Twilio number, chose a song, and got my lyrics.

Putting a bow on it

Once I knew it was working, I needed to get it off my development machine. There are a hundred ways to deploy these days. Recently I’ve been deploying to my VPS on Digital Ocean using Dokku, which gives you Heroku-like deployments via Git. Here’s a great tutorial from Digital Ocean on using Ruby and Dokku together.

One thing you’ll read about in that tutorial is the necessity of a config.ru to tell your server what exactly it’s supposed to do with the repository you just gave it. In my case, I want it to include twilio.rb and start Sinatra. So I created a new config.ru and wrote:

require './twilio.rb'
run Sinatra::Application

Since I already had a git repo going, I just had to make sure everything was up to date, create a new Dokku instance and push it real good.

git remote add dokku dokku@apps.baugues.com:carols
git add -A
git commit -m ‘Deploying’
git push dokku master

Last step was to update the webhook in my Twilio dashboard to the production URL.

And that’s it!

I’m trying to get the word out about the Christmas Carol Lyric Line so that people know about it as they go out to spread holiday cheer this weekend. Would you be up for retweeting this tweet or simply sharing the phone number: 907.312.1412.

Merry Christmas,
Greg B.
gb@twilio.com