Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2010) with:

Cell Phones, SMS, Twilio, Pusher, ColdFusion, And Google Maps == Fun

By Ben Nadel on

As of late, I've been playing around some very cool technologies like Pusher's HTML5 WebSocket server and Twilio's mobile SMS text messaging web services. Each of these technologies is exciting on its own; but, I wanted to see if I could create a fairly simple but fun way to start integrating these technologies into some sort of ColdFusion work flow. What I came up with was the idea of allowing people to post SMS text messages to a Google map. While this idea isn't hugely useful, it does create a small system that touches Twilio, ColdFusion, Pusher, and the Google Maps API.


 
 
 

 
Twilio, Pusher, ColdFusion, And Google Maps API Work Flow Video For Mobile Integration.  
 
 
 

The idea here is straight forward: the user opens up the following Google Map:

 
 
 
 
 
 
Twilio, Pusher, ColdFusion, And Google Maps Powered Work Flow. 
 
 
 

Then, the user sends an SMS text message to the given phone number. This phone number is a rented number in the Twilio service. The SMS text message gets delivered to Twilio. Twilio then takes the SMS text message and posts it via HTTP to the ColdFusion-powered SMS end point. The end point then looks at the form data to see if it can determine the user's address. If it cannot, it prompts the user (via an SMS response) for their information. Once the address is determined, the SMS end point posts the address information to Pusher's REST API. Pusher then "pushes" that information onto our HTML5 web client over the native WebSockets or a SWF-based fallback. The client then uses the Google Maps API to geocode and display the text message on the open map.

 
 
 
 
 
 
Twilio, Pusher, ColdFusion, And Google Maps Powered Work Flow. 
 
 
 

Once the phone / Twilio / ColdFusion / Pusher / Google Maps work flow has completed, the SMS text message appears in realtime in front of the user's eyes:

 
 
 
 
 
 
Twilio, Pusher, ColdFusion, And Google Maps Powered Work Flow With SMS Text Message Realtime Display. 
 
 
 

This might not be the most useful demo; but, I think it nicely illustrates how easily some of these 3rd party services can be integrated with ColdFusion. Of course, seeing the code will demonstrate this more than any video. The core of the demo lies in the Twilio SMS end point and the google maps page. Let's take at a look at the SMS end point first:

Twilio SMS End Point (Public ColdFusion URL)

  • <!--- Param the form variables. --->
  • <cfparam name="form.fromCity" type="string" default="" />
  • <cfparam name="form.fromState" type="string" default="" />
  • <cfparam name="form.fromZip" type="string" default="" />
  • <cfparam name="form.fromCountry" type="string" default="" />
  • <cfparam name="form.body" type="string" default="" />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • If Twilio was not able to give us any location information,
  • we'll have asked for clarification from the user. Let's see
  • if we have an outstanding request for the address.
  • --->
  • <cfif session.addressRequested>
  •  
  •  
  • <!---
  • Since we already asked the user for their location, we
  • know (hope) that this message is the user's manually
  • entered location. Store the current text message as
  • the address.
  •  
  • NOTE: We are adding a comma to the end here only to make
  • sure that the serializeJSON() call on a zip code doesn't
  • create a decimal number. Adding the comma does not adversely
  • affect the geocoding of the address.
  • --->
  • <cfset session.address = "#form.body#," />
  •  
  •  
  • <!---
  • Check to see if Twilio was able to determine the address of
  • the user based on their phone number.
  • --->
  • <cfelseif (
  • len( form.fromCity ) ||
  • len( form.fromState ) ||
  • len( form.fromZip ) ||
  • len( form.fromCountry )
  • )>
  •  
  •  
  • <!---
  • Twilio was able to determine the user's location based
  • on their phone number. As such, we wouldn't need to take
  • any more steps with this user.
  • --->
  •  
  • <!--- Store the text message. --->
  • <cfset session.message = form.body />
  •  
  • <!---
  • Build the location based on the message meta data. Even
  • if Twilio didn't fill out all of these values, it doesn't
  • seem to affect the geocoding very much and it allows us to
  • err on the side of more data.
  • --->
  • <cfset session.address = "#form.fromCity#, #form.fromState# #form.fromZip#, #form.fromCountry#" />
  •  
  •  
  • <!---
  • At this point, Twilio has not been able to determine the
  • location of the user from their phone number and we have
  • not asked for it manually yet.
  • --->
  • <cfelse>
  •  
  •  
  • <!---
  • Store the user's current message in their session for
  • the next request.
  • --->
  • <cfset session.message = form.body />
  •  
  • <!---
  • Flag that we are asking the user for their location
  • on the next request.
  • --->
  • <cfset session.addressRequested = true />
  •  
  • <!--- Set the response, asking the user for location. --->
  • <cfset response = "Sorry, your location could not be determined :( What is your zip code?" />
  •  
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • At this point, we may or may not have both the message and
  • the address from the user. We only want to act IF we have both.
  • Otherwise, we'll assume we are waiting on an action from the
  • user.
  • --->
  • <cfif (
  • len( session.message ) &&
  • len( session.address )
  • )>
  •  
  •  
  • <!---
  • Now that we have both the message and the address, let's
  • post the message data to the Pusher app so that it can be
  • pushed to the client and the map.
  •  
  • NOTE: We are using array notation when creating the struct
  • so that key-case will be retained when ColdFusion serializes
  • the struct into JSON.
  • --->
  • <cfset message = {} />
  • <cfset message[ "address" ] = session.address />
  • <cfset message[ "message" ] = session.message />
  •  
  • <!---
  • Now, we need to push the message. For some reason, I am
  • getting random 401 Unauthorized problems with the service.
  • As such, I'm going to try a few times if it fails.
  • --->
  • <cfloop
  • index="tryIndex"
  • from="1"
  • to="3"
  • step="1">
  •  
  • <!--- Push the message. --->
  • <cfset pushResponse = application.pusher.pushMessage(
  • "sms",
  • "textMessage",
  • message
  • ) />
  •  
  • <!---
  • <cfdump
  • var="#pushResponse#"
  • output="#expandPath( './log.htm' )#"
  • format="html" />
  • --->
  •  
  • <!--- Check to see if we should break out. --->
  • <cfif reFind( "20\d", pushResponse.statusCode )>
  •  
  • <!--- This request worked, break out of try loop. --->
  • <cfbreak />
  •  
  • </cfif>
  •  
  • <!---
  • Sleep very briefly to allow the time to change in case
  • there is something time-specific breaking this approach.
  • --->
  • <cfthread
  • action="sleep"
  • duration="500"
  • />
  •  
  • </cfloop>
  •  
  • <!---
  • Now that we have tried the Pusher app submission up to a
  • certain number of times, let's check to see if the Pusher
  • HTTP request failed.
  • --->
  • <cfif reFind( "20\d", pushResponse.statusCode )>
  •  
  • <!---
  • The pusher request was successful. This means the
  • message arrive at the client successfully.
  • --->
  • <cfset response = "Thanks! Check the map for your text message!" />
  •  
  • <cfelse>
  •  
  • <!---
  • The pusher request failed for some reason; let the user
  • know about the failure.
  • --->
  • <cfset response = "Sorry! I could not communicate with Pusher's HTML5 WebSockets. Please try again :)" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Regardless of what the pusher app has told us, we are going
  • to consider this pass as "successful" as possible. As such,
  • let's reset the session information.
  •  
  • To keep this code easier to maintain, just use the
  • onSessionStart() event handler to manage the session reset.
  • --->
  • <cfset createObject( "component", "Application" )
  • .onSessionStart()
  • />
  •  
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Convert the message into Twilio XML response. --->
  • <cfsavecontent variable="responseXml">
  • <cfoutput>
  •  
  • <?xml version="1.0" encoding="UTF-8"?>
  • <Response>
  • <Sms>#xmlFormat( response )#</Sms>
  • </Response>
  •  
  • </cfoutput>
  • </cfsavecontent>
  •  
  •  
  • <!---
  • Stream XML response to Twilio client. Make sure to TRIM
  • the XML response such that it is valid XML.
  • --->
  • <cfcontent
  • type="text/xml"
  • variable="#toBinary( toBase64( trim( responseXml ) ) )#"
  • />

At first, this code was actually much shorter. But then during testing, I discovered that not all mobile carriers report the addresses of their mobile customers. As such, I had to expand the code to prompt the user for their location if it could not be determined from Twilio's meta data. To do this, I had to keep track of the user's information across multiple SMS text messages. Fortunately, the Twilio Proxy service acts like a true web browser which allows us to use ColdFusion's native session management (the Application.cfc is shown a bit farther down).

Once both the SMS text message and the user's location have been established, the Twilio SMS end point posts the message information to Pusher's HTML5 WebSocket server. Unfortunately, I was having some trouble getting consistent authorization when posting to Pusher's REST web service. This is why, in the code, I am allowing the SMS text message to be posted and re-posted to Pusher's web service up to 3 times. I have submitted a help question on Pusher's support forum and will be working on debugging this issue with the Pusher team.

Once the message is successfully posted to the Pusher web service, Pusher "pushes" it over the "sms" WebSocket channel and triggers a "textMessage" event on the client (web browser). The client then geocodes the address and posts both a marker and an infoWindow on the visible map:

Google Map Page

  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html" />
  •  
  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>Twilio + Pusher + ColdFusion + Google Maps</title>
  • <link rel="stylesheet" type="text/css" href="./map.css"></link>
  • <script type="text/javascript" src="jquery-1.4.2.min.js"></script>
  • <script type="text/javascript" src="http://js.pusherapp.com/1.4/pusher.min.js"></script>
  • <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
  • <script type="text/javascript">
  •  
  • // This is for compatability with browsers that don't yet
  • // support Web Sockets, but DO support Flash.
  • //
  • // NOTE: This SWF can be downloaded from the PusherApp
  • // website. It is a Flash proxy to the standard Web
  • // Sockets interface.
  • WebSocket.__swfLocation = "./WebSocketMain.swf";
  •  
  •  
  • // When the DOM has loaded, initialize the map and scripts.
  • $(function(){
  •  
  • // Get a reference to the header.
  • var siteHeader = $( "#siteHeader" );
  •  
  • // Get a reference to the site map.
  • var siteMap = $( "#siteMap" );
  •  
  • // Get a reference to the intro message.
  • var introMessage = $( "#introMessage" );
  •  
  • // The google map object - this will be initialized once
  • // the page has loaded fully.
  • var map = null;
  •  
  • // Get an instance of the Google map geocoder object so
  • // that we can get the lat/long for addresses.
  • var geocoder = new google.maps.Geocoder();
  •  
  • // This is the visible window that displays the text
  • // message above a given marker.
  • var infoWindow = new google.maps.InfoWindow();
  •  
  • // This is a collection of markers, indexed by address.
  • // We are using this so we don't add mulitple markers for
  • // repeat text messages.
  • var markers = {};
  •  
  • // Create a Pusher server object with your app's key and
  • // the SMS channel we want to listen on.
  • var server = new Pusher(
  • "52f3e571a0c9b08ee647",
  • "sms"
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Bind to the window resize event so we can re-size the
  • // map to fill any available space.
  • $( window ).resize(
  • function(){
  • // Resize the map.
  • siteMap.height(
  • $( window ).height() - siteHeader.height()
  • );
  • }
  • );
  •  
  • // Trigger the resize to get the right map dimensions
  • // once the window has loaded.
  • $( window ).resize();
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // Bind to the server to listen for SMS text messages.
  • server.bind(
  • "textMessage",
  • function( data ){
  • // Check to make sure we have a map object. If
  • // we don't then we can't react yet.
  • if (!map){
  • return;
  • }
  •  
  • // I prepare the text message for display on the
  • // map using a marker and info window.
  • addTextMessageToMap( data.address, data.message );
  • }
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // I take the given address and message and try to
  • // prepare them for the map interface by turning the
  • // address into a latitude / longitude point (geocoding).
  • var addTextMessageToMap = function( address, message ){
  •  
  • // Check to make sure that this marker has not
  • // already been found.
  • if (address in markers){
  •  
  • // Store the new message with the existing
  • // marker.
  • markers[ address ].smsData = message;
  •  
  • // Simply show the window above the current text
  • // message.
  • openInfoWindow( markers[ address ] );
  •  
  • // Return out since we don't need to decode
  • // anything else at this point.
  • return;
  •  
  • }
  •  
  • // If we made it here, then this is a new address.
  • // Geocode the address code into a lat/long object.
  • geocoder.geocode(
  • {
  • "address": address
  • },
  • function( results, status ){
  • // Check to see make sure the geocoding was
  • // successful and that we got a result based
  • // on the zip code.
  • if (
  • (status == google.maps.GeocoderStatus.OK) &&
  • results.length
  • ){
  •  
  • // Create the marker at the given
  • // location with the given message.
  • var marker = addMarkerToMap(
  • results[ 0 ].geometry.location,
  • message
  • );
  •  
  • // Store the marker based on the address
  • // so that we can reference it later if
  • // we need to.
  • markers[ address ] = marker;
  •  
  • // Open this new info window.
  • openInfoWindow( marker );
  •  
  • } else {
  •  
  • alert( "Geocoding failed!" );
  •  
  • }
  • }
  • );
  •  
  • };
  •  
  •  
  • // I add maker to the map and initialize it such that it
  • // will respond to click events.
  • var addMarkerToMap = function( latLong, message ){
  • // Create new marker from the location.
  • var marker = new google.maps.Marker({
  • map: map,
  • position: latLong,
  • title: "SMS Text Message From Twilio"
  • });
  •  
  • // Store the SMS data with the marker itself. This
  • // is a corruption of the Marker object, but I am
  • // not sure how else to keep this data with the
  • // marker accross clicks (outside of a closure).
  • marker.smsData = message;
  •  
  • // Add a click-event handler fo the marker as well to
  • // allow the info window to be shown on demand.
  • google.maps.event.addListener(
  • marker,
  • "click",
  • function(){
  • openInfoWindow( marker );
  • }
  • );
  •  
  • // Return the newly created marker.
  • return( marker );
  • };
  •  
  •  
  • // I open the info window above the given marker using
  • // the stored SMS text data for the info winod content.
  • var openInfoWindow = function( marker ){
  • // Set the info window contents.
  • infoWindow.setContent(
  • "<div class='messageBody'>" +
  • marker.smsData +
  • "</div>"
  • );
  •  
  • // Open the info window above the given marker.
  • infoWindow.open( map, marker );
  •  
  • // Pan to the position of the new marker.
  • map.panTo( marker.getPosition() );
  • };
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // When the window has fully loaded, we need to set up
  • // the Google map.
  • $( window ).load(
  • function(){
  •  
  • // Empty the map container.
  • siteMap.empty();
  •  
  • // Create the new Goole map controller using our
  • // site map (pass in the actual DOM object).
  • // Center it above the United States (lat/long)
  • // with a reasonable zoom (5).
  • map = new google.maps.Map(
  • siteMap[ 0 ],
  • {
  • zoom: 5,
  • center: new google.maps.LatLng(
  • 38.925229,
  • -96.943359
  • ),
  • mapTypeId: google.maps.MapTypeId.ROADMAP
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // ---------------------------------------------- //
  • // ---------------------------------------------- //
  •  
  •  
  • // I hide the intro message.
  • var hideIntroMessage = function(){
  • introMessage.animate(
  • {
  • opacity: 0,
  • marginTop: -300
  • },
  • 1500,
  • function(){
  • introMessage.remove();
  • }
  • );
  • };
  •  
  •  
  • // Bind to the document click to hide the intro message
  • // when someone clicks the document.
  • $( document ).bind(
  • "click.intro",
  • function(){
  • // Unbind this click.
  • $( document ).unbind( "click.intro" );
  •  
  • // Hide the intro message.
  • hideIntroMessage()
  • }
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <div id="siteHeader">
  •  
  • <h1>
  • Send an SMS text message to <strong>(917) 791-2120</strong>
  • </h1>
  •  
  • <div id="logos">
  • Twilio + Pusher + ColdFusion + Google Maps
  • </div>
  •  
  • </div>
  •  
  • <div id="siteMap">
  • Loading Map....
  • </div>
  •  
  • <div id="introMessage">
  • <span class="instructions">
  • Send a text message to the following number and it will
  • show up on the map.
  • </span>
  • <span class="number">
  • (917) 791-2120
  • </span>
  • <span class="close">
  • Click anywhere in this window to hide these instructions.
  • </span>
  • </div>
  •  
  • </body>
  • </html>

And that's pretty much all there is to this. It's not a tiny amount of code; but, considering how many services this work flow touches, the amount of code is rather small! While the code posted above constitutes the bulk of the work flow, I'll post the Application.cfc and the Pusher.cfc below:

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Define the application. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 1, 0, 0 ) />
  • <cfset this.sessionManagement = true />
  • <cfset this.sessionTimeout = createTimeSpan( 0, 0, 7, 0 ) />
  •  
  •  
  • <!--- Define the request settings. --->
  • <cfsetting
  • requesttimeout="15"
  • showdebugoutput="false"
  • />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!--- Cache an instance of our Pusher utility. --->
  • <cfset application.pusher = createObject( "component", "Pusher" ).init(
  • "1527",
  • "52f3e571a0c9b08ee647",
  • "********************"
  • ) />
  •  
  • <!--- Return true so the page can process. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I initialize the session.">
  •  
  • <!---
  • Set up the session variables. Remember, since
  • Twilio supports cookies, we can turn on stanard
  • session management.
  • --->
  • <cfset session.message = "" />
  • <cfset session.address = "" />
  • <cfset session.addressRequested = false />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the request.">
  •  
  • <!--- Check for manually application reset. --->
  • <cfif structKeyExists( url, "init" )>
  •  
  • <!--- Reset application. --->
  • <cfset this.onApplicationStart() />
  •  
  • </cfif>
  •  
  • <!--- Return true so the page can process. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onError"
  • access="public"
  • returntype="void"
  • output="true"
  • hint="I handle any uncaught application errors.">
  •  
  • Error!
  •  
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

Remember, since Twilio supports cookies, we can turn on and leverage ColdFusion session management as we would in any standard web application. Here, I am keeping the session timeout low - 7 minutes - since we need to keep track of the user across, at most, two different SMS text messages.

The Pusher.cfc is nothing more than encapsulated version of the Pusher integration code that I have posted before:

Pusher.cfc

  • <cfcomponent
  • output="false"
  • hint="I provide access to the Pusher App's RESTful web service API.">
  •  
  •  
  • <cffunction
  • name="init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return the initialized component.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="appID"
  • type="string"
  • required="true"
  • hint="I am the Pusher application ID defined by Pusher."
  • />
  •  
  • <cfargument
  • name="appKey"
  • type="string"
  • required="true"
  • hint="I am the Pusher application Key defined by Pusher."
  • />
  •  
  • <cfargument
  • name="appSecret"
  • type="string"
  • required="true"
  • hint="I am the Pusher application secret Key defined by Pusher."
  • />
  •  
  • <!--- Store the component properties. --->
  • <cfset variables.appID = arguments.appID />
  • <cfset variables.appKey = arguments.appKey />
  • <cfset variables.appSecret = arguments.appSecret />
  •  
  • <!--- Return this object reference. --->
  • <cfreturn this />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="pushMessage"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I push the given message over the given channel. The message will be serialized internally into JSON - be mindful of the case-sensitivity required in Javascript when defininig your data. I return the HTTP response of the HTTP post.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="channel"
  • type="string"
  • required="true"
  • hint="I am the channel over which the message will be pushed to the client (assuming they ar subscribed to the channel)."
  • />
  •  
  • <cfargument
  • name="event"
  • type="string"
  • required="true"
  • hint="I am the event to trigger as part of the message transfer over the given channel."
  • />
  •  
  • <cfargument
  • name="message"
  • type="any"
  • required="true"
  • hint="I am the message being pushed - this can be any kind of data that can be serialized into JSON."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • Serialize the message into JSON. All data pushed to the
  • web service must be in JSON format.
  •  
  • NOTE: In ColdFusion, unless you use array-notation to
  • define struct keys, JSON-serialized keys are turned
  • into uppercase.
  • --->
  • <cfset local.pusherData = serializeJSON( arguments.message ) />
  •  
  • <!--- Authentication information. --->
  • <cfset local.authVersion = "1.0" />
  • <cfset local.authMD5Body = lcase( hash( local.pusherData, "md5" ) ) />
  • <cfset local.authTimeStamp = fix( getTickCount() / 1000 ) />
  •  
  • <!--- Build the post resource (the RESTfule resource). --->
  • <cfset local.pusherResource = "/apps/#variables.appID#/channels/#arguments.channel#/events" />
  •  
  •  
  • <!--- -------------------------------------------------- --->
  • <!--- -------------------------------------------------- --->
  •  
  •  
  • <!---
  • The following is the digital signing of the HTTP request.
  • Frankly, this stuff is pretty far above my understanding
  • of cryptology. I have adapted code from the PusherApp
  • ColdFusion component written by Bradley Lambert:
  •  
  • http://github.com/blambert/pusher-cfc
  • --->
  •  
  • <!---
  • Create the raw signature data. This is the HTTP
  • method, the resource, and the alpha-ordered query
  • string (non-URL-encoded values).
  • --->
  • <cfset local.signatureData = (
  • ("POST" & chr( 10 )) &
  • (local.pusherResource & chr( 10 )) &
  • (
  • "auth_key=#variables.appKey#&" &
  • "auth_timestamp=#local.authTimeStamp#&" &
  • "auth_version=#local.authVersion#&" &
  • "body_md5=#local.authMD5Body#&" &
  • "name=#arguments.event#"
  • )) />
  •  
  • <!---
  • Create our secret key generator. This can create a secret
  • key from a given byte array. Initialize it with the byte
  • array version of our PushApp secret key and the algorithm
  • we want to use to generate the secret key.
  • --->
  • <cfset local.secretKeySpec = createObject(
  • "java",
  • "javax.crypto.spec.SecretKeySpec"
  • ).init(
  • toBinary( toBase64( variables.appSecret ) ),
  • "HmacSHA256"
  • )
  • />
  •  
  • <!---
  • Create our MAC (Message Authentication Code) generator
  • to encrypt the message data using the PusherApp shared
  • secret key.
  • --->
  • <cfset local.mac = createObject( "java", "javax.crypto.Mac" )
  • .getInstance( "HmacSHA256" )
  • />
  •  
  • <!--- Initialize the MAC instance using our secret key. --->
  • <cfset local.mac.init( local.secretKeySpec ) />
  •  
  • <!---
  • Complete the mac operation, encrypting the given secret
  • key (that we created above).
  • --->
  • <cfset local.encryptedBytes = local.mac.doFinal(
  • local.signatureData.getBytes()
  • ) />
  •  
  •  
  • <!---
  • Now that we have the encrypted data, we have to convert
  • that data to a HEX-encoded string. We will use the big
  • integer for this.
  • --->
  • <cfset local.bigInt = createObject( "java", "java.math.BigInteger" )
  • .init( 1, local.encryptedBytes )
  • />
  •  
  • <!--- Convert the encrypted bytes to the HEX string. --->
  • <cfset local.secureSignature = local.bigInt.toString(16) />
  •  
  • <!---
  • Apparently, we need to make sure the signature is at
  • least 32 characters long. As such, let's just left-pad
  • with spaces and then replace with zeroes.
  • --->
  • <cfset local.secureSignature = replace(
  • lJustify( local.secureSignature, 32 ),
  • " ",
  • "0",
  • "all"
  • ) />
  •  
  •  
  • <!--- -------------------------------------------------- --->
  • <!--- -------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have all the values we want to post,
  • including our encrypted signature, we can post to the
  • PusherApp REST web service using CFHTTP.
  • --->
  • <cfhttp
  • result="local.post"
  • method="post"
  • url="http://api.pusherapp.com#local.pusherResource#">
  •  
  • <!---
  • Alert the post resource that the value is coming
  • through as JSON data.
  • --->
  • <cfhttpparam
  • type="header"
  • name="content-type"
  • value="application/json"
  • />
  •  
  • <!--- Set the authorization parameters. --->
  •  
  • <cfhttpparam
  • type="url"
  • name="auth_version"
  • value="#local.authVersion#"
  • />
  •  
  • <cfhttpparam
  • type="url"
  • name="auth_key"
  • value="#variables.appKey#"
  • />
  •  
  • <cfhttpparam
  • type="url"
  • name="auth_timestamp"
  • value="#local.authTimeStamp#"
  • />
  •  
  • <cfhttpparam
  • type="url"
  • name="body_md5"
  • value="#local.authMD5Body#"
  • />
  •  
  • <!--- Sent the name of the pusher event. --->
  • <cfhttpparam
  • type="url"
  • name="name"
  • value="#arguments.event#"
  • />
  •  
  • <!--- Send the actual message data (JSON data). --->
  • <cfhttpparam
  • type="body"
  • value="#local.pusherData#"
  • />
  •  
  • <!--- Digitally sign the HTTP request. --->
  • <cfhttpparam
  • type="url"
  • name="auth_signature"
  • value="#local.secureSignature#"
  • />
  •  
  • </cfhttp>
  •  
  • <!--- Return the HTTP status code. --->
  • <cfreturn local.post />
  • </cffunction>
  •  
  • </cfcomponent>

Again, this demo doesn't have much value in and of itself; but, I hope that it can demonstrate how easily ColdFusion can be the glue behind complex work flows that power mobile and realtime messaging applications.




Reader Comments

Ben, this is cool creative stuff. Thanks for sharing. One you can do to simplify the demo is take Pusher out of the picture and use BlazeDS within ColdFusion 9. BlazeDS won't push, but with long-polling your client-side piece won't know the difference.

@Aaron,

I'll have to check out BlazeDS. I've heard about it forever, but I have never played with it myself. My only concern about the long-polling is that it might put undue stress on the server? What I like about Pusher is that it only communicates when necessary. Of course, that might just be an invalid concern. I'll have to look into it.

@Baz,

Not currently.

I was thinking about maybe making an SMS Guest Book for fun. Anyone think that would be a nice idea? Or would people be too turned off about the idea of their general locations (zip) being on a map?