Using WebAIM with ASP.NET 2.0 and ASP.NET AJAX, Part 4: Sending Instant Messages

Enable the Subscriptions block here!

by Christian Wenz
July 24, 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. The first three articles in this series showcased some of the features of ASP.NET AJAX and the ASP.NET AJAX Control Toolkit and implemented the first building blocks to write a web interface to AIM. This article starts where part three of the series ended, so make sure to read the previous three articles and take special care regarding system requirements and information about the license keys.

Preparing the Application

In order to enable the current code base to send instant messages, we need to extend and also to refactor the code a bit. Let’s start with the new elements first: we need a UI for sending and receiving messages. In order to have an Ajax-y widget, as well, we once again use the DragPanelExtender from the ASP.NET AJAX Control Toolkit. The markup looks very similar to the buddy list markup that already exists in the .aspx page:

<asp:Panel ID="divIM" runat="server" CssClass="panelStyle">
  <asp:Panel ID="divIMDrag" runat="server" CssClass="dragStyle"><div>Instant Messages</div></asp:Panel>
  <div id="divMessages" runat="server" style="overflow: scroll;"><span></span></div>
  <div id="divInput" runat="server">Message to <span id="spanName"></span><br />
  <textarea id="txtIM" rows="2" cols="30" runat="server"></textarea>
  <input type="button" id="btnSend" value="Send" /></div>
</asp:Panel>
<ajaxToolkit:DragPanelExtender id="dpe2" DragHandleID="divIMDrag" TargetControlID="divIM" runat="server" />

Note the following IDs; many of them will later be used from the JavaScript code:

  • divIM: The surrounding <div> element.
  • divDrag: The drag handle.
  • divMessages: The <div> element that will hold the messages sent and received.
  • divInput: The <div> element that contains the input field for new messages.
  • spanName: A <span> placeholder for the name of the current recipient.
  • txtIM: The text field where users can enter new text messages.
  • btnSend: The button that will send a message to the server.

For the sake of simplicity, there will only be one window for sending and receiving messages; all messages will appear here. When you are chatting with many people at the same time, you might want to instead use individual windows for this task, or at least a tabbed interface (one tab per active contact).

You now get a draggable messages list, which will show both input and output messages for outgoing and incoming IMs (instant messages).

All other changes will be done in the WebAIM.js and WebService.asmx files. First of all, the getBuddies() and getBuddiesCallback() functions need to be altered. As previously mentioned in the article series, the client is continuously fetching events from the remote server; one of those events is a change in the buddy list. However, for instant messages, we might want to use other events too. Therefore, these two functions are first renamed to getEvents() and getEventsCallback(), and all references to the previous names need to be updated, as well.

Then, when the buddy list is retrieved from the server, getEventsCallback() calls displayBuddies(), which puts the buddies into the first draggable panel on the page; all names now must be clickable. When a name is clicked, this name needs to be entered into the second draggable panel (the one for the IMs). Here is an excerpt from the updated displayBuddies() function, with changes in bold:

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.style.cursor = "pointer";
      li2.onclick = new Function("sendWindow('" + buddy + "');");
      li2.appendChild(li2text);
      ul1.appendChild(li2);
    }
  
    buddylist.appendChild(li1);
    buddylist.appendChild(ul1);
  }

  while ($get(_outputID).firstChild != null) {
    $get(_outputID).removeChild($get(_outputID).firstChild);
  }
  $get(_outputID).appendChild(buddylist); 
  
  _intervalID = window.setInterval("getEvents();", _interval);
}

As you can see, the code now also completely empties the buddy list whenever displayBuddies() is called; a new feature that will be introduced in the next series installment could cause the buddy list to be updated while you are online (for instance, when buddies sign on or off).

The above code executes a sendWindow() JavaScript function once a buddy name is clicked. This function is rather simple: it displays the name of the currently clicked buddy (so that the users know to whom they are sending an IM), and also activates the send button:

function sendWindow(buddy) {
  $get("spanName").innerHTML = buddy;
  $get("btnSend").onclick = 
    new Function("sendMessage('" + buddy + "', $get('txtIM').value);");
}

When the send button is clicked, the sendMessage() JavaScript function is executed. This is, in turn, delegating this work to the server by calling a remote web service method. Note that with this web service call, an additional context parameter is provided (here: an object with the buddy's name and the message to be sent). This information will then be automatically provided to the callback function as an argument. You will see in a minute why this is very convenient here.

function sendMessage(buddy, message) {
  dump("Sending message");
  WebService.sendMessage(_token.a, buddy, message, sendMessageCallback, myError, 
    {"buddy": buddy, "message": message});
}

Let's move away from the client for a short while and have a look at the server. Sending an IM requires a call to http://api.oscar.aol.com/im/sendIM; you also have to provide a list of parameters:

  • f: The output format; we prefer JSON.
  • k: The WebAIM license key (alternatively, the AIMSID value retrieved by startSession()).
  • a: The token from the getToken() call.
  • t: The recipient of the IM.
  • message: The text of the IM (at one point in the WebAIM documentation, this parameter is called "msg," which is wrong).

This code assembles the request URL, makes the HTTP request, and returns the (JSON) result:

public string sendMessage(string token, string recipient, string message)
{

  string myURL = String.Format(
    "http://api.oscar.aol.com/im/sendIM?f=json&k={0}&a={1}&t={2}&message={3}",
    HttpUtility.UrlEncode(
      System.Configuration.ConfigurationManager.AppSettings["WebAIMKey"]),
    token,
    recipient,
    message);

  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();

}

Unfortunately, there is one catch: the first time you are calling the sendIM API, you will most probably get an "error 450: insufficient rights." Remember the security screen users get when they are using the web interface to log onto AIM? This screen resurfaces, this time asking users to grant the application permission for sending instant messages on their behalf.

The user needs to grant the web application permission to send instant messages
Figure 1. The user needs to grant the web application permission to send instant messages

Therefore, the message sending callback function is a bit more complicated: it checks the status code of the sendIM call. If this is 200, the message has been successfully sent, and the message text is then put on top of the current message list. If not, however, the data returned from the server contains the URL of the consent dialog. This URL needs then to be shown in the <iframe> element. Once the user grants the access to the API, JavaScript appends this information to the hash of the current page. The checkURL() function periodically has a look at this URL (twice a second), monitoring it for changes.

function sendMessageCallback(data, context) {
  data = Sys.Serialization.JavaScriptSerializer.deserialize(data);
  if (data.response && data.response.statusCode) {
    if (data.response.statusCode == 200) {
      dump("Success.");
      var text = "<b>To ";
    } else {
      dump("Error " + data.response.statusCode);
      text = "<b>Error " + data.response.statusCode + " sending ";
      if (data.response.statusCode == 450 && 
        data.response.data && data.response.data.redirectURL) {
        dump("Rights denied");
        $get(_iframeID).src = data.response.data.redirectURL + "?k=" + __KEY;
        window.clearInterval(_intervalID);
        _context = context;
        _intervalID = setInterval("checkURL(true)", 500);
      }
    }
    text += htmlEncode(context.buddy) + ": </b>" + 
            htmlEncode(context.message) + "<br />";
    $get(_messagesID).innerHTML = text + $get(_messagesID).innerHTML;
  }
}

But wait a minute--didn't we use checkURL() before? Indeed, we did, when we were waiting for the user's access grant before loading the buddy list. Therefore, we are simply reusing the code that is already in the WebAIM.js script. If you have a close look at the preceding code, you saw that we are providing the argument true to the checkURL() function. The function is using this information to know whether we are waiting for the first user consent (buddy list) or the second one (sending instant messages). Before calling checkURL(), the context information when sending the instant message (buddy name, message text) was saved in the global variable _context. checkURL() is using this information to try to send the instant message again once the user grants access to the WebAIM API.

Here is the updated checkURL() code, with changes highlighted.

function checkURL(secondConsent) {
  var lh = location.hash;
  if (lh == "AUTHDONE" || lh == "#AUTHDONE") {
    // ...
    // stripped for brevity
    // ...
  } else if (lh == "CONSENTDONE" || lh == "#CONSENTDONE") {
    location.hash = "#";
    dump("Consent detected");
    window.clearInterval(_intervalID);
    if (secondConsent) {
      _intervalID = window.setInterval("getEvents();", _interval);
      dump("second consent");
      sendMessage(_context.buddy, _context.message);
      _context = null;
    } else {
      startSession();
    }
  }
}

At least on the second attempt the IM should be sent, and will reach its recipient shortly thereafter.

The web application sends the instant message
Figure 2. The web application sends the instant message

AIM client receives it
Figure 3. And a "real" AIM client receives it

Next Steps

This article showed you how to use the WebAIM API to send instant messages, a task that usually takes two attempts at first. The next (and final) installment of the ASP.NET AJAX WebAIM series will show you how you can also receive messages.

References