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