Tropo WebAPI Development Guide Home  |  Frameset Home

  Tutorials  |  TOC  |  Behind the Scenes JSON - Volunteer Opportunities  

Ruby - Volunteer Opportunities Example


This example application is written in Ruby and utilizes our Ruby WebAPI Library and Sinatra. Typically, you would need all three of those installed on your web server, as well as Rubygems in order for this to work, but for this example we're working under the assumption you're using the service provider Heroku, which has all of that installed for you already.

If you want to view the entire code as one piece, you can find it on Github; a complete video outlining the creation and use of this example can also be found on the Tropo YouTube Channel.

To start, Tropo sends the initial session JSON to the Ruby script whenever someone calls, texts, or IMs your app (or launches it using an external event, such as a link on a website; the script then converts this to a hash using this line of code in the Ruby application (line 11 of Figure 5.0):


The session hash has a lot of information in it, like the network, the channel, the caller ID or end user's IM name, the SIP headers for voice calls and more. This particular application specifically gets network from v[:session][:to][:network] (line 20 of Figure 5.0) and channel from v[:session][:to][:channel] (line 21 of Figure 5.0), as well as the from information (line 19 of Figure 5.0). If you want to see all the info in the session object, you can add puts v.inspect under line 14 of Figure 5.0 to get the hash to output to the Heroku logs (which can be accessed by typing heroku logs at the command prompt):

%w(rubygems sinatra tropo-webapi-ruby open-uri json/pure helpers.rb).each{|lib| require lib}

# For the Web UI
set :views, File.dirname(__FILE__) + '/templates'
set :public, File.dirname(__FILE__) + '/public'
set :haml, { :format => :html5 }

# To manage the web session coookies
use Rack::Session::Pool

# Resource called by the Tropo WebAPI URL setting
post '/index.json' do
  # Fetches the HTTP Body (the session) of the POST and parse it into a native Ruby Hash object
  v = Tropo::Generator.parse request.env["rack.input"].read
  
  # Fetching certain variables from the resulting Ruby Hash of the session details
  # into Sinatra/HTTP sessions; this can then be used in the subsequent calls to the
  # Sinatra application
  session[:from] = v[:session][:from]
  session[:network] = v[:session][:to][:network]
  session[:channel] = v[:session][:to][:channel]

As seen in Figure 5.0, the application starts off at the index.json resource, which we defined in our Tropo URL (review the Quick Start for info on where to input URLs). The initial portion of the code contained in our Ruby script follows next; line 1 determines the required resources (rubygems sinatra tropo-webapi-ruby open-uri json/pure helpers.rb), which includes our Ruby WebAPI Library. Next you can see where index.json is defined (line 12 of Figure 5.0) and where we instruct the Ruby application to generate the hash (line 14 of Figure 5.0). Also note use Rack::Session::Pool (line 9 of Figure 5.0); this enables the initial web session to be stored using cookies, so elements of it can be used by multiple resources as the app progresses.

Next, in Figure 5.1 that follows, the app breaks down what to do if the original session is voice or text. We use an if statement (lines 5 through 19) to determine the channel by checking if initial_text is present; if it is, that means it was an IM, SMS or Tweet. If not, it's voice. Since the original session was over the voice channel, the application chooses a voice-oriented ask. The application then instructs Tropo to ask the caller to enter in their zip code (line 17), determines if it's a valid input, then moves on to process_zip.json next (line 24) - unless the user hangs up, at which point it instructs Tropo to move on to hangup.json (line 22):

# Create a Tropo::Generator object which is used to build the resulting JSON response
t = Tropo::Generator.new
    # If there is Initial Text available, we know this is an IM/SMS/Twitter session and 
    # not voice
    if v[:session][:initial_text]
      # Add an 'ask' WebAPI method to the JSON response with appropriate options
      t.ask :name => 'initial_text', :choices => { :value => "[ANY]"}
      # Set a session variable with the zip the user sent when they sent the IM/SMS/Twitter 
      # Request
      session[:zip] = v[:session][:initial_text]
    else
      # If this is a voice session, then add a voice-oriented ask to the JSON response
      # with the appropriate options
      t.ask :name => 'zip', :bargein => true, :timeout => 60, :attempts => 2,
          :say => [{:event => "timeout", :value => "Sorry, I did not hear anything."},
                   {:event => "nomatch:1 nomatch:2", :value => "Oops, that wasn't a five-digit zip code."},
                   {:value => "Please enter your zip code to search for volunteer opportunities in your area."}],
                    :choices => { :value => "[5 DIGITS]"}
    end      
    
    # Add a 'hangup' to the JSON response and set which resource to go to if a Hangup event occurs on Tropo
    t.on :event => 'hangup', :next => '/hangup.json'
    # Add an 'on' to the JSON response and set which resource to go when the 'ask' is done executing
    t.on :event => 'continue', :next => '/process_zip.json'
  
  # Return the JSON response via HTTP to Tropo
  t.response
end

Once Tropo has the zip code, it sends the result back to the application. The following section of the script (Figure 5.2) outlines what to do with it once it's received. The application takes the zip code (line 9 of Figure 5.2) - this particular caller entered the zip code 94070 - and uses it to search allforgood.org for the volunteer opportunities that match up (line 29).

# The next step in the session is posted to this resource when the 'ask' is completed in 'index.json'
post '/process_zip.json' do
  # Fetch the HTTP Body (the session) of the POST and parse it into a native Ruby Hash object
  v = Tropo::Generator.parse request.env["rack.input"].read
  
  # Create a Tropo::Generator object which is used to build the resulting JSON response
  t = Tropo::Generator.new
    # If no initial text was captured, use the zip in response to the ask in the previous route
    session[:zip] = v[:result][:actions][:zip][:value].gsub(" ","") unless session[:zip]
    # Construct and generate the params url. This is used to generate the JSON request, or in the case of Twitter, the URL to the website.
    params = {
      :num => "9",
      :output => "json",
      :vol_loc => session[:zip],
      :vol_startdate => Time.now.strftime("%Y-%m-%d"),
      :vol_enddate => (Time.now+604800).strftime("%Y-%m-%d")
      }      
    params_str = ""
    params.each{|key,value| params_str < "&#{key}=#{value}"}
    # If using Twitter, we'll just give them a URL to the website. We don't want to flood Twitter with all the details we give voice/IM users
    if session[:network] == "TWITTER"
      # Add a 'say' to the JSON response
      t.say "Volunteer opportunities in your area for the next 7 days: #{tinyurl("http://www.allforgood.org/search?"+params_str)}"
      # Add a 'hangup' to the JSON response
      t.hangup
    end
    # Fetch JSON output for the volunteer opportunities from our API provider, allforgood.org
    begin
      session[:data] = JSON.parse(open("http://www.allforgood.org/api/volopps?key=tropo"+params_str).read)
    rescue
      # Add a 'say' to the JSON response
      t.say "It looks like something went wrong with our volunteer data source. Please try again later."
      t.hangup
    end
    # List the opportunities to the user in the form of a question. The selected opportunity will be handled in the next route.
    if session[:data]["items"].size > 0
      # Add a 'say' to the JSON response
      t.say "Here are #{session[:data]["items"].size} opportunities. Press the opportunity number you want more information about."
      items_say = []
      session[:data]["items"].each_with_index{|item,i| items_say < "Opportunity ##{i+1} #{item["title"]}"}
      # Add an 'ask' to the JSON response
      t.ask :name => 'selection', :bargein => true, :timeout => 60, :attempts => 1,
          :say => [{:event => "nomatch:1", :value => "That wasn't a one-digit opportunity number. Here are your choices: "},
                   {:value => items_say.join(", ")}], :choices => { :value => "[1 DIGITS]"}
    else
      # Add a 'say' to the JSON response
      t.say "No volunteer opportunities found in that zip code. Please try again later."
    end
    
    # Add an 'on' to the JSON reponse and set which resource to go to when the 'ask' is done executing
    t.on  :event => 'continue', :next => '/process_selection.json'
    # Add a 'hangup' to the JSON reponse and set which resource to go to if a Hangup event occurs on Tropo
    t.on  :event => 'hangup', :next => '/hangup.json'
    
  # Return the JSON response via HTTP to Tropo
  t.response  
end

As before with voice and text, what the application will do is determined by how the request came through; for this one, it's determined by the channel, split between Twitter and Voice/Text. If the zip code came over via Twitter, the app instructs Tropo to just post a link to the available volunteer opportunities (lines 21 through 26 of Figure 5.2). If it's via voice or text, it instructs Tropo to list out the opportunities and provide the user with the option to select one of them for more details (lines 36 through 48). Since the zip code was 94070, the application returns the results from the search that match San Carlos, CA and tells the caller to select one of the opportunities for more information.

Our example caller selected opportunity number 2; Tropo sends that back to the application, which then redirects to the process_selection.json resource (defined by line 52 of Figure 5.2). This resource extracts more information about that particular opportunity from http://www.allforgood.org and sends it back to Tropo (lines 9 through 14 of Figure 5.3); it also instructs Tropo to ask if they would like the opportunity information to be sent via text message (lines 20 through 25):

# The next step in the session is posted to this resource when the 'ask' is completed in 'process_zip.json'
post '/process_selection.json' do
  # Fetch the HTTP Body (the session) of the POST and parse it into a native Ruby Hash object
  v = Tropo::Generator.parse request.env["rack.input"].read
  
  # Create a Tropo::Generator object which is used to build the resulting JSON response
  t = Tropo::Generator.new
    # If we have a valid response from the last ask, do this section
    if v[:result][:actions][:selection][:value]
      item = session[:data]["items"][v[:result][:actions][:selection][:value].to_i-1]
      session[:say_string] = "" # storing in a session variable to send it via text message later (if the user wants)
      session[:say_string] += "Information about opportunity #{item["title"]} is as follows: "      
      session[:say_string] += "Event Details: #{construct_details_string(item)} "
      session[:say_string] += "Description: #{item["description"]}. End of description. " unless item["description"].empty?       
      t.say session[:say_string]

      # Ask the user if they would like an SMS sent to them
      t.ask :name => 'send_sms', :bargein => true, :timeout => 60, :attempts => 1,
            :say => [{:event => "nomatch:1", :value => "That wasn't a valid answer. "},
                   {:value => "Would you like to have a text message sent to you?
                               Press 1 or say 'yes' to get a text message; Press 2 or say 'no' to conclude this session."}],
            :choices => { :value => "true(1,yes), false(2,no)"}
    else # No opportunity found
      t.say "No opportunity with that value. Please try again."
    end
    
    # Add an 'on' to the JSON response and set which resource to go to when the 'ask' is done executing
    t.on  :event => 'continue', :next => '/send_text_message.json'
    # Add a 'hangup' to the JSON response and set which resource to go to if a Hangup event occurs on Tropo
    t.on  :event => 'hangup', :next => '/hangup.json'
    
  # Return the JSON response via HTTP to Tropo
  t.response
end

Our example caller pressed 1 to say yes, they did want it texted, so the application shifts down to the send_text_message.json resource (defined by line 28 of Figure 5.3). This portion of the script tells Tropo to ask for a phone number so it can send the opportunity detail text message (line 19 of Figure 5.4); had the caller said no, they didn't want the text message, the application would instead immediately redirect to goodbye.json (line 25):

# The next step in the session is posted to this resource when the 'ask' is completed in 'process_selection.json'
post '/send_text_message.json' do
  # Fetch the HTTP Body (the session) of the POST and parse it into a native Ruby Hash object
  v = Tropo::Generator.parse request.env["rack.input"].read
  
  # Create a Tropo::Generator object which is used to build the resulting JSON response
  t = Tropo::Generator.new
    if v[:result][:actions][:number_to_text] # The caller provided a phone # to text message
      t.message({
        :to => v[:result][:actions][:number_to_text][:value],
        :network => "SMS",
        :say => {:value => session[:say_string]}})
      t.say "Message sent."
    else # We dont have a number, so either ask for it if they selected to send a text message, or send to goodbye.json
      if v[:result][:actions][:send_sms][:value] == "true"
        t.ask :name => 'number_to_text', :bargein => true, :timeout => 60, :required => false, :attempts => 2,
              :say => [{:event => "timeout", :value => "Sorry, I did not hear anything."},
                     {:event => "nomatch:1 nomatch:2", :value => "Oops, that wasn't a 10-digit number."},
                     {:value => "What 10-digit phone number would you like to send the information to?"}],
                      :choices => { :value => "[10 DIGITS]"}
        next_url = '/send_text_message.json'
      end # No need for an else, send them off to /goodbye.json
    end
    
    # Tell it to say goodbye if there is no next_url set above
    next_url = '/goodbye.json' if next_url.nil?
    # Add an 'on' to the JSON response and set which resource to go to when the 'ask' is done executing
    t.on  :event => 'continue', :next => next_url
    # Add a 'hangup' to the JSON response and set which resource to go to if a Hangup event occurs on Tropo
    t.on  :event => 'hangup', :next => '/hangup.json'
  
  # Return the JSON response via HTTP to Tropo
  t.response
end

Once received, Tropo sends the number back to application; this step is unique in comparison to the others, because it redirects back to the send_text_message.json resource again - that resource is built to either ask for the number or send the text message if a number has already been received.

After the text message has been sent, the application then redirects to goodbye.json (defined by line 26 of Figure 5.4). This resource is relatively simple; all it does is provide a goodbye message, slightly modified for voice or text channels:

# The next step in the session is posted to this resource when the 'ask' is completed in 'send_text_message.json'
post '/goodbye.json' do
  # Fetch the HTTP Body (the session) of the POST and parse it into a native Ruby Hash object
  v = Tropo::Generator.parse request.env["rack.input"].read
  
  # Create a Tropo::Generator object which is used to build the resulting JSON response
  t = Tropo::Generator.new
    if session[:channel] == "VOICE"
      t.say "That's all. Communication services donated by tropo dot com, data by all for good dot org. Have a nice day. Goodbye."
    else # For text users, we can give them a URL (most clients will make the links clickable)
      t.say "That's all. Communication services donated by http://Tropo.com; data by http://AllForGood.org"
    end 
    t.hangup
    
    # Add a 'hangup' to the JSON response and set which resource to go to if a Hangup event occurs on Tropo
    t.on  :event => 'hangup', :next => '/hangup.json'
  t.response
end

The application then redirects to hangup.json resource once the user disconnects. This resource logs the call duration to Heroku's running console "log" (which can be viewed by inputting "heroku logs" into your command prompt):

# The next step in the session is posted to this resource when any of the resources do a hangup
post '/hangup.json' do
  v = Tropo::Generator.parse request.env["rack.input"].read
  puts " Call complete (CDR received). Call duration: #{v[:result][:session_duration]} second(s)"
end

That's it - the full breakdown of a relatively complex WebAPI application, covering an extensive back and forth, request and response interaction with a web server. While we used Ruby and Sinatra for this example, virtually any modern web language and framework will work - Python with Django, PHP with Cake or Limonade, etc.


Next Step: PHP - Favorite Movie Trilogy Example




If you're interested, you can also view the complete JSON exchange by going to Behind the Scenes JSON section of the guide.



  ANNOTATIONS: EXISTING POSTS
0 posts - click the button below to add a note to this page

login
  Tutorials  |  TOC  |  Behind the Scenes JSON - Volunteer Opportunities  

© 2012 Voxeo Corporation  |  Voxeo IVR  |  VoiceXML & CCXML IVR Developer Site