diff --git a/scripts/kloeri.pl b/scripts/kloeri.pl deleted file mode 120000 index 920eae2..0000000 --- a/scripts/kloeri.pl +++ /dev/null @@ -1 +0,0 @@ -/home/wowaname/.weechat-test/perl/kloeri.pl \ No newline at end of file diff --git a/scripts/kloeri.pl b/scripts/kloeri.pl new file mode 100644 index 0000000..3d97750 --- /dev/null +++ b/scripts/kloeri.pl @@ -0,0 +1,1366 @@ +# Released into the Public Domain + +use strict; +use warnings; +use DBI; +use feature 'switch'; + +no strict 'subs'; # `perl -c` bitches about weechat::WEECHAT_RC_OK +no warnings 'experimental::smartmatch'; + + +my $SCRIPT_NAME = 'kloeri'; +my $SCRIPT_AUTHOR = 'The Krusty Krab '; +my $SCRIPT_VERSION = '1.0'; +my $SCRIPT_LICENCE = 'Public domain'; +my $SCRIPT_DESC = "HERE'S YOUR BADGE KID, GO BE AN ASSHOLE."; + +my %OPTIONS = ( + debug => ['Enable debugging output. Can be VERY verbose', '0'], + timeout => ['Time to wait for IRCd responses (e.g. VERSION or WHOIS '. + 'queries) before giving up, in seconds', '60'], + + banmask => ['Default ban mask flags (any of n{uUi}{hHp}, see '. + '/help kloeri)', 'uh'], + opmask => ['Default automode mask flags (any of n{uUi}{hHp}, see '. + '/help kloeri)', 'uH'], + killmask => ['Default akill/tkline mask flags (any of n{uUi}{hHpr}, see '. + '/help kloeri)', 'p'], + cidr4 => ['Default CIDR value for IPv4 bans', '32'], + cidr6 => ['Default CIDR value for IPv6 bans', '64'], + + kickonban => ['Whether to kick users who match bans (override '. + 'per-channel with /kloeri set)', '0'], + banafter => ['Whether to ban after kick', '0'], + deop => ['Whether to deop before kick', '0'], + services => ['Use services if detected (0 - never, 1 - fallback if not '. + 'opped, 2 - always', '0'], + threshold => ['Threshold for wide-matching kick/kill masks; to override '. + 'use * flag in commands. 0 - no threshold', '1'], + + knockout => ['Default time for knockout bans, in minutes', '5'], + tkline => ['Default time for akills/tklines, in minutes', '10080'], + + limit => ['Limit joins to users:seconds (override per-channel with '. + '/kloeri set). Will use +j or +f on supported networks, otherwise '. + 'resorts to +l and +i. Omit :seconds and +j/f/i will not be used.', + '10:60'], + limit_delta => ['On servers without a +j style mode, set +l when there '. + 'are this many more/fewer users in channel. Recommended to set half '. + 'of the "users" value in the limit option', '5'], + limit_on => ['Whether autolimit is enabled for all channels by default', + '0'], + ); + +my %COMMANDS = ( +AKILL => [\&cmd_akill, '[-(switches)(length)(banmask)(cidr)] [reason]', +qr&^(-[^ /]*|)(/?[0-9]{0,3}:?[0-9]{0,3}) *([^ ]+) *(.*)&, q& + autokill users. consider enabling appropriate snomasks on your server in order + to catch users before they enter a channel + + switches may be any of 'd*': + d - dry run. print matching nicks and placed bans first + * - override sanity checks. kick matching users regardless of + threshold config + + additional banmask options (see KICKBAN for the rest): + r - match gecos / realname (this has to be used by itself) +&], +ALIAS => [\&cmd_alias, '[-(switches)]', qr&^(-[^ ]*|)&, q& + set aliases for all available commands in this script. PLEASE NOTE this will + override builtin commands as well as aliases you already have mapped using the + same names. consider backing up config files or renaming aliases if need be. + you may always manually add, remove, and rename any aliases you'd like + + switches: + d - dry run. print new aliases as well as whether they will override or + replace old commands + l - long aliases only (AUTOOP, KICKBAN, ...) + s - short aliases only (AOP, KB, ...) +&], +AUTOOP => [\&cmd_autoop, '[-(switches)(mask)] [mode string]', +qr&^(-[^ ]*|) *(.*)$&, q& + add or remove people from the channel's automode list. for example, + "/aop +ov-o friend chatter luser" will add "friend" to op and "chatter" to + voice, and remove "luser" from auto-op. you may also manually specify the op + mask such as "/aop +v *@trusted.example.org". to remove all listings for a + mask, specify -* as the mode, such as "/aop -* luser" + + if mode string isn't given, lists current autoop list + + switches: + a - apply ops to matching people immediately + * - override sanity checks (only useful with -a). op matching users + regardless of threshold config + + mask may be any of 'n{uUi}{hHp}' or 'v'. by default it is -uH. see KICKBAN + help for details on mask format +&], +DEBUG => [\&cmd_debug, '', qr&([^ ]*)&, q& + various debugging crap. see the source for available crap, as this crap may + change at any time and you should already have read the source if you're in + need of the crap +&], +DNS => [\&cmd_dns, '', qr&([^ ]+)&, q& + perform dns lookups on users matching mask. also performs resolution on + webchat users whose IP addresses are encoded in the ident field +&], +DOWN => [\&cmd_down, '[mask]', qr&(.*)&, q& + deop/devoice users, masks separated by space or comma +&], +HELP => [\&cmd_help, '[command]', qr&([^ ]*)&, q& + show script help, or show extended command help +&], +KEY => [\&cmd_key, '', qr&&, q& + add random key to channel +&], +KICK => [\&cmd_kick, '[-(switches)] [reason]', +qr&^(-[^ ]*|) *([^ ]+) *(.*)&, q& + kick users + + switches may be any of 'd*': + d - dry run. print matching nicks first + * - override sanity checks. kick matching users regardless of + threshold config +&], +KICKBAN => [\&cmd_kickban, '[-(switches)(banmask)(cidr)] [reason]', +qr&^(-[^ /:]*|)((?:/[0-9]{0,3})?(?::[0-9]{0,3})?) *([^ ]+) *(.*)&, q& + kick and permanently ban users. if mask is nickmask, banmask flags will be + used to place bans; if it is a full hostmask, only that hostmask ban will + be placed + + cidr examples: + /32 - IPv4 = 32 bits, IPv6 from config + :128 - IPv4 from config, IPv6 = 128 bits + /24:64 - IPv4 = 24 bits, IPv6 = 64 bits + + switches may be any of 'd*': + d - dry run. print matching nicks and placed bans first + * - override sanity checks. kick matching users regardless of + threshold config + + banmask may be any of 'n{uUi}{hHp}' or 'v'. by default it is -uH: + n - nickname + u - username (given ~luser@host, bans *luser@*. given luser@host, + bans luser@*) + U - full username (bans verbatim) + i - ident (given ~luser@host, bans *@*. given luser@host, bans luser@*) + h - partial hostname (given luser@example.org, bans *@example.org. given + luser@1-2-3-4.dsl.example.net, bans *@*.dsl.example.net. given + luser@1.2.3.4, bans *@1.2.3.*, etc.) + H - full hostname + p - resolved IP address. on cloaked or unresolvable IPs, the hostname will + be used + v - verbatim mask. must be used alone +&], +KILL => [\&cmd_kill, '[-(switches)] [reason]', +qr&^(-[^ ]*|) *([^ ]+) *(.*)&, q& + kill users + + switches may be any of 'd*': + d - dry run. print matching nicks and placed bans first + * - override sanity checks. kick matching users regardless of + threshold config +&], +KLINE => [\&cmd_kline, '[-(switches)(banmask)(cidr)] [reason]', +qr&^(-[^ /]*|)(/?[0-9]{0,3}:?[0-9]{0,3}) *([^ ]+) *(.*)&, q& + deny users from server. see KICKBAN help for banmask format + + KLINE tries to decide whether to use a kline, resv/qline, xline/gecos, + or dline/zline depending on the banmask specified + + switches may be any of 'gd*': + g - try to set a network-wide ban (gline or similar) + d - dry run. print matching nicks and placed bans first + * - override sanity checks. kick matching users regardless of + threshold config + + additional banmask options (see KICKBAN for the rest): + r - match gecos / realname (this has to be used by itself) +&], +KNOCKOUT => [\&cmd_kickban, +'[-(length)(switches)(banmask)(cidr)] [reason]', +qr&^(-[^ /]*|)((?:/[0-9]{0,3})?(?::[0-9]{0,3})?) *([^ ]+) *(.*)&, q& + kick and temporarily ban users. same as KICKBAN except with optional length + option. length is specified in minutes; if unspecified, it will use the + "knockout" setting +&], +QUIET => [\&cmd_quiet, '[-(length)(switches)(banmask)(cidr)] ', +qr&^(-[^ /]*|)(/?[0-9]{0,3}:?[0-9]{0,3}) *([^ ]*)&, q& + quiet users with appropriate mode depending on ircd +&], +TKLINE => [\&cmd_tkline, '[-(switches)(length)(banmask)(cidr)] [reason]', +qr&^(-[^ /]*|)(/?[0-9]{0,3}:?[0-9]{0,3}) *([^ ]+) *(.*)&, q& + temporarily deny users from server. see KICKBAN help for banmask format + + TKLINE tries to decide whether to use a kline, resv/qline, xline/gecos, + or dline/zline depending on the banmask specified + + switches may be any of 'gd*': + g - try to set a network-wide ban (gline or similar) + d - dry run. print matching nicks and placed bans first + * - override sanity checks. kick matching users regardless of + threshold config + + additional banmask options (see KICKBAN for the rest): + r - match gecos / realname (this has to be used by itself) +&], +REASON => [\&cmd_reason, '[shorthand [expansion]]', qr&^([^ ]*) *(.*)$&, q& + manage reason expansions, useful for kick/kill messages you see yourself using + often. without shorthand, display current mappings. specify shorthand without + expansion to remove that shorthand. specify both shorthand and expansion to + map that value. + + to use, simply replace the kick/kill/ban reason with the shorthand, and it + will be expanded +&], +SET => [\&cmd_set, '[option [value]]', qr&^([^ ]*) *(.*)$&, q& + without option, display current channel/server settings. specifying an option + without a value displays the value for that option. specifying both an option + and a value sets that option + + Channel settings: + kickonban - whether to kick users who match bans, overrides kickonban + global option + limit - limit joins to users:seconds, overrides limit global option + limit_on - whether autolimit is enabled, overrides limit_on global option +&], +); + +my $SCRIPT_PREFIX; +my $HELP = <<'HELP'; + ,-------------------------------------------------------------. + /))))))))) | kloeri.pl provides a multitude of commands, aliases, and | + //) __ __\ | functions that aid in channel and network management. it | + C==/_o|^|o_\ | also provides useful information on users even if you do | + | _\ ) | not operate a channel or server. most commands take an | + \ .--- / < optional target argument, specified by the @ prefix and | + _/`-. __.'_ | formatted @channel or @server.channel (for commands such as | + /` \`'-,._./|\ | kickban) or @server (for commands such as kline). | +/ \ /`\_/\/ \ `-------------------------------------------------------------' +Commands: +HELP + +$HELP .= "\n$_ ${COMMANDS{$_}[1]}" for (sort keys %COMMANDS); + +# database globals +our ($dbh, %aops, %settings); +# globals shared across functions +our ($buffer, $target, @parameters); +# caches and running operations +our (%ircds, %cbs, %joins_aop, %joins_lim); + +# -- general functions -- # + +sub bracket +{ + return join '' => + weechat::color('chat_delimiters'), '[', + weechat::color('reset'), join(' ', @_), + weechat::color('chat_delimiters'), ']', + weechat::color('reset'); +} + +sub error +{ + my ($msg, $buf) = (pop, pop // $buffer); + weechat::print($buf, weechat::prefix('error')."$SCRIPT_NAME: ".$msg); + return weechat::WEECHAT_RC_ERROR; +} + +sub info +{ + my ($msg, $buf) = (pop, pop // $buffer); + weechat::print($buf, "$SCRIPT_PREFIX\t".$msg); +} + +sub list +{ + my ($msg, $buf) = (pop, pop // $buffer); + my ($server, $channel) = servchan($buffer); + weechat::print( + $buf, weechat::prefix('network'). + bracket(weechat::color('chat_channel'). + ($channel // $server // $SCRIPT_NAME))." $msg"); +} + +sub debug +{ + return if weechat::config_get_plugin('debug') eq '0'; + my ($msg, $buf) = (pop, pop // $buffer // ''); + weechat::print($buf, "$SCRIPT_PREFIX\t(debug) ".$msg); +} + +# get currently-set limit for channel +sub chanlimit +{ + my ($servname, $channame) = servchan($buffer); + my ($hserv, $hchan) = (weechat::hdata_get('irc_server'), + weechat::hdata_get('irc_channel')); + my $servptr = weechat::hdata_search($hserv, + weechat::hdata_get_list($hserv, 'irc_servers'), + "\${irc_server.name} == $servname", 1); + my $chanptr = weechat::hdata_search($hchan, + weechat::hdata_pointer($hserv, $servptr, 'channels'), + "\${irc_channel.name} == $channame", 1); + weechat::hdata_integer($hchan, $chanptr, 'limit'); +} + +# normalise cidr format string from flags +sub cidr +{ + my ($cidr4, $cidr6) = shift =~ m&/?([^:]*):?(.*)&; + + $cidr4 = weechat::config_get_plugin('cidr4') unless $cidr4; + $cidr6 = weechat::config_get_plugin('cidr6') unless $cidr6; + + return ($cidr4, $cidr6); +} + +# compile list of masks and return list of regexes +sub compile +{ + # may replace with map function + my @rtn; + for my $pattern (split ',', shift) { + for ($pattern) { + /!.*@/ and last; + /!/ and do { $pattern .= '@*'; last }; + /@/ and do { $pattern = '*!'.$pattern; last }; + m&[.:/]& and do { $pattern = '*!*@'.$pattern; last }; + $pattern .= '!*@*'; last; + } + + $pattern = ($pattern =~ /^!re/ + ? $pattern =~ s/^!re//r + : quotemeta $pattern); + debug("&compile: received /$pattern/"); + $pattern =~ s/\\\?/./g; + $pattern =~ s/\\\*/.*/g; + debug("&compile: compiling /^$pattern\$/i"); + push @rtn, qr/^$pattern$/i; + } + return @rtn; +} + +sub deop +{ + my $nicks = shift; + my (%modes, @targets); + my ($letters, $prefixes) = opmodes(weechat::buffer_get_string($buffer, + 'localvar_server')); + debug("&deop(nicks => @$nicks); letters => $letters, prefixes => $prefixes"); + + for my $nick (@$nicks) { + my $iptr = weechat::infolist_get('irc_nick', '', + weechat::buffer_get_string($buffer, 'localvar_server').','. + weechat::buffer_get_string($buffer, 'localvar_channel').','. + $nick); + next unless $iptr; + $modes{$nick} = weechat::infolist_string($iptr, 'prefixes') + if weechat::infolist_next($iptr); + eval "\$modes{\$nick} =~ y/$prefixes/$letters/" if defined $modes{$nick} + or warn $@; + $modes{$nick} =~ s/ //g; + if ($modes{$nick}) { + push(@targets, $nick) for (1 .. length $modes{$nick}); + } + weechat::infolist_free($iptr); + } + + modes('-', join('', values %modes), 0, @targets); +} + +# return shortest list of banmasks given flags and list of users +sub get_bans +{ + my ($flags, $hosts, $cidr) = @_; + debug("&get_bans(flags => $flags, hosts => ..., cidr => $cidr)"); + my %rtn; + + for (@$hosts) { + my $mask; + if ($flags =~ /r/) { + $rtn{"!re$_"} = 1; + } + my ($nick, $user, $host) = /^([^!]*)!([^@]*)@(.*)/; + $mask = ($flags =~ /n/ ? $nick : '*') . '!'; + for ($flags) { + /u/ and ($mask .= ($user =~ /^~/ ? '*'.substr($user, 1) : $user)), + last; + /U/ and ($mask .= $user), last; + /i/ and ($mask .= ($user =~ /^~/ ? '*' : $user)), last; + ($mask .= '*'), last; + } + $mask .= '@'; + for ($flags) { + /h/ and ($mask .= hostpart($host)), last; + /H/ and ($mask .= $host), last; + /p/ and ($mask .= lookup($host, $cidr)), last; + ($mask .= '*'), last; + } + $rtn{$mask} = 1; + } + return keys %rtn; +} + +# return people who match mask +sub get_nicks +{ + my $masks = shift; + my (@nicks, @hosts, $iptr); + my ($server, $channel) = servchan($buffer); + + if ($masks =~ m&[*?!@.:/]&) { + # looks like a mask? fetch entire nicklist and iter through it ourselves + $iptr = weechat::infolist_get('irc_nick', '', "$server,$channel"); + return (\@nicks, \@hosts) unless $iptr; + + my @maskre = compile($masks); + + while (weechat::infolist_next($iptr)) { + my $nick = weechat::infolist_string($iptr, 'name'); + next if $nick eq weechat::info_get('irc_nick', $server); + my $hostmask = $nick.'!'.weechat::infolist_string($iptr, 'host'); + + next unless match($hostmask, @maskre); + push @nicks, $nick; + push @hosts, $hostmask; + } + + weechat::infolist_free($iptr); + } else { + # looks like nicks? let weechat pick what we want + for my $nick (split ',', $masks) { + $iptr = weechat::infolist_get('irc_nick', '', + "$server,$channel,$nick"); + next unless $iptr; + if (weechat::infolist_next($iptr) + and lc weechat::infolist_string($iptr, 'name') eq lc $nick) { + push @nicks, $nick; + push @hosts, "$nick!".weechat::infolist_string($iptr, 'host'); + }; + weechat::infolist_free($iptr); + } + } + return (\@nicks, \@hosts); +} + +# mask hostname or IP to significant sections +sub hostpart +{ + my $host = shift; + + # need to fumble with ipv6 as well + if ($host =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)[0-9]{1,3}$/) { + return $1.'*'; + } elsif ($host =~ /^[^.]+(\.[0-9a-z-]+\.[.0-9a-z-]+)$/i) { + return '*'.$1; + } elsif ($host =~ /^[^:]+([:0-9A-F]+:IP)$/) { # unrealircd ipv6 cloaks + return '*'.$1; + } else { + return $host; + } +} + +# case insensitive search for irc buffer (takes one or two args) +sub ircbuf { weechat::buffer_search('irc', "(?i)".(join '.', @_)) } + +# return ircd info. if currently unavailable, requests VERSION from server, +# hooks a callback to the calling function, and returns false +sub ircd +{ + my ($fnname, $cb, $args) = @_; + my $server = lc weechat::buffer_get_string($buffer, 'localvar_server'); + + return $ircds{$server} if exists $ircds{$server}; + return '' if (exists $cbs{"irc_351_${server}"} + or exists $cbs{"irc_351_${server}_timeout"}); + + info("$fnname is fetching ircd version; command may be delayed"); + weechat::command($buffer, '/quote version'); + + # next time &irc_004_351 is called, it'll see this in %cb + $cbs{"irc_351_${server}"} = [$cb, $args]; + $cbs{"irc_351_${server}_timeout"} = [ + weechat::hook_timer(weechat::config_get_plugin('timeout') * 1000, + 0, 1, 'irc_351_timeout', $buffer), + $cb, $args]; + + return ''; +} + +# kick list of nicks, stacking if possible +sub kick +{ + my ($nicks, $reason) = @_; + my ($stack) = (weechat::info_get('irc_server_isupport_value', + weechat::buffer_get_string($buffer, 'localvar_server').',TARGMAX') + =~ /KICK:([0-9]+)/); + $stack //= 1; + debug("&kick(nicks => @$nicks, reason => $reason); stack => $stack"); + + for (my $i = 0; $i < @$nicks; $i += $stack) { + weechat::command($buffer, "/command irc kick ".join(',', + @$nicks[$i .. $i + $stack - 1]).($reason ? " $reason" : "")); + } +} + +# do DNS lookup on host and return IP address, or return host if failed +# (for now this only returns host unless host is IP, in which it formats +# the IP address to appropriate CIDR notation) +sub lookup +{ + my ($host, $cidr) = @_; + my ($cidr4, $cidr6) = cidr($cidr); + debug("&lookup(host => $host, cidr => $cidr); /$cidr4:$cidr6"); + + # lazy checking, maybe improve this in the future + if ($host =~ /^([0-9.]{7,15})$/) { return "$1/$cidr4"; + } elsif ($host =~ /^([0-9a-f:]{2,71}).?$/i) { return "$1/$cidr6"; + } else { return $host; + } +} + +# match user host against list of regexes and return if found +sub match +{ + my ($hostmask, @patterns) = @_; + + return (lc $hostmask) ~~ @patterns; +} + +# stack modes and parameters and send to ircd +sub modes +{ + my ($pre, $mode, $length, @args) = @_; + $mode x= @args if length $mode == 1 and @args; + my $modelen = length $mode; + my $stack = weechat::info_get('irc_server_isupport_value', + weechat::buffer_get_string($buffer, 'localvar_server').',MODES') + || 3; # safe default + debug("&modes(pre => $pre, mode => $mode, length => $length, args => @args)"); + + for (my $i = 0; $i < $modelen; $i += $stack) { + my $n = 0; + unless ($i > @args) { + $n = ($i + $stack > @args) ? @args % $stack : $stack; + } + my $modestr = substr($mode, $i, $stack). + ($n ? ' '.join(' ', @args[$i .. $i + $n - 1]) : ''); + #debug("&modes: i => $i, modestr => $pre$modestr"); + weechat::command($buffer, "/mode $pre$modestr"); + weechat::command($buffer, "/wait ${length}m /mode ". + ($pre eq '+' ? '-' : '+')."$modestr") if $length; + } +} + +# return op modes/prefixes for server +sub opmodes +{ + my $server = shift; + my @prefix = weechat::info_get('irc_server_isupport_value', + "$server,PREFIX") =~ /^\(([^)]*)\)(.*)$/; + + if (@prefix) { + return @prefix; + } else { + return qw(ov @+); + } +} + +sub servchan +{ + my $buf = shift; + return (lc weechat::buffer_get_string($buf, 'localvar_server'), + weechat::buffer_get_string($buf, 'localvar_type') ne 'server' && + lc weechat::buffer_get_string($buf, 'localvar_channel')); +} + +# send banlist to ircd +sub set_bans +{ + my ($bans, $length) = @_; + debug("&set_bans(bans => @$bans, length => $length)"); + + modes('+', 'b', $length, @$bans); +} + +# send quietlist to ircd +sub set_quiets +{ + my $ircd = ircd('set_quiets', \&set_quiets, \@_); + return unless $ircd; + + my ($bans, $length) = @_; + debug("&set_quiets(bans => @$bans, length => $length); ircd => $ircd"); + my $server = weechat::buffer_get_string($buffer, 'localvar_server'); + + given ($ircd) { + when (['bahamut', 'hybrid', 'ircd', 'ircu', 'ngircd', 'plexus', 'ratbox']) { + modes('+', 'b', $length, @$bans); + + } when (['charybdis', 'seven']) { + modes('+', 'q', $length, @$bans); + + } when ('inspircd') { + @$bans = map { "m:$_" } @$bans if + weechat::info_get('irc_server_isupport_value', "$server,EXTBAN") + =~ /^,.*m/; + modes('+', 'b', $length, @$bans); + + } when ('unreal') { + @$bans = map { "~q:$_" } @$bans if + weechat::info_get('irc_server_isupport_value', "$server,EXTBAN") + =~ /^~,.*q/; + modes('+', 'b', $length, @$bans); + + } when ('other') { + # do some guessing if we don't know the ircd + my $extban = weechat::info_get('irc_server_isupport_value', + "$server,EXTBAN"); + + if ($extban =~ /^~,.*q/) { + # unreal-like + modes('+', 'b', $length, map { "~q:$_" } @$bans); + + } elsif ($extban =~ /^,.*m/) { + # insp-like + modes('+', 'b', $length, map { "m:$_" } @$bans); + + } elsif (weechat::info_get('irc_server_isupport_value', + "$server,CHANMODES") =~ /[^,]*q.*,/) { + # chary-like + debug("&set_quiets: ".weechat::info_get('irc_server_isupport_value', + "$server,CHANMODES")); + modes('+', 'q', $length, @$bans); + + } else { + # let's hope they're at least rfc1459! + modes('+', 'b', $length, @$bans); + } + } + } + +} + +# -- callbacks -- # + +sub ccmd_kloeri +{ + $buffer = $_[1]; + my ($command, $target, $paramstring) = + $_[2] =~ /([^ ]+)(?: +@([^ ]+))? *(.*)/; + + return error('Missing command') unless defined $command; + + $command = uc $command; + if (defined $target) { + my $server = weechat::buffer_get_string($buffer, 'localvar_server'); + $buffer = ircbuf($server, $target) + || ircbuf($server) + || ircbuf($target); + } else { + $target = ''; + } + chomp $paramstring; + + debug("&cmd_kloeri(command => $command, target => $target, ". + "paramstring => $paramstring)"); + + if (exists $COMMANDS{$command}) { + unless (@parameters = $paramstring =~ $COMMANDS{$command}[2]) { + debug("\$paramstring must be /${COMMANDS{$command}[2]}/"); + return error("Command $command requires parameters in format ". + weechat::color('underline').$COMMANDS{$command}[1]. + weechat::color('-underline')."; see ". + weechat::color('underline')."/kloeri help $command". + weechat::color('-underline')." for more information"); + } + debug("&cmd_kloeri: stepping into $command"); + $COMMANDS{$command}[0]->($command); + debug("&cmd_kloeri: stepping out of $command"); + } else { + return error("$command is not a known command"); + } + return weechat::WEECHAT_RC_OK; +} + +sub cmd_akill +{ + # TODO +} + +sub cmd_alias +{ + my ($flags) = @parameters; + my @aliases = ( + 'akill kloeri akill', + 'chankey kloeri key', + 'dns kloeri dns', + 'down kloeri down', + 'kill kloeri kill', + 'kline kloeri kline', + 'reason kloeri reason', + 'kset kloeri set', + 'quiet kloeri quiet', + 'tkline kloeri tkline', + ); + + if ($flags !~ /s/) { + @aliases = (@aliases, + 'autoop kloeri autoop', + 'kick kloeri kick', + 'kickban kloeri kickban', + 'knockout kloeri knockout', + ); + } + if ($flags !~ /l/) { + @aliases = (@aliases, + 'aop kloeri autoop', + 'k kloeri kick', + 'kb kloeri kickban', + 'ko kloeri knockout', + ); + } + + ($flags =~ /d/) and info('', 'I would add the following aliases:'); + for my $alias (@aliases) { + ($flags =~ /d/) and info('', ' - '.($alias =~ s/ / => /r)), next; + weechat::command('', "/alias add $alias"); + } + ($flags !~ /d/) and info('', "Don't forget to /save!"); +} + +sub cmd_autoop +{ + my ($flags, $args) = @parameters; + my ($server, $channel) = servchan($buffer); + my ($letters, $prefixes) = opmodes(weechat::buffer_get_string($buffer, + 'localvar_server')); + + # just print current aop list + unless ($args) { + my %prefixcols; + my $stmt = $dbh->prepare('SELECT mode, mask FROM automodes + WHERE server=? AND channel=?'); + $stmt->execute($server, $channel); + + for ( split ';', weechat::config_string( + weechat::config_get('irc.color.nick_prefixes')) ) { + if (/(.):(.+)/) { $prefixcols{$1} = $2; } + } + + while (my $row = $stmt->fetchrow_arrayref()) { + my $prefix = eval "\$row->[0] =~ y/$letters/$prefixes/r" or warn; + my ($nicks, undef) = get_nicks($row->[1]); + + list(weechat::color($prefixcols{$row->[0]} // $prefixcols{'*'}). + $prefix.weechat::color('reset'). + (@$nicks ? bracket(weechat::color('chat_nick'). + join(' ',@$nicks)) : "")." $row->[1]"); + } + list('End of automode list'); + return; + } + + # count '/aop -o nick' as mode string rather than use 'o' as flag + if ($flags and $args !~ /^[+-]/) { + $args = "$flags $args"; + $flags = ''; + } + + my ($modes, $maskstr, @masks); + + ($modes, $maskstr) = $args =~ /([-+*$letters]+) +([^,]*)/ + or return error("Mode string for AUTOOP must only contain +, -, *, ". + "and $letters, and masks may not contain commas"); + @masks = split ' ', $maskstr; + $modes x= @masks if length $modes == 1; + + my ($addstmt, $delstmt, $selstmt, $selallstmt) = ( + $dbh->prepare('INSERT INTO automodes + (server, channel, mask, mode) VALUES (?, ?, ?, ?)'), + $dbh->prepare('DELETE FROM automodes WHERE rowid=?'), + $dbh->prepare('SELECT rowid FROM automodes WHERE + server=? AND channel=? AND mask=? AND mode=?'), + $dbh->prepare('SELECT rowid FROM automodes WHERE + server=? AND channel=? AND mask=?') + ); + + my $add = 1; + my $opmask = ($flags =~ /[nuUihHpr]/) ? + $flags : weechat::config_get_plugin('opmask'); + + for my $mode (split '', $modes) { + $mode =~ /[+-]/ and $add = $mode eq '+', next; + + (my $mask = shift @masks) or last; + my $host; + + unless ($flags =~ /v/ or $mask =~ m&[*?!@.:/]&) { + (undef, $host) = get_nicks($mask); + next unless @$host; + } + + ($mask) = get_bans($opmask, [@$host[0]], '') if defined $host; + + $selstmt->execute($server, $channel, $mask, $mode); + my $selrtn = $selstmt->fetchrow_arrayref(); + + if ($add) { + next if $selrtn; + $addstmt->execute($server, $channel, $mask, $mode); + } elsif (!$add and $mode eq '*') { + $selallstmt->execute($server, $channel, $mask); + $delstmt->execute($_->[0]) for ($selallstmt->fetchrow_arrayref()) + } else { + $delstmt->execute($selrtn->[0]) if $selrtn; + } + + my ($nicks, undef) = get_nicks($mask); + # TODO: add threshold + modes('+', $mode, 0, @$nicks) if $flags =~ /a/ and @$nicks; + } + weechat::print($buffer, + weechat::prefix('network').'Automode '.weechat::color('chat_channel'). + $channel.' '.bracket(($args =~ /^[+-]/ ? "" : "+").$args)); + + # sync cache + my $stmt = $dbh->prepare('SELECT server, channel, mask, mode + FROM automodes'); + $stmt->execute(); + + # cache results for channels we're in + %aops = (); + while (my $row = $stmt->fetchrow_arrayref()) { + #next unless ircbuf($row->[0], $row->[1]); + $aops{$row->[0]}{$row->[1]}{$row->[2]} = $row->[3]; + } +} + +sub cmd_debug +{ + my ($crap) = lc $parameters[0]; + + if ($crap eq 'who') { + my (undef, $hosts) = get_nicks('*'); + list("$_") for (@$hosts); + list('End of who list'); + } elsif ($crap eq 'ircd') { + list("$_ => ${ircds{$_}}") for (keys %ircds); + list('End of ircd version list'); + } elsif ($crap eq 'cb') { + list("$_ => ${cbs{$_}}") for (keys %cbs); + list('End of callbacks list'); + } elsif ($crap eq 'aop') { + for my $server (keys %aops) { + list ("$server.$_") for (keys %{ $aops{$server} }); + } + } elsif ($crap eq 'limit') { + info('Invoking limit hook'); + irc_delay_limit($buffer); + } +} + +sub cmd_dns +{ + # TODO +} + +sub cmd_down +{ + my ($mask) = (shift @parameters) =~ s/ /,/gr; + debug("&cmd_down(mask => $mask)"); + my ($nicks, undef) = get_nicks( $mask || weechat::info_get('irc_nick', + weechat::buffer_get_string($buffer, 'localvar_server')) ); + + deop($nicks); +} + +sub cmd_help +{ + my $topic = uc $parameters[0]; + + if (!$topic) { + weechat::command('', '/help kloeri'); + } elsif (exists $COMMANDS{$topic}) { + info('', "$topic ${COMMANDS{$topic}[1]}"); + debug('', "(param regex: /${COMMANDS{$topic}[2]}/)"); + info('', "${COMMANDS{$topic}[3]}"); + } else { + return error('', "No help on $topic"); + } +} + +# TODO maybe? +sub cmd_key +{ + my @chars = ('a' .. 'z', '0' .. '9'); + my $string; + $string .= $chars[rand @chars] for (1 .. 8); + weechat::command('', "/mode +k $string"); +} + +sub cmd_kick +{ + my ($flags, $mask, $reason) = @parameters; + my ($nicks, undef) = get_nicks($mask); + + if ($flags =~ /d/) { + info( "I would kick ".bracket(weechat::color('chat_nick'). + join(' ', @$nicks)) ); + } else { + deop($nicks) if weechat::config_get_plugin('deop'); + + if (@$nicks > (weechat::config_get_plugin('threshold') || @$nicks) + and $flags !~ /\*/) { + return error("Too many targets; not kicking without -* flag"); + } else { + kick($nicks, $reason); + } + } +} + +sub cmd_kickban +{ + my ($command, $flags, $cidr, $mask, $reason) = (shift, @parameters); + my ($nicks, $hosts) = get_nicks($mask); + my $banmask = ($flags =~ /[nuUihHpr]/) ? + $flags : weechat::config_get_plugin('banmask'); + my @bans = ($flags =~ /v/) ? split ',', $mask : + get_bans($banmask, $hosts, $cidr // ''); + + my $len; + if ($command eq 'KICKBAN') { + $len = 0; + } else { + ($len) = ($flags =~ /([0-9]+)/); + $len //= weechat::config_get_plugin('knockout'); + } + + if ($flags =~ /d/) { + info("I would kick ".bracket(weechat::color('chat_nick'). + join(' ', @$nicks))." and ban ". + bracket(@bans).($len ? " for $len minutes" : "")); + } else { + deop($nicks) if weechat::config_get_plugin('deop'); + set_bans(\@bans, $len) unless weechat::config_get_plugin('banafter'); + + if (@$nicks > (weechat::config_get_plugin('threshold') || @$nicks) + and $flags !~ /\*/) { + return error("Too many targets; not kicking without -* flag"); + } else { + kick($nicks, $reason); + } + set_bans(\@bans, $len) if weechat::config_get_plugin('banafter'); + } +} + +sub cmd_kill +{ + # TODO +} + +sub cmd_kline +{ + # TODO +} + +sub cmd_quiet +{ + my ($flags, $cidr, $mask) = @parameters; + + return weechat::command($buffer, '/command irc quiet') unless $mask; + + my ($nicks, $hosts) = get_nicks($mask); + my $banmask = ($flags =~ /[nuUihHpr]/) ? + $flags : weechat::config_get_plugin('banmask'); + my @bans = ($flags =~ /v/) ? $mask : + get_bans($banmask, $hosts, $cidr // ''); + + my ($len) = ($flags =~ /([0-9]+)/); + $len //= weechat::config_get_plugin('knockout'); + + if ($flags =~ /d/) { + info("I would quiet ".bracket(@bans).($len ? " for $len minutes" : "")); + } else { + deop($nicks); + set_quiets(\@bans, $len); + } +} + +sub cmd_reason +{ + # TODO +} + +sub cmd_set +{ + my ($key, $value) = @parameters; + my ($server, $channel) = servchan($buffer); + + # nothing to do if we're not on a server + return error('SET only works on servers and channels!') + unless defined $server; + + # just recall settings or remove setting + # TODO: reflect %settings cache if possible + if ($value eq '') { + my $stmt; + if ($key && $key =~ /^-/) { + $key =~ s/^-//; + $stmt = $dbh->prepare('DELETE FROM settings + WHERE server=? AND channel=? AND key=?'); + $stmt->execute($server, $channel // '', $key); + delete $settings{$server}{$channel}{$key}; + info("Removed setting $key"); + return weechat::WEECHAT_RC_OK; + } + if ($key) { + $stmt = $dbh->prepare('SELECT key, value FROM settings + WHERE server=? AND channel=? AND key=?'); + $stmt->execute($server, $channel // '', $key); + } else { + $stmt = $dbh->prepare('SELECT key, value FROM settings + WHERE server=? AND channel=?'); + $stmt->execute($server, $channel // ''); + } + + while (my $row = $stmt->fetchrow_arrayref()) { + list("$row->[0] => $row->[1]"); + } + list('End of '.($channel ? 'channel' : 'server'). + ' settings'); + return weechat::WEECHAT_RC_OK; + } + + if (defined $channel) { + return error("Option $key doesn't exist for channels") + unless ($key =~ /kickonban|limit|limit_delta|limit_on/); + + my $stmt = $dbh->prepare('DELETE FROM settings + WHERE server=? AND channel=? AND key=?'); + + $stmt->execute($server, $channel, $key); + + $stmt = $dbh->prepare('INSERT INTO settings + (server, channel, key, value) VALUES (?, ?, ?, ?)'); + $stmt->execute($server, $channel, $key, $value); + $settings{$server}{$channel}{$key} = $value; + info("Setting $key changed to $value"); + } + return weechat::WEECHAT_RC_OK; +} + +sub cmd_tkline +{ + # TODO +} + +sub cb_shutdown +{ + $dbh->disconnect() if $dbh; + return weechat::WEECHAT_RC_OK; +} + +sub irc_004_351 +{ + my (undef, $server, $numeric, $message) = (shift, + shift =~ /(.+),irc_raw_in_(...)/, shift); + $server = lc $server; + $ircds{$server} = 'other'; + debug("&irc_004_351(server => $server, numeric = $numeric, ". + "message => $message)"); + + my $version; + for ($message =~ /^:[^ ]* (...)/) { + ($1 eq '004') and + ($version) = $message =~ /^:[^ ]* 004 [^ ]+ [^ ]+ ([^ ]+)/; + ($1 eq '351') and + ($version) = $message =~ /^:[^ ]* 351 [^ ]+ (.*)/; + } + + return weechat::WEECHAT_RC_OK unless defined $version; + + for ($version) { + /^bahamut-/ and ($ircds{$server} = 'bahamut'), last; + /^charybdis-/ and ($ircds{$server} = 'charybdis'), last; + /^:?InspIRCd-/ and ($ircds{$server} = 'inspircd'), last; + /^[0-9]\./ and ($ircds{$server} = 'ircd'), last; + /^u[0-9]+\./ and ($ircds{$server} = 'ircu'), last; + /^ngircd-/i and ($ircds{$server} = 'ngircd'), last; + /^hybrid-/ and ($ircds{$server} = 'hybrid'), last; + /^hybrid-.*\+plexus/ and ($ircds{$server} = 'plexus'), last; + /^ircd-ratbox-/ and ($ircds{$server} = 'ratbox'), last; + /^ircd-seven-/ and ($ircds{$server} = 'seven'), last; + /^Unreal/ and ($ircds{$server} = 'unreal'), last; + } + + return weechat::WEECHAT_RC_OK unless exists $cbs{"irc_351_${server}"}; + + $cbs{"irc_351_${server}"}[0]->(@{ $cbs{"irc_351_${server}"}[1] }); + weechat::unhook($cbs{"irc_351_${server}_timeout"}[0]) + if exists $cbs{"irc_351_${server}_timeout"}[0]; + delete $cbs{"irc_351_${server}"}; + delete $cbs{"irc_351_${server}_timeout"}; + return weechat::WEECHAT_RC_OK; +} + +sub irc_351_timeout +{ + $buffer = shift; + my $server = lc weechat::buffer_get_string($buffer, 'localvar_server'); + + $ircds{$server} = 'other'; + info("timeout for command reached; I'll just guess the ircd version"); + $cbs{"irc_351_${server}_timeout"}[1]-> + (@{ $cbs{"irc_351_${server}_timeout"}[2] }); + + weechat::unhook($cbs{"irc_351_${server}_timeout"}[0]) + if exists $cbs{"irc_351_${server}_timeout"}[0]; + delete $cbs{"irc_351_${server}"}; + delete $cbs{"irc_351_${server}_timeout"}; + return weechat::WEECHAT_RC_OK; +} + +sub irc_join +{ + my (undef, $server, $nick, $host, $channel) = (shift, + shift =~ /(.+),irc_raw_in_join/i, + shift =~ /:([^!]*)(![^ ]*|) join :?([^ ]*)/i); + $buffer = ircbuf($server, $channel); + ($server, $channel) = (lc $server, lc $channel); + debug("&irc_join(server => $server, nick => $nick, host => $host, ". + "channel => $channel)"); + + return weechat::WEECHAT_RC_OK unless exists $aops{$server}{$channel}; + $cbs{"irc_join_${server}$;${channel}_delay"} = + weechat::hook_timer(5000, 0, 1, 'irc_join_delay', $buffer) + unless exists $cbs{"irc_join_${server}$;${channel}_delay"}; + push @{ $joins_aop{$server}{$channel} }, [$nick, $host]; + + return hook_limit($server, $channel, 1); +} + +sub irc_part_kick +{ + my (undef, $server, $channel) = (shift, + shift =~ /(.+),irc_raw_in_(?:part|kick)/i, + shift =~ /:[^ ]* (?:part|kick) ([^ ]*)/i); + $buffer = ircbuf($server, $channel); + ($server, $channel) = (lc $server, lc $channel); + + return hook_limit($server, $channel); +} + +sub irc_quit +{ + my (undef, $server) = (shift, shift =~ /(.+),irc_raw_in_quit/i); + $server = lc $server; + + # we'll be lazy as fuck and just check all limited chans on server + hook_limit($server, lc $_) for keys %{ $settings{$server} }; + + return weechat::WEECHAT_RC_OK; +} + +sub hook_limit +{ + my ($server, $channel, $is_join) = @_; + $is_join //= 0; + if ($settings{$server}{$channel}{limit_on} + // weechat::config_get_plugin('limit_on')) { + my $buf = ircbuf($server, $channel); + my ($limit, $delta, $nicks) = ( + $settings{$server}{$channel}{limit} + // weechat::config_get_plugin('limit'), + $settings{$server}{$channel}{limit_delta} + // weechat::config_get_plugin('limit_delta'), + weechat::buffer_get_integer($buf, 'nicklist_nicks_count'), + ); + + # max joins in n // 30 seconds + my ($joins, $seconds) = ($limit =~ /^([0-9]+)/, $limit =~ /:([0-9]+)$/); + + # we don't set mode +i for three reasons: + # - we could clobber an existing mode +i (& accidentally unset it later) + # - +i may be mlocked where +l isn't + # - people may be on +I list that are joinspamming + if ($is_join and $seconds and + ++$joins_lim{$server}{$channel} == $joins) { + weechat::command($buf, '/mode +l 1'); + weechat::unhook($cbs{"irc_limit_${server}$;${channel}"}); + delete $cbs{"irc_limit_${server}$;${channel}"}; + }; + $cbs{"irc_limit_${server}$;${channel}"} = + weechat::hook_timer(($seconds // 30) * 1000, 0, 1, + 'irc_delay_limit', $buf) + unless exists $cbs{"irc_limit_${server}$;${channel}"}; + } + + return weechat::WEECHAT_RC_OK; +} + +sub irc_join_delay +{ + $buffer = shift; + my ($server, $channel) = servchan($buffer); + my ($modes, @targets); + my ($letters, $prefixes) = opmodes($buffer); + debug("&irc_join_delay(server => $server, channel => $channel)"); + + if (exists $cbs{"irc_join_${server}$;${channel}_delay"}) { + weechat::unhook($cbs{"irc_join_${server}$;${channel}_delay"}); + delete $cbs{"irc_join_${server}$;${channel}_delay"}; + } + + my @maskkeys = keys %{ $aops{$server}{$channel} }; + my @maskre = compile(join ',', @maskkeys); + for (@{ $joins_aop{$server}{$channel} }) { + my ($nick, $host) = @$_; + my $iptr = weechat::infolist_get('irc_nick', '', + "$server,$channel,$nick"); + next unless $iptr; + my $mode = weechat::infolist_string($iptr, 'prefixes') + if weechat::infolist_next($iptr); + weechat::infolist_free($iptr); + my $mask; + for my $i (0 .. $#maskre) { + next unless "$nick$host" =~ $maskre[$i]; + $mask = $maskkeys[$i]; + last; + } + next unless defined $mask; + eval "\$mode =~ y/$prefixes/$letters/" if defined $mode or warn $@; + next if $mode =~ /${aops{$server}{$channel}{$mask}}/; + + debug("&irc_join_delay: $nick$host"); + $modes .= $aops{$server}{$channel}{$mask}; + push @targets, $nick; + } + delete $joins_aop{$server}{$channel}; + modes('+', $modes, 0, @targets) if $modes and @targets; + return weechat::WEECHAT_RC_OK; +} + +# XXX: for now just use cmode +l regardless of network +sub irc_delay_limit +{ + $buffer = shift; + my ($server, $channel) = servchan($buffer); + my ($limit, $delta, $nicks, $curlimit) = ( + ($settings{$server}{$channel}{limit} + // weechat::config_get_plugin('limit')) =~ /^([0-9]+)/, + $settings{$server}{$channel}{limit_delta} + // weechat::config_get_plugin('limit_delta'), + weechat::buffer_get_integer($buffer, 'nicklist_nicks_count'), + chanlimit($buffer), + ); + + if (exists $cbs{"irc_limit_${server}$;${channel}"}) { + weechat::unhook($cbs{"irc_limit_${server}$;${channel}"}); + delete $cbs{"irc_limit_${server}$;${channel}"}; + } + delete $joins_lim{$server}{$channel}; + + debug("nicks=>$nicks curlimit=>$curlimit delta=>$delta"); + weechat::command($buffer, '/mode +l '.($nicks + $limit)) + if $nicks <= $curlimit - $limit - $delta + || $nicks > $curlimit - $limit + $delta; + + return weechat::WEECHAT_RC_OK; +} + +sub irc_connect +{ + my $server = pop; + my $stmt = $dbh->prepare('SELECT channel, mask, mode FROM automodes + WHERE server=?'); + $server = lc $server; + $stmt->execute($server); + + while (my $row = $stmt->fetchrow_arrayref()) { + next unless ircbuf($server, $row->[0]); + $aops{$server}{$row->[0]}{$row->[1]} = $row->[2]; + } + return weechat::WEECHAT_RC_OK; +} + +sub irc_disconnect +{ + my $server = pop; + $server = lc $server; + weechat::unhook($cbs{"irc_351_${server}"}[0]) + if exists $cbs{"irc_351_${server}"}; + weechat::unhook($cbs{"irc_351_${server}_timeout"}[0]) + if exists $cbs{"irc_351_${server}_timeout"}; + delete $aops{$server}; + delete $cbs{"irc_351_${server}"}; + delete $cbs{"irc_351_${server}_timeout"}; + delete $ircds{$server}; + delete $joins_aop{$server}; + delete $joins_lim{$server}; + delete $settings{$server}; + return weechat::WEECHAT_RC_OK; +} + + +if (weechat::register($SCRIPT_NAME, $SCRIPT_AUTHOR, $SCRIPT_VERSION, + $SCRIPT_LICENCE, $SCRIPT_DESC, 'cb_shutdown', '')) { + my $homedir = weechat::info_get( 'weechat_dir', ''); + $dbh = DBI->connect("dbi:SQLite:dbname=$homedir/kloeri.sqlite", '', '', { + RaiseError => 1 }) or warn "SQLite error: $@"; + $dbh->do('CREATE TABLE IF NOT EXISTS automodes + (server TEXT NOT NULL, + channel TEXT NOT NULL, + mask TEXT NOT NULL, + mode TEXT NOT NULL)'); + $dbh->do('CREATE TABLE IF NOT EXISTS settings + (server TEXT NOT NULL, + channel TEXT, + key TEXT NOT NULL, + value TEXT)'); + + weechat::hook_command('kloeri', $SCRIPT_DESC, + '[command] [@[server.]channel] [parameters]', + $HELP, '', 'ccmd_kloeri', ''); + # TODO: mute 351/005 numerics when we request ircd version, + # using hook_hsignal + weechat::hook_signal('*,irc_raw_in_004', 'irc_004_351', ''); + weechat::hook_signal('*,irc_raw_in_351', 'irc_004_351', ''); + weechat::hook_signal('*,irc_raw_in_join', 'irc_join', ''); + weechat::hook_signal('*,irc_raw_in_part', 'irc_part_kick', ''); + weechat::hook_signal('*,irc_raw_in_kick', 'irc_part_kick', ''); + #weechat::hook_signal('*,irc_raw_in_quit', 'irc_quit', ''); + weechat::hook_signal('irc_server_connected', 'irc_connect', ''); + weechat::hook_signal('irc_server_disconnected', 'irc_disconnect', ''); + + $SCRIPT_PREFIX = bracket(weechat::color('chat_nick').$SCRIPT_NAME); + + for my $option (keys %OPTIONS) { + weechat::config_set_plugin($option, $OPTIONS{$option}[1]) + unless weechat::config_is_set_plugin($option); + weechat::config_set_desc_plugin($option, $OPTIONS{$option}[0]); + } + + my $stmt = $dbh->prepare('SELECT server, channel, mask, mode + FROM automodes'); + $stmt->execute(); + + # cache results for channels we're in + while (my $row = $stmt->fetchrow_arrayref()) { + next unless ircbuf('server', $row->[0]); + $aops{$row->[0]}{$row->[1]}{$row->[2]} = $row->[3]; + } + + $stmt = $dbh->prepare('SELECT server, channel, key, value FROM settings'); + $stmt->execute(); + + # same thing but for settings + while (my $row = $stmt->fetchrow_arrayref()) { + next unless ircbuf('server', $row->[0]); + # channel ($row->[1]) is blank for server settings + $settings{$row->[0]}{$row->[1]}{$row->[2]} = $row->[3]; + } +}