OpenP2P.com    
 Published on OpenP2P.com (http://www.openp2p.com/)
 http://www.openp2p.com/pub/a/p2p/2001/04/13/jabber.html
 See this if you're having trouble printing code examples


Fun with Jabber

A More Sensitive Mail Notifier

04/13/2001

Related Articles:

Incoming Mail Notifier Using Jabber

The Jabber Jihad: Universal Instant Messaging With audio

Jabber Works: Here's How

XML Messaging With Jabber

Search O'Reilly Net for "jabber"

Instant Messaging topic area


More from the OpenP2P.com

In the first article in this series, Incoming Mail Notifier, we looked at a simple script that parsed an e-mail message, connected to a Jabber server and sent a message to a Jabber client. That article ended with a couple of questions:

The first one is straightforward to answer: Jabber is a store-and-forward system. That means that if the intended recipient of a message is unreachable (whether the recipient is registered on the same Jabber server as the sender or on a different Jabber server) the Jabber server will store the message and send it the next time the recipient connects.

So our existing notify script can happily pump out messages destined for its recipient without worrying whether they are connected or whether the message will get through that instant. In other words, we're creating a situation where the recipient could receive a mass of notifications when logging on after a long absence; the sheer number of messages would make this experience less useful than originally intended. This is one flavor of insensitivity that we want to address.

Even if the user is connected, it might not always be appropriate to send messages. The user might be away from the computer for an extended period (even though the workstation is on and the Jabber client connected) or might simply not want to be interrupted (although as we've already discussed, on a Jabber client that supports them, arrival of "normal" type messages can be fairly unobtrusive).

As it stands, our script will send notifications whether the recipient wants them or not. It turns out that we can address this and the other flavors of insensitivity at the same time.

Jabber Presence

To address the issues described, we need the script to become sensitive to presence. Presence is a Jabber term that encompasses the connectedness and availability of a Jabber user (or more precisely, of a Jabber ID, or JID).

When you connect, your presence is set by default to "normal," which means "online and available." In a similar way that "chat" type messages are related to "normal" type messages (see the Incoming Mail Notifier"Jabber and Message Types" section of the first article, Incoming Mail Notifier for more details), "normal" presence has a sister, "chat," which means the user is free to chat.

At the moment, some Jabber clients don't support the distinction between "normal" and "chat," but these two similar types (both "available" in the widest sense) will lend themselves to allowing clients to negotiate connectivity and availability on a slightly finer level than simply "are you available or not?"

Presence Types

Jabber defines five presence options:

We've mentioned the two "available" presences already - Normal and Chat; the other three - Away, Extended Away and Do Not Disturb - represent the opposite in a rough sense: "not available (although still connected)."

Although your presence is set for you by default to "normal" when you start your Jabber client and connect to a Jabber server, you can change your presence information. Figure 1 shows a popup window from WinJab that allows me to change my presence setting.

WinJab's Presence settings Figure 1: WinJab's Presence settings  

Show and Status

The "Show Me As" box contains the choice of the five presence settings. Notice also the space for a status message. Presence is actually made of two parts: a mandatory "show" setting (that is, the presence status) and an optional "status" setting, which can carry some text that could be interpreted and used by a Jabber client so that my colleagues could, for example, know that the reason I had set my presence to Extended Away was the fact that I'd gone to lunch.

Presence XML



DJ Adams is a featured speaker at the upcoming O'Reilly Open Source Convention in San Diego, CA, July 23 - 27, 2001. Take this opportunity to rub elbows with open source leaders while relaxing in the beautiful setting of the beach-front Sheraton San Diego Hotel and Marina. For more information, visit our conference home page. You can register online.



We know that XML underlies the Jabber architecture. In the first article we learned about message containers - structures that convey message types such as "normal" and "chat." These structures are XML. There is a Jabber XML structure that conveys presence information, too. The lunch example above would look like this:

<presence>
  <status>Gone for a pub lunch</status>
  <show>xa</show>
</presence>

In other words, the mandatory section is the <show/> element and the optional section is the <status/> section. Actually, the mandatory section isn't really mandatory, as the simplest presence XML message is this:

<presence/>

which represents "normal" presence (show) with no specific status message. As you might have guessed, this is the presence message that is sent by your client to the Jabber server when you start it and connect. And it is the "normal" presence that is taken as default if no show is specified. (This is why there's no keyword in brackets for the Normal presence above.)

When you change your presence (as with the WinJab popup shown in Figure 1), a new presence message is sent to the Jabber server.

There is actually another presence type, "unavailable," which is set by the Jabber server if you disconnect.

Presence Subscription

"Set by the Jabber server - for whose benefit?" you might ask. Indeed. We've only really been talking about sending presence information from the client to the server. But it's not much use if it just sits there - and judging from the little icons changing as different users log on and off, it seems obvious that presence information is meant to be sent to other clients.

Presence information is set, and sent to the Jabber server, to be distributed to certain other Jabber users. But it's not a free-for-all. For two big reasons:

So Jabber has the concept of presence subscription, not unlike AOL Instant Messenger's Buddy Lists. If fred@yak wants to know when jim@yak comes online, or otherwise changes his presence, fred@yak must subscribe to jim@yak's presence.

Request and Response

The presence subscription takes the form of a request and a response (from the client's perspective). fred@yak adds jim@yak to his user list (the Jabber term is Roster) and at the same time a presence subscription request is sent from fred@yak to jim@yak. This request is sent via the Jabber server. The request says:

"Hi, I'm fred@yak, and I'd like to be on the list of people that the Jabber server relays your presence to when you set or change it; can I be, please?"

jim@yak can reply yes or no. Usually, when proffering such a request to jim, jim's Jabber client will offer him the chance not only to say yes or no, but also to add fred@yak to his roster and send a presence subscription request to fred@yak, if he isn't already there.

The E-mail Notification Example

notify@yak's roster in JabberIM Figure 2: notify@yak's roster in JabberIM

To recap, who are the Jabber IDs that are partaking in our e-mail notification script example? Well, there's me - dj@yak - and I want to receive the notifications, and there's the notify.pl script, which runs as user notify@yak. I mentioned in the first article that for speed I created the notify@yak JID for notify.pl to use. I used a normal Jabber client (JabberIM) to create a new account on the Jabber server on yak.

To make the notification script "sensitive" to dj@yak's availability, notify@yak needs to add dj@yak to its roster and send a presence subscription request to dj@yak, which dj@yak must approve. Again, for speed, I have done this manually using a normal Jabber client (as opposed to coding something in Perl to use Net::Jabber for a one-off exercise), and when connected to the Jabber server with the JabberIM client, the notify@yak's roster looks like this:

"dj" (which in this case is the nickname for dj@yak) is shown in the list of users, which means that dj@yak has approved a presence subscription request from notify@yak and is now listed in the roster. This is the subscription state that we need to have for notify@yak (relating to dj@yak, or whichever JID is to be the recipient of the notifications) for the sensitivity to work.

Modifications to the Script

The version of the (insensitive) script from the first article is here.

What we need to do is check for the dj@yak's presence before deciding whether to create and send a message. If dj@yak is "available," which we will define in this example as having a presence (Show) of "normal" or "chat," then we will send the notification. Otherwise, we won't.

Sending Our Presence

Before the Jabber server will send us presence notifications from other JIDs, we must send our presence information. Along with sending a request to retrieve our roster (which is stored on the Jabber server), a client usually sends its (initial) presence as soon as it's connected and authenticated. In our case we don't really need to retrieve our roster as we're not going to do anything with the information, so all we do is send our presence, which as mentioned above, is represented by

<presence/>

To send our initial presence using Net::Jabber, we can call the PresenceSend() method of the Net::Jabber::Client class, like this:

$connection->PresenceSend();

Receiving the Presence of Others

So far, so good. But how do we request the presence for JIDs in our roster? Well, we don't, directly. The Jabber server sends them to us, as a result of us sending it our presence. So you could say that sending our presence is inherently the request. But the point is that we have to wait for the Jabber server to send us the presence information for anyone we've subscribed to.

Wait for? How? With the Process() method of the Net::Jabber::Client class. In a way, one could say that Process is to Net::Jabber as MainLoop is to Perl/Tk. Process() blocks either indefinitely (if no argument is provided) or for a number of seconds, e.g. Process(2) will block for two seconds and then return. Being from the Jabber server, the data will be in XML format, and will represent one of three main data types:

We haven't covered the third type, but the first two should be familiar. What we are looking for is presence information data. But how do we know which data type we are receiving and how do we handle it? Net::Jabber provides us with a callback mechanism that allows us to set handlers, in the form of subroutines, for the different data types and it will call the relevant handler when data is received.

As we're only interested in presence information, we'll do the following:

Define Store for Presence Info

Before starting, define a hash to hold presence information received, keyed on JID:

use vars qw/%presence/;

Set Up Handler

After we're connected, set up a handler subroutine to process incoming presence data:

$connection->SetCallBacks( "presence" => \&handle_presence );

Define the handler subroutine thus:

sub handle_presence {
  my $presence = Net::Jabber::Presence->new(@_);
  my $jid = $presence->GetFrom(); 
  $jid =~ s!\/.*$!!;  # remove any resource suffix from JID
  $presence{$jid} = $presence->GetShow() || 'normal';
}

The handle_presence subroutine will pass data (@_) from the handler distributor, which is in turn passed directly to the constructor for a Presence object via the Net::Jabber::Presence class. This object affords access to all the presence information we require - who the presence is for (GetFrom(), which returns a "full" JID with potentially a trailing resource identifier, which we remove with the regular expression shown); and what their presence is (GetShow(), which is not set for "normal" presence - the default - which I therefore set to "normal" to have a nonfalse value in the hash entry).

Wait for Data

After the call to PresenceSend(), call the Process() method to wait for as many as 2 seconds for presence data to be sent from the Jabber server (for users we're subscribed to -- in notify@yak's case this is just dj@yak):

die "Uh-oh - something has gone wrong with the connection\n"
  unless(defined($connection->Process(2)));

Conditional Message

Now we have all the information we need to decide whether it's appropriate to send a notification to dj@yak - it's in the %presence hash, and we've decided to only construct and send the message if the presence for dj@yak is "normal" or "chat."

my $r = RECIPIENT;
if ($presence{$r} =~ /(normal|chat)/) {
  # code to construct and send message
}

If the Jabber server doesn't send us any presence information for the recipient, then it most likely means that the recipient isn't connected (presence information is only sent for connected Jabber users). This means that the hash entry for dj@yak won't exist, and the above condition will take care of the first what-if mentioned at the start of this article.

Summary

As an overview, the new version of the script in its entirety, with comments, is here:

# notify.pl
# Version 2
# Email notification script to Jabber client
# Only send notification if recipient is willing to receive

use strict;
use Mail::Internet;
use Net::Jabber;

# Hash to hold presence info received from Jabber server
# ------------------------------------------------------
use vars qw/%presence/;

# Declare our constants
# ---------------------
use constant RECIPIENT => 'dj@yak';      # Jabber ID to be notified
use constant SERVER    => 'yak';         # Jabber server to connect to
use constant PORT      => 5222;          # Port to connect to
use constant USER      => 'notify';      # user this script connects as
use constant PASSWORD  => 'notify';      # password associated with USER
use constant RESOURCE  => 'perlscript';

# Read and parse email and extract header info
# --------------------------------------------
my $header = Mail::Internet->new(*STDIN)->head()->header_hashref();
chomp $header->{$_}[0] foreach keys(%{$header});

# Create a new Jabber client and connect
# --------------------------------------
my $connection = Net::Jabber::Client->new();
$connection->Connect( "hostname" => SERVER,
                      "port"     => PORT )
   or die "Cannot connect ($!)\n";

# Identify and authenticate with the server
# -----------------------------------------
my @result = $connection->AuthSend( "username" => USER,
                                    "password" => PASSWORD,
                                    "resource" => RESOURCE );
if ($result[0] ne "ok") {
  die "Ident/Auth with server failed: $result[0] - $result[1]\n";
}

# Set presence callback
# ---------------------
$connection->SetCallBacks( "presence" => \&handle_presence );

# Send our presence
# -----------------
$connection->PresenceSend();

# Wait for data
# -------------
die "Uh-oh - something has gone wrong with the connection\n"
  unless(defined($connection->Process(2)));

# Send notification only if appropriate
# -------------------------------------
my $r = RECIPIENT;
if ($presence{$r} =~ /(normal|chat)/) {

  # Create a message and build it up
  # --------------------------------
  my $msg = Net::Jabber::Message->new();
  $msg->SetMessage( "to"      => RECIPIENT,
                    "subject" => "Email from $header->{From}[0]",
                    "body"    => join("\n", "Subject: $header->{Subject}[0]",
                                            "Date: $header->{Date}[0]") );
  # Send the message
  # ----------------
  $connection->Send($msg);

}

# Disconnect from the Jabber server
# ---------------------------------
$connection->Disconnect();

exit;



# Presence data handler
# ---------------------
sub handle_presence {
  my $presence = Net::Jabber::Presence->new(@_);
  my $jid = $presence->GetFrom(); 
  $jid =~ s!\/.*$!!;  # remove any resource suffix from JID
  $presence{$jid} = $presence->GetShow() || 'normal';
}

You can see that with not many changes, the simple e-mail notification script has become much more useful. This is primarily due to the great implementation of the Jabber protocol in Net::Jabber, and the design of Jabber itself.

A Note on Net::Jabber 1.0021

The Perl script in this article uses the Net::Jabber module version 1.0020. There is a newer version (1.0021) that has recently become available on CPAN.

If you wish to use this newer version, there are two things you must bear in mind:

Of course, it's not necessary to upgrade to version 1.0021 to run the scripts in this series - you can happily stay with version 1.0020.

Final Words

This example, across the two articles, has only really scratched the surface of what is possible with small scripts and the Jabber system. I hope it has inspired you to create your own solutions and systems using Jabber.


DJ Adams is the author of O'Reilly's Programming Jabber book.


Discuss this article in the O'Reilly Network General Forum.

Return to OpenP2P.com.

 

Copyright © 2007 O'Reilly Media, Inc.