Building a simple voicemail system with Twilio and ASP.NET MVC, Part 1

In an effort to give myself more things to write about I’m going to start writing about code I’ve written recently.

I’ve been interested in writing something using Twilio since it became available late last year. Twilio is a service that provides an API for building telephony applications. I’ve had a lot of ideas over the years for integrating phone-related features into applications and Twilio makes it really easy.

I used to run a softball league and the past few years I was using GrandCentral (now Google Voice) for a weather hotline. On days when the weather wasn’t conducive to playing softball the players and coaches would call into the hotline to see if we were going to play or not. I no longer run the league and didn’t want to donate my precious Google Voice number to the league so I started looking for an alternative. Most of the services out there are too expensive if you don’t use them a lot and there’s only a couple weeks a year when the league needs the hotline.

If you’re not familiar with how Twilio works be sure to read up on it. In short, when your number gets an incoming call, the Twilio service makes a request to a specified URL, to which your app responds with a set of commands in XML.

The hotline has a few simple requirements:

  • When a call is received, play a greeting. This can either be text-to-speech (if there’s no recorded greeting) or the currently recorded greeting.
  • After the greeting is played, allow callers to leave a message (coaches use this to report issues to the league) which is emailed to the League Director.
  • Allow the League Director to record a new greeting by calling the number and entering a secret PIN.

I created a new MVC site, removed all the default cruft and created a simple route (/{action}) to allow me to use the following URLs:

  • / (root) – Entry point for every incoming phone call.
  • /Greeting – Invoked when the PIN is entered and prompts caller to record new greeting
  • /RecordGreeting – Handles the completion of the recording. If a digit is pressed while recording a new greeting, it restarts recording. If a hang up is detected, it saves the URL of the greeting audio file to the settings file.
  • /RecordVoicemail – After a voicemail is left, this action method handles downloading it from the URL Twilio provides and emails the .mp3 recording to the League Director.

Because we’ll be returning a lot of XML, I created an XmlResult to take an XDocument and output it:

public class XmlResult : ActionResult
{
    private XDocument _doc;
 
    public XmlResult(XDocument doc) {
        _doc = doc;
    }
 
    public override void ExecuteResult(ControllerContext context) {
 
        context.HttpContext.Response.ContentType = "text/xml";
        _doc.Save(context.HttpContext.Response.Output);
 
    }
}

I also have a Settings class for managing the application settings:

public class Settings
{
    private static XDocument doc = 
                   XDocument.Load(HttpContext.Current.Server.MapPath("~/App_Data/settings.xml"));
 
    private static string Get(string key) {
        return doc.Descendants(key).FirstOrDefault().Value;
    }
 
    private static void Set(string key, string value) {
        doc.Root.SetElementValue(key, value);
        doc.Save(HttpContext.Current.Server.MapPath("~/App_Data/settings.xml"));
    }
 
    public static string GreetingUrl {
        get {
            return Get("greetingUrl");
        }
        set {
            Set("greetingUrl", value);
        }
    }
    public static string PIN {
        get {
            return Get("pin");
        }
    }
    public static string VoicemailEmailFromAddress {
        get {
            return Get("voicemailEmailFromAddress");
        }
    }
    public static string VoicemailEmailToAddress {
        get {
            return Get("voicemailEmailToAddress");
        }
    }
}

The settings file looks like so (obviously this isn’t the most secure solution in the world, but this is only an example):

<?xml version="1.0" encoding="utf-8" ?>
<settings>
    <greetingUrl></greetingUrl>
    <pin>1234</pin>
    <voicemailEmailToAddress>test@example.com</voicemailEmailToAddress>
    <voicemailEmailFromAddress>voicemail@example.com</voicemailEmailFromAddress>
</settings>

We’re ready to start implementing our action methods. I’ll start with the first one hit when a call is received.

public ActionResult Index(string CallGuid, string Caller, string CallStatus) {
    var doc = new XDocument();
    var response = new XElement("Response");
    var gather = Verb("Gather", "", new { action = ActionUrl.Greeting, 
                                          method = "POST", 
                                          finishOnKey = "#" });
 
    // say current greeting
    if (string.IsNullOrEmpty(Settings.GreetingUrl)) {
        gather.Add(Verb("Say", "Thank you for calling the league hot line. Please leave a message."));
    }
    else {
        gather.Add(Verb("Play", Settings.GreetingUrl));
    }
 
    response.Add(gather);
 
    response.Add(Verb("Record", "", new { maxLength = 120, 
                                          action = ActionUrl.RecordVoicemail, 
                                          method = "POST" }));
 
    doc.Add(response);
    return new XmlResult(doc);
}

This method builds the required Twilio response XML. The first thing we add is a Gather verb which listens for digits being pressed. When the # key is pressed, a POST request is sent off to /Greeting (generated using the ActionUrl helper class I wrote to make sure the complete URL is returned). To keep listening for input while the greeting is being played, we nest the Say or Play verb inside the Gather verb. Lastly a Record verb is added which lets the caller leave a message which is posted to /RecordVoicemail.

Twilio passes some standard parameters (like CallGuid, Caller and CallStatus) with every request. ASP.NET MVC makes it really easy to get these values using parameter binding.

I’ve written a helper method that makes it easy to generate XElements in the proper form. The only thing it doesn’t really support very well is nesting, but it’s easy enough to get around that and you could easily add params parameter to accept an array of child elements.

private XElement Verb(string verb, string value) {
    return Verb(verb, value, null);
}
 
private XElement Verb(string verb, string value, object paramObject) {
    var element = new XElement(verb, value);
    foreach (var item in paramObject.ToDictionary()) {
        element.Add(new XAttribute(item.Key, item.Value));
    }
 
    return element;
}

The last parameter lets you pass in an anonymous object to set attributes on the XElement, similar to ASP.NET MVC’s use of anonymous objects. ToDictionary() is an extension method which has been posted in numerous places.

Since this is getting long, I think I’ll stop here and do a Part 2 with the remaining method implementations.

Posted May 4th, 2009 9:06 PM
Read more posts about ASP.NET MVC, Programming, Tips.

View Comments
Link

  • jgid
    I am trying to post via the following to create outgoing calls. I keep getting an authentication error. Any tips on how you may have done this already?

    Function PostTo(Data, URL)
    Dim objXMLHTTP, xml
    On Error Resume Next
    Set xml = Server.CreateObject("MSXML2.ServerXMLHTTP.3.0")
    xml.Open "POST", URL, False
    xml.setRequestHeader "Content-Type", "application/x-www-form-urlencoded"
    xml.Send Data
    If xml.readyState <> 4 then
    xml.waitForResponse 10
    End If
    If Err.Number = 0 AND http.Status = 200 then
    PostTo = xml.responseText
    else
    PostTo = "Failed"
    End If
    Set xml = Nothing
    End Function


    My data I am posting looks like this:
    PostData = "AuthToken=myAuthTokenhere&Caller=5095555555&Called=5095555555&Url=http://www.myurl.com/call/index.php"
    rslt = PostTo(PostData,"https://api.twilio.com/2008-08-01/Accounts/AccountSID/Calls/")
  • jgid
    No problem. I am on my own as the Twilio guys are using PHP and rest. I'll figure it out and post here for others to find. Thanks.
  • I would post this question to http://www.twilio.com/forum/ as I'm not
    familiar with the MSXML2 semantics.
  • Lav
    Thank you! We plan on testing out your code this weekend. Twilio rocks!
  • Hi John,

    This is a great use for Twilio, congratulations and I'm looking forward to part 2 of your post. If you will drop me a line at danielle@twilio.com, I'd love to send you a Twilio t-shirt in thanks and talk about featuring your story as a guest post on our blog. Thanks!

    Cheers,
    Danielle Morrill
    Twilio Community Manager
blog comments powered by Disqus