# Copyright (C) 2005,2006,2007, Software Freedom Law Center, Inc. # Licensed under GPL, Version 2 or any later version package Bot::BasicBot::Time; use warnings; use strict; use base qw(Bot::BasicBot); use Date::Manip; use SFLC::TimeTracker::Input; use SFLC::TimeTracker::Entry; use SFLC::TimeTracker::Question; use SFLC::TimeTracker::DB; use SFLC::TimeTracker::Reports; use SFLC::TimeTracker::Output::DeleteEntry; use SFLC::TimeTracker::Output::Entry; use Lingua::EN::Inflect qw ( PL PL_N PL_V); use String::Approx 'amatch'; use Date::Manip; use IO::File; use POSIX; my $NOW; my $SHORTEN_LINE_FOR_REOUTPUT = 15; # I was really lame here with these user options. There has got to be some # CPAN module, Data::Type for example, that I could use for type validation # and error reporting. When I saw all the prerequesite list for Data::Type, # I realized that making a big ugly function that validated the fields was # so much easier. my %USER_OPTIONS; ############################################################################### srand (time ^ $$ ^ unpack "%L*", `/bin/ps axww | /bin/gzip`); sub myrand ($$) { my($min, $max) = @_; my $tries = 10; my $cnt = 0; my $randNum; my $val; foreach my $file ('/dev/random', '/dev/urandom') { sysopen(RANDOM, $file, O_NONBLOCK|O_RDONLY) || warn "You have no $file: $!!"; $val = undef; while ( (not defined ($val = sysread(RANDOM, $randNum, 4)) ) and $cnt < $tries) { $cnt++;} close RANDOM; last if ($cnt < $tries and defined $val and $val == 4); } if ($cnt >= $tries or not defined $val or $val != 4) { $randNum = int(rand($max-$min+1)) + $min; } else { $randNum = (unpack("I", $randNum) % ($max-$min+1)) + $min; } return $randNum; } ############################################################################### # FIXME: This should be a utility function somewhere sub FindMinMatch ($@) { my($given, @candidates) = @_; # No choices mean no matches return @candidates if @candidates <= 0; my $approxVal = 0; my @matches; while (@matches == 0 and ++$approxVal < 60) { (@matches) = amatch($given, ['i', "${approxVal}%"], @candidates); } print "APPROXED TO: $approxVal\n"; print "giving matches: $given for candidates, ", join(",", @matches), "\n"; return @matches; } ############################################################################### sub UpdateNow { $NOW = ParseDate(POSIX::strftime("%a, %d %b %Y %H:%M:%S %z", localtime())); &Date_Init("ForceDate=" . UnixDate($NOW, "%Y-%m-%d-%H:%M:%S")); # FIXME: This is a bit nasty, but the only way to be assured that the # date delta routines work properly. The should always be operating on # the date the bot currently thinks it is. I want to avoid a few second # drift during processing. } # added this POE just to get OBJECT and KERNEL barewords defined and working use POE::Kernel; use POE::Session; use POE::Wheel::Run; use POE::Filter::Line; use POE::Component::IRC; my $VERBOSE_LOGS = 1; our $DATABASE; use Unix::Syslog qw(:subs); use Unix::Syslog qw(:macros); use Carp; { my $PAGE_EMAIL = '6463617006@tmomail.net'; my %messageHistory; sub WarnLog ($) { my($message) = @_; syslog LOG_INFO, $message; my $lastTime = $messageHistory{$message}; my $sendIt = 0; if (not defined $lastTime) { $sendIt = 1; } else { my $err; my $sinceLast = DateCalc($lastTime,"+ 10 minutes",\$err); $sendIt = 1 if ($NOW gt $sinceLast); } if ($sendIt) { open(SENDMAIL, "|/usr/sbin/sendmail -f root\@softwarefreedom.org -t"); print SENDMAIL "To: $PAGE_EMAIL\nFrom: root\@softwarefreedom.org\n", "Subject: IRC,TIM\n\n$message\n.\n"; close(SENDMAIL); if ($? != 0) { syslog LOG_INFO, "Unable to perform sendmail: $!"; } else { $messageHistory{$message} = $NOW; } } } } ############################################################################### sub setOption { my($self, $userHandle, $line) = @_; return "I cannot understand your SET command paramaters of $line" unless $line =~ /^\s*(\S+)\s+(\S+)\s*$/; my($opt, $value) = ($1, $2); my @matches = FindMinMatch($opt, keys %USER_OPTIONS); if (@matches <= 0) { return "Cannot find help for option, \"$opt\""; } $opt = $matches[0]; if ($USER_OPTIONS{$opt}{type} eq "boolean") { if ($value =~ /(?:ye*s*|1|tr?u?e?)/i) { $value = 1; } elsif ($value =~ /(?:n+o*|0|fa?l?s?e?)/i) { $value = 0; } else { return "$opt is of type boolean and $value is not a boolean value."; } } elsif ($opt eq "categoryParser") { @matches = FindMinMatch($value, qw/AdminAssume BasicLawyer Assistant CTO Press/); if (@matches <= 0) { return "$value is not a valid value for $opt." } $value = $matches[0]; } else { return "Woah, REPORT THIS: unknown type of $opt of " . "$USER_OPTIONS{$opt}{type}."; } my $oldVal = $DATABASE->getUserConfigValue($userHandle, $opt); my $newVal = $DATABASE->setUserConfigValue($userHandle, $opt, $value); return "Changed value of \"$opt\" from \"$oldVal\" to \"$newVal\""; } ############################################################################### # Argh: FIXME: copied this from Lawyer.pm, we need a place for utility # functions! # Note I do a "noaliases" here, whereas it's with aliases in Lawyer.pm sub _lookupClientHash ($) { my($self) = @_; my(@clientCats) = $DATABASE->getCategoryLevelNoAliases("/legal/client"); my %clients; foreach my $cat (@clientCats) { croak "bad client entry, \"$cat\", in database" unless $cat =~ m%^/legal/client/([^/]+)(.*)$%i; my($client, $matter) = ($1, $2); $matter =~ s%^/*%%; $matter = "general" if not defined $matter or $matter =~ /^\s*$/; $clients{$client} = [] unless defined $clients{$client}; push(@{$clients{$client}}, $matter); } return %clients; } ############################################################################### sub listFunction { my($self, $userHandle, $line) = @_; return "I cannot understand your LIST command paramaters: \"$line\"" unless $line =~ s/^\s*(\S+)\s*//; my($type) = ($1); return <getCategoryLevelNoAliases("/admin"); @admins = grep s%^\s*/admin/%%i, @admins; return "Sub" . PL_N('category', scalar @admins) . " under /admin: " . join(", ", sort {$a cmp $b} @admins); } elsif ($type eq "tech" or ($type eq "matters" and $line =~ s/^\s*(?:(?:in|for|on|with)\s+)*\s*te?ch\s*//i)) { my(@tech) = $DATABASE->getCategoryLevelNoAliases("/tech"); @tech = grep s%^\s*/tech/%%i, @tech; return "Sub" . PL_N('category', sort { $a cmp $b} scalar @tech) . " under /tech: " . join(", ", @tech); } elsif ($type eq "clients") { my(%clients) = $self->_lookupClientHash(); return PL("Client", scalar keys %clients) . " " . PL_V("is") . ": " . join(", ", sort { $a cmp $b } keys %clients); } elsif ($type eq "matters") { return "a \"list matters\" request should be followed by the client name." unless $line =~ s/^\s*(?:(?:in|for|on|with)\s+)*\s*(\S+)\s*//i; my $client = $1; my(%clients) = $self->_lookupClientHash(); my $answer = ""; if (not defined $clients{$client}) { my(@cliMatch) = FindMinMatch($client, keys %clients); if (@cliMatch <= 0) { return "Requested client, \"$client\" is unknown." } else { $answer .= "Client, \"$client\" is unknown. " . "Maybe you meant client, \"$cliMatch[0]\"? "; $client = $cliMatch[0]; print $answer; } } my $num = scalar @{$clients{$client}}; return $answer . PL("Matter", $num) . " in $client " . PL_V("is", $num) . ": " . join(", ", sort { $a cmp $b } @{$clients{$client}}); } elsif ($type eq "alias") { my $db = $DATABASE; my(@aliases) = $db->getCategoryAliasList($userHandle); my $cnt = scalar(@aliases); my $answer = "You have $cnt " . PL_N("alias", $cnt). "."; if ($line !~ /^\s*$/) { $line = "/" . join("/", split(/\s+/, $line)); my $handle = $db->getCategoryHandleForUserHandle( $line, $userHandle); $answer .= (defined $handle) ? (" $line is aliased to " . $db->getCategoryName($handle)) : " $line is not a valid alias."; } return $answer; } else { return "REPORT_THIS: Somehow I got to somewhere I shouldn't be."; } } ############################################################################### sub _parseStartEnd { my($self, $line) = @_; my %date; ($date{start}, $date{end}) = split(/\s+(?:to|thro?ug?h?|until)\s+/i, $line); $date{end} = $date{start} if (not defined $date{end} or $date{end} =~ /^\s*$/); foreach my $day (qw/start end/) { $date{$day} = $NOW if (not defined $date{$day}); $date{$day} = ParseDate($date{$day}); $date{$day} = undef if defined $date{$day} and $date{$day} =~ /^\s*$/; } return %date; } ############################################################################### sub statusFunction ($$$) { my($self, $userHandle, $line) = @_; my $verbose = ($line =~ s/^\s*ve?r?b?o?s?e?(?:\s+|$)//i); $line =~ s/^\s*fro?m//i; $verbose = ($line =~ s/^\s*ve?r?b?o?s?e?(?:\s+|$)//i) if not $verbose; my $email = ($line =~ s/^\s*ema?i?l(?:\s+|$)//i); $line =~ s/^\s*fro?m//i; $email = ($line =~ s/^\s*ve?r?b?o?s?e?(?:\s+|$)//i) if not $email; my $noteFilter; if ($line =~ s/no?te?:\"?\s*(.+)\"?\s*$//) { $noteFilter = $1; } my(%date) = $self->_parseStartEnd($line); return ("Unable to parse start date in \"$line\" (Maybe you have arguments in the wrong order?).") if not defined $date{start}; return ("Unable to parse end date in \"$line\".") if not defined $date{end}; my(@answers) = SFLC::TimeTracker::Reports::QuickSummary($DATABASE, $userHandle, $date{start}, $date{end}, $verbose, $email, $NOW, $noteFilter, 1); return @answers; } ############################################################################### sub detailFunction ($$$) { my($self, $userHandle, $line) = @_; my(%date) = $self->_parseStartEnd($line); return ("Unable to parse start date in \"$line\"") if not defined $date{start}; return ("Unable to parse end date in \"$line\".") if not defined $date{end}; my $botName = $self->nick; SFLC::TimeTracker::Reports::Detail ($DATABASE, $userHandle, $date{start}, $date{end}, 'ALL', 'ALL', "$userHandle\@softwarefreedom.org", <getEntryById($userHandle, $id); if (not defined $entry) { return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "UNPARSEABLE", type => "", string => "\"$id\" is not a valid entry ID for $userHandle. Cannot DELETE.", edge => 'proceed'))) } my $date = ParseDate($entry->get('dateOccured')); $entry->remove; my $entryOutput = new SFLC::TimeTracker::Output::DeleteEntry( date => $date, userHandle => $userHandle); $entryOutput->set('entries', [$entry]); return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "DONE", type => "", string => $entryOutput->prettyPrintIRC($NOW), edge => 'proceed'))); } $date = "last" if ($line =~ s/\s*la?st$//i); ($category, $newDate) = split(/\s+(?:on|at|for|du?ri?ng)\s+/i, $line); $date = $newDate unless (defined $date); return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "UNPARSEABLE", type => "", string => "I cannot understand your DELETE command paramaters: \"$line\"", edge => 'proceed'))) unless defined $date and $date !~ /^\s*$/ and ( (defined $category and $category !~ /^\s*$/) or $date eq "last"); if ($date ne "last") { my $normalizedDate = ParseDate($date); return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "UNPARSEABLE", type => "", string => "\"$line\" does not contain a valid date. Cannot DELETE.", edge => 'proceed'))) if (not defined $normalizedDate or $normalizedDate =~ /^\s*$/); $date = $normalizedDate; print "DELETE: Normalized date to $date cateogry is $category\n"; } if ($date eq "last" and $line =~ /^\s*$/) { my $entry = $DATABASE->getLastEntry($userHandle); $entry->remove; my $entryOutput = new SFLC::TimeTracker::Output::DeleteEntry( date => $date, userHandle => $userHandle); $entryOutput->set('entries', [$entry]); return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "DONE", type => "", string => $entryOutput->prettyPrintIRC($NOW), edge => 'proceed'))); } my $entryOutput = new SFLC::TimeTracker::Output::DeleteEntry( date => $date, userHandle => $userHandle); my $parser = new SFLC::TimeTracker::Input::CategoryParser( $DATABASE->getUserConfigValue($userHandle, 'categoryParser'), userHandle => $userHandle); my $promptString = "Delete entry"; $promptString = "Delete last entry" if $date eq 'last'; # FIXME date FORMAT option my $pretty = UnixDate($date, '%b %E'); $promptString = "Delete entry (or entries) on $pretty" if (defined $pretty and $pretty !~ /^\s*$/); my($resolutionTree, $entryResolver) = $parser->parse($promptString, $category, $timeStamp); if (not defined $entryResolver and not defined $resolutionTree) { return new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Statement( status => "UNPARSEABLE", type => "", string => "REPORT THIS: strangly, I can't handle this delete request", edge => 'proceed'))); } $entryResolver->addEntry($entryOutput); return $resolutionTree; } ############################################################################### sub createFunction { my($self, $userHandle, $line) = @_; return "I cannot understand your CREATE command paramaters: \"$line\"" unless $line =~ s/^\s*(\S+)\s+//; my($type) = ($1); my @matches = FindMinMatch($type, qw/admin matter client tech/); if (@matches <= 0) { return "Unknown list command argument \"$type\""; } $type = $matches[0]; $line =~ s/\s+$//; if ($type eq "admin") { my $newCatString = "/admin/" . join("/", split(m%[\s:\.]%, $line)); $newCatString = "\L$newCatString\E"; my(@admins) = $DATABASE->getCategoryLevel($userHandle, "/admin"); my %admins; @admins{@admins} = @admins; if (defined $admins{$newCatString}) { return "The category, $newCatString, already exists."; } my $cat = new SFLC::TimeTracker::Category(userHandle => $userHandle, name => $newCatString); return "Created new category " . $cat->prettyPrint() . "\n"; } elsif ($type eq "client") { return "Client names must single words and be all alphanumeric characters." unless $line =~ /^\s*([A-Za-z0-9]+)\s*$/; my $client = "\L$1\E"; my(%clients) = $self->_lookupClientHash(); if (defined $clients{$client}) { return "The client, $client, already exists."; } my $genCat = new SFLC::TimeTracker::Category(userHandle => $userHandle, name => "/legal/client/$client/general"); my $pressCat = new SFLC::TimeTracker::Category(userHandle => $userHandle, name => "/legal/client/$client/press"); return "Created new client, $client, and categories: " . $genCat->prettyPrint() . " and " . $pressCat->prettyPrint() . ".\n"; } elsif ($type eq "matter") { return "Client names must single words and be all alphanumeric characters." unless $line =~ s/\s+(?:(?:in|for|on|with)\s+)*([A-Za-z0-9]+)\s*$//; my $client = "\L$1\E"; my(%clients) = $self->_lookupClientHash(); return "Requested client, \"$client\" is unknown." unless defined $clients{$client}; return "Matter names must single words and be all alphanumeric characters." unless $line =~ s/^\s*([A-Za-z0-9]+)\s*$//i or ($client eq "fsf" and $line =~ s%^\s*\s*(gplv3/[A-Za-z0-9]+)\s*$%%i); my $newMatter = "\L$1\E"; my %matters; @matters{@{$clients{$client}}} = @{$clients{$client}}; if (defined $matters{$newMatter}) { return "Matter, $newMatter, in client, $client, already exists."; } my $cat = new SFLC::TimeTracker::Category(userHandle => $userHandle, name => "/legal/client/$client/$newMatter"); return "Created new matter, $newMatter, in client, $client, " . "yielding category " . $cat->prettyPrint() . "\n"; } else { return "REPORT_THIS: Somehow I got to somewhere I shouldn't be."; } } ############################################################################### sub aliasFunction ($$$$) { my($self, $userHandle, $line, $doUnalias) = @_; if ($doUnalias) { return "The command should be: \"unalias EXISTING_ALIAS\"\n" unless ($line =~ /^\s*(\S+)\s*$/); my $oldAlias = $1; my $oldVal = $DATABASE->unsetCategoryAlias($userHandle, $oldAlias); my $cat; $cat = $DATABASE->getCategory($oldVal) if defined $oldVal; return (not defined $cat) ? "Found no existing alias of $oldAlias" : ("alias $oldAlias no longer refers to " . $cat->prettyPrint()); } return "The command should be: \"alias NEW_ALIAS EXISTING_CATEGORY\"." . " Note that both NEW_ALIAS and EXISTING_CATEGORY cannot have spaces" unless $line =~ (/^\s*(\S+)\s+(\S+)\s*$/); my($newAlias, $existingCat) = ("\L$1\E", "\L$2\E"); my $category = $DATABASE->categoryAliasLookup($userHandle, $existingCat); return "The category, $existingCat, does not exist." unless (defined $category); $DATABASE->setCategoryAlias($userHandle, $newAlias, $category); my $cat = $DATABASE->getCategory($category)->prettyPrint(); return "$newAlias now refers to $existingCat" . ( ($cat eq $existingCat) ? "" : " (aka $cat)") . "."; } ############################################################################### sub init { my $self = shift; carp "a database DBM file path is needed for Time.pm" unless defined $self->{dbPath}; return 0 unless defined $self->{dbPath}; $DATABASE = new SFLC::TimeTracker::DB("MLDBM", "$self->{dbPath}/time-data.mldbm", "$self->{dbPath}/pending-time.mldbm"); SFLC::TimeTracker::Entry::Initialize($DATABASE); SFLC::TimeTracker::Question::Initialize($DATABASE); SFLC::TimeTracker::Input::Initialize($DATABASE); $self->{parser} = new SFLC::TimeTracker::Input($DATABASE); $self->{greeted} = {}; $self->{endTimeAskDelta} = ParseDateDelta("+6 hours"); $self->{repeatAskDelta} = ParseDateDelta("+2 minutes"); my $botName = $self->nick; %USER_OPTIONS = ( categoryParser => { type => 'enum', help => < { type => 'boolean', help => < { type => 'boolean', help => <getUserList(); foreach my $user (@users) { $shameData{$weekNumber}{$user} = {}; my $seconds = myrand(0, 6 * 60 * 60); $shameData{$weekNumber}{$user}{when} = DateCalc(ParseDate("today at 11am"), "+ $seconds seconds"); } } my $startDate = ParseDate("1 week ago Monday at 00:00"); my $endDate = ParseDate("last Sunday at 23:59"); foreach my $user (keys %{$shameData{$weekNumber}}) { my @entries = $DATABASE->getEntriesInDateRange('main', $user, $startDate, $endDate); my $total = 0.0; foreach my $entry (@entries) { $total += Delta_Format($entry->get('amountTime'), 0, "%hd"); } $shameData{$weekNumber}{$user}{total} = $total; } } } my $CURRENT_TIM_TOPIC = ""; my $SAVED_TOPIC = ""; my $REMOVED_TIM_TOPIC = ""; sub shame ($$$$) { my($self, $weekNumber, $userList, $channel) = @_; my %baddies; foreach my $user (@{$userList}) { my $hours = $shameData{$weekNumber}{$user}{total}; $baddies{$user} = $hours if $hours < 32.0; } my $topic = "All your time entry are belong to tim. Congratulation !!"; if ( (keys %baddies) > 0) { $topic = "Missing hours last week: "; foreach my $user (sort {$a cmp $b } keys %baddies) { $topic .= sprintf("$user (%d), ", $baddies{$user}); } $topic =~ s/\s*,\s*$//; } if ($CURRENT_TIM_TOPIC ne $topic or $REMOVED_TIM_TOPIC ne $CURRENT_TIM_TOPIC) { $CURRENT_TIM_TOPIC = $topic; $topic .= " | $SAVED_TOPIC" if defined $SAVED_TOPIC and $SAVED_TOPIC !~ /^\s*$/; $self->{kernel}->post( $self->{IRCNAME}, 'topic', $channel, $topic); } foreach my $user (keys %baddies) { if (defined $shameData{$weekNumber}{$user}{when} and $shameData{$weekNumber}{$user}{when} le $NOW) { my $msg = "$user: You have no chance to survive, make your time (entries)."; $msg .= ($baddies{$user} <= 0.0) ? " Got no hours for you last week!" : sprintf(" Only got %.1f hours for you last week.", $baddies{$user}); $self->say(channel => $channel, body => $msg); delete $shameData{$weekNumber}{$user}{when}; } } } ############################################################################### sub tick { my ($self) = @_; UpdateNow(); my %chans = $self->channels(); foreach my $channel (keys %chans) { $self->say(channel => $channel, body => "We get signal. Main screen turn on. " . "All your time entry are belong to us.") unless defined $self->{greeted}{$channel}; $self->{greeted}{$channel} = 1; my $dayOfWeek = UnixDate($NOW, "%A"); my $weekNumber = UnixDate($NOW, "%W"); # Setup this week's data for Shaming, if needed. SetupShameData($weekNumber, $dayOfWeek); # FIXME: I think this is a hack....Is there a better way to get names? my(@users) = keys %{$self->{channel_data}{$channel}}; print "$NOW: Ticking ", join(@users, ", "), "\n" if $VERBOSE_LOGS; if ($dayOfWeek eq "Wednesday" and (keys %{$shameData{$weekNumber}} <= 0) ) { $self->{kernel}->post( $self->{IRCNAME}, 'topic', $channel, $SAVED_TOPIC); delete $shameData{$weekNumber}; } if ($dayOfWeek eq "Tuesday") { my @canonicalUserList; foreach my $user (@users) { my $userH = $DATABASE->getUserHandle($user); next unless (defined $userH); push(@canonicalUserList, $userH); } $self->shame($weekNumber, \@canonicalUserList, $channel); } } if (defined $self->{DIE_AT_TICK} and $self->{DIE_AT_TICK} == 1) { $self->{kernel}->post( $self->{IRCNAME}, 'quit', $self->charset_encode($self->quit_message) ); exit 0; } return 120; } ############################################################################### sub topic { my ($self, $mess) = @_; $SAVED_TOPIC = $mess->{topic}; $SAVED_TOPIC =~ s/^(\s*(?:Missing\s*hours\s+(?:last|this)\s+week\s*:|All your time)[^\|]*)(\||$)//i; $REMOVED_TIM_TOPIC = $1; $REMOVED_TIM_TOPIC = "" unless defined $REMOVED_TIM_TOPIC; $REMOVED_TIM_TOPIC =~ s/^\s+//; $REMOVED_TIM_TOPIC =~ s/\s+$//; $SAVED_TOPIC =~ s/^\s+//; $SAVED_TOPIC =~ s/\s+$//; } ############################################################################### sub said { my ($self, $mess, $pri) = @_; UpdateNow(); return undef if $self->ignore_nick($mess->{who}); my $botName = $self->nick; my $body = $mess->{body}; my $wasAddressed = (defined $mess->{address} and $mess->{address} ne ""); if ($body =~ /(\S+):/) { my $anotherAddressed = $1; if ($body =~ s/^\s*(?:time?|$botName)(?::|\s+)?\s*//) { # Oh, we really were addressed, force the setting $wasAddressed = 1; } else { # If we see an address to some other known user, ignore it by default. return undef if (defined $self->{channel_data}{$mess->{channel}}{"\L$anotherAddressed\E"}); } } my $userHandle = $DATABASE->getUserHandle($mess->{who}); if (not defined $userHandle) { return ("Who is this guy, $mess->{who}, anyway!!?! I have never heard of him!"); } return undef if (not $wasAddressed) and $DATABASE->getUserConfigValue($userHandle, 'speakWhenSpokenTo'); my @answers; my(@answersWithEmpties); if ($body =~ s/^\s*(pop|push)(?:\s+|$)//i) { push(@answersWithEmpties, "pop/push functionality is not currently available."); } elsif ($body =~ s/^\s*(un?)?ali?a?s\s+//i) { my $val = $1; push(@answersWithEmpties, $self->aliasFunction($userHandle, $body, (defined $val and $val ne ""))); } elsif ($body =~ s/^\s*cre?a?te?\s+//i) { push(@answersWithEmpties, $self->createFunction($userHandle, $body)); } elsif ($body =~ s/^\s*set\s+//i) { push(@answersWithEmpties, $self->setOption($userHandle, $body)); } elsif ($body =~ s/^\s*(li?s?t|li)\s+//i) { push(@answersWithEmpties, $self->listFunction($userHandle, $body)); } elsif ($body =~ s/^\s*statu?s?(\s+|$)//i) { push(@answers, $self->statusFunction($userHandle, $body)); } elsif ($body =~ s/^\s*de?ta?i?l?s?\s*//i) { push(@answers, $self->detailFunction($userHandle, $body)); } else { # Easter eggs: if ($userHandle eq "vasile" and $body =~ /stfu/) { push(@answersWithEmpties, "Please don't curse at me, James. Besides, I can't shut up! :)"); } elsif ($body =~ /love\s+pat/i) { $self->emote(channel => $mess->{channel}, who => $mess->{who}, body => "curls up in ${userHandle}'s lap and purrs."); return undef; #end easter eggs } elsif ($body =~ /I\s+(?:love|want|like|care\s+for|am\s+(interested|attracted|excited|aroused)\s+(to|in|for|by))\s+(you|$botName)/i) { push(@answersWithEmpties, "While I respect and admire you, ${userHandle}, I don't think that I am ready for that kind of relationship. Perhaps tem would be interested instead? tem's a lot like me, you know..."); #end easter eggs } else { (@answersWithEmpties) = $self->TimeSystem($wasAddressed, $pri, 0, $userHandle, $body); } } foreach my $ans (@answersWithEmpties) { $ans = join(" ", split /\n/, $ans); push(@answers, $ans) unless not defined $ans or $ans =~ /^\s*$/; } my $answerChannel = ($DATABASE->getUserConfigValue($userHandle, 'alwaysAnswerPrivate')) ? 'msg' : $mess->{channel}; print "CHANNEL IS: $mess->{channel}\n"; if (@answers == 0) { return $wasAddressed ? "I'm sorry; I just don't understand." : undef; } else { $self->say(who => $mess->{who}, address => 1, channel => $answerChannel, body => join(" ", @answers)); return undef; } } ############################################################################### sub TimeSystem { my ($self, $wasAddressed, $pri, $override, $userHandle, $line) = @_; my @answers; print "$NOW: About to parse: <$userHandle> $line\n"; if ($userHandle eq "bkuhn" and $line =~ /^you\s+have\s+no\s+chance\s+to\s+survive[\s,:;]+make\s+your\s+time\.?\s*$/) { push(@answers, "Somebody set up us the bomb."); $self->{DIE_AT_TICK} = 1; return @answers; } if ($line =~ s/^\s*dry[\-\s\_]run\s*:?\s*//i) { my $data = $self->{parser}->dryRun($NOW, $userHandle, $line); push(@answers, "Internal parse structure would yield: ", split(/\n/, Data::Dumper->Dump([$data]))); return @answers; } my $tree; $tree = $DATABASE->shiftInteractionTree($userHandle); my $shortendLine = (length($line) <= $SHORTEN_LINE_FOR_REOUTPUT) ? $line : ( (substr($line, 0, $SHORTEN_LINE_FOR_REOUTPUT)) . " ..."); my $middleOfConversation = <deleteFunction($userHandle, $line) : $self->{parser}->parseLine($NOW, $userHandle, $line, $wasAddressed); $actionData = {}; } else { if ( not $wasAddressed and $self->{parser}->checkIfLineIsTime($NOW, $userHandle, $line)) { push(@answers, $middleOfConversation); $actionData = {userInput => "INVALID DATA"}; } elsif ($wasAddressed and $line =~ s/^\s*del(?:e?t?e?)?\s+//i) { # This extra check for a user asking for a delete in the middle of a # conversation is a bit clugy to go right here, but it has to be done # somewhere lest we start parsing deletes in the middle of tree work. push(@answers, $middleOfConversation); $actionData = {userInput => "INVALID DATA"}; } elsif ($wasAddressed) { $actionData = {userInput => $line }; } else { $DATABASE->unshiftInteractionTree($userHandle, $tree); $tree = undef; } } my $ans; print "Before process: $tree\n"; $tree->traverse(sub { my ($_tree) = @_; print( ("-" x ($_tree->getDepth() *4)) . $_tree->getNodeValue()->edge . " -- " . $_tree->getNodeValue()->object . "with value " . Data::Dumper->Dump([$_tree->getNodeValue()->object]) . "\n"); }) if defined $tree; ($tree, $ans) = RecursiveTreeProcess($tree, $actionData); $DATABASE->unshiftInteractionTree($userHandle, $tree) if defined $tree; push(@answers, @{$ans}) if defined $ans; # print "After process: ", (defined $tree) ? $tree : "", "\n"; # $tree->traverse(sub { # my ($_tree) = @_; # print( # ("-" x $_tree->getDepth()) . $_tree->getNodeValue()->edge . " -- " . $_tree->getNodeValue()->object . "with value " . $_tree->getNodeValue()->object->prettyPrintIRC() . "\n"); # }) if defined $tree; return @answers; } ############################################################################### sub RecursiveTreeProcess { my($tree, $actionData) = @_; my @answers; return ($tree, \@answers) if not defined $tree; my $curObject = $tree->getNodeValue->get('object'); # Perform action on this tree .. $actionData = $curObject->action($actionData); # Don't prettyPrint until we've performed the action my $outputString = $curObject->prettyPrintIRC($NOW); push(@answers, $outputString) unless $outputString =~ /^\s*$/; # If we have now reached a leaf, return an empty tree and the answers. return(undef, \@answers) if ($tree->isLeaf); # If no action was performed, it means that the tree will have to be # evaluated at a later time. Return the existing tree and any answers # that we got up to this point. return ($tree, \@answers) unless (defined $actionData and $actionData->{edge} ne "loopback"); # Otherwise, we need to continue action down the subtrees. my(@kids) = $tree->getAllChildren(); my @subtrees; foreach my $kid (@kids) { my $node = $kid->getNodeValue; # If a child has an edge that was specifically mentioned as one we should # act on by the actionData we received, or if it is a special "proceed" # node (which means "evaluate no matter what), we recursively evaluate # the subtree. if ($node->edge eq $actionData->{edge} or $node->edge eq 'proceed') { my($subTree, $ans) = RecursiveTreeProcess($kid, $actionData); push(@answers, @{$ans}); # We have to save a list of defined subtrees here. This is in # case the child node was not able to fully evaluate its actions. push(@subtrees, $subTree) if defined $subTree; } } if (@subtrees == 0) { # If we received no defined subtrees, we know that processing of this # tree is complete and we can return an undefined tree. print STDERR " no subtrees on edge value of " ,$actionData->{edge}, "\n"; $tree = undef; } elsif (@subtrees == 1) { return($subtrees[0], \@answers); } else { # Ok, if we have some defined subtrees, it means that action was not # complete on them. We should gather them all up into one Dummy node # parent and return that tree for later processing. $tree = undef; foreach my $subTree (@subtrees) { # First, create that dummy node if it has yet to be created. $tree = new Tree::Simple( new SFLC::TimeTracker::Input::CategoryParser::ResolutionNode( object => new SFLC::TimeTracker::Output::Dummy( string => "", status => "DUMMY", type => ""), edge => 'proceed')) unless defined $tree; $tree->addChild($subTree); } } return ($tree, \@answers); } ############################################################################### sub help { my($self, $mess) = @_; my $userHandle = $DATABASE->getUserHandle($mess->{who}); return "I cannot help you because I don't know you $mess->{who}" unless defined $userHandle; $mess->{channel} = ($DATABASE->getUserConfigValue($userHandle, 'alwaysAnswerPrivate')) ? 'msg' : $mess->{channel}; my $line = $mess->{body}; print "line is $line\n"; $line =~ s/^\s*help//i; if ($line =~ s/^\s*set\s*//i) { if ($line =~ /^\s*$/) { return "The following options can be set: " . join(", ", sort keys %USER_OPTIONS) . ". help set OPTION will give" . " you more information on any of these options."; } my @matches = FindMinMatch($line, keys %USER_OPTIONS); if (@matches <= 0) { return "Cannot find help for option, \"$line\""; } else { my $opt = $matches[0]; my($val, $currentSettings); ; if ($opt eq "categoryParser") { $val = '(AdminAssume|BasicLawyer|Assistant|CTO|Press)'; my $name = $DATABASE->getUserConfigValue($userHandle, 'categoryParser'); my $parser = new SFLC::TimeTracker::Input::CategoryParser($name, userHandle => $userHandle); $currentSettings = " This option is currently set to \"$name\": " . $parser->get('description'); } else { $val = "\U$USER_OPTIONS{$opt}{type}\E_VALUE"; $currentSettings = " This option is currently set to " . $DATABASE->getUserConfigValue($userHandle, $opt) . "."; } return "SET $opt $val: " . join(" ", split /\n/, $USER_OPTIONS{$opt}{help}) . $currentSettings; } } elsif ($line =~ s/^\s*list\s*//i) { return <