3 # SoftSnow XChat2 filter script
7 # Filter out fileserver announcements and SPAM on IRC
11 # This script started as an upgrade to the SoftSnow filter script
12 # from http://dukelupus.pri.ee/softsnow/ircscripts/scripts.shtml
13 # or http://softsnow.griffin3.com/ircscripts/scripts.shtml
14 # (originally http://www.softsnow.biz/softsnow_filter/filter.shtml)
15 # It borrows some ideas from filter-ebooks (#ebooks Xchat2 filter
16 # script) by KiBo, and its older version by RJVJR, mainly moving
17 # from the old IRC:: interface to the new Xchat2 API.
19 # Tested on #ebooks channel on IRCHighWay (irc.irchighway.net)
21 # Use '/FILTER HELP' (or '/HELP FILTER') to list all commands.
22 # By default filter is turned off: use '/FILTER ON' to start
23 # filtering contents, use '/FILTERWINDOW ON' to log filtered
24 # lines to '(filtered)' window.
29 # Place SoftSnow_filter.pl in your ~/.xchat directory
31 ## URL (repositories):
32 # * http://github.com/jnareb/softsnow-xchat2-filter
33 # * http://gitorious.org/projects/softsnow-xchat2-filter
34 # * http://repo.or.cz/w/softsnow_xchat2_filter.git
36 ## ChangeLog (main points only):
39 # * Original version of SoftSnow filter this one is based on
41 # * Add /FILTER command, to turn filter off and on, and to limit
42 # filtering to current IRC server only
44 # * Allow to save and load filter rules from file (UNIX only)
45 # * Add ALLOW rules, for example to show '@search' while filtering '@'
47 # * Use new XChat2 API (Xchat:: instead of IRC::)
49 # * More secure saving rules to a file (always save whole file)
51 # * Allow printing (logging) filtered content to '(filtered)' window
52 # via 'Channel Message' text event, with nick of sender
54 # * /FILTERWINDOW command to control and query of logging filtered
55 # contents to separate '(filtered)' window
59 # * Add GUI and MENU (would require XChat >= 2.4.5)
60 # * Change format of saved rules to 'm/.../' or 'qr{...}';
61 # see YAML (YAML::Types) and Data::Dumper code and output
62 # * Save and read config together with filter rules
63 # * Autosave (and autoread) configuration, if turned on
64 # * Read default config and rules from __DATA__, add reset
65 # * Save filter rules usage statistics
66 # * Import filter rules from filter-ebooks3.3FINAL script
67 # * Limit filter to specified channels (or all channels)
68 # * Filter private SPAM (SPAM sent to you, but not other)
69 # * ? Don't accept DCC from users not on common channel
70 # * ? Do not accept files, or dangerous files, from regular users
71 # * Color nicks in '(filtered)' window according to matched rule
72 # * Add command to clear '(filtered)' window
73 # * Add option to strip codes from logged filtered lines
74 # * Limit number of lines in '(filtered)' window
75 # * ? Perhaps something about '@find' and '!find' results?
76 # * Triggers, for example automatic /dccallow + resubmit,
77 # if request fails (due to double '.' in filename, etc.)
82 use File
::Temp
qw(tempfile);
83 use File
::Copy
qw(move);
86 my $scriptName = "SoftSnow XChat2 Filter";
87 my $scriptVersion = "2.2.1";
88 my $scriptDescr = "Filter out file server announcements and IRC SPAM";
91 my $U = chr 31; # underline
92 my $C = chr 3; # start of color sequence
93 my $R = chr 22; # reverse
94 my $O = chr 15; # reset
97 my $filter_file = Xchat
::get_info
("xchatdir") . "/SoftSnow_filter.conf";
99 my $filter_turned_on = 0; # is filter is turned on
100 my $limit_to_server = ''; # if true limit to given server (host)
101 my $use_filter_allow = 0; # use overrides (ALLOW before DENY)
103 my $filtered_to_window = 0;
104 my $filter_window = "(filtered)";
107 my $filter_commands = 'ON|OFF|STATUS|SERVER|SERVERON|ALL|HELP|DEBUG|CLEARSTATS|SORT|PRINT|ALLOW|ADD|DELETE|SAVE|LOAD';
109 my $filter_help = <<"EOF";
110 ${B}/FILTER $filter_commands${B}
111 /FILTER ON|OFF - turns filtering on/off
112 /FILTER HELP - prints this help message
113 /FILTER STATUS - prints if filter is turned on, and with what limits
114 /FILTER DEBUG - shows some info; used in debuggin the filter
115 /FILTER CLEARSTATS - reset filter statistics
116 /FILTER SORT - sort deny rules to have more often matched rules first
117 /FILTER PRINT - prints all the rules
118 /FILTER ALLOW - toggle use of ALLOW rules (before DENY).
119 /FILTER SERVER - limits filtering to current server (host)
120 /FILTER SERVERON - limits to server and turns filter on
121 /FILTER ALL - resumes filtering everywhere i.e. removes limits
122 /FILTER SAVE - saves the rules to the file $filter_file
123 /FILTER LOAD - loads the rules from the file, replacing existing rules
124 /FILTER ADD <rule> - add rule at the end of the DENY rules
125 /FILTER DELETE [<num>] - delete rule number <num>, or last rule
126 /FILTER SHOW [<num>] - show rule number <num>, or last rule
127 /FILTER VERSION - prints the name and version of this script
128 /FILTER WINDOW <arg>... - same as /FILTERWINDOW <arg>...
129 /FILTER without parameter is equivalent to /FILTER STATUS
132 my $filterwindow_commands = 'ON|OFF|CLOSE|HELP|STATUS|DEBUG';
134 my $filterwindow_help = <<"EOF";
135 ${B}/FILTERWINDOW $filterwindow_commands${B}
136 /FILTERWINDOW ON|OFF - turns saving filtered content to ${U}$filter_window${U}
137 /FILTERWINDOW CLOSE - close ${U}$filter_window${U} (and turn off logging)
138 /FILTERWINDOW STATUS - prints if saving to ${U}$filter_window${U} is turned on
139 /FILTERWINDOW HELP - prints this help message
140 /FILTERWINDOW DEBUG - shows some info; used in debugging this part of filter
141 /FILTERWINDOW without parameter is equivalent to /FILTERWINDOW STATUS
144 Xchat
::register
($scriptName, $scriptVersion, $scriptDescr);
146 Xchat
::hook_command
("FILTER", \
&filter_command_handler
,
147 { help_text
=> $filter_help });
148 Xchat
::hook_command
("FILTERWINDOW", \
&filterwindow_command_handler
,
149 { help_text
=> $filterwindow_help });
150 Xchat
::hook_server
("PRIVMSG", \
&privmsg_handler
);
152 Xchat
::print("Loading ${B}$scriptName $scriptVersion${B}...\n");
156 if ($filtered_to_window) {
157 Xchat
::command
("QUERY $filter_window");
160 # information about (default) options used
161 if ($filter_turned_on) {
162 Xchat
::print("Filter turned ${B}ON${B}\n");
164 Xchat
::print("Filter turned ${B}OFF${B}\n");
166 if ($limit_to_server) {
167 Xchat
::print("Filter limited to server $limit_to_server\n")
169 if ($use_filter_allow) {
170 Xchat
::print("Filter uses ALLOW rules\n");
173 # ------------------------------------------------------------
189 q
/(?i)fserve.*trigger/,
191 q
/(?i)trigger.*\/ctcp
/,
193 q
/(?i)file server online/,
202 #messages for when a file is received/failed to receive
203 q
/(?i)DEFINITELY had the right stuff to get/,
204 q
/(?i)has just received/,
205 q
/(?i)I have just received/,
216 q
/brave soldier in the war/,
219 my $nlines = 0; # how many lines we passed through filter
220 my $nfiltered = 0; # how many lines were filtered
221 my $checklensum = 0; # how many rules to check to catch filtered
222 my $nallow = 0; # how many lines matched ALLOW rule
223 my %stats = (); # histogram: how many times given rule was used
225 # return 1 (true) if text given as argument is to be filtered out
230 #strip colour, underline, bold codes, etc.
231 $text = Xchat
::strip_code
($text);
233 # count all filtered lines;
236 if ($use_filter_allow) {
237 foreach $regexp (@filter_allow) {
238 if ($text =~ /$regexp/) {
245 my $nrules_checked = 0;
246 foreach $regexp (@filter_deny) {
249 if ($text =~ /$regexp/) {
252 $checklensum += $nrules_checked;
253 if (exists $stats{$regexp}) {
266 #called when someone says something in the channel
267 #1: address of speaker
270 #4: text said (prefixed with :)
271 sub privmsg_handler
{
272 # $_[0] - array reference containing the IRC message or command
273 # and arguments broken into words
274 # $_[1] - array reference containing the Nth word to the last word
275 my ($address, $msgtype, $channel) = @
{$_[0]};
276 my ($nick, $user, $host) = ($address =~ /^:([^!]*)!([^@]+)@(.*)$/);
278 my $text = $_[1][3]; # Get server message
280 my $server = Xchat
::get_info
("host");
282 #-- EXAMPLE RAW COMMANDS: --
283 #chanmsg: [':epitaph!~epitaph@CPE00a0241892b7-CM014480119187.cpe.net.cable.rogers.com', 'PRIVMSG', '#werd', ':mah', 'script', 'is', 'doing', 'stuff.']
284 #action: [':rlz!railz@bzq-199-176.red.bezeqint.net', 'PRIVMSG', '#werd', ':\x01ACTION', 'hugs', 'elhaym', '\x01']
285 #private: [':olene!oqd@girli.sh', 'PRIVMSG', 'epinoodle', ':hey']
288 return Xchat
::EAT_NONE
unless $filter_turned_on;
289 if ($limit_to_server) {
290 return Xchat
::EAT_NONE
unless $server eq $limit_to_server;
292 # do not filter out private messages
293 return Xchat
::EAT_NONE
unless ($channel =~ /^#/);
297 if (isFiltered
($text)) {
298 if (defined $nick && $filtered_to_window) {
299 #Xchat::print($text, $filter_window)
301 my $ctx = Xchat
::get_context
();
302 Xchat
::set_context
($filter_window);
303 Xchat
::emit_print
('Channel Message', $nick, $text);
304 Xchat
::set_context
($ctx);
306 #return Xchat::EAT_XCHAT;
307 return Xchat
::EAT_ALL
;
309 return Xchat
::EAT_NONE
;
313 # ------------------------------------------------------------
316 my ($fh, $tmpfile) = tempfile
($filter_file.'.XXXXXX', UNLINK
=>1);
319 Xchat
::print("${B}FILTER:${B} ".
320 "Couldn't open temporary file $tmpfile to save filter: $!\n");
324 Xchat
::print("${B}FILTER SAVE >$filter_file${B}\n");
325 foreach my $regexp (@filter_deny) {
326 Xchat
::print("/".$regexp."/ saved\n");
327 print $fh $regexp."\n";
331 Xchat
::print("${B}FILTER:${B} Couldn't close file to save filter: $!\n");
334 #move($tmpfile, $filter_file);
335 rename($tmpfile, $filter_file);
336 Xchat
::print("${B}FILTER SAVED ----------${B}\n");
344 Xchat
::print("${B}FILTER:${B} ...loading filter patterns\n");
345 unless (open $fh, '<', $filter_file) {
346 Xchat
::print("${B}FILTER:${B} Couldn't open file to load filter: $!\n");
350 @filter_deny = <$fh>;
351 map (chomp, @filter_deny);
354 Xchat
::print("${B}FILTER:${B} Couldn't close file to load filter: $!\n");
358 Xchat
::print("${B}FILTER DENY START ----------${B}\n");
359 for (my $i = 0; $i <= $#filter_deny; $i++) {
360 Xchat
::print(" [$i]: /".$filter_deny[$i]."/\n");
362 Xchat
::print("${B}FILTER DENY END ------------${B}\n");
368 # always ading rules at the end
369 push @filter_deny, $rule;
372 sub delete_rule
( $ ) {
373 my $num = shift || $#filter_deny;
375 splice @filter_deny, $num, 1;
381 $text =~ s!([\/])!\$1!g;
386 # ============================================================
387 # ------------------------------------------------------------
388 # ............................................................
391 Xchat
::print("${B}$scriptName $scriptVersion${B}\n");
392 Xchat
::print(" * URL: http://github.com/jnareb/softsnow-xchat2-filter\n");
393 Xchat
::print(" * URL: http://gitorious.org/projects/softsnow-xchat2-filter\n");
394 Xchat
::print(" * URL: http://repo.or.cz/w/softsnow_xchat2_filter.git\n");
400 if ($filter_turned_on) {
401 Xchat
::print("Filter is turned ${B}ON${B}\n");
403 Xchat
::print("Filter is turned ${B}OFF${B}\n");
405 if ($limit_to_server) {
406 if ($server eq $limit_to_server) {
407 Xchat
::print("Filter is limited to ${B}current${B} ".
408 "server $limit_to_server\n");
410 Xchat
::print("Filter is limited to server ".
411 "$limit_to_server != $server\n");
414 if ($use_filter_allow) {
415 Xchat
::print("Filter is using ALLOW rules (before DENY)\n");
420 Xchat
::print("${B}FILTER DEBUG START ----------${B}\n");
421 Xchat
::print("Channel: ".Xchat
::get_info
("channel")."\n");
422 Xchat
::print("Host: ".Xchat
::get_info
("host")."\n");
423 Xchat
::print("Server: ".Xchat
::get_info
("server")."\n");
424 Xchat
::print("Server Id: ".Xchat
::get_info
("id")."\n");
425 Xchat
::print("Network: ".Xchat
::get_info
("network")."\n");
428 Xchat
::printf("%3u %s rules\n", scalar(@filter_allow), "allow");
429 Xchat
::printf("%3u %s rules\n", scalar(@filter_deny), "deny");
432 # %deny_idx = map { $filter_deny[$_] => $_ } 0..$#filter_deny;
433 @deny_idx{ @filter_deny } = (0..$#filter_deny);
435 Xchat
::print("filtered lines = $nfiltered out of $nlines\n");
437 Xchat
::printf("filtered ratio = %f (%5.1f%%)\n",
438 $nfiltered/$nlines, 100.0*$nfiltered/$nlines);
440 if ($nfiltered > 0) {
441 Xchat
::print("average to match = ".$checklensum/$nfiltered."\n");
442 foreach my $rule (sort { $stats{$b} <=> $stats{$a} } keys %stats) {
443 Xchat
::printf("%5u: %5.1f%% [%2u] /%s/\n",
444 $stats{$rule}, 100.0*$stats{$rule}/$nfiltered,
445 $deny_idx{$rule}, slquote
($rule));
448 if ($use_filter_allow || $nallow > 0) {
449 Xchat
::print("allow matches = $nallow\n");
451 Xchat
::print("${B}FILTER DEBUG END ------------${B}\n");
454 sub cmd_clear_stats
{
461 Xchat
::print("${B}FILTER:${B} stats cleared\n");
464 sub cmd_sort_by_stats
{
468 sort { ($stats{$b} || 0) <=> ($stats{$a} || 0) }
471 Xchat
::print("${B}FILTER:${B} DENY rules sorted by their use descending\n");
474 sub cmd_server_limit
{
478 # adding limiting to given (single) server
479 if ($limit_to_server) {
480 Xchat
::print("${B}FILTER:${B} Changing server from $limit_to_server to $server\n");
481 Xchat
::print("[FILTER LIMITED TO SERVER ${B}$server${B} (WAS TO $limit_to_server)]",
484 Xchat
::print("${B}FILTER:${B} Limiting filtering to server $server\n");
485 Xchat
::print("[FILTER LIMITED TO SERVER ${B}$server${B} (WAS UNLIMITED)]",
488 $limit_to_server = $server;
491 # removing limiting to server
492 if ($limit_to_server) {
493 Xchat
::print("${B}FILTER:${B} Removing limit to server $limit_to_server\n");
494 Xchat
::print("[FILTER ${B}NOT LIMITED${B} TO SERVER (WAS TO $limit_to_server)]",
497 $limit_to_server = '';
502 sub cmd_print_rules
{
503 Xchat
::print("${B}FILTER PRINT START ----------${B}\n");
504 Xchat
::print("${B}ALLOW${B}".($use_filter_allow ?
' (on)' : ' (off)')."\n");
506 for (my $i = 0; $i <= $#filter_allow; $i++) {
507 Xchat
::print("[$i]: /".$filter_allow[$i]."/\n");
509 Xchat
::print("${B}DENY${B}\n");
510 for (my $i = 0; $i <= $#filter_deny; $i++) {
511 Xchat
::print("[$i]: /".$filter_deny[$i]."/\n");
513 Xchat
::print("${B}FILTER PRINT END ------------${B}\n");
521 Xchat
::print("${B}FILTER RULE [$#filter_deny]:${B} /$rule/\n");
523 Xchat
::print("Syntax: ${B}/FILTER ADD ${U}rule${U}${B} to add\n")
527 sub cmd_delete_rule
{
531 $num =~ s/^\s*(.*?)\s*$/$1/g if $num;
534 Xchat
::print("${B}FILTER:${B} deleting /".$filter_deny[-1]."/\n");
536 Xchat
::print("${B}FILTER:${B} deleted successfully last rule\n");
539 if ($num !~ /^\d+$/) {
540 Xchat
::print("${B}FILTER:${B} $num is not a number\n");
543 if ($num < 0 || $num > $#filter_deny) {
544 Xchat
::print("${B}FILTER:${B} $num outside range [0,$#filter_deny]\n");
549 Xchat
::print("${B}FILTER:${B} deleting /".$filter_deny[$num]."/\n");
551 Xchat
::print("${B}FILTER:${B} deleted successfully rule $num\n");
559 $num =~ s/^\s*(.*?)\s*$/$1/g if $num;
561 if (defined $num && $num !~ /^\d+$/) {
562 Xchat
::print("${B}FILTER:${B} $num is not a number\n");
563 } elsif (defined $num && !defined $filter_deny[$num]) {
564 Xchat
::print("${B}FILTER:${B} rule $num does not exist\n");
566 Xchat
::print("${B}FILTER:${B} ".(defined $num ?
"[$num]" : "last").
567 " rule /".$filter_deny[defined $num ?
$num : -1]."/\n");
571 # ============================================================
572 # ============================================================
573 # ============================================================
575 sub filter_command_handler
{
576 my $cmd = $_[0][1]; # 1st parameter (after FILTER)
577 my $arg = $_[1][2]; # 2nd word to the last word
578 my $server = Xchat
::get_info
("host");
581 if (!$cmd || $cmd =~ /^STATUS$/i) {
584 } elsif ($cmd =~ /^ON$/i) {
585 $filter_turned_on = 1;
586 Xchat
::print("Filter turned ${B}ON${B}\n");
587 Xchat
::print("[FILTER TURNED ${B}ON${B}]",
590 } elsif ($cmd =~ /^OFF$/i) {
591 $filter_turned_on = 0;
592 Xchat
::print("Filter turned ${B}OFF${B}\n");
593 Xchat
::print("[FILTER TURNED ${B}OFF${B}]",
596 } elsif ($cmd =~ /^SERVER$/i) {
597 cmd_server_limit
($server);
599 } elsif ($cmd =~ /^SERVERON$/i) {
600 cmd_server_limit
($server);
602 Xchat
::print("[FILTER TURNED ${B}ON${B}]",
604 if (!$filter_turned_on);
605 $filter_turned_on = 1;
606 Xchat
::print("Filter turned ${B}ON${B}\n");
608 } elsif ($cmd =~ /^ALL$/i) {
609 cmd_server_limit
(undef);
611 } elsif ($cmd =~ /^HELP$/i) {
612 Xchat
::print($filter_help);
613 Xchat
::print($filterwindow_help);
615 } elsif ($cmd =~ /^VERSION$/i) {
618 } elsif ($cmd =~ /^DEBUG$/i || $cmd =~ /^INFO$/i) {
621 } elsif ($cmd =~ /^CLEARSTAT(?:S)?$/i) {
624 } elsif ($cmd =~ /^SORT$/i) {
627 } elsif ($cmd =~ /^(?:PRINT|LIST)$/i) {
630 } elsif ($cmd =~ /^ALLOW$/i) {
631 $use_filter_allow = !$use_filter_allow;
632 Xchat
::print("${B}FILTER:${B} ALLOW rules ".
633 ($use_filter_allow ?
"enabled" : "disabled")."\n");
635 } elsif ($cmd =~ /^ADD$/i) {
638 } elsif ($cmd =~ /^DEL(?:ETE)$/i) {
639 cmd_delete_rule
($arg);
641 } elsif ($cmd =~ /^SHOW$/i) {
644 } elsif ($cmd =~ /^SAVE$/i) {
646 Xchat
::print("${B}FILTER:${B} saved DENY rules to $filter_file\n");
648 } elsif ($cmd =~ /^(RE)?LOAD$/i) {
650 Xchat
::print("${B}FILTER:${B} loaded DENY rules from $filter_file\n");
652 } elsif ($cmd =~ /^WINDOW$/i) {
653 return filterwindow_command_handler
(
654 [ 'FILTERWINDOW', @
{$_[0]}[2..$#{$_[0]}] ],
655 [ "FILTERWINDOW $_[1][2]", @
{$_[1]}[2..$#{$_[1]}] ],
660 Xchat
::print("Unknown command ${B}/FILTER $_[1][1]${B}\n") if $cmd;
665 sub filterwindow_command_handler
{
666 my $cmd = $_[0][1]; # 1st parameter (after FILTER)
667 #my $arg = $_[1][2]; # 2nd word to the last word
668 my $ctx = Xchat
::find_context
($filter_window);
670 if (!$cmd || $cmd =~ /^STATUS$/i) {
671 Xchat
::print(($filtered_to_window ?
"Show" : "Don't show").
672 " filtered content in ".
673 (defined $ctx ?
"open" : "closed").
674 " window ${B}$filter_window${B}\n");
676 } elsif ($cmd =~ /^DEBUG$/i) {
677 my $ctx_info = Xchat
::context_info
($ctx);
678 Xchat
::print("${B}FILTERWINDOW DEBUG START ----------${B}\n");
679 Xchat
::print("filtered_to_window = $filtered_to_window\n");
680 Xchat
::print("filter_window = $filter_window\n");
682 Xchat
::print("$filter_window is ${B}open${B}\n");
683 Xchat
::print("$filter_window: network => $ctx_info->{network}\n")
684 if defined $ctx_info->{'network'};
685 Xchat
::print("$filter_window: host => $ctx_info->{host}\n")
686 if defined $ctx_info->{'host'};
687 Xchat
::print("$filter_window: channel => $ctx_info->{channel}\n");
688 Xchat
::print("$filter_window: server_id => $ctx_info->{id}\n")
689 if defined $ctx_info->{'id'};
691 Xchat
::print("$filter_window is ${B}closed${B}\n");
693 # requires XChat >= 2.8.2
694 #Xchat::print("'Channel Message' format: ".
695 # Xchat::get_info("event_text Channel Message")."\n");
696 #Xchat::print("'Channel Msg Hilight' format: ".
697 # Xchat::get_info("event_text Channel Msg Hilight")."\n");
698 Xchat
::print("${B}FILTERWINDOW DEBUG END ------------${B}\n");
700 } elsif ($cmd =~ /^ON$/i) {
701 Xchat
::command
("QUERY $filter_window");
702 Xchat
::print("${B}----- START LOGGING FILTERED CONTENTS -----${B}\n",
704 if !$filtered_to_window;
706 $filtered_to_window = 1;
707 Xchat
::print("Filter shows filtered content in ${B}$filter_window${B}\n");
709 } elsif ($cmd =~ /^(?:OFF|CLOSE)$/i) {
710 Xchat
::print("${B}----- STOP LOGGING FILTERED CONTENTS ------${B}\n",
712 if $filtered_to_window;
713 Xchat
::command
("CLOSE", $filter_window)
714 if ($cmd =~ /^CLOSE$/i);
716 $filtered_to_window = 0;
717 Xchat
::print("Filter doesn't show filtered content in ${B}$filter_window${B}\n");
718 Xchat
::print("${B}FILTER:${B} ${B}$filter_window${B} closed\n")
719 if ($cmd =~ /^CLOSE$/i);
721 } elsif ($cmd =~ /^HELP$/i) {
722 Xchat
::print($filterwindow_help);
725 Xchat
::print("Unknown command ${B}/FILTERWINDOW $_[1][1]${B}\n") if $cmd;
726 Xchat
::print("${B}${U}USAGE:${U} /FILTERWINDOW $filterwindow_commands${B}\n");
732 # ======================================================================
733 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
734 # ----------------------------------------------------------------------
736 Xchat
::print("${B}$scriptName $scriptVersion${B} loaded\n",
737 " For help: ${B}/FILTER HELP${B}\n");