Saturday, May 4, 2013

A Phone Number For All Members

When I worked at Voxeo, I was a developer on the Tropo platform. Tropo is a platform for implementing voice applications, the kind of applications that you get when you call banks or businesses and get an automated menu.

For my Toastmasters club, I realized how beneficial this could be in facilitating communication between members as well as with external parties. I compiled a Google Doc spreadsheet in the club account (via Google for Nonprofits) with the contact information of the members. Then I exported the spreadsheet to the web so that a Tropo program could access the information.

The primary reason for using the Google Doc was so that other officers could easily change the club information without having to modify the Tropo application. That part so far is working well.

The Tropo program I wrote reads in this information to discover the members' and officers' names and phone numbers. This allows the program to contact any member! Now all that is needed is to give the caller access.

Voice Calls

When a call is received, the program provides the caller with a simple set of choices:
  1. Say "info" to get club information.
  2. Say a member's name to be transferred to that member.
  3. Say a club office to be transferred to that officer.
  4. Say "message" to leave a voice message for the club officers.
The caller then simply speaks and Tropo attempts to match what was spoken to the various choices. If a member's name or a club office is matched, the application transfers the call to that member or officer -- all without disclosing members' contact information!

If the caller requests to leave a message, then the call is transferred to our club's Google Voice number which can take a message, save a recording in our club's Google Voice account, transcribe it, and email the club officers.

As a bonus, our Tropo number can also be called via Skype and SIP!

Text Messages

If a text message is sent to the Tropo number, then the program behaves very differently: it forwards the text message to all of the club officers' text-enabled phone numbers. If the text was sent by a club officer, then that office's initials are prepended to the text; otherwise the phone number sending the text is prepended. This allows everyone to know the origin of the original message since each will be receiving a text message from the club's Tropo number.

This is most effective for pressing communications such as reaching each other in the last hour or so before meeting time.

Check It Out!

The club I first implemented this for is the Central Florida Facilitators club in Altamonte Springs, Florida. The phone number is live and active, so if you do try it and attempt to reach a member or officer, please keep in mind that we are in the Eastern time zone in the United States! The club information and the voice mail are not a problem, though.

If you are interetsted, see the information on our club's contact page!

Future Plans

It is my hope that this program can be expanded at a later date to provide access to the club schedule and thereby easy access to contact members according to the role they are scheduled for.

Cost

Currently this Tropo application is running as a development application for which there are no charges, however I did have to deposit $10 to be granted permissions for the application to make outgoing calls and text messages. Tropo's development platform can be less stable than the production platform, but production quality is not a need. Since our traffic usage will be very low, we should not be pressured to move our application to production.

The Program

The script is written in Groovy, which I chose because it has the easiest access to the underlying Java platform. I know Java very well (see my other blog!), and I needed an easy way to access the Java APIs to request the Google Doc information. Below is the source code, with that particular URL obfuscated of course!

// Load member information from member spreadsheet in Google Docs
memberData = "https://docs.google.com/spreadsheet/pub?key=a1b2c3d4etc&single=true&gid=0&output=csv"
reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.net.URL(memberData).openStream()))
memberArrayIndex=0
headers = reader.readLine().split(",")
memberArray = []
while ((inline = reader.readLine()) != null)
{
    log("line " + (memberArrayIndex+1) + ": " + inline)
    values = inline.split(",")
    nextMember = [:]
    for (headerIndex = 0;
         headerIndex < values.length;
         ++headerIndex)
    {
        nextMember[headers[headerIndex]] = values[headerIndex]
    }
    memberArray.add(nextMember)
}

for (entry1 in memberArray)
{
    log("entry1: " + entry1)
}

// Process SMS
if (currentCall.channel == "text")
{
    for (entry in memberArray)
    {
        destTexts = []
        // Determine who will receive the forwarded message
        // and also abbreviation if sent from a member.
        if (entry["Text 1"] != null && entry["Text 1"] != "")
        {
            destTexts.add(entry["Text 1"])
            if (entry["Text 1"] == currentCall.callerID &&
                entry["Abb"] != null)
            {
                textAbb = entry["Abb"]
            }
        }
        if (entry["Text 2"] != null && entry["Text 2"] != "")
        {
            destTexts.add(entry["Text 2"])
            if (entry["Text 2"] == currentCall.callerID &&
                entry["Abb"] != null)
            {
                textAbb = entry["Abb"]
            }
        }
    }
    log("destTexts: " + destTexts)
    log("textAbb: " + textAbb)

    String toSend = null
    if (currentCall == null)
    {
        // This actually means we received a message via IM
        toSend = msg
    }
    else
    {
        if (textAbb == null)
        {
            toSend = currentCall.callerID + ":" + ask("", [ choices:"[ANY]" ]).value
        }
        else
        {
            toSend = textAbb + ":" + ask("", [ choices:"[ANY]" ]).value
        }
    }

    log("message is " + toSend)

    if (toSend != null)
    {
        for (dest in destTexts)
        {
            log("Send message to " + dest)
            message(toSend, [to:dest, channel:"TEXT", network:"SMS"])
        }
    }
}
else // VOICE channel
{
    // Assemble the choices available to the caller
    people = [:]
    choices = "info, message"
    for (entry in memberArray)
    {
        if (entry["Phonetic"] != null && entry["Phonetic"] != "")
        {
            people[entry["Phonetic"]] = entry
            choices = choices + ", " + entry["Phonetic"]
        }
        if (entry["Office"] != null && entry["Office"] != "")
        {
            people[entry["Office"]] = entry
            choices = choices + ", " + entry["Office"]
        }
    }

    log("people: " + people)
    log("choices: " + choices)

    done = false

    say("Hello, and thank you for calling Central Florida Facilitators Toastmasters Club nine nine five eight.")

    while (!done)
    {
        prompt = "For more information about our club, please say info. To reach a specific member, please say the member's name or the club office held. To leave a message for the club officers, please say message.";

        selection = ask(prompt, [
            choices: choices,
            attempts: 3,
            onBadChoice: { event -> say("I'm sorry, I could not match your request.") },
            mode: "speech",
            bargein: true,
            timeout: 10
            ])

        log("selection: " + selection)
        log("concept: " + selection.choice.concept)

        leaveMessage = true

        if (selection.name == "nomatch")
        {
            say("I'm sorry, but I could not match your request to an available option. If you leave a message, it will be provided to the club officers.")
        }
        else if (selection.choice.concept == "info")
        {
            say("Central Florida Facilitators meets every Wednesday evening from six thirty to eight thirty at the Hah spiss of the Comforter located at four eighty west central parkway in al ta mont springs Florida. Please park to the rear of the building and come to the employee entrance where we should have a doorbell or a member stationed to let you in. At the request of our host, please do not knock on the door or ask any hah spiss employees for assistance. If you need immediate assistance getting in, please request the president, the sergeant-at-arms, or one of the other officers from the main menu. Central Florida Facilitators is an open club, and guests are always welcome at any meeting without any charge or obligation. You can find out more about us online at central florida facilitators dot org.")
            leaveMessage = false
        }
        else if (people[selection.choice.concept] != null)
        {
            voiceNumbers = []
            person = people[selection.choice.concept]
            log("Matched person: " + person)
            if (person["Voice 1"] != null && person["Voice 1"] != "")
            {
                voiceNumbers.add("+" + person["Voice 1"])
            }
            if (person["Voice 2"] != null && person["Voice 2"] != "")
            {
                voiceNumbers.add("+" + person["Voice 2"])
            }
            if (person["Voice 3"] != null && person["Voice 3"] != "")
            {
                voiceNumbers.add("+" + person["Voice 3"])
            }

            log("voiceNumbers: " + voiceNumbers)

            if (!voiceNumbers.isEmpty())
            {
                verify = ask("I understood your selection to be " + selection.choice.concept + ". Is that correct?",
                [
                    choices: "yes, no",
                    attempts: 3,
                    onBadChoice: { event -> say("I'm sorry, I could not match your request.") },
                    mode: "speech",
                    bargein: true,
                    timeout: 10
                ])
               
                if (verify.choice.concept == "yes")
                {
                    say("Transfering you to " + selection.choice.concept)
                    log("Transfer to: " + voiceNumbers)
                    transfer(voiceNumbers as String[], [network:"PSTN", playvalue:"http://www.soundjay.com/phone/sounds/phone-calling-1.mp3"])
                    done = true
                    leaveMessage = false
                }
            }
            else
            {
                say("I do not have a way to reach that person at this time.")
                leaveMessage = false
            }
        }
        else if (selection.choice.concept != "message")
        {
            say("I'm sorry, but I have encountered technical difficulty. If you leave a message, it will be provided to the club officers.")
        }

        if (leaveMessage)
        {
            log("Transfering to Google Voice number")
            transfer("tel:+14074941233", [ network:"PSTN", playvalue:"http://www.soundjay.com/phone/sounds/phone-calling-1.mp3"])
            done = true
        }
    }

    say("Thank you for calling Central Florida Facilitators.")
}

No comments:

Post a Comment