# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*- ################################ # Seen Module # ################################ package BotModules::Seen; use vars qw(@ISA); @ISA = qw(BotModules); use AnyDBM_File; use Fcntl; 1; # SpottedNickChange would be a nice one to do if you # can solve the problem of working out which channel # to say stuff in... # database for seen data our $seen = {'times' => {}, 'states' => {}}; # the times that the relevant nicks were last seen active tie(%{$seen->{'times'}}, 'AnyDBM_File', 'seen-times', O_RDWR|O_CREAT, 0666); # what the relevant nicks were last seen doing tie(%{$seen->{'states'}}, 'AnyDBM_File', 'seen-states', O_RDWR|O_CREAT, 0666); sub Help { my $self = shift; my ($event) = @_; my %commands = { 'seen' => 'Says how long it\'s been since the last time someone was seen. Syntax: seen victim', }; if ($self->isAdmin($event)) { $commands{'mute'} = 'Stop responding to !seen in a channel unless told directly. Syntax: mute seen in '; $commands{'unmute'} = 'Start responding to !seen in a channel. Syntax: unmute seen in '; } return \%commands; } # RegisterConfig - Called when initialised, should call registerVariables sub RegisterConfig { my $self = shift; $self->SUPER::RegisterConfig(@_); $self->registerVariables( # [ name, save?, settable? ] ['overrides', 1, 1, {'therapist' => 'Look, dude, I\'m feeling fine, mm\'k?'}], # canned responses ['maxLines', 1, 1, 5], ['directOnlyChannels', 1, 1, []], #list of channels where we're only observing and not responding to !seen unless told. ); } sub Told { my $self = shift; my ($event, $message) = @_; my $now = $event->{'time'}; $self->{'_lastSpoken'}->{$event->{'user'}} = $now; if ($event->{'channel'} ne '') { my $channel = $event->{'channel'}; $seen->{'times'}->{lc $event->{'from'}} = $now; $seen->{'states'}->{lc $event->{'from'}} = "saying '$message' to me in $channel."; } if ($self->isAdmin($event) and $message =~ /^\s*(un)?mute\s+seen\s+in\s+(\S+)\s*$/osi){ my $mute = !defined($1); my $channel = lc $2; $channel =~ s/^\#?/\#/; # Add # character if needed. $self->MuteOrUnmuteChannel($event, $mute, $channel); }elsif ($message =~ /^\s*!?seen\s+(\S+?)[\s?.!]*$/osi) { $self->DoSeen($event, $1); } else { return $self->SUPER::Told(@_); } return 0; # we've dealt with it, no need to do anything else. } sub Heard { my $self = shift; my ($event, $message) = @_; if ($event->{'channel'} ne '') { my $channel = $event->{'channel'}; $seen->{'times'}->{lc $event->{'from'}} = $event->{'time'}; $seen->{'states'}->{lc $event->{'from'}} = "saying '$message' in $channel."; } if (!(grep {$event->{'channel'} eq $_} @{$self->{'directOnlyChannels'}}) and $message =~ /^\s*!seen\s+(\S+)\s*$/osi) { $self->DoSeen($event, $1); } else { return $self->SUPER::Heard(@_); } return 0; # we've dealt with it, no need to do anything else. } sub Felt { my $self = shift; my ($event, $message) = @_; if ($event->{'channel'} ne '') { my $nick = $event->{'from'}; my $channel = $event->{'channel'}; $seen->{'times'}->{lc $event->{'from'}} = $event->{'time'}; $seen->{'states'}->{lc $event->{'from'}} = "saying '* $nick $message' in $channel."; } else { return $self->SUPER::Felt(@_); } return 0; # we've dealt with it, no need to do anything else. } sub Saw { my $self = shift; my ($event, $message) = @_; if ($event->{'channel'} ne '') { my $nick = $event->{'from'}; my $channel = $event->{'channel'}; $seen->{'times'}->{lc $event->{'from'}} = $event->{'time'}; $seen->{'states'}->{lc $event->{'from'}} = "saying '* $nick $message' in $channel."; } else { return $self->SUPER::Felt(@_); } return 0; # we've dealt with it, no need to do anything else. } # SpottedNickChange - Called when someone changes nick sub SpottedNickChange { my $self = shift; my ($event, $from, $to) = @_; $seen->{'times'}->{lc $event->{'from'}} = $event->{'time'}; $seen->{'states'}->{lc $event->{'from'}} = "changing nick to $to."; return $self->SUPER::SpottedNickChange(@_); } sub DoSeen { my $self = shift; my ($event, $who) = @_; my $pattern; if (lc $who eq lc $event->{'from'}) { $self->say($event, 'You\'re right here, duh!'); } elsif (lc $who eq lc $event->{'nick'}) { $self->say($event, 'I\'m right here, duh!'); } elsif (defined($self->{'overrides'}->{$who})) { $self->say($event, $self->{'overrides'}->{$who}); } else { my $regexp; my @nicksToList = (); if ($who =~ m!^/(\S+)/$!) { # shouldn't allow mix and match or blank RE or spaces. $regexp = $1; my $re = $self->sanitizeRegexp($regexp); # security + safety first! $re = qr/$re/i; #precompile for performance if ('' =~ $re){ # will match everything, throw error. $self->say($event, 'That pattern matches everything, please be more specific.'); return; } @nicksToList = grep {$_ =~ $re} (keys %{$seen->{'times'}}); $pattern = 1; } else { if ($who =~ /\*/){ # no point going through the motions if there's no wildcard. $regexp = quotemeta(lc $who); $regexp =~ s/\\\*/\\S*/g; # replace the escaped * from quotemeta with a \S* (XXX wanted: the ? wildcard) my $re = qr/^$regexp$/; if ('' =~ $re){ # will match everything, throw error. $self->say($event, 'That pattern matches everything, please be more specific.'); return; } @nicksToList = grep {$_ =~ $re} (keys %{$seen->{'times'}}); } else { @nicksToList = (lc $who) if defined($seen->{'times'}{lc $who}); # short circuit for the majority of uses } $pattern = 0; } if (@nicksToList > $self->{'maxLines'}) { # if it's more than the set threshold, don't flood :) $self->say($event,"There are more than $self->{'maxLines'} nicks matching that wildcard, please be more specific."); } elsif (@nicksToList > 0) { foreach my $nick (@nicksToList) { my $seconds = $seen->{'times'}->{$nick}; $seconds = $event->{'time'} - $seconds; my $time = ''; if ($seconds > 90) { my $minutes = int $seconds / 60; $seconds %= 60; if ($minutes > 90) { my $hours = int $minutes / 60; $minutes %= 60; if ($hours > 36) { my $days = int $hours / 24; $hours %= 24; if ($days > 10) { my $weeks = int $days / 7; $days %= 7; if ($weeks > 10) { # good god, nice connection } if ($weeks != 0) { if ($time ne '') { $time .= ', '; } if ($weeks == 1) { $time .= "$weeks week"; } else { $time .= "$weeks weeks"; } } } if ($days != 0) { if ($time ne '') { $time .= ', '; } if ($days == 1) { $time .= "$days day"; } else { $time .= "$days days"; } } } if ($hours != 0) { if ($time ne '') { $time .= ', '; } if ($hours == 1) { $time .= "$hours hour"; } else { $time .= "$hours hours"; } } } if ($minutes != 0) { if ($time ne '') { $time .= ', '; } if ($minutes == 1) { $time .= "$minutes minute"; } else { $time .= "$minutes minutes"; } } } if ($seconds == 0) { if ($time eq '') { $time .= 'right about now'; } else { $time .= ' ago'; } } else { if ($time ne '') { $time .= ' and '; } if ($seconds == 1) { $time .= 'a second ago'; } elsif ($seconds == 2) { $time .= 'a couple of seconds ago'; } else { $time .= "$seconds seconds ago"; } } my $what = $seen->{'states'}->{$nick}; $self->say($event, "$nick was last seen $time, $what"); } } else { my $n = ''; if ($who =~ /^[aeiou]/o) { $n = 'n'; } if ($pattern == 1) { $self->say($event, "I've never seen anyone matching the pattern '$who', sorry."); } else { $self->say($event, "I've never seen a$n '$who', sorry."); } } } } sub Unload { untie(%{$seen->{'times'}}); untie(%{$seen->{'states'}}); } sub MuteOrUnmuteChannel { my $self = shift; my ($event, $mute, $channel) = @_; if ($mute){ if (grep {$_ eq $channel} @{$self->{'directOnlyChannels'}}){ $self->say($event,"I'm already ignoring !seen in $channel."); } else{ push @{$self->{'directOnlyChannels'}}, $channel; $self->say($event, "I won't respond to !seen in $channel anymore unless told directly."); $self->saveConfig(); } } else { if (grep {$_ eq $channel} @{$self->{'directOnlyChannels'}}){ @{$self->{'directOnlyChannels'}} = map {$_ ne $channel} @{$self->{'directOnlyChannels'}}; $self->say($event,"I'll start responding to !seen in $channel now."); $self->saveConfig(); } else{ $self->say($event, "I'm already responding to !seen in $channel."); } } }