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 [email protected].
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 protocolit'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 senteither 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 bugthe 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 logfilenothing 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 componentsfor 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::BasicBotsubclass 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 functionsagain, 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