Editor's Note--In an earlier article, Is Jabber's Chatbot the Command Line of the Future?, we took an introductory look at ChatBot, the Jabber bot written in Perl and a regular occupant in the "jdev" conference room on conference.jabber.org. Having presented ChatBot's standard features, which exist as plug-ins, in this article we will discuss adding a new feature by writing our own plug-in for ChatBot.
Now we know how to get ChatBot up and running, let's look at how we might expand its repertoire of features. Rather than write a silly function that doesn't do much more than say "Hello World," let's look at giving ChatBot a facility that has some use beyond this article. Despite the arrival of the Euro currency in Europe, currency conversion still has its uses, especially for those countries (like my homeland) that have not yet taken the plunge. Another use of currency conversion is it allows for discussion of prices of items essential for everyday life, such as MP3 hardware, between the U.K. and the U.S.
So, let's add a currency conversion feature to ChatBot.
Hey, wait a minute! Currency conversion is useful! So it's likely that it's been done before. Rummaging around in the dark recesses of my brain, and those of Usenet, I remember that infobot, another Perl bot mentioned in the previous article, has this facility. Taking a quick look at infobot's home page, and the CVS repository, I find a script called exchange.pl, written by Bobby Billingsley. (See the "Resources" section later in this article for details.) Hmmm, this looks promising. Looking inside the script (hurrah for open source!) and at infobot's release notes at http://www.infobot.org/guide-0.43.x.html, it seems that this is exactly what we're looking for.
|
Related Reading
|
The script provides currency-exchange facilities by calling a Web service that does the actual currency conversions. The Web service is at http://www.xe.net, and the script uses the LWP::UserAgent and HTTP::Request::Common modules to make the HTTP calls.
Now, every scripter worth his or her salt knows that laziness, impatience, and hubris are three excellent qualities to have when it comes to coding. Putting all three of these qualities into practice, it turns out that it's easy to get this script to work for us in a ChatBot environment instead of in its native environment, infobot.
Let's go through the steps required to add a plug-in to ChatBot, as described in the PLUGIN_API.txt file in the chatbot/ directory.
|
Each feature, or plug-in, must have a unique name. We'll choose "currency" for our plug-in. The name will be used to determine what the script inside the plugins/ directory will be called, and it will also be used as the name of the feature's flag, which can be toggled on and off to control the feature's availability.
|
| |
Each ChatBot plug-in exists as a Perl script in the plugins/ directory, and all the scripts in that directory are loaded by the ChatBot program when it starts up. So, normally we would create currency.pl at this stage, and place our currency-conversion code in that file. But we already have our exchange.pl script from infobot. Let's see what we can do.
Taking a look at the exchange.pl script, shown in Listing 1, we see the main exchange() function, which does the hard work of making the currency-conversion calls. The definition of the exchange() function starts at line 17. exchange() uses two helper functions
GetAbb() and GetTlds(), which return currency abbreviations and
country TLDs (top-level domains), respectively.
There's also a BEGIN block, which attempts to load the LWP and HTTP modules mentioned earlier. It also switches off the function if it fails. So, don't forget to make sure you have those modules installed first.
In summary, it looks like we have to call the exchange() function to have a currency conversion performed for us. The expected parameters are:
$From - the 'from' currency
$To - the 'to' currency
$Amount - the amount to be converted
Before we go any further, however, there's a particular line within
the exchange() function definition to which we have to pay particular attention. Line 27, which looks like this:
if (my $proxy = main::getparam('httpproxy')) { $ua->proxy('http', $proxy)
};
calls a function named getparam() in the main package. It's used to retrieve
any HTTP proxy information that the LWP::UserAgent might need to reach
the Web service. Normally, this exchange.pl script would be running in its native environment of infobot, where the
getparam() function is provided in the main infobot sources (in the src/Channel.pl file of the infobot tarball, to be precise).
But because exchange() is going to
be called in ChatBot's plug-ins environment, getparam() is not going to
be available. So we need to make sure that calling
exchange() doesn't
immediately fail because it can't find main::getparam().
Let's get back to the currency.pl script that we're going to create in ChatBot's plugins/ directory. This is the script that ChatBot is going to load on startup, and it's the script where we must define and register our currency-conversion function so ChatBot knows about it.
So that there's no confusion between our new currency.pl script and the exchange.pl script from infobot, we'll create a new directory within the plugins/ directory, called infobot/, and place the exchange.pl script in there. That way, it's clear where the script came from, and ChatBot won't try to load it when it pulls in every Perl script it can find within the plugins/ directory.
It's now time to write our currency.pl script. A good starting point if you're going to add your own features to ChatBot is to look at the existing scripts in the plugins/ directory and take direction from them. The full script is shown in Listing 2. Here's how it pans out, chunk by chunk:
use strict;
require 'plugins/infobot/exchange.pl';
In addition to specifying the strict pragma, which is recommended for all but the most trivial of scripts, we also pull in the exchange.pl script from the infobot/ directory. Note that the path is relative to the ChatBot script that loads _this_ currency.pl script, which means that the path contains the name of the plugins/ directory too.
sub getparam {};
Remembering that the exchange() function in exchange.pl calls main::getparam(), we define it here as a null function. If you do have an HTTP proxy, you could define getparam() here to return
appropriate information (instead of returning nothing), but to keep
things simple, we'll just assume that we have a proxy-less connection
to the Web.
&RegisterFlag("currency");
The PLUGIN_API.txt file in ChatBot's main directory describes the
plug-in API. There are a number of functions in the API to manage the flags that control
which features are available in what channels.
Here, with the RegisterFlag() function, we're registering our
feature as 'toggleable,' and providing a name for the flag. With this,
the currency feature can be turned on and off using the
!toggle_flag command, as shown in the previous article, like this:
!toggle_flag flags currency
After registering our feature flag, we can now register the specific command that people can give to ChatBot to avail themselves of the conversion services:
&RegisterCommand(command => "!convert",
handler => \&plugin_currency_convert,
desc => "Converts from one currency to another",
usage => "<amount> <from-curr> to <to-curr>");
What this does is register the function
plugin_currency_convert() with
ChatBot, to be called whenever someone invokes the
!convert command, which is expected to look like this:
!convert 100 GBP to USD
Now that we've set up our flag and registered the command, it's time to write the function that will take care of the conversion request by
calling exchange.pl's exchange() function.
sub plugin_currency_convert {
my $message = shift;
my $conversion = shift;
my $fromJID = $message->GetFrom("jid");
Our plugin_currency_convert() function, like all the functions
registered using the RegisterCommand() function from ChatBot's
plug-ins API, receives a Net::Jabber::Message object that represents
what was said in the invocation of the !convert command. From this
we can determine, for example, the JID whence the message originated,
as shown here, stored as a Net::Jabber::JID object in $fromJID.
(This is, of course, the conference-specific JID, not the real JID
of the user who issued the command--the anonymity of
JIDs in conference rooms is a feature of the conference component.)
ChatBot sees sentences as well commands simply:
<command> <list of parameters>
such that from our conversion example earlier, the <command> would
be "!convert" and the <list of parameters> would be "100 GBP to USD".
In addition to sending the whole message object to our function, ChatBot also conveniently makes this list of parameters available as a second argument in the call, which we catch in $conversion.
Next, we make sure that our feature flag has been set--that ChatBot's administrator has turned the feature on. If it hasn't, we don't want to do anything and simply return from the function:
return unless &CheckFlag($fromJID->GetJID(),"currency");
Note that the first argument sent in the call to
CheckFlag() is $fromJID->GetJID(), which returns the "user@hostname" part of the JID we stored in $fromJID. This "resourceless" JID represents the
conference room in which the command was uttered. If an occupant
with a nickname of "fred" in the "jdev" room hosted at conference.jabber.org
uttered the command, his JID relating to that room would be:
jdev@conference.jabber.org/fred
The version of this JID without the resource is:
jdev@conference.jabber.org
which is the "fully qualified" name of the conference room. We need to
send the name of the conference room to the CheckFlag() function, as flags
are maintained on either a per-room or per-channel basis.
Finally, we get to the matter at hand. We use a regular expression to pull the component parts out of the command parameters passed:
my ($amount, $from, undef, $to) =
$conversion =~ m|^([\d\.]+)\s+(\w+)\s+(to\s+)?(\w+)|;
For our example command:
!convert 100 GBP to USD
$amount would contain "100", $from would contain "GBP", and $to would
contain "USD". These are exactly the sorts of values we need to be able
to call the exchange() function in exchange.pl.
Before making the call, we do a final sanity check on the values received, returning an appropriate message if they're not going to fit:
unless (defined($amount) and defined($from) and defined($to)) {
return ($message->GetType(),
$fromJID->GetResource().": sorry, I don't understand");
}
There are two arguments to return. The first is the type of message you want the feature to emit. The second is the message content. Here, we're getting ChatBot to reply to the currency conversion in the same way that it was requested. That is, if the request came publicly via a 'groupchat' type message, then the response should be public too. But if the request was part of a one-on-one chat, ChatBot would reply privately too.
Now for the moment of truth. We now call the exchange() function, and return
whatever it produces. Because exchange() returns a simple string regardless
of whether the call to the Web service was successful or not, we can simply
close our eyes at this point and "press the button":
return ($message->GetType(),"[convert]:". exchange($from, $to, $amount));
}
1;
And that's pretty much all there is to it.
|
Now we have our new plug-in script, let's start up ChatBot in the normal way:
[dj@cicero chatbot]$ ./chatbot
This time, Chatbot's been configured to join the room "test" on my local server, and go by the name "chatbot". To try out the currency-conversion service, don't forget to toggle the feature on, first, like this:
!toggle_flag flags currency
Now you can have ChatBot convert currencies for you ad nauseam. Figure 1 shows a typical interaction.
Figure 1. ChatBot providing currency-conversion services in the "test" room
Through the power of Perl, we've now given ChatBot the facility to perform currency conversions for us. Wonderful. And what's more, we've not at all been slack in our pursuit of the three key values: we were impatient enough to want a currency-conversion function without having to write one from scratch, we were lazy enough to do as little as possible with the exchange.pl script once we found it, as well as being lazy enough to want a bot to perform the conversions for us in the first place, and we had enough hubris to combine code from infobot and ChatBot (after all, "hubris" has the same etymological root as "hybrid," and implies an "insult" to both parties being combined) to get the job done.
It's clear to see that ChatBot has a great framework for adding new features; I hope I've shown you how straightforward it is.
Go on--make a new acquaintance today, and get ChatBot working for you!
DJ Adams is the author of O'Reilly's Programming Jabber book.
O'Reilly & Associates published (June 2001) Programming Jabber.
Sample Chapter 5: Jabber Technology Basics, is available free online.
You can also look at the Table of Contents, the Index, and the Full Description of the book.
For more information, or to order the book, click here.
Copyright © 2007 O'Reilly Media, Inc.