oreilly.comSafari Books Online.Conferences.
Articles Radar Books  

The Command Line of the Future Part 2: Expanding ChatBot's Repertoire
Pages: 1, 2, 3

Step 1: Pick a name for the plug-in

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.

Step 2: Create a .pl file in the plugins/ directory

Comment on this articleAside from your experiences with this plug-in, if you've created one, or you're using any new ones, let us know.
Post your comments

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.


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:


The version of this JID without the resource is:


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));

And that's pretty much all there is to it.

Pages: 1, 2, 3

Next Pagearrow

P2P Weblogs

Richard Koman Richard Koman's Weblog
Supreme Court Decides Unanimously Against Grokster
Updating as we go. Supremes have ruled 9-0 in favor of the studios in MGM v Grokster. But does the decision have wider import? Is it a death knell for tech? It's starting to look like the answer is no. (Jun 27, 2005)

> More from O'Reilly Developer Weblogs

More Weblogs
FolderShare remote computer search: better privacy than Google Desktop? [Sid Steward]

Data Condoms: Solutions for Private, Remote Search Indexes [Sid Steward]

Behold! Google the darknet/p2p search engine! [Sid Steward]

Open Source & The Fallacy Of Composition [Spencer Critchley]