Troydm's Blog

A personal blog about software development

Writing IRC Bot Using Perl 5 and POCO::IRC

Some people use IRC to chat, some don’t. It was invented a really long time ago and isn’t going away anytime soon despite some new generation alternatives popping up like Jabber.

Personally I always have my IRC client running (I’m using weechat + tmux) and chat with lots of interesting people who inspire me to try new technologies and learn something different every day. One person, who’s nickname I won’t name, was always telling me about how awesome Perl as a programming language is and how great it’s potential is thanks to CPAN that has almost 124k modules for any life situation. I always thought he was exaggerating and literally acting like a Perl fanboy. Perl was the first programming language I’ve learned back in the late 90’s and remembering how frustrating my experience with it was and how cryptic it really was for me do something with it when I was unexperienced and lacked lots of qualities that make up a any decent software engineer I was skeptic about using it again. Well, time passed, time always passes, and I haven’t written anything more than quick 50 line server scripts in Perl for almost 13 years. I’ve almost forgotten everything about Perl. Since lately I was having this crazy idea about writing IRC bot that could store and execute shell scripts on server so I could automate my servers through IRC, I thought why not write it in Perl. I’ve remembered that person who was always bragging about Perl’s greatness wrote an IRC bot in Perl using POE::Component::IRC so I’ve decided to try and use the same framework for my bot. It’s based on really popular POE event loop framework which is very easy to learn and use. Matt Cashner wrote a really good introduction article called Application Design with POE

The whole code for my bot is just 500 lines and is available from this repository shellbot. I’m going to walk through a key concepts that are essential for writing an IRC bot in POCO::IRC using my bot’s source code as a reference.

Chobits

Before we’ll start our Perl IRC bot we need some way to store configuration for it. Since CPAN has lots of modules that deal with configuration the choice wasn’t an easy one but I’ve decided to use YAML which is module for loading YAML data into Perl that can work the other way too. YAML is a simple markup language that is perfect for storing configuration and it’s really quick to learn. For loading YAML configuration I’ve used LoadFile function and to store Perl data back into file I’ve used DumpFile function. Just two simple functions that do all the complex work work for me. Since I wanted to store commands in the same file I just used Perl’s list construct to specify that I’m loading two separate YAML documents.

1
2
3
4
5
# Loading configuration 
my ($config, $commands) = LoadFile($config_file);

# Storing configuration
DumpFile($config_file, ($config, $commands));

Next step is to create POE Session and start event loop. Note that since my module is named Shellbot I need to specify it otherwise event loop won’t be able to call functions. Each of this functions are called by POE when specified events occur and all the bot logic is handled by those functions.

1
2
3
4
5
6
7
8
POE::Session->create(
    package_states => [
        Shellbot => [ qw(_start _default irc_join irc_msg irc_bot_addressed irc_connected 
                         got_job_stdout got_job_stderr got_job_close got_child_signal) ]
    ]
);

POE::Kernel->run();

First event that is executed after the POE event loop starts is _start event so we need to initialize bot state in that event. Also since POCO::IRC comes with some essential plugins and is modular by itself we can take advantage of this. Instead of manually making bot reconnect when it looses connection with server I’ve used Connector plugin. If we want our bot to automatically join some channel we can use AutoJoin plugin. And to easily handle when someone addresses bot I’ve used BotAddressed plugin. Since I wanted my bot to handle commands I could have used BotCommand plugin however I wanted my bot to have two modes of commands so I’ve decided to write bot command handling functions manually.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
my $irc = POE::Component::IRC::State->spawn(%opts);
$heap->{irc} = $irc;

# Connector plugin
$heap->{connector} = POE::Component::IRC::Plugin::Connector->new();
$irc->plugin_add( 'Connector' => $heap->{connector} );

# Autojoin plugin
if(exists($config->{'channels'}) && $config->{'channels'} > 0){
    $irc->plugin_add('AutoJoin', POE::Component::IRC::Plugin::AutoJoin->new(
       Channels => $config->{'channels'}
    ));
}

# BotAddressed plugin
$irc->plugin_add( 'BotAddressed', POE::Component::IRC::Plugin::BotAddressed->new() );

$irc->yield(register => qw(join msg connected));
$irc->yield('connect');

Since bot can be addressed both using a private message and refering him on a channel i’ve decided to handle both irc_msg and irc_bot_addressed events uniformly in msg_received function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sub irc_msg {
    my $irc = $_[SENDER]->get_heap();
    my @nick = ( split /!/, $_[ARG0] );
    my $msg = $_[ARG2];
    my $heap = $_[HEAP];
    my $kernel = $_[KERNEL];

    msg_received $irc, $heap, $kernel, $nick[0], $nick[1], '', $msg;
}

sub irc_bot_addressed {
    my $irc = $_[SENDER]->get_heap();
    my @nick = ( split /!/, $_[ARG0] );
    my $channel = $_[ARG1]->[0];
    my $msg = $_[ARG2];
    my $heap = $_[HEAP];
    my $kernel = $_[KERNEL];

    msg_received $irc, $heap, $kernel, $nick[0], $nick[1], $channel, $msg;
}

Before bot accepts a command we need some way to check if person who is issuing a command is authorized to do so. I’m doing a simple check of full IRC name that can be specified in configuration list option authorizations

1
2
3
4
sub is_authorized {
    my $name = $_[0];
    return grep { $_ eq $name } @{ $config->{'authorizations'} };
}

All that is left is to match commands in msg_recieved using Perl’s regex. This part is just a long series of bot command logic and if elsif else statements so I won’t reference them here. Also the key function is run_job which executes pre-stored shell script. It creates a temporary file that is executed using a shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
my $program = "/bin/bash";
if(exists($config->{'shell'})){
    $program = $config->{'shell'};
}

# create a temporary script
my $script = File::Temp->new();
my $scriptname = $script->filename;
chmod 0700, $script;
if(ref $cmd eq 'ARRAY'){
    foreach my $line (@{ $cmd }){
        print $script "$line\n";
    }
}else{
    print $script $cmd;
}
$program .= " $scriptname $args";

Shell execution is handled by POE using POE::Wheel::Run and output from shell is received on got_job_stdout and got_job_stderr functions

1
2
3
4
5
6
7
8
my $job = POE::Wheel::Run->new(
    Program      => $program,
    StdioFilter  => POE::Filter::Line->new(),
    StderrFilter => POE::Filter::Line->new(),
    StdoutEvent  => "got_job_stdout",
    StderrEvent  => "got_job_stderr",
    CloseEvent   => "got_job_close",
);

That’s all there is to it. To install and trying it out just look through a readme

Offcourse making a temporary shell script and executing it is generally unsafe. Also there is security concern that bot can be somehow hacked and commanded by unauthorized nick but I’m not sure how can this be done without changing vhost. That is why I called this bot a potentially unsafe. If anyone can find any security holes please do pull request or just email me patch.

To make this bot little more secure we need to make him connect to IRC using SSL. For this I’ll walk through a general steps for configuring CertFP and generating self signed certificate for Freenode as an example. First step is to register your bot’s nick with NickServ. Choose a nickname for your bot and start it up. Ask your bot to register with NickServ by private messaging him on IRC.

1
/msg botnick msg NickServ REGISTER nickservpass email@address.com

Shortly you’ll receive an email verification that will include verification code, privately message your bot again to verify your registration

1
/msg botnick msg NickServ VERIFY REGISTER botnick verificationcode

Now your bot’s nick is registered with NickServ so edit your configuration file and uncomment nickserv line to specify your nickservpass that will be used each time your bot will connect to server for authorization of your nick

Now we’ll generate a self signed certificate for SSL, i’ve used this manual for reference.

1
2
umask 077
openssl req -newkey rsa:2048 -days 730 -x509 -keyout botnick.key -out botnick.crt

You’ll be asked some questions and after that openssl will generate two files, your bot’s certificate and key. For CertFP to work we need a fingerprint of your key and certificate

1
2
cat botnick.crt botnick.key > botnick.pem
openssl x509 -sha1 -noout -fingerprint -in botnick.pem | sed -e 's/^.*=//;s/://g;y/ABCDEF/abcdef/'

After this you’ll get a fingerprint that you need to add to NickServ

1
/msg botnick msg NickServ CERT ADD fingerprint

Now all that is left is to specify in configuration that we are connecting using SSL certificate and key. Just uncomment sslcrt and sslkey options and specify full path to your bot’s certificate and key. Also don’t forget to change the port since we need to specify an SSL port instead of usual one, just specify 6697, 7000 or 7070 and restart your bot to enjoy fully secured connection to irc server. Also i’ve used Proc::Daemon for running this bot as daemon so you can uncomment daemon and log options to run it as daemon process.

So what did i learned from writing this small irc bot in Perl? Now the first most common mistake i was always making when coding in Perl was the difference between list and array, so I recommend this article by Mike Friedman Arrays vs. Lists in Perl that helped me a lot. Also dereferncing was the second most common mistake i had, until i’ve read this article Dereferencing in Perl. But the biggest thing i’ve learned was that Perl as a scripting language is really easy to use and powerfull despite some people claiming it’s dead, it’s still alive and it’s doing just fine! Just look how many modules are released on CPAN everyday! And it’s really really fun to code in, so everyone should try learning and using it!!!

Comments