#!/usr/bin/perl -w =head1 VoteBot This is the #c-lounge vote bot. Author: D. Robins <dbrobins@davidrobins.net> (czth) History: 20030210 dbr created XXX suggestions XXX allow comments with votes (alycat) vote (yes|no) for foo "comment" comments for <> abstain / don't care vote (alycat) =cut package VoteBot; use strict; use POE qw(Kernel Session Component::IRC Wheel::ReadLine); use Data::Dumper; use IO::File; use POSIX qw(strftime); my $Who = 'czth!dbrobins@davidrobins.net'; my $Id = 'VoteBot'; my $Me = 'C-Vote'; my $Lounge = '#c-lounge'; my $Key = 'polite'; my $Data = 'VoteBot.dat'; my $TZ = 'CST'; my %Vote; my $Log; my $Quit; sub now { my $t = @_ ? shift : time(); $t ? strftime('%Y-%m-%d %H:%M '.$TZ,localtime($t)) : '<unknown>' } # log to file sub flog(@) { unless(defined $Log) { $Log = IO::File->new(">>VoteBot.log") or die "create log: $!"; $Log->autoflush(); $Log->print("*** logging started at ".now()." ***\n"); } my $line = join ', ',@_; $line =~ s/\n?$/\n/; $Log->print(strftime('%H:%M ',localtime()).$line); } sub _start { my ($k,$s,$h) = @_[KERNEL,SESSION,HEAP]; flog "_start: read votes\n"; %Vote = %{do $Data}; flog "votes: ".(keys %Vote)."\n" if scalar keys %Vote; flog "creating ReadLine wheel\n"; $h->{wheel} = POE::Wheel::ReadLine->new(InputEvent => 'input'); $h->{wheel}->clear(); $h->{wheel}->put("VoteBot\n"); $h->{wheel}->get('> '); flog "_start: register P::C::I\n"; $k->post($Id,'register','all'); $k->post($s,'reconnect'); } sub _stop { flog "_stop - saving votes\n"; my $vote = Dumper(\%Vote); $vote =~ s/^\$VAR1 = {/{/; open VOTE,">$Data" or die "write: $!"; print VOTE $vote; close VOTE; $Log->close() if $Log; } sub _default { flog "_default caught unhandled $_[ARG0] event\n"; flog " parameters: @{$_[ARG1]}\n"; 0 } sub say { my ($k,$target,$text) = @_; my $pre = ''; if(ref $target) { ($target,my $nick) = @{$target}; $pre = "$nick: "; } $k->post($Id,'privmsg',$target,$pre.$_) for split /\n/,$text; } sub irc_connected { flog "connected\n"; } sub irc_001 { # welcome message, ready for commands my ($self,$k) = @_[OBJECT,KERNEL]; flog "welcome\n"; $k->post($Id,'mode',$Me,'+i'); $self->do_join($k); } sub irc_kick { my ($k,$who,$where,$nick,$why) = @_[KERNEL,ARG0..ARG3]; $k->delay('rejoin',30) if lc $nick eq lc $Me and not $Quit; } sub irc_socketerr { my ($k,$err) = @_[KERNEL,ARG0]; flog "connect failed: $err\n"; $k->delay('reconnect',600) unless $Quit; } sub irc_error { my ($k,$err) = @_[KERNEL,ARG0]; flog "server error: $err\n"; $k->delay('reconnect',600) unless $Quit; } sub irc_disconnected { my $k = $_[KERNEL]; flog "disconnected\n"; $k->delay('reconnect',30) unless $Quit; } sub irc_public { my ($self,$k,$who,$where,$text) = @_[OBJECT,KERNEL,ARG0..ARG2]; return unless grep { lc $_ eq lc $Lounge } @{$where}; $self->command($k,$who,1,$text); } sub have_topic { my ($self,$k,$target,$topic) = @_; if(not defined $topic) { unless(scalar keys %Vote) { say($k,$target,'no votes at present'); return 0; } return 1; } unless(exists $Vote{$topic}) { say($k,$target,"sorry, no such topic as '$topic'"); return 0; } 1 } sub split_votes { my ($self,$vote) = @_; my ($yes,$no) = ([],[]); while(my ($nick,$info) = each %{$vote->{vote}}) { push @{$info->[0] ? $yes : $no}, $nick; } ($yes,$no) } # type is 0, private, 1, public, 2, local sub command { my ($self,$k,$who,$type,$text) = @_; my $nick = (split /!/,$who)[0]; $text =~ s/^\s+//; $text =~ s/\s+$//; $text =~ s/\s+/ /g; my $target = $text =~ s/ (on|in|to) chan(nel)?$// ? [$Lounge,$nick] : $nick; $target = $Lounge if 2 == $type; # local commands output to channel # vote (yes|no) (on|for) <topic> if($text =~ /^vote (yes|no) (?:on|for) (\w+)$/i) { unless(1 == $type) { say($k,$target,"sorry, votes must be cast in the channel"); return; } $target = [$Lounge,$nick]; my $topic = lc $2; return unless $self->have_topic($k,$target,$topic); my $yes = $1 =~ /^y/; my $vote = $yes ? 'yes' : 'no'; my $rec = $Vote{$topic}; my $desc = $rec->{desc}; unless(exists $rec->{vote}->{$nick}) { $rec->{vote}->{$nick} = [$yes,$who]; say($k,$target,"registered '$vote' vote for '$topic'"); return; } if($rec->{vote}->{$nick}->[0] eq $yes) { say($k,$target,"I already had it that way"); } else { say($k,$target,"changed vote to '$vote' for '$topic'"); $rec->{vote}->{$nick} = [$yes,$who]; } # new vote "<description>" as <topic> } elsif($text =~ /^new vote \"([^"]+)\" as (\w+)$/i) { if(0 == $type) { say($k,$target,"sorry, votes must be created in the channel"); return; } $target = [$Lounge,$nick]; my ($topic,$desc) = (lc $2,$1); if(exists $Vote{$topic}) { say($k,$target,"sorry, topic '$topic' already exists"); return; } $Vote{$topic} = { desc => $desc, topic => $topic, vote => {}, when => time(), author => $nick }; say($k,$target,"added vote '$desc' as '$topic'"); # stats for <topic> } elsif($text =~ /^stats for (\w+)$/i) { my $topic = lc $1; return unless $self->have_topic($k,$target,$topic); say($k,$target,"topic '$topic': $Vote{$topic}->{desc} \@ ".now($Vote{$topic}->{when}).' by '.$Vote{$topic}->{author}); my ($yes,$no) = $self->split_votes($Vote{$topic}); my ($y,$n) = ($#{$yes}+1,$#{$no}+1); say($k,$target,"$y yes vote".($y==1 ? '' : 's').(@{$yes} ? ": @{$yes}" : '')); say($k,$target,"$n no vote".($n==1 ? '' : 's').(@{$no} ? ": @{$no}" : '')); if($y+$n) { say($k,$target,sprintf "%.2f%% yes, %.2f%% no (%d vote%s)", 100*$y/($y+$n),100*$n/($y+$n),$y+$n,($y+$n == 1) ? '' : 's'); } # (show|list) votes } elsif($text =~ /^list votes$/) { return unless $self->have_topic($k,$target); say($k,$target,"currently voting on '".(join "', '",keys %Vote)."'"); # summarize votes } elsif($text =~ /^summ(?:arize)? votes$/) { return unless $self->have_topic($k,$target); my @sum; while(my ($topic,$vote) = each %Vote) { my ($yes,$no) = $self->split_votes($vote); push @sum, "$topic (".($#{$yes}+1).'/'.($#{$yes}+$#{$no}+2).')'; } say($k,$target,join ', ',@sum); # describe vote <topic> } elsif($text =~ /^desc(?:ribe)? vote (\w+)$/) { my $topic = lc $1; return unless $self->have_topic($k,$target,$topic); say($k,$target,"'$topic' is ".$Vote{$topic}->{desc}.' created by '.$Vote{$topic}->{author}.' @ '.now($Vote{$topic}->{when})); # votes (for|by) <topic> } elsif($text =~ /^votes for (\w+)$/ or $text =~ /^my votes$/) { my $user = defined $1 ? $1 : $nick; my (@yes,@no); while(my ($topic,$vote) = each %Vote) { my $info = $vote->{vote}->{$user}; push @{$info->[0] ? \@yes : \@no}, $topic if defined $info; } if(not @yes and not @no) { say($k,$target,"$user has no votes"); } else { @yes = qw(<nothing>) unless @yes; @no = qw(<nothing>) unless @no; say($k,$target,"$user voted yes on ".(join ', ',@yes)." and no on ". join(', ',@no)); } # show votes } elsif($text =~ /^show (votes|unvoted)$/i) { return unless $self->have_topic($k,$target); my $unv = lc $1 eq 'unvoted'; my $pre = $unv ? 'you have not voted on ' : 'current votes are '; my @vote; while(my ($topic,$vote) = each %Vote) { next if $unv and $vote->{vote}->{$nick}; push @vote, "$topic (".$vote->{desc}.')'; } if($#vote < 0) { say($k,$target,$unv ? 'no new votes' : 'no votes'); } else { say($k,$target,$pre.join ', ',@vote); } # help vote } elsif($text =~ /^help vot\w+$/i) { # voting, votes, vote, ... say($k,$target,"new vote \"<description>\" as <key>, vote (yes|no) on <key>, stats for <key>, summarize votes, describe vote <key>, list votes, show votes, show unvoted, my votes, votes (for|by) <nick>"); # unknown command in private } elsif(0 == $type) { say($k,$target,"sorry... (try 'help vote')") if 0 == $type; } return unless 2 == $type; # admin commands # delete vote <topic> if($text =~ /^delete vote (\w+)$/) { my $topic = lc $1; return unless $self->have_topic($k,$target,$topic); delete $Vote{$topic}; say($k,$target,"deleted vote '$topic'"); # quit <message> } elsif($text =~ /^quit(?: (.+$))?$/) { my $bye = defined $1 ? $1 : ''; $Quit = 1; # don't reconnect, we're leaving $k->post($Id,'quit',$bye); # say <text> } elsif($text =~ /^s(?:ay)? (.+)$/) { say($k,$target,$1); } } sub irc_msg { my ($self,$k,$who,$where,$text) = @_[OBJECT,KERNEL,ARG0..ARG2]; $self->command($k,$who,0,$text); } sub do_join { my ($self,$k) = @_; $k->post($Id,'join',$Lounge,$Key); } sub rejoin { my ($self,$k) = @_[OBJECT,KERNEL]; flog "rejoin\n"; $self->do_join($k); } sub reconnect { my $k = $_[KERNEL]; flog "connecting\n"; $k->post($Id,'connect',{ Nick => $Me, UserName => 'cvotebot', IrcName => "#c ops' voting bot", Server => 'irc.qeast.net', Port => 6669, }); } sub input { my ($self,$k,$h,$text) = @_[OBJECT,KERNEL,HEAP,ARG0]; $text = 'quit ^C' unless defined $text; # interrupt -> quit flog "local: '$text'"; $h->{wheel}->addhistory($text); $self->command($k,$Who,2,$text); if($Quit) { $h->{wheel}->put("quit\n\n"); delete $h->{wheel}; flog "quitting"; } else { $h->{wheel}->get('> '); } } package main; use strict; use POE qw(Kernel Session Component::IRC); POE::Component::IRC->new($Id) or die "P::C::I init failed"; my $bot = bless {},'VoteBot'; POE::Session->create( object_states => [$bot => [qw(_start _stop _default irc_connected irc_001 irc_kick irc_disconnected irc_public irc_socketerr irc_error irc_msg rejoin reconnect input)]], ); eval { $poe_kernel->run(); }; if($@) { VoteBot::flog "died: $@"; }
Site design and content copyright ©2003 David B. Robins. All Rights Reserved. pH?