2 # DynaConfig.pl: Read/Write configuration files dynamically.
4 # Version: v0.1 (20010120)
6 # NOTE: Merged from User.pl
11 use vars qw(%chanconf %cache %bans %channels %nuh %users %ignore
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);
19 ##### USERFILE CONFIGURATION READER/WRITER
23 my $f = "$bot_state_dir/blootbot.users";
26 &DEBUG("userfile not found; new fresh run detected.");
30 if ( -f $f and -f "$f~") {
35 &FIXME("rUF: backup file bigger than current file.");
40 &ERROR("Cannot read userfile ($f): $!");
45 undef %users; # clear on reload.
47 undef %ignore; # reset.
51 &ERROR("old or invalid user file found.");
64 if (/^--(\S+)[\s\t]+(.*)$/) { # user: middle entry.
65 my ($what,$val) = ($1,$2);
67 if (!defined $val or $val eq "") {
68 &WARN("$what: val == NULL.");
73 &WARN("DynaConfig: invalid line: $_");
78 if ($what eq "HOSTS") {
79 $users{$nick}{$what}{$val} = 1;
81 $users{$nick}{$what} = $val;
84 } elsif (/^(\S+)$/) { # user: start entry.
87 } elsif (/^::(\S+) ignore$/) { # ignore: start entry.
91 } elsif (/^- (\S+):\+(\d+):\+(\d+):(\S+):(.*)$/ and $type eq "ignore") {
92 ### ignore: middle entry.
94 my(@array) = ($2,$3,$4,$5);
95 ### DEBUG purposes only!
96 if ($mask !~ /^$mask{nuh}$/) {
97 &WARN("ignore: mask $mask is invalid.");
100 $ignore{$chan}{$mask} = \@array;
102 } elsif (/^::(\S+) bans$/) { # bans: start entry.
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;
113 &WARN("unknown line: $_");
118 &status( sprintf("USERFILE: Loaded: %d users, %d bans, %d ignore",
119 scalar(keys %users)-1,
120 scalar(keys %bans), # ??
121 scalar(keys %ignore), # ??
127 if (!scalar keys %users) {
128 &DEBUG("wUF: nothing to write.");
132 if (!open OUT,">$bot_state_dir/blootbot.users") {
133 &ERROR("Cannot write userfile ($bot_state_dir/blootbot.users): $!");
137 my $time = scalar(gmtime);
139 print OUT "#v1: blootbot -- $ident -- written $time\n\n";
143 foreach (sort keys %users) {
146 my $count = scalar keys %{ $users{$user} };
148 &WARN("user $user has no other attributes; skipping.");
154 foreach (sort keys %{ $users{$user} }) {
156 my $val = $users{$user}{$_};
158 if (ref($val) eq "HASH") {
159 foreach (sort keys %{ $users{$user}{$_} }) {
160 print OUT "--$what\t\t$_\n";
163 } elsif ($_ eq 'FLAGS') {
164 print OUT "--$_\t\t" . join('', sort split('', $val)) . "\n";
166 print OUT "--$_\t\t$val\n";
174 foreach (keys %bans) {
178 my $count = scalar keys %{ $bans{$chan} };
180 &WARN("bans: chan $chan has no other attributes; skipping.");
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.");
193 printf OUT "- %s:+%d:+%d:%d:%s:%s\n", $_, @array;
196 print OUT "\n" if ($cbans);
200 foreach (keys %ignore) {
204 my $count = scalar keys %{ $ignore{$chan} };
206 &WARN("ignore: chan $chan has no other attributes; skipping.");
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.");
220 printf OUT "- %s:+%d:+%d:%s:%s\n", $_, @array;
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...");
234 ##### CHANNEL CONFIGURATION READER/WRITER
238 my $f = "$bot_state_dir/blootbot.chan";
239 if ( -f $f and -f "$f~") {
244 &FIXME("rCF: backup file bigger than current file.");
249 &ERROR("Cannot read chanfile ($f): $!");
253 undef %chanconf; # reset.
255 $_ = <IN>; # version string.
262 next if /^\// or /^\;/; # / or ; are comment lines.
268 next unless (defined $chan);
270 if (/^[\s\t]+\+(\S+)$/) { # bool, true.
271 $chanconf{$chan}{$1} = 1;
273 } elsif (/^[\s\t]+\-(\S+)$/) { # bool, false.
274 # although this is supported in run-time configuration.
275 $chanconf{$chan}{$1} = 0;
277 } elsif (/^[\s\t]+(\S+)[\s\t]+(.*)$/) {# what = val.
278 $chanconf{$chan}{$1} = $2;
281 &WARN("unknown line: $_") unless (/^#/);
286 # verify configuration
287 ### TODO: check against valid params.
288 foreach $chan (keys %chanconf) {
289 foreach (keys %{ $chanconf{$chan} }) {
292 &WARN("invalid param: chanconf{$chan}{$_}; removing.");
293 delete $chanconf{$chan}{$_};
294 undef $chanconf{$chan}{$_};
298 &status("CHANFILE: Loaded: ".(scalar(keys %chanconf)-1)." chans");
302 if (!scalar keys %chanconf) {
303 &DEBUG("wCF: nothing to write.");
307 if (!open OUT,">$bot_state_dir/blootbot.chan") {
308 &ERROR("Cannot write chanfile ($bot_state_dir/blootbot.chan): $!");
312 my $time = scalar(gmtime);
313 print OUT "#v1: blootbot -- $ident -- written $time\n\n";
317 ### Process 1: if defined in _default, remove same definition
318 ### from non-default channels.
319 foreach (keys %{ $chanconf{_default} }) {
321 my $val = $chanconf{_default}{$opt};
324 foreach (keys %chanconf) {
327 next if ($chan eq "_default");
328 next unless (exists $chanconf{$chan}{$opt});
329 next unless ($val eq $chanconf{$chan}{$opt});
332 delete $chanconf{$chan}{$opt};
336 &DEBUG("Removed config $opt to @chans since it's defiend in '_default'");
340 ### Process 2: if defined in all chans but _default, set in
341 ### _default and remove all others.
342 my (%optsval, %opts);
343 foreach (keys %chanconf) {
345 next if ($chan eq "_default");
348 foreach (keys %{ $chanconf{$chan} }) {
350 if (exists $optsval{$opt} and $optsval{$opt} eq $chanconf{$chan}{$opt}) {
354 $optsval{$opt} = $chanconf{$chan}{$opt};
359 foreach (keys %opts) {
360 next unless ($opts{$_} > 2);
361 &DEBUG(" opts{$_} => $opts{$_}");
364 ### other optimizations are in UserDCC.pl
368 foreach (sort keys %chanconf) {
373 foreach (sort keys %{ $chanconf{$chan} }) {
374 my $val = $chanconf{$chan}{$_};
376 if ($val =~ /^0$/) { # bool, false.
379 } elsif ($val =~ /^1$/) { # bool, true.
382 } else { # what = val.
383 print OUT " $_ $val\n";
393 $wtime_chanfile = time();
394 &status("--- Saved CHANFILE (".scalar(keys %chanconf).
397 if (defined $msgType and $msgType =~ /^chat$/) {
398 &performStrictReply("--- Writing chan file...");
406 # TODO: support multiple flags.
407 # TODO: return all flags for opers
410 my ($ret, $f, $o) = "";
412 &verifyUser($who, $nuh);
414 foreach $f (split //, $users{$userHandle}{FLAGS}) {
415 foreach $o ( split //, $flags ) {
416 next unless ($f eq $o);
427 my ($nick, $lnuh) = @_;
430 if ($userHandle = $dcc{'CHATvrfy'}{$who}) {
431 &VERB("vUser: cached auth for $who.",2);
437 foreach $user (keys %users) {
438 next if ($user eq "_default");
440 foreach $m (keys %{ $users{$user}{HOSTS} }) {
443 $m =~ s/([\@\(\)\[\]])/\\$1/g;
445 next unless ($lnuh =~ /^$m$/i);
447 if ($user !~ /^\Q$nick\E$/i and !exists $cache{VUSERWARN}{$user}) {
448 &status("vU: host matched but diff nick ($nick != $user).");
449 $cache{VUSERWARN}{$user} = 1;
456 last if ($userHandle ne "");
458 if ($user =~ /^\Q$nick\E$/i and !exists $cache{VUSERWARN}{$user}) {
459 &status("vU: nick matched but host is not in list ($lnuh).");
460 $cache{VUSERWARN}{$user} = 1;
464 $userHandle ||= "_default";
465 # what's talkchannel for?
466 $talkWho{$talkchannel} = $who if (defined $talkchannel);
473 # returns true if arg1 encrypts to arg2
474 my ($plain, $encrypted) = @_;
475 if ($encrypted eq "") {
476 ($plain, $encrypted) = split(/\s+/, $plain, 2);
478 return 0 unless ($plain ne "" and $encrypted ne "");
480 # MD5 // DES. Bobby Billingsley++.
482 if ($encrypted =~ /^(\S{2})/ and length $encrypted == 13) {
484 } elsif ($encrypted =~ /^\$\d\$(\w\w)\$/) {
487 &DEBUG("unknown salt from $encrypted.");
491 return ($encrypted eq crypt($plain, $salt));
494 # mainly for dcc chat... hrm.
498 if (&IsFlag($flag) eq $flag) {
501 &status("DCC CHAT: <$who> $message -- not enough flags.");
502 &performStrictReply("error: you do not have enough flags for that. ($flag required)");
507 # expire is time in minutes
509 my($mask,$chan,$expire,$comment) = @_;
511 $chan ||= "*"; # global if undefined.
512 $comment ||= ""; # optional.
513 $expire ||= 0; # permament.
517 $expire = ($expire*60) + time();
523 $exist++ if (exists $ignore{$chan}{$mask});
525 $ignore{$chan}{$mask} = [$expire, time(), $who, $comment];
527 # TODO: improve this.
529 &status("ignore: Added $mask for $chan to NEVER expire, by $who, for $comment");
531 &status("ignore: Added $mask for $chan to expire $expire mins, by $who, for $comment");
535 $utime_userfile = time();
548 ### TODO: support wildcards.
549 foreach (keys %ignore) {
552 foreach (grep /^\Q$mask\E$/i, keys %{ $ignore{$chan} }) {
553 delete $ignore{$chan}{$mask};
557 &DEBUG("iD: scalar => ".scalar(keys %{ $ignore{$chan} }) );
561 $utime_userfile = time();
569 my($nick,$mask) = @_;
571 if (exists $users{$nick}) {
575 $utime_userfile = time();
578 if (defined $mask and $mask !~ /^\s*$/) {
579 &DEBUG("userAdd: mask => $mask");
580 $users{$nick}{HOSTS}{$mask} = 1;
583 $users{$nick}{FLAGS} ||= $users{_default}{FLAGS};
591 if (!exists $users{$nick}) {
595 $utime_userfile = time();
598 delete $users{$nick};
604 my($mask,$chan,$expire,$reason) = @_;
610 $expire = $expire*60 + time();
614 $exist++ if (exists $bans{$chan}{$mask} or
615 exists $bans{'*'}{$mask});
616 $bans{$chan}{$mask} = [$expire, time(), 0, $who, $reason];
618 my @chans = ($chan eq "*") ? keys %channels : $chan;
624 foreach (keys %{ $channels{$chan}{''} }) {
625 next unless (exists $nuh{lc $_});
626 next unless ($nuh{lc $_} =~ /^$m$/i);
627 &FIXME("nuh{$_} =~ /$m/");
632 $utime_userfile = time();
643 foreach (keys %bans) {
646 foreach (grep /^\Q$mask\E$/i, keys %{ $bans{$chan} }) {
647 delete $bans{$chan}{$_};
651 &DEBUG("bans: scalar => ".scalar(keys %{ $bans{$chan} }) );
655 $utime_userfile = time();
665 if ( &getUser($user) ) {
675 if (!defined $user) {
676 &WARN("getUser: user == NULL.");
680 if (my @retval = grep /^\Q$user\E$/i, keys %users) {
681 if ($retval[0] ne $user) {
682 &WARN("getUser: retval[0] ne user ($retval[0] ne $user)");
684 my $count = scalar keys %{ $users{$retval[0]} };
685 &DEBUG("count => $count.");
694 my($cmd, $chan, $what, $val) = @_;
696 if ($cmd eq "+chan") {
697 if (exists $chanconf{$chan}) {
698 &performStrictReply("chan $chan already exists.");
701 $chanconf{$chan}{_time_added} = time();
702 $chanconf{$chan}{autojoin} = $conn->nick();
704 &performStrictReply("Joining $chan...");
710 if (!exists $chanconf{$chan}) {
711 &performStrictReply("no such channel $chan");
717 if (defined $what and $what =~ s/^([+-])(\S+)/$2/) {
719 ### ".chanset +blah 10" -- error.
721 my $set = ($1 eq "+") ? 1 : 0;
722 my $was = $chanconf{$chan}{$what};
724 if ($set) { # add/set.
725 if (defined $was and $was eq "1") {
726 &performStrictReply("setting $what for $chan already 1.");
732 } else { # delete/unset.
734 &performStrictReply("setting $what for $chan is not set.");
741 # alter for cosmetic (print out) reasons only.
742 $was = (defined $was) ? "; was '$was'" : "";
745 &performStrictReply("Unsetting $what for $chan$was.");
746 delete $chanconf{$chan}{$what};
748 &performStrictReply("Setting $what for $chan to '$val'$was.");
749 $chanconf{$chan}{$what} = $val;
754 } elsif (defined $val) {
755 ### ".chanset blah testing"
757 my $was = $chanconf{$chan}{$what};
758 if (defined $was and $was eq $val) {
759 &performStrictReply("setting $what for $chan already '$val'.");
762 $was = ($was) ? "; was '$was'" : "";
763 &performStrictReply("Setting $what for $chan to '$val'$was.");
765 $chanconf{$chan}{$what} = $val;
769 } else { # read only.
773 if (!defined $what) {
774 &WARN("chanset/DC: what == undefine.");
778 if (exists $chanconf{$chan}{$what}) {
779 &performStrictReply("$what for $chan is '$chanconf{$chan}{$what}'");
781 &performStrictReply("$what for $chan is not set.");
786 $utime_chanfile = time();
794 # this is an attempt to fix where an option is enabled but the module
795 # has been not loaded. it also can be used for other things.
797 foreach (keys %{ $cache{confvars} }) {
798 my $i = $cache{confvars}{$_};
799 &DEBUG("rehashConfVars: _ => $_");
801 if (/^news$/ and $i) {
802 &loadMyModule('News');
803 delete $cache{confvars}{$_};
806 if (/^uptime$/ and $i) {
807 &loadMyModule('Uptime');
808 delete $cache{confvars}{$_};
811 if (/^rootwarn$/i and $i) {
812 &loadMyModule('RootWarn');
813 delete $cache{confvars}{$_};
816 if (/^onjoin$/i and $i) {
817 &loadMyModule('OnJoin');
818 delete $cache{confvars}{$_};
822 &DEBUG("end of rehashConfVars");
824 delete $cache{confvars};
828 # possible chars to include in FLAG
829 "A", # bot administration over /msg
830 # default is only via DCC CHAT
831 "O", # dynamic ops (as on channel). (automatic +o)
833 "a", # ask/request factoid.
834 "m", # modify factoid. (includes renaming)
835 "n", # bot owner, can "reload"
836 "o", # master of bot (automatic +amrt)
837 # can search on factoid strings shorter than 2 chars
838 # can tell bot to join new channels
839 # can [un]lock factoids
840 "r", # remove factoid.
841 "t", # teach/add factoid.