diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:19:02 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:19:02 +0000 |
commit | 03929dac2a29664878d2c971648a4fe1fb698462 (patch) | |
tree | 02c5e2b3e006234aa29545f7a93a1ce01b291a8b /scripts/seen.pl | |
parent | Initial commit. (diff) | |
download | irssi-scripts-03929dac2a29664878d2c971648a4fe1fb698462.tar.xz irssi-scripts-03929dac2a29664878d2c971648a4fe1fb698462.zip |
Adding upstream version 20231031.upstream/20231031upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-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; +}; |