Posted on by No comments yet

LinkedInGoogle+TwitterFacebook

Tropo includes an “ask” method, which allows you to ask a question and wait for a response.  On a voice call, the user is actually on the phone and typically responds quickly; with SMS, they might receive your text with the question but wait to respond until they have the free time.  If your ask has a timeout of 30 seconds, and your user responds in a couple hours, how do you manage it? The absolute maximum timeout you can set is 2 hours – what if they respond the next day?

The best way to work around this limitation is by storing and reading the progress of an SMS conversation in a database, effectively eliminating any timeout concerns. This blog will discuss – and show – how to start, save and work with CouchDB to accomplish this. For the sake of simplity in this sample code, we’re getting back all the users. Realize that in a production system, you’ll probably want to tune this lookup so instead of getting back the couch documents for every user, you should search for the callerID and get only the documents for a particular user.

First things first, you need to create an instance of CouchDB by going to the following URL - iriscouch.com. Once there, hit the signup button and fill out the signup form (shown below).

couchDB signup form

couchDB signup form

When you hit the send button, you’ll be redirected to the confirmation page (shown below). Take note of the URL provided, as this is the location of your CouchDB instance.

couchDB confirmation page

couchDB confirmation page

To finish setting up CouchDB, click the link you received from the confirmation and then once there, click “Create Database” towards the top – you can name your database whatever you want; to follow the app’s design, I named mine sms.

Now that the database is created, you need to make a “New Document”. When you click “New Document”, there should only be one field in it – _id – which will be the name of the document. I named this document “currentUsers”; our SMS app will be looking for that name later, so keep the name in mind for later.

To add new fields, click “Add Field” – below is a list of all the fields you’ll need:

_id = "currentUsers"
people = {"users": {}, "total": "0"}
type = "SMS Database"

Once this is filled out, save it and your final product should look something like this (don’t worry about making the _rev field, it will generate automatically):

couchDB setup

couchDB setup

Your CouchDB instance is now set up and we can start working on the code for the Tropo application. The first thing to do, as always, is import the libraries that we need.

require 'rubygems'
require 'net/http'
require 'json'

The next bit of code is a class that was made for accessing CouchDB. This class just makes HTTP requests to update or retrieve information from our CouchDB instance.

module Couch
class Server
def initialize(host, port, options = nil)
@host = host
@port = port
@options = options
end
def delete(uri)
request(Net::HTTP::Delete.new(uri))
end
def get(uri)
request(Net::HTTP::Get.new(uri))
end
def put(uri, json)
req = Net::HTTP::Put.new(uri)
req["content-type"] = "application/json"
req.body = json
request(req)
end
def request(req)
res = Net::HTTP.start(@host, @port) { |http|http.request(req) }
unless res.kind_of?(Net::HTTPSuccess)
handle_error(req, res)
end
res
end
private
def handle_error(req, res)
e = RuntimeError.new("#{res.code}:#{res.message}nMETHOD:#{req.method}nURI:#{req.path}n#{res.body}")
raise e
end
end
end

Now that we have our class ready to go, we can start using it. This next method will use the class to retrieve all of the information in our database and parse it into JSON for use:

def getCounchDBData
url = URI.parse("http://sms.iriscouch.com/_utils")
server = Couch::Server.new(url.host, url.port)
res = server.get("/sms/currentUsers")
json = res.body
json = JSON.parse(json)
end

The next and final method will log the callerID, track the conversation and save any important information to our database. I have commented inline for better guidance when reading the code:

def updateCouchDBData(callerID, extra)

#Call the getCounchDBData method to get the database information
json = getCounchDBData
url = URI.parse("http://sms.iriscouch.com/_utils")
server = Couch::Server.new(url.host, url.port)
server.delete("/sms")
server.put("/sms", "")
sessions = json["people"]

i = 1
not_exit = true
not_found = true

while i <= sessions["total"].to_i && not_exit

if callerID == sessions["users"][i.to_s]["callerID"]

not_found = false
not_exit = false

#If the user sent an incorrect reply to an answer, set the convoNum back one and ask
#the question again
if extra == "back"
convoNum = sessions["users"][i.to_s]["convoNum"].to_i - 1
sessions["users"][i.to_s]["convoNum"] = (convoNum).to_s

else
#The number exists, increment the conversation number
if sessions["users"][i.to_s]["convoNum"].to_i < 3
convoNum = sessions["users"][i.to_s]["convoNum"].to_i + 1
sessions["users"][i.to_s]["convoNum"] = (convoNum).to_s

#This is the user's important message to save
elsif sessions["users"][i.to_s]["convoNum"].to_i == 3
convoNum = sessions["users"][i.to_s]["convoNum"].to_i + 1
sessions["users"][i.to_s]["convoNum"] = (convoNum).to_s
sessions["users"][i.to_s]["Final Message"] = "#{extra}"

#User has already gave their opinion, their last message will be always be the same
else
convoNum = 5
end
end
end
i += 1
end

#Number does not exist, create it.
if not_found
convoNum = 0
sessions["total"] = (sessions["total"].to_i + 1).to_s
sessions["users"]["#{sessions["total"]}"] = {"callerID"=>"#{callerID}", "convoNum"=>"0"}
end

#Get JSON ready
doc = <<-JSON
{"type":"SMS Database","people": #{sessions.to_json}}
JSON

#send the JSON back to the database
server.put("/sms/currentUsers", doc.strip)

return convoNum
end

Now that the helper methods and CouchDB class are set up, we can add the rest of the code that implements it. The next bit covers the message responses – the app implements a low level of interaction by prompting the user to respond with a specific answer. That’s handled three ways in the code – the first way prompts the user to enter a digit for the corresponding answer, the second prompts the user for a specific word, and the third asks for a short response of their choosing, which will be saved in the database as “Final Message”.

The messages are setup in an array, with each index of the array associated with a number. For example, in the messages listed below, the first is:

“Hello Tropo developer! Enter 1 if you love Tropo, 2 if you think it’s peachy keen or 3 if you think this is the easiest API ever created.”

That corresponds to index “0″, which means no inbound messages have been received from that user, so that’s the message that will be sent as the initial outbound message that starts the conversation. Each of the subsequent indexes is associated with the number of replies. In those following messages, there’s independent responses that hinge on what the user sent back – if they reply back with “1″, the app will look at the second index (which corresponds to 1 reply back) and apply their response to determine which of the options to send back. Same applies for the next index, where “scripting” or “webapi” as responses will trigger different replies.

messages = [
{"1"=>"Hello Tropo developer!",
"message"=>"Enter 1 if you love Tropo, 2 if you think it's peachy keen or 3 if you think this is the easiest API ever created."
},

{"1" => "We love you, too.",
"2"=>"Only thing peachier is Grandma's cobbler.",
"3" => "We're totally blushing over here right now.",
"message"=>"Reply back with scripting or webapi to see a short description of each."
},

{"scripting"=>"Tropo Scripting is a simple, yet powerful, synchronous API that lets you build communications applications, hosted on servers in the Tropo cloud.",
"webapi"=>"The Tropo Web API is a web-service API that lets you build communications applications that run on your servers and drive the Tropo cloud using JSON.",
"message"=>"Reply back with 1 if you want to learn how to sign up, or 2 if you're already signed up."
},

{"1"=>"Head over to following URL to sign up: https://www.tropo.com/account/register.jsp ",
"2"=>"Woo hoo! Awesome having you as a Tropo developer.",
"message"=>"If you have a second, let us know why you chose Tropo."
},

"Thank you for your response and interest!",

"Any further questions or comments, shoot em on over to support@tropo.com."
]

If the user replies to a question with an answer that doesn’t match the predefined answers, the app will say that it was an invalid choice and ask the question again. When doing this, the app logs back into couchDB and resets the “convoNum” to the previous question. The app will do this indefinitely until a correct answer is received. Also, after the app receives the “Final Message” from the user, the user will receive the same message from the app if another SMS is sent, which in this case would be - ”Any further questions or comments, shoot em on over to support@tropo.com.”.

There will be two ways this app will be initiated -

1) Initiated by a REST call

-This calls the number that will start the SMS conversation.

2) Initiated by an incoming SMS

-This receives an SMS and sends the next message.

Below is an if and else statement that deciphers both to determine if it should calculate the conversation number and send a message or start the call. I have commented inline so you can better understand what is going on:

if $currentCall

#This variable will correspond to which message should be played
$status = updateCouchDBData($currentCall.callerID, $currentCall.initialText)

#This variable will use the users response to give the appropriate answer
#Also note, I downcased the initial text so capitalization won't matter
$reply = $currentCall.initialText.downcase

#These two responses only have an answer, not an answer and question
if $status == 4 || $status == 5
say "#{messages[$status.to_i]}"

#This status needs to be broken up because of length
elsif $status == 2

#If the user responds with an answer that does not correspond to my answers,
#It will ask the question again
if messages[$status.to_i][$reply] == nil
$newStatus = updateCouchDBData($currentCall.callerID, "back")
say "Sorry, you have entered a wrong choice. #{messages[$newStatus.to_i]['message']}"

else
say "#{messages[$status.to_i][$reply]}"
say "#{messages[$status.to_i]['message']}"
end

#The rest of the questions and answers are short enough to have in one say
else

#If the user responds with an answer that does not correspond to my answers,
#It will ask the question again
if messages[$status.to_i][$reply] == nil
$newStatus = updateCouchDBData($currentCall.callerID, "back")
say "Sorry, I didn't understand your choice. #{messages[$newStatus.to_i]['message']}"

else
say "#{messages[$status.to_i][$reply]} #{messages[$status.to_i]['message']}"
end
end

#There is no reason to keep the session alive, so we hangup
hangup

else

#Grab the $numToDial parameter and initiate the SMS conversation
call($numToDial, {
:network => "SMS"})

#This primarily updates the database with the new number. This variable should always be 0
status = updateCouchDBData($numToDial, nil)

#This sends the initial messsage with the first question
say "#{messages[$status.to_i]['1']} #{messages[$status.to_i]['message']}"

#There is no reason to keep the session alive, so we hangup
hangup
end

That will finish up the code. Once you start using this app, your CouchDB will start to look like this:

Working couchDB data

Working couchDB data

Thank you for reading this blog and I hope it helps! You can find the complete app on my Github, feel free to use it and abuse it as you see fit.

Leave a Reply

  • (will not be published)