]> git.donarmstrong.com Git - infobot.git/blob - src/IRC/IrcHelpers.pl
add any message flood repeat tracking; ignore specific message regexes
[infobot.git] / src / IRC / IrcHelpers.pl
1 #
2 # IrcHooks.pl: IRC Hooks stuff.
3 #      Author: dms
4 #     Version: 20010413
5 #     Created: 20010413
6 #        NOTE: Based on code by Kevin Lenzo & Patrick Cole  (c) 1997
7 #
8
9 #######################################################################
10 ####### IRC HOOK HELPERS   IRC HOOK HELPERS   IRC HOOK HELPERS ########
11 #######################################################################
12
13 #####
14 # Usage: &hookMode($nick, $modes, @targets);
15 sub hookMode {
16     my ( $nick, $modes, @targets ) = @_;
17     my $parity = 0;
18
19     my $mode;
20     foreach $mode ( split( //, $modes ) ) {
21
22         # sign. tmp parity needed to store current state
23         if ( $mode =~ /[-+]/ ) {
24             $parity = 1 if ( $mode eq '+' );
25             $parity = 0 if ( $mode eq '-' );
26             next;
27         }
28
29         # mode with target.
30         if ( $mode =~ /[bklov]/ ) {
31             my $target = shift @targets;
32
33             if ($parity) {
34                 $chanstats{ lc $chan }{'Op'}++  if ( $mode eq 'o' );
35                 $chanstats{ lc $chan }{'Ban'}++ if ( $mode eq 'b' );
36             }
37             else {
38                 $chanstats{ lc $chan }{'Deop'}++  if ( $mode eq 'o' );
39                 $chanstats{ lc $chan }{'Unban'}++ if ( $mode eq 'b' );
40             }
41
42             # modes w/ target affecting nick => cache it.
43             if ( $mode =~ /[bov]/ ) {
44                 $channels{ lc $chan }{$mode}{$target}++      if $parity;
45                 delete $channels{ lc $chan }{$mode}{$target} if !$parity;
46
47                 # lets do some custom stuff.
48                 if ( $mode =~ /o/ and not $parity ) {
49                     if ( $target =~ /^\Q$ident\E$/i ) {
50                         &VERB( 'hookmode: someone deopped us!', 2 );
51                         &chanServCheck($chan);
52                     }
53
54                     &chanLimitVerify($chan);
55                 }
56             }
57
58             if ( $mode =~ /[l]/ ) {
59                 $channels{ lc $chan }{$mode} = $target if $parity;
60                 delete $channels{ lc $chan }{$mode} if !$parity;
61             }
62         }
63
64         # important channel modes, targetless.
65         if ( $mode =~ /[mt]/ ) {
66             $channels{ lc $chan }{$mode}++      if $parity;
67             delete $channels{ lc $chan }{$mode} if !$parity;
68         }
69     }
70 }
71
72 sub hookMsg {
73     ( $msgType, $chan, $who, $message ) = @_;
74     my $skipmessage = 0;
75     $addressed      = 0;
76     $addressedother = 0;
77     $orig{message}  = $message;
78     $orig{who}      = $who;
79     $addrchar       = 0;
80
81     $message =~ s/[\cA-\c_]//ig;    # strip control characters
82     $message =~ s/^\s+//;           # initial whitespaces.
83     $who     =~ tr/A-Z/a-z/;        # lowercase.
84     my $mynick = $conn->nick();
85
86     &showProc();
87
88     # addressing.
89     if ( $msgType =~ /private/ ) {
90
91         # private messages.
92         $addressed = 1;
93         if ( &IsChanConf('addressCharacter') > 0 ) {
94             $addressCharacter = getChanConf('addressCharacter');
95             if ( $message =~ s/^\Q$addressCharacter\E// ) {
96                 &msg( $who,
97 "The addressCharacter \"$addressCharacter\" is to get my attention in a normal channel. Please leave it off when messaging me directly."
98                 );
99             }
100         }
101     }
102     else {
103
104         # public messages.
105         # addressing revamped by the xk.
106         ### below needs to be fixed...
107         if ( &IsChanConf('addressCharacter') > 0 ) {
108             $addressCharacter = getChanConf('addressCharacter');
109             if ( $message =~ s/^\Q$addressCharacter\E// ) {
110                 $addrchar  = 1;
111                 $addressed = 1;
112             }
113         }
114
115         if ( $message =~ /^($mask{nick})([\;\:\>\, ]+) */ ) {
116             my $newmessage = $';
117             if ( $1 =~ /^\Q$mynick\E$/i ) {
118                 $message   = $newmessage;
119                 $addressed = 1;
120             }
121             else {
122
123                 # ignore messages addressed to other people or unaddressed.
124                 $skipmessage++ if ( $2 ne '' and $2 !~ /^ / );
125             }
126         }
127     }
128
129     # Determine floodwho.
130     my $c = '_default';
131     if ( $msgType =~ /public/i ) {
132
133         # public.
134         $floodwho = $c = lc $chan;
135     }
136     elsif ( $msgType =~ /private/i ) {
137
138         # private.
139         $floodwho = lc $who;
140     }
141     else {
142
143         # dcc?
144         &FIXME('floodwho = ???');
145     }
146
147     my $val = &getChanConfDefault( 'floodRepeat', '2:5', $c );
148     my ( $count, $interval ) = split /:/, $val;
149     my ($count_ra,$interval_ra) = split /:/,
150         getChanConfDefault('floodRepeatAll','2:3',$c);
151
152     # flood repeat protection.
153     if ($addressed) {
154         my $time = $flood{$floodwho}{$message} || 0;
155         my $time_ra = $flood{$floodwho}{"\0"} || 0;
156
157         if (    !&IsFlag('o')
158                 and $msgType eq 'public' and ((time() - $time < $interval) or (time - $time_ra < $interval_ra))) {
159             return if ($lobotomized);
160             if (time() - $time < $interval ) {
161                 ### public != personal who so the below is kind of pointless.
162                 my @who;
163                 foreach ( keys %flood ) {
164                     next if (/^\Q$floodwho\E$/);
165                     next if ( defined $chan and /^\Q$chan\E$/ );
166                     
167                     push( @who, grep /^\Q$message\E$/i, keys %{ $flood{$_} } );
168                 }
169                 
170                 
171                 if ( !scalar @who ) {
172                     push( @who, 'Someone' );
173                 }
174                 &msg( $who,
175                       join( ' ', @who )
176                       . ' already said that '
177                       . ( time - $time )
178                       . ' seconds ago' );
179             }
180             else {
181                 &msg($who,
182                      "You already asked me something ".(time - $time_ra). " seconds ago."
183                     );
184             }
185             ### TODO: delete old floodwarn{} keys.
186             my $floodwarn = 0;
187             if ( !exists $floodwarn{$floodwho} ) {
188                 $floodwarn++;
189             }
190             else {
191                 $floodwarn++ if ( time() - $floodwarn{$floodwho} > $interval );
192             }
193             
194             if ($floodwarn) {
195                 &status("FLOOD repetition detected from $floodwho.");
196                 $floodwarn{$floodwho} = time();
197             }
198             return;
199         }
200
201         if ($addrchar) {
202             &status("$b_cyan$who$ob is short-addressing $mynick");
203         }
204         elsif ( $msgType eq 'private' ) {    # private.
205             &status("$b_cyan$who$ob is /msg'ing $mynick");
206         }
207         else {                               # public?
208             &status("$b_cyan$who$ob is addressing $mynick");
209         }
210
211         $flood{$floodwho}{$message} = time();
212         $flood{$floodwho}{"\0"} = time;
213     }
214     elsif ( $msgType eq 'public' and &IsChanConf('kickOnRepeat') > 0 ) {
215
216         # unaddressed, public only.
217
218         ### TODO: use a separate 'short-time' hash.
219         my @data;
220         @data = keys %{ $flood{$floodwho} } if ( exists $flood{$floodwho} );
221     }
222
223     $val = &getChanConfDefault( 'floodMessages', '5:30', $c );
224     ( $count, $interval ) = split /:/, $val;
225
226     # flood overflow protection.
227     if ($addressed) {
228         foreach ( keys %{ $flood{$floodwho} } ) {
229             next unless ( time() - $flood{$floodwho}{$_} > $interval );
230             delete $flood{$floodwho}{$_};
231         }
232
233         my $i = scalar keys %{ $flood{$floodwho} };
234         if ( $i > $count ) {
235             my $expire = $param{'ignoreAutoExpire'} || 5;
236
237             #       &msg($who,"overflow of messages ($i > $count)");
238             &msg( $who,
239                 "Too many queries from you, ignoring for $expire minutes." );
240             &status("FLOOD overflow detected from $floodwho; ignoring");
241
242             &ignoreAdd( "*!$uh", $chan, $expire,
243                 'flood overflow auto-detected.' );
244             return;
245         }
246
247         $flood{$floodwho}{$message} = time();
248         $flood{$floodwho}{"\0"} = time();
249     }
250
251     my @ignore;
252     if ( $msgType =~ /public/i ) {    # public.
253         $talkchannel = $chan;
254         &status("<$orig{who}/$chan> $orig{message}");
255         push( @ignore, keys %{ $ignore{$chan} } ) if ( exists $ignore{$chan} );
256     }
257     elsif ( $msgType =~ /private/i ) {    # private.
258         &status("[$orig{who}] $orig{message}");
259         $talkchannel = undef;
260         $chan        = '_default';
261     }
262     else {
263         &DEBUG("unknown msgType => $msgType.");
264     }
265     push( @ignore, keys %{ $ignore{'*'} } ) if ( exists $ignore{'*'} );
266     my @ignore_msg = ();
267     push @ignore_msg, keys %{$ignore{'*_msg'}} if exists $ignore{'*_msg'};
268
269     if (    ( !$skipmessage or &IsChanConf('seenStoreAll') > 0 )
270         and &IsChanConf('sed') > 0
271         and &IsChanConf('seen') > 0
272         and $msgType       =~ /public/
273         and $orig{message} =~ /^s\/([^;\/]*)\/([^;\/]*)\/([g]*)$/ )
274     {
275         my $sedmsg = $seencache{$who}{'msg'};
276         eval "\$sedmsg =~ s/\Q$1\E/\Q$2\E/$3;";
277         $sedmsg =~ s/^(.{255}).*$/$1.../;    # 255 char max to prevent flood
278
279         if ( $sedmsg ne $seencache{$who}{'msg'} ) {
280             &DEBUG( "sed \""
281                   . $orig{message} . "\" \""
282                   . $seencache{$who}{'msg'} . "\" \""
283                   . $sedmsg
284                   . "\"" );
285             &msg( $talkchannel, "$orig{who} meant: $sedmsg" );
286         }
287     }
288     elsif ( ( !$skipmessage or &IsChanConf('seenStoreAll') > 0 )
289         and &IsChanConf('seen') > 0
290         and $msgType =~ /public/ )
291     {
292         $seencache{$who}{'time'} = time();
293         $seencache{$who}{'nick'} = $orig{who};
294         $seencache{$who}{'host'} = $uh;
295         $seencache{$who}{'chan'} = $talkchannel;
296         $seencache{$who}{'msg'}  = $orig{message};
297         $seencache{$who}{'msgcount'}++;
298     }
299     if ( &IsChanConf('minVolunteerLength') > 0 ) {
300
301         # FIXME hack to treat unaddressed as if using addrchar
302         $addrchar = 1;
303     }
304     return if ($skipmessage);
305     return unless ( $addrchar or $addressed );
306
307     foreach (@ignore) {
308         s/\*/\\S*/g;
309
310         next unless ( eval { $nuh =~ /^$_$/i } );
311
312         # better to ignore an extra message than to allow one to get
313         # through, although it would be better to go through ignore
314         # checking again.
315         if ( time() - ( $cache{ignoreCheckTime} || 0 ) > 60 ) {
316             &ignoreCheck();
317         }
318
319         &status("IGNORE <$who> $message");
320         return;
321     }
322
323     for my $msg_regex (@ignore_msg) {
324
325         next unless ( eval { $message =~ /\Q$msg_regex\E/i } );
326
327         # better to ignore an extra message than to allow one to get
328         # through, although it would be better to go through ignore
329         # checking again.
330         if ( time() - ( $cache{ignoreCheckTime} || 0 ) > 60 ) {
331             &ignoreCheck();
332         }
333
334         &status("IGNORE <$who> $message");
335         return;
336     }
337
338     if ( defined $nuh ) {
339         if ( !defined $userHandle ) {
340             &DEBUG('line 1074: need verifyUser?');
341             &verifyUser( $who, $nuh );
342         }
343     }
344     else {
345         &DEBUG("hookMsg: 'nuh' not defined?");
346     }
347
348 ### For extra debugging purposes...
349     if ( $_ = &process() ) {
350
351         #       &DEBUG("IrcHooks: process returned '$_'.");
352     }
353
354     # hack to remove +o from ppl with +O flag.
355     if (   exists $users{$userHandle}
356         && exists $users{$userHandle}{FLAGS}
357         && $users{$userHandle}{FLAGS} =~ /O/ )
358     {
359         $users{$userHandle}{FLAGS} =~ s/o//g;
360     }
361
362     return;
363 }
364
365 # this is basically run on on_join or on_quit
366 sub chanLimitVerify {
367     my ($c) = @_;
368     $chan = $c;
369     my $l = $channels{$chan}{'l'};
370
371     return unless ( &IsChanConf('chanlimitcheck') > 0 );
372
373     if ( scalar keys %netsplit ) {
374         &WARN("clV: netsplit active (1, chan = $chan); skipping.");
375         return;
376     }
377
378     if ( !defined $l ) {
379         &DEBUG("$chan: running chanlimitCheck from chanLimitVerify.");
380         &chanlimitCheck();
381         return;
382     }
383
384     # only change it if it's not set.
385     my $plus  = &getChanConfDefault( 'chanlimitcheckPlus', 5, $chan );
386     my $count = scalar( keys %{ $channels{$chan}{''} } );
387     my $int   = &getChanConfDefault( 'chanlimitcheckInterval', 10, $chan );
388
389     my $delta = $count + $plus - $l;
390
391     #   $delta    =~ s/^\-//;
392
393     if ( $plus <= 3 ) {
394         &WARN("clc: stupid to have plus at $plus, fix it!");
395     }
396
397     if ( exists $cache{chanlimitChange}{$chan} ) {
398         if ( time() - $cache{chanlimitChange}{$chan} < $int * 60 ) {
399             return;
400         }
401     }
402
403     &chanServCheck($chan);
404
405     ### TODO: unify code with chanlimitcheck()
406     return if ( $delta > 5 );
407
408     &status("clc: big change in limit for $chan ($delta);"
409           . "going for it. (was: $l; now: "
410           . ( $count + $plus )
411           . ')' );
412
413     $conn->mode( $chan, '+l', $count + $plus );
414     $cache{chanlimitChange}{$chan} = time();
415 }
416
417 sub chanServCheck {
418     ($chan) = @_;
419
420     if ( !defined $chan or $chan =~ /^\s*$/ ) {
421         &WARN('chanServCheck: chan == NULL.');
422         return 0;
423     }
424
425     return unless ( &IsChanConf('chanServCheck') > 0 );
426
427     &VERB( "chanServCheck($chan) called.", 2 );
428
429     if ( &IsParam('nickServ_pass') and !$nickserv ) {
430         $conn->who('NickServ');
431         return 0;
432     }
433
434     # check for first hash then for next hash.
435     # TODO: a function for &ischanop()? &isvoice()?
436     if (    exists $channels{ lc $chan }
437         and exists $channels{ lc $chan }{'o'}{$ident} )
438     {
439         return 0;
440     }
441
442     &status("ChanServ ==> Requesting ops for $chan. (chanServCheck)");
443     &msg( 'ChanServ', "OP $chan" );
444     return 1;
445 }
446
447 1;
448
449 # vim:ts=4:sw=4:expandtab:tw=80