Find Volunteer Opportunities using Twilio, VolunteerMatch and ASP.NET

December 17, 2015
Written by

Twilio_VM_API_project

It’s the holiday season. For many of us it’s a time we reflect on how fortunate we are in our own lives and also how we can use our skills to give back to others in our communities. There are many organizations who are dedicated to the service of others and getting involved with one can be an immensely rewarding experience.  But how do you know what organizations and opportunities are available in your local area?

VolunteerMatch is a website that makes it easy to match your skills to volunteer opportunities in your local area.  They also have an API which is something we at Twilio are super fond of. In the spirit of the holiday season we partnered with VolunteerMatch to build an app that makes it even easier to find local volunteer opportunities.

Give it a try right now by texting your zip code to (314)-282-8630.

In this post I’ll show you how we built this app using a simple ASP.NET MVC website, the VolunteerMatch API and Twilio SMS.  Of course if you want to just go grab the source and set up your own service you can do that by heading over to the Github repo, but I think it’s more fun to build it from scratch.  Let’s go!

Setup

To build your own volunteer opportunities search app you’ll need to gather a few tools before you start coding.  To start you will need:

You’ll also need:

  • Visual Studio in order create an ASP.NET MVC 5 application.  Here again, the free community edition should work just fine.
  • ngrok, which is my prefered tool for exposing the web server running on my local machine out to the internet via a public URL.

Once you’ve assembled all of the accounts and tools you can dive right into the code.  Start by creating an empty ASP.NET MVC project in Visual Studio and then adding a new controller named HomeController.

Next we’ll add two Nuget packages to the project, Newtonsoft.Json and Twilio.Mvc.  We’ll use these to parse the volunteer opportunity data we get back from the VolunteerMatch API and tell Twilio how to respond to incoming search request text messages.

Once the Nuget packages are installed, we’ll make a couple of changes to the controller.  Start by changing the controller’s base class to TwilioController.  Next, because eventually we’ll need to call awaitable methods on the HttpClient object, change the Index action method to be an async method.


namespace VolunteerMatch.Sample.Controllers
{
    public class HomeController : TwilioController
    {
        // GET: Home
        public async Task<ActionResult> Index(string To, string From, string Body)
        {
            var response = new TwilioResponse();
            response.Message("Hello World");
            return TwiML(response);
        }
}

Within the Index method create a new TwilioResponse object in the action method. For now use the Message method to generate the TwiML that tells Twilio to send an SMS response with the message ‘Hello World’ back to the sender of the incoming message.  We’ll change this later to return a set of volunteer opportunities.

Finally, add a set of model classes that correspond to the data we’ll get back from the VolunteerMatch API.

You can copy these classes from the GitHub repository or download them in this ZIP file.

We’ve now got the basic project setup and ready to try out.  Start up up ngrok and point it at your local web server.  To start ngrok on windows, grab the port assigned to your IIS Express website and use it with the following commands from a Command Prompt:

ngrok http [your_iisexpress_port] -host-header="localhost:[your_iisexpress_port]"

Once ngrok is started, copy the public URL is assigned to your connection:

Configure the Message Request URL of your Twilio Phone number with that URL.

Now send a text message from your phone to your Twilio phone number.  If everything is working correctly, you should get a text message response telling you “Hello World”.

Searching for Opportunity

With the basic application up and running we can connect it to the VolunteerMatch API so that we can find local volunteer opportunities.  The VolunteerMatch API is pretty straightforward.  It accepts GET requests containing two parameters:

  • action – the specific operation you want to perform.  There are two main actions in the API, searchOpportunities and searchOrganizations.
  • query – a set of parameters available for each individual action.  For example, the searchOpportunities action includes parameters that let you filter on location and keyword and also control the number of opportunities returned.

For our application we’ll use the searchOpportunities action and the location parameter to find volunteer opportunities that are close to us.

Create a new class named Match.  To that class add:

  • a string property named Action that returns searchOpportunities, the name of the API action we want to perform.
  • a single method named Find that accepts a string of the location filter and returns an OpportunityResult:

private string Action { get { return "searchOpportunities"; } }

public async Task<OpportunityResult> Find(string location)
{
        
}

Next, we need to construct the query parameter so we can constrain the search based on a location.  The VolunteerMatch API expects the query parameter to be a JSON object containing parameters and values.  Because in this case the JSON object we need is pretty simple I’ll just construct it by hand and use the new string interpolation feature of C# 6, substituting into that string the location method parameter:


private string Action { get { return "searchOpportunities"; } }

public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";
}

Now we’re ready to set up the HttpClient object that will make the HTTP request to the VolunteerMatch API.  Create a new instance of HttpClient and set the Content-Type header to application/json.


public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";

    var client = new HttpClient();
    client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
}

Next, create an instance of the HttpRequestMessage class and set the HTTP method to GET and the Uri to the VolunteerMatch API URL.  Make sure you include the action and query parameters in the querystring:


public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";

    var client = new HttpClient();
    client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

    HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, $"http://www.volunteermatch.org/api/call?action={this.Action}&query={query}");
}

Next, send the request to the API, read its response as a string, attempt to deserialize the response into an OpportunityResult type and return that from the Find method:


public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";

    var client = new HttpClient();
    client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

    HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, $"http://www.volunteermatch.org/api/call?action={this.Action}&query={query}");
    var response = await client.SendAsync(requestMessage);
    var content = await response.Content.ReadAsStringAsync();

    var result = JsonConvert.DeserializeObject<OpportunityResult>(content);

    return result;
}

Now that we have a class that can get volunteer opportunities from the VolunteerMatch API we’ll use that class in our controller’s action method and call its Find method.  We’ll pass the content of the incoming text message, which should be a location like a city or zip code, into the Find method.  When we get a response from VolunteerMatch we’ll use that to respond back to the inbound message with the found opportunities:


// GET: Home
public async Task<ActionResult> Index(string To, string From, string Body)
{
    var response = new TwilioResponse();

    var match = new Match();
    var opportunityResult = await match.Find(Body);

    var builder = new StringBuilder();
    foreach(var opportunity in opportunityResult.Opportunities) 
    {
        builder.Append($"{opportunity.Title}rn");
    }

    response.Message(builder.ToString());
    return TwiML(response);
}

Start the web application.  Make sure ngrok is still running and send a text message to your phone number.

Boom!  Wait….nothing happens.  Why?  Oh yeah, we didn’t include any authentication information in our request with the VolunteerMatch API, so it failed.  Let’s modify the request to include the necessary authentication.

WSSE Authentication

The VolunteerMatch API uses a type of authentication typically referred to as WSSE.  This authentication style requires that we create a token based on several computed values and include that token in a custom HTTP header.  Let’s walk through creating that token and adding the header.

Start by adding some configuration properties to our web.config file:

<appSettings>
  <add key="vmUsername" value="YOUR_VOLUNTEERMATCH_API_USERNAME"/>
  <add key="vmApiKey" value="YOUR_VOLUNTEERMATCH_API_KEY"/>
  <add key="vmDateFormat" value="yyyy-MM-dd'T'HH:mm:sszz00"/>
</appSettings>

The Username and ApiKey values you should have received when you signed up for the VolunteerMatch API.  The DateFormat value is a format string that we’ll use to send the API a DateTime object in the format it expects.

Back in the Match class add three String properties and a DateTime property:

private string Username { get; set; }
private string ApiKey { get; set; }
private string DateFormat { get; set; }
private DateTime Created { get; set; }

Create a constructor that loads the Username, ApiKey and DateFormat values from the web.config file:

public Match()
{
    this.Username = ConfigurationManager.AppSettings["vmUsername"];
    this.ApiKey = ConfigurationManager.AppSettings["vmApiKey"];
    this.DateFormat = ConfigurationManager.AppSettings["vmDateFormat"];
}

Awesome! Now let’s modify the HttpClient to have it include an Authentication header:


public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";

    var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("WSSE", "profile="UsernameToken"");
    client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

    HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, $"http://www.volunteermatch.org/api/call?action={this.Action}&query={query}");
    var response = await client.SendAsync(requestMessage);
    var content = await response.Content.ReadAsStringAsync();

    var result = JsonConvert.DeserializeObject<OpportunityResult>(content);

    return result;
}

Finally, we need to add the custom X-WSSE header and populate it with the UsernameToken.  To add a custom header we’ll use the existing HttpRequestMessage instance.

As we saw earlier the UsernameToken is a concatenated string containing the VolunteerMatch API Username, a Password Digest, Nonce and Datetime value.  We already have the Username from the web.config file, so we just need to write the code that will create the datetime, nonce and digest values.

In the Match class add a new property named Nonce that generates the nonce value and returns a string:

string _nonce;
private string Nonce
{
    get
    {
        if (String.IsNullOrWhiteSpace(this._nonce))
        {
            RNGCryptoServiceProvider provider = new RNGCryptoServiceProvider();
            byte[] bytes = new byte[20];
            provider.GetBytes(bytes);
            this._nonce = System.Convert.ToBase64String(bytes);
        }

        return this._nonce;
    }
}

The property uses a cryptographic random number generator to create a Base64 encoded string of cryptographically strong random numbers.

Next add a property that returns the string of our PasswordDigest. This property concatenates together the Nonce, the formatted DateTime returned by the Created property and the API key.  It hashes that string and then Base64 encodes the hash.

private string PasswordDigest
{
    get
    {
        string passwordDigest = $"{this.Nonce}{this.Created.ToString(this.DateFormat)}{this.ApiKey}";

        byte[] intermediateBytes = Encoding.UTF8.GetBytes(passwordDigest);
        byte[] hash = SHA256.Create().ComputeHash(intermediateBytes);
        return Convert.ToBase64String(hash, Base64FormattingOptions.None);
    }
}

Now let’s use the Nonce and PasswordDigest properties, plus the Created date to populate the X-WSSE header.


public async Task<OpportunityResult> Find(string location)
{
    var query = $"{{\"location\":\"{location}\",\"numberOfResults\":3}}";

    this.Created = DateTime.Now;

    var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("WSSE", "profile="UsernameToken"");
    client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

    HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, $"http://www.volunteermatch.org/api/call?action={this.Action}&query={query}");

    string wsse = $"UsernameToken Username="{this.Username}",PasswordDigest="{this.PasswordDigest}",Nonce="{this.Nonce}",Created="{this.Created.ToString(this.DateFormat)}"";
    requestMessage.Headers.Add("X-WSSE", wsse);

    var response = await client.SendAsync(requestMessage);
    var content = await response.Content.ReadAsStringAsync();

    var result = JsonConvert.DeserializeObject<OpportunityResult>(content);

    return result;
}

With the authentication code added, try running the website again and sending a text message to your Twilio number.  This time the request to the VolunteerMatch API should succeed and you should receive back a text message containing a list of opportunities in the location you specified.

Wrapping it Up

Volunteering your time, the most valuable asset you have, can be an immensely fun and rewarding gift to give this holiday season or really any time of the year.  While most of us don’t have to travel far to make a real difference in our community, what can be a challenge is knowing what opportunities are actually available.

VolunteerMatch is a great resource to help you match your skills with local volunteer opportunities. Combining their data with Twilio SMS makes for an easy and accessible way to you to find the opportunity that is right for you.

The app we built in this post is pretty simple and there are multiple ways you could use the VolunteerMatch API to extend it including:

  • adding a way to page through results,
  • enhancing the location search beyond ZIP code,
  • or allowing a user to get more details about an individual volunteer opportunity via text message.

Let me know how you’re giving back this season.  Hit me up on twitter or in the comments below.