# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*- ################################ # Greeting Module # ################################ package BotModules::Greeting; 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) = @_; return { '' => 'A polite module for saying hello and goodbye and so on.', 'hi' => 'To greet the bot.', 'bye' => 'To say goodbye to the bot.', 'ping' => 'To check the bot is alive.', 'status' => 'Gives the amount of time that the bot has been active.', 'seen' => 'Says how long it\'s been since the last time someone was seen. Syntax: seen victim', }; } # RegisterConfig - Called when initialised, should call registerVariables sub RegisterConfig { my $self = shift; $self->SUPER::RegisterConfig(@_); $self->registerVariables( # [ name, save?, settable? ] ['greetings', 1, 1, ['hi %', 'yo %', 'salut %', '%! dude!', '%: hello', '%', 'bonjour %', 'g\'day mate']], ['greetingsIndex', 1, 1, 0], ['byes', 1, 1, ['seeya %', 'bye %', 'night %', '/me waves goodbye to %']], ['byesIndex', 1, 1, 0], ['ow', 1, 1, ['%!! stop it!!', '%? You want something?', 'I\'m working! Leave me alone!', 'ow!', 'Leave me out of it!', '%: mean!']], ['owIndex', 1, 1, 0], ['veryow', 1, 1, ['OOOOWWWW!!!', 'GETOFF!!!', '/me fights back', 'Yikes! I\'m being attacked!!', '/me hits % over the head with a 2-by-4']], ['veryowIndex', 1, 1, 0], ['hit', 1, 1, ['/me smacks %target', '/me hits %target over the head with a hammer', '/me trips %target up and laughs', '%target! look over there! *smack*', '/me pokes %target in the ribs']], ['hitIndex', 1, 1, 0], ['hitProtected', 1, 1, {'hixie' => '%target: %source wanted me to hurt you but don\'t worry, i wuv you, i\'d never hurt you...', 'me' => '/me wacks %source in the legs with a crowbar', '' => '%source: Oh you\'d like that, wouldn\'t you, you sadist pervert.', 'yourself' => 'hey look everyone! %source likes to see others hurt themselves!', 'urself' => 'oh my! %source can\'t even spell! It\'s written "yourself", moron!'}], ['hitEnabled', 1, 1, 1], # set to 0 to disable hitting ['pat', 1, 1, ['/me patpats %target', '%target: yes dear, *pat* *pat*', '/me pats %target condescendingly', '%target: *pat* *pat*']], ['patIndex', 1, 1, 0], ['patProtected', 1, 1, {'' => '%source: what did I do now?', 'yourself' => '%source: why? what did i do wrong?'}], ['hug', 1, 1, ['/me hugs %target', '%target: *hug*', '/me hugs %target lovingly', '%target: come \'ere! *hugs and kisses*']], ['hugIndex', 1, 1, 0], ['yousuck', 1, 1, ['%: no, *you* suck!', '/me pouts', '/me cries', '/me . o O ( now what have i done... )']], ['yousuckIndex', 1, 1, 0], ['thanks', 1, 1, ['sure thing %', 'np', '%: np', '%: just doing my job!']], ['thanksIndex', 1, 1, 0], ['listen', 1, 1, ['(*', '%: I\'m listening.', '%?']], ['listenIndex', 1, 1, 0], ['happy', 1, 1, [':)', '/me smiles', 'yay', '/me beams']], ['happyIndex', 1, 1, 0], ['unhappy', 1, 1, [':(', '/me sobs', '/me cries', '*sniff*', 'but... but...', '/me is all sad']], ['unhappyIndex', 1, 1, 0], ['vhappy', 1, 1, ['OOoh! %!', 'I love you too, %.']], ['vhappyIndex', 1, 1, 0], ['kinky', 1, 1, ['eep!', 'me-ow!', 'oh yeah! spank me baby!', '/me tickles %', 'he-llo, baby!']], ['kinkyIndex', 1, 1, 0], ['tickle', 1, 1, ['eep!', 'iiiih!', 'meep!', '/me tickles % back', 'yelp!']], ['tickleIndex', 1, 1, 0], ['apology', 1, 1, ['Apology accepted.', 'thanks', 's\'ok', 'heh', 'that\'s ok']], ['apologyIndex', 1, 1, 0], ['whoami', 1, 1, 'I am a bot. /msg me the word \'help\' for a list of commands.'], ['lastrheet', 0, 0, 0], # time of last rheet ['rheetbuffer', 1, 1, 10], # max of 1 rheet per this many seconds ['rheetMaxEs', 1, 1, 100], # number of es at which to stop responding. ['autoGreetMute', 1, 1, []], # channels to mute in ['autoGreetings', 1, 1, {}], # people to greet and their greeting ['autoGreeted', 0, 0, {}], # people to NOT greet, and the last time ['autoGreetedBackoffTime', 1, 1, 20], # how long to not greet people (seconds) ['evil', 1, 1, ['c++ is evil', '/me mumbles something about c++ being evil', 'c++ is e-- ah, nevermind.', 'c++ sucks', '/me frowns at %']], ['evilIndex', 1, 1, 0], ['evilBackoffTime', 1, 1, 36000], # how long to not insult c++ (10 hours by default) ['lastEvil', 1, 0, 0], # when the last c++ insult took place ['assumeThanksTime', 1, 1, 10], # how long to assume that thanks are directed to us after hearing from them (seconds) ['_lastSpoken', 0, 0, {}], # who has spoken to us ['seenOverrides', 1, 1, {'therapist' => 'Look, dude, I\'m feeling fine, mm\'k?'}], # canned responses ['source', 1, 1, 'http://lxr.mozilla.org/mozilla/source/webtools/mozbot/'], # reply to give for CTCP SOURCE ); } 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."; } my $me = quotemeta($event->{'bot'}->nick); my $expandedme = join('+', split(//gos, $me)).'+'; if ($message =~ /^\s*(?:(?:g[ood\']*\s*)?(?:mornin[g\']?|evenin[g\']?|afternoon|day)|hi|heya?|bonjour|hoi|w+a+[sz]+u+p+\?*|hello|lo|wb|welcome\s+back|greetings|yo(?:\s+yo)*(?:\s+du+de)?|m+[ayh]+(?:\s+m+a+i+n+)?\s+m+a+n+|d+u+d+e+)[?!1.\s]*(?::-?[\)Pp]\s*)*$/osi) { if ($self->canGreet($event)) { $self->Perform($event, 'greetings'); } } elsif ($message =~ /^\s*(?:bye|(?:g?'?|good\s+)night|seeya|ciao)[?!1.\s]*$/osi) { $self->Perform($event, 'byes'); } elsif ($message =~ /^\s*say[\s:,\"\']+(hi|hello|lo|good\s*bye|seeya)(?:\s+to\s+(\S+))(?:[,\s]*please)?[?!1.\s]*$/osi) { if ($2) { $self->say($event, "$2: $1"); } else { $self->say($event, "$1"); } } elsif ($message =~ /^\s* (?: (?:you|u) \s+ (?:really\s+)? suck (?: \s+hard | (?:\s+big)? \s+ rocks)? | (?:you|u) \s+ (?:smell|stick) | (?:you|u) (?:\s+are|\s+r|'re|r) \s+ (?:an?\s+)? (?:really\s+)* (?:idiot|stupid|dumb|moron|moronic|useless) (?:\s+bot)? | i \s+ hate \s+ (?:you|u) | bi+tch) [?!1.\s]*$/osix) { $self->Perform($event, 'yousuck'); } elsif ($message =~ /^\s*(?:oh[!1?.,\s]*)?(?:thanks|thank\s+you|cheers)[\s!1.]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/osi) { $self->Perform($event, 'thanks'); } elsif ($message =~ /^\s*(?:good\s+bot[.!1\s]*|(?:you|u)\s+rock(?:\s+bot)?|:-?\)|(?:have\s+a\s+)?bot\s*snack[.!1\s]*)\s*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/osi) { $self->Perform($event, 'happy'); } elsif ($message =~ /^\s*(?:i|we)\s+love\s+(?:you|u)[.!1\s]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/osi) { $self->Perform($event, 'happy'); } elsif ($message =~ /^\s*(?:please[\s,.]+)?(?:(?:would|will)\s+you\s+)?(?:hit|kick|slap|smack)\s+(\S+?)(?:[\s,.]+please)?[.!?\s]*\s*$/osi) { if ($self->{'hitEnabled'}) { $self->PerformOnOther($event, 'hit', $1); } } elsif ($message =~ /^\s*(?:please[\s,.]+)?(?:(?:would|will)\s+you\s+)?(?:pat|pat\s*pat)\s+(\S+?)(?:[\s,.]+please)?[.!?\s]*\s*$/osi) { $self->PerformOnOther($event, 'pat', $1); } elsif ($message =~ /^\s*(?:please[\s,.]+)?(?:(?:would|will)\s+you\s+)?(?:hug)\s+(\S+?)(?:[\s,.]+please)?[.!?\s]*\s*$/osi) { $self->PerformOnOther($event, 'hug', $1); } elsif ($message =~ /^\s*(?:useless|die|get\s+a\s+life|kiss\s+my\s+ass|you\s+stupid\s+piece\s+o[f']?\s+code)[!1.\s]*$/osi) { $self->Perform($event, 'unhappy'); } elsif ($message =~ /^\s*sorry\b/osi) { # note that any trailing text is ignored $self->Perform($event, 'apology'); } elsif ($message =~ /^\s*(?:how\s+are\s+you|how\s+do\s+you\s+do|how\'?s\s+things|are\s+you\s+ok)(?:[?!1.,\s]+$expandedme)?\s*[?!1.\s]*$/osi) { $uptime = $self->days($^T); $self->say($event, "$event->{'from'}: fine thanks! I've been up $uptime so far!"); } elsif ($message =~ /^\s*(?:who\s+are\s+you)\s*[?!1.\s]*$/osi) { $self->say($event, "$event->{'from'}: $self->{'whoami'}"); } elsif ($message =~ /^\s*(?:up\s*(?:time)|status)[?!1.\s]*$/osi) { $uptime = $self->days($^T); $self->say($event, "$event->{'from'}: I've been up $uptime."); } elsif ($message =~ /^\s*r+h+e(e+)t+[!1.\s]*$/osi) { if (length($1) < $self->{'rheetMaxEs'}) { $self->say($event, "$event->{'from'}: rhe$1$1t!"); } else { $self->say($event, "$event->{'from'}: uh, whatever."); } } elsif ($message =~ /^\s*ping\s*$/osi) { $self->say($event, "$event->{'from'}: pong"); } 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."; } my $me = quotemeta($event->{'bot'}->nick); my $expandedme = join('+', split(//gos, $me)).'+'; if ($message =~ /^\s*(?:(?:(?:(?:g[ood\']*\s*)?(?:mornin[g\']?|evenin[g\']?|afternoon|day)|hi|heya?|bonjour|hoi|w+a+[sz]+u+p+|hello|lo|wb|welcome\s+back|greetings|yo(?:\s+yo)*)\s+)?$expandedme[!1\s]*|o+h[\s,.!?]+look[\s,.!?]+a\s+$me[\s.!1]*)(?::-?[\)Pp]\s*)*$/si) { if ($self->canGreet($event)) { $self->Perform($event, 'greetings'); } } elsif ($message =~ /^\s*(?:bye|(?:g?\'?|good\s+)night|seeya|ciao)\s+$me[!1.\s]*$/si) { $self->Perform($event, 'byes'); } elsif ($message =~ /^\s*(?:oh[!1?,.\s]*)?(?:thanks|thank\s*you|cheers)\s+$me[\s!1.]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/si) { $self->Perform($event, 'thanks'); } elsif (($message =~ /^\s*(?:oh[!1?,.\s]*)?(?:thanks|thank\s*you|cheers)[\s!1.]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/osi) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'thanks'); } elsif (($message =~ /^\s*(?:good\s+bot)[!1.\s]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/osi) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'happy'); } elsif (($message =~ /^\s*(?:bad|foo[l\']?|idiot|dumb|useless|moron|moronic)(?:\s+bot)?[!.\s]*?$/osi) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'unhappy'); } elsif (($message =~ /^\s*bad\s*$me[!.\s]*$/si) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'unhappy'); } elsif (($message =~ /^\s* (?: (?:you|u) \s+ (?:really\s+)? suck (?: \s+hard | (?:\s+big)? \s+ rocks)? | (?:you|u) \s+ (?:smell|stick) | (?:you|u) (?:\s+are|\s+r|'re|r) \s+ (?:an?\s+)? (?:really\s+)? (?:idiot|stupid|dumb|moron|moronic) (?:\s+bot)? | i \s+ hate \s+ (?:you|u) | bi+tch) [?!1.\s]*$/osix) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'yousuck'); } elsif ($message =~ /^\s*(?:good(?:\s$me)?|yay[\s!1.]*|i\s+love\s+(?:you|u))\s+$me[\s!1.]*(?:[;:8][-o]?[]()\|O0<>[]\s*)?$/si) { $self->Perform($event, 'happy'); } elsif ($message =~ /^\s*(?:$me\s*[.?\/]+)\s*$/si) { $self->Perform($event, 'listen'); } elsif ($message =~ /^\s*r+h(e+)t+[!1.\s]*$/osi) { if (($event->{'time'}-$self->{'lastrheet'}) > $self->{'rheetbuffer'}) { if (length($1) < $self->{'rheetMaxEs'}) { $self->say($event, "rhe$1$1t!"); } $self->{'lastrheet'} = $event->{'time'}; } } elsif ($message =~ /^.+\s+c\+\+\s+.+$/osi) { if (($event->{'time'} - $self->{'lastEvil'}) > $self->{'evilBackoffTime'}) { $self->{'lastEvil'} = $event->{'time'}; $self->Perform($event, 'evil'); # calls GetNext which calls saveConfig } } elsif ($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."; } my $me = quotemeta($event->{'bot'}->nick); if ($message =~ /^\s*(?:greets\s+$me|shakes\s+$me'?s\s+hand)[\s!1.]*$/si) { $self->Perform($event, 'greetings'); } elsif ($message =~ /^\s*(?:pokes|prods)\s+$me(?:[,\s]+too|\s+as\s+well)?[\s!1.]*$/si) { $self->Perform($event, 'ow'); } elsif ($message =~ /^\s*(?:stabs|slaps|kicks|kills|hits|punches)\s+$me[\s!1.]*$/si) { $self->Perform($event, 'veryow'); } elsif ($message =~ /^\s*lights\s+$me\s+on\s+fire[!1.\s]*$/si) { $self->Perform($event, 'veryow'); } elsif ($message =~ /^\s*(?:pats|strokes|pets)\s+$me(:?\s+affectionately|\s+lovingly)?[!1.\s]*$/si) { $self->Perform($event, 'happy'); } elsif ($message =~ /^\s*slaps\s+$me\s+(?:around\s+)?(?:a\s+(?:bit|lot|little|while)\s+)?with\s+a\s+(?:(?:big|fat|large|wet|and)[\s,]+)*trout[\s!1.]*$/si) { $self->Perform($event, 'ow'); } elsif ($message =~ /^\s*(?:hits|kicks|slaps|smacks)\s+$me[\s!1.]*$/si) { $self->Perform($event, 'yousuck'); } elsif ($message =~ /^\s*(?:glares|stares)\s+at\s+$me[\s!1.]*$/si) { $self->Perform($event, 'yousuck'); } elsif ($message =~ /^\s*(?:hugs|cuddles|snuggles(?:\s+up\s*to|\s+with)?|kisses|loves)\s+$me[\s!1.]*$/si) { $self->Perform($event, 'vhappy'); } elsif ($message =~ /^\s*(?:bites|spanks)\s+$me[\s.]*$/si) { $self->Perform($event, 'kinky'); } elsif ($message =~ /^\s*(?:tickles)\s+$me[\s.]*$/si) { $self->Perform($event, 'tickle'); } elsif ($message =~ /^\s*(?:gives|hands|passes|offers)\s+$me\s+(?:a\s+(?:bot\s*)?(?:snack|cookie)|a\s+present|cash|congratulations|applause|praise)[\s!1.]*$/si) { $self->Perform($event, 'happy'); } elsif ($message =~ /^\s*(?:gives|hands|passes|offers)\s+$me\s+(?:a\s+hot\s+date)[\s!1.]*$/si) { $self->Perform($event, 'vhappy'); } 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."; } if ($message =~ /^\s*r+h+e(e+)t+s?[!1.\s]*$/osi) { if (($event->{'time'}-$self->{'lastrheet'}) > $self->{'rheetbuffer'}) { $self->say($event, "rhe$1$1t!"); $self->{'lastrheet'} = $event->{'time'}; } } elsif (($message =~ /^\s*(?:smiles)\s*[!1.\s]*$/si) and ($self->canAssumeThanks($event))) { $self->Perform($event, 'happy'); } else { return $self->SUPER::Felt(@_); } return 0; # we've dealt with it, no need to do anything else. } # SpottedJoin - Called when someone joins a channel sub SpottedJoin { my $self = shift; my ($event, $channel, $who) = @_; return if grep lc($_) eq $channel, @{$self->{'autoGreetMute'}}; my $user = $event->{'user'}; if ($self->canGreet($event) and $self->{'autoGreetings'}->{$who}) { $self->sayOrEmote($event, $self->Expand($event, $self->{'autoGreetings'}->{$who})); $self->{'autoGreeted'}->{$user} = $event->{'time'}; } return 1; # don't block other modules... } # 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 CTCPPing { my $self = shift; my ($event, $who, $what) = @_; $self->ctcpReply($event, 'PING', $what); } sub CTCPSource { my $self = shift; my ($event, $who, $what) = @_; $self->ctcpReply($event, 'SOURCE', $self->{'source'}); } sub GetNext { my $self = shift; my ($list) = @_; $self->{"${list}Index"} = 0 if $self->{"${list}Index"} > $#{$self->{$list}}; my $reply = $self->{$list}->[$self->{"${list}Index"}++]; $self->saveConfig(); return $reply; } sub canGreet { my $self = shift; my ($event) = @_; my $user = $event->{'user'}; my $reply = 1; if (defined($self->{'autoGreeted'}->{$user})) { $reply = (($event->{'time'} - $self->{'autoGreeted'}->{$user}) > $self->{'autoGreetedBackoffTime'}); delete($self->{'autoGreeted'}->{$user}); } return $reply; } sub canAssumeThanks { my $self = shift; my ($event) = @_; my $who = $event->{'user'}; return ((defined($self->{'_lastSpoken'}->{$who})) and (($event->{'time'} - $self->{'_lastSpoken'}->{$who}) <= $self->{'assumeThanksTime'})); } sub Perform { my $self = shift; my ($event, $list) = @_; $self->sayOrEmote($event, $self->Expand($event, $self->GetNext($list))); } # replaces '%' with the target nick (XXX cannot escape a "%"!!!) sub Expand { my $self = shift; my ($event, $data) = @_; $data =~ s/%/$event->{'from'}/gos; return $data; } sub PerformOnOther { my $self = shift; my ($event, $list, $other) = @_; my $data; my $me = quotemeta($event->{'nick'}); if ($other =~ m/^$me$/si and defined $self->{"${list}Protected"}->{''}) { $data = $self->{"${list}Protected"}->{''}; } elsif (defined $self->{"${list}Protected"}->{lc $other}) { $data = $self->{"${list}Protected"}->{lc $other}; } else { $data = $self->GetNext($list); } if ($other eq 'me') { $other = $event->{'from'}; } $data =~ s/%source/$event->{'from'}/gos; $data =~ s/%target/$other/gos; $self->sayOrEmote($event, $data); } sub DoSeen { my $self = shift; my ($event, $who) = @_; 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->{'seenOverrides'}->{$who})) { $self->say($event, $self->{'seenOverrides'}->{$who}); } else { my $seconds = $seen->{'times'}->{lc $who}; if (defined($seconds)) { my $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'}->{lc $who}; $self->say($event, "$who was last seen $time, $what"); } else { my $n = ''; if ($who =~ /^[aeiou]/o) { $n = 'n'; } $self->say($event, "I've never seen a$n '$who', sorry."); } } } sub Unload { untie(%{$seen->{'times'}}); untie(%{$seen->{'states'}}); }