Channels ▼
RSS

Web Development

Writing IRC Bots in Perl with Bot::BasicBot


Tom Insam is a full-time perl programmer, currently working for Fotango (http://opensource.fotango.com), where he writes application frameworks. He can be contacted at tom@jerakeen.org.


Why would you want an IRC bot? IRC bots are amazingly boring. I can't think of an IRC channel that I've joined that hasn't had either a bot managing channel operator status, or an infobot to make annoying comments. Most often, these are off-the-shelf bots with simple config file tweaks, which is probably a good thing. Op bots are annoyingly hard to write properly, and infobots are a problem that's been solved too many times already.

Personally, I want to write much more interesting bots. For instance, I wanted a bot that would read out the title of any URL mentioned in the channel, or something to announce commits to my subversion repository. Maybe I want to watch a logfile in an IRC channel. One of my favourite bots watches a folder for uploads via webdav, announces their progress in channel, then announces a publication url once the upload is finished.

IRC is Annoying

When I do this stuff, I really don't want to be messing around with the IRC protocol—it's a pain. I see Perl IRC bot examples of varying cleverness, using modules from IO::Socket, through Net::IRC, up to POE::Component::IRC and beyond, but for most of them need you to know at least something about the internals of IRC, and that sounds too much like hard work to me. When I want a very fast proof of concept, I want to implement one method to try it, and tell the code "join this channel."

Bot::BasicBot, which is currently maintained by me, was designed to be smart enough that you don't need to know anything at all about the internals of IRC, while still being simple enough that you're not having to write a lot of overhead code to do simple stuff. Naturally, this being Perl, I also try to make interesting and clever stuff possible. As the maintainer, I think it's perfect, but then I would. Let me try to convince you.

A Very Minimal Bot

Let's say that you want to write a bot that will insult anyone foolish enough to say anything about Perl. Then you can join some random channel with it and people will realise how great you are. What could be easier? Bot::BasicBot hides all the code that will connect to the server, reconnect if the connection drops, join and leave channels, etc. It will parse things said in channels that the bot is in into nice method calls, noticing directly addressed comments, and makes replying in kind very easy.

    #!/usr/bin/perl
    use warnings;
    use strict;

    package MyBot;
    use base qw( Bot::BasicBot );

    # the 'said' callback gets called when someone says something in
    # earshot of the bot.
    sub said {
      my ($self, $message) = @_;
      if ($message->{body} =~ /\bperl\b/) {
        return "I hear Ruby is better than perl..";
      }
    }

    # help text for the bot
    sub help { "I'm annoying, and do nothing useful." }

    # Create an instance of the bot and start it running. Connect
    # to the main perl IRC server, and join some channels.
    MyBot->new(
      server => 'irc.perl.org',
      channels => [ '#weloveperl', '#penguins' ],
      nick => 'rubybot',
    )->run();

Here, MyBot subclasses Bot::BasicBot, and overrides methods that are called as a result of activity on the IRC server. The most common of these is the said method, which is called whenever anything is said in any of the channels that the bot is in, or if someone sends the bot a private message. The method is passed a single parameter, a hashref, of which the only interesting keys (for now) are who, the nick of the person sending the message, and body, which is the contents of the message. If you return anything from this function, the bot will use this to reply to the message in the context that it was sent—either in the same channel, or as a reply to the private message. In the case of our example, we'll annoy anyone who uses the word "perl."

There is also an informal protocol followed by all bots that subclass Bot::BasicBot. If you address them with the word "help," they will reply, explaining what it is that they do. The help() routine in the example provides a nice, useless, description of our bot. There's a default implementation of this method, but it's boringly default and should probably be overridden.

After we're done with the implementation of the bot, we instantiate it, and call run, which starts everything working. The new() method takes

a hash of parameters, most of which have sensible defaults. You'll always want to give the bot a nick, and you'll probably want to provide a server and list of channels to join. The bot will start, try to connect to the server, and will probably be kicked almost immediately. Maybe they prefer python?

Use of Clever Features

Of course, Bot::BasicBot is capable of much more than this. There are methods called when people join and leave channels, for instance, so you can write a bot that will greet new people to the channel:

    sub chanjoin {
      my ($self, $message) = @_;
      return "Hello, $message->{who}. You really should use Ruby.\n";
    }

The return values from these methods are implicitly treated as replies to whatever event caused the message, and will be in the same context. For instance, in the case of the first example, comments in #weloveperl containing the word "perl" will have a reply from the bot in that channel, and a private message to the bot will get a private reply. If you want to say something somewhere explicit, or perform some other operation, there are plenty of methods provided by Bot::BasicBot.

The simplest of these is the say method, which takes a hash similar to the one you get when you implement your said callback:

    sub said {
      my ($self, $message) = @_;
      $self->say(
        channel => "#soopersekritspys",
        body => "$message->{who} said '$message->{body}' in $message->{channel}",
      );
      return undef;
    }

If you want the bot to say something to someone privately, set the channel parameter to the special string msg, and if you want to directly address someone in a channel (by prefixing the body with nick:), set the address parameter to a true value. Likewise, for incoming messages, the address and channel parameters will indicate if the message was directly addressed to the bot, or was in a private message. There is also an emote method, which will make the bot emote the body parameter of the call, instead of saying it.

Naturally, there's a bug in the above code. It will tell people in #soopersekritspys about things they say themselves. This is easily fixed, of course, but fortunately it's not a dangerous bug—the bot will be smart enough to ignore things it said itself, there are no horrible endless loops here.

Lots of clever things I do involve dealing with the system in some way, normally in a way that can't wait for user interactions. Let's tail a logfile:

    package TailBot;
    use base qw( Bot::BasicBot );

    sub connected {
      my $self = shift;
      $self->forkit({
        channel => "#welovelogfiles",
        run     => [qw( /usr/bin/tail -f /var/log/messages )],
      });
    }

The forkit method forks a background process. You can optionally pass in a handler parameter that will receive the STDOUT of the process. But, by default, the output will just be sent to the channel specified. This bot will tail a logfile—nothing else is required.

Of course, some things can't be waited on. You need to poll.

    sub tick {
      my $self = shift;
      $self->say(
        channel => "#easily_annoyed_people",
        body => "The time is now ".scalar(localtime),
      );
      return 60; # wait 1 minute before another tick event.
    }

The tick method is called regularly, and lets you do things that don't rely on external events. I typically use it to poll for new files in watched folders, but you could regularly check RSS feeds or web pages for updates, or read your mail and announce the unread count. If you return a number from the method, it will next be called in that many seconds, otherwise it will never be called again.

Of course, if you want to, you can take advantage of the fact that, underneath everything, we are really a POE wheel, and use other POE components—for instance, POE::Component::RSSAggregator could be used to watch multiple feeds and announce changes in them. Because Bot::BasicBot is itself a POE component, we can use a similar technique to pass messages between two IRC servers.

  #!/usr/bin/perl
  use warnings;
  use strict;

  package BridgeBot;
  use base qw( Bot::BasicBot );

  # when the bot sees something, the other bot should repeat it.
  sub said {
    my ($self, $message) = @_;
    $self->{other_bot}->repeat($message);
  }

  # when told to repeat something, say it into our channel.
  sub repeat {
    my ($self, $msg) = @_;
    $msg->{channel} = $self->{channels}->[0];
    $msg->{body} = "$msg->{who}: $msg->{body}";
    $self->say($msg);
  }

  # start two bots on different servers

  my $bot1 = BridgeBot->new(
    nick => "bridgebot",
    server => "irc.network.one",
    channels => ['#channel_one'],
    no_run => 1,
  );

  my $bot2 = BridgeBot->new(
    nick => "bridgebot",
    server => "irc.network.two",
    channels => ['#channel_two'],
    no_run => 1,
  );

  # tell the bots about each other
  $bot1->{other_bot} = $bot2;
  $bot2->{other_bot} = $bot1;

  # start them
  $bot1->run();
  $bot2->run();
  use POE;
  $poe_kernel->run();

We create two instances of the BridgeBot, tell them about each other, and join them to different servers. The said method in each bot, called when it sees activity, simply calls the repeat method on the other bot. The repeat method changes the message to go to the right channel, and adds the nick of the person making the statement to the body. Then it repeats it in this bot's channel. This will work equally well between two channels on one server, or two channels on different servers.

The no_run parameter to the new method tells Bot::BasicBot that we want to handle the POE startup ourselves, otherwise the first bot's run command would start POE and we'd never see the second one. We start POE manually ourselves as the last thing we do.

Converting Modules for Bot::BasicBot::Pluggable

Now, since I don't want to have to manage ten tiny little bots, adding another one every time I want some new toy, Bot::BasicBot::Pluggable runs multiple bot modules, implemented as Perl modules, in one bot process, with one nick. It's designed so that the interface you have to implement is almost exactly the same as that for Bot::BasicBot—subclass a different module and you're practically done.

Bot::BasicBot::Pluggable ships with a lot of useful modules that will keep track of the channels your bot is in, join and leave channels on demand, and other basic bot functions—again, the sort of thing you don't want to have to think about when running more complex bots.

Finally, gratuitously coming full circle and undermining my entire point, Bot::Infobot is an infobot clone implemented using Bot::BasicBot::Pluggable modules. If you're familiar with the most recent incarnation of "dipsy" on the Perl IRC network, you've seen it work. When a toy basicbot that I'm playing with becomes mature enough that I want it in my channel all the time, I can just load it into my infobot and stop worrying.

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV