Add further TODO from dlFilter for mIRC, add original SoftSnow filter URL
[softsnow_xchat2_filter.git] / SoftSnow_filter.pl
blob882e6c3eedb52b539884370c0cc2a3a4900e3b29
1 #!/usr/bin/perl
3 # SoftSnow XChat2 filter script
5 ## Summary:
6 ##
7 # Filter out fileserver announcements and SPAM on IRC
9 ## Description:
11 # This script started as an upgrade to the SoftSnow filter script
12 # from http://dukelupus.pri.ee/softsnow/ircscripts/scripts.shtml
13 # (originally http://www.softsnow.biz/softsnow_filter/filter.shtml)
14 # It borrows some ideas from filter-ebooks (#ebooks Xchat2 filter
15 # script) by KiBo, and its older version by RJVJR, mainly moving
16 # from the old IRC:: interface to the new Xchat2 API.
18 # Tested on #ebooks channel on IRCHighWay (irc.irchighway.net)
20 ## Install:
22 # Place SoftSnow_filter.pl in your ~/.xchat directory
24 ## URL (repositories):
25 # * http://github.com/jnareb/softsnow-xchat2-filter
26 # * http://gitorious.org/projects/softsnow-xchat2-filter
27 # * http://repo.or.cz/w/softsnow_xchat2_filter.git
29 ## ChangeLog (main points only):
31 # Version 1.2:
32 # * Original version of SoftSnow filter this one is based on
33 # Version 1.2.2:
34 # * Add /FILTER command, to turn filter off and on, and to limit
35 # filtering to current IRC server only
36 # Version 1.2.3:
37 # * Allow to save and load filter rules from file (UNIX only)
38 # * Add ALLOW rules, for example to show '@search' while filtering '@'
39 # Version 2.0.1:
40 # * Use new XChat2 API (Xchat:: instead of IRC::)
41 # Version 2.0.5:
42 # * More secure saving rules to a file (always save whole file)
43 # Version 2.1.0:
44 # * Allow printing (logging) filtered content to '(filtered)' window
45 # via 'Channel Message' text event, with nick of sender
46 # Version 2.1.3:
47 # * /FILTERWINDOW command to control and query of logging filtered
48 # contents to separate '(filtered)' window
50 ## TODO:
52 # * Add GUI and MENU (would require XChat >= 2.4.5)
53 # * Change format of saved rules to 'm/.../' or 'qr{...}';
54 # see YAML (YAML::Types) and Data::Dumper code and output
55 # * Save and read config together with filter rules
56 # * Read default config and rules from __DATA__, add reset
57 # * Save filter rules usage statistics
58 # * Import filter rules from filter-ebooks3.3FINAL script
59 # * Limit filter to specified channels (or all channels)
60 # * Filter private SPAM (SPAM sent to you, but not other)
61 # * ? Don't accept DCC from users not on common channel
62 # * ? Do not accept files, or dangerous files, from regular users
63 # * Color nicks in '(filtered)' window according to matched rule
64 # * Add command to clear '(filtered)' window
65 # * Add option to strip codes from logged filtered lines
66 # * Limit number of lines in '(filtered)' window
67 # * ? Perhaps something about '@find' and '!find' results?
69 use strict;
70 use warnings;
72 use File::Temp qw(tempfile);
73 use File::Copy qw(move);
76 my $scriptName = "SoftSnow XChat2 Filter";
77 my $scriptVersion = "2.1.4";
78 my $scriptDescr = "Filter out file server announcements and IRC SPAM";
80 my $B = chr 2; # bold
81 my $U = chr 31; # underline
82 my $C = chr 3; # start of color sequence
83 my $R = chr 22; # reverse
84 my $O = chr 15; # reset
86 ### config ###
87 my $filter_file = Xchat::get_info("xchatdir") . "/SoftSnow_filter.conf";
89 my $filter_turned_on = 0; # is filter is turned on
90 my $limit_to_server = ''; # if true limit to given server (host)
91 my $use_filter_allow = 0; # use overrides (ALLOW before DENY)
93 my $filtered_to_window = 0;
94 my $filter_window = "(filtered)";
95 ### end config ###
97 my $filter_commands = 'ON|OFF|STATUS|SERVER|SERVERON|ALL|HELP|DEBUG|PRINT|ALLOW|ADD|DELETE|SAVE|LOAD';
99 my $filter_help = <<"EOF";
100 ${B}/FILTER $filter_commands${B}
101 /FILTER ON|OFF - turns filtering on/off
102 /FILTER HELP - prints this help message
103 /FILTER STATUS - prints if filter is turned on, and with what limits
104 /FILTER DEBUG - shows some info; used in debuggin the filter
105 /FILTER PRINT - prints all the rules
106 /FILTER ALLOW - toggle use of ALLOW rules (before DENY).
107 /FILTER SERVER - limits filtering to current server (host)
108 /FILTER SERVERON - limits to server and turns filter on
109 /FILTER ALL - resumes filtering everywhere i.e. removes limits
110 /FILTER SAVE - saves the rules to the file $filter_file
111 /FILTER LOAD - loads the rules from the file, replacing existing rules
112 /FILTER ADD <rule> - add rule at the end of the DENY rules
113 /FILTER DELETE [<num>] - delete rule number <num>, or last rule
114 /FILTER SHOW [<num>] - show rule number <num>, or last rule
115 /FILTER VERSION - prints the name and version of this script
116 /FILTER WINDOW <arg>... - same as /FILTERWINDOW <arg>...
117 /FILTER without parameter is equivalent to /FILTER STATUS
120 my $filterwindow_commands = 'ON|OFF|CLOSE|HELP|STATUS|DEBUG';
122 my $filterwindow_help = <<"EOF";
123 ${B}/FILTERWINDOW $filterwindow_commands${B}
124 /FILTERWINDOW ON|OFF - turns saving filtered content to ${U}$filter_window${U}
125 /FILTERWINDOW CLOSE - close ${U}$filter_window${U} (and turn off logging)
126 /FILTERWINDOW STATUS - prints if saving to ${U}$filter_window${U} is turned on
127 /FILTERWINDOW HELP - prints this help message
128 /FILTERWINDOW DEBUG - shows some info; used in debugging this part of filter
129 /FILTERWINDOW without parameter is equivalent to /FILTERWINDOW STATUS
132 Xchat::register($scriptName, $scriptVersion, $scriptDescr);
134 Xchat::hook_command("FILTER", \&filter_command_handler,
135 { help_text => $filter_help });
136 Xchat::hook_command("FILTERWINDOW", \&filterwindow_command_handler,
137 { help_text => $filterwindow_help });
138 Xchat::hook_server("PRIVMSG", \&privmsg_handler);
140 Xchat::print("Loading ${B}$scriptName $scriptVersion${B}...\n");
143 # GUI, windows, etc.
144 if ($filtered_to_window) {
145 Xchat::command("QUERY $filter_window");
148 # information about (default) options used
149 if ($filter_turned_on) {
150 Xchat::print("Filter turned ${B}ON${B}\n");
151 } else {
152 Xchat::print("Filter turned ${B}OFF${B}\n");
154 if ($limit_to_server) {
155 Xchat::print("Filter limited to server $limit_to_server\n")
157 if ($use_filter_allow) {
158 Xchat::print("Filter uses ALLOW rules\n");
161 # ------------------------------------------------------------
163 my @filter_allow = (
164 q/^\@search\s/,
167 my @filter_deny = (
168 q/\@/,
169 q/^\s*\!/,
170 q/slot\(s\)/,
171 #q/~&~&~/,
173 #xdcc
174 q/^\#\d+/,
176 #fserves
177 q/(?i)fserve.*trigger/,
178 q/(?i)trigger.*\!/,
179 q/(?i)trigger.*\/ctcp/,
180 q/(?i)type\:\s*\!/,
181 q/(?i)file server online/,
183 #ftps
184 q/(?i)ftp.*l\/p/,
186 #CTCPs
187 q/SLOTS/,
188 q/MP3 /,
190 #messages for when a file is received/failed to receive
191 q/(?i)DEFINITELY had the right stuff to get/,
192 q/(?i)has just received/,
193 q/(?i)I have just received/,
195 #mp3 play messages
196 q/is listening to/,
197 q/\]\-MP3INFO\-\[/,
199 #spammy scripts
200 q/\]\-SpR\-\[/,
201 q/We are BORG/,
203 #general messages
204 q/brave soldier in the war/,
207 # return 1 (true) if text given as argument is to be filtered out
208 sub isFiltered {
209 my $text = shift;
210 my $regexp = '';
212 #strip colour, underline, bold codes, etc.
213 $text = Xchat::strip_code($text);
215 if ($use_filter_allow) {
216 foreach $regexp (@filter_allow) {
217 return 0 if ($text =~ /$regexp/);
221 foreach $regexp (@filter_deny) {
222 return 1 if ($text =~ /$regexp/);
225 return 0;
228 #called when someone says something in the channel
229 #1: address of speaker
230 #2: PRIVMSG constant
231 #3: channel
232 #4: text said (prefixed with :)
233 sub privmsg_handler {
234 # $_[0] - array reference containing the IRC message or command
235 # and arguments broken into words
236 # $_[1] - array reference containing the Nth word to the last word
237 my ($address, $msgtype, $channel) = @{$_[0]};
238 my ($nick, $user, $host) = ($address =~ /^:(.*?)!(.*?)@(.*)$/);
240 my $text = $_[1][3]; # Get server message
242 my $server = Xchat::get_info("host");
244 #-- EXAMPLE RAW COMMANDS: --
245 #chanmsg: [':epitaph!~epitaph@CPE00a0241892b7-CM014480119187.cpe.net.cable.rogers.com', 'PRIVMSG', '#werd', ':mah', 'script', 'is', 'doing', 'stuff.']
246 #action: [':rlz!railz@bzq-199-176.red.bezeqint.net', 'PRIVMSG', '#werd', ':\x01ACTION', 'hugs', 'elhaym', '\x01']
247 #private: [':olene!oqd@girli.sh', 'PRIVMSG', 'epinoodle', ':hey']
250 return Xchat::EAT_NONE unless $filter_turned_on;
251 if ($limit_to_server) {
252 return Xchat::EAT_NONE unless $server eq $limit_to_server;
254 # do not filter out private messages
255 return Xchat::EAT_NONE unless ($channel =~ /^#/);
257 $text =~ s/^://;
259 if (isFiltered($text)) {
260 if (defined $nick && $filtered_to_window) {
261 #Xchat::print($text, $filter_window)
263 my $ctx = Xchat::get_context();
264 Xchat::set_context($filter_window);
265 Xchat::emit_print('Channel Message', $nick, $text);
266 Xchat::set_context($ctx);
268 #return Xchat::EAT_XCHAT;
269 return Xchat::EAT_ALL;
271 return Xchat::EAT_NONE;
275 # ------------------------------------------------------------
277 sub save_filter {
278 my ($fh, $tmpfile) = tempfile($filter_file.'.XXXXXX', UNLINK=>1);
280 unless ($fh) {
281 Xchat::print("${B}FILTER:${B} ".
282 "Couldn't open temporary file $tmpfile to save filter: $!\n");
283 return;
286 Xchat::print("${B}FILTER SAVE >$filter_file${B}\n");
287 foreach my $regexp (@filter_deny) {
288 Xchat::print("/".$regexp."/ saved\n");
289 print $fh $regexp."\n";
292 unless (close $fh) {
293 Xchat::print("${B}FILTER:${B} Couldn't close file to save filter: $!\n");
294 return;
296 #move($tmpfile, $filter_file);
297 rename($tmpfile, $filter_file);
298 Xchat::print("${B}FILTER SAVED ----------${B}\n");
300 return 1;
303 sub load_filter {
304 my $fh;
306 Xchat::print("${B}FILTER:${B} ...loading filter patterns\n");
307 unless (open $fh, '<', $filter_file) {
308 Xchat::print("${B}FILTER:${B} Couldn't open file to load filter: $!\n");
309 return;
312 @filter_deny = <$fh>;
313 map (chomp, @filter_deny);
315 unless (close $fh) {
316 Xchat::print("${B}FILTER:${B} Couldn't close file to load filter: $!\n");
317 return;
320 Xchat::print("${B}FILTER DENY ----------${B}\n");
321 for (my $i = 0; $i <= $#filter_deny; $i++) {
322 Xchat::print(" [$i]: /".$filter_deny[$i]."/\n");
324 Xchat::print("${B}FILTER DENY ----------${B}\n");
327 sub add_rule ( $ ) {
328 my $rule = shift;
330 # always ading rules at the end
331 push @filter_deny, $rule;
334 sub delete_rule ( $ ) {
335 my $num = shift || $#filter_deny;
337 splice @filter_deny, $num, 1;
340 # ============================================================
341 # ------------------------------------------------------------
342 # ............................................................
344 sub cmd_version {
345 Xchat::print("${B}$scriptName $scriptVersion${B}\n");
346 Xchat::print(" * URL: http://github.com/jnareb/softsnow-xchat2-filter\n");
347 Xchat::print(" * URL: http://gitorious.org/projects/softsnow-xchat2-filter\n");
348 Xchat::print(" * URL: http://repo.or.cz/w/softsnow_xchat2_filter.git\n");
351 sub cmd_status {
352 my $server = shift;
354 if ($filter_turned_on) {
355 Xchat::print("Filter is turned ${B}ON${B}\n");
356 } else {
357 Xchat::print("Filter is turned ${B}OFF${B}\n");
359 if ($limit_to_server) {
360 if ($server eq $limit_to_server) {
361 Xchat::print("Filter is limited to ${B}current${B} ".
362 "server $limit_to_server\n");
363 } else {
364 Xchat::print("Filter is limited to server ".
365 "$limit_to_server != $server\n");
368 if ($use_filter_allow) {
369 Xchat::print("Filter is using ALLOW rules (before DENY)\n");
373 sub cmd_debug {
374 Xchat::print("${B}FILTER DEBUG ----------${B}\n");
375 Xchat::print("Channel: ".Xchat::get_info("channel")."\n");
376 Xchat::print("Host: ".Xchat::get_info("host")."\n");
377 Xchat::print("Server: ".Xchat::get_info("server")."\n");
378 Xchat::print("Server Id: ".Xchat::get_info("id")."\n");
379 Xchat::print("Network: ".Xchat::get_info("network")."\n");
380 Xchat::print("\n");
381 Xchat::printf("%3u %s rules\n", scalar(@filter_allow), "allow");
382 Xchat::printf("%3u %s rules\n", scalar(@filter_deny), "deny");
383 Xchat::print("${B}FILTER DEBUG ----------${B}\n");
386 sub cmd_server_limit {
387 my $server = shift;
389 if ($server) {
390 # adding limiting to given (single) server
391 if ($limit_to_server) {
392 Xchat::print("${B}FILTER:${B} Changing server from $limit_to_server to $server\n");
393 Xchat::print("[FILTER LIMITED TO SERVER ${B}$server${B} (WAS TO $limit_to_server)]",
394 $filter_window);
395 } else {
396 Xchat::print("${B}FILTER:${B} Limiting filtering to server $server\n");
397 Xchat::print("[FILTER LIMITED TO SERVER ${B}$server${B} (WAS UNLIMITED)]",
398 $filter_window);
400 $limit_to_server = $server;
402 } else {
403 # removing limiting to server
404 if ($limit_to_server) {
405 Xchat::print("${B}FILTER:${B} Removing limit to server $limit_to_server\n");
406 Xchat::print("[FILTER ${B}NOT LIMITED${B} TO SERVER (WAS TO $limit_to_server)]",
407 $filter_window);
409 $limit_to_server = '';
414 sub cmd_print_rules {
415 Xchat::print("${B}FILTER PRINT ----------${B}\n");
416 Xchat::print("${B}ALLOW${B}".($use_filter_allow ? ' (on)' : ' (off)')."\n");
418 for (my $i = 0; $i <= $#filter_allow; $i++) {
419 Xchat::print("[$i]: /".$filter_allow[$i]."/\n");
421 Xchat::print("${B}DENY${B}\n");
422 for (my $i = 0; $i <= $#filter_deny; $i++) {
423 Xchat::print("[$i]: /".$filter_deny[$i]."/\n");
425 Xchat::print("${B}FILTER PRINT ----------${B}\n");
428 sub cmd_add_rule {
429 my $rule = shift;
431 if ($rule) {
432 add_rule($rule);
433 Xchat::print("${B}FILTER RULE [$#filter_deny]:${B} /$rule/\n");
434 } else {
435 Xchat::print("Syntax: ${B}/FILTER ADD ${U}rule${U}${B} to add\n")
439 sub cmd_delete_rule {
440 my $num = shift;
442 # strip whitespace
443 $num =~ s/^\s*(.*?)\s*$/$1/g if $num;
444 SWITCH: {
445 unless ($num) {
446 Xchat::print("${B}FILTER:${B} deleting /".$filter_deny[-1]."/\n");
447 $#filter_deny--;
448 Xchat::print("${B}FILTER:${B} deleted successfully last rule\n");
449 last SWITCH;
451 if ($num !~ /^\d+$/) {
452 Xchat::print("${B}FILTER:${B} $num is not a number\n");
453 last SWITCH;
455 if ($num < 0 || $num > $#filter_deny) {
456 Xchat::print("${B}FILTER:${B} $num outside range [0,$#filter_deny]\n");
457 last SWITCH;
459 # default
461 Xchat::print("${B}FILTER:${B} deleting /".$filter_deny[$num]."/\n");
462 delete_rule($num);
463 Xchat::print("${B}FILTER:${B} deleted successfully rule $num\n");
468 sub cmd_show_rule {
469 my $num = shift;
471 $num =~ s/^\s*(.*?)\s*$/$1/g if $num;
473 if (defined $num && $num !~ /^\d+$/) {
474 Xchat::print("${B}FILTER:${B} $num is not a number\n");
475 } elsif (defined $num && !defined $filter_deny[$num]) {
476 Xchat::print("${B}FILTER:${B} rule $num does not exist\n");
477 } else {
478 Xchat::print("${B}FILTER:${B} ".(defined $num ? "[$num]" : "last").
479 " rule /".$filter_deny[defined $num ? $num : -1]."/\n");
483 # ============================================================
484 # ============================================================
485 # ============================================================
487 sub filter_command_handler {
488 my $cmd = $_[0][1]; # 1st parameter (after FILTER)
489 my $arg = $_[1][2]; # 2nd word to the last word
490 my $server = Xchat::get_info("host");
493 if (!$cmd || $cmd =~ /^STATUS$/i) {
494 cmd_status($server);
496 } elsif ($cmd =~ /^ON$/i) {
497 $filter_turned_on = 1;
498 Xchat::print("Filter turned ${B}ON${B}\n");
499 Xchat::print("[FILTER TURNED ${B}ON${B}]",
500 $filter_window);
502 } elsif ($cmd =~ /^OFF$/i) {
503 $filter_turned_on = 0;
504 Xchat::print("Filter turned ${B}OFF${B}\n");
505 Xchat::print("[FILTER TURNED ${B}OFF${B}]",
506 $filter_window);
508 } elsif ($cmd =~ /^SERVER$/i) {
509 cmd_server_limit($server);
511 } elsif ($cmd =~ /^SERVERON$/i) {
512 cmd_server_limit($server);
514 Xchat::print("[FILTER TURNED ${B}ON${B}]",
515 $filter_window)
516 if (!$filter_turned_on);
517 $filter_turned_on = 1;
518 Xchat::print("Filter turned ${B}ON${B}\n");
520 } elsif ($cmd =~ /^ALL$/i) {
521 cmd_server_limit(undef);
523 } elsif ($cmd =~ /^HELP$/i) {
524 Xchat::print($filter_help);
525 Xchat::print($filterwindow_help);
527 } elsif ($cmd =~ /^VERSION$/i) {
528 cmd_version();
530 } elsif ($cmd =~ /^DEBUG$/i || $cmd =~ /^INFO$/i) {
531 cmd_debug();
533 } elsif ($cmd =~ /^(?:PRINT|LIST)$/i) {
534 cmd_print_rules();
536 } elsif ($cmd =~ /^ALLOW$/i) {
537 $use_filter_allow = !$use_filter_allow;
538 Xchat::print("${B}FILTER:${B} ALLOW rules ".
539 ($use_filter_allow ? "enabled" : "disabled")."\n");
541 } elsif ($cmd =~ /^ADD$/i) {
542 cmd_add_rule($arg);
544 } elsif ($cmd =~ /^DEL(?:ETE)$/i) {
545 cmd_delete_rule($arg);
547 } elsif ($cmd =~ /^SHOW$/i) {
548 cmd_show_rule($arg);
550 } elsif ($cmd =~ /^SAVE$/i) {
551 save_filter();
552 Xchat::print("${B}FILTER:${B} saved DENY rules to $filter_file\n");
554 } elsif ($cmd =~ /^(RE)?LOAD$/i) {
555 load_filter();
556 Xchat::print("${B}FILTER:${B} loaded DENY rules from $filter_file\n");
558 } elsif ($cmd =~ /^WINDOW$/i) {
559 return filterwindow_command_handler(
560 [ 'FILTERWINDOW', @{$_[0]}[2..$#{$_[0]}] ],
561 [ "FILTERWINDOW $_[1][2]", @{$_[1]}[2..$#{$_[1]}] ],
562 $_[2]
565 } else {
566 Xchat::print("Unknown command ${B}/FILTER $_[1][1]${B}\n") if $cmd;
568 return 1;
571 sub filterwindow_command_handler {
572 my $cmd = $_[0][1]; # 1st parameter (after FILTER)
573 #my $arg = $_[1][2]; # 2nd word to the last word
574 my $ctx = Xchat::find_context($filter_window);
576 if (!$cmd || $cmd =~ /^STATUS$/i) {
577 Xchat::print(($filtered_to_window ? "Show" : "Don't show").
578 " filtered content in ".
579 (defined $ctx ? "open" : "closed").
580 " window ${B}$filter_window${B}\n");
582 } elsif ($cmd =~ /^DEBUG$/i) {
583 my $ctx_info = Xchat::context_info($ctx);
584 Xchat::print("${B}FILTERWINDOW DEBUG ----------${B}\n");
585 Xchat::print("filtered_to_window = $filtered_to_window\n");
586 Xchat::print("filter_window = $filter_window\n");
587 if (defined $ctx) {
588 Xchat::print("$filter_window is ${B}open${B}\n");
589 Xchat::print("$filter_window: network => $ctx_info->{network}\n")
590 if defined $ctx_info->{'network'};
591 Xchat::print("$filter_window: host => $ctx_info->{host}\n")
592 if defined $ctx_info->{'host'};
593 Xchat::print("$filter_window: channel => $ctx_info->{channel}\n");
594 Xchat::print("$filter_window: server_id => $ctx_info->{id}\n")
595 if defined $ctx_info->{'id'};
596 } else {
597 Xchat::print("$filter_window is ${B}closed${B}\n");
599 # requires XChat >= 2.8.2
600 #Xchat::print("'Channel Message' format: ".
601 # Xchat::get_info("event_text Channel Message")."\n");
602 #Xchat::print("'Channel Msg Hilight' format: ".
603 # Xchat::get_info("event_text Channel Msg Hilight")."\n");
604 Xchat::print("${B}FILTERWINDOW DEBUG ----------${B}\n");
606 } elsif ($cmd =~ /^ON$/i) {
607 Xchat::command("QUERY $filter_window");
608 Xchat::print("${B}----- START LOGGING FILTERED CONTENTS -----${B}\n",
609 $filter_window)
610 if !$filtered_to_window;
612 $filtered_to_window = 1;
613 Xchat::print("Filter shows filtered content in ${B}$filter_window${B}\n");
615 } elsif ($cmd =~ /^(?:OFF|CLOSE)$/i) {
616 Xchat::print("${B}----- STOP LOGGING FILTERED CONTENTS -----${B}\n",
617 $filter_window)
618 if $filtered_to_window;
619 Xchat::command("CLOSE", $filter_window)
620 if ($cmd =~ /^CLOSE$/i);
622 $filtered_to_window = 0;
623 Xchat::print("Filter doesn't show filtered content in ${B}$filter_window${B}\n");
624 Xchat::print("${B}FILTER:${B} ${B}$filter_window${B} closed\n")
625 if ($cmd =~ /^CLOSE$/i);
627 } elsif ($cmd =~ /^HELP$/i) {
628 Xchat::print($filterwindow_help);
630 } else {
631 Xchat::print("Unknown command ${B}/FILTERWINDOW $_[1][1]${B}\n") if $cmd;
632 Xchat::print("${B}${U}USAGE:${U} /FILTERWINDOW $filterwindow_commands${B}\n");
635 return 1;
638 # ======================================================================
639 # ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
640 # ----------------------------------------------------------------------
642 Xchat::print("${B}$scriptName $scriptVersion${B} loaded\n",
643 " For help: ${B}/FILTER HELP${B}\n");