NYLXS - Do'ers
Mon Mar 25 18:58:07 2002 e.s.t.
JOURNAL
HOME

CURRENT
ISSUE
HOME

TABLE
OF
CONTENTS

PREVIOUS
ARTICLE

NEXT
ARTICLE


New York Linux Scene Journal

What is an IRC bot?

  • Automated client
  • Examples
  • Etiquette

What you will need to program an IRC bot

  • Perl
  • Net::IRC module
  • An IRC server (own for debugging)
  • Other software for functionality

A very simple bot: HelloBot

  • What HelloBot does
  • Creating $irc object
  • Creating $conn object
  • Event driven programming
  • on_connect
  • on_join
  • irc->start()

Handling other common events: RokerBot

  • What RokerBot does
  • on_public
  • on_msg
  • Other events in Event.pm

TriviaBot: using $irc->do_one_loop

  • What TriviaBot does
  • Handling timing
  • Parsing commands

Resources


What 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:

  • File serving: This type of bot emulates an FTP program by interfacing with a filesystem. Users talk to the bot using private messages with commands like "ls" and "get". The user can send and receive files using DCC (a part of IRC that allows the initiation of peer-to-peer file transfers).
  • Channel administration: This bot maintains a list of channel ops (people who run the channel) and makes sure they stay in control of it, even if individual people are disconnected. They may also kick people from the channel who violate its etiquette (e.g., talking in all caps, using colors, flooding, etc.)
  • Games: Some bots will allow the people in the channel to text-based games. We'll learn later how to program a trivia bot.

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.

What you will need to program an IRC bot

Perl

You 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 Module

Net::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 server

You'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 functionality

No 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.

A very simple bot: HelloBot

What HelloBot does

HelloBot 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 object

The 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 object

Now 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 programming

So 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_connect

The 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_join

Next, 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.

Handling other common events: RokerBot

What RokerBot does

RokerBot (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_public

This 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_msg

Our 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 methods

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

TriviaBot: using $irc->do_one_loop

What TriviaBot does

TriviaBot (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 timing

Our 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 commands

One 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;
                }       
        }       
}

Resources