]> git.donarmstrong.com Git - infobot.git/blob - src/DynaConfig.pl
root[wW]arn -> RootWarn
[infobot.git] / src / DynaConfig.pl
1 #
2 # DynaConfig.pl: Read/Write configuration files dynamically.
3 #        Author: dms
4 #       Version: v0.1 (20010120)
5 #       Created: 20010119
6 #          NOTE: Merged from User.pl
7 #
8
9 use strict;
10
11 use vars qw(%chanconf %cache %bans %channels %nuh %users %ignore
12         %talkWho %dcc %mask);
13 use vars qw($utime_userfile $ucount_userfile $utime_chanfile $who
14         $ucount_chanfile $userHandle $chan $msgType $talkchannel
15         $ident $bot_state_dir $talkWho $flag_quit $wtime_userfile
16         $wcount_userfile $wtime_chanfile $nuh $message);
17
18 #####
19 ##### USERFILE CONFIGURATION READER/WRITER
20 #####
21
22 sub readUserFile {
23     my $f = "$bot_state_dir/blootbot.users";
24
25     if (! -f $f) {
26         &DEBUG("userfile not found; new fresh run detected.");
27         return;
28     }
29
30     if ( -f $f and -f "$f~") {
31         my $s1 = -s $f;
32         my $s2 = -s "$f~";
33
34         if ($s2 > $s1*3) {
35             &FIXME("rUF: backup file bigger than current file.");
36         }
37     }
38
39     if (!open IN, $f) {
40         &ERROR("Cannot read userfile ($f): $!");
41         &closeLog();
42         exit 1;
43     }
44
45     undef %users;       # clear on reload.
46     undef %bans;        # reset.
47     undef %ignore;      # reset.
48
49     my $ver = <IN>;
50     if ($ver !~ /^#v1/) {
51         &ERROR("old or invalid user file found.");
52         &closeLog();
53         exit 1; # correct?
54     }
55
56     my $nick;
57     my $type;
58     while (<IN>) {
59         chop;
60
61         next if /^$/;
62         next if /^#/;
63
64         if (/^--(\S+)[\s\t]+(.*)$/) {           # user: middle entry.
65             my ($what,$val) = ($1,$2);
66
67             if (!defined $val or $val eq "") {
68                 &WARN("$what: val == NULL.");
69                 next;
70             }
71
72             if (!defined $nick) {
73                 &WARN("DynaConfig: invalid line: $_");
74                 next;
75             }
76
77             # nice little hack.
78             if ($what eq "HOSTS") {
79                 $users{$nick}{$what}{$val} = 1;
80             } else {
81                 $users{$nick}{$what} = $val;
82             }
83
84         } elsif (/^(\S+)$/) {                   # user: start entry.
85             $nick       = $1;
86
87         } elsif (/^::(\S+) ignore$/) {          # ignore: start entry.
88             $chan       = $1;
89             $type       = "ignore";
90
91         } elsif (/^- (\S+):\+(\d+):\+(\d+):(\S+):(.*)$/ and $type eq "ignore") {
92             ### ignore: middle entry.
93             my $mask = $1;
94             my(@array) = ($2,$3,$4,$5);
95             ### DEBUG purposes only!
96             if ($mask !~ /^$mask{nuh}$/) {
97                 &WARN("ignore: mask $mask is invalid.");
98                 next;
99             }
100             $ignore{$chan}{$mask} = \@array;
101
102         } elsif (/^::(\S+) bans$/) {            # bans: start entry.
103             $chan       = $1;
104             $type       = "bans";
105
106         } elsif (/^- (\S+):\+(\d+):\+(\d+):(\d+):(\S+):(.*)$/ and $type eq "bans") {
107             ### bans: middle entry.
108             # $btime, $atime, $count, $whoby, $reason.
109             my(@array) = ($2,$3,$4,$5,$6);
110             $bans{$chan}{$1} = \@array;
111
112         } else {                                # unknown.
113             &WARN("unknown line: $_");
114         }
115     }
116     close IN;
117
118     &status( sprintf("USERFILE: Loaded: %d users, %d bans, %d ignore",
119                 scalar(keys %users)-1,
120                 scalar(keys %bans),             # ??
121                 scalar(keys %ignore),           # ??
122         )
123     );
124 }
125
126 sub writeUserFile {
127     if (!scalar keys %users) {
128         &DEBUG("wUF: nothing to write.");
129         return;
130     }
131
132     if (!open OUT,">$bot_state_dir/blootbot.users") {
133         &ERROR("Cannot write userfile ($bot_state_dir/blootbot.users): $!");
134         return;
135     }
136
137     my $time            = scalar(gmtime);
138
139     print OUT "#v1: blootbot -- $ident -- written $time\n\n";
140
141     ### USER LIST.
142     my $cusers  = 0;
143     foreach (sort keys %users) {
144         my $user = $_;
145         $cusers++;
146         my $count = scalar keys %{ $users{$user} };
147         if (!$count) {
148             &WARN("user $user has no other attributes; skipping.");
149             next;
150         }
151
152         print OUT "$user\n";
153
154         foreach (sort keys %{ $users{$user} }) {
155             my $what    = $_;
156             my $val     = $users{$user}{$_};
157
158             if (ref($val) eq "HASH") {
159                 foreach (sort keys %{ $users{$user}{$_} }) {
160                     print OUT "--$what\t\t$_\n";
161                 }
162
163             } else {
164                 print OUT "--$_\t\t$val\n";
165             }
166         }
167         print OUT "\n";
168     }
169
170     ### BAN LIST.
171     my $cbans   = 0;
172     foreach (keys %bans) {
173         my $chan = $_;
174         $cbans++;
175
176         my $count = scalar keys %{ $bans{$chan} };
177         if (!$count) {
178             &WARN("bans: chan $chan has no other attributes; skipping.");
179             next;
180         }
181
182         print OUT "::$chan bans\n";
183         foreach (keys %{ $bans{$chan} }) {
184 # format: bans: mask expire time-added count who-added reason
185             my @array = @{ $bans{$chan}{$_} };
186             if (scalar @array != 5) {
187                 &WARN("bans: $chan/$_ is corrupted.");
188                 next;
189             }
190
191             printf OUT "- %s:+%d:+%d:%d:%s:%s\n", $_, @array;
192         }
193     }
194     print OUT "\n" if ($cbans);
195
196     ### IGNORE LIST.
197     my $cignore = 0;
198     foreach (keys %ignore) {
199         my $chan = $_;
200         $cignore++;
201
202         my $count = scalar keys %{ $ignore{$chan} };
203         if (!$count) {
204             &WARN("ignore: chan $chan has no other attributes; skipping.");
205             next;
206         }
207
208         ### TODO: use hash instead of array for flexibility?
209         print OUT "::$chan ignore\n";
210         foreach (keys %{ $ignore{$chan} }) {
211 # format: ignore: mask expire time-added who-added reason
212             my @array = @{ $ignore{$chan}{$_} };
213             if (scalar @array != 4) {
214                 &WARN("ignore: $chan/$_ is corrupted.");
215                 next;
216             }
217
218             printf OUT "- %s:+%d:+%d:%s:%s\n", $_, @array;
219         }
220     }
221
222     close OUT;
223
224     $wtime_userfile = time();
225     &status("--- Saved USERFILE ($cusers users; $cbans bans; $cignore ignore) at $time");
226     if (defined $msgType and $msgType =~ /^chat$/) {
227         &pSReply("--- Writing user file...");
228     }
229 }
230
231 #####
232 ##### CHANNEL CONFIGURATION READER/WRITER
233 #####
234
235 sub readChanFile {
236     my $f = "$bot_state_dir/blootbot.chan";
237     if ( -f $f and -f "$f~") {
238         my $s1 = -s $f;
239         my $s2 = -s "$f~";
240
241         if ($s2 > $s1*3) {
242             &FIXME("rCF: backup file bigger than current file.");
243         }
244     }
245
246     if (!open IN, $f) {
247         &ERROR("Cannot read chanfile ($f): $!");
248         return;
249     }
250
251     undef %chanconf;    # reset.
252
253     $_ = <IN>;          # version string.
254
255     my $chan;
256     while (<IN>) {
257         chop;
258
259         next if /^\s*$/;
260         next if /^\// or /^\;/; # / or ; are comment lines.
261
262         if (/^(\S+)\s*$/) {
263             $chan       = $1;
264             next;
265         }
266         next unless (defined $chan);
267
268         if (/^[\s\t]+\+(\S+)$/) {               # bool, true.
269             $chanconf{$chan}{$1} = 1;
270
271         } elsif (/^[\s\t]+\-(\S+)$/) {          # bool, false.
272             &DEBUG("deprecated support of negative options.") unless ($cache{negative});
273             # although this is supported in run-time configuration.
274             $cache{negative} = 1;
275 #           $chanconf{$chan}{$1} = 0;
276
277         } elsif (/^[\s\t]+(\S+)[\ss\t]+(.*)$/) {# what = val.
278             $chanconf{$chan}{$1} = $2;
279
280         } else {
281             &WARN("unknown line: $_") unless (/^#/);
282         }
283     }
284     close IN;
285
286     # verify configuration
287     ### TODO: check against valid params.
288     foreach $chan (keys %chanconf) {
289         foreach (keys %{ $chanconf{$chan} }) {
290             next unless /^[+-]/;
291
292             &WARN("invalid param: chanconf{$chan}{$_}; removing.");
293             delete $chanconf{$chan}{$_};
294             undef $chanconf{$chan}{$_};
295         }
296     }
297
298     delete $cache{negative};
299
300     &status("CHANFILE: Loaded: ".(scalar(keys %chanconf)-1)." chans");
301 }
302
303 sub writeChanFile {
304     if (!scalar keys %chanconf) {
305         &DEBUG("wCF: nothing to write.");
306         return;
307     }
308
309     if (!open OUT,">$bot_state_dir/blootbot.chan") {
310         &ERROR("Cannot write chanfile ($bot_state_dir/blootbot.chan): $!");
311         return;
312     }
313
314     my $time            = scalar(gmtime);
315     print OUT "#v1: blootbot -- $ident -- written $time\n\n";
316
317     if ($flag_quit) {
318
319         ### Process 1: if defined in _default, remove same definition
320         ###             from non-default channels.
321         foreach (keys %{ $chanconf{_default} }) {
322             my $opt     = $_;
323             my $val     = $chanconf{_default}{$opt};
324             my @chans;
325
326             foreach (keys %chanconf) {
327                 $chan = $_;
328
329                 next if ($chan eq "_default");
330                 next unless (exists $chanconf{$chan}{$opt});
331                 next unless ($val eq $chanconf{$chan}{$opt});
332
333                 push(@chans,$chan);
334                 delete $chanconf{$chan}{$opt};
335             }
336
337             if (scalar @chans) {
338                 &DEBUG("Removed config $opt to @chans since it's defiend in '_default'");
339             }
340         }
341
342         ### Process 2: if defined in all chans but _default, set in
343         ###             _default and remove all others.
344         my (%optsval, %opts);
345         foreach (keys %chanconf) {
346             $chan = $_;
347             next if ($chan eq "_default");
348             my $opt;
349
350             foreach (keys %{ $chanconf{$chan} }) {
351                 $opt = $_;
352                 if (exists $optsval{$opt} and $optsval{$opt} eq $chanconf{$chan}{$opt}) {
353                     $opts{$opt}++;
354                     next;
355                 }
356                 $optsval{$opt}  = $chanconf{$chan}{$opt};
357                 $opts{$opt}     = 1;
358             }
359         }
360
361         foreach (keys %opts) {
362             next unless ($opts{$_} > 2);
363             &DEBUG("  opts{$_} => $opts{$_}");
364         }
365
366         ### other optimizations are in UserDCC.pl
367     }
368
369     ### lets do it...
370     foreach (sort keys %chanconf) {
371         $chan   = $_;
372
373         print OUT "$chan\n";
374
375         foreach (sort keys %{ $chanconf{$chan} }) {
376             my $val = $chanconf{$chan}{$_};
377
378             if ($val =~ /^0$/) {                # bool, false.
379                 print OUT "    -$_\n";
380
381             } elsif ($val =~ /^1$/) {           # bool, true.
382                 print OUT "    +$_\n";
383
384             } else {                            # what = val.
385                 print OUT "    $_ $val\n";
386
387             }
388
389         }
390         print OUT "\n";
391     }
392
393     close OUT;
394
395     $wtime_chanfile = time();
396     &status("--- Saved CHANFILE (".scalar(keys %chanconf).
397                 " chans) at $time");
398
399     if (defined $msgType and $msgType =~ /^chat$/) {
400         &pSReply("--- Writing chan file...");
401     }
402 }
403
404 #####
405 ##### USER COMMANDS.
406 #####
407
408 # TODO: support multiple flags.
409 # TODO: return all flags for opers
410 sub IsFlag {
411     my $flags = shift;
412     my ($ret, $f, $o) = "";
413
414     &verifyUser($who, $nuh);
415
416     foreach $f (split //, $users{$userHandle}{FLAGS}) {
417         foreach $o ( split //, $flags ) {
418             next unless ($f eq $o);
419
420             $ret = $f;
421             last;
422         }
423     }
424
425     $ret;
426 }
427
428 sub verifyUser {
429     my ($nick, $lnuh) = @_;
430     my ($user, $m);
431
432     if ($userHandle = $dcc{'CHATvrfy'}{$who}) {
433         &VERB("vUser: cached auth for $who.",2);
434         return $userHandle;
435     }
436
437     $userHandle = "";
438
439     foreach $user (keys %users) {
440         next if ($user eq "_default");
441
442         foreach $m (keys %{ $users{$user}{HOSTS} }) {
443             $m =~ s/\?/./g;
444             $m =~ s/\*/.*?/g;
445             $m =~ s/([\@\(\)\[\]])/\\$1/g;
446
447             next unless ($lnuh =~ /^$m$/i);
448
449             if ($user !~ /^\Q$nick\E$/i and !exists $cache{VUSERWARN}{$user}) {
450                 &status("vU: host matched but diff nick ($nick != $user).");
451                 $cache{VUSERWARN}{$user} = 1;
452             }
453
454             $userHandle = $user;
455             last;
456         }
457
458         last if ($userHandle ne "");
459
460         if ($user =~ /^\Q$nick\E$/i and !exists $cache{VUSERWARN}{$user}) {
461             &status("vU: nick matched but host is not in list ($lnuh).");
462             $cache{VUSERWARN}{$user} = 1;
463         }
464     }
465
466     $userHandle ||= "_default";
467     # what's talkchannel for?
468     $talkWho{$talkchannel} = $who if (defined $talkchannel);
469     $talkWho = $who;
470
471     return $userHandle;
472 }
473
474 sub ckpasswd {
475     # returns true if arg1 encrypts to arg2
476     my ($plain, $encrypted) = @_;
477     if ($encrypted eq "") {
478         ($plain, $encrypted) = split(/\s+/, $plain, 2);
479     }
480     return 0 unless ($plain ne "" and $encrypted ne "");
481
482     # MD5 // DES. Bobby Billingsley++.
483     my $salt;
484     if ($encrypted =~ /^(\S{2})/ and length $encrypted == 13) {
485         $salt = $1;
486     } elsif ($encrypted =~ /^\$\d\$(\w\w)\$/) {
487         $salt = $1;
488     } else {
489         &DEBUG("unknown salt from $encrypted.");
490         return 0;
491     }
492
493     return ($encrypted eq crypt($plain, $salt));
494 }
495
496 # mainly for dcc chat... hrm.
497 sub hasFlag {
498     my ($flag) = @_;
499
500     if (&IsFlag($flag) eq $flag) {
501         return 1;
502     } else {
503         &status("DCC CHAT: <$who> $message -- not enough flags.");
504         &pSReply("error: you do not have enough flags for that. ($flag required)");
505         return 0;
506     }
507 }
508
509 # expire is time in minutes
510 sub ignoreAdd {
511     my($mask,$chan,$expire,$comment) = @_;
512
513     $chan       ||= "*";        # global if undefined.
514     $comment    ||= "";         # optional.
515     $expire     ||= 0;          # permament.
516     my $count   ||= 0;
517
518     if ($expire > 0) {
519         $expire         = ($expire*60) + time();
520     } else {
521         $expire         = 0;
522     }
523
524     my $exist   = 0;
525     $exist++ if (exists $ignore{$chan}{$mask});
526
527     $ignore{$chan}{$mask} = [$expire, time(), $who, $comment];
528
529     # TODO: improve this.
530     if ($expire == 0) {
531         &status("ignore: Added $mask for $chan to NEVER expire, by $who, for $comment");
532     } else {
533         &status("ignore: Added $mask for $chan to expire $expire mins, by $who, for $comment");
534     }
535
536     if ($exist) {
537         $utime_userfile = time();
538         $ucount_userfile++;
539
540         return 2;
541     } else {
542         return 1;
543     }
544 }
545
546 sub ignoreDel {
547     my($mask)   = @_;
548     my @match;
549
550     ### TODO: support wildcards.
551     foreach (keys %ignore) {
552         my $chan = $_;
553
554         foreach (grep /^\Q$mask\E$/i, keys %{ $ignore{$chan} }) {
555             delete $ignore{$chan}{$mask};
556             push(@match,$chan);
557         }
558
559         &DEBUG("iD: scalar => ".scalar(keys %{ $ignore{$chan} }) );
560     }
561
562     if (scalar @match) {
563         $utime_userfile = time();
564         $ucount_userfile++;
565     }
566
567     return @match;
568 }
569
570 sub userAdd {
571     my($nick,$mask)     = @_;
572
573     if (exists $users{$nick}) {
574         return 0;
575     }
576
577     $utime_userfile = time();
578     $ucount_userfile++;
579
580     if (defined $mask and $mask !~ /^\s*$/) {
581         &DEBUG("userAdd: mask => $mask");
582         $users{$nick}{HOSTS}{$mask} = 1;
583     }
584
585     $users{$nick}{FLAGS}        ||= $users{_default}{FLAGS};
586
587     return 1;
588 }
589
590 sub userDel {
591     my($nick)   = @_;
592
593     if (!exists $users{$nick}) {
594         return 0;
595     }
596
597     $utime_userfile = time();
598     $ucount_userfile++;
599
600     delete $users{$nick};
601
602     return 1;
603 }
604
605 sub banAdd {
606     my($mask,$chan,$expire,$reason) = @_;
607
608     $chan       ||= "*";
609     $expire     ||= 0;
610
611     if ($expire > 0) {
612         $expire         = $expire*60 + time();
613     }
614
615     my $exist   = 1;
616     $exist++ if (exists $bans{$chan}{$mask} or
617                 exists $bans{'*'}{$mask});
618     $bans{$chan}{$mask} = [$expire, time(), 0, $who, $reason];
619
620     my @chans   = ($chan eq "*") ? keys %channels : $chan;
621     my $m       = $mask;
622     $m          =~ s/\?/\\./g;
623     $m          =~ s/\*/\\S*/g;
624     foreach (@chans) {
625         my $chan = $_;
626         foreach (keys %{ $channels{$chan}{''} }) {
627             next unless (exists $nuh{lc $_});
628             next unless ($nuh{lc $_} =~ /^$m$/i);
629             &FIXME("nuh{$_} =~ /$m/");
630         }
631     }
632
633     if ($exist == 1) {
634         $utime_userfile = time();
635         $ucount_userfile++;
636     }
637
638     return $exist;
639 }
640
641 sub banDel {
642     my($mask)   = @_;
643     my @match;
644
645     foreach (keys %bans) {
646         my $chan        = $_;
647
648         foreach (grep /^\Q$mask\E$/i, keys %{ $bans{$chan} }) {
649             delete $bans{$chan}{$_};
650             push(@match, $chan);
651         }
652
653         &DEBUG("bans: scalar => ".scalar(keys %{ $bans{$chan} }) );
654     }
655
656     if (scalar @match) {
657         $utime_userfile = time();
658         $ucount_userfile++;
659     }
660
661     return @match;
662 }
663
664 sub IsUser {
665     my($user) = @_;
666
667     if ( &getUser($user) ) {
668         return 1;
669     } else {
670         return 0;
671     }
672 }
673
674 sub getUser {
675     my($user) = @_;
676
677     if (!defined $user) {
678         &WARN("getUser: user == NULL.");
679         return;
680     }
681
682     if (my @retval = grep /^\Q$user\E$/i, keys %users) {
683         if ($retval[0] ne $user) {
684             &WARN("getUser: retval[0] ne user ($retval[0] ne $user)");
685         }
686         my $count = scalar keys %{ $users{$retval[0]} };
687         &DEBUG("count => $count.");
688
689         return $retval[0];
690     } else {
691         return;
692     }
693 }
694
695 sub chanSet {
696     my($cmd, $chan, $what, $val) = @_;
697
698     if ($cmd eq "+chan") {
699         if (exists $chanconf{$chan}) {
700             &pSReply("chan $chan already exists.");
701             return;
702         }
703         $chanconf{$chan}{_time_added}   = time();
704         $chanconf{$chan}{autojoin}      = $conn->nick();
705
706         &pSReply("Joining $chan...");
707         &joinchan($chan);
708
709         return;
710     }
711
712     if (!exists $chanconf{$chan}) {
713         &pSReply("no such channel $chan");
714         return;
715     }
716
717     my $update  = 0;
718
719     if (defined $what and $what =~ s/^([+-])(\S+)/$2/) {
720         ### ".chanset +blah"
721         ### ".chanset +blah 10"         -- error.
722
723         my $state       = ($1 eq "+") ? 1 : 0;
724         my $was         = $chanconf{$chan}{$what};
725
726         if ($state) {                   # add/set.
727             if (defined $was and $was eq "1") {
728                 &pSReply("setting $what for $chan already 1.");
729                 return;
730             }
731
732             $val        = 1;
733
734         } else {                        # delete/unset.
735             if (!defined $was) {
736                 &pSReply("setting $what for $chan is not set.");
737                 return;
738             }
739
740             if ($was eq "0") {
741                 &pSReply("setting $what for $chan already 0.");
742                 return;
743             }
744
745             $val        = 0;
746         }
747
748         # alter for cosmetic (print out) reasons only.
749         $was    = ($was) ? "; was '$was'" : "";
750
751         if ($val eq "0") {
752             &pSReply("Unsetting $what for $chan$was.");
753             delete $chanconf{$chan}{$what};
754         } else {
755             &pSReply("Setting $what for $chan to '$val'$was.");
756             $chanconf{$chan}{$what}     = $val;
757         }
758
759         $update++;
760
761     } elsif (defined $val) {
762         ### ".chanset blah testing"
763
764         my $was = $chanconf{$chan}{$what};
765         if (defined $was and $was eq $val) {
766             &pSReply("setting $what for $chan already '$val'.");
767             return;
768         }
769         $was    = ($was) ? "; was '$was'" : "";
770         &pSReply("Setting $what for $chan to '$val'$was.");
771
772         $chanconf{$chan}{$what} = $val;
773
774         $update++;
775
776     } else {                            # read only.
777         ### ".chanset"
778         ### ".chanset blah"
779
780         if (!defined $what) {
781             &WARN("chanset/DC: what == undefine.");
782             return;
783         }
784
785         if (exists $chanconf{$chan}{$what}) {
786             &pSReply("$what for $chan is '$chanconf{$chan}{$what}'");
787         } else {
788             &pSReply("$what for $chan is not set.");
789         }
790     }
791
792     if ($update) {
793         $utime_chanfile = time();
794         $ucount_chanfile++;
795     }
796
797     return;
798 }
799
800 sub rehashConfVars {
801     # this is an attempt to fix where an option is enabled but the module
802     # has been not loaded. it also can be used for other things.
803
804     foreach (keys %{ $cache{confvars} }) {
805         my $i = $cache{confvars}{$_};
806         &DEBUG("rehashConfVars: _ => $_");
807
808         if (/^news$/ and $i) {
809             &loadMyModule('News');
810             delete $cache{confvars}{$_};
811         }
812
813         if (/^uptime$/ and $i) {
814             &loadMyModule('uptime');
815             delete $cache{confvars}{$_};
816         }
817
818         if (/^rootwarn$/i and $i) {
819             &loadMyModule('RootWarn');
820             delete $cache{confvars}{$_};
821         }
822     }
823
824     &DEBUG("end of rehashConfVars");
825
826     delete $cache{confvars};
827 }
828
829 # registered flags... not used yet.
830 my @regFlagsChan = (
831         "autojoin",
832         "limitcheckInterval",
833         "limitcheckPlus",
834         "allowConv",
835         "allowDNS",
836 ### TODO: finish off this list.
837 );
838
839 my @regFlagsUser = (
840         "m",    # modify factoid. (includes renaming)
841         "r",    # remove factoid.
842         "t",    # teach/add factoid.
843         "a",    # ask/request factoid.
844         "n",    # bot owner
845                         # can "reload"
846         "o",    # master of bot (automatic +amrt)
847                         # can search on factoid strings shorter than 2 chars
848                         # can tell bot to join new channels
849                         # can [un]lock factoids
850         "O",    # dynamic ops (as on channel). (automatic +o)
851         "A",    # bot administration over /msg
852                         # default is only via DCC CHAT
853         "T",    # add topics.
854 );
855
856 1;