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"); } }