Task Notification with Google Talk via XMPP

Internally here at Delphic Sage, we use an in-house built task system for assigning bugs or tasks, and to generally get stuff done and / or keep track of it. It has your basic functionality… create and assign tasks, change the status or resolution, add comments. It has become slightly more advanced when we integrated Basecamp into it. It’s getting there…

When an employee is assigned a task, they would not know about it unless they had the task system open and were refreshing their list. To alleviate this, and to help Project Managers keep their sanity, we developed a task notification system built on AOL’s IM protocol, using a library found on the internet. This library proved to not be reliable. It would lose connection to AIM and it wouldn’t report it to the application, so the application would be happily sending task notifications into oblivion, marking them as sent in the database. Luckily, it’s not mission critical! Employees will still have the tasks assigned to them, whether or not they get a notification.

In an effort to move toward a more reliable system, we decided to give XMPP and Google Talk a whirl. XMPP (eXtensible Messaging and Presence Protocol) is a standard implemented by many servers and has many different clients supporting it. Since our email here at Delphic Sage is hosted by Google, and every google account comes with a Google Talk account, it would seem to be a logical move. It would also make it easier for future employees to start receiving task notifications immediately. There are some people without AIM accounts, if you can believe it. Read on about our solution…

In searching for a library to build our notification application on, we weren’t too picky, but some libraries claim to be libraries (in the sense that I think about libraries as methods to manipulate data), but they also have UIs built in. We aren’t running a UI for this. It simply makes calls to the database, and sends messages to the user, as a Windows Service. However, we (and I’m speaking about everyone, not just Delphic Sage and not just developers) are very lucky that the agsXMPP SDK exists. It has UI elements though, but it is generally one of the best structured libraries I’ve seen. And it works beautifully. Alex is the maintainer and is always answering posts on the forums and helping everyone get their XMPP applications up and running. To the point where I’ve seen new questions posted directly to Alex, instead of something along the lines of “Can someone help me?”.

Let’s take a look at how to implement a basic task notification system using the agsXMPP library.

So after I removed all the old code for the AIM client, and deleted that cursed dll, I had a pretty empty file.

public Worker() {
this.Running = true;
this.Connected = false;
}

public void Run() {
try {
while (this.Running){
try {
if (this.Connected) {
TaskIMNotificationList notProcessed = //load unsent messages from the database
foreach (TaskIMNotification imNotification in notProcessed) {
}
}
}
catch {...}

Thread.Sleep(60000); // check for new messages every minute
}
} catch { ... }
}

I then started adding the XMPP stuff. Generally there are a bunch of events that you want to listen for, pretty much in order to show status messages to your user in a UI environment, but in our case, we’re just dumping status messages to the event log.

Here’s the basic initialization/configuration of our XMPP client. I did these in the constructor of my Worker class:

client = new XmppClientConnection();
client.SocketConnectionType = agsXMPP.net.SocketConnectionType.Direct;
client.ConnectServer = "talk.google.com";
client.Port = 5222;
client.UseStartTLS = true;
client.AutoResolveConnectServer = false;
client.Show = ShowType.chat;
client.Server = "delphicsage.com";

And we are listening for the following events:

client.OnClose += new ObjectHandler(client_OnClose);
client.OnLogin += new ObjectHandler(client_OnLogin);
client.OnXmppConnectionStateChanged += new XmppConnectionStateHandler(client_OnXmppConnectionStateChanged);
client.OnError += new ErrorHandler(client_OnError);
client.OnAuthError += new XmppElementHandler(client_OnAuthError);
client.OnBinded += new ObjectHandler(client_OnBinded);
client.OnRosterItem += new XmppClientConnection.RosterHandler(client_OnRosterItem);
client.OnPresence += new agsXMPP.protocol.client.PresenceHandler(client_OnPresence);
client.OnMessage += new agsXMPP.protocol.client.MessageHandler(client_OnMessage);

We have a list of online users, and a list of contacts:

this.online = new List();
this.contacts = new List();

And finally I call SignOn(). We’ll take a look at some of these event handlers. First OnLogin:

void client_OnLogin(object sender) {
this.Connected = true;
System.Diagnostics.EventLog.WriteEntry("TaskTron9000", "Logged in successfully.", EventLogEntryType.SuccessAudit);
}

There are a few connection state changes between the SignOn call and when the OnLogin event is raised. We only set our Connected property to true when we receive the login event, and our application can start sending messages. Some other connection states are “Connecting”, “Connected”, “Authorizing”, “Authorized”. Luckily the agsXMPP library has the Login event so we don’t have to try to figure out what all of the other connection states mean!

An important event is the OnPresence event. This gets raised when a user goes online or offline, when they “Subscribe” to you (meaning, add a contact), as well as for other presense queries / changes, like unsubscribe, become available, go away. Here’s how we handle it:

void client_OnPresence(object sender, agsXMPP.protocol.client.Presence pres) {
if (pres.Type == PresenceType.subscribe)
client.PresenceManager.ApproveSubscriptionRequest(pres.From);

Jid jid = pres.From;
string username = jid.User + "@" + jid.Server;
if (pres.Type == PresenceType.available)
{
if (!this.online.Contains(username))
this.online.Add(username);
}
else if (pres.Type == PresenceType.unavailable || pres.Type == PresenceType.unsubscribe)
this.online.Remove(username);
}

The main idea here is, when we receive a subscription request, we approve it. When someone is available, we add them to our list of online users of who we can send messages to, and conversely remove them in other instances. The Presence event is raised many times. There’s also a “probe” presence, so someone’s XMPP client would be checking if our user is online, and we would likely be sending a Presence object back.

OnRosterItem is called at the beginning for each contact you have in your contacts list. Also, when a new user is subscribed to. We will add to our contacts list:

void client_OnRosterItem(object sender, RosterItem item) {
Jid jid = item.Jid;
string username = jid.User + "@" + jid.Server;
if (!this.contacts.Contains(username))
this.contacts.Add(username);
}

Our last event is just having a little fun. Since the original name of our program is the TaskTron9000, and we generally branded it as HAL9000 from 2001: A Space Odyssey, our OnMessage event will just send a user a HAL9000 quote. It’s just hard coded…

void client_OnMessage(object sender, agsXMPP.protocol.client.Message msg) {
agsXMPP.protocol.client.Message message = new agsXMPP.protocol.client.Message(msg.From);
if (msg.Type == MessageType.chat && !string.IsNullOrEmpty(msg.Body)){
message.Body = “This mission is too important for me to allow you to jeopardize it.”;
client.Send(message);
}
}

Finally, we can take a look at the main loop of the program, sending messages.

while (this.Running) {
try {
if (this.Connected) {
TaskIMNotificationList notProcessed = // load unsent messages
foreach (TaskIMNotification imNotification in notProcessed) {
Jid jid = new Jid(imNotification.SendToUsername);
if (!this.online.Contains(imNotification.SendToUsername)) {
if (!this.contacts.Contains(imNotification.SendToUsername)) {
client.PresenceManager.Subscribe(jid);
client.RosterManager.AddRosterItem(jid);
}
}
else {
agsXMPP.protocol.client.Message message = new agsXMPP.protocol.client.Message(jid);
message.Body = imNotification.Message;
client.Send(message);

imNotification.SentIndicator = true;
imNotification.Save();
}
}
}
} catch { ... }

Thread.Sleep(60000);
}

SignOff();

You can find a Visual Studio 2008 Solution with all of the code used in our project here.

« Prev Article
Next Article »