Served up in an instant

The classic use for a messaging server is fault reporting – a server detects a problem and sends messages to various users who might be able to fix it. Within our company, we use a server similar to the one we’ll build here with the excellent Nagios (www.nagios.org) monitoring software. This software doesn’t just send instant messages, but also receives them, and you can use this to set up your messaging bot.

Served up in an instant

Our messaging server will be written in Perl, and open-source resources freely available on the internet will provide everything else we need. Perl was one of the first open-source programs, whose development can be traced back to the work of Larry Wall in 1987. His original aim was to combine the features of well-known Unix utilities awk and sed in a C-like programming language. Sed (stream editor) is a command-line utility that performs search-and-replaces on files one line at a time using regular expressions to match content, while awk is a scripting language with high-level constructs for pattern matching and text processing. Much of the semantics of Perl derives from these venerable utilities, but that isn’t a problem because they were sufficiently strong that these constructs are still hard to better. Perl’s syntax was based on that of C, but it’s an interpreted rather than compiled language, and it’s become the default scripting language for many Unix users. However, it can equally be used to create full-blown applications.

Obtaining Perl couldn’t be simpler: if you’re using a recent Unix system, whether Linux, Solaris or other, you probably have it already (but it’s worth checking whether it’s the latest version). We recommend version 5.8.5 or later – some non-Linux Unix systems ship with version 5.6, which may give you problems with the later steps of our project. If you’re using Windows, the packaged version from Active State (www.activestate.com) is popular.

When you start writing Perl, it helps to think how you’d write something in your favourite language and then translate that into Perl. The reason is that one of Perl’s great strengths – and in our view a weakness too – is that “There’s more than one way to do it”, a principle enshrined in the acronym TMTOWTDI (usually pronounced “Tim Toady”; compliments to Wikipedia for that) and it will save confusion if you decide on your way rather than floundering through too many possibilities.

For example, every programming language has some form of conditional statement, like IF…ELSE, and indeed a normal Perl conditional looks like this:


if ($x == 0) {



print("x is zero

");



} else {



print("x is not zero

");



}

(Note that unlike in C and PHP, the curly brackets are compulsory.) However, Perl also offers several other ways to express this condition, such as:

print “x is zero

” if ($x == 0) ;

print “x is not zero

” unless ($x == 0) ;

(Note the brackets are gone.) Many introductions to Perl revel in all these variations and use them in every possible combination in their examples, but while we’ll admit they can be very useful, they also make it easy to write unreadable code – imagine mixing an “unless” into an “if…else”.

Here are two other small warnings about writing Perl programs if you’re used to another language:

1. Although Perl has a full set of data types like numbers, strings and arrays, they can all be coerced to other types based on content: for example, treating an array as a number will return its length. There are various tests for equality: above we used the == operator to check whether $x was zero, but if we wanted to test whether the string variable $s contains “simon” then we’d have used the “eq” operator instead.
2. You can write Perl programs in virtually any style you like, so if you want to write an old-fashioned BASIC-style program using GOTO and global variables you can. Equally, you can write properly structured programs with parameterised functions and procedures, or modular programs or object-oriented programs in Perl. Perl employs a set of syntactic structures that emulate rather than directly implement all these features. For example, if you write an object-oriented Perl program you won’t find syntax equivalent to C++ or Java, but you’ll get the same result.

Don’t be put off by these warnings – Perl is a great language and there’s a wealth of resources available on the internet and in print to help you learn it. When you install Perl, it comes with a bunch of manual pages worth reading; they’ll tell you how the syntax, data types and the more esoteric features work. On a Unix system, start from “man perl” and look at the reference pages on syntax and data types, plus the various tutorials.

Instant messaging

The world seems split between those people who think instant messaging is a great invention and those who regard it as a “so what” technology. It’s probably true that most dedicated IM users belong to the “texting” generation, but it has an increasing number of corporate users too. A big barrier to its acceptance is the fact that the most popular IM systems can’t interoperate with each other. There are three main systems – AOL, MSN and Yahoo – which until recently operated separately (MSN and Yahoo can now interoperate). If you’re registered on AOL Instant Messenger (AIM), you can’t send a message to your friend on MSN or Yahoo. There’s one other IM we need to mention: Jabber (www.jabber.org) is an open standard that has various open-source implementations. It’s worth noting that Google Talk is based on Jabber.

Most of these IM protocols are proprietary, but they’ve all been thoroughly reverse engineered by now. When we first decided to adopt IM, we had to decide which system to use. We were already using AIM, because it’s built into iChat (which ships on Macs), so we started by looking at that. AIM employs a collection of protocols and we discovered the one we need is called OSCAR. The next problem was how to access OSCAR from Perl, which is where the Comprehensive Perl Archive Network (CPAN) came to our aid.

CPAN is a repository from which people who write Perl modules can distribute their software. What’s so useful about CPAN is that you can access it from the command line on your machine. All modern Perl implementations will already have installed the “cpan” command, but, if not, access it with the command “perl -MCPAN -e shell”. Once in the CPAN command line, you can search for packages that contain certain keywords in their title. In our case, we looked for “i /oscar/”, leading us to the Net::OSCAR package, which we then installed straight from the CPAN command line using a simple “install Net::OSCAR”.

Once you have the Net::OSCAR module installed, you’ll need to read the documentation to see how it works. On a Unix system, you’ll find a manual page, but you can also read the manual at www.cpan.org. You’ll also need an AIM account, and there are two main routes you can take to get one: register at www.aim.com or www.mac.com (to get an @mac.com AIM account). As you’ll see, we registered realworldoss and that’s what we’re going to use in our examples.
Writing a Perl server

The basic structure of a Perl server can be read in the manual pages and we’ve used that for the basic code below, although we’ve added some extra bits to improve its structure:


use Net::OSCAR qw(:standard);



use strict;



use constant USERNAME => 'realworldoss ';



use constant PASSWORD => 'password';



sub im_in($$$$) {



    my($oscar, $sender, $message, $is_away) = @_;



    print "[AWAY] " if $is_away;



    print "$sender: $message

";



}



my $oscar = Net::OSCAR->new();



$oscar->set_callback_im_in(\&im_in);



$oscar->signon(USERNAME , PASSWORD );



while(1) {



    $oscar->do_one_loop();



    sleep 1;



}

There are four blocks of code here to look at in more detail. The first says we want to use two extensions to Perl, the first being Net::OSCAR (that qw :standard argument means we also get some constant definitions that might be useful). The “use strict” statement makes the Perl interpreter strict about syntax and, in particular, checks whether variables have been declared before use. The second block introduces two constants for the AIM username and password; although not necessary, using named constants instead of string literals is good practice.

The third block defines a function called “im_in” that takes four arguments (as shown by the four $ signs). The next line shows how arguments are processed in Perl. If you want to know what it means and how you can change it, read a book. The next lines print out some results.

The last block of code is the main program: first, it creates a Perl object called $oscar of class Net::OSCAR, which implements the whole OSCAR protocol for you, logging into the server and processing all messages to and from that server. So that you don’t need to know how the protocol actually works, the next line registers a call-back function with the Net::OSCAR object, which it calls whenever it detects some event has happened. In this case, our function im_in prints out an incoming message. The next line requests sign-on to the AIM server, then goes into an endless loop to handle OSCAR messages, sleeping for one second each time around.

Note that the sign-on function doesn’t sign you onto AIM immediately, but merely starts the process that will be handled within the loop – it might take many iterations around the loop to actually achieve login. To see what’s happening you need some debugging information, and to get that you can change that first “use Net::OSCAR” statement to read:


use Net::OSCAR qw(:standard :loglevels);



and add the line:



$oscar->loglevel(OSCAR_DBG_NOTICE);



When you run the Perl script with the correct password, you'll see the following logged output:



login: Connecting to login.oscar.aol.com:5190.



login: Connected.



login: Connected to login server.



login: Sending password.



login: Login OK - connecting to BOS



basic OSCAR service: Connecting to 64.12.24.192:5190.



login: Closing.



basic OSCAR service: Connected.



basic OSCAR service: Sending BOS-Signon.



basic OSCAR service: BOS handshake complete!



basic OSCAR service: Signon BOS handshake complete!



OSCAR session: Setting user information.



You can now send a message that gets printed out as:



xxxxx@mac.com: <HTML>Hello world</HTML>

Before going any further, some improvements are needed. First, we need to be able to handle errors like bad passwords, so we define a new function called “handle_error”:


sub handle_error($$$$$) {



     my($oscar, $connection, $error, $description, $fatal) = @_ ;
printf "Error %d: %s ", $error, $description ; die "$description " if ($fatal) ; }

This is written in more of a classic Perl style – note how we haven’t used brackets in the printf and the die functions. We need to add this as a new call-back function following that first call-back, so:


$oscar->set_callback_error(\&handle_error);



Now when you run the program with an incorrect password you'll see:



login: Connecting to login.oscar.aol.com:5190.



login: Connected.



login: Connected to login server.



login: Sending password.



Error 0: Invalid password.



Invalid password.

Extending the server

We’re now going to look at two extensions to this server: a method by which people can register to receive messages, and a method other programs can use to send messages. Our aim is to create a server that people register with and then receive information from another program like a fault monitor. We’ve implemented registration using buddy lists, an AIM facility that lets each user say who their buddies are and divide them into groups. Buddy information is held on AIM’s servers, so we don’t need to store any information locally. To implement the buddy list, add the following code:


use constant BUDDYGROUP => 'pcpro';



sub im_in {



   my($oscar, $sender, $message, $is_away) = @_;



   print "[AWAY] " if $is_away;



   print "$sender: $message

";



   if ($message =~ /join\s+${\BUDDYGROUP}/i) {



          $oscar->add_buddy(BUDDYGROUP, $sender);



          $oscar->commit_buddylist();



  $oscar->send_im($sender, "$sender: added to buddy group ${\BUDDYGROUP}");



   } elsif ($message =~ /remove\s+${\BUDDYGROUP}/i) {



        $oscar->remove_buddy(BUDDYGROUP, $sender);



        $oscar->commit_buddylist();



   $oscar->send_im($sender, "$sender: removed from buddy group   ${\BUDDYGROUP}");



   } elsif ($message =~ /members\s+${\BUDDYGROUP}/i) {



        my $outgoing = sprintf "Group %s members: %s",



                                BUDDYGROUP,



                                join(',',$oscar->buddies(BUDDYGROUP));



        $oscar->send_im($sender, $outgoing);



     }



}

What we’ve done here is set up a constant for our group name “pcpro”, then used it in a new version of our incoming message function im_in that now examines the incoming message and decides what to do. If it receives “join pcpro”, it adds this user to the buddy list, while “remove pcpro” removes them from the list. If it receives the message “members pcpro”, it sends the user a list of all members of the buddy group. All these messages are handled in the same way, by matching them against a regular expression, in the line:

if ($message =~ /join\s+${\BUDDYGROUP}/i) {

which means “if the message received ($message) matches (=~) the regular expression that starts with the word “join” followed by any number of spaces (\s+) and the buddy group name, then perform the next statement”. The regular expression is the part enclosed in forward-slashes, and that “I” at the end means ignore case so the program handles “Join PCPro” the same as “join pcpro”. There’s some tricky Perl here, as using the constant BUDDYGROUP inside a regular expression requires advanced knowledge about references.

Now our buddy list is set up, we have to decide how to send those messages. There are many methods for putting messages into a Perl program (see the manual page perlipc for examples) but we’re going to use a TCP/IP socket. Add the following lines to the program after the other “use” statements:


use IO::Socket;



use constant OURPORT => 2345;



and then change the main program to:



my $server = IO::Socket::INET->new( Proto    => 'tcp',



                              LocalPort=> OURPORT,



                              Listen   => SOMAXCONN,



                              Timeout  => 1,



                              Reuse    => 1);



while(1) {



    $oscar->do_one_loop();



    my $client ;



    if ($client = $server->accept()) {



        my $message = ''; 



        while (<$client>) {



            $message = $message . $_;



        }



        close $client;



        my $buddy;



        foreach $buddy ($oscar->buddies(BUDDYGROUP)) {



            $oscar->send_im($buddy, $message);



        }



    }



    sleep 1;



}

This program now accepts messages on TCP port 2345 and sends them to all your buddies. The server is set up to listen to a socket, and the accept() call tells us if someone is trying to contact us. When they are, we get a variable called $client that we can use like a file – those angle brackets mean read a line from the file and put it into a special variable called $_. The “while” loop reads the whole message from the remote server and then the “foreach” loop sends it to all our buddies.

I’m sure you’ll agree that this example shows how powerful Perl is for developing utilities, and don’t forget there’s a huge collection of open-source extensions that implement functions too tedious to create from scratch. The basic model here could be employed with any other IM system like Jabber simply by using the appropriate class in place of Net::OSCAR. To see our extended version of the server described here, send the message “help” to realworldoss on AIM.

Disclaimer: Some pages on this site may include an affiliate link. This does not effect our editorial in any way.