--- /dev/null
+#!/usr/bin/perl
+###########################################################
+# #
+# PostgreSQL IRC Info Bot #
+# #
+# Copyright 2005-2006 by: #
+# Petr Jelinek, Devdas Bhagat, Steve Atkins, David Fetter #
+# Andreas Scherbaum, Greg Sabino Mullane #
+# #
+# Released under BSD licence #
+# #
+###########################################################
+
+###########
+# #
+# Modules #
+# #
+###########
+
+use strict;
+use warnings;
+use DBI;
+use POE;
+use POE::Component::IRC;
+use Getopt::Mixed "nextOption";
+use FileHandle;
+require("./config.pm");
+
+
+###############
+# #
+# Global Vars #
+# #
+###############
+my $irc_nick = "pg_docbot_test";
+my $irc_server = "invalid.freenode.net:6667";
+my $irc_name = "PostgreSQL IRC infobot";
+my $irc_pass = "";
+my @irc_channels = qw(
+ #postgresql
+ #pg_docbot
+ #postgresql-es
+ #pgfoundry
+ #postgresql-de
+ #postgresql-br
+ #foss.in
+ #dbi-link
+ #plparrot
+ #postgresql-eu
+ #arpug
+ ##NoEnd
+ #postgresql-pe
+ #slony
+ #skytools
+ #sfpug
+ #jdcon
+ #writeable_cte
+ #postgresqlfr
+ #pgtestfest
+ #pg_charlie_foxtrot
+);
+
+
+
+my @mute_nicks = qw(rtfm_please pg_docbot_ads);
+
+my $db_host = "";
+my $db_name = "";
+my $db_user = "";
+my $db_pass = "";
+my $db_port = 5432;
+my $db_schema = "";
+my $admin_commands = {
+ '?learn' => \&do_learn,
+ '?forget' => \&do_forget,
+ '?config' => \&do_config
+};
+my $messagefile = "messages.txt";
+my $query_prefix = '??';
+my $url_pattern = '(http|ftp|news|bt|https)://';
+
+use vars qw($dbh $SQL $sth %sth $count);
+
+my $pg_docbot = qw(pg_docbot);
+my $shutdown = 0;
+
+use constant DEBUG => 3;
+use constant ERROR => 2;
+use constant WARN => 1;
+use constant INFO => 0;
+
+## Maximum items found before we wrap our responses
+my $MAXWRAP = 3;
+
+## Allow "LIKE" searches via ???
+my $LIKESEARCH = 0;
+
+## Are searches case-sensitive?
+my $CASESEARCH = 0;
+
+## Can everyone perform all actions (if running on a private network)
+my $EVERYONE_AUTHORIZED = 0;
+
+## Lock the database (only needed if running more than one bot on the same database)
+my $LOCK_DATABASE = 1;
+
+my $loglevel = DEBUG;
+
+my %loglevels = (
+ 3 => 'DEBUG',
+ 2 => 'ERROR',
+ 1 => 'WARN',
+ 0 => 'INFO',
+);
+
+
+# stores the last question possible to answer if the main bot does not know
+%main::last_question = ();
+%main::last_question_ts = ();
+%main::last_question_nick = ();
+
+# store messages printed for keywords
+%main::messages = ();
+
+###############################
+# handle command line arguments
+###############################
+my $args_init_string = "help h d debug c=s config=s";
+Getopt::Mixed::init($args_init_string);
+my $help = 0;
+my $debug = 0;
+my $config_file = "";
+# parse options
+my ($argv_option, $argv_value, $argv_pretty);
+while (($argv_option, $argv_value, $argv_pretty) = nextOption()) {
+ if ($argv_option eq "h" or $argv_option eq "help") {
+ $help = 1;
+ }
+ if ($argv_option eq "d" or $argv_option eq "debug") {
+ $debug = 1;
+ }
+ if ($argv_option eq "c" or $argv_option eq "config") {
+ $config_file = $argv_value;
+ }
+}
+Getopt::Mixed::cleanup();
+
+#############
+# Config file
+#############
+my %cfg_directives = (
+ 'IRCNick', \$irc_nick,
+ 'IRCServer', \$irc_server,
+ 'IRCName', \$irc_name,
+ 'IRCPass', \$irc_pass,
+ 'IRCChannels', \@irc_channels,
+ 'IRCMuteNicks', \@mute_nicks,
+ 'DBHost', \$db_host,
+ 'DBName', \$db_name,
+ 'DBUsername', \$db_user,
+ 'DBPassword', \$db_pass,
+ 'DBPort', \$db_port,
+ 'DBSchema', \$db_schema,
+ 'MessageFile', \$messagefile
+);
+
+if (length($config_file) > 0) {
+ read_config($config_file);
+}
+
+# for later use - we will need to know whats configured nick
+# and whats our real nick
+my $my_nick = $irc_nick;
+
+# remove my own nick from mute_nicks
+my @mute_nicks_tmp = ();
+foreach (@mute_nicks) {
+ if (lc($_) ne lc($irc_nick)) {
+ push(@mute_nicks_tmp, $_);
+ }
+}
+@mute_nicks = @mute_nicks_tmp;
+
+## Make sure the database is up, might as well know now
+is_db_ok();
+
+################
+# Signal handlers
+################
+$SIG{INT} = \&death;
+$SIG{TERM} = \&death;
+$SIG{KILL} = \&death;
+$SIG{HUP} = \&reread_config;
+
+################
+# Logging
+################
+my $logfile = 'docbot.log';
+
+# Fork and log to a file unless the debug command line argument is given, in
+# which case log to STDOUT and don't fork.
+
+close (STDIN);
+
+if ($debug == 0) {
+ if (!open (STDOUT, ">>$logfile")) {
+ death ("Can't open logfile $logfile: $!\n");
+ }
+ if (!open (STDERR, ">>$logfile")) {
+ death ("Can't open the logfile $logfile for STDERR: $!\n");
+ }
+ autoflush STDOUT 1;
+ if (fork ()) {
+ exit(0);
+ }
+}
+
+################
+# Functions
+################
+
+
+########
+# read_config ( config_file )
+####
+# read config & set variables accordingly
+#
+sub read_config {
+ my $config_file = shift;
+
+ $main::config = docbot::config->new($config_file);
+ $main::config->set_autosave("off");
+
+ while (my ($key, $var) = each (%cfg_directives)) {
+ my $val = $main::config->get_key($key);
+ if (defined($val))
+ {
+ if (ref($var) eq 'ARRAY') {
+ @$var = split /;/, $val;
+ } else {
+ $$var = $val;
+ }
+ }
+ }
+
+ read_messages($messagefile);
+}
+
+
+########
+# reread_config ( )
+####
+# reread config and apply changes at runtime
+# currently handles only changes in DB settings, irc channels and message files
+#
+sub reread_config {
+ print_msg('Rereading config');
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "reread config");
+
+ my @old_irc_channels = @irc_channels;
+
+ read_config($config_file);
+
+ foreach my $channel (@irc_channels) {
+ @old_irc_channels = grep { lc($channel) ne lc($_) } @old_irc_channels;
+ $poe_kernel->post(pg_docbot => 'join', $channel);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "join new channel: $channel");
+ }
+
+ if (scalar(@old_irc_channels)>0) {
+ $poe_kernel->post(pg_docbot => 'part', @old_irc_channels);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "part old channel: @old_irc_channels");
+ }
+
+ if ($my_nick ne $irc_nick) {
+ $my_nick = $irc_nick;
+ $poe_kernel->post( $pg_docbot => nick => $my_nick);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "change nick to: $my_nick");
+ }
+
+ # remove my own nick from mute_nicks
+ my @mute_nicks_tmp = ();
+ foreach (@mute_nicks) {
+ if (lc($_) ne lc($irc_nick)) {
+ push(@mute_nicks_tmp, $_);
+ }
+ }
+ @mute_nicks = @mute_nicks_tmp;
+
+ read_messages($messagefile);
+}
+
+
+########
+# print_msg ( message, [ detail_level ] )
+####
+# message handler
+#
+sub print_msg {
+ my $msg = shift;
+ my $level = shift || $loglevel;
+ return if $level > $loglevel;
+ my $timestamp = localtime;
+ $msg =~ s/\n//g;
+ print "$timestamp ";
+ printf "%-8s", "[" . $loglevels{$level} . "]";
+ print "- $msg\n";
+ return 1;
+}
+
+
+sub word_wrap {
+ my ($sep, $len, @text) = @_;
+ $len -= length($sep);
+
+ my @ret;
+ my $line = '';
+
+ foreach my $word (@text) {
+ if (length($line.$word) > 250) {
+ push (@ret, $line);
+ $line = '';
+ }
+ $line .= (length($line) ? $sep : '').$word;
+ }
+ push (@ret, $line);
+
+ return @ret;
+}
+
+
+########
+# is_db_ok ( )
+####
+# handles db (re)connection, should be called before any sql commands
+#
+sub is_db_ok {
+ my $DSN = "dbi:Pg:dbname=$db_name";
+ if ($db_host) {
+ $DSN .= ";host=$db_host";
+ }
+ if ($db_port) {
+ $DSN .= ";port=$db_port";
+ }
+
+ $dbh = DBI->connect_cached(
+ $DSN,
+ $db_user,
+ $db_pass,
+ {
+ "RaiseError" => 0,
+ "PrintError" => 0,
+ "AutoCommit" => 0
+ }
+ );
+
+ unless ($dbh) {
+ print_msg("Can't connect to database - $DBI::errstr\n", ERROR);
+ return 0;
+ }
+
+ $dbh->{RaiseError} = 1;
+
+ if ($db_schema) {
+ $dbh->do("SET search_path = '$db_schema'");
+ $dbh->commit();
+ }
+
+ return 1;
+}
+
+
+########
+# get_answer ( query )
+####
+# search database for keywords
+#
+sub get_answer {
+ my ($query,$like) = @_;
+
+ $CASESEARCH or $query = lc $query;
+
+ my @keys = split(/\s+/, $query);
+ my $num = @keys;
+
+ unless (is_db_ok()) {
+ return \["Database error"];
+ }
+
+ my $searchnum = "search$num";
+ $like and $searchnum .= "like";
+
+ if (!exists $sth{$searchnum}) {
+ my $INNERSEARCH = "SELECT kurl FROM docbot_key WHERE lower(key) = ?";
+ $CASESEARCH and $INNERSEARCH =~ s/lower//;
+ $LIKESEARCH and $like and $INNERSEARCH =~ y/=/~/;
+ $SQL = "SELECT url FROM docbot_url WHERE id IN (\n";
+ $SQL .= join "\n INTERSECT\n" => map {"$INNERSEARCH\n"} @keys;
+ $SQL .= ")";
+ print_msg("Preparing $SQL\n", 9);
+ $sth{$searchnum} = $dbh->prepare($SQL);
+ }
+ $sth{$searchnum}->execute(@keys);
+ my $ret = $dbh->selectcol_arrayref($sth{$searchnum}, {Columns => [1]});
+ return $ret;
+}
+
+
+########
+# authorized ( {action, nick } )
+####
+# Check if a particular user is authorized to perform an action
+#
+sub authorized {
+
+ my $arg = shift;
+ if (ref $arg ne 'HASH') {
+ die qq{Subroutine "authorized" must be passed a hashref\n};
+ }
+
+ ## Check for required arguments
+ for my $req (qw(action nick)) {
+ exists $arg->{$req} and length $arg->{$req}
+ or die qq{Subroutine "authorized" required argument "$req" not found\n};
+ }
+
+ return "ok" if $EVERYONE_AUTHORIZED;
+
+ $SQL = "SELECT 1 FROM docbot_user WHERE LOWER(u_nick) = ?";
+ $sth = $dbh->prepare_cached($SQL);
+ $count = $sth->execute(lc $arg->{nick});
+ $sth->finish();
+
+ return $count == 1 ? 1 : 0;
+
+} ## end of authorized
+
+
+########
+# do_learn ( nick, query )
+####
+# auth user nick and save keyword(s) to database
+#
+sub do_learn {
+ my ($kernel, $nick, $channel, $query) = @_;
+
+ unless (is_db_ok()) {
+ return "Database error";
+ }
+
+ ## Make sure the user is authorized to perform this action
+ if (! &authorized({action => 'learn', nick => $nick, channel => $channel})) {
+ print_msg("Unauthorized ?learn from $nick\n", WARN);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "user $nick not authorized to do a 'learn' in channel: $channel");
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "learn query was: $query");
+ return "You are not authorized";
+ }
+
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "learn ($nick/$channel): $query");
+ my ($url, @keywords);
+ my @keys = split(/\s+/, $query);
+
+ # parse query
+ foreach my $keyword (@keys) {
+ if ($keyword =~ /^$url_pattern/) {
+ $url = $keyword;
+ # rewrite to current
+ if ($url =~ /^(http:\/\/.*?postgresql\.org\/docs\/)[0-9\.]+(\/.*)$/i) {
+ $url = $1 . "current" . $2;
+ }
+ # rewrite to static
+ if ($url =~ /^(http:\/\/\/\/.*?postgresql\.org\/docs\/current\/)interactive(\/.*)$/i) {
+ $url = $1 . "static" . $2;
+ }
+ last;
+ }
+
+ push(@keywords, lc($keyword));
+ }
+
+ if (@keywords == 0 || !defined($url)) {
+ return "Bad parameters";
+ }
+
+ # start transaction
+ $dbh->commit;
+# if (!$dbh->begin_work) {
+# print_msg("Database error: could not start transaction - $DBI::errstr\n", ERROR);
+# return "Database error: could not start transaction";
+# }
+
+ # lock tables to avoid double inserts from two running bots
+ if ($LOCK_DATABASE) {
+ if (!$dbh->do("LOCK TABLE docbot_url, docbot_key IN ACCESS EXCLUSIVE MODE")) {
+ print_msg("Database error: could not lock tables - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Database error: could not lock tables";
+ }
+ }
+
+ # insert keywords
+ $sth = $dbh->prepare("SELECT id FROM docbot_url WHERE url = ?");
+ unless ($sth->execute($url)) {
+ print_msg("Error inserting url - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Error inserting url";
+ }
+
+ my $kurl;
+
+ if (($kurl) = $sth->fetchrow()) {
+ $sth = $dbh->prepare("SELECT has_key (?, ?)");
+
+ my $has_key;
+ foreach my $keyword (@keywords) {
+ unless ($sth->execute($kurl, $keyword) && (($has_key) = $sth->fetchrow())) {
+ print_msg("Error inserting key - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Error while inserting key";
+ }
+ if ($has_key eq 't') {
+ @keywords = grep( !/^$keyword$/i, @keywords );
+ }
+ }
+
+ unless (@keywords) {
+ $dbh->rollback;
+ return "All keywords already exist in database";
+ }
+ } else {
+ $sth = $dbh->prepare("INSERT INTO docbot_url (url) VALUES (?)");
+ if (!$sth->execute($url)) {
+ print_msg("Error inserting url - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Error while inserting url";
+ }
+
+ $sth = $dbh->prepare("SELECT currval(pg_get_serial_sequence('docbot_url', 'id'))");
+ if (!$sth->execute()) {
+ print_msg("Error while selecting currval after inserting url - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Error inserting key";
+ }
+ ($kurl) = $sth->fetchrow();
+ }
+
+ $sth = $dbh->prepare("INSERT INTO docbot_key (key, kurl) VALUES (?, ?)");
+
+ foreach my $keyword (@keywords) {
+ if (!$sth->execute($keyword, $kurl)) {
+ print_msg("Error inserting key - $DBI::errstr\n", ERROR);
+ $dbh->rollback;
+ return "Error while inserting key";
+ }
+ }
+
+ $dbh->commit;
+ return "Successfully added "
+ . scalar @keywords
+ . ' keyword'
+ . ((scalar(@keywords)==1)?'':'s')
+ ;
+}
+
+
+########
+# do_forget ( nick, query )
+####
+# auth user nick and remove keyword from database
+#
+sub do_forget {
+ my ($kernel, $nick, $channel, $query) = @_;
+
+ unless (is_db_ok()) {
+ return "Database error";
+ }
+
+ ## Make sure the user is authorized to perform this action
+ if (! &authorized({action => 'forget', nick => $nick, channel => $channel})) {
+ print_msg("Unauthorized ?forget from $nick\n", WARN);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "user $nick not authorized to do a 'forget' in channel: $channel");
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "forget query was: $query");
+ return "You are not authorized";
+ }
+
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "forget ($nick/$channel): $query");
+ my ($url, @keywords);
+ my @keys = split(/\s+/, $query);
+
+ if (($keys[0] !~ /$url_pattern/i) && ($keys[1] =~ /$url_pattern/i) && ($#keys >= 2)) {
+ return "Usage: ?forget key url OR ?forget key key key OR ?forget URL URL URL";
+ print_msg("Unauthorized ?forget from $nick\n", WARN);
+ return qq{You are not authorized to run "?forget"};
+ }
+
+ my $rows = 0;
+
+ if ($#keys == 1 && $keys[1] =~ /$url_pattern/) { # key url
+ $sth = $dbh->prepare("DELETE FROM docbot_key WHERE key = ? AND kurl IN (SELECT id FROM docbot_url WHERE url = ?)");
+ if ($sth->execute(@keys)) {
+ $dbh->commit;
+ return "Successfully deleted " . $sth->rows . " key" . (($sth->rows > 1) ? "s" : "") if ($sth->rows);
+ }
+ else {
+ print_msg("Error deleting key(s) - $DBI::errstr\n", ERROR);
+ return "Error while deleting key(s)";
+ }
+ }
+ elsif ($keys[0] =~ /$url_pattern/) { # one or more urls
+ $sth = $dbh->prepare("DELETE FROM docbot_url WHERE url = ?");
+ foreach my $keyword (@keys) {
+ if ($keyword =~ /^$url_pattern/) {
+ if ($sth->execute($keyword)) {
+ $rows += $sth->rows;
+ } else {
+ $dbh->rollback;
+ print_msg("Error deleting url - $DBI::errstr\n", ERROR);
+ return "Error while deleting url";
+ }
+ }
+ }
+ $dbh->commit;
+ return "Successfully deleted " . $rows . " url" . (($rows > 1) ? "s" : "") if ($rows);
+ return "Url(s) not found";
+ }
+ else { # one or more keys, TODO delete urls with no keys left
+ $sth = $dbh->prepare("DELETE FROM docbot_key WHERE key = ?");
+ foreach my $keyword (@keys) {
+ if ($sth->execute($keyword)) {
+ $rows += $sth->rows;
+ }
+ else {
+ $dbh->rollback;
+ print_msg("Error deleting key - $DBI::errstr\n", ERROR);
+ return "Error while deleting key";
+ }
+ }
+ $dbh->commit;
+ return "Successfully deleted " . $rows . " key" . (($rows > 1) ? "s" : "") if ($rows);
+ return "Key(s) not found";
+ }
+}
+
+
+########
+# do_config ( nick, query )
+####
+# config manipulation from IRC
+#
+sub do_config {
+ my ($kernel, $nick, $channel, $query) = @_;
+
+ print_msg('In do_config');
+
+ unless (is_db_ok()) {
+ return "Database error";
+ }
+
+ ## Make sure the user is authorized to perform this action
+ if (! &authorized({action => 'config', nick => $nick, channel => $channel})) {
+ print_msg("Unauthorized ?config from $nick\n", WARN);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "user $nick not authorized to do a 'config' in channel: $channel");
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "config query was: $query");
+ return "You are not authorized";
+ }
+
+ return "Configuration not supported on this bot" unless (defined($main::config));
+
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "config ($nick/$channel): $query");
+ my ($cmd, $var, $val) = split(/\s+/, $query);
+ $cmd = lc($cmd);
+
+ if (!defined($var) ||
+ ($cmd ne "set" && $cmd ne "get") ||
+ ($cmd eq "set" && !defined($val))) {
+ return "Bad parameters";
+ }
+
+ return "Unknown variable '$var'" unless defined($cfg_directives{$var});
+
+ if ($cmd eq "get") {
+ if (ref($cfg_directives{$var}) eq 'ARRAY') {
+ return $var." = ".join(';', @{$cfg_directives{$var}});
+ }
+ else {
+ return $var." = ".${$cfg_directives{$var}};
+ }
+ }
+ else {
+ $main::config->set_key($var, $val);
+ $main::config->save_config();
+ reread_config();
+ return "Successfully set config variable '$var'";
+ }
+}
+
+
+########
+# on_reconnect ( )
+####
+#
+sub on_reconnect {
+ $poe_kernel->delay( autoping => undef );
+ $poe_kernel->delay( connect => 60 );
+}
+
+
+########
+# on_connect ( )
+####
+# called when conencted to irc, joins to selected channels
+#
+sub on_connect {
+ my ( $kernel, $heap ) = @_[ KERNEL, HEAP ];
+
+ print_msg("Connected ok, joining channels\n", 4);
+
+ my %chan_data;
+ foreach my $channel (@irc_channels) {
+ $chan_data{$channel} = {};
+ $kernel->post( $pg_docbot => join => $channel );
+ }
+
+ $heap->{chan_data} = \%chan_data;
+ $heap->{seen_traffic} = 1;
+ $kernel->delay( autoping => 300 );
+}
+
+
+########
+# on_message ( )
+####
+# called when some message was sent to channel or to bot
+#
+sub on_message {
+ my ( $kernel, $heap, $who, $where, $msg ) = @_[ KERNEL, HEAP, ARG0, ARG1, ARG2 ];
+ my $nick = ( split /!/, $who )[0];
+ my $channel = $where->[0];
+ my $keep_silent = 0;
+ my $do_it_anyway = 0;
+ my $replyto = $channel;
+ my $full_msg = $msg;
+ my $like = 0;
+
+ foreach my $do_channel (keys(%main::last_question)) {
+ if (length($main::last_question{$do_channel}) > 0 && ($main::last_question_ts{$do_channel} + 20) < time()) {
+ # delete any old stored question which is older than 20 seconds
+ print_msg("deleting old question ($main::last_question{$do_channel})", DEBUG);
+ $main::last_question{$do_channel} = "";
+ }
+ }
+
+ # was this a reply from one of the bots and we have the question?
+ if ($msg =~ /^nothing found/ && length($main::last_question{$channel}) > 0) {
+ foreach my $mute_nick (@mute_nicks) {
+ #look if one of the mute nicks told us that he does not know
+ if (lc($mute_nick) eq lc($nick)) {
+ # yes, push pack your question
+ $nick = $main::last_question_nick{$channel};
+ $msg = $main::last_question{$channel};
+ print_msg("pushing back old question ($main::last_question{$channel})", DEBUG);
+ $main::last_question{$channel} = "";
+ $do_it_anyway = 1;
+ }
+ }
+ }
+
+
+ if ($msg =~ /^\s*(\?(?:learn|forget|config))\s+(.+)/i) {
+ my ($com,$string) = ($1,$2);
+ my $answer;
+
+ if (!index($channel,'#') and $my_nick ne $irc_nick) {
+ if (grep( /^$channel$/i, find_nick( $heap, $irc_nick ) )) {
+ print_msg("Not processing admin command, master bot is on channel", DEBUG);
+ return;
+ }
+ }
+
+ if (!defined($heap->{whois_callback}->{$nick}->{authed})) {
+ $heap->{whois_callback}->{$nick} = {event => (lc($channel) eq lc($my_nick)) ? 'irc_msg' : 'irc_public', authed => 0};
+ @{$heap->{whois_callback}->{$nick}->{args}} = ($who, $where, $msg);
+
+ $kernel->post( pg_docbot => whois => $nick );
+ return;
+ }
+ elsif ($heap->{whois_callback}->{$nick}->{authed} != 1 and ! $EVERYONE_AUTHORIZED) {
+ $answer = "You are not authorized";
+ }
+ else {
+ # execute desired command
+ $answer = $admin_commands->{$com}($kernel, $nick, $channel, $string);
+ }
+ undef ($heap->{whois_callback}->{$nick});
+
+ if (length($answer)) {
+ # if command was called in channel print answer to channel, if it was PM print it as PM
+ if (lc($channel) eq lc($my_nick)) {
+ $kernel->post(pg_docbot => privmsg => $nick, $answer);
+ }
+ else {
+ $kernel->post(pg_docbot => privmsg => $channel, $answer);
+ }
+ return;
+ }
+ }
+ elsif ($msg =~ /^\s*\?\?(\?*)(.+)/i) {
+ $like = ($1 and $LIKESEARCH) ? 1 : 0;
+ $msg = $2;
+ $msg =~ s/^\s+//;
+
+ if (substr($channel, 0, 1) eq '#')
+ {
+ if ($my_nick ne $irc_nick) {
+ if (grep( /^$channel$/i, find_nick( $heap, $irc_nick ) )) {
+ print_msg("Not processing query command, master bot is on channel", DEBUG);
+ return;
+ }
+ }
+
+ if ($do_it_anyway == 0) {
+ foreach my $mnick (@mute_nicks) {
+ if (grep( /^$channel$/i, find_nick( $heap, $mnick ) )) {
+ print_msg("Not processing query command, bot with nickname $mnick is on channel", DEBUG);
+ #return;
+ # do not return, instead just continue and dont output anything
+ $keep_silent = 1;
+ }
+ }
+ }
+ }
+
+ if ($msg =~ /^(.+)\s+>\s+(\w+)/i) {
+ return unless (grep( /^$channel$/i, find_nick( $heap, $2 ) ));
+ $replyto = $2;
+ $msg = $1;
+ } elsif (lc($channel) eq lc($my_nick)) {
+ $replyto = $nick;
+ } else {
+ $replyto = $channel;
+ }
+ }
+ elsif ($msg =~ /^\s*$my_nick\W+tell\s+(\w+)\s+about\s+(.+)/i) {
+ if ($1 eq "me") {
+ $replyto = $nick;
+ } elsif ($1 eq "us") {
+ $replyto = $channel;
+ } else {
+ $replyto = $1;
+ return unless (grep( /^$channel$/i, find_nick( $heap, $replyto ) ));
+ }
+
+ $msg = $2;
+ } else {
+ return;
+ }
+ # now decide if to keep silent
+ if ($keep_silent == 1) {
+ # yes, just store the question and a timestamp
+ $main::last_question{$channel} = $full_msg;
+ $main::last_question_ts{$channel} = time();
+ $main::last_question_nick{$channel} = $nick;
+ print_msg("storing old question ($main::last_question{$channel})", DEBUG);
+ # now return
+ return;
+ }
+
+ # get data from db
+ my $answers = get_answer($msg, $like);
+ my $message_to_say = get_message($msg);
+ my $numanswers = @$answers;
+
+ # print each answer as one line, except when there are more than $MAXWRAP
+ if ($numanswers) {
+ $kernel->post(pg_docbot => privmsg => $replyto, "For information about '$msg' see:\n");
+ if ($numanswers <= $MAXWRAP) {
+ for my $answer (@$answers) {
+ $kernel->post(pg_docbot => privmsg => $replyto, $answer)
+ }
+ }
+ else {
+ for my $answer (word_wrap(' :: ', 250, @$answers)) {
+ $kernel->post(pg_docbot => privmsg => $replyto, $answer);
+ }
+ }
+ }
+ else { # "nothing found," it always sends to the caller, not to the receiver
+ # if command was called in channel print, answer to channel, if it was PM print it as PM
+ if ($do_it_anyway == 0) {
+ if (lc($channel) eq lc($my_nick)) {
+ $kernel->post(pg_docbot => privmsg => $nick, "Nothing found");
+ } else {
+ $kernel->post(pg_docbot => privmsg => $channel, "Nothing found");
+ }
+ }
+ }
+}
+
+
+########
+# on_kick ( )
+####
+# called when somebody was kicked from channel
+#
+sub on_kick {
+ my ( $kernel, $heap, $channel, $who ) = @_[ KERNEL, HEAP, ARG1, ARG2 ];
+
+ my $nick = ( split /!/, $who )[0];
+
+ # if we was kicked, we should rejoin
+ if ( lc($nick) eq lc($my_nick) ) {
+ remove_channel($heap, $channel);
+ print_msg("I was kicked from channel ".$channel.", rejoining\n", 4);
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "I was kicked from channel " . $channel . ", rejoining");
+ $kernel->post( $pg_docbot => join => $channel );
+ }
+ else {
+ remove_nick( $heap, $nick, $channel );
+ }
+}
+
+
+sub on_join {
+ my ( $kernel, $heap, $who, $channel ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
+ my $nick = ( split /!/, $who )[0];
+
+ add_nick( $heap, $nick, $channel );
+}
+
+sub on_quit {
+ my ( $kernel, $heap, $who ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
+ my $nick = ( split /!/, $who )[0];
+
+ remove_nick( $heap, $nick );
+}
+
+sub on_part {
+ my ( $kernel, $heap, $who, $channel ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
+ $channel =~ s/^(.*?) :.*$/$1/;
+ my $nick = ( split /!/, $who )[0];
+
+ if (lc($nick) eq lc($my_nick)) {
+ print_msg("Leaving $channel");
+ remove_channel($heap, $channel);
+ } else {
+ remove_nick( $heap, $nick, $channel );
+ }
+}
+
+sub on_nick {
+ my ( $kernel, $heap, $who, $new ) = @_[ KERNEL, HEAP, ARG0, ARG1 ];
+ my $nick = ( split /!/, $who )[0];
+
+ my @channels = find_nick( $heap, $nick );
+
+ foreach my $channel (@channels) {
+ remove_nick( $heap, $nick, $channel );
+ add_nick( $heap, $new, $channel );
+ }
+}
+
+sub on_names {
+ my ( $kernel, $heap, $server, $detail ) = @_[KERNEL, HEAP, ARG0, ARG1];
+ my ( $channel, $nicknames ) = $detail =~ /^. (.*?) :(.*)$/;
+ if ( !defined( $nicknames )) {
+ print_msg ("Parse failed in on_names for $detail");
+ return;
+ }
+
+ my @nicknames = split( /\s+/, $nicknames );
+ for my $nick ( @nicknames ) {
+ $nick =~ s/^@//;
+ add_nick( $heap, $nick, $channel );
+ }
+}
+
+########
+# on_start ( )
+####
+# start bot
+#
+sub on_start {
+ $poe_kernel->post( pg_docbot => register => "all" );
+
+ my ($server, $port) = split(":", $irc_server);
+ $poe_kernel->post( pg_docbot => connect =>
+ {
+ Debug => $debug,
+ Nick => $my_nick,
+ Username => $irc_nick,
+ Password => $irc_pass,
+ Ircname => $irc_name,
+ Server => $server,
+ Port => $port
+ }
+ );
+}
+
+sub on_nickused {
+ $my_nick .= '_';
+ $poe_kernel->post( $pg_docbot => nick => $my_nick);
+}
+
+sub on_whois_identified {
+ my ( $kernel, $heap, $detail ) = @_[KERNEL, HEAP, ARG1];
+ my $nick = ( split / /, $detail )[0];
+
+ if (defined($heap->{whois_callback}->{$nick})) {
+ $heap->{whois_callback}->{$nick}->{authed} = 1;
+ }
+}
+
+sub on_whois_end {
+ my ( $kernel, $heap, $detail ) = @_[KERNEL, HEAP, ARG1];
+ my $nick = ( split / /, $detail )[0];
+
+ if (defined($heap->{whois_callback}->{$nick}->{event})) {
+ $kernel->yield($heap->{whois_callback}->{$nick}->{event}, @{$heap->{whois_callback}->{$nick}->{args}});
+ }
+}
+
+sub add_nick {
+ my ( $heap, $who, $channel ) = @_;
+ my %channels = %{$heap->{chan_data}};
+ my @nicknames;
+
+ @nicknames = @{$channels{$channel}->{names}}
+ if (defined($channels{$channel}->{names}));
+####################################################################
+# #
+# The following avoids dealing with clever nicknames like ]X[X]X[ #
+# #
+####################################################################
+ return if grep{ lc($who) eq lc($_) } @nicknames;
+
+ push(@nicknames, $who);
+ $channels{$channel}->{names} = \@nicknames;
+
+ $heap->{chan_data} = \%channels;
+}
+
+
+sub remove_nick {
+ my ( $heap, $who, $channel ) = @_;
+ my %channels = %{$heap->{chan_data}};
+
+ if (defined($channel)) {
+ if (defined($channels{$channel}->{names})) {
+ my @nicknames;
+ foreach my $nickname ( @{$channels{$channel}->{names}} ) {
+ next if lc($who) eq lc($nickname);
+ push @nicknames, $nickname;
+ }
+ $channels{$channel}->{names} = \@nicknames;
+ $heap->{chan_data} = \%channels;
+ }
+ } else {
+ foreach $channel ( keys %channels ) {
+ if (defined($channels{$channel}->{names})) {
+ my @nicknames;
+ foreach my $nickname ( @{$channels{$channel}->{names}} ) {
+ next if lc($who) eq lc($nickname);
+ push @nicknames, $nickname;
+ }
+ $channels{$channel}->{names} = \@nicknames;
+ }
+ }
+ $heap->{chan_data} = \%channels;
+ }
+
+ if ($who eq $irc_nick)
+ {
+ unless (find_nick($heap, $who)) {
+ $my_nick = $irc_nick;
+ $poe_kernel->post( $pg_docbot => nick => $my_nick);
+ }
+ }
+}
+
+sub remove_channel {
+ my ( $heap, $channel ) = @_;
+ my %channels = %{$heap->{chan_data}};
+
+ if (defined($channels{$channel})) {
+ undef($channels{$channel});
+ $heap->{chan_data} = \%channels;
+ }
+}
+
+sub find_nick {
+ my ( $heap, $who ) = @_;
+ my %channels = %{$heap->{chan_data}};
+ my @channels;
+
+ foreach my $channel ( keys %channels ) {
+ if ( defined( $channels{$channel}->{names}) ) {
+ if ( grep {lc($who) eq lc($_)} @{$channels{$channel}->{names}} ) {
+ push (@channels, $channel);
+ }
+ }
+ }
+
+ return @channels;
+}
+
+sub on_disconnected {
+ if ($shutdown == 1) {
+ $poe_kernel->post ( $pg_docbot => unregister => "all" );
+ $poe_kernel->post ( $pg_docbot => 'shutdown');
+ print_msg ("Shutting down in on_disconnected");
+ exit;
+ } else {
+ &on_reconnect;
+ }
+}
+
+sub on_error {
+ if ($shutdown == 1) {
+ $poe_kernel->post ( $pg_docbot => unregister => "all" );
+ $poe_kernel->post ( $pg_docbot => 'shutdown');
+ print_msg ("Shutting down in on_error");
+ exit;
+ } else {
+ &on_reconnect;
+ }
+}
+
+sub death {
+ my ($where) = $_[ ARG1 ];
+ my $channel = $where->[0];
+ my $text = shift;
+ if ($text =~ m/^(INT|TERM|KILL|HUP)$/) {
+ $text = "Signal received: $text - shutting down";
+ } else {
+ $text = "Error: $text - shutting down";
+ }
+ $shutdown = 1;
+ $poe_kernel->post ( $pg_docbot => quit => $text );
+ print_msg ("Sending quit message: $text");
+}
+
+# http://poe.perl.org/?POE_Cookbook/IRC_Bot_Debugging
+# http://poe.perl.org/?POE_Cookbook/IRC_Bot_Disconnecting
+# http://poe.perl.org/?POE_Cookbook/IRC_Bot_Reconnecting
+# http://poe.perl.org/?POE_Cookbook
+# http://poe.perl.org/?Tutorials
+# http://poe.perl.org/?POE_Support_Resources
+
+sub _default {
+ my ( $event, $args ) = @_[ ARG0 .. $#_ ];
+ open(UNHANDLED, ">>", "unhandled.log") || return 0;
+ print UNHANDLED "unhandled $event\n";
+
+ my $print_it = 1;
+ if ($event eq "autoping") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_ping") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_connected") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_snotice") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_whois") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_mode") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_topic") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_ctcp_action") {
+ $print_it = 0;
+ }
+ if ($event eq "irc_notice") {
+ if (@$args[0] =~ /^NickServ/) {
+ $print_it = 0;
+ }
+ if (@$args[0] =~ /^ChanServ/) {
+ $print_it = 0;
+ }
+ }
+ if ($event eq "irc_ctcp_version") {
+ if (@$args[0] =~ /^freenode\-connect/) {
+ $print_it = 0;
+ }
+ }
+ if ($event =~ /^irc_\d+/) {
+ $print_it = 0;
+ }
+ if ($print_it == 1) {
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', "unhandled event: $event");
+ }
+
+ my $arg_number = 0;
+ foreach (@$args) {
+ print UNHANDLED " ARG$arg_number = ";
+ if ( ref($_) eq 'ARRAY' ) {
+ print UNHANDLED "$_ = [", join ( ", ", @$_ ), "]\n";
+ my $my_nick_quoted = quotemeta($my_nick);
+ if ($print_it == 1 and @$_[0] !~ /$my_nick_quoted/i) {
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', " $_ = [" . join ( ", ", @$_ ) . "]");
+ }
+ }
+ else {
+ print UNHANDLED "'$_'\n";
+ if ($print_it == 1 and length($_) > 0) {
+ $poe_kernel->post(pg_docbot => privmsg => '#pg_docbot', " '$_'");
+ }
+ }
+ $arg_number++;
+ }
+ close(UNHANDLED);
+ return 0; # Don't handle signals.
+}
+
+sub read_messages {
+ my $file = shift;
+
+ # reset messages
+ %main::messages = ();
+
+ # check if the file exist
+ if (length($file) < 1) {
+ return;
+ }
+ if (!-f $file) {
+ return;
+ }
+
+ # open file
+ my $fh = new FileHandle;
+ if (!open($fh, "<", $file)) {
+ die("could not open existing message file ($file): $!\n");
+ }
+ my @file = <$fh>;
+ close($fh);
+
+ # now go through every line of the file and get messages
+ foreach my $line (@file) {
+ $line =~ s/[\r\n]//gs;
+ if ($line =~ /^[\s]*\#/) {
+ # skip comments
+ next;
+ }
+ # validate if this is a key:value line
+ if ($line =~ /^([^\s\:]+):[\s]*(.+)$/) {
+ my $key = $1;
+ my $value = $2;
+ # do we already have an array for this key?
+ if (!defined($main::messages{$key})) {
+ $main::messages{$key} = [];
+ }
+ push(@{$main::messages{$key}}, $value);
+ }
+ }
+
+}
+
+sub get_message {
+ my $msg = shift;
+
+ if (!defined($msg)) {
+ return '';
+ }
+
+ # key not found in message file
+ if (!defined($main::messages{$msg})) {
+ return '';
+ }
+
+ return '';
+
+}
+
+################
+# Main
+################
+
+print_msg("Creating new IRC bot\n");
+
+POE::Component::IRC->new($pg_docbot) or die "Failed to create pg_docbot $!";
+
+print_msg("Starting IRC session\n");
+
+POE::Session->create(
+ inline_states => {
+ _start => \&on_start,
+ _default => \&_default,
+ irc_error => \&on_error,
+ irc_disconnected => \&on_disconnected,
+ irc_socketerr => \&on_reconnect,
+ irc_001 => \&on_connect,
+ irc_433 => \&on_nickused,
+ irc_353 => \&on_names,
+ irc_join => \&on_join,
+ irc_part => \&on_part,
+ irc_quit => \&on_quit,
+ irc_nick => \&on_nick,
+ irc_330 => \&on_whois_identified,
+ irc_318 => \&on_whois_end,
+ irc_public => \&on_message,
+ irc_msg => \&on_message,
+ },
+);
+
+
+# Run the bot until it is done.
+#POE::Kernel->run;
+$poe_kernel->run;
+exit 0;
+
+# vi: ts=4