http://qs321.pair.com?node_id=1070166

three18ti has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks

I'm attempting to learn POE (for fun) and trying to write an IRC bot to watch log files.

The POE IRC Cookbook has a link to an old article from 2004 for creating a log watcher bot for IRC. However, it seems that this code is deprecated as it doesn't match the design patterns outlined in the POE manual and "POE::Component::IRC->new" generates a "->new is deprecated" warning. So I'd like to update the script to be compliant with the current versions of POE as well as helping me understand POE.

The first thing I don't understand is the use of "heartbeat" on line 39 of the example program

=39= $_[KERNEL]->yield("heartbeat"); # start heartbeat

This generates a "session 4 caught an unhandled heartbeat event." log message when running the example script. There is a "my_heartbeat" function defined, but if you replace the "heartbeat" with a call to "my_heartbeat", then the test script just posts "$time: heartbeat: beep" every ten seconds. I don't think POE actually has a "heartbeat" function, so I'm thinking that my_heartbeat is completely unnecessary. (commenting it out doesn't affect the test script from running) Maybe it's useful for debugging.

Anyway, I rewrote the bot, but it doesn't seem to want to post messages to the channel and it doesn't post the heartbeat messages like the original example does. Can someone please point me in a direction as to where I should begin my debugging? I'm clearly not doing something right. I wonder if creating a new POE::Session has anything to do with the issue, but I thought there was only one POE::Session...

This is the meat of the code, the first 50 or so lines are boilerplate for setting the various config options, I've included the full script in a spoiler tag, I swear there was a "readmore" tag but I can't find it in the PerlMonks docs.

Thanks for your assistance!

my %FOLLOWS = ( ACCESS => "/var/log/messages", ); my $IRC_ALIAS = $nick; my $SKIPPING = 0; # if skipping, how many we've done my $SEND_QUEUE; # cache my $CHANNEL = $channel; POE::Session->create( package_states => [ main => [ qw( _default _start irc_public irc_join my_add my_tailed my_heartbeat ) ], ], ); $poe_kernel->run(); sub _start { my $irc = POE::Component::IRC::State->spawn( nick => $nick, Username => $username, ircname => $desc, server => $server_address, ); $_[HEAP]{dns} = POE::Component::Client::DNS->spawn; $_[HEAP]{irc} = $irc; $irc->plugin_add( 'NickServID', POE::Component::IRC::Plugin::NickServID->new( Password => 'iamrobot' ), ); $irc->plugin_add( 'AutoJoin', POE::Component::IRC::Plugin::AutoJoin->new( Channels => $channel, ), ); $irc->yield( register => 'all' ); $irc->yield( 'connect' ); $irc->yield("my_heartbeat"); # start heartbeat $irc->yield("my_add", $_) for keys %FOLLOWS; # start following return; } sub my_add { my $trailing = $_[ARG0]; my $session = $_[SESSION]; POE::Session->create( inline_states => { _start => sub { $_[HEAP]->{wheel} = POE::Wheel::FollowTail->new( Filename => $FOLLOWS{$trailing}, InputEvent => 'got_line', ); }, got_line => sub { $_[KERNEL]->post( $session => my_tailed => time, $trailing, $_[ARG0] ); }, }, ); } sub my_tailed { my ($time, $file, $line) = @_[ARG0..ARG2]; ## $time will be undef on a probe, or a time value if a real line ## PoCo::IRC has throttling built in, but no external visibility ## so this is reaching "under the hood" $SEND_QUEUE ||= $_[KERNEL]->alias_resolve($IRC_ALIAS)->get_heap->{send_queue}; ## handle "no need to keep skipping" transition if ($SKIPPING and @$SEND_QUEUE < 1) { $_[KERNEL]->post($IRC_ALIAS => privmsg => $CHANNEL => "[discarded $SKIPPING messages]"); $SKIPPING = 0; } ## handle potential message display if ($time) { if ($SKIPPING or @$SEND_QUEUE > 3) { # 3 msgs per 10 seconds $SKIPPING++; } else { my @time = localtime $time; $_[KERNEL]->post( $IRC_ALIAS => privmsg => $CHANNEL => sprintf "%02d:%02d:%02d: %s: %s", ($time[2] + 11) % 12 + 1, $time[1], $time[0], $file, $ +line); } } ## handle re-probe/flush if skipping if ($SKIPPING) { $_[KERNEL]->delay($_[STATE] => 0.5); # $time will be undef } } sub my_heartbeat { $_[KERNEL]->yield(my_tailed => time, "heartbeat", "beep"); $_[KERNEL]->delay($_[STATE] => 10); } # Default event handler, for some reason, stops output after logged in sub _default { my ($event, $args) = @_[ARG0 .. $#_]; my @output = ( "$event: " ); for my $arg (@$args) { if (ref $arg eq 'ARRAY') { push @output, '[' . join ', ', @$arg . ']'; } else { push @output, "'$arg'"; } } print join ' ', @output, "\n"; return; } # Event when a public message is broadcast to a channel sub irc_public { my ($sender, $where, $what) = @_[SENDER, ARG1, ARG2]; my $nick = parse_nick $_[ARG0]; my $irc = $_[SENDER]->get_heap; } # Event when someone joins a channel sub irc_join { my $nick = parse_nick $_[ARG0]; my $channel = $_[ARG1]; my $irc = $_[SENDER]->get_heap; if ($nick eq $irc->nick_name) { $irc->yield(privmsg => $channel, "I am online and responding t +o commands"); } }

Here's the full version of the code

#!/usr/bin/perl use 5.010; use strict; use warnings; use POE; use Config::Any; use File::HomeDir; use POE::Wheel::FollowTail; use Getopt::Long::Descriptive; use POE::Component::IRC::State; use POE::Component::IRC::Plugin::AutoJoin; use POE::Component::IRC::Plugin::NickServID; sub parse_nick { (split /!/, shift)[0] } my $home = File::HomeDir->my_home; # take options from command line my ($opt, $usage) = describe_options( "\%c \%o", [ "server-address|s=s" => 'IRC Server Address', + ], [ "server-port|p=s" => 'IRC Server Port [default: 6667 +]', ], [ "protocol|P=s" => 'IRC Server Protocol [default: +TCP]', ], [ "timeout|t=s" => 'IRC Server Connection Timeout +(in seconds) [default: 30]', ], [ "channel|c=s" => 'IRC Channel', + ], [ "config-name|n=s" => 'Config File Name', { default => 'testbot' }, + ], [ "nick|N=s" => 'IRC Nick', + ], [ "password|A=s" => 'IRC Password', + ], [ "username|u=s" => 'IRC Username, defaults to Nick + if unset', ], [ "hostname|h=s" => 'IRC Hostname', + ], [ "desc|d=s" => 'IRC User Desc.', + ], [], [ "verbose|v" => 'Print Status Messages' + ], [ "help|h" => 'Print This message and Exit', + ], ); ( print $usage->text ), exit if $opt->help; # get config name my $config = $opt->config_name; # look for config file my $cfg = Config::Any->load_stems( { stems => ["/etc/$config", "$home/etc/$config", "$home/etc/$con +fig", "./etc/$config", "./$config", ], use_ext => 1, } ); # take only the last loaded config file my $last_config = shift @$cfg; my ($file_name, $loaded_config); if ($last_config) { ($file_name, $loaded_config) = %$last_config; } else { ($file_name, $loaded_config) = ('NONE', {} ); } say "[*] Loaded config file $file_name" if $opt->verbose; # Thinking about config file design, my $server_address = $opt->server_address || $loaded_config->{'serve +r'}->{'address'} || 'myserver'; my $server_port = $opt->server_port || $loaded_config->{'serve +r'}->{'port'} || '6667'; my $server_protocol = $opt->protocol || $loaded_config->{'serve +r'}->{'protocol'} || 'tcp'; my $timeout = $opt->timeout || $loaded_config->{'serve +r'}->{'timeout'} || '30'; my $channel = $opt->channel || $loaded_config->{'serve +r'}->{'channel'} || [ '#foo',]; my $nick = $opt->nick || $loaded_config->{'user' +}->{'nick'} || 'testbot2'; my $password = $opt->password || $loaded_config->{'user' +}->{'password'} || ''; my $username = $opt->username || $loaded_config->{'user' +}->{'username'} || $nick; my $hostname = $opt->hostname || $loaded_config->{'user' +}->{'hostname'} || 'foo.test.irc'; my $desc = $opt->desc || $loaded_config->{'user' +}->{'desc'}; my %FOLLOWS = ( ACCESS => "/var/log/messages", ); my $IRC_ALIAS = $nick; my $SKIPPING = 0; # if skipping, how many we've done my $SEND_QUEUE; # cache my $CHANNEL = $channel; POE::Session->create( package_states => [ main => [ qw( _default _start irc_public irc_join my_add my_tailed my_heartbeat ) ], ], ); $poe_kernel->run(); sub _start { my $irc = POE::Component::IRC::State->spawn( nick => $nick, Username => $username, ircname => $desc, server => $server_address, ); $_[HEAP]{dns} = POE::Component::Client::DNS->spawn; $_[HEAP]{irc} = $irc; $irc->plugin_add( 'NickServID', POE::Component::IRC::Plugin::NickServID->new( Password => 'iamrobot' ), ); $irc->plugin_add( 'AutoJoin', POE::Component::IRC::Plugin::AutoJoin->new( Channels => $channel, ), ); $irc->yield( register => 'all' ); $irc->yield( 'connect' ); $irc->yield("my_heartbeat"); # start heartbeat $irc->yield("my_add", $_) for keys %FOLLOWS; # start following return; } sub my_add { my $trailing = $_[ARG0]; my $session = $_[SESSION]; POE::Session->create( inline_states => { _start => sub { $_[HEAP]->{wheel} = POE::Wheel::FollowTail->new( Filename => $FOLLOWS{$trailing}, InputEvent => 'got_line', ); }, got_line => sub { $_[KERNEL]->post( $session => my_tailed => time, $trailing, $_[ARG0] ); }, }, ); } sub my_tailed { my ($time, $file, $line) = @_[ARG0..ARG2]; ## $time will be undef on a probe, or a time value if a real line ## PoCo::IRC has throttling built in, but no external visibility ## so this is reaching "under the hood" $SEND_QUEUE ||= $_[KERNEL]->alias_resolve($IRC_ALIAS)->get_heap->{send_queue}; ## handle "no need to keep skipping" transition if ($SKIPPING and @$SEND_QUEUE < 1) { $_[KERNEL]->post($IRC_ALIAS => privmsg => $CHANNEL => "[discarded $SKIPPING messages]"); $SKIPPING = 0; } ## handle potential message display if ($time) { if ($SKIPPING or @$SEND_QUEUE > 3) { # 3 msgs per 10 seconds $SKIPPING++; } else { my @time = localtime $time; $_[KERNEL]->post( $IRC_ALIAS => privmsg => $CHANNEL => sprintf "%02d:%02d:%02d: %s: %s", ($time[2] + 11) % 12 + 1, $time[1], $time[0], $file, $ +line); } } ## handle re-probe/flush if skipping if ($SKIPPING) { $_[KERNEL]->delay($_[STATE] => 0.5); # $time will be undef } } sub my_heartbeat { $_[KERNEL]->yield(my_tailed => time, "heartbeat", "beep"); $_[KERNEL]->delay($_[STATE] => 10); } # Default event handler, for some reason, stops output after logged in sub _default { my ($event, $args) = @_[ARG0 .. $#_]; my @output = ( "$event: " ); for my $arg (@$args) { if (ref $arg eq 'ARRAY') { push @output, '[' . join ', ', @$arg . ']'; } else { push @output, "'$arg'"; } } print join ' ', @output, "\n"; return; } # Event when a public message is broadcast to a channel sub irc_public { my ($sender, $where, $what) = @_[SENDER, ARG1, ARG2]; my $nick = parse_nick $_[ARG0]; my $irc = $_[SENDER]->get_heap; } # Event when someone joins a channel sub irc_join { my $nick = parse_nick $_[ARG0]; my $channel = $_[ARG1]; my $irc = $_[SENDER]->get_heap; if ($nick eq $irc->nick_name) { $irc->yield(privmsg => $channel, "I am online and responding t +o commands"); } }

Replies are listed 'Best First'.
Re: [POE::Component::IRC] Updating The IRC Log Watcher bot
by rcaputo (Chaplain) on Jan 10, 2014 at 19:49 UTC

    Here's a refactored version that seems to do what you want. You'll have to merge it into your version. I cut out all the option and config file handling because it was easier for me to ignore all that.

    The immediate problem is that you called $irc->yield("my_heartbeeat") rather than $_[KERNEL]->yield("my_heartbeat") near the end of _start(). The IRC component doesn't know what you mean, and it discards the message.

Re: [POE::Component::IRC] Updating The IRC Log Watcher bot
by moritz (Cardinal) on Jan 11, 2014 at 08:08 UTC

      EDIT: I just wanted to come back and say, after the initial frustration of the sql stuff failing, it was relatively easy to setup. I've still not got the whole web side of things working completely, the plack stuff works but apparently mod_fastcgi has been replaced by mod_fcgid in RHEL6 so I had a heck of a time getting apache to... well... not work. I'd prefer nginx anyway. I'll take a crack at configuring a reverse proxy to run starman and the static content together. This is actually the opposite of what I was looking for, but it's something that I was planning on implementing eventually

      Hey, that's a pretty cool idea. But there seems to be an issue with the sql schema

      I get the following error on a vanilla checkout:

      ~/ilbot$ ./install Checking dependencies ... dependencies all OK Database Access =============== You need a mysql database where ilbot stores the logs. For installation you need privileges for creating tables and indxes. For running ilbot, you need INSERT, SELECT and UPDATE privileges. Database host [localhost]> Database port [3306]> Database name [ilbot]> Database username [ilbot]> Database password> mysupersecretpassword Now testing your database connection... Database connection is fine, creating the schema for you... Schema creation failed: DBD::mysql::db do failed: You have an error in + your SQL syntax; check the manual that corresponds to your MySQL ser +ver version for the right syntax to use near 'DROP TABLE IF EXISTS `i +lbot_lines`; DROP TABLE IF EXISTS `ilbot_day`; DROP TABLE' at line 2 at ./install line 116, <$_[...]> chunk 1.

      So I whacked the drop table lines, and I get the following error:

      ~/ilbot$ ./install Checking dependencies ... dependencies all OK Database Access =============== You need a mysql database where ilbot stores the logs. For installation you need privileges for creating tables and indxes. For running ilbot, you need INSERT, SELECT and UPDATE privileges. Database host [localhost]> Database port [3306]> Database name [ilbot]> Database username [ilbot]> Database password> mysupersecretpassword Now testing your database connection... Database connection is fine, creating the schema for you... Schema creation failed: DBD::mysql::db do failed: You have an error in + your SQL syntax; check the manual that corresponds to your MySQL ser +ver version for the right syntax to use near 'CREATE TABLE `ilbot_cha +nnel` ( `id` int(11) NOT NULL auto_increment, `ch' at line 2 at ./install line 116, <$_[...]> chunk 1.

      I was able to import the schema manually, so I don't think it's actually an issue with the sql syntax

      Anyway, I'm off to try and figure out how to get this thing to work now that it's "installed".

      Thanks for the link

        I think I know what's going on. The C API of most databases are limited to one statement per API call, and the installer tries to run several statements at once.

        Since the installer doesn't know how to detect the end of SQL statements, it uses blank lines for detection. Thus this commit should fix it.

        As for the HTTP side, you can use any of the servers listed on http://plackperl.org/#servers. I hear Starman is a good choice. Then an nginx or Apache as reverse proxy should be pretty easy to set up.

        If you have any more questions, feel free to drop in on #ilbot on irc.freenode.org.