diff options
Diffstat (limited to 'scripts/seen.pl')
| -rw-r--r-- | scripts/seen.pl | 1198 | 
1 files changed, 1198 insertions, 0 deletions
| diff --git a/scripts/seen.pl b/scripts/seen.pl new file mode 100644 index 0000000..9d811b4 --- /dev/null +++ b/scripts/seen.pl @@ -0,0 +1,1198 @@ +use strict; +use 5.005_62;       # for 'our' +use Irssi 20020428; # for Irssi::signal_continue +use vars qw($VERSION %IRSSI); + +$VERSION = "1.8"; +%IRSSI = ( +    authors     => 'Marcin \'Qrczak\' Kowalczyk', +    contact     => 'qrczak@knm.org.pl', +    name        => 'Seen', +    description => 'Tell people when other people were online', +    license     => 'GPL', +    url         => 'http://qrnik.knm.org.pl/~qrczak/irssi/seen.pl', +); + +######## User interface ######## + +# COMMANDS +# ======== +# +# /seen <nick> +#     Show last seen info about nick. +# +# /say_seen [<to_whom>] <nick> +#     Say last seen info about nick in the current window. If to_whom +#     is present, answer as if that person issued a seen request. +# +# /listen on [[<chatnet>] <channel>] +#     Turn on listening for seen requests in the current or given channel. +# +# /listen off [[<chatnet>] <channel>] +#     Turn off listening for seen requests in the current or given channel. +# +# /listen delay [[<chatnet>] <channel>] +#     Turn on listening for seen requests in the current or given channel. +#     We will reply only if nobody else replies with a message containing +#     the given nick (probably a seen reply from another bot) in seen_delay +#     seconds. +# +# /listen private [[<chatnet>] <channel>] +#     Turn on listening for seen requests in the current or given channel. +#     The reply will be sent as a private notice. +# +# /listen disable [[<chatnet>] <channel>] +#     Same as "off", used to distinguish channels where we won't listen +#     for sure from channels we didn't specify anything about. +# +# /listen list +#     Show which channels we are listening for seen requests on. + +# Forms of seen requests from other people: +#     Public message "<our_nick>: seen <nick>". +#     Public message "seen <nick>" on channels where we are listening. +#     Private message "seen <nick>". +#     Any of the above with "!seen" instead of "seen". +#     Any of the above with a question mark at the end. +#     Any of the above with "jest <nick>?", "by³ <nick>?", "by³a <nick>?", +#       "<nick> jest?", "<nick> by³?", "<nick> by³a?", with optional +#       "czy" at the beginning - provided that we know that nick +#       (to avoid treating some other message as a seen request). + +# VARIABLES +# ========= +# +# seen_expire_after +#     After that number of days we forget about nicks and addresses. +#     Default 30. +# +# seen_expire_asked_after +#     After that number of days we forget that that somebody was +#     searched for and don't send a notice. Default 7. +# +# seen_delay    +#     On channels set to '/listen delay' we reply if after that number +#     of seconds nobody else replies. Default 60. + +######## Internal structure of the database in memory ######## + +# %listen_on      = (chatnet => {channel => listening}) +# %address_absent = (chatnet => {address => time}) +# %nicks          = (chatnet => {address => [nick]}) +# %last_nicks     = (chatnet => {address => nick}) +# %how_quit       = (chatnet => {address => how_quit}) +# %spoke          = (chatnet => {address => time}) +# %nick_absent    = (chatnet => {nick => time}) +# %addresses      = (chatnet => {nick => address}) +# %orig_nick      = (chatnet => {nick => nick}) +# %channels       = (chatnet => {nick => [channel]}) +# %asked          = (chatnet => {nick => {nick_asks => time}}) + +# listening: +#   'on', undef = 'off', 'delay', 'private', 'disable' + +# how_quit: +#   ['disappeared'] +#   ['was_left', kanal] +#   ['left', channel, reason] +#   ['quit', channels, reason] +#   ['was_kicked', channel, kicker, reason] + +######## Global variables ######## + +our %listen_on = (); +our %address_absent = (); +our %nicks = (); +our %last_nicks = (); +our %how_quit = (); +our %spoke = (); +our %nick_absent = (); +our %addresses = (); +our %orig_nick = (); +our %channels = (); +our %asked = (); + +Irssi::settings_add_int "seen", "seen_expire_after", 30;      # days +Irssi::settings_add_int "seen", "seen_expire_asked_after", 7; # days +Irssi::settings_add_int "seen", "seen_delay", 60;             # seconds + +our $database     = Irssi::get_irssi_dir . "/seen.dat"; +our $database_tmp = Irssi::get_irssi_dir . "/seen.tmp"; +our $database_old = Irssi::get_irssi_dir . "/seen.dat~"; + +######## Utilities ######## + +our $nick_regexp = qr/ +  [A-Z\[\\\]^_`a-z{|}\200-\377] +  [\-0-9A-Z\[\\\]^_`a-z{|}\200-\377]* +  /x; +our $seen_regexp = qr/^ *!?seen +($nick_regexp) *\?* *$/i; +our $maybe_seen_regexp1 = qr/ +  ^\ * +  (?:a\ +)? +  (?:(?:if|when|here)\ +)? +  (?:(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e)\ +)* +  (?:in|by[³l]a?)\ + +  (?:(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e)\ +)* +  ($nick_regexp) +  (?:\ +(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e))* +  \ *\?+\ *$/ix; +our $maybe_seen_regexp2 = qr/ +  ^\ * +  (?:a\ +)? +  (?:(?:czy|kiedy|gdzie)\ +)? +  (?:(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e)\ +)* +  ($nick_regexp)?\ + +  (?:(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e)\ +)* +  (?:in|by[³l]a?) +  (?:\ +(?:dzi[¶s]|today|last time|recently|ju[¿z]|here|tutaj|mo[¿z]e))* +  \ *\?+\ *$/ix; +our $exclude_regexp = qr/^(?:kto[¶s]?|who?|that?|that|ladna|i|a)$/i; + +sub lc_irc($) { +    my ($str) = @_; +    $str =~ tr/A-Z[\\]/a-z{|}/; +    return $str; +} + +sub uc_irc($) { +    my ($str) = @_; +    $str =~ tr/a-z{|}/A-Z[\\]/; +    return $str; +} + +our %lc_regexps = (); + +sub lc_irc_regexp($) { +    my ($str) = @_; +    $str =~ s/(.)/my $lc = lc_irc $1; my $uc = uc_irc $1; "[\Q$lc$uc\E]"/eg; +    return $str; +} + +sub canonical($) { +    my ($address) = @_; +    $address =~ s/^[\^~+=-]//; +    return $address; +} + +sub show_list(@) { +    @_ == 0 and return ""; +    @_ == 1 and return $_[0]; +    return join(", ", @_[0..$#_-1]) . " i " . $_[$#_]; +} + +sub show_time_since($) { +    my ($time) = @_; +    my $diff = time() - $time; +    $diff >= 0 or return "nie wiem kiedy (zegarek mi sie popsul)"; +    my $s = $diff % 60; $diff = int(($diff - $s) / 60); +    my $m = $diff % 60; $diff = int(($diff - $m) / 60); +    my $h = $diff % 24; $diff = int(($diff - $h) / 24); +    my $d = $diff; +    my $s_txt = $s ? "${s}s " : ""; +    my $m_txt = $m ? "${m}m " : ""; +    my $h_txt = $h ? "${h}h " : ""; +    my $d_txt = $d ? "${d}d " : ""; +    return +      $d ? "$d_txt${h_txt}ago" : +      $h ? "$h_txt${m_txt}ago" : +      $m ? "$m_txt${s_txt}ago" : +      "${s}s ago"; +} + +sub all_channels($@) { +    my ($chatnet, @nicks) = @_; +    my %chans = (); +    foreach my $nick (@nicks) { +        if ($channels{$chatnet}{lc_irc $nick}) { +            foreach my $channel (@{$channels{$chatnet}{lc_irc $nick}}) { +                $chans{$channel} = 1; +            } +        } +    } +    return keys %chans; +} + +sub is_private($) { +    my ($channel) = @_; +    return $channel && $channel->{mode} =~ /^[^ ]*[ps]/; +} + +sub mark_private($$) { +    my ($channel, $name) = @_; +    return is_private $channel ? "-$name" : $name; +} + +######## Actions on the database in memory ######## + +sub do_listen($$$) { +    my ($chatnet, $channel, $state) = @_; +    if ($state eq 'off') { +        delete $listen_on{$chatnet}{$channel}; +    } else { +        $listen_on{$chatnet}{$channel} = $state; +    } +} + +sub do_join($$$$) { +    my ($chatnet, $address, $nick, $channel) = @_; +    my $lc_nick = lc_irc $nick; +    my $lc_channel = lc_irc $channel; +    delete $address_absent{$chatnet}{$address}; +    push @{$nicks{$chatnet}{$address}}, $nick +      unless grep {lc_irc $_ eq $lc_nick} @{$nicks{$chatnet}{$address}}; +    push @{$channels{$chatnet}{$lc_nick}}, $channel +      unless grep {lc_irc $_ eq $lc_channel} @{$channels{$chatnet}{$lc_nick}}; +    delete $how_quit{$chatnet}{$address}; +    delete $nick_absent{$chatnet}{$lc_nick}; +    $addresses{$chatnet}{$lc_nick} = $address; +    $orig_nick{$chatnet}{$lc_nick} = $nick; +} + +sub do_quit_all($$$$$) { +    my ($time, $chatnet, $address, $nick, $reason) = @_; +    $address_absent{$chatnet}{$address} = $time; +    delete $nicks{$chatnet}{$address}; +    $last_nicks{$chatnet}{$address} = $nick; +    $how_quit{$chatnet}{$address} = $reason; +} + +sub do_quit($$$$) { +    my ($time, $chatnet, $address, $nick) = @_; +    my $lc_nick = lc_irc $nick; +    $nicks{$chatnet}{$address} = +      [grep {lc_irc $_ ne $lc_nick} @{$nicks{$chatnet}{$address}}]; +    delete $channels{$chatnet}{$lc_nick}; +    $nick_absent{$chatnet}{$lc_nick} = $time; +    $addresses{$chatnet}{$lc_nick} = $address; +    $orig_nick{$chatnet}{$lc_nick} = $nick; +} + +sub do_part($$$$) { +    my ($chatnet, $address, $nick, $channel) = @_; +    my $lc_nick = lc_irc $nick; +    my $lc_channel = lc_irc $channel; +    $channels{$chatnet}{$lc_nick} = +      [grep {lc_irc $_ ne $lc_channel} @{$channels{$chatnet}{$lc_nick}}]; +} + +sub do_nick($$$$$) { +    my ($time, $chatnet, $address, $old_nick, $new_nick) = @_; +    my $lc_old_nick = lc_irc $old_nick; +    my $lc_new_nick = lc_irc $new_nick; +    $nicks{$chatnet}{$address} = +      [(grep {lc_irc $_ ne $lc_old_nick} @{$nicks{$chatnet}{$address}}), $new_nick]; +    my $chans = $channels{$chatnet}{$lc_old_nick}; +    delete $channels{$chatnet}{$lc_old_nick}; +    $channels{$chatnet}{$lc_new_nick} = $chans; +    $nick_absent{$chatnet}{$lc_old_nick} = $time; +    delete $nick_absent{$chatnet}{$lc_new_nick}; +    $addresses{$chatnet}{$lc_new_nick} = $address; +    $orig_nick{$chatnet}{$lc_new_nick} = $new_nick; +} + +sub do_spoke($$$) { +    my ($time, $chatnet, $address) = @_; +    my $old_time = $spoke{$chatnet}{$address}; +    $spoke{$chatnet}{$address} = $time +      unless defined $old_time && $old_time > $time; +} + +sub do_ask($$$$) { +    my ($time, $chatnet, $nick, $nick_asks) = @_; +    my $lc_nick = lc_irc $nick; +    my $lc_nick_asks = lc_irc $nick_asks; +    my $old_time = $asked{$chatnet}{$lc_nick}{$lc_nick_asks}; +    $asked{$chatnet}{$lc_nick}{$lc_nick_asks} = $time +      unless defined $old_time && $old_time > $time; +} + +sub do_forget_ask($$$) { +    my ($chatnet, $nick, $nick_asks) = @_; +    my $lc_nick = lc_irc $nick; +    my $lc_nick_asks = lc_irc $nick_asks; +    delete $asked{$chatnet}{$lc_nick}{$lc_nick_asks}; +} + +######## Actions on the database in memory and in the file ######## + +sub append_to_database(@) { +    open DATABASE, ">>$database"; +    print DATABASE map {"$_\n"} @_; +    close DATABASE; +} + +sub on_listen($$$) { +    my ($chatnet, $channel, $state) = @_; +    do_listen $chatnet, $channel, $state; +    append_to_database "listen $state $chatnet $channel"; +} + +sub on_join($$$$) { +    my ($chatnet, $address, $nick, $channel) = @_; +    do_join $chatnet, $address, $nick, $channel; +    append_to_database "join $chatnet $address $nick $channel"; +} + +sub on_quit_all($$$$) { +    my ($chatnet, $address, $nick, $reason) = @_; +    my $time = time(); +    do_quit_all $time, $chatnet, $address, $nick, $reason; +    append_to_database "quit_all $time $chatnet $address $nick @$reason"; +} + +sub on_quit($$$$) { +    my ($chatnet, $address, $nick, $reason) = @_; +    my $time = time(); +    do_quit $time, $chatnet, $address, $nick; +    append_to_database "quit $time $chatnet $address $nick"; +    on_quit_all $chatnet, $address, $nick, $reason +      unless @{$nicks{$chatnet}{$address}}; +} + +sub on_part($$$$$) { +    my ($chatnet, $address, $nick, $channel, $reason) = @_; +    do_part $chatnet, $address, $nick, $channel; +    append_to_database "part $chatnet $address $nick $channel"; +    on_quit $chatnet, $address, $nick, $reason +      unless @{$channels{$chatnet}{lc_irc $nick}}; +} + +sub on_nick($$$$) { +    my ($chatnet, $address, $old_nick, $new_nick) = @_; +    my $time = time(); +    do_nick $time, $chatnet, $address, $old_nick, $new_nick; +    append_to_database "nick $time $chatnet $address $old_nick $new_nick"; +} + +sub on_spoke($$) { +    my ($chatnet, $address) = @_; +    my $time = time(); +    return if $spoke{$chatnet}{$address} == $time; +    do_spoke $time, $chatnet, $address; +    append_to_database "spoke $time $chatnet $address"; +} + +sub on_ask($$$) { +    my ($chatnet, $nick, $nick_asks) = @_; +    my $time = time(); +    do_ask $time, $chatnet, $nick, $nick_asks; +    append_to_database "ask $time $chatnet $nick $nick_asks"; +} + +######## Reading the database from file ######## + +sub syntax_error() { +    die "Syntax error in $database: $_"; +} + +our %parse_how_quit = ( +    disappeared => sub { +        return ['disappeared']; +    }, +    was_left => sub { +        $_[0] =~ /^ ([^ ]*)$/ or syntax_error; +        return ['was_left', $1]; +    }, +    left => sub { +        $_[0] =~ /^ ([^ ]*) (.*)$/ or syntax_error; +        return ['left', $1, $2]; +    }, +    quit => sub { +        $_[0] =~ /^ ([^ ]*) (.*)$/ or syntax_error; +        return ['quit', $1, $2]; +    }, +    was_kicked => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) (.*)$/ or syntax_error; +        return ['was_kicked', $1, $2, $3]; +    }, +); + +sub parse_how_quit($) { +    my ($how_quit) = @_; +    $how_quit =~ /^([^ ]*)(| .*)$/ or syntax_error; +    my $func = $parse_how_quit{$1} or syntax_error; +    return $func->($2); +} + +our %parse_database = ( +    listen => sub { +        $_[0] =~ /^ (on|off|delay|private|disable) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_listen $2, $3, $1; +    }, +    join => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_join $1, $2, $3, $4; +    }, +    quit_all => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (.*)$/ or syntax_error; +        my ($time, $chatnet, $address, $nick, $how_quit) = ($1, $2, $3, $4, $5); +        do_quit_all $time, $chatnet, $address, $nick, parse_how_quit($how_quit); +    }, +    quit => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_quit $1, $2, $3, $4; +    }, +    part => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_part $1, $2, $3, $4; +    }, +    nick => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_nick $1, $2, $3, $4, $5; +    }, +    spoke => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_spoke $1, $2, $3; +    }, +    ask => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_ask $1, $2, $3, $4; +    }, +    forget_ask => sub { +        $_[0] =~ /^ ([^ ]*) ([^ ]*) ([^ ]*)$/ or syntax_error; +        do_forget_ask $1, $2, $3; +    }, +); + +sub read_database() { +    open DATABASE, $database or return; +    while (<DATABASE>) { +        chomp; +        /^([^ ]*)(| .*)$/ or syntax_error; +        my $func = $parse_database{$1} or syntax_error; +        $func->($2); +    } +    close DATABASE; +} + +######## Writing the database to file ######## + +sub write_database { +    open DATABASE, ">$database_tmp"; +    foreach my $chatnet (keys %listen_on) { +        foreach my $channel (keys %{$listen_on{$chatnet}}) { +            my $state = $listen_on{$chatnet}{$channel}; +            print DATABASE "listen $state $chatnet $channel\n"; +        } +    } +    foreach my $chatnet (keys %nick_absent) { +        foreach my $nick (keys %{$nick_absent{$chatnet}}) { +            my $time = $nick_absent{$chatnet}{$nick}; +            my $address = $addresses{$chatnet}{$nick}; +            my $orig = $orig_nick{$chatnet}{$nick}; +            print DATABASE "quit $time $chatnet $address $orig\n"; +        } +    } +    foreach my $chatnet (keys %address_absent) { +        foreach my $address (keys %{$address_absent{$chatnet}}) { +            my $time = $address_absent{$chatnet}{$address}; +            my $nick = $last_nicks{$chatnet}{$address}; +            my $reason = $how_quit{$chatnet}{$address}; +            print DATABASE "quit_all $time $chatnet $address $nick @$reason\n"; +        } +    } +    foreach my $chatnet (keys %spoke) { +        foreach my $address (keys %{$spoke{$chatnet}}) { +            my $time = $spoke{$chatnet}{$address}; +            print DATABASE "spoke $time $chatnet $address\n"; +        } +    } +    foreach my $chatnet (keys %nicks) { +        foreach my $address (keys %{$nicks{$chatnet}}) { +            foreach my $nick (@{$nicks{$chatnet}{$address}}) { +                foreach my $channel (@{$channels{$chatnet}{lc_irc $nick}}) { +                    print DATABASE "join $chatnet $address $nick $channel\n"; +                } +            } +        } +    } +    foreach my $chatnet (keys %asked) { +        foreach my $nick (keys %{$asked{$chatnet}}) { +            foreach my $nick_asked (keys %{$asked{$chatnet}{$nick}}) { +                my $time = $asked{$chatnet}{$nick}{$nick_asked}; +                print DATABASE "ask $time $chatnet $nick $nick_asked\n"; +            } +        } +    } +    close DATABASE; +    rename $database, $database_old; +    rename $database_tmp, $database; +} + +######## Update the database to reflect currently joined users ######## + +sub initialize_database() { +    my $time = time(); +    foreach my $chatnet (keys %nicks) { +        my @addresses = keys %{$nicks{$chatnet}}; +        foreach my $address (@addresses) { +            my @nicks = @{$nicks{$chatnet}{$address}}; +            foreach my $nick (@nicks) { +                do_quit $time, $chatnet, $address, $nick; +            } +            do_quit_all $time, $chatnet, $address, $nicks[0], ['disappeared']; +        } +    } +    foreach my $server (Irssi::servers()) { +        foreach my $channel ($server->channels()) { +            foreach my $nick ($channel->nicks()) { +                do_join lc $server->{chatnet}, +                  canonical $nick->{host}, $nick->{nick}, $channel->{name} +                  if $nick->{host} ne ""; +            } +        } +    } +} + +######## Expire old entries ######## + +sub expire_database() { +    my $days = Irssi::settings_get_int("seen_expire_after"); +    my $time = time() - $days*24*60*60; +    my %reachable_addresses = (); +    foreach my $chatnet (keys %addresses) { +        foreach my $address (values %{$addresses{$chatnet}}) { +            $reachable_addresses{$chatnet}{$address} = 1; +        } +    } +    foreach my $chatnet (keys %address_absent) { +        foreach my $address (keys %{$address_absent{$chatnet}}) { +            if ($address_absent{$chatnet}{$address} <= $time || +                !$reachable_addresses{$chatnet}{$address}) { +                delete $address_absent{$chatnet}{$address}; +                delete $last_nicks{$chatnet}{$address}; +                delete $how_quit{$chatnet}{$address}; +            } +        } +    } +    foreach my $chatnet (keys %spoke) { +        foreach my $address (keys %{$spoke{$chatnet}}) { +            if ($spoke{$chatnet}{$address} <= $time || +                !$reachable_addresses{$chatnet}{$address}) { +                delete $spoke{$chatnet}{$address}; +            } +        } +    } +    foreach my $chatnet (keys %nick_absent) { +        foreach my $nick (keys %{$nick_absent{$chatnet}}) { +            if ($nick_absent{$chatnet}{$nick} <= $time) { +                delete $nick_absent{$chatnet}{$nick}; +                delete $addresses{$chatnet}{$nick}; +                delete $orig_nick{$chatnet}{$nick}; +            } +        } +    } +    my $days_asked = Irssi::settings_get_int("seen_expire_asked_after"); +    my $time_asked = time() - $days_asked*24*60*60; +    foreach my $chatnet (keys %asked) { +        foreach my $nick (keys %{$asked{$chatnet}}) { +            foreach my $nick_asks (keys %{$asked{$chatnet}{$nick}}) { +                if ($asked{$chatnet}{$nick}{$nick_asks} <= $time_asked) { +                    delete $asked{$chatnet}{$nick}{$nick_asks}; +                } +            } +        } +    } +} + +######## Compose a description when did we see that person ######## + +sub show_reason($) { +    my ($reason) = @_; +    return ":" if $reason eq ""; +    $reason =~ s/\cc\d\d?(,\d\d?)?|[\000-\037]//g; +    return ": $reason"; +} + +sub only_public(@$) { +    my $can_show = pop @_; +    my @channels = (); +    foreach my $channel (@_) { +        if ($channel =~ /^-(.*)$/) { +            push @channels, $1 if $can_show->($1); +        } else { +            push @channels, $channel; +        } +    } +    return wantarray ? @channels : $channels[0]; +} + +sub is_here(\@$) { +    my ($channels, $where_asks) = @_; +    return if !defined $where_asks; +    my $lc_where_asks = lc_irc $where_asks; +    foreach my $i (0..$#{$channels}) { +        if (lc_irc $channels->[$i] eq $lc_where_asks) { +            splice @{$channels}, $i, 1; +            return 1; +        } +    } +    return 0; +} + +sub on_channels(@) { +    return @_ == 1 ? "on the channel $_[0]" : "on the channels " . show_list(@_); +} + +our %show_how_quit = ( +    disappeared => sub { +        return "they disappeared.  No more information is available."; +    }, +    was_left => sub { +        my ($true_channel, $where_asks, $can_show) = @_; +        my $channel = only_public $true_channel, $can_show; +        return +          defined $channel ? +            lc_irc $channel eq lc_irc $where_asks ? +              "byla here i wtedy stad wyszedlem." : +              "byla na kanale $channel, z ktorego wtedy wyszedlem." : +            "byla na kanale, z ktorego wtedy wyszedlem."; +    }, +    left => sub { +        my ($true_channel, $reason, $where_asks, $can_show) = @_; +        my $channel = only_public $true_channel, $can_show; +        return +          (defined $channel ? +            lc_irc $channel eq lc_irc $where_asks ? +              "person left" : "they left the channel $channel" : +            "left because") . +          show_reason($reason); +    }, +    quit => sub { +        my ($true_channels, $reason, $where_asks, $can_show) = @_; +        my @channels = only_public split(/,/, $true_channels), $can_show; +        my $is_here = is_here @channels, $where_asks; +        return +          (@channels == 0 ? +            $is_here ? "they left " : "" : +            ($is_here ? "byla tutaj oraz " : "they were seen quitting ") . +            on_channels(@channels) . +            " ") . +          "with the message" . show_reason($reason); +    }, +    was_kicked => sub { +        my ($true_channel, $kicker, $reason, $where_asks, $can_show) = @_; +        my $channel = only_public $true_channel, $can_show; +        return +          "they " . +          (defined $channel ? +            lc_irc $channel eq lc_irc $where_asks ? +              "were kicked" : "were kicked from $channel" : +            "kicked") . +          " by $kicker" . show_reason($reason); +    }, +); + +sub show_how_quit($$$) { +    my ($how_quit, $where_asks, $can_show) = @_; +    return $show_how_quit{$how_quit->[0]} +      (@{$how_quit}[1..$#{$how_quit}], $where_asks, $can_show); +} + +sub show_where_is($$$$$$$) { +    my ($server, $nick, $address, $where_asks, $can_show, $asked_and, $spoke_and) = @_; +    my $chatnet = lc $server->{chatnet}; +    my $lc_nick = lc_irc $nick; +    my @nicks = @{$nicks{$chatnet}{$address}}; +    @nicks = sort @nicks; +    my @channels = all_channels($chatnet, @nicks); +    @channels = +      only_public +      map ({mark_private($server->channel_find($_), $_)} sort @channels), +      $can_show; +    my $is_here = is_here @channels, $where_asks; +    my $this_nick_absent = $nick_absent{$chatnet}{$lc_nick}; +    return +      (defined $this_nick_absent ? +        "Osoba, ktora uzywala nicka $nick " . +        show_time_since($this_nick_absent) . +        ", $asked_and${spoke_and}teraz jest jako " . +        show_list(@nicks) . +        " " : +        "Queried user $asked_and${spoke_and}$nick is currently " . +        (@nicks == 1 ? "" : "(rowniez jako " . +          show_list(grep {lc_irc $_ ne $lc_nick} @nicks) . ") ")) . +      (@channels == 0 ? +        $is_here ? "in this channel" : "on IRC" : +        ($is_here ? "here on " : "") . on_channels(@channels)) . +      "."; +} + +sub seen($$$$$$) { +    my ($server, $nick, $who_asks, $where_asks, $can_show, $asked) = @_; +    my $chatnet = lc $server->{chatnet}; +    my $lc_nick = lc_irc $nick; +    my $address = $addresses{$chatnet}{$lc_nick}; +    unless (defined $address) { +        if (defined $asked) {return "You asked- $asked about $nick.", 0, 0} +        return "Sorry, I don't know of $nick.", 0, 0; +    } +    $nick = $orig_nick{$chatnet}{$lc_nick}; +    if ($address eq canonical $server->{userhost}) { +        return "I am $nick!", 1, 0; +    } +    if (defined $who_asks && $address eq $who_asks) { +        return "You are $nick!", 1, 0; +    } +    my $asked_and = defined $asked ? "$asked; " : ""; +    my $spoke = $spoke{$chatnet}{$address}; +    my $spoke_and = defined $spoke ? +      "last spoke " . show_time_since($spoke) . ".  " : ""; +    if (defined $address_absent{$chatnet}{$address}) { +        my $last_nick = $last_nicks{$chatnet}{$address}; +        my $when_address = show_time_since $address_absent{$chatnet}{$address}; +        if (lc_irc $last_nick eq $lc_nick) { +            return "The person with the nick $nick $asked_and$spoke_and$when_address " . +              show_how_quit($how_quit{$chatnet}{$address}, +                            $where_asks, $can_show), 1, 1; +        } else { +            my $when_nick = show_time_since $nick_absent{$chatnet}{$lc_nick}; +            return "Person, who $when_nick used nick $nick, " . +              "$asked_and$spoke_and$when_address jako $last_nick " . +              show_how_quit($how_quit{$chatnet}{$address}, +                            $where_asks, $can_show), 1, 1; +        } +    } else { +        return show_where_is($server, $nick, $address, +                             $where_asks, $can_show, +                             $asked_and, $spoke_and), 1, 0; +    } +} + +######## Initialization ######## + +read_database; +expire_database; +initialize_database; +write_database; + +Irssi::timeout_add 60*60*1000, sub {expire_database; write_database}, undef; + +######## Irssi signal handlers ######## + +sub can_show_this_channel($) { +    my ($channel) = @_; +    my $lc_channel = lc_irc $channel; +    return sub {lc_irc $_[0] eq $lc_channel}; +} + +sub can_show_his_channels($$) { +    my ($chatnet, $nick) = @_; +    my $lc_nick = lc_irc $nick; +    my @channels = $channels{$chatnet}{$lc_nick} ? +      @{$channels{$chatnet}{$lc_nick}} : (); +    return sub { +        my $channel = lc_irc $_[0]; +        return grep {lc_irc $_ eq $channel} @channels; +    }; +} + +sub check_asked($$$) { +    my ($chatnet, $server, $nick) = @_; +    my $lc_nick = lc_irc $nick; +    my $who_asked = $asked{$chatnet}{$lc_nick}; +    return unless $who_asked; +    foreach my $nick_asked (sort {$who_asked->{$a} <=> $who_asked->{$b}} +                            keys %{$who_asked}) { +        my $when_asked = show_time_since $who_asked->{$nick_asked}; +        my ($reply, $found, $remember_asked) = +          seen $server, $nick_asked, undef, undef, +          can_show_his_channels($chatnet, $nick), +          "szukala Cie $when_asked"; +        $server->command("notice $nick $reply"); +        do_forget_ask $chatnet, $nick, $nick_asked; +        append_to_database "forget_ask $chatnet $nick $nick_asked"; +    } +} + +Irssi::signal_add "channel wholist", sub { +    my ($channel) = @_; +    my $server = $channel->{server}; +    my $chatnet = lc $server->{chatnet}; +    foreach my $nick ($channel->nicks()) { +        my $lc_nick = lc_irc $nick->{nick}; +        my $lc_channel = lc_irc $channel->{name}; +        on_join $chatnet, canonical $nick->{host}, $nick->{nick}, $channel->{name} +          unless $nick->{host} eq "" || +          $channels{$chatnet}{$lc_nick} && +          grep {lc_irc $_ eq $lc_channel} @{$channels{$chatnet}{$lc_nick}}; +        check_asked $chatnet, $server, $nick->{nick}; +    } +}; + +Irssi::signal_add_first "channel destroyed", sub { +    my ($channel) = @_; +    my $chatnet = lc $channel->{server}{chatnet}; +    foreach my $nick ($channel->nicks()) { +        on_part $chatnet, canonical $nick->{host}, $nick->{nick}, $channel->{name}, +          ['was_left', mark_private($channel, $channel->{name})] +          unless $nick->{host} eq ""; +    } +}; + +Irssi::signal_add "event join", sub { +    my ($server, $args, $nick, $address) = @_; +    $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return; +    my $channel = $1; +    my $chatnet = lc $server->{chatnet}; +    on_join $chatnet, canonical $address, $nick, $channel; +    check_asked $chatnet, $server, $nick; +}; + +Irssi::signal_add "event part", sub { +    my ($server, $args, $nick, $address) = @_; +    $args =~ /^([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+)$/ or $args =~ /^([^ ]+)()$/ or return; +    my ($channel, $reason) = ($1, $2); +    my $chatnet = lc $server->{chatnet}; +    return if defined $nick_absent{$chatnet}{lc_irc $nick}; +    $reason = "" if $reason eq $nick; +    on_part $chatnet, canonical $address, $nick, $channel, +      ['left', mark_private($server->channel_find($channel), $channel), $reason]; +}; + +Irssi::signal_add "event quit", sub { +    my ($server, $args, $nick, $address) = @_; +    $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or $args =~ /^()$/ or return; +    my $reason = $1; +    my $chatnet = lc $server->{chatnet}; +    my $lc_nick = lc_irc $nick; +    return if defined $nick_absent{$chatnet}{$lc_nick}; +    $reason = "" if $reason =~ /^(Quit: )?(leaving)?$/; +    my @channels = $channels{$chatnet}{$lc_nick} ? +      @{$channels{$chatnet}{$lc_nick}} : (); +    on_quit $chatnet, canonical $address, $nick, +      ['quit', join(",", map {mark_private($server->channel_find($_), $_)} sort @channels), $reason]; +}; + +Irssi::signal_add "event kick", sub { +    my ($server, $args, $kicker, $kicker_address) = @_; +    $args =~ /^([^ ]+) +([^ ]+) +:(.*)$/ or $args =~ /^([^ ]+) +([^ ]+) +([^ ]+)$/ or +      $args =~ /^([^ ]+) +([^ ]+)()$/ or return; +    my ($channel, $nick, $reason) = ($1, $2, $3); +    my $chatnet = lc $server->{chatnet}; +    $reason = "" if $reason eq $kicker; +    on_part $chatnet, $addresses{$chatnet}{lc_irc $nick}, $nick, $channel, +      ['was_kicked', mark_private($server->channel_find($channel), $channel), $kicker, $reason]; +}; + +Irssi::signal_add "event nick", sub { +    my ($server, $args, $old_nick, $address) = @_; +    $args =~ /^:(.*)$/ or $args =~ /^([^ ]+)$/ or return; +    my $new_nick = $1; +    return if $address eq ""; +    my $chatnet = lc $server->{chatnet}; +    on_nick $chatnet, canonical $address, $old_nick, $new_nick; +    check_asked $chatnet, $server, $new_nick; +}; + +######## Commands ######## + +Irssi::command_bind "seen", sub { +    my ($args, $server, $target) = @_; +    my $nick; +    if ($args =~ /^ *([^ ]+) *$/) { +        $nick = $1; +    } else { +        Irssi::print "Usage: /seen <nick>"; +        return; +    } +    unless ($server && $server->{connected}) { +        Irssi::print "Not connected to server"; +        return; +    } +    my ($reply, $found, $remember_asked) = +      seen $server, $nick, undef, undef, sub {1}, undef; +    Irssi::print $reply; +}; + +Irssi::command_bind "say_seen", sub { +    my ($args, $server, $target) = @_; +    my $chatnet = lc $server->{chatnet}; +    my ($nick_asks, $prefix, $nick); +    if ($args =~ /^ *([^ ]+) *$/) { +        $nick_asks = undef; +        $prefix = ""; +        $nick = $1; +    } elsif ($args =~ /^ *([^ ]+) +([^ ]+) *$/) { +        $nick_asks = $1; +        $prefix = "$1: "; +        $nick = $2; +    } else { +        Irssi::print "Usage: /say_seen [<to_whom>] <nick>"; +        return; +    } +    unless ($server && $server->{connected}) { +        Irssi::print "Not connected to server"; +        return; +    } +    unless ($target) { +        Irssi::print "Not in a channel or query"; +        return; +    } +    my $can_show = +      $target->{type} eq 'CHANNEL' ? +        can_show_this_channel($target->{name}) : +      $target->{type} eq 'QUERY' ? +        can_show_his_channels($chatnet, $target->{name}) : +      sub {0}; +    my ($reply, $found, $remember_asked) = +      seen $server, $nick, undef, $target->{name}, $can_show, undef; +    on_ask $chatnet, $nick, $nick_asks +      if defined $nick_asks && $remember_asked; +    $server->command("msg $target->{name} $prefix$reply"); +}; + +sub cmd_listen_switch($$$$) { +    my ($state, $args, $server, $target) = @_; +    if ($args =~ /^ *$/) { +        unless ($server && $server->{connected}) { +            Irssi::print "Not connected to server"; +            return; +        } +        unless ($target && $target->{type} eq 'CHANNEL') { +            Irssi::print "Not in a channel"; +            return; +        } +        on_listen lc $server->{chatnet}, lc_irc $target->{name}, $state; +    } elsif ($args =~ /^ *([^ ]+) *$/) +    { +        unless ($server && $server->{connected}) { +            Irssi::print "Not connected to server"; +            return; +        } +        on_listen lc $server->{chatnet}, lc_irc $1, $state; +    } elsif ($args =~ /^ *([^ ]+) +([^ ]+) *$/) +    { +        on_listen lc $1, lc_irc $2, $state; +    } else { +        Irssi::print "Usage: /listen $state [[<chatnet>] <channel>]"; +    } +} + +Irssi::command_bind "listen", sub { +    my ($args, $server, $target) = @_; +    Irssi::command_runsub "listen", $args, $server, $target; +}; + +Irssi::command_bind "listen on", sub { +    my ($args, $server, $target) = @_; +    cmd_listen_switch "on", $args, $server, $target; +}; + +Irssi::command_bind "listen off", sub { +    my ($args, $server, $target) = @_; +    cmd_listen_switch "off", $args, $server, $target; +}; + +Irssi::command_bind "listen delay", sub { +    my ($args, $server, $target) = @_; +    cmd_listen_switch "delay", $args, $server, $target; +}; + +Irssi::command_bind "listen private", sub { +    my ($args, $server, $target) = @_; +    cmd_listen_switch "private", $args, $server, $target; +}; + +Irssi::command_bind "listen disable", sub { +    my ($args, $server, $target) = @_; +    cmd_listen_switch "disable", $args, $server, $target; +}; + +our @joined_text = ("      ", "joined"); + +Irssi::command_bind "listen list", sub { +    my ($args, $server, $target) = @_; +    if ($args =~ /^ *$/) { +        my %all_channels = (); +        foreach my $server (Irssi::servers()) { +            my $chatnet = lc $server->{chatnet}; +            foreach my $channel ($server->channels()) { +                $all_channels{$chatnet}{lc_irc $channel->{name}}[0] = 1; +            } +        } +        foreach my $chatnet (keys %listen_on) { +            foreach my $channel (keys %{$listen_on{$chatnet}}) { +                $all_channels{$chatnet}{$channel}[1] = $listen_on{$chatnet}{$channel}; +            } +        } +        my $max_chatnet_width = 1; +        my $max_channel_width = 1; +        foreach my $chatnet (keys %all_channels) { +            $max_chatnet_width = length $chatnet +              if length $chatnet > $max_chatnet_width; +            foreach my $channel (keys %{$all_channels{$chatnet}}) { +                $max_channel_width = length $channel +                  if length $channel > $max_channel_width; +            } +        } +        Irssi::print "'seen' is listening:"; +        foreach my $chatnet (sort keys %all_channels) { +            foreach my $channel (sort keys %{$all_channels{$chatnet}}) { +                Irssi::print +                  $chatnet . +                  " " x ($max_chatnet_width - length ($chatnet) + 1) . +                  $channel . +                  " " x ($max_channel_width - length ($channel) + 3) . +                  $joined_text[$all_channels{$chatnet}{$channel}[0]] . +                  "   " . +                  $all_channels{$chatnet}{$channel}[1]; +            } +        } +    } else { +        Irssi::print "Usage: /listen list"; +    } +}; + +Irssi::command_bind "forget", sub { +    my ($args, $server, $target) = @_; +    my $nick; +    if ($args =~ /^ *([^ ]+) *$/) { +        $nick = $1; +    } else { +        Irssi::print "Usage: /forget <nick>"; +        return; +    } +    unless ($server) { +        Irssi::print "Not connected to server"; +        return; +    } +    my $chatnet = lc $server->{chatnet}; +    return unless $asked{$chatnet}{$nick}; +    foreach my $nick_asked (keys %{$asked{$chatnet}{$nick}}) { +        do_forget_ask $chatnet, $nick, $nick_asked; +        append_to_database "forget_ask $chatnet $nick $nick_asked"; +    } +}; + +######## Listen to seen requests from other people ######## + +our $last_reply = undef; +our $last_asked = undef; + +our %pending_replies = (); + +sub seen_reply($$$$$$) { +    my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; +    my $chatnet = lc $server->{chatnet}; +    my ($reply, $found, $remember_asked) = +      seen $server, $nick, $address, $target, +        can_show_this_channel($target), undef; +    return unless $sure || $found; +    unless ($reply eq $last_reply && $nick eq $last_asked) { +        Irssi::print "[$target] $nick_asks: $reply"; +        $server->command("msg $target $nick_asks: $reply"); +        $last_reply = $reply; +        $last_asked = $nick; +    } +    on_ask $chatnet, $nick, $nick_asks if $remember_asked; +} + +sub private_seen_reply($$$$$$) { +    my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; +    my $chatnet = lc $server->{chatnet}; +    my ($reply, $found, $remember_asked) = +      seen $server, $nick, $address, undef, +        can_show_his_channels($chatnet, $nick_asks), undef; +    return unless $sure || $found; +    $server->command("notice $nick_asks $reply"); +    $server->command("notice $nick_asks " . +      "Pytac o obecnosc ludzi mozesz mnie tez prywatnie, np. /msg $server->{nick} seen $nick"); +    on_ask $chatnet, $nick, $nick_asks if $remember_asked; +} + +sub delayed_seen_reply($$$$$$) { +    my ($server, $nick_asks, $address, $target, $nick, $sure) = @_; +    my $chatnet = lc $server->{chatnet}; +    my $lc_nick = lc_irc $nick; +    return if defined $pending_replies{$chatnet}{$target}{$lc_nick}; +    my $timeout = Irssi::settings_get_int("seen_delay") * 1000; +    $pending_replies{$chatnet}{$target}{$lc_nick} = Irssi::timeout_add_once $timeout, sub { +        delete $pending_replies{$chatnet}{$target}{$lc_nick}; +        seen_reply $server, $nick_asks, $address, $target, $nick, $sure; +    }, undef; +} + +our %reply_method = ( +    on => \&seen_reply, +    off => undef, +    delay => \&delayed_seen_reply, +    private => \&private_seen_reply, +    disable => undef, +); + +sub check_another_seen($$$$) { +    my ($chatnet, $channel, $msg, $nick_asks) = @_; +    my $lc_channel = lc_irc $channel; +    if ($listen_on{$chatnet}{$lc_channel} eq 'delay') { +        foreach my $nick (keys %{$pending_replies{$chatnet}{$channel}}) { +            my $nick_regexp = lc_irc_regexp $nick; +            if ($msg =~ /(^|[ \cb])$nick_regexp($|[ !,.:;?\cb])/ || +                lc_irc $nick_asks eq $nick) { +                my $tag = $pending_replies{$chatnet}{$channel}{$nick}; +                Irssi::timeout_remove $tag; +                delete $pending_replies{$chatnet}{$channel}{$nick}; +            } +        } +    } +} + +Irssi::signal_add "message public", sub { +    my ($server, $msg, $nick_asks, $address, $channel) = @_; +    my $chatnet = lc $server->{chatnet}; +    $address = canonical $address; +    on_spoke $chatnet, $address; +    my $lc_channel = lc_irc $channel; +    my ($msg_body, $func) = +      $msg =~ /^\Q$server->{nick}\E(?:|:|\cb:\cb) +(.*)$/i ? ($1, \&seen_reply) : +      ($msg, $reply_method{$listen_on{$chatnet}{$lc_channel} || 'off'}); +    if (defined $func) { +        my $sure = +          $msg_body =~ $seen_regexp ? 1 : +          $msg_body =~ $maybe_seen_regexp1 || +          $msg_body =~ $maybe_seen_regexp2 ? 0 : +          undef; +        if (defined $sure) { +            my $nick = $1; +            return if $sure == 0 && $nick =~ $exclude_regexp; +            Irssi::signal_continue @_; +            $func->($server, $nick_asks, $address, $channel, $nick, $sure); +            return; +        } +    } +    check_another_seen $chatnet, $channel, $msg, $nick_asks; +}; + +Irssi::signal_add "message irc notice", sub { +    my ($server, $msg, $nick_asks, $address, $target) = @_; +    my $chatnet = lc $server->{chatnet}; +    check_another_seen $chatnet, $target, $msg, $nick_asks; +}; + +Irssi::signal_add "message private", sub { +    my ($server, $msg, $nick_asks, $address) = @_; +    my $chatnet = lc $server->{chatnet}; +    on_spoke $chatnet, canonical $address; +    check_asked $chatnet, $server, $nick_asks; +    my $sure = +      $msg =~ $seen_regexp ? 1 : +      $msg =~ $maybe_seen_regexp1 || +      $msg =~ $maybe_seen_regexp2 ? 0 : +      undef; +    if (defined $sure) { +        my $nick = $1; +        my ($reply, $found, $remember_asked) = +          seen $server, $nick, canonical $address, undef, +          can_show_his_channels($chatnet, $nick_asks), undef; +        return unless $sure || $found; +        Irssi::signal_continue @_; +        $server->command("msg $nick_asks $reply"); +        on_ask $chatnet, $nick, $nick_asks if $remember_asked; +    } +}; + +Irssi::signal_add "message irc action", sub { +    my ($server, $msg, $nick, $address, $target) = @_; +    on_spoke lc $server->{chatnet}, canonical $address; +}; | 
