Using WebAIM with ASP.NET 2.0 and ASP.NET AJAX, Part 3

Enable the Subscriptions block here!

by Christian Wenz
May 18, 2007

AOL's WebAIM API provides a very comfortable means to incorporate a JavaScript-powered AIM client in a web page. With some extra effort it is also possible to control most of WebAIM's features using a server-side technology. This article concludes the series and shows how to use the ASP.NET AJAX Control Toolkit to create a buddy list. In order to get the most out of the article, you should have read parts I and II. We start off right where we stopped at the end of part II.

Starting a Session

The next step on our workflow list is starting the actual WebAIM session by calling the startSession() API method. As before, the client-side code for that is rather trivial.

function startSession() {
  WebService.startSession(_token.a, startSessionCallback, myError);
}

The server code is a bit more complicated this time. This is caused by the way the WebAIM session works. When you start a session, you have to subscribe to certain server events at the same time. These server events include AIM buddies signing on and off, new IM messages arriving, and more. When you instantiate the session, you already have to provide a list of the events you are interested in. Note that if you have a typo in the event list, you currently get a strange error message not hinting at what was really causing the error. Therefore be super careful in this step.

Here is the first version of the server-side code that calls the startSession() WebAIM API method. It is not the final version yet, but a good start. We subscribe to two events, IM events and buddylist events.

[WebMethod(EnableSession = true)]
public string startSession(string token)
{
  string myURL = String.Format(
    "http://api.oscar.aol.com/aim/startSession?f=json&k={0}&a={1}&events=im,buddylist",
    HttpUtility.UrlEncode(
      System.Configuration.ConfigurationManager.AppSettings["WebAIMKey"]),
    token);

  HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(myURL);
  myRequest.Referer = System.Configuration.ConfigurationManager.AppSettings["BaseURL"];

  HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();

  if (myResponse.StatusCode != HttpStatusCode.OK)
  {
    throw new Exception("Status: " + myResponse.StatusCode.ToString());
  }

  Stream s = myResponse.GetResponseStream();
  StreamReader rs = new StreamReader(s, Encoding.UTF8);
  string json = rs.ReadToEnd();
  return json;
}

Again this API call usually fails on the first attempt and returns "Rights denied", along with a URL where the user is prompted to acknowledge that WebAIM accesses the account data. This is similar to the "Authorization required" message when calling getToken() for the first time. Again this is a problem that must be solved on the client side, by displaying the acknowledgement form in the inline frame (<iframe> element). Here is the relevant client JavaScript code for that:

if (data.response && data.response.statusText &&
    data.response.statusText == "Rights Denied" && 
    data.response.data && data.response.data.redirectURL) {
  $get("myIframe").src = data.response.data.redirectURL + "?k=" + __KEY;
  _intervalID = setInterval("checkURL()", 500);
}

Again checkURL() is called, this time waiting for the special code #CONSENTDONE in the URL. This value is automatically appended to the URL once the user allowed WebAIM to access the data.

Figure 1
Figure 1. The user has to grant the application access to the WebAIM API

So once there is consent, we are ready to call startSession() again:

if (lh == "CONSENTDONE" || lh == "#CONSENTDONE") {
    location.hash = "#";
  clearInterval(_intervalID);
  startSession();
}

Coming back to the event subscription mechanism of WebAIM, once the startSession() call succeeds, the server provides us with a special URL we can call to poll event information (like the buddy list, or a list of incoming instant messages). This URL is also subject to change, so we cannot hardcode it into our application but have to request it using startSession(). To retain this special URL on the server, we use the ASP.NET session management which also works for web services. In order to make the startSession() web service method session-aware, the EnableSession attribute is used in the following fashion:

[WebMethod(EnableSession = true)]
public string startSession(string token)
{
  // ...
}

Then we can read and write session data. The following regular expression searches for the URL in the data returned from the server (the JSON value is called fetchBaseURL) and saves the value in a session variable:

Regex r = new Regex("\"fetchBaseURL\"\\s*:\\s*\"(.*?)\"");
Match m = r.Match(json);
if (m.Value != String.Empty && m.Groups.Count == 2) {
  Session["fetchBaseURL"] = m.Groups[1].Value;
}

The client side does not care about this special URL, but is only interested in another piece of information returned from the WebAIM API: The AIM session ID, or AIMSID:

function startSessionCallback(result) {
  if (typeof(result) != "object") {
    data = Sys.Serialization.JavaScriptSerializer.deserialize(result);
  } else {
    data = result;
  }
  if (data.response && data.response.data && data.response.data.aimsid) {
    _aimsid = data.response.data.aimsid;
    getBuddies();
  }
  // ...
}

So the AIM session ID now resides in a JavaScript variable, and the final JavaScript function in our application is called: getBuddies().

Creating a Dragable Buddy List

The getBuddies() function does what its name suggests: it retrieves a list of buddies, via a simple web service call:

function getBuddies() {
  WebService.getBuddies(getBuddiesCallback, myError);
}

On the server side, the previously saved fetchBaseURL session variable is used to retrieve the buddy list - remember that we subscribed to any buddy list events and have not polled these events yet, so we get a complete list. Do not forget the EnableSession attribute, since we do need access to session variables.

[WebMethod(EnableSession = true)]
public string getBuddies()
{
  if (Session["fetchBaseURL"] == null)
  {
    throw new Exception("Failed to read fetchBaseURL");
  }

  string myURL = Session["fetchBaseURL"].ToString() + "&f=json";

  HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(myURL);
  myRequest.Referer = System.Configuration.ConfigurationManager.AppSettings["BaseURL"];

  HttpWebResponse myResponse = (HttpWebResponse)myRequest.GetResponse();

  if (myResponse.StatusCode != HttpStatusCode.OK)
  {
    throw new Exception("Status: " + myResponse.StatusCode.ToString());
  }

  Stream s = myResponse.GetResponseStream();
  StreamReader rs = new StreamReader(s, Encoding.UTF8);
  return rs.ReadToEnd();
}

The buddy list information is now in JSON format and contains a list of all the buddies, no matter whether online or not. In order to focus on buddies that are online only, this data must now be filtered. Therefore, we create a new JavaScript object and write all buddies into it that are online; a special property of the data returned from the server provides us with the buddy status.

During the process of creating arrays of online buddies, the Array.add() method proves to be helpful. By default, JavaScript arrays do not support this method, but ASP.NET AJAX enriches JavaScript arrays so that add() now works. In the end, we create an array of buddy group objects. Every buddy group object consists of an array of buddies which all have a name. We also keep track of the number of buddies in each group so that we later can display how many buddies are online, how many are not. Here is the JavaScript code:

var _buddies = [];

function getBuddiesCallback(data) {
  data = Sys.Serialization.JavaScriptSerializer.deserialize(data);
  if (data.response && data.response.data && data.response.data.events) {
    for (var i = 0; i < data.response.data.events.length; i++) {
      var ev = data.response.data.events[i];
      if (ev.type == "buddylist") {
        for (var j = 0; j < ev.eventData.groups.length; j++) {
          var g = ev.eventData.groups[j];
          var _g = new Object();
          _g.buddies = [];
          var cnt = 0;
          for (var k = 0; k < g.buddies.length; k++) {
            var b = g.buddies[k];
            if (b.state == "online") {
              Array.add(_g.buddies, b.displayId);
              cnt++;
            }
          }
          _g.name = g.name + " (" + cnt + "/" + g.buddies.length + " online)";
          Array.add(_buddies, _g);
        }
      }
    }
    displayBuddies();
  }
}

Do not expect that this code came easy. It required analyzing the JSON data returned from the server and based on that tweaking the code so that the relevant buddy list information is retrieved and saved in the _buddies array. Do also note that WebAIM has a built-in flood control: There is a certain number of API calls per time interval. Remember that you have to call quite a number of API functions to get signed into AIM, so sometimes this limit is reach quite easily during development and testing. So when you get an error message that suggest that you have tried to often, just wait a few minutes and then try again.

The last missing piece, at least of this sample application, is to display the buddy list. For this, some JavaScript DOM effects will be used to create a nested bulleted list with all groups and, within those groups, all buddies that are currently online:

var _outputID = "div1";

function displayBuddies() {
  var buddylist = document.createElement("ul");
  for (var i = 0; i < _buddies.length; i++) {
    var group = _buddies[i];
    
    var li1 = document.createElement("li");
    var li1text = document.createTextNode(group.name);
    li1.appendChild(li1text);
    
    var ul1 = document.createElement("ul");
    
    for (var j = 0; j < group.buddies.length; j++) {
      var buddy = group.buddies[j];
      var li2 = document.createElement("li");
      var li2text = document.createTextNode(buddy);
      li2.appendChild(li2text);
      ul1.appendChild(li2);
    }
  
    buddylist.appendChild(li1);
    buddylist.appendChild(ul1);
  }

  $get(_outputID).removeChild($get(_outputID).firstChild);
  $get(_outputID).appendChild(buddylist); 
}

Note that the JavaScript variable _outputID holds the ID of the HTML element where the nested list will be put into. However there is yet more to come: We do not use a plain and boring <div> element, but we manage to make the element draggable, allowing users to drag the buddy list anywhere on the page.

This is done by using a control from the ASP.NET AJAX Control Toolkit called DragPanelExtender. The DragPanelExtender "extends" a panel so that it is draggable. Actually, we need two panels: An outer panel defines the whole area that is draggable. Within this panel you can define another panel that will be used as the drag handle: the area where the mouse pointer has to hold the panel and release it. Here is the markup; <asp:Panel> is more or less the ASP.NET way of saying <div>.

<asp:Panel ID="divPanel" runat="server" CssClass="panelStyle">
  <asp:Panel ID="divDrag" runat="server" CssClass="dragStyle"><div>Buddy List (WebAIM/ASP.NET AJAX)</div></asp:Panel>
  <div id="div1" runat="server" style="overflow: scroll;"><span>Loading the buddy list ...</span></div>
</asp:Panel>

Currently this is just a set of panels; but the DragPanelExtender attaches the JavaScript functionality. The DragHandleID property provides the ID of the drag handle, the TargetControlID property provides the ID of the whole buddy list, including the handle:

<ajaxToolkit:DragPanelExtender id="dpe1" DragHandleID="divDrag" TargetControlID="divPanel" runat="server" />

Finally, you should consider styling the application a bit. Provide a nice layout for the drag handle and the buddy list by adding the following CSS into the StyleSheet.css file:

.dragStyle 
{
  width: 300px;
  border: solid 2px Black;
  color: White;
  background-color: Blue;
}

.panelStyle
{
  width: 300px;
  border: solid 2px Black;
  color: Black;
  background-color: White;
}

Figure 2
Figure 2. The draggable buddy list, and the debug window in the background

Next Steps and Best Practices

These three article installments have guided you through all the steps to access the WebAIM API from the server side. You have seen that the code became quite tricky from time to time, and that several workarounds had to be used. However you also saw how nicely ASP.NET AJAX integrates into ASP.NET and how easy it is to use some of the effects of the framework and also of the ASP.NET AJAX Control Toolkit.

The application, however, is far from complete. Obvious next steps would be the implementation of sending and receiving instant messages, of updating the buddy list when buddies sign on or off. However the groundwork has been laid with these articles; the other methods of the WebAIM API work quite similarly to the ones you now know.

Before going live with an application like that, you should take care of security, of course. For the sake of simplicity and shortness, the data validation we are using in the sample code is not yet complete. Be aware that opening up an Ajax API on the server also might open up your server data for attackers, so take data validation and sanitization very seriously.

During development, some extra browser tools can also prove to be extremely helpful, especially when it comes to debugging the HTTP traffic and having a look at the JSON data that is sent between client and server. For Firefox browser, the Firebug extension (http://www.getfirebug.com/) was a vital ingredient of the developer's toolkit. For Internet Explorer, the Web Development Helper (http://projects.nikhilk.net/Projects/WebDevHelper.aspx) is an invaluable piece of software.

References