by James Turner
April 3, 2007
When last we left Larry and his llamas, he had just become familiar with the sample AIM Java bot that comes packaged with the SDK. Now that he understands the general idea of how the callback mechanism in the toolkit works, he's ready to hook up his automated analyst to ease the mental burdens of his employees.
As you may recall, all the action to date has occurred in the OnImReceived method, which is required by the AccEvents interface. This method gets called whenever an IM is received by the bot. In the example, the method builds up a collection of regular expressions, and allows them to be applied to incoming text. As written, there is only one set of regular expressions for everyone who uses the bot, so they all pee in the same pool, so to speak.
While this may work for a simple demo program, Larry's needs are a little more complicated. Since each person should appear to be having a separate conversation, it wouldn't do for information from one session to "bleed" into another one. If you've ever used one of the Eliza-like programs, you may have noticed that it sometimes refers back to previous parts of a conversation, as in, "Earlier you mentioned that you hate your mother." We don't want Joe getting messages based on the context of things that Mary had told the bot. It might violate HIPPA, for one thing.
The solution is to have a conversation object that processes the incoming messages and generates a response, and to have a hash map that associates a conversation with each AIM user. That way, each conversation can have its own context. To begin, we create a simple class called Conversation, which we'll give a method called processInput, which takes a string and returns a string. For the moment, we'll just stub it out.
package com.larrysllamas.llamashrink;
public class Conversation {
public String processInput (String input) {
return null;
}
}
With this in place, we continue by gutting out the existing code in the OnImReceived method and the associated variables in the class. We'll save the bit that converts the incoming message to plain text and trims it, but that's all. We add a HashMap to the class that keys on the AccUser (the AIM user account) of the requesting user, and stores a Conversation object for each one.
Now we can flesh out the OnImReceived method. It looks up the AccUser in the participant object that it's passed as part of its signature, and sees if it can find a corresponding conversation in the HashMap. If not, it creates one and adds it to the hash. Once it has a conversation, it calls the processInput method on it with the message as input, and sets the text to send to whatever the method returns. Finally, it sends the response IM.
HashMap<AccUser, Conversation> conversations = new HashMap<AccUser, Conversation>();
public void OnImReceived(AccSession session, AccImSession imSession,
AccParticipant participant, AccIm im) {
try {
String msg = im.getConvertedText("text/plain");
msg = msg.trim();
Conversation conversation = conversations.get(participant.getUser());
if (conversation == null) {
conversation = new Conversation();
conversations.put(participant.getUser(), conversation);
}
im.setText(conversation.processInput(msg));
imSession.sendIm(im);
} catch (AccException e) {
System.out.println("Someone through an AccException with the HR: "+e.errorCode);
e.printStackTrace();
}
}
As you can see, there's not much in the OnImReceived method anymore. All the real action takes place in the processInput method of the conversation. So we need to take that stub we put in the file and put some meat on it. Luckily, we don't have to implement our own Eliza program in Java; the work has already been done for us. Charles Hayden has a nice Eliza available at www.chayden.net/eliza/Eliza.html.
Changing Charles' code to work for us is simply a matter of moving it from the Eliza package he used to com.larrysllamas.llamashrink.eliza and changing the signature on one method (ElizaMain.readScript) to be public, so that we can call it from outside the package. With these changes in place, we can redo our stubbed-out Conversation class.
package com.larrysllamas.llamashrink;
import com.larrysllamas.llamashrink.eliza.ElizaMain;
public class Conversation {
private ElizaMain eliza;
public Conversation () {
eliza = new ElizaMain();
eliza.readScript(true, "script");
}
public String processInput (String input) {
return eliza.processInput(input);
}
}
When the Conversation object is created, the constructor creates an ElizaMain object inside it. The constructor then calls the readScript method to load in an initialization data set for the Eliza program. Obviously, in production code, you'd check to make sure the load succeeded.
As I just mentioned, the Eliza code comes with a script file that's used for configuration; the ElizaMain code tries to open it in whatever the current directory is when the program is started. As you may remember from the previous tutorial, we needed to set this to the lib directory so that the AIM DLL files could load successfully. Thus, we also have to place the script file in the same directory. Now we're ready to fire up our new and improved bot, and get
some problems off our shoulders.

Because we're storing a separate instance of the ElizaMain object for each Conversation, and a separate Conversation for each employee, there won't be any "bleedthrough" of data from one session to another. This is in contrast to the demo bot, which stores all the regular expressions for all the people talking to it in the same objects, so anyone can see all the other user's regular expressions. Which approach you take will depend on the particular bot you're writing, of course.
Sending the Bill and Spying on the Employees
Larry's decided to take things one step further, and actually bill his employees for their sessions with the llamapsych. Ah that Larry, never one to let a profit center slip by untapped. When the employee says goodbye, the bot should send a bill automatically to the victim employee at the other end.
Let's assume there's already a bill sitting in the same lib directory as the script and the DLLs. For purposes of this example, I've created a simple PDF document, called bill.pdf. What changes do we need to make to send it? Believe it or not, you can do it with just three additional lines of code in the OnImReceived method:
im.setText(conversation.processInput(msg));
imSession.sendIm(im);
if ("goodbye".equalsIgnoreCase(msg)) {
AccFileXferSession fileXferSession =
session.getFileXferManager().send(partName, "bill.pdf",
"Your bill for this session.", null);
}
The arguments to session.getFileXferManager().send are, respectively, the screen name to send to, the file to send, the message to associate with the file transfer, and any flags for the transfer (which we can leave null). Now, when an employee says goodbye, they'll be presented with their bill.

Larry has decided to take spying on his employees to the next level, and secretly monitor when they log in to AIM and when they leave. To do this, he needs to add anyone who ever uses the llamapsych to its buddy list, and then use some of the event handlers that are available in the AccEvents interface to watch comings and goings. To begin with, he needs to make sure that once an employee has talked to the llamapsych, that employee will appear in the bot's buddy list. That is because the bot only receives notifications for users who have been added to the list. This makes sense, because otherwise bots would have to receive notifications for every user in AIM, which might create just a bit of a performance issue.
To add the incoming user to the buddy list, you need to use this snippet of code at the top of OnImReceived:
String partName = participant.getName();
AccBuddyList list = session.getBuddyList();
AccUser user = list.getBuddyByName(partName);
if (user == null) {
AccGroup group = list.getGroupByName("Employees");
if (group == null) {
group = list.insertGroup(new AccVariant("Employees"), -1);
}
group.insertBuddy(partName, -1);
}
The first thing the code does is to get the buddy list for the bot. If the employee is already in the buddy list, we don't need to do anything. However, if not, we're going to store all the employees under a buddy list group called Employees, coincidentally enough. Since the first time the bot starts up, this group may not exist, we first have to check to see if it's there using getGroupByName. If not, we need to use AccBuddyList.insertGroup to add the group. The two arguments to the method are the name of the group and the position in which to add it. The group name is passed in as an AccVariant, which is essentially a catch-all object that can hold a string, a number, a Boolean, etc. We can make one with the group name as the argument to the constructor, and we're all set. Using a position of -1 means "put it at the end of the group list."
With the group in place, we can now just use AccGroup.insertBuddy to add our employee's screen name, again using -1 to say "put them at the end." Now that the employee is on the buddy list, the event method OnUserChange will be called whenever the status of any employee in the buddy list changes. Our version of that method looks like this:
public void OnUserChange(AccSession arg0, AccUser arg1, AccUser arg2,
AccUserProp arg3, AccResult arg4) {
try {
AccUserState newState = arg2.getState();
if (arg1.getState() != newState) {
if (newState == AccUserState.Away) {
System.out.println(arg2.getName() + " is now away");
}
if (newState == AccUserState.Idle) {
System.out.println(arg2.getName() + " is now idle");
}
if (newState == AccUserState.Online) {
if (arg1.getState() == AccUserState.Offline) {
System.out.println(arg2.getName() + " has come online");
} else if (arg1.getState() == AccUserState.Unknown) {
System.out.println(arg2.getName() + " was already online");
} else {
System.out.println(arg2.getName() + " has returned");
}
}
if (newState == AccUserState.Offline) {
System.out.println(arg2.getName() + " has gone offline");
}
}
} catch (AccException e) {
e.printStackTrace();
}
}
OnUserChange gets called for a number of reasons, including a change to name formatting, but we're only interested in state changes. The second and third arguments to the method are snapshots of the previous version of user, and their new version. We can check to see if the user state has changed, and simply ignore things if it hasn't. If it has, we can use the old state and new state (both of which are enums) to figure out what has changed. Most of the state types are self-explanatory, except for AccUserState.Unknown, which is the state that any online users will have as their "old" state when the bot first starts up.
With this new code in place, Larry can stalk his employees with ease:
ConnectingACC_S_OK ValidatingACC_S_OK TransferringACC_S_OK NegotiatingACC_S_OK StartingACC_S_OK llamapsych was already online OnlineACC_S_OK <user> was already online <user> is now away <user> is now away <user> has gone offline <user> has come online
Conclusion
Larry is ecstatic. He's saved a bundle on health care costs, has bilked his employees out of some money, and is watching them like a bloodhound. This, of course, is an extreme example of how a bot can be misused. But it's just common sense to remember that anyone, bot or human, can track your online status once you have let them put you in their buddy list. It's also important to note that Larry has violated the terms of the AOL developer license with this bot, because it is required to send a privacy policy to the user when requested.
In the final installment of this series on the AIM Java SDK, we'll watch Larry create his own llama-branded version of an AIM client, using SWT. This will give us a chance to see what a few of the other events in the AccEvent interface do. A llama-branded chat client--one can only imagine the horror.
Resources
-
llamashrink.zip: The source code
References
- "Creating AIM-Enabled Applications in Java, Part 1," James Turner, AOL Developer Network, February 28, 2007
- www.chayden.net/eliza/Eliza.html: Charles Hayden's Eliza
