Wednesday, May 22, 2013

When Called Upon

As a member of Toastmasters International and my club, I promise:
...
To serve my club as an officer when called upon to do so
...

It's that time of year again! Time to figure out who will be the club officers for the next term! Will you be one?

Here are some of the reasons I have heard from Toastmasters members for not taking on a club officer role:
  • I don't have the time.
  • I'm not ready.
  • I don't think I will do a good job.
  • That's too much responsibility.
  • I don't know how to do it.
In essence, all of these reasons boil down to: "I am afraid to fail." I believe this indicates a deeper misunderstanding of what being a club officer actually means.

Remember what Toastmasters is about! From the club mission (I prefer the old wording, but the new wording says the same thing):

We provide a supportive and positive learning experience in which members are empowered to develop communication and leadership skills, resulting in greater self-confidence and personal growth.

The key phrases I want to point out are "supportive and positive learning experience" and "empowered to develop ... leadership skills".

I certainly do not expect anybody to be a world-class public speaker when giving the first Icebreaker speech. As a Toastmaster, I note the skills the speaker already has and suggest what skills can be developed and ways to develop them. I encourage everyone to get up and speak again, even to give the same speech again. As Toastmasters, we are all familiar with this, and that is what we expect when it comes our own turn to speak.

Here is the misunderstanding from all of those reasons given above:
The underlying assumption is that one must already have the leadership skills BEFORE taking the office!

That is the same as expecting every Icebreaker to be a world championship speech!

I do not expect ANY officer to do a perfect job. I don't even expect an officer to do a good job. I just expect them to be willing to do the best job they can. As a Toastmaster, I am more than willing to help any officer with their duties. I am happy to provide feedback as well, even to suggest how to develop the skills they feel they are lacking.

As an aside, what I love about the High Performance Leadership program: the guidance committee provides feedback about my leadership skills. I believe every club officer should have similar, formal, regular feedback. That is something I intend to try doing at my clubs.

At times, I have heard a member express discontent with how a particular member is performing their office duties. My question to that member is this: "What have you done to help that member succeed?"

I am not saying to take every office offered to you. I have turned down offices before, but not for fear of failure. I have also taken on certain offices because I felt I was lacking the skills necessary for that office, and I intended to use that office as an opportunity to develop those skills. After all, training is provided, you can't be fired, and everyone wants you to succeed.

So when a member says to you that they are not ready to hold an office, simply respond with "That is exactly why you are!"

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.")
}