Timeless SMS – Using CouchDB to Track Two Way SMS Conversations

March 28th, 2012 by kbond

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.

Related posts:

  1. SMS Voting App in 10 Minutes with Tropo and CouchDB
  2. No More Coworking Lockouts with Tropo and CouchDB
  3. Audio of Tropo.com launch at eComm now on IT Conversations
  4. Cloud Awesomeness with Tropo and CouchDB

Tags: ,

Leave a Reply

Please note: By submitting a comment you agree to comply with our Comment Policy. We welcome all comments, positive or negative, but do reserve the right to remove all or part of blog comments that do not comply with our policy.

Additionally, the first time you leave a comment on this blog, it will be held for moderation. After that first comment has been approved, future comments will be posted without delay.

Additional comments powered by BackType