# 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]; } }