Adventures in Twilio and C#
One of the more interesting companies and services on our ProWorks radar these days is Twilio, a company that provides a simple but powerful REST API which allows developers to easily integrate telephone calls with custom business logic into their web applications. You could almost call it "Coolio" because it is pretty cool!
Try out our ProWorks Twilio demo now: http://twiliodemo.proworks.com
The basic idea of Twilio is that voice narration and business logic can be controlled by the developer the same way they might code any application and there are a number of ways the text can either be read back by the talking Twilio text to speech voices or via playback of your own pre-recorded audio.
Where things get even more interesting is the ability to gather user input (e.g., "Press 1 for option A," etc.) and how those inputs can trigger different branches of code logic, which is of course application specific and usually determined by the client's needs and or their processes. This and other great features are supported via TwilML (Twilio Markup Lanaguage) which is pure XML that expresses a variety of "verbs" or actions that can be carried out by the Twilio service.
For example, let's say I wanted to make sure that I've got a warm body on the other line - maybe my app needs to post a wake-up call or a reminder to someone. Here's how that might be expressed in TwilML:
<Response>
<Say voice="woman">Hi there. Just checking to see if you are awake.</Say>
- Create yourself a Twilio account. It's free and easy.
- Download the Twilio helper API of your choice.
- Browse through their samples and docs - there's a lot there to get you going quickly.
For this ProWorks/Twilio demo, I wrote a simple web app that utilized the Twilio REST API - Twilio provides a single TwilioRest.cs file for this on their site. To enable reuse in different projects, I ended up wrapping it into a single .NET assembly but you can obviously just include the TwilioRest.cs file in your own project if that works for you.
I created a simple landing page in ASP.NET that looks like this:
You can access our demo from this URL: http://twiliodemo.proworks.com
When the user clicks the "Call My Phone Now" link, our demo page will validate that you've entered the expected info, etc.
Now lets look at a bit of code - you'll want to put your Twilio account ID and authorization ticket in a safe place - the Web.config is perfect for this.
string twilioAcountSID = ConfigurationManager.AppSettings["twilioAcountSID"];
string twilioAuthToken = ConfigurationManager.AppSettings["twilioAuthToken"];
string twilioCallerID = ConfigurationManager.AppSettings["twilioCallerID"];
string twilioAPIVersion = ConfigurationManager.AppSettings["twilioAPIVersion"];
string demo1Url = ConfigurationManager.AppSettings["demo1Url"];
Once we have our application specific configuration values, including the url that the Twilio service will redirect to, we can instantiate the Twilio helper API. Again, I've wrapped it in our own assembly:
PWTwilioWrapper.Account account = new PWTwilioWrapper.Account(twilioAcountSID, twilioAuthToken);
I made a slight modification to their helper class (TwilioRest.cs) to utilize a Dictionary rather than the typical Hashtable - this was my own preference:
Dictionary<string, string> h = new Dictionary<string, string>();
And one of the "gotchas" was that since we're allowing users to enter free form text is that they can enter any stream of characters. So to be safe, the text is run through a custom "safe" filter that weeds out potentially harmful characters. This might not be necessary for a demo, but it's often the case when we work with client input:
string safeMessage = PWTwilioHelper.SafeTwilioMessageText(this.ctrlMessage.Text);
Next, we need to gather the input values that will be passed to and injected into our TwilML page. Note that Twilio seemed a bit fussy about encoding and white space, so the user entered text is Url encoded - yet another one of the "gotchas" that I had to work through when putting this demo together:
string urlargs = string.Format("?_message={0}&_voice={1}&_music={2}", HttpUtility.UrlEncode(safeMessage), this.ctrlVoicePreference.SelectedValue == "man" ? "man" : "woman", this.ctrlPlayMusic.Checked ? "1" : "0");
h.Add("Url", demo1Url + urlargs);
h.Add("From", twilioCallerID);
h.Add("To", this.ctrlPhoneNumber.Text);
With the Url, From, and To values packed up for Twilio, we can now invoke their service. Here's the next line:
string resultXML = account.request(String.Format("/{0}/Accounts/{1}/Calls", twilioAPIVersion, twilioAcountSID), "POST", h);
Note that you might want to put the above in its own try/catch block. If there's an error invoking the Twilio API, chances are good that you'll get the ASP.NET yellow screen of death. Oh, well. This is only a demo - next section:
Twilio will return XML back as the result. Parsing it is optional of course, but we want to see what Twilio returns back to us so here's one way to do this:
try
{
XDocument xDoc = XDocument.Parse(resultXML, LoadOptions.PreserveWhitespace);
string status = "Unknown";
string message = "None";
string code = "None";
string moreInfo = "None";
foreach (XElement element in xDoc.Descendants("Status"))
{
status = element.Value;
}
foreach (XElement element in xDoc.Descendants("Message"))
{
message = element.Value;
}
foreach (XElement element in xDoc.Descendants("Code"))
{
code = element.Value;
}
foreach (XElement element in xDoc.Descendants("MoreInfo"))
{
moreInfo = element.Value;
}
string msgDetails = string.Format("<br />STATUS: {0}<br />MESSAGE: {1}<br />CODE: {2}<br />MORE INFO: {3}", status, message, code, moreInfo);
this.ctrlFeedbackLabel.Text = string.Format("<h4>Results of the attempt to call {0}:</h4><p><i>Twilio response: {1}</i></p>", this.ctrlPhoneNumber.Text, msgDetails);
}
catch ( Exception x )
{
this.ctrlFeedbackLabel.Text = string.Format("<h4>Results of the attempt to call {0}:</h4><p><i>Sorry, there was an exception parsing the Twilio response. Details: {1}</i></p>", this.ctrlPhoneNumber.Text, x.Message);
}
So that's what I would call the "invocation" page - it bootstraps things up so that the Twilio service can redirect to the target Twilio XML (or TwilML) page. But we're writing a C# web app and we need to inject our custom logic into the XML, right? We need the ASP.NET page to act like an XML stream. So the approach we take is to simply create an ASPX page (demo1Script01.aspx) with no code behind and add in the appropriate "ContentType" value, which is "text/xml" . Keep in mind that this is the page that the Twilio service will be calling to invoke the script to make the phone call and "speak" the text that was passed to the caller and potentially capture any of their input to (optionally) re-direct to other pages and scripts, etc., etc. For this demo, we're keeping it somewhat simple (just a single TwilML script page), but still want to explore and illustrate some possible techniques for dealing with this handy text to speech technology.
The first line of our target ASP.NET page (sans any code-behind) that will get called by Twilio looks like this:
<%@ Page Language="C#" ContentType="text/xml" Title="Demo1" %><?xml version="1.0" encoding="utf-8" ?><%
Here's where we can write any C# we want. For this demo the first thing I wanted to tackle was how to "speak" a date and time. It's simple enough in C# to get the current date and time, but we have to do just a bit more work to make that date and time "speak" nicely (in English or any other language) - so here's a hunk of code that might make a nice helper function:
DateTime dt = DateTime.Now;
string dayOfWeek = String.Format("{0:dddd}", dt);
string month = String.Format("{0:MMMMM}", dt);
string day = String.Format("{0}", dt.Day);
string hour = String.Format("{0}", dt.Hour <= 12 ? dt.Hour : dt.Hour-12);
string minute = String.Format("{0:00}", dt.Minute);
string amPM = String.Format("{0}", dt.Hour >= 12 ? "p m" : "a m");
string[] args =
{
dayOfWeek,
month,
day,
hour,
minute,
amPM
};
// spaces represent pauses, e.g., "Thursday November 18 at 3 19 P M " (note no year)
string spokenDate = string.Format("{0} {1} {2} at {3} {4} {5}", args);
And note that I'm not dealing with the year portion of the date/time to voice - doing so would muddy up the waters for the listener - besides this is intended to be a real-time message and hopefully they know what year it is!
Next, we'll gather up the custom parameters that were passed to this URL and parse them to ensure we've got the correct values or at least some proper defaults:
string voicePref = Request["_voice"];
if (string.IsNullOrEmpty(voicePref))
{
voicePref = "woman";
}
string message = Request["_message"];
bool hasMessage = true;
if (string.IsNullOrEmpty(message))
{
hasMessage = false;
}
string rawUrl = Request.RawUrl;
string sMusicValue = Request["_music"];
bool playMusic = string.IsNullOrEmpty(sMusicValue) == false && (sMusicValue.Trim() == "1" || sMusicValue.ToLower().Trim() == "true" || sMusicValue.ToLower().Trim() == "yes");
%>
And this marks the end of the param gathering portion of the page. The params we've gathered can now be used, in part, to generate the TwilML the way we want it to play back over the phone to the person being called (and in the case of this demo, that's you!). So now we start the output of the TwilML, which is nothing more than pure XML with the appropriate TwilML tags. As you can see, we're injecting in the parameters in the appropriate places in the markup. I realize that this looks a lot like classic ASP and I confess I'm not satisfied with this. There are probably better approaches for sure (like having a TwilML C# library helper!) but I was neither able to fully test any that are out there nor write my own.
But what should be clear is that it's fairly easy to block out the TwilML and then find the appropriate ASP.NET injection points and/or logic flow areas (if/else) blocks. Here's the TwilML with the ASP.NET params and logic included:
<Response>
<Say voice="<%=voicePref %>">Thank you for trying out the Pro Works Twilio demo. The current date and time, Pacific Standard, is <%=spokenDate %></Say>
<% if (hasMessage)
{ %>
<Say voice="<%=voicePref %>">Here is the message text you wanted us to convey: </Say>
<Say voice="<%=voicePref %>"><%=message%></Say>
<% } %>
<% else {%>
<Say voice="<%=voicePref %>">You did not have any special message to convey. Hey, whatever!</Say>
<% } %>
<% if ( playMusic ) { %>
<Say voice="<%=voicePref %>">You are clearly a person of style and taste, so to conclude the demo, we invite you to sit back and enjoy the soothing tones of this jazzy music, written and performed by Rob Birdwell, a senior developer with Pro Works! Enjoy!</Say>
<Play>http://birdwellmusic.com/Data/Audio/Troubadour.mp3</Play>
<% } %>
<% else {%>
<Say voice="<%=voicePref %>">Ah-HA you do not seem to be a fan of music - or maybe you were just uncertain. That's okay. We will now assume that you have changed your mind and do want music; so sit back and enjoy the soothing tones of this jazzy music, written and performed by Rob Birdwell, a senior developer with Pro Works! Enjoy!</Say>
<Play>http://birdwellmusic.com/Data/Audio/Troubadour.mp3</Play>
<% } %>
</Response>
As you can see, we've incorporated some simple but non-trivial business logic into our little demo: the preference for voice type (man or woman); the ability to read aloud a custom message; and of course the ability to listen to some music (written by me, Rob Birdwell) - which, as you discover, is no option at all!
In summary, the Twilio service is powerful and definitely something ProWorks will be looking to integrate into our existing and new clients as they express their need for such capabilities.