What is an IRC bot?
What you will need to program an IRC bot
A very simple bot: HelloBot
Handling other common events: RokerBot
TriviaBot: using $irc->do_one_loop
ResourcesWhat is an IRC bot?
An automated client:To an IRC server, an IRC bot is virtually indistinguishable from a regular IRC client (i.e., a person using a program such as X-Chat, mIRC, or ircii). However, there's no person typing behind an IRC bot. It only makes automated responses, based on (usually) what is happening on IRC. An IRC bot can do things based on public messages, private messages, pings, or any other IRC event. But a bot isn't limited to the world of IRC. It can talk to a database, the web, a filesystem, or anything else you may imagine.
Examples:Here are some common IRC bots that you may have seen in your travels already:
Etiquette:Many IRC networks restrict the use of bots. When you log into an IRC server, check the MOTD (message of the day) to see what the policy is on bots. If you violate this policy, you may be banned from the server. In practice, however, bots aren't automatically detectable. As long as your bot doesn't join an established channel and annoy people, you probably won't get in trouble. But IRC channels are free; make your own, invite your friends, and have a ball. If you want to be really safe, you can run your own IRC server. If you're planning on extensive debugging, running an IRC on localhost will save you lots of time anyway. Check out generic IRCd and DALnet's Bahamut IRCd. These are very stable programs and should compile easily on a UNIX system, however the specifics of configuration are beyond the scope of this document.
PerlYou can program in IRC bot in any language. Some IRC clients offer their own scripting languages that add automated functionality to your normal IRC session. However, we'll be using Perl.
Net::IRC ModuleNet::IRC is Perl module that talks the IRC protocol, abstracting most of the low-level details of programming an IRC bot. It primarily works as event-driven programming (we'll get to that later). You can get Net::IRC from CPAN.
IRC serverYou'll need to find an IRC server to connect to. A fairly comprehensive list can be found at irchelp.org. However, for debugging purposes, you may want to run your own IRC server.
Other software for functionalityNo IRC bot is an island. While some IRC bots, such as a channel administration bot, can be useful without any interface to the outside world, we'll be using some non-IRC-specific software to make our bot more useful. To program use the bots in this tutorial, you'll need the Perl modules LWP (Library for WWW access in Perl), HTML::Parser, and Text::Wrapper. For TriviaBot, we'll be using the MySQL database and the Perl DBI module. While learning about IRC bots, you may also find the Data::Dumper module useful. Go to CPAN to get these if you don't have them already.
What HelloBot doesHelloBot is a greeting bot. When a user, let's give him the nick "Joe," joins a channels, HelloBot will say "Hello, Joe!" When Joe leaves, HelloBot will say "Goodbye, Joe!" (Of course, since Joe has already left, he won't see the message, but other users in the channel will.) [02:03:53] HelloBot (brian@L36-72.DATANET.NYU.EDU) has joined ## [02:03:53] <HelloBot> Hello everyone! [02:03:53] <HelloBot> Hello, HelloBot! [02:04:20] Harry (brian@turtle.wholok.com) has joined ## [02:04:20] <HelloBot> Hello, Harry! [02:04:25] Sally (brian@turtle.wholok.com) has left ## [02:04:25] <HelloBot> Goodbye, Sally! [02:04:38] Harry (brian@turtle.wholok.com) has left ## [02:04:38] <HelloBot> Goodbye, Harry!
Creating $irc objectThe first thing we need to do is create the $irc object. The $irc object can handle more than one IRC connection, but for this tutorial we'll just be using one. So, let's do it: use Net::IRC; # create the IRC object my $irc = new Net::IRC;
Creating $conn objectNow we need to create a connection object. This requires quite a few arguments, which should make sense if you're familiar with IRC. Our interactions with the IRC server will be through the $conn object. # Create a connection object. You can have more than one "connection" per # IRC object, but we'll just be working with one. my $conn = $irc->newconn( Server => shift || 'turtle.wholok.com', # Note: IRC port is normally 6667, but my firewall won't allow it Port => shift || '80', Nick => 'HelloBot', Ircname => 'I like to greet!', Username => 'hello' ); # We're going to add this to the conn hash so we know what channel we # want to operate in. $conn->{channel} = shift || '##'; The "shift ||" parts allow us to change these parameters on the command line. For example, I could run: ./hello_nick.pl irc.slashnet.org 6667 #mychannel
Event driven programmingSo how does HelloBot know when someone joins the channel? In an iterative program, you would poll the IRC filehandle, parse the input, and call a subroutine if you have a join or leave event. Using Net::IRC removes the first two steps. You merely have to decide which events you want to handle, and define subroutines to handle them.
on_connectThe first event we have to handle is connecting. We know that we're connected when we get the end of MOTD event, which is normally sent by an IRC server. This event is given the value "376". When we get the event, what we want to do join our channel and greet the channel. Here's how: sub on_connect { # shift in our connection object that is passed automatically my $conn = shift; # when we connect, join our channel and greet it $conn->join($conn->{channel}); $conn->privmsg($conn->{channel}, 'Hello everyone!'); $conn->{connected} = 1; } # The end of MOTD (message of the day), numbered 376 signifies we've connect $conn->add_handler('376', \&on_connect); In the on_connect subroutine, we introduce two methods of the $conn object, join and privmsg. join simply takes a channel as an argument. prvimsg is a bit more complicated. It is used to send both public and private messages (despite its name). If you put a channel name as the first argument, you will send publically to a channel. If you put a nick, you will send privately to the person with that nick. The second argument is the string you want to send. After we've defined our subroutine, we use the add_handler method to link it to the event we're handling. The first argument is the event identifier, which is the string "376," not the number 376. It will not work if you send it as a number. Most of the popular events are identified by alphabetic strings, as we'll see later. The next argument is a reference to a function.
on_joinNext, we'll set up our join handler: sub on_join { # get our connection object and the event object, which is passed # with this event automatically my ($conn, $event) = @_; # this is the nick that just joined my $nick = $event->{nick}; # say hello to the nick in public $conn->privmsg($conn->{channel}, "Hello, $nick!"); } $conn->add_handler('join', \&on_join); The new thing here is the $event hash that is passed to the function. Most event handlers will take this as an argument (and it's the only argument event handlers will take, not counting the object, of course). Depending on the event, the contents of this hash will vary. Later, we'll see how to print out the contents of $event to learn how to program for different events. The only part of $event we're interested in here is the nick that initiated the event. This is held in an obvious place, as we can see. Next, we say "Hello" to the nick.
$irc->start()Now that we've set up our handlers, we're ready to start IRC. This is done with the simple command: $irc->start();
Now we should be connected and ready to roll. Notice that we're using the first object we created, $irc, and
not the $conn object. This is because Net::IRC can handle more than one IRC connection in a single
instance.
What RokerBot doesRokerBot (you'll also need WeatherGet.pm and WeatherParse.pm) retrieves weather information from http://www.weatherunderground.com/auto/roker/ and sends it to IRC. It does this whenever a user says "!roker [city], [state]" [02:03:40] RokerBot (roker@turtle.wholok.com) has joined ## [02:03:40] <RokerBot> booya! it's Roker time! [02:05:58] <wholok> !roker New York, NY [02:05:59] <RokerBot> wholok, it's 50 F in New York, NY right now. Wind is NNW at 8 mph. Humidity is 61%. Roker would say it's Clear. Tonight Mostly clear. Lows in the lower 40s. North wind 10 to 15 mph becoming light. Thursday Mostly sunny. Highs in the upper 50s. Light and variable wind becoming south and increasing to 10 mph. [02:08:55] <wholok> !roker adsfoi34 234lkndf [02:08:57] <RokerBot> Unable to retrieve weather. Sorry, wholok.
on_publicThis is a new event for us to handle, the public message event. Here's how RokerBot handles it: sub on_public { # on an event, we get connection object and event hash my ($conn, $event) = @_; # this is what was said in the event my $text = $event->{args}[0]; # regex the text to see if it begins with !roker if ($text =~ /^\!roker (.+)/) { # if so, pass the text and the nick off to the weather method my $weather_text = weather($1, $event->{nick}); # wrap text at 400 chars (about as much as you should put # into a single IRC message my $wrapped_text = $wrapper->wrap($weather_text); my @texts = split("\n", $wrapped_text); # $event->{to}[0] is the channel where this was said foreach (@texts) { $conn->privmsg($event->{to}[0], $_); } } } $conn->add_handler('public', \&on_public); Here there are two new parts of the $event hash that we are dealing with. The first one is $event->{args}. This is a reference to an array. The first value in this array is the text of the public message. We regex this text to check if it begins with !roker. If so, we pass off the rest of the message to the weather subroutine, which will return our weather text. The other new $event hash part we are looking at is {to}. This is an array reference of each entity the event was sent to. In our case, we simply want our channel name, so we take the first part of that array. From there, we wrap our text to a reasonable limit (sometimes the weather reports can be long), and send each line to the channel.
on_msgOur next event to handle is msg. An msg is a private message sent to the IRC bot directly. It is best to use msg events to interact with your bot if there are going to be a lot of interactions that could annoy other users. There's no practical difference in using msg as opposed to public messages, so here's a quick example: sub on_msg { my ($conn, $event) = @_; # Under normal circumstances, simply reply to the nick that there's # no reason to /msg RokerBot. $conn->privmsg($event->{nick}, "Don't nobody /msg RokerBot."); } $conn->add_handler('msg', \&on_msg); Obviously, you could make this interaction more useful, but the example shows the concept.
Other events and methodsCheck out the source code of Events.pm (or do a perldoc Net::IRC::Events) to see what other events an IRC bot can handle. When experimenting with these events, you may want to use the module Data::Dumper to see what the event is sending you: use Data::Dumper; sub default { # This is helpful to see what an event returns. Data::Dumper will # recursively reveal the structure of any value my ($conn, $event) = @_; print Dumper($event); } # experiment with the cping event, printing out to standard output $conn->add_handler('cping', \&default); Pretty much any IRC client command is a method of IRC::Connection. If you look in the source of Connection.pm, you'll get fairly good documentation on how to call them. For example, here's how you can use the mode method to make someone an op: $conn-$gt;mode('#linux', '+o', 'tux');
What TriviaBot doesTriviaBot (you'll also need Trivia.pm) takes questions from a database, asks them, and checks users for answers to the question. There's on tricky thing, though, it has to give a 10 second interval between each question. With our event driven model, we can't quite do this. [01:52:30] TriviaBot (brian@L36-72.DATANET.NYU.EDU) has joined ## [01:52:30] <TriviaBot> booya! it's Trivia time! [01:53:10] <Joe> !trivon [01:53:10] <TriviaBot> Trivia is on! First question in 10 seconds. [01:53:22] <TriviaBot> Pick a hand. [01:53:22] <TriviaBot> ***** [01:53:25] <Joe> le [01:53:30] <Joe> left [01:53:34] <wholok> ri [01:53:34] <TriviaBot> ri*** [01:53:35] <wholok> rig [01:53:35] <TriviaBot> rig** [01:53:39] <Sally> right [01:53:39] <TriviaBot> Correct answer by Sally! [01:53:39] <TriviaBot> right [01:53:39] <TriviaBot> Next question in 10 seconds [01:53:51] <TriviaBot> How large is an IPv6 address? [01:53:51] <TriviaBot> *** **** [01:53:56] <wholok> really big [01:54:01] <Joe> 128 bits [01:54:01] <TriviaBot> Correct answer by Joe! [01:54:01] <TriviaBot> 128 bits [01:54:01] <TriviaBot> Next question in 10 seconds [01:54:12] <Joe> !score [01:54:12] <TriviaBot> Score: [01:54:12] <TriviaBot> Sally: 1 [01:54:12] <TriviaBot> Joe: 1 [01:54:13] <TriviaBot> Who invented Perl? [01:54:13] <TriviaBot> ***** **** [01:54:28] <Sally> Larry rall [01:54:28] <TriviaBot> Larry *all [01:54:32] <wholok> larry wall [01:54:32] <TriviaBot> Correct answer by wholok! [01:54:32] <TriviaBot> Larry Wall [01:54:32] <TriviaBot> Next question in 10 seconds [01:54:36] <wholok> ha ha Sally! [01:54:44] <TriviaBot> Who is the Linux mascot? [01:54:44] <TriviaBot> *** [01:54:50] <Joe> penguin [01:54:56] <wholok> TUX! [01:54:56] <TriviaBot> Correct answer by wholok! [01:54:56] <TriviaBot> Tux [01:54:56] <TriviaBot> Next question in 10 seconds [01:55:08] <TriviaBot> What TV network does Mickey Mouse own? [01:55:08] <TriviaBot> *** [01:55:14] <Sally> NBC [01:55:14] <TriviaBot> *BC [01:55:19] <wholok> ABC [01:55:19] <TriviaBot> Correct answer by wholok! [01:55:19] <TriviaBot> ABC [01:55:19] <TriviaBot> Next question in 10 seconds [01:55:23] <wholok> !trivoff [01:55:23] <TriviaBot> Trivia is off! [01:55:25] <wholok> !score [01:55:25] <TriviaBot> Score: [01:55:25] <TriviaBot> Sally: 1 [01:55:25] <TriviaBot> Joe: 1 [01:55:25] <TriviaBot> wholok: 3
Handling timingOur problem is fixed with the method do_one_loop. This takes the place of the start method. After establish our handlers, we call it like this: sub handle_trivia_loop { my $conn = shift; $_ = $conn->{trivia}{status}; STATUS: { if (/^getquestion$/) { # get question from trivia object $conn->{trivia}->get_question(); # say the question, and show the hint repeat_question($conn); repeat_hint($conn); last STATUS; } if (/^waiting$/) { handle_waiting($conn); last STATUS; } } } # while forever, handle_trivia, then do an IRC loop while (1) { handle_trivia_loop($conn); $irc->do_one_loop(); } Inside of handle_trivia_loop, we check timestamps if we need a 10 second interval. We also get a new question if our status tell us so. However, checking trivia answers is still done with the event driven model.
Parsing commandsOne of the hallmarks of an IRC bot that takes public command is the syntax "!command". This allows the bot to easily parse the traffic in the channel, since it's very unlikely that someone would want to say a real sentence starting with "!". sub on_public { # two args are passed on events, the connection object and a # hash that describes the event my ($conn, $event) = @_; # this is the text of the event, eg, what the person said my $text = $event->{args}[0]; # we first check to see if it's a command (if it starts with !) if ($text =~ /^\!(.+)$/) { on_public_command($conn, $1, $event->{nick}); } # otherwise, we assume it's a guess to the current question else { # check the answers my $res = $conn->{trivia}->check_answer($event->{nick}, $text); # if we've got the right answer, then celebrate if ($res) { on_answer($conn, $res, $event->{nick}); } # otherwise, repeat the current hint else { repeat_hint($conn); } } } sub on_public_command { # here, we get the connection object, the text of the command, and # the nick of the person who issued the command my ($conn, $command, $nick) = @_; # branch off given the nature of the command $_ = $command; COM: { # this turns trivia on if (/^trivon$/) { # don't turn on trivia unless it's off if ($conn->{trivia}{status} ne '') { last COM; } # set status to wait 10 seconds $conn->{trivia}{status} = 'waiting'; # alert users that trivia is starting $conn->privmsg($conn->{channel}, "Trivia is on! First question in 10 seconds."); # save the current timestamp $conn->{newtime} = time; # set the amount we've waited to 0 $conn->{waittime} = 0; last COM; } if (/^trivoff$/) { # set status to nothing $conn->{trivia}{status} = ''; # tell everyone that trivia over $conn->privmsg($conn->{channel}, "Trivia is off!"); last COM; } if (/^score$/) { # we want to see the scores, so let's have 'em handle_score($conn); last COM; } } }
|