diff --git a/scripts/zmartfilter.pl b/scripts/zmartfilter.pl new file mode 100644 index 000000000..47e6531d2 --- /dev/null +++ b/scripts/zmartfilter.pl @@ -0,0 +1,239 @@ +use strict; +use Irssi; +use vars qw($VERSION %IRSSI); + +$VERSION = "1.00"; +%IRSSI = ( + name => "zmartfilter", + description => "smartfilter.pl reimagined, optimized for unusually flakey networks such as IRC over I2P", + license => "Public Domain", +); + +# The maximum number of filtered messages to display from a user within the +# allow_time before squelching early; set to <=0 for infinite. +Irssi::settings_add_int($IRSSI{name}, "$IRSSI{name}_allow_max", 12); + +# The length of time to continue displaying filtered messages from users after +# they last exhibited activity (until allow_max is reached). +Irssi::settings_add_time($IRSSI{name}, "$IRSSI{name}_allow_time", "8hour"); + +# The length of time after an inactive user joins to squelch automated mode +# change by services messages. +Irssi::settings_add_time($IRSSI{name}, "$IRSSI{name}_join_time", "1min"); + +# A whitespace-delimited list of channels that are not subject to filtering. +Irssi::settings_add_str($IRSSI{name}, "$IRSSI{name}_whitelist_channels", ""); + + +Irssi::theme_register([ + squelched => "{line_start} {channick_hilight \$0} {chanhost_hilight \$1} squelched", +]); + + +my ($allow_max, $allow_time, $join_time, @whitelist_channels, $completion_re); + +sub setup_changed { + $allow_max = Irssi::settings_get_int("$IRSSI{name}_allow_max"); + $allow_time = Irssi::settings_get_time("$IRSSI{name}_allow_time") / 1000; + $join_time = Irssi::settings_get_time("$IRSSI{name}_join_time") / 1000; + @whitelist_channels = split(/\s+/, Irssi::settings_get_str("$IRSSI{name}_whitelist_channels")); + $completion_re = qr/^\s*(\S+)\Q@{[Irssi::settings_get_str("completion_char")]}\E\s/; +} + +Irssi::signal_add("setup changed", \&setup_changed); +setup_changed(); + + +# %tactive are last activity timestamps, and %nallowed are the number of +# filterable messages allowed through since the last activity. Both are keyed +# by ($server->{tag}, $channel, $address). +my (%tactive, %nallowed) = ( collect => time() ); + +# %tjoined are join timestamps keyed by ($server->{tag}, $channel, $nick), used +# to squelch automated mode change by services messages shortly after inactive +# users join a channel. +my %tjoined = ( collect => time() ); + +# indicate activity from the given address (or nick if the address is unknown) +# in the given channel on the given server +sub activity { + my ($server, $target, $address, $nick) = @_; + + if (!$server->ischannel($target) || ($nick && $server->{nick} eq $nick)) { + # don't bother tracking query windows or myself + return; + } + + if (!$address) { + if (!$nick) { + return; + } + my $user = $server->channel_find($target)->nick_find($nick); + if (!$user) { + return; + } + $address = $user->{host}; + if (!$address) { + return; + } + } + + if ($server->mask_match_address('*!*@services.*', "", $address)) { + # don't bother tracking network services + return; + } + + my ($key, $now) = ( join($;, $server->{tag}, $target, $address), time() ); + my $threshold = $now - $allow_time; + if (!exists $tactive{$key} && $tactive{collect} <= $threshold) { + # remove stale entries from the table before inserting new + # ones, but at most once every $allow_time + for my $key (grep { $tactive{$_} <= $threshold } keys %tactive) { + delete $tactive{$key}; + delete $nallowed{$key}; + } + $tactive{collect} = $now; + } + + $tactive{$key} = $now; + $nallowed{$key} = 0; +} + +sub message_server_x_x_address_target { activity(@_[0, 4, 3]); } + +sub message_own_public { + my ($server, $msg, $target) = @_; + if ($msg =~ $completion_re) { + # I posted a message directed at someone else -- count the + # other user as active so that I get to see if he changes nicks + # or leaves + activity($server, $target, undef, $1); + } +} + +sub message_join { + my ($server, $channel, $nick, $address) = @_; + + my ($key, $now) = ( join($;, $server->{tag}, $channel, $nick), time() ); + my $threshold = $now - $join_time; + if (!exists $tjoined{$key} && $tjoined{collect} <= $threshold) { + # remove stale entries from the table before inserting new + # ones, but at most once every $join_time + for my $key (grep { $tjoined{$_} <= $threshold } keys %tjoined) { + delete $tjoined{$key}; + } + $tjoined{collect} = $now; + } + + $tjoined{$key} = $now; +} + +sub message_kick { + my ($server, $channel, $nick, $kicker, $address) = @_; + activity($server, $channel, $address); + # also count the kickee as active so that we get to see if he rejoins + activity($server, $channel, undef, $nick); +} + +sub message_server_channel_nick_address { activity(@_[0, 1, 3, 2]); } + +Irssi::signal_add({ + "message public" => \&message_server_x_x_address_target, + "message own_public" => \&message_own_public, + "message join" => \&message_join, + "message kick" => \&message_kick, + "message invite" => \&message_server_channel_nick_address, + "message topic" => sub { activity(@_[0, 1, 4]); }, + "message irc op_public" => \&message_server_x_x_address_target, + "message irc action" => \&message_server_x_x_address_target, + "message irc own_notice" => \&message_own_public, + "message irc notice" => \&message_server_x_x_address_target, + "message irc ctcp" => sub { activity(@_[0, 5, 4]); }, + "message irc mode" => \&message_server_channel_nick_address, +}); + + +# filter at the "print text" signal so that other scripts may still receive the +# message signals if they need them +sub print_text { + my ($dest, $text, $stripped) = @_; + + # only filter text bound for channel windows + my ($now, $server, $target) = ( time(), $dest->{server}, $dest->{target} ); + if (!$server || !$server->{connected} || !$server->ischannel($target)) { + return; + } + + # don't filter text bound for channels in the whitelist + if (grep { $_ eq $target } @whitelist_channels) { + return; + } + + my ($level, @nicks) = $dest->{level}; + if ($level & (MSGLEVEL_JOINS | MSGLEVEL_PARTS | MSGLEVEL_QUITS)) { + if ($stripped =~ /([^ ]+) \[.+\] has (joined|left|quit)/) { + @nicks = ($1); + } + } elsif ($level & MSGLEVEL_NICKS) { + if ($stripped =~ /[^ ]+ is now known as ([^ ]+)/) { + @nicks = ($1); + } + } elsif ($level & MSGLEVEL_MODES) { + if ($stripped =~ /mode\/[^ ]+ \[\+[^ ]+ (.+)\] by ChanServ/) { + @nicks = split(/ +/, $1); + } + } + if (!@nicks) { + # doesn't match a filtered message + return; + } + + # use all filtered message levels for the squelched message level + my $slevel = MSGLEVEL_JOINS | MSGLEVEL_PARTS | MSGLEVEL_QUITS | + MSGLEVEL_NICKS | MSGLEVEL_MODES; + my ($athreshold, $jthreshold) = ( $now - $allow_time, $now - $join_time ); + for my $nick (@nicks) { + if ($nick eq $server->{nick}) { + # it's text that relates to me + return; + } + my $address = $server->channel_find($target); + $address = $address->nick_find($nick); + $address = $address->{host}; + if (!$address) { + next; + } + if ($server->mask_match_address('*!*@services.*', "", $address)) { + # relates to network services + return; + } + my $key = join($;, $server->{tag}, $target, $address); + my $tstamp = $tactive{$key}; + if ($tstamp && $athreshold < $tstamp) { + if (0 < $allow_max && ++$nallowed{$key} <= $allow_max) { + # relates to an active user who hasn't been + # squelched by $allow_max yet + return; + } + $server->printformat($target, $slevel, "squelched", + $nick, $address); + delete $tactive{$key}; + delete $nallowed{$key}; + } + if ($level & MSGLEVEL_MODES) { + # also check whether any nicks affected a mode change + # were NOT recently joined, in which case this was a + # spontaneous mode change (perhaps a new operator + # elevation) and I want to see it + $tstamp = $tjoined{$server->{tag}, $target, $nick}; + if (!$tstamp || $tstamp <= $jthreshold) { + return; + } + } + } + + # if I made it this far it means this text is to be suppressed + Irssi::signal_stop(); +} + +Irssi::signal_add_first("print text", \&print_text);