Headline viewer

Wow. That was a biggie. So we're going to close this Chapter on a lighter note, and what could be more fitting than to build a complementary script with which we can keep an eye on those RSS headlines that are punted to us throughout the day.

In most of my workplaces, the monitor I have is 17 inches. Which means that every application must make a justification for existence on a plot of the pixel real-estate, a justification relative to it's window size. So something that allows me to watch RSS headlines as they scroll by ought to be small. It has no reason to be large.

Until now, I've droned on about the great leverage that there is to building solutions that make use of off the shelf clients. [1] But it's time to buck the trend, indeed to make another point. While you can orientate the features of your Jabber-based applications in the direction of standard clients, to take advantage of the installed base of Jabber clients, if you do want to create a client that works differently, a client that fits your needs exactly, then go ahead: it will be surprisingly straightforward. The mantra of "server side complex, client side simple" (with apologies to George Orwell) is there to help us. What's more, we can put into action an idea expressed earlier in the book (in the section called Custom Clients in Chapter 2):

A Jabber client is a piece of software that implements as much of the Jabber protocol as required to get the job done.

If we're going to build a headline viewer client, and know that the information is going to get punted to us in headline type <message/> elements, why have our viewer client understand or deal with anything else? To implement a Jabber solution, we pick and choose the parts of the protocol that make sense in the context of that solution. If you want to transport RPC calls in data payloads between two endpoints, why bother with roster management or rich-text chat facilities? If you just want to join a conference room to talk with the other room occupants, why bother with threaded one-on-one conversations? If you need routing and presence capabilities to have your oven know when you've arrived home, why engineer anything else?

What we're going to write here is a simple headline viewer. Nothing more. Nothing less. It will know the tiniest thing about presence— as the headlines come in as <message/> elements, it will need to announce its availability to the JSM—and the viewer we're going to build will be a Jabber client that will have a session context with the JSM, we need to tell the JSM that we're available when we start the client up. Otherwise the headlines will simply be queued up by the store and forward mechanism ready for the "next time" we're available.

We'll leave the registration with the RSS punter to another client that knows about agents (services) and can interact in a jabber:iq:register kind of way. I'm not a big fan of the "one size fits all, one client for everything" philosophy; I prefer to use different features of different programs to get me through my day. So while our headline viewer will receive, understand and display the <message type='headline'/>s, we'll use WinJab, or even JIM (Jabber Instant Messenger) to manage our RSS source subscriptions. Figure 8-8 illustrates the process of registration with our RSS punter component, using JIM.

Figure 8-8. Registering with the RSS punter with JIM

The suggestion of JIM as a client to complement (cheers!) or make up for the lack of features in (boos!) our headline viewer is deliberately ironic. JIM's remit is to provide support for core Jabber client features, of which (as yet) headline messages are not considered to be a part. So while JIM can interact with services and register (and unregister—which will send the <remove/> tag in the query, as described in the section called Handling registration requests), it doesn't handle headline type messages. Which is perfectly fine. Our headline viewer won't handle chat or normal messages. The point is, it's not supposed to.

It's worth pointing out that another reason why our headline viewer client can remain simple is because the RSS punter component will be doing all the hard work for us. Unlike other (non-Jabber) headline viewer programs that are available, our script depends upon the RSS punter. It's that component that will maintain the list of RSS sources. It's also that component that will retrieve those sources at regular intervals and check for new items. All we have to do is sit back and have those new items pushed to us, at which point our client has to make a slight effort to insert the details of those new items into the viewer display. That's more or less it.

The plan

The viewer is visual, so let's see at what it's going to look like. Figure 8-9 shows the headline viewer client in action. There's a scrollable area where the headline titles are displayed. We can clear that area, or select a headline and call up a web browser to fetch the story by passing the URL to it.

Figure 8-9. The headline viewer client

It's also nice and small, visually and in the amount of code we're going to have to write. We connect to our Jabber server, set up a handler for the incoming headline messages, build our display, send our availability, and sit back.

Actually, we need to say a few things about the "sitting back" bit. We know that Jabber programming implies an event model. We're going to use Tk—the widget library for building GUI applications, with bindings for many languages. Tk itself has an event model, which in many ways reflects Jabber's. Table 8-2 shows how Jabber and Tk reflect in this programming model puddle.

Table 8-2. Jabber and Tk event model reflections

JabberTk
Establish connection to serverConstruction of widgets
Definition of callbacks to handle incoming elementsDefinition of callbacks to handle UI events
Setting of a heartbeat function [a]Setting of a command to execute regularly with repeat()
Launching the event loopStarting MainLoop()
Notes:
a. See the section called Preparation of the RSS event function and element handlers.

Having one program governed by two independent event loops is not what we want to try and achieve. We want Jabber and Tk's event models to cooperate. This is achievable by making one of the two models the master and the other one the slave. Using Tk's repeat() method to invoke a function that calls our Jabber library's process() method should do the trick. We can hand over control to Tk with MainLoop(), and know that our Jabber event model will get a look in because of the Tk event callback we've defined with repeat().

The script

We're going to use Perl, and the Net::Jabber Jabber library. Starting with the declarations of the libraries we're going to use, along with some constants:

use Tk;
use Net::Jabber qw(Client);
use strict;

use constant SERVER   => 'gnu.pipetree.com';
use constant PORT     => 5222;
use constant USER     => 'dj';
use constant PASSWORD => 'secret';
use constant RESOURCE => 'hlv';

use constant BROWSER  => '/usr/bin/konqueror';

The script will connect to Jabber as a client, so we specify that in our use statement to have the appropriate Net::Jabber modules loaded. We're going to be connecting to the Jabber server at gnu.pipetree.com, although, as we said, the RSS punter might live somewhere else. It just so happens that in our scenario, there's a reference to the component in gnu.pipetree.com's JSM <browse/> section, so that we can carry out our registration conversations with it (using JIM, for example).

If the Fetch button is pressed when an item in the list is selected (see Figure 8-9), we want to jump to the story by launching a web browser. The constant BROWSER used here refers to the browser on our local machine. [2]

my @headlines;
my @list;

We declare two arrays: @headlines, which we'll use to hold the items as they arrive contained in the headline <message/> elements on the XML stream, and @list, to hold the URLs that relate to those items in @headlines.

After connecting to and authenticating ourselves with the Jabber server (this is very similar to the way the coffee monitor script connects and authenticates in the section called setup_Jabber()):

my $connection = Net::Jabber::Client->new();

$connection->Connect(
  hostname => SERVER,
  port     => PORT,
) or die "Cannot connect ($!)\n";

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";
}

we set up our callback to take care of incoming <message/> elements. This is the handle_message() function:

$connection->SetCallBacks( message => \&handle_message );

Now it's time to build our GUI. We start by creating a main window, giving it a title and geometry, and establishing the cooperation between the two event models with the repeat() method:

my $main = MainWindow->new( -title => "Headline Viewer" );
$main->geometry('50x5+10+10');
$main->repeat(5000, \&check_headlines);

repeat() will arrange Tk's main event loop to hiccup every five seconds (the first argument is measured in milliseconds) and call our check_headlines() function.

Next, we build a frame to hold the three buttons Clear, Fetch, and Exit, and a scrollable list to contain the item titles as we receive them:

# Button frame
my $buttons = $main->Frame();
$buttons->pack(qw/-side bottom -fill x/);

# Headline list
my $list = $main->Scrolled(qw/Listbox -scrollbars e -height 40 -setgrid 1/);

Defining the buttons, one at a time, brings our attention to the Tk UI event model, in that we define the handlers using the -command argument of the Button() method. The handlers' jobs are quite small, so we can get away with writing them "in-line":

# Clear button
my $button_clear = $buttons->Button(
              -text      => 'Clear',
              -underline => '0',
              -command   => sub
              {
                @list = (); $list->delete(0, 'end')
              },
            );

If called, the Clear button will clear the scrollable display by calling the delete() method on our $list object, and emptying the corresponding array of URLs.

The Fetch button extracts the URL from the item that is highlighted in the scrollable list (using the curselection() method to retrieve the index value) which is then used to look up the @list array, and calls the external browser program in the background, passing it that URL. Many browsers accept a URL as the first argument, if your choice of browser doesn't, you'll need to modify this call slightly.

# Fetch Button
my $button_fetch = $buttons->Button(
              -text      => 'Fetch',
              -underline => '0',
              -command   => sub
              {
                system(
                  join(" ", (BROWSER, $list[$list->curselection], "&"))
                )
              },
           );

The Exit button, if pressed, uses destroy() to, well, destroy the main window. This will cause Tk's main event loop to come to an end, passing control back to the statement in the script following where that main event loop was launched (with MainLoop()).

# Exit button
my $button_exit = $buttons->Button(
              -text      => 'exit',
              -underline => '0',
              -command   => [$main => 'destroy'],
           );

Having created all the buttons, and packed everything into our window with the pack() method:

$button_clear->pack(qw/-side left -expand 1/);
$button_fetch->pack(qw/-side left -expand 1/);
$button_exit->pack(qw/-side left -expand 1/);

$list->pack(qw/-side left -expand 1 -fill both/);

we announce to the JSM that we're available:

$connection->PresenceSend();

All that remains for us to do is start Tk's main event loop. We include a call to the Net::Jabber Disconnect() method for when the Exit button is pressed and control returns to the script, so we can gracefully end our Jabber connection:

MainLoop();

$connection->Disconnect;
exit(0);

We defined the check_headlines() function as the function to invoke every five seconds.

sub check_headlines {
  $connection->Process(1);
  while (@headlines) {
    my $headline = pop @headlines;
    $list->insert(0, $headline->{title});
    unshift @list, $headline->{link};
  }
}

To check for any messages that have arrived on the XML stream, we can call the Process() method on our connection object. If there are any waiting messages, the callback that we defined to handle them—handle_message()—will be called:

sub handle_message {
  my $msg = new Net::Jabber::Message($_[1]);
  return unless $msg->GetType eq 'headline';

  my ($oob) = $msg->GetX('jabber:x:oob');
  push @headlines, {
                     link => $oob->GetURL(),
                     title => $msg->GetSubject(),
                   };                    
}

This message handling callback will ignore everything but <message/> elements that have the value "headline" in the type attribute. Remembering that a headline message, complete with an <x/> extension, qualified with the jabber:x:oob namespace, looks like this:

<message type='headline' to='dj@qmacro.dyndns.org'>
  <subject>JabberCon Update 11:45am - Aug 20</subject>
  <body>JabberCon Update - Monday Morning</body>
  <x xmlns='jabber:x:oob'>
    <url>http://www.jabbercentral.com/news/view.php?news_id=998329970</url>
    <desc>JabberCon Update - Monday Morning</desc>
  </x>
</message>

we can see fairly easily what the GetX() method does. It returns, in list context, all the <x/> elements contained in the element represented by $msg that are qualified by the jabber:x:oob namespace. We're only expecting there to be one, which is why we plan to throw all but the first array item away with the ($oob) construction. After the call to GetX(), the object in $oob represents this part of the message:

<x xmlns='jabber:x:oob'>
  <url>http://www.jabbercentral.com/news/view.php?news_id=998329970</url>
  <desc>JabberCon Update - Monday Morning</desc>
</x>

The item's details—the URL and title—are pushed onto the @headlines list, and our headline type message handling function has done its job. Control passes back to the check_headlines() script, to immediately after the call to the Process() method.

The handle_message() function may have bee called multiple times, depending on how many elements had arrived; so the @headlines array might contain more than one item. We run through the array, pop()ping off each headline in turn, inserting the title into our scrollable list object, and the URL into the corresponding position in our @list array:

$list->insert(0, $headline->{title});
unshift @list, $headline->{link};

The script

Here's the script in its entirety.

use Tk;
use Net::Jabber qw(Client);
use strict;

use constant SERVER   => 'gnu.pipetree.com';
use constant PORT     => 5222;
use constant USER     => 'dj';
use constant PASSWORD => 'secret';
use constant RESOURCE => 'hlv';

use constant BROWSER  => '/usr/bin/konqueror';

my @headlines;
my @list;

my $connection = Net::Jabber::Client->new();

$connection->Connect(
  hostname => SERVER,
  port     => PORT,
) or die "Cannot connect ($!)\n";

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";
}

$connection->SetCallBacks( message => \&handle_message );

my $main = MainWindow->new( -title => "Headline Viewer" );
$main->geometry('50x5+10+10');
$main->repeat(5000, \&check_headlines);

# Button frame
my $buttons = $main->Frame();
$buttons->pack(qw/-side bottom -fill x/);

# Headline list
my $list = $main->Scrolled(qw/Listbox -scrollbars e -height 40 -setgrid 1/);

# Clear button
my $button_clear = $buttons->Button(
              -text      => 'Clear',
              -underline => '0',
              -command   => sub
              {
                @list = (); $list->delete(0, 'end')
              },
            );

# Fetch Button
my $button_fetch = $buttons->Button(
              -text      => 'Fetch',
              -underline => '0',
              -command   => sub
              {
                system(
                  join(" ", (BROWSER, $list[$list->curselection], "&"))
                )
              },
           );

# Exit button
my $button_exit = $buttons->Button(
              -text      => 'exit',
              -underline => '0',
              -command   => [$main => 'destroy'],
           );


$button_clear->pack(qw/-side left -expand 1/);
$button_fetch->pack(qw/-side left -expand 1/);
$button_exit->pack(qw/-side left -expand 1/);

$list->pack(qw/-side left -expand 1 -fill both/);

$connection->PresenceSend();

MainLoop();

$connection->Disconnect;
exit(0);

sub check_headlines {
  $connection->Process(1);
  while (@headlines) {
    my $headline = pop @headlines;
    $list->insert(0, $headline->{title});
    unshift @list, $headline->{link};
  }
}

sub handle_message {
  my $msg = new Net::Jabber::Message($_[1]);
  return unless $msg->GetType eq 'headline';

  my ($oob) = $msg->GetX('jabber:x:oob');
  push @headlines, {
                     link => $oob->GetURL(),
                     title => $msg->GetSubject(),
                   };                    
}

Notes

[1]

Alright, I said it. "Leverage." It's the only occasion in the book, ok?

[2]

(Konqueror, my browser of choice in the KDE environment).