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 # Show last seen info about nick. # # /say_seen [] # 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 [[] ] # Turn on listening for seen requests in the current or given channel. # # /listen off [[] ] # Turn off listening for seen requests in the current or given channel. # # /listen delay [[] ] # 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 [[] ] # Turn on listening for seen requests in the current or given channel. # The reply will be sent as a private notice. # # /listen disable [[] ] # 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 ": seen ". # Public message "seen " on channels where we are listening. # Private message "seen ". # 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 ?", "by³ ?", "by³a ?", # " jest?", " by³?", " 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 () { 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 "; 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 [] "; 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 [[] ]"; } } 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 "; 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; };