Annotation of botnow/BNC.pm, Revision 1.1
1.1 ! bountyht 1: #!/usr/bin/perl
! 2:
! 3: package BNC;
! 4:
! 5: use strict;
! 6: use warnings;
! 7: use OpenBSD::Pledge;
! 8: use OpenBSD::Unveil;
! 9: use MIME::Base64;
! 10: use Digest::SHA qw(sha256_hex);
! 11: use lib './';
! 12: require "SQLite.pm";
! 13: require "Hash.pm";
! 14: require "DNS.pm";
! 15: require "Mail.pm";
! 16:
! 17: my %conf = %main::conf;
! 18: my $chans = $conf{chans};
! 19: my $teamchans = $conf{teamchans};
! 20: my @teamchans = split /[,\s]+/m, $teamchans;
! 21: my $staff = $conf{staff};
! 22: my $zncdir = $conf{zncdir};
! 23: my $znclog = $conf{znclog} || "$zncdir/.znc/moddata/adminlog/znc.log";
! 24: my $hostname = $conf{hostname};
! 25: my $terms = $conf{terms};
! 26: my @logs;
! 27: my $expires = $conf{expires};
! 28: my $sslport = $conf{sslport};
! 29: my $plainport = $conf{plainport};
! 30: my $mailfrom = $conf{mailfrom};
! 31: my $mailname = $conf{mailname};
! 32: my $zncconfpath = $conf{zncconfpath} || "$zncdir/.znc/configs/znc.conf";
! 33: my $znctree = { Node => "root" };
! 34:
! 35: use constant {
! 36: NONE => 0,
! 37: ERRORS => 1,
! 38: WARNINGS => 2,
! 39: ALL => 3,
! 40: };
! 41:
! 42: `doas chown znc:daemon /home/znc/home/znc/.znc/configs/znc.conf`;
! 43: `doas chmod g+r /home/znc/home/znc/.znc/`;
! 44: my @zncconf = main::readarray($zncconfpath);
! 45: $znctree;
! 46: my @users;
! 47: foreach my $line (@zncconf) {
! 48: if ($line =~ /<User (.*)>/) {
! 49: push(@users, $1);
! 50: }
! 51: }
! 52: #$znctree = parseml($znctree, @zncconf);
! 53: main::cbind("pub", "-", "bnc", \&mbnc);
! 54: main::cbind("msg", "-", "bnc", \&mbnc);
! 55: main::cbind("msg", "-", "regex", \&mregex);
! 56: main::cbind("msg", "-", "foreach", \&mforeach);
! 57: main::cbind("msgm", "-", "*", \&mcontrolpanel);
! 58: main::cbind("msg", "-", "taillog", \&mtaillog);
! 59: main::cbind("msg", "-", "lastseen", \&mlastseen);
! 60:
! 61: sub init {
! 62: #znc.conf file
! 63: unveil("$zncconfpath", "r") or die "Unable to unveil $!";
! 64: #dependencies for figlet
! 65: unveil("/usr/local/bin/figlet", "rx") or die "Unable to unveil $!";
! 66: unveil("/usr/lib/libc.so.95.1", "r") or die "Unable to unveil $!";
! 67: unveil("/usr/libexec/ld.so", "r") or die "Unable to unveil $!";
! 68: unveil("/usr/bin/tail", "rx") or die "Unable to unveil $!";
! 69: #znc.log file
! 70: unveil("$znclog", "r") or die "Unable to unveil $!";
! 71: #print treeget($znctree, "AnonIPLimit")."\n";
! 72: #print treeget($znctree, "ServerThrottle")."\n";
! 73: #print treeget($znctree, "ConnectDelay")."\n";
! 74: #print "treeget\n";
! 75: #print Dumper \treeget($znctree, "User", "Node");
! 76: #print Dumper \treeget($znctree, "User", "Network", "Node");
! 77: }
! 78:
! 79: # parseml($tree, @lines)
! 80: # tree is a reference to a hash
! 81: # returns hash ref of tree
! 82: sub parseml {
! 83: my ($tree, @lines) = @_;
! 84: #if (scalar(@lines) == 0) { return $tree; }
! 85: while (scalar(@lines) > 0) {
! 86: my $line = shift(@lines);
! 87: if ($line =~ /^\s*([^=<>\s]+)\s*=\s*([^=<>]+)\s*$/) {
! 88: my ($tag, $val) = ($1, $2);
! 89: $tree->{$tag} = $val;
! 90: } elsif ($line =~ /^\/\//) { # skip comments
! 91: } elsif ($line =~ /^\s*$/) { # skip blank lines
! 92: } elsif ($line =~ /^\s*<([^>\s\/]+)\s*([^>\/]*)>\s*$/) {
! 93: my ($tag, $val) = ($1, $2);
! 94: if (!defined($tree->{$tag})) { $tree->{$tag} = []; }
! 95: my @newlines;
! 96: while (scalar(@lines) > 0) {
! 97: my $line = shift(@lines);
! 98: if ($line =~ /^\s*<\/$tag>\s*$/) {
! 99: my $subtree = parseml({ Node => $val }, @newlines);
! 100: push(@{$tree->{$tag}}, $subtree);
! 101: return parseml($tree, @lines);
! 102: }
! 103: push(@newlines, $line);
! 104: }
! 105: } else { print "ERROR: $line\n"; }
! 106: #TODO ERRORS not defined??
! 107: # } else { main::debug(ERRORS, "ERROR: $line"); }
! 108: }
! 109: return $tree;
! 110: }
! 111:
! 112: #Returns array of all values
! 113: #treeget($tree, "User");
! 114: #treeget($tree, "MaFFia Network");
! 115: sub treeget {
! 116: my ($tree, @keys) = @_;
! 117: my $subtree;
! 118: my @rest = @keys;
! 119: my $key = shift(@rest);
! 120: $subtree = $tree->{$key};
! 121: if (!defined($subtree)) {
! 122: return ("Undefined");
! 123: } elsif (ref($subtree) eq 'HASH') {
! 124: return treeget($subtree, @rest);
! 125: } elsif (ref($subtree) eq 'ARRAY') {
! 126: my @array = @{$subtree};
! 127: my @ret;
! 128: foreach my $hashref (@array) {
! 129: push(@ret, treeget($hashref, @rest));
! 130: }
! 131: return @ret;
! 132: #my @array = @{$subtree};
! 133: #print Dumper treeget($hashref, @rest);
! 134: #print Dumper treeget({$key => $subtree}, @rest);
! 135: #return (treeget($hashref, @rest), treeget({$key => $subtree}, @rest));
! 136: } else {
! 137: return ($subtree);
! 138: }
! 139: }
! 140:
! 141: sub mbnc {
! 142: my ($bot, $nick, $host, $hand, @args) = @_;
! 143: my ($chan, $text);
! 144: if (@args == 2) {
! 145: ($chan, $text) = ($args[0], $args[1]);
! 146: } else { $text = $args[0]; }
! 147: my $hostmask = "$nick!$host";
! 148: if (defined($chan) && $chans =~ /$chan/) {
! 149: main::putserv($bot, "PRIVMSG $chan :$nick: Please check private message");
! 150: }
! 151: if ($text =~ /^$/) {
! 152: main::putserv($bot, "PRIVMSG $nick :Type !help for new instructions");
! 153: foreach my $chan (@teamchans) {
! 154: main::putservlocalnet($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
! 155: }
! 156: return;
! 157: } elsif (main::isstaff($bot, $nick) && $text =~ /^delete\s+([[:ascii:]]+)/) {
! 158: my $username = $1;
! 159: if (SQLite::deleterows("bnc", "username", $username)) {
! 160: main::putserv($bot, "PRIVMSG *controlpanel :deluser $username");
! 161: foreach my $chan (@teamchans) {
! 162: main::putserv($bot, "PRIVMSG $chan :$username deleted");
! 163: }
! 164: }
! 165: return;
! 166: } elsif ($staff =~ /$nick/ && $text =~ /^cloneuser$/i) {
! 167: main::putserv($bot, "PRIVMSG *controlpanel :deluser cloneuser");
! 168: sleep 3;
! 169: main::putserv($bot, "PRIVMSG *controlpanel :get Nick cloneuser");
! 170: }
! 171: ### TODO: Check duplicate emails ###
! 172: my @rows = SQLite::selectrows("irc", "hostmask", $hostmask);
! 173: foreach my $row (@rows) {
! 174: my $password = SQLite::get("bnc", "ircid", $row->{id}, "password");
! 175: if (defined($password)) {
! 176: main::putserv($bot, "PRIVMSG $nick :Sorry, only one account per person. Please contact staff if you need help.");
! 177: return;
! 178: }
! 179: }
! 180: if ($text =~ /^captcha\s+([[:alnum:]]+)/) {
! 181: my $text = $1;
! 182: # TODO avoid using host mask because cloaking can cause problems
! 183: my $ircid = SQLite::id("irc", "nick", $nick, $expires);
! 184: my $captcha = SQLite::get("bnc", "ircid", $ircid, "captcha");
! 185: if ($text ne $captcha) {
! 186: main::putserv($bot, "PRIVMSG $nick :Wrong captcha. To get a new captcha, type !bnc <username> <email>");
! 187: return;
! 188: }
! 189: my $pass = Hash::newpass();
! 190: chomp(my $encrypted = `encrypt $pass`);
! 191: my $username = SQLite::get("bnc", "ircid", $ircid, "username");
! 192: my $email = SQLite::get("bnc", "ircid", $ircid, "email");
! 193: my $hashirc = SQLite::get("irc", "id", $ircid, "hashid");
! 194: my $bindhost = "$username.$hostname";
! 195: SQLite::set("bnc", "ircid", $ircid, "password", $encrypted);
! 196: if (DNS::nextdns($username)) {
! 197: sleep(2);
! 198: createbnc($bot, $username, $pass, $bindhost);
! 199: main::putserv($bot, "PRIVMSG $nick :Check your email!");
! 200: mailbnc($username, $email, $pass, "bouncer", $hashirc);
! 201: #www($newnick, $reply, $password, "bouncer");
! 202: } else {
! 203: foreach my $chan (@teamchans) {
! 204: main::putserv($bot, "PRIVMSG $chan :Assigning bindhost $bindhost failed");
! 205: }
! 206: }
! 207: return;
! 208: } elsif ($text =~ /^([[:alnum:]]+)\s+([[:ascii:]]+)/) {
! 209: my ($username, $email) = ($1, $2);
! 210: # my @users = treeget($znctree, "User", "Node");
! 211: foreach my $user (@users) {
! 212: if ($user eq $username) {
! 213: main::putserv($bot, "PRIVMSG $nick :Sorry, username taken. Please contact staff if you need help.");
! 214: }
! 215: }
! 216: #my $captcha = join'', map +(0..9,'a'..'z','A'..'Z')[rand(10+26*2)], 1..4;
! 217: my $captcha = int(rand(999));
! 218: my $ircid = int(rand(9223372036854775807));
! 219: my $hashid = sha256_hex("$ircid");
! 220: SQLite::set("irc", "id", $ircid, "localtime", time());
! 221: SQLite::set("irc", "id", $ircid, "hashid", sha256_hex($ircid));
! 222: SQLite::set("irc", "id", $ircid, "date", main::date());
! 223: SQLite::set("irc", "id", $ircid, "hostmask", $hostmask);
! 224: SQLite::set("irc", "id", $ircid, "nick", $nick);
! 225: SQLite::set("bnc", "ircid", $ircid, "username", $username);
! 226: SQLite::set("bnc", "ircid", $ircid, "email", $email);
! 227: SQLite::set("bnc", "ircid", $ircid, "captcha", $captcha);
! 228: SQLite::set("bnc", "ircid", $ircid, "hashid", $hashid);
! 229: main::whois($bot->{sock}, $nick);
! 230: main::ctcp($bot->{sock}, $nick);
! 231: main::putserv($bot, "PRIVMSG $nick :".`figlet $captcha`);
! 232: main::putserv($bot, "PRIVMSG $nick :https://$hostname/$hashid/captcha.png");
! 233: main::putserv($bot, "PRIVMSG $nick :https://$hostname/register.php?hashirc=$hashid");
! 234: main::putserv($bot, "PRIVMSG $nick :Type !bnc captcha <text>");
! 235: foreach my $chan (@teamchans) {
! 236: main::putservlocalnet($bot, "PRIVMSG $chan :$nick\'s captcha is $captcha");
! 237: }
! 238: } else {
! 239: main::putserv($bot, "PRIVMSG $nick :Invalid username or email. Type !bnc <username> <email> to try again.");
! 240: foreach my $chan (@teamchans) {
! 241: main::putservlocalnet($bot, "PRIVMSG $chan :Help *$nick* on ".$bot->{name});
! 242: }
! 243: }
! 244: }
! 245:
! 246: sub mregex {
! 247: my ($bot, $nick, $host, $hand, $text) = @_;
! 248: if (!main::isstaff($bot, $nick)) { return; }
! 249: if ($text =~ /^ips?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
! 250: my $ips = $1; # space-separated list of IPs
! 251: main::putserv($bot, "PRIVMSG $nick :".regexlist($ips));
! 252: } elsif ($text =~ /^users?\s+([-_()|0-9A-Za-z:\.?*\s]{3,})$/) {
! 253: my $users = $1; # space-separated list of usernames
! 254: main::putserv($bot, "PRIVMSG $nick :".regexlist($users));
! 255: } elsif ($text =~ /^[-_()|0-9A-Za-z:,\.?*\s]{3,}$/) {
! 256: my @lines = regex($text);
! 257: foreach my $l (@lines) { print "$l\n"; }
! 258: }
! 259: }
! 260: sub mforeach {
! 261: my ($bot, $nick, $host, $hand, $text) = @_;
! 262: if ($staff !~ /$nick/) { return; }
! 263: if ($text =~ /^network\s+del\s+([[:graph:]]+)\s+(#[[:graph:]]+)$/) {
! 264: my ($user, $chan) = ($1, $2);
! 265: foreach my $n (@main::networks) {
! 266: main::putserv($bot, "PRIVMSG *controlpanel :delchan $user $n->{name} $chan");
! 267: }
! 268: }
! 269: }
! 270:
! 271: sub mcontrolpanel {
! 272: my ($bot, $nick, $host, $hand, @args) = @_;
! 273: my ($chan, $text);
! 274: if (@args == 2) {
! 275: ($chan, $text) = ($args[0], $args[1]);
! 276: } else { $text = $args[0]; }
! 277: my $hostmask = "$nick!$host";
! 278: if($hostmask eq '*controlpanel!znc@znc.in') {
! 279: if ($text =~ /^Error: User \[cloneuser\] does not exist/) {
! 280: createclone($bot);
! 281: foreach my $chan (@teamchans) {
! 282: main::putserv($bot, "PRIVMSG $chan :Cloneuser created");
! 283: }
! 284: } elsif ($text =~ /^User (.*) added!$/) {
! 285: main::debug(ALL, "User $1 created");
! 286: } elsif ($text =~ /^Password has been changed!$/) {
! 287: main::debug(ALL, "Password changed");
! 288: } elsif ($text =~ /^Queued network (.*) of user (.*) for a reconnect.$/) {
! 289: main::debug(ALL, "$2 now connecting to $1...");
! 290: } elsif ($text =~ /^Admin = false/) {
! 291: foreach my $chan (@teamchans) {
! 292: main::putserv($bot, "PRIVMSG $chan :ERROR: $nick is not admin");
! 293: }
! 294: die "ERROR: $nick is not admin";
! 295: } elsif ($text =~ /^Admin = true/) {
! 296: main::debug(ALL, "$nick is ZNC admin");
! 297: } elsif ($text =~ /(.*) = (.*)/) {
! 298: my ($key, $val) = ($1, $2);
! 299: main::debug(ALL, "ZNC: $key => $val");
! 300: } else {
! 301: main::debug(ERRORS, "Unexpected 290 BNC.pm: $hostmask $text");
! 302: }
! 303: }
! 304: }
! 305: sub loadlog {
! 306: open(my $fh, '<', "$znclog") or die "Could not read file 'znc.log' $!";
! 307: chomp(@logs = <$fh>);
! 308: close $fh;
! 309: }
! 310:
! 311: # return all lines matching a pattern
! 312: sub regex {
! 313: my ($pattern) = @_;
! 314: if (!@logs) { loadlog(); }
! 315: return grep(/$pattern/, @logs);
! 316: }
! 317:
! 318: # given a list of IPs, return matching users
! 319: # or given a list of users, return matching IPs
! 320: sub regexlist {
! 321: my ($items) = @_;
! 322: my @items = split /[,\s]+/m, $items;
! 323: my $pattern = "(".join('|', @items).")";
! 324: if (!@logs) { loadlog(); }
! 325: my @matches = grep(/$pattern/, @logs);
! 326: my @results;
! 327: foreach my $match (@matches) {
! 328: if ($match =~ /^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[([^]\/]+)(\/[^]]+)?\] connected to ZNC from (.*)/) {
! 329: my ($user, $ip) = ($1, $3);
! 330: if ($items =~ /[.:]/) { # items are IP addresses
! 331: push(@results, $user);
! 332: } else { # items are users
! 333: push(@results, $ip);
! 334: }
! 335: }
! 336: }
! 337: my @sorted = sort @results;
! 338: @results = do { my %seen; grep { !$seen{$_}++ } @sorted }; # uniq
! 339: return join(' ', @results);
! 340: }
! 341:
! 342: sub createclone {
! 343: my ($bot) = @_;
! 344: my $socket = $bot->{sock};
! 345: my $password = Hash::newpass();
! 346: my $msg = <<"EOF";
! 347: adduser cloneuser $password
! 348: set Nick cloneuser cloneuser
! 349: set Altnick cloneuser cloneuser_
! 350: set Ident cloneuser cloneuser
! 351: set RealName cloneuser cloneuser
! 352: set MaxNetworks cloneuser 1000
! 353: set ChanBufferSize cloneuser 1000
! 354: set MaxQueryBuffers cloneuser 1000
! 355: set QueryBufferSize cloneuser 1000
! 356: set NoTrafficTimeout cloneuser 600
! 357: set QuitMsg cloneuser IRCNow and Forever!
! 358: set RealName cloneuser cloneuser
! 359: set DenySetBindHost cloneuser true
! 360: set Timezone cloneuser US/Pacific
! 361: LoadModule cloneuser controlpanel
! 362: LoadModule cloneuser chansaver
! 363: EOF
! 364: #LoadModule cloneuser buffextras
! 365: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
! 366: foreach my $n (@main::networks) {
! 367: my $net = $n->{name};
! 368: my $server = $n->{server};
! 369: my $port = $n->{port};
! 370: my $trustcerts = $n->{trustcerts};
! 371: $msg = <<"EOF";
! 372: addnetwork cloneuser $net
! 373: addserver cloneuser $net $server $port
! 374: disconnect cloneuser $net
! 375: EOF
! 376: if ($trustcerts) {
! 377: $msg .= "SetNetwork TrustAllCerts cloneuser $net True\r\n";
! 378: }
! 379: my @chans = split /[,\s]+/m, $chans;
! 380: foreach my $chan (@chans) {
! 381: $msg .= "addchan cloneuser $net $chan\r\n";
! 382: }
! 383: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
! 384: }
! 385: }
! 386:
! 387: sub createbnc {
! 388: my ($bot, $username, $password, $bindhost) = @_;
! 389: my $netname = $bot->{name};
! 390: my $msg = <<"EOF";
! 391: cloneuser cloneuser $username
! 392: set Nick $username $username
! 393: set Altnick $username ${username}_
! 394: set Ident $username $username
! 395: set RealName $username $username
! 396: set Password $username $password
! 397: set MaxNetworks $username 1000
! 398: set ChanBufferSize $username 1000
! 399: set MaxQueryBuffers $username 1000
! 400: set QueryBufferSize $username 1000
! 401: set NoTrafficTimeout $username 600
! 402: set QuitMsg $username IRCNow and Forever!
! 403: set BindHost $username $bindhost
! 404: set DCCBindHost $username $bindhost
! 405: set DenySetBindHost $username true
! 406: reconnect $username $netname
! 407: EOF
! 408: #set Language $username en-US
! 409: main::putserv($bot, "PRIVMSG *controlpanel :$msg");
! 410: return 1;
! 411: }
! 412: sub mailbnc {
! 413: my( $username, $email, $password, $service, $hashirc )=@_;
! 414: my $passhash = sha256_hex("$username");
! 415:
! 416: my $body = <<"EOF";
! 417: You created a bouncer!
! 418:
! 419: Username: $username
! 420: Password: $password
! 421: Server: $hostname
! 422: Port: $sslport for SSL (secure connection)
! 423: Port: $plainport for plaintext
! 424:
! 425: *IMPORTANT*: Verify your email address:
! 426:
! 427: https://$hostname/register.php?hashirc=$hashirc
! 428:
! 429: You *MUST* click on the link or your account will be deleted.
! 430:
! 431: IRCNow
! 432: EOF
! 433: Mail::mail($mailfrom, $email, $mailname, "Verify IRCNow Account", $body);
! 434: }
! 435:
! 436: sub mtaillog {
! 437: my ($bot, $nick, $host, $hand, @args) = @_;
! 438: my ($chan, $text);
! 439: if (@args == 2) {
! 440: ($chan, $text) = ($args[0], $args[1]);
! 441: } else { $text = $args[0]; }
! 442: my $hostmask = "$nick!$host";
! 443: open(my $fh, "-|", "/usr/bin/tail", "-f", $znclog) or die "could not start tail: $!";
! 444: while (my $line = <$fh>) {
! 445: foreach my $chan (@teamchans) {
! 446: main::putserv($bot, "PRIVMSG $chan :$line");
! 447: }
! 448: }
! 449: }
! 450:
! 451: sub mlastseen {
! 452: my ($bot, $nick, $host, $hand, @args) = @_;
! 453: my ($chan, $text);
! 454: if (@args == 2) {
! 455: ($chan, $text) = ($args[0], $args[1]);
! 456: } else { $text = $args[0]; }
! 457: my $hostmask = "$nick!$host";
! 458: if (!@logs) { loadlog(); }
! 459: my @users = treeget($znctree, "User", "Node");
! 460: foreach my $user (@users) {
! 461: my @lines = grep(/^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\] \[$user\] connected to ZNC from [.0-9a-fA-F:]+/, @logs);
! 462: if (scalar(@lines) == 0) {
! 463: foreach my $chan (@teamchans) {
! 464: main::putserv($bot, "PRIVMSG $chan :$user never logged in");
! 465: }
! 466: next;
! 467: }
! 468: my $recent = pop(@lines);
! 469: if ($recent =~ /^\[(\d{4}-\d\d-\d\d) \d\d:\d\d:\d\d\] \[$user\] connected to ZNC from [.0-9a-fA-F:]+/) {
! 470: my $date = $1;
! 471: foreach my $chan (@teamchans) {
! 472: main::putserv($bot, "PRIVMSG $chan :$user $date");
! 473: }
! 474: }
! 475: }
! 476: }
! 477: #sub resend {
! 478: # my ($bot, $newnick, $email) = @_;
! 479: # my $password = newpass();
! 480: # sendmsg($bot, "*controlpanel", "set Password $newnick $password");
! 481: # mailverify($newnick, $email, $password, "bouncer");
! 482: # sendmsg($bot, "$newnick", "Email sent");
! 483: #}
! 484:
! 485: # if ($reply =~ /^!resend ([-_0-9a-zA-Z]+) ([-_0-9a-zA-Z]+@[-_0-9a-zA-Z]+\.[-_0-9a-zA-Z]+)$/i) {
! 486: # my ($newnick, $email) = ($1, $2);
! 487: # my $password = newpass();
! 488: # resend($bot, $newnick, $email);
! 489: # }
! 490:
! 491: #sub resetznc {
! 492: #
! 493: #AnonIPLimit 10000
! 494: #AuthOnlyViaModule false
! 495: #ConnectDelay 0
! 496: #HideVersion true
! 497: #LoadModule
! 498: #ServerThrottle
! 499: #1337 209.141.38.137
! 500: #31337 209.141.38.137
! 501: #1337 2605:6400:20:5cc::
! 502: #31337 2605:6400:20:5cc::
! 503: #1337 127.0.0.1
! 504: #1338 127.0.0.1
! 505: #}
! 506: #
! 507: #alias Provides bouncer-side command alias support.
! 508: #autoreply Reply to queries when you are away
! 509: #block_motd Block the MOTD from IRC so it's not sent to your client(s).
! 510: #bouncedcc Bounces DCC transfers through ZNC instead of sending them directly to the user.
! 511: #clientnotify Notifies you when another IRC client logs into or out of your account. Configurable.
! 512: #ctcpflood Don't forward CTCP floods to clients
! 513: #dcc This module allows you to transfer files to and from ZNC
! 514: #perform Keeps a list of commands to be executed when ZNC connects to IRC.
! 515: #webadmin Web based administration module.
! 516:
! 517:
! 518: 1; # MUST BE LAST STATEMENT IN FILE
CVSweb