Creating a Custom Open AIM Chat Client Using Adobe AIR

By Richard Bates
April 23, 2008

In today's business world, the web isn't just a static repository of information: it's a dynamic plane of communication that lets people instantly interact with each other. AOL's latest release of its Open AIM API supports this idea by giving Adobe AIR and Adobe Flex developers the tools to move messaging functionality into their applications to create full-blown communication tools. This article lays the groundwork for that leap to the next level of communication by showing you how to create a functional chat application that you can embed in your AIR or Flex applications.

To get you started, in this article I will walk you through the steps of creating an embeddable chat application. I'll cover basic Open AIM skills, including:

  • Logging in to the AOL Instant Messenger (AIM) network
  • Understanding and handling the BuddyList object
  • Sending and receiving IMs
  • Becoming familiar with Open AIM API methods, events, and properties

In creating the application, I also will cover launching NativeWindows in AIR, using the operating system to notify the user of program events, parsing text and formatting HTML on the fly, writing to the file system, and using view states to give the user readable, meaningful information and a clear workflow. As you work through the example application, you'll need to be comfortable working with basic ActionScript 3 functions and MXML components in Adobe's Flex Builder.

Obtaining the API

AOL's new Open AIM API makes building AIM-enabled Flex, Flash, and AIR components fast and simple. To get started, you'll first need to download the Open AIM ActionScript 3 libraries from Google Code. Access to the source code is provided through a code repository. To download the code into Flex Builder, you'll need an add-on called Subclipse. You can find the download and installation instructions at http://subclipse.tigris.org/install.html.

Note: You don't need to download the API if you're following along with the sample application; the project archive contains these classes.

Download and save the Flex project archive to a location you can remember. Then, go into Flex Builder. From the top menu, click File, and then move your mouse over Import. Click Flex Project from the menu that appears. Make sure Archive File: is selected, and then click Browse. Click the project archive you previously downloaded, and then click Finish. Flex Builder will import the project into the workspace, and the project's file tree will appear in the left pane of the Flex Builder window.

After you've installed Subclipse and restarted Flex Builder, click the slim arrow icon in the upper right corner to change the active perspective. Click SVN Repository Exploring.

Next, to add a new SVN repository, click the +SVN icon.

You'll need to enter the URL for the project wimas3 on Google Code: http://wimas3.googlecode.com/svn/trunk/.

Click Finish, and the directories in the SVN repository will populate the SVN browser pane in Flex Builder.

Expand the wimas3 folder, and then right-click the src folder. Click Checkout. Leave the default options and click Finish. You'll see a list of choices for your project type. Expand the Flex Builder folder, and then click Flex Project. On the next page, type a project name, and then select the option Desktop Application (Runs in Adobe AIR). Click Finish, and Flex Builder will create your project. If any warnings or notifications are displayed, just acknowledge or accept them. You should now have your project file tree on the left, and the code or design view on the right. Make sure the com folder is beneath your source folder, in the same folder as your main MXML file. If it isn't, you can drag and drop it to that location. Your Flex builder window should now look something like this:

Now, you're ready to get started with your own custom Open AIM chat client.

Laying the ActionScript Groundwork for the Open AIM Client

Note: If you've downloaded the sample project, you can import all the code from this article into Flex Builder. Simply click File, then click Import, and then click the project archive.

To get going, first you'll need to import the classes you need from the API you obtained from Google Code. To do that, create a new <mx:Script> block in the Flex Builder code view, and insert these import statements:

<mx:Script>
		<![CDATA[
		import com.aol.api.wim.events.SessionEvent;
		import com.aol.api.wim.Session;
		import com.aol.api.wim.events.BuddyListEvent;
		import com.aol.api.wim.data.BuddyList;
		import com.aol.api.wim.events.DataIMEvent;
		import com.aol.api.wim.events.IMEvent;
		import flash.display.Stage;
		]]>
</mx:Script>

The first class you'll use is the Session class, which provides all the base functionality for dispatching and receiving Open AIM events. Begin by creating a logIn function:

private function logIn():void{			
session.addEventListener(SessionEvent.STATE_CHANGED, onStateChange);
session.addEventListener(BuddyListEvent.LIST_RECEIVED, onBLReceived);
				
		} 

Now, take a look at the two event listeners you added to the session.

STATE_CHANGED is dispatched when the user's session state changes. You'll use this one to let the user know if they are offline, online, or if their login was rejected and why.

LIST_RECEIVED will receive notification when the Buddy List is sent from the AIM server to your client. You add it now because the Buddy List is fundamental to the application; it will provide your buddies' information so you can send them messages without having to type the recipient for each message.

Now that you've added the necessary event listeners, you need to send your credentials to log in. To do that, you'll declare some variables in the <mx:Script> block, immediately after the import statements:

public var devId:String = "CCvgrtrt654tgf678";
public var session:Session = new Session(stage, devId, "Test Client", ".1");

The devId variable should contain your developer API Key. If you don't have one, you can sign up at http://dev.aol.com/aim, then go to http://developer.aim.com/manageKeys.jsp to manage your API keys. When you request a new key, you'll need to enter some information and make some choices. For this AIR application, enter http://localhost for your URL. For Referrer Check, click No, and for Client Login, click Yes.

The session variable instantiates your session, and passes some necessary arguments. The first argument, stage, is the container for your session. The devId is also passed, followed by the application name (in this case, Test Client). The last argument, .1, is the client version.

Now that your developer information is set up, you need to pass in the user's personal credentials--their AIM username and password. Rather than hard-code this information, you'd like to have the user type it in when they run the application. To do that, you'll add two text input controls, and two labels to tell the user what they should type there. Do this beneath your closing </mx:Script> tag:

<mx:VBox width="100%" height="100%" id="mainBox" horizontalAlign="center">
		<mx:ComboBox enabled="false" width="150" dataProvider="{availableStates}" id="combobox1" fillAlphas="[0.5, 0.76, 0.5, 0.7]"></mx:ComboBox>
		<mx:VBox verticalGap="0" id="vbox1">
			<mx:TextInput id="aimuser"/>
			<mx:Label text="username" color="#FFFEFE" fontStyle="italic"/>
			<mx:TextInput id="aimpass" displayAsPassword="true"/>
			<mx:Label text="password" color="#FFFFFF" fontStyle="italic"/>
			<mx:HBox width="100%" horizontalAlign="center">
				<mx:Button label="Login" id="loginBtn" click="logIn()"/>
			</mx:HBox>
		</mx:VBox>
		<mx:Label id="statusLabel" color="#FFFFFF"/>
	</mx:VBox>

As you can see, this includes the inputs for username and password, and also three other controls: a login button to initiate the login process, a status label to tell the user what is happening, and a combo box to allow the user to change their AIM session state to offline.

Back in your <mx:Script> block, inside the logIn function, add these lines to send the user's input to the AIM server using clientLogin:

session.signOn(aimuser.text, aimpass.text);

ClientLogin is a new authentication method for Open AIM. Your application will use clientLogin through the clientLogin.as file contained in com/aol/api/openauth. You won't need to modify this file, but if you'd like to know more about how it works, go to http://dev.aol.com/authentication_for_clients#clientLogin.

When the logIn function is called, and when the server responds, a STATE_CHANGED event will be dispatched. You'll use this event to let the user know what is happening using statusLabel. Your logIn function adds the necessary event listener, and triggers the onStateChange function, which you'll create now:

private function onStateChange(event:SessionEvent):void {
	statusLabel.text = session.sessionState.toString();
		}

When this function executes, it will change the text of your statusLabel control to show the user the status of their login request.

There are several state change events that are dispatched during login. Many of them are simply informational if the application and the user's Internet connection are functioning properly. To handle these informational state events, create a new view state in Flex Builder called loading. Additionally, when the Buddy List is received, you know that the login has been successful, and you can start sending and receiving messages. This warrants another view state; call this one online. Add this code to your main MXML file, outside of your <mx:Script> block:

<mx:states>
		<mx:State name="loading">
			<mx:RemoveChild target="{vbox1}"/>
			<mx:AddChild relativeTo="{mainBox}" position="lastChild">
				<mx:VBox width="100%" height="100%" horizontalAlign="center" verticalAlign="top" id="vbox2">
					<mx:Spacer height="25" id="spacer1"/>
					<mx:Label text="Loading..." fontSize="14" id="label1" color="#FDFDFD"/>
				</mx:VBox>
			</mx:AddChild>
			<mx:RemoveChild target="{statusLabel}"/>
			<mx:AddChild relativeTo="{vbox2}" position="lastChild" target="{statusLabel}"/>
		</mx:State>
		<mx:State name="online" basedOn="loading">
			<mx:RemoveChild target="{label1}"/>
			<mx:RemoveChild target="{spacer1}"/>
			<mx:AddChild relativeTo="{vbox2}" position="lastChild">
				<mx:Accordion width="200" height="340">
					<mx:Canvas label="Buddies" width="100%" height="100%">
						<mx:List x="0" y="0" width="100%" click="activeBuddy = buddiesList.selectedItem.aimId" id="buddiesList" doubleClickEnabled="true" doubleClick="openChat()" borderStyle="none" height="100%"></mx:List>
					</mx:Canvas>
					<mx:Canvas label="Family" width="100%" height="100%">
					</mx:Canvas>
					<mx:Canvas label="Co-Workers" width="100%" height="100%">
					</mx:Canvas>
				</mx:Accordion>
			</mx:AddChild>
			<mx:SetProperty name="height" value="499"/>
			<mx:SetProperty name="width" value="214"/>
			<mx:SetProperty target="{combobox1}" name="enabled" value="true"/>
			<mx:SetProperty target="{combobox1}" name="selectedIndex" value="1"/>
			<mx:SetEventHandler target="{combobox1}" name="change" handler="setStatus()"/>
			<mx:SetStyle target="{combobox1}" name="color" value="#FDFDFD"/>
		</mx:State>
	</mx:states>

There's a lot of code here, but its function is fairly simple. When the loading state is entered, the user sees this:

And when the application enters the online state, the user sees this:

To trigger the loading view state, add this line to your logIn function:

currentState = 'loading';

To trigger the online view state, you'll need to first receive and parse the Buddy List.

Handling the Buddy List

When a client signs on to the AIM server, the user's Buddy List is automatically sent. The BuddyList object contains information about all of the user's buddies. Let's take a look at the structure of the BuddyList object:

As you can see, BuddyList contains a lot of information. Only a few apply to the sample application you are building here:

  • aimId: This is the buddy's AIM ID. You'll use it to specify a particular buddy as the recipient of a message.
  • displayId: This is the display name the buddy has specified for their AIM account. You'll use it to tell the user which buddy it is, because this name is sometimes more meaningful than the aimId.
  • state: This property indicates whether the buddy is online, offline, away, etc. Although you can send messages to a buddy in states other than online, the syntax changes slightly, and your application will need to react accordingly.

Now that you know which properties you want and where they are, you need to pass them to the visual controls in your online view state.

Now take a look at the list control you added earlier as part of the online view state:

<mx:List x="0" y="0" width="100%" click="activeBuddy = buddiesList.selectedItem.aimId" id="buddiesList" doubleClickEnabled="true" doubleClick="openChat()" borderStyle="none" height="100%"></mx:List>

This code creates a list called buddiesList, which will hold the users who are members of the Buddies group. You also set a variable called activeBuddy, and add an openChat()function call that is called when the user double-clicks a buddy's name.

Next, you'll use ActionScript to populate the list with the user's buddies.

You'll recall that you added an event listener for the LIST_RECEIVED event in your logIn function. Here is the function that is called on the receipt of the Buddy List, onBLReceived. Insert the code in your <mx:Script> block:

protected function onBLReceived(event:BuddyListEvent):void
{
currentState = 'online';
				buddiesList.dataProvider = event.buddyList.groups[0].users;
				buddiesList.labelFunction = blLF;
}

This function accepts the Buddy List, and parses the Buddy Group called Buddies, which is located at position [0] in the buddyList.groups array. Members of the Buddies group are then displayed in your buddiesList MXML component. The way these names are displayed is controlled by a labelFunction, blLF. Let's examine the labelFunction, flLF, to see what is returned. Add the following code to your <mx:Script> block:


private function blLF(item:Object):String {	
var blLabelText:String = item.aimId.toString() + ' - ' + item.state.toString();
return blLabelText;
}

This function accepts the buddy's array, which contains all of that buddy's information. However, you only want to show two properties in your visual Buddy List: the buddy's aimId, and the buddy's state. The previous function takes these two values and separates them with a space and a hyphen. The result will look like this:

oneBuddysName - online

anotherBuddysName - offline

And so on for each buddy in the user's Buddies group.

Opening a New NativeWindow for Chat

When the user double-clicks a buddy's name, the openChat function is called. To create the function, add this code in your <mx:Script> block:

private function openChat():void {
				var options:NativeWindowInitOptions = new NativeWindowInitOptions();
				options.systemChrome = NativeWindowSystemChrome.NONE;
				options.transparent = true;
				var win:NativeWindow = new NativeWindow(options);
				var canvas:ChatCanvas = new ChatCanvas();
				hiddenCanvas.addChild( canvas);
				hiddenCanvas.removeChild( canvas );
				win.stage.scaleMode = 'noScale';
				win.stage.align = 'topLeft';
				canvas.session = this.session;
				canvas.activeBuddy = this.activeBuddy;
				canvas.id = this.activeBuddy;
				win.title = 'Chatting with: ' + activeBuddy;
				win.stage.addChild(canvas);				 
				canvas.withLabel.text = 'Chatting with: ' + activeBuddy;
				win.width = 348;
				win.height = 310;
				win.activate();				
			}

The code here creates a new NativeWindow, or what the user would simply call a window. First, you set up the options for the window using the options variable. Then, a custom MXML component, ChatCanvas, is instantiated. It is then added to your main window (to an invisible container, hiddenCanvas) and instantly removed.

Note: This seemingly pointless step must be performed, or else the custom component will not appear in your newly created NativeWindow.

If you haven't imported the accompanying sample project, you'll need to add this container to your main MXML file:

<mx:Canvas id="hiddenCanvas" visible="false" />

Next, you pass some information about the selected buddy and the active session to your new window. Finally, you call the new NativeWindow's activate() method, which actually creates the window on the user's screen.

Adding Chat Controls and Sending and Receiving IMs

Let's take a look at the custom component I referred to earlier, ChatCanvas.mxml. You can find it in the accompanying sample project in the src folder. This is the rendered result:

The component contains MXML components for displaying the chat and for typing a message, but it also contains all the ActionScript needed to send and receive messages.

The first function, winInit, is called when the window is created. It gets the current date and inserts it into the conversation box, convoBox. It also adds listeners for sent and received IMs, and adds a keyboard listener that calls a function when the user presses the ENTER key:

private function winInit():void {			
session.addEventListener(IMEvent.IM_RECEIVED, acceptIM);		
session.addEventListener(IMEvent.IM_SEND_RESULT, onIMSendResult);
			var date:Date = new Date();
			var stamp:String = dateStamper.format(date);
			convoBox.htmlText = '<font size="12">' + stamp + '</font>';
			messageInput.addEventListener(KeyboardEvent.KEY_DOWN, keyHandler);
		}

The next function, acceptIM, accepts an incoming message, formats it, and displays it in the conversation box, using the color #4761aa for the sender's name. It also notifies the user by making the taskbar window icon flash orange (in Microsoft Windows):

private function acceptIM(event:IMEvent):void {
			var date:Date = new Date;
			var dateString:String = dateFormatter.format(date);
			convoBox.htmlText += '<font size="12">' + dateString + ' ' + '</font><font color="#4761aa" size="12">' + event.im.sender.displayId + '</font>: <font size="12">' + event.im.message;
			this.parent.stage.nativeWindow.notifyUser(NotificationType.CRITICAL);
		}

The next function, onIMSendResult, is called after an IM is sent. It is very similar to the acceptIM function, except that it first checks to make sure the message was successfully sent. If the statusCode is 200, you know the send was successful, so you can add it to the conversation box. The username also displays in a different color:

protected function onIMSendResult(event:IMEvent):void
		{				
			if(event.statusCode == '200')
			{
			//this is the stuff you say
			var date:Date = new Date;
			var dateString:String = dateFormatter.format(date);
			convoBox.htmlText += '<font size="12pt">' + dateString + ' ' + '</font><font color="#bf4747" size="12pt">' + event.im.sender.displayId + '</font>: <font size="12pt">' + event.im.message + '</font>';
			messageInput.text = '';
			}
		}

The function to actually send an IM is quite simple. The function in your ChatCanvas.mxml component simply takes the user's input from a text field and sends it to the selected buddy, which was passed in when you created your chat window:

private function sendIM():void {
			session.sendIMToBuddy(activeBuddy, messageInput.text, false, true);
		}

These functions, coupled with the MXML controls in your custom component, allow for basic IM functionality: sending and receiving IMs. However, the AIR platform provides tools for many powerful, advanced features. You've already added native OS windows, and used the notification built into the Windows operating system, but now let's explore writing files to the file system.

Creating and Saving HTML Chat Logs

In the ChatCanvas.mxml custom component, beneath the functions I've already covered, you'll find the saveChat() function:

private function saveChat():void {
			file = File.documentsDirectory.resolvePath(activeBuddy.toString() + '.html');
			file.addEventListener(Event.SELECT, fileSelected);
			file.browseForSave("Please choose a name and location for the file:");
		}

This function doesn't actually save the chat, despite its name. It simply prompts the user for a location to save the chat. It also defaults the filename to the active buddy's name with the .html extension. When the location is selected, the fileSelected function is called:

private function fileSelected(e:Event):void {
		    var myFile:File = new File(file.nativePath);
		    var myFileStream:FileStream = new FileStream();
			myFileStream.addEventListener(Event.COMPLETE, completed);
			myFileStream.openAsync(myFile, FileMode.WRITE);
			var fontAdj1:String = convoBox.htmlText.replace(/SIZE="12"/gi,'SIZE="2"');
			var convo:String = fontAdj1.replace(/SIZE="10"/gi,'SIZE="2"');
			var text:String = '<html>' +
			'<body>' +
			convo +
			'</body>' +
			'</html>';
			myFileStream.writeUTFBytes(text);
		}

The first part of this function is fairly straightforward: it creates the file the user specified, and opens it for writing. And, because you are saving the chat as HTML, it seems logical that you could simply write the htmlText of the convoBox text control to the file. However, this will not work properly; the font size will be incorrect if the text is unchanged. To avoid this problem, you can handle the process in three steps:

  1. Pull the htmlText from the convoBox control, and replace all SIZE="12" tags with SIZE="2".
  2. Take the processed text and replace all SIZE="10" tags with SIZE="2".
  3. Place the resulting string within <html> and <body> tags.

Now, the file is written and saved to the specified location. The user can open it in any web browser, and review the conversation while retaining the formatting and timestamps.

Here's the resulting .html file in Firefox:

And here's your custom client chatting with iChat on the Mac:

Where to Go from Here

As you can see, IM becomes even more powerful when you can not only allow people to communicate, but also allow powerful client applications to communicate. The application in this article works fine on its own and is nearly complete, but more importantly, the code can be plugged into a new or existing application with little effort.

Now that you have the basics of working with the Open AIM API, you'll probably want to move on to integrating chat into your own applications. As you explore the API, you'll discover you can add advanced functionality such as:

  • Sending and receiving data IMs (files, images, etc.)
  • Adding buddies and custom buddy groups
  • Sending IMs to multiple recipients
  • Adding typing notification (i.e., "buddyName is typing...")
  • Allowing the user to customize the look and feel of the app (font sizes/colors, background images)
  • Showing an image to represent a buddy in the BuddyList (display an AIM avatar, or let the user pick an image)

For a complete list of objects, events, and methods in the Open AIM API, go to http://code.google.com/p/wimas3 and click the Downloads tab. There, you'll find the most recent documentation. To download the code, click here.