Creating AIM-Enabled Applications in Java, Part 1

AIM-Enabled Applications in Java

by James Turner
February 28, 2007

Instant messaging is one of the most convenient ways to communicate between an end user and an application. For one thing, most people already have an IM account, just as they have an email account. In addition, IM clients are available for a variety of platforms, from cell phones all the way up to high-end desktop computers. Using the AIM Java SDK, it's both simple and quick to create applications that can be accessed via the AOL instant messaging infrastructure. In this series of articles, we'll look at how to create two types of AIM applications in Java: bots and full instant-messaging clients.

In many ways, bots are the simpler of the two approaches, so it's with bots that we'll start. In this first article, we'll look at how to set up the Eclipse IDE to develop using the AIM SDK, and then take a look at the accjbot application that comes packaged with the SDK. Most of what will be discussed here is not specific to Eclipse, but it makes a good commonly used sample platform to demonstrate on.

If you haven't read my tutorial on developing AIM-enabled websites, you may not be familiar with Larry and his llama store. Well, as a result of adding AIM presence support to his web store, he's now doing a huge volume in llamas. So much so that his sales staff is starting to crack under the strain. So Larry, always mindful of keeping his employees happy, is instituting a mental health program. Unfortunately, Larry is too cheap to spring for a real mental health professional, so he's gotten his hands on a copy of the old Eliza "therapy" program, written in Java, and wants to hook it up as a bot. He figures that after a long day shoveling llama poop, they'll be too wasted to notice the difference.

Larry has already registered llamapsych as a new AIM screen name. (Amazingly, llamashrink had actually been taken already; the field of llama mental health is evidently a rich one.) But before he uses it for his bot, he needs to convert it to a bot by going to developer.aim.com/bot.

AIM Bot Page

On the following page, Larry registers the bot as an AIM developer.

Registering the Bot

Finally, there's one last screen to agree to. It's worth reading the license agreement for bots, because among other things, you'll learn that a bot can't send more than 10,000 messages a day or 150,000 messages a month. You also can't have more than 10,000 users actively showing you as online in their buddy list at any one time. Anything beyond that requires a contract (and probably money) to be negotiated with AOL. You also can't use a bot to advertise alcohol, gambling, "adult" sites, or other IM products. Finally, if a user enters the words "privacy" or "privacy policy," the bot needs to provide a link to a web page with the privacy policy that has been implemented for the bot.

Final Bot Screen

Upon agreeing to these terms, Larry is delivered at last to the final screen, which informs him (for the third time) that bots can not initiate conversations, and gives him a link to get a client key, which he'll need to connect his bot to the AIM servers.

Larry's Been Botted

Clicking "Get a Custom Client Key" brings him to the custom client page (which we won't include for space). From here, he clicks Get Started, which brings him to yet another screen looking for information and requesting another license agreement (this time for the customer client software.)

Yet Another License

After clicking agreement to this page, Larry at long last comes to the key management page, and the license key for his bot. What Larry has been issued is a development key. What is a development key? It's a key with enough horsepower to develop an application, but not much else. A developer key allows 500 total logins per day. For a bot, this won't be an issue, since it pretty much logs in once and stays up. However, if you were developing an application that wanted to integrate AIM client functionality inside it, you'd need to step up to a deployment license. We'll talk about how to get a deployment license, and the fingerprinting process that goes along with it, in part three of this tutorial.

Finally, a Key

Larry tucks away that key (in this case, "ll1Hb2FhVt0SYgtu") somewhere safe for later; for the moment he needs to get the Java API and get his development environment set up. The Java API is delivered as a JAR file called accjwrap.jar, which acts as a wrapper around a native implementation of the API. At the moment, there are native implementations for Windows, Linux (RH Enterprise 4), OS X, and the Pocket PC. For the moment, we'll restrict ourselves to the Windows version; we'll look at the Linux implementation in Part 3. The APIs can be downloaded by clicking on the appropriate link for an SDK under the newly created key. In the case of the Windows SDK, you'll end up with a ZIP file that has a directory structure like this:

Top level structure of SDK zip

Underneath the dist directory, you'll find a release directory, and underneath that you'll find this directory:

Release level of SDK Zip

Let's keep this WinZip window open for the moment while we set up our IDE. Like many Java developers, Larry uses the Eclipse IDE to do his work in Java. For the purposes of this tutorial, he'll be using the 3.2.1 Windows version. To begin with, Larry creates a new plain old Java project called llamashrink, specifying a lib directory on the project search path and src and bin directories for the Java sources and class files.

Creating the Eclipse Project

Now Larry is ready to unpack the DLLs and JAR file that he'll need to build and run his bot. He tells WinZip to unpack all the files in the release directory of the ZIP file into his newly created lib directory in his workspace. Technically, there are files like the Windows executables that aren't needed to develop a Java application, but it doesn't hurt to unpack them. After he's done, his project looks like this:

Eclipse with library unpacked

Going into the project properties, Larry can now add accjwrap.jar to the class path of the application he's going to be building:

Modifying the Build Path

Now Larry is finally ready to start writing some code. But rather than starting from scratch, he can leverage an existing sample application included in the SDK. Under the samples directory of the ZIP file is a bunch of sample applications, including one called accjbot. As you might guess, this is a Java implementation of an AIM bot. There are four files in that directory: an Ant build.xml, a readme, and the two Larry actually wants, Pref.java and accjbot.java. He unpacks those two into the src directory of his llamashrink project. Now his project looks like this:

Eclipse with samples

Before Larry goes any further, there are a few things to fix. First off, it's bad Java practice to have an all-lowercase class name, and Larry wants to call his bot something different anyway. So he uses the Eclipse refactoring facility to rename the class LlamaShrinkMain:

Renaming the Class

The other problem is that both classes currently reside in the Java default package, which causes all sorts of problems in JDK1.5, which is the version Larry has chosen to work in. So he moves both classes with another refactor into the com.larrysllamas.llamashrink package. When he's done, his project looks like this:

Final Unpacked Project

What's actually in these two classes? Let's start with Prefs.java, since it's the easiest. When a client connects to the AIM server, a bunch of preferences for the user are sent up to tell the server how the screen name is configured. The Java API requires that an AccSession (which is what is used to provision the server connection) provide an object that extends AccPreferencesHook. All the Prefs class does is implement such an object, which can be used to store preferences to hand up to the server.

The LlamaShrinkMain class (formerly accjbot.java) is more interesting. Before we get into the code in detail, let's fire it up and see what it does. Using the "Run..." menu item, we can create a run profile for the LlamaShrinkMain class.

Initial Run Profile

When we actually run it, however, it immediately exits with the message: "usage: java accjbot username password." We must have to give a username and password on the command line, so we return to the run profile and add a pair of arguments:

Adding arguments

But once again, our desires are frustrated, as the program errors out with the stack trace:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no accjwrap in java.library.path
    at java.lang.ClassLoader.loadLibrary(Unknown Source)
    at java.lang.Runtime.loadLibrary0(Unknown Source)
    at java.lang.System.loadLibrary(Unknown Source)
    at com.aol.acc.AccSession.<clinit>(AccSession.java:13)
    at com.larrysllamas.llamashrink.LlamaShrinkMain.<init>(LlamaShrinkMain.java:78)
    at com.larrysllamas.llamashrink.LlamaShrinkMain.main(LlamaShrinkMain.java:68)

What gives? Well, the way the Java API is written, it's looking for those pesky DLL files and not finding them. You would think there are two ways to fix this: you can either set the working directory to the lib directory, so that it will find them in the working directory, or you can set up the java.library.path environment variable. Unfortunately, the accjwrap.dll library tries to load dependent libraries, and it only looks in the current working directory, so you have to choose plan A, and set the program to run with lib as its working directory:

Setting the CWD

Now, when you fire up the application, it works fine. If you look on the console for the application, you'll see some diagnostic output:

ConnectingACC_S_OK
ValidatingACC_S_OK
TransferringACC_S_OK
NegotiatingACC_S_OK
StartingACC_S_OK
OnlineACC_S_OK

That all looks hopeful, so we can add llamapsych to our buddy list, and sure enough, llamapsych has shown up as a logged-in screen name. If we open a chat window to the bot, we can see what it does:

Stop Repeating Me

Well, isn't that exciting. A quick peer into the source code shows that the words "stats" and "help" are supposed to do something, let's see what they do:

Ooops, That's Not Right

Hmm, seems like we have a bug on our hands. I guess it's time to dive into the code for real. The top of the file has the main method:

public static void main(String[] args) {

    if(args.length != 2) {
        System.out.print("usage: java accjbot username password");
        return;
    }
    
    try {
        new LlamaShrinkMain(args[0], args[1]);
    } catch (AccException e) {
        System.out.println("Main AccException, hr: "+e.errorCode);
        e.printStackTrace();
    }
}

Not much to see here; it just takes the two arguments that are handed in and passes them to a new instance of the class. We can presume from this that the instantiator method contains the actual implementation of the bot. In passing, we can also make a note to change the print statement to print the right class name, since we renamed it from accjbot.

Moving down, we find the previously mentioned instantiator method:

public LlamaShrinkMain(String username, String password) throws AccException
    { 
        // Create main session object
        session = new AccSession();
		
        // Add event listener
        session.setEventListener(this);
		
        // set key
        AccClientInfo info = session.getClientInfo();
        info.setDescription(key);
		
        // set screen name
        session.setIdentity(username);
		
        // setup prefs so anyone can IM us, but not chats or DIMs
        session.setPrefsHook(new Prefs());
        AccPreferences prefs = session.getPrefs();
        prefs.setValue("aimcc.im.chat.permissions.buddies", AccPermissions.RejectAll);
        prefs.setValue("aimcc.im.chat.permissions.nonBuddies", AccPermissions.RejectAll);
        prefs.setValue("aimcc.im.direct.permissions.buddies", AccPermissions.RejectAll);
        prefs.setValue("aimcc.im.direct.permissions.nonBuddies", AccPermissions.RejectAll);
        prefs.setValue("aimcc.im.standard.permissions.buddies", AccPermissions.AcceptAll);
        prefs.setValue("aimcc.im.standard.permissions.nonBuddies", AccPermissions.AcceptAll);
	
        session.signOn(password);

        //msg pump
        while( running ) 
        {
            try {
                AccSession.pump(50);
            } catch (Exception e) {
                e.printStackTrace();
            }
	    	
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

         }
	    
         info = null;
         session = null;
         prefs = null;
        
         System.gc();
         System.runFinalization();
    }

We'll look at some of this stuff in more detail later, but for the moment it looks like the method creates a new AccSession object; provisions it with this class as its event handler; sets up the username and password for the screen name of the bot; sets the preferences to reject chats and direct connections, but permit standard connections from buddies and non-buddies alike; and then fires up a while loop that alternates between running the AccSession.pump method and sleeping.

One thing to look at is this line:

 info.setDescription(key);

What is key? If you look higher up in the file, you'll see:

 String key = "accjbot (Key:ju1lyCD_aTaZybdU)";

Ah, this must be where the mysterious bot key that Larry requested pages and pages ago must go. As it turns out, you can run the application just fine with the key as configured in the sample; it's a real key. But let's configure it correctly:

 String key = "llamapsych (Key:ll1Hb2FhVt0SYgtu)";

We can guess, based on the fact that the class was set as an event listener for the session, that the API expects our class to act as an event handler for the session. And, in fact, if you look at the class declaration for LlamaShrinkMain, you'll see public class LlamaShrinkMain implements AccEvents. AccEvents requires our class to implement a ton of event handling callbacks, most of which are stubbed out in the sample file. The important one to notice is:

public void OnImReceived(AccSession session, AccImSession imSession, AccParticipant participant, AccIm im) {
    try { 
			
        String richText = im.getConvertedText("application/xhtml+xml");
        String msg = im.getConvertedText("text/plain");
        String errors = "";

        if(msg.equals("stats"))
        {
            long diff = (new Date()).getTime() - start;
            diff /= 1000; // convert to seconds
            long days = diff/86400;
            diff = diff - 86400*days;
            long hours = diff/3600;
            diff = diff - 3600*hours;
            long minutes = diff/60;
				
            richText = "Running for "+days+" day"+ ((days!=1)?"s":"")+
            ", "+hours+" hour"+((hours!=1)?"s":"")+", "+
            minutes+" minute"+((minutes!=1)?"s":"")+".";
        }
        else if(msg.startsWith("list"))
        {
            richText = "List of regexps:<br>";
            for(String regexp : regexps)
            {
                richText += regexp+"<br>";
            }
        }
        else if(msg.startsWith("remove"))
        {
            int i = msg.indexOf(" ");
            if(i != -1)
            {
                String message = msg.substring(i+1);
                if(regexps.remove(message))
                {
                    richText = "Removed "+message+" from the list.";
                }
                else
                {
                    richText = message+" not found in the list, try list to see what exists.";
                }
            }

        }
        else if(msg.startsWith("add"))
        {
            int i = msg.indexOf(" ");
            if(i != -1)
            {
                String message = msg.substring(i+1);
                regexps.remove(message);
                regexps.add(message);
                richText = "Added "+message+" to list.";
            }
        }
        else if(msg.startsWith("help"))
        {
            richText = "help<br>list<br>add<br>remove<br>quit";
        }
        else if(msg.startsWith("quit"))
        {
            int i = msg.indexOf(" ");
            if(i != -1)
            {
                String message = msg.substring(i+1);
                if(message.equals(signOffPassword))
                {
                    session.signOff();
                    return;
                }
            }
				
            richText = "uh uh uh! you didn't say the magic word...";

        }
        else
        {
            // respond with regexps
            for(String regexp : regexps)
            {
                String[] regexpArr = regexp.split(",");
                if(regexpArr.length != 2)
                {
                    errors += "<br>"+regexp+
                       " is not a properly formatted regexp, use the format regexp,replacement";
						
                }
                else
                {
                    try {
                        richText = richText.replaceAll(regexpArr[0], regexpArr[1]);
                    } catch (PatternSyntaxException ex) {
                        errors += "<br>"++regexpArr[0]+", "+regexpArr[1]+
                           ", textRegex does not contain a valid regular expression" + 
                           ex.getDescription();
                    } catch (IllegalArgumentException ex) {
                        errors += "<br>"++regexpArr[0]+", "+regexpArr[1]+
                           ", textReplace contains inappropriate dollar signs" + 
                           ex.getMessage();
                    } catch (IndexOutOfBoundsException ex) {
                        errors += "<br>"++regexpArr[0]+", "+regexpArr[1]+
                           ", textReplace contains a backreference that does not exist " +
                           "(e.g. $4 if there are only three groups)" + ex.getMessage();
                    } catch (Exception ex) {
                        errors += "<br>"++regexpArr[0]+", "+regexpArr[1]+", " + ex.getMessage();
                    }
                }
            }
				
        }
        if(running)
        {
            im.setText(richText + errors);
            imSession.sendIm(im);
        }
		
        } catch (AccException e) {
			System.out.println("Someone through an AccException with the HR: "+e.errorCode);
			e.printStackTrace();
        }
    }

There are a couple of things in here that make an experienced Java developer cringe, but the main one is embedded carriage returns in strings. Ah well, this code isn't sticking around for long anyway. It looks like the method reads in the received message as rich text and as a plain string. It tries to match the plain string against a series of commands, which seem to involve adding or removing regular expressions from a list. If it doesn't match any of the commands, it applies the regular expressions already added to the string that was sent. In any event, it uses im.setText and imSession.sendIm to send a reply. Why don't the commands work? If we debug and put a breakpoint on the line right after msg is set, we can see in an instant:

Extra Space, Oops

If you look closely, you can see that there's a space prepending before the word "stats" in msg. That's not going to work, is it? Looks like imGetConvertedText("text/plain") is adding a space to the start. Well, we can fix that in a jiffy by adding the line:

 msg = msg.trim();

Now, when we fire the bot up again, things work much more smoothly:

Now It Works

Cool. To finish up, let's actually try the regular expression matching, even though we're going to throw it away.

Working regexps

Outstanding. So, to sum up, Larry has now created an AIM bot screen name, gotten a development key for it, set up Eclipse to run the sample bot, and even corrected a bug in the program. In the next tutorial, Larry will teach his bot to act like a bad therapist, send files, and monitor its AIM buddy list.

Nice

Bst Rgds,
Michael B.

hello

i am on can enter in chat

Exception

Exception in thread "main" java.lang.UnsatisfiedLinkError: C:\Program Files (x86)\Java\jdk1.6.0_05\bin\accjwrap.dll: Can't find dependent libraries
at java.lang.ClassLoader$NativeLibrary.load(Native Method)
at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1751)
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1676)
at java.lang.Runtime.loadLibrary0(Runtime.java:823)
at java.lang.System.loadLibrary(System.java:1030)
at com.aol.acc.AccSession.(AccSession.java:13)
at com.larrysllamas.llamashrink.AccjBot.(AccjBot.java:35)
at com.larrysllamas.llamashrink.AccjBot.main(AccjBot.java:25)
The vision I use is vista.I do the case follow you,but it can't work except for xp.
answer me:huanghaiwangyi@sina.com
thank you very much!

Servlet development

Hi there,

I'm trying to convert your bot into a servlet that runs with Tomcat. Although I could get your sample application to run correctly, I'm having some problems deploying it as a servlet. The problems seem to be related to the location where I'm supposed to place the Windows DLL files. I've have tried many locations (inside Tomcat's common/lib directory or JDK's jre/lib, etc.) but I always seen to be getting the following error :

- StandardWrapper.Throwable
java.lang.UnsatisfiedLinkError: no accjwrap in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1682)
at java.lang.Runtime.loadLibrary0(Runtime.java:822)
at java.lang.System.loadLibrary(System.java:993)
at com.aol.acc.AccSession.(AccSession.java:13)
:
:

It seems that the file 'accjwrap.jar' was loaded properly, but it can't find the 'accjwrap.dll' ??

Do you have any suggestions?? Thanks for any help you can provide...

Cheers
J Heng

It's been over a year since you posted this

However, can I ask if you ever solved it?

I solved this problem today, but immediately fell afoul of another one.

First the solution to your problem:

See this message thread which points to the solution - basically you have to define java.library.path in such a way so that Tomcat can see it and this thread shows how.

That, unfortunately, was the easy part. The hard part is figuring out why the bot cannot log in to AIM, as I explain in
this thread.

I am trying to get as much visibility as possible on this issue.