aboutsummaryrefslogtreecommitdiffstats
path: root/weechat/python/chanop.py
diff options
context:
space:
mode:
Diffstat (limited to 'weechat/python/chanop.py')
-rw-r--r--weechat/python/chanop.py3236
1 files changed, 3236 insertions, 0 deletions
diff --git a/weechat/python/chanop.py b/weechat/python/chanop.py
new file mode 100644
index 0000000..848837e
--- /dev/null
+++ b/weechat/python/chanop.py
@@ -0,0 +1,3236 @@
+# -*- coding: utf-8 -*-
+###
+# Copyright (c) 2009-2013 by Elián Hanisch <lambdae2@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+###
+# Helper script for IRC Channel Operators
+#
+# Inspired by auto_bleh.pl (irssi) and chanserv.py (xchat) scripts.
+#
+# Networks like Freenode and some channels encourage operators to not stay
+# permanently with +o privileges and only use it when needed. This script
+# works along those lines, requesting op, kick/ban/etc and deop
+# automatically with a single command.
+# Still this script is very configurable and its behaviour can be configured
+# in a per server or per channel basis so it can fit most needs without
+# changing its code.
+#
+# Features several completions for ban/quiet masks and a memory for channel
+# masks and users (so users that parted are still bannable by nick).
+#
+#
+# Commands (see detailed help with /help in WeeChat):
+# * /oop: Request or give op.
+# * /odeop: Drop or remove op.
+# * /okick: Kick user (or users).
+# * /oban: Apply ban mask.
+# * /ounban: Remove ban mask.
+# * /oquiet: Apply quiet mask.
+# * /ounquiet: Remove quiet mask.
+# * /obankick: Ban and kick user (or users)
+# * /otopic: Change channel topic
+# * /omode: Change channel modes
+# * /olist: List cached masks (bans or quiets)
+# * /ovoice: Give voice to user
+# * /odevoice: Remove voice from user
+#
+#
+# Settings:
+# Most configs (unless noted otherwise) can be defined for a server or a
+# channel in particular, so it is possible to request op in different
+# networks, stay always op'ed in one channel while
+# auto-deop in another.
+#
+# For define an option for a specific server use:
+# /set plugins.var.python.chanop.<option>.<server> "value"
+# For define it in a specific channel use:
+# /set plugins.var.python.chanop.<option>.<server>.<#channel> "value"
+#
+# * plugins.var.python.chanop.op_command:
+# Here you define the command the script must run for request op, normally
+# is a /msg to a bot, like chanserv in freenode or Q in quakenet.
+# It accepts the special vars $server, $channel and $nick
+#
+# By default it ask op to chanserv, if your network doesn't use chanserv,
+# then you must change it.
+#
+# Examples:
+# /set plugins.var.python.chanop.op_command
+# "/msg chanserv op $channel $nick"
+# (globally for all servers, like freenode and oftc)
+# /set plugins.var.python.chanop.op_command.quakenet
+# "/msg q op $channel $nick"
+# (for quakenet only)
+#
+# * plugins.var.python.chanop.autodeop:
+# Enables auto-deop'ing after using any of the ban or kick commands.
+# Note that if you got op manually (like with /oop) then the script won't
+# deop you.
+# Valid values: 'on', 'off' Default: 'on'
+#
+# * plugins.var.python.chanop.autodeop_delay:
+# Time it must pass (without using any commands) before auto-deop, in
+# seconds. Using zero causes to deop immediately.
+# Default: 180
+#
+# * plugins.var.python.chanop.default_banmask:
+# List of keywords separated by comas. Defines default banmask, when using
+# /oban, /obankick or /oquiet
+# You can use several keywords for build a banmask, each keyword defines how
+# the banmask will be generated for a given hostmask, see /help oban.
+# Valid keywords are: nick, user, host and exact.
+# Default: 'host'
+#
+# Examples:
+# /set plugins.var.python.chanop.default_banmask host
+# (bans with *!*@host)
+# /set plugins.var.python.chanop.default_banmask host,user
+# (bans with *!user@host)
+#
+# * plugins.var.python.chanop.kick_reason:
+# Default kick reason if none was given in the command.
+#
+# * plugins.var.python.chanop.enable_remove:
+# If enabled, it will use "/quote remove" command instead of /kick, enable
+# it only in networks that support it, like freenode.
+# Valid values: 'on', 'off' Default: 'off'
+#
+# Example:
+# /set plugins.var.python.chanop.enable_remove.freenode on
+#
+# * plugins.var.python.chanop.display_affected:
+# Whenever a new ban is set, chanop will show the users affected by it.
+# This is intended for help operators to see if their ban is too wide or
+# point out clones in the channel.
+# Valid values: 'on', 'off' Default: 'off'
+#
+#
+# The following configs are global and can't be defined per server or channel.
+#
+# * plugins.var.python.chanop.enable_multi_kick:
+# Enables kicking multiple users with /okick command.
+# Be careful with this as you can kick somebody by accident if
+# you're not careful when writting the kick reason.
+#
+# This also applies to /obankick command, multiple bankicks would be enabled.
+# Valid values: 'on', 'off' Default: 'off'
+#
+# * plugins.var.python.chanop.enable_bar:
+# This will enable a pop-up bar for displaying chanop messages that would
+# otherwise be printed in the buffer. This bar also shows in realtime the
+# users affected by a ban you're about to set.
+# Valid values: 'on', 'off' Default: 'on'
+#
+#
+# The following configs are defined per server and are updated by the script only.
+#
+# * plugins.var.python.chanop.watchlist:
+# Indicates to chanop which channels should watch and keep track of users and
+# masks. This config is automatically updated when you use any command that needs
+# op, so manual setting shouldn't be needed.
+#
+# * plugins.var.python.chanop.isupport:
+# Only used in WeeChat versions prior to 0.3.3 which lacked support for
+# irc_005 messages. These aren't meant to be set manually.
+#
+#
+# Completions:
+# Chanop has several completions, documented here. Some aren't used by chanop
+# itself, but can be used in aliases with custom completions.
+# Examples:
+# apply exemptions with mask autocompletion
+# /alias -completion %(chanop_ban_mask) exemption /mode $channel +e
+# if you use grep.py script, grep with host autocompletion, for look clones.
+# /alias -completion %(chanop_hosts) ogrep /grep
+#
+# * chanop_unban_mask (used in /ounban)
+# Autocompletes with banmasks set in current channel, requesting them if needed.
+# Supports patterns for autocomplete several masks: *<tab> for all bans, or
+# *192.168*<tab> for bans with '192.168' string.
+#
+# * chanop_unquiet (used in /ounquiet)
+# Same as chanop_unban_mask, but with masks for q channel mode.
+#
+# * chanop_ban_mask (used in /oban and /oquiet)
+# Given a partial IRC hostmask, it will try to complete with hostmasks of current
+# users: *!*@192<tab> will try to complete with matching users, like
+# *!*@192.168.0.1
+#
+# * chanop_nicks (used in most commands)
+# Autocompletes nicks, same as WeeChat's completer, but using chanop's user
+# cache, so nicks from users that parted the channel will be still be completed.
+#
+# * chanop_users (not used by chanop)
+# Same as chanop_nicks, but with the usename part of the hostmask.
+#
+# * chanop_hosts (not used by chanop)
+# Same as chanop_nicks, but with the host part of the hostmask (includes previously used
+# hostnames).
+#
+#
+# TODO
+# * use dedicated config file like in urlgrab.py?
+# * ban expire time
+# * save ban.mask and ban.hostmask across reloads
+# * allow to override quiet command (for quiet with ChanServ)
+# * freenode:
+# - support for bans with channel forward
+# - support for extbans (?)
+# * Sort completions by user activity
+#
+#
+# History:
+# 2013-05-24
+# version 0.3.1: bug fixes
+# * fix exceptions while fetching bans with /mode
+# * fix crash with /olist command in networks that don't support +q channel masks.
+#
+# 2013-04-14
+# version 0.3:
+# * cycle between different banmasks in /oban /oquiet commands.
+# * added pop-up bar for show information.
+# * save ban mask information (date and operator)
+# * remove workarounds for < 0.3.2 weechat versions
+# * python 3.0 compatibility (not tested)
+#
+# 2013-01-02
+# version 0.2.7: bug fixes:
+# * fix /obankick, don't deop before kicking.
+#
+# 2011-09-18
+# version 0.2.6: bug fixes:
+# * update script to work with freenode's new quiet messages.
+# * /omode wouldn't work with several modes.
+#
+# 2011-05-31
+# version 0.2.5: bug fixes:
+# * /omode -o nick wouldn't work due to the deopNow switch.
+# * unban_completer could fetch the same masks several times.
+# * removing ban forwards falied when using exact mask.
+# * user nick wasn't updated in every call.
+#
+# 2011-02-02
+# version 0.2.4: fix python 2.5 compatibility
+#
+# 2011-01-09
+# version 0.2.3: bug fixes.
+#
+# 2010-12-23
+# version 0.2.2: bug fixes.
+#
+# 2010-10-28
+# version 0.2.1: refactoring mostly
+# * deop_command option removed
+# * removed --webchat switch, freenode's updates made it superfluous.
+# * if WeeChat doesn't know a hostmask, use /userhost or /who if needed.
+# * /oban and /oquiet without arguments show ban/quiet list.
+# * most commands allows '-o' option, that forces immediate deop (without configured delay).
+# * updated for WeeChat 0.3.4 (irc_nick infolist changes)
+#
+# 2010-09-20
+# version 0.2: major update
+# * fixed quiets for ircd-seven (freenode)
+# * implemented user and mask cache.
+# * added commands:
+# - /ovoice /odevoice for de/voice users.
+# - /omode for change channel modes.
+# - /olist for list bans/quiets on cache.
+# * changed /omute and /ounmute commands to /oquiet and /ounquiet, as q masks
+# is refered as a quiet rather than a mute.
+# * autocompletions:
+# - for bans set on a channel.
+# - for make new bans.
+# - for nicks/usernames/hostnames.
+# * /okban renamed to /obankick. This is because /okban is too similar to
+# /okick and bankicking somebody due to tab fail was too easy.
+# * added display_affected feature.
+# * added --webchat ban option.
+# * config options removed:
+# - merge_bans: superseded by isupport methods.
+# - enable_mute: superseded by isupport methods.
+# - invert_kickban_order: now is fixed to "ban, then kick"
+# * Use WeeChat isupport infos.
+# * /oop and /odeop can op/deop other users.
+#
+# 2009-11-9
+# version 0.1.1: fixes
+# * script renamed to 'chanop' because it was causing conflicts with python
+# 'operator' module
+# * added /otopic command
+#
+# 2009-10-31
+# version 0.1: Initial release
+###
+
+WEECHAT_VERSION = (0x30200, '0.3.2')
+
+SCRIPT_NAME = "chanop"
+SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
+SCRIPT_VERSION = "0.3.1"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Helper script for IRC Channel Operators"
+
+# default settings
+settings = {
+'op_command' :'/msg chanserv op $channel $nick',
+'autodeop' :'on',
+'autodeop_delay' :'180',
+'default_banmask' :'host',
+'enable_remove' :'off',
+'kick_reason' :'',
+'enable_multi_kick' :'off',
+'display_affected' :'on',
+'enable_bar' :'on',
+}
+
+try:
+ import weechat
+ from weechat import WEECHAT_RC_OK, prnt
+ import_ok = True
+except ImportError:
+ print("This script must be run under WeeChat.")
+ print("Get WeeChat now at: http://www.weechat.org/")
+ import_ok = False
+
+import os
+import re
+import time
+import string
+import getopt
+from collections import defaultdict
+from shelve import DbfilenameShelf as Shelf
+
+chars = string.maketrans('', '')
+
+# -----------------------------------------------------------------------------
+# Messages
+
+script_nick = SCRIPT_NAME
+def error(s, buffer=''):
+ """Error msg"""
+ prnt(buffer, '%s%s %s' % (weechat.prefix('error'), script_nick, s))
+ value = weechat.config_get_plugin('debug')
+ if value and boolDict[value]:
+ import traceback
+ if traceback.sys.exc_type:
+ trace = traceback.format_exc()
+ prnt('', trace)
+
+def say(s, buffer=''):
+ """normal msg"""
+ prnt(buffer, '%s\t%s' %(script_nick, s))
+
+def _no_debug(*args):
+ pass
+
+debug = _no_debug
+
+# -----------------------------------------------------------------------------
+# Config
+
+# TODO Need to refactor all this too
+
+boolDict = {'on':True, 'off':False, True:'on', False:'off'}
+def get_config_boolean(config, get_function=None, **kwargs):
+ if get_function and callable(get_function):
+ value = get_function(config, **kwargs)
+ else:
+ value = weechat.config_get_plugin(config)
+ try:
+ return boolDict[value]
+ except KeyError:
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is invalid, allowed: 'on', 'off'" %value)
+ return boolDict[default]
+
+def get_config_int(config, get_function=None):
+ if get_function and callable(get_function):
+ value = get_function(config)
+ else:
+ value = weechat.config_get_plugin(config)
+ try:
+ return int(value)
+ except ValueError:
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is not a number." %value)
+ return int(default)
+
+valid_banmask = set(('nick', 'user', 'host', 'exact'))
+def get_config_banmask(config='default_banmask', get_function=None):
+ if get_function and callable(get_function):
+ value = get_function(config)
+ else:
+ value = weechat.config_get_plugin(config)
+ values = value.lower().split(',')
+ for value in values:
+ if value not in valid_banmask:
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is an invalid value, allowed: %s." %(value, ', '.join(valid_banmask)))
+ return default
+ #debug("default banmask: %s" %values)
+ return values
+
+def get_config_list(config):
+ value = weechat.config_get_plugin(config)
+ if value:
+ return value.split(',')
+ else:
+ return []
+
+def get_config_specific(config, server='', channel=''):
+ """Gets config defined for either server or channel."""
+ value = None
+ if server and channel:
+ string = '%s.%s.%s' %(config, server, channel)
+ value = weechat.config_get_plugin(string)
+ if server and not value:
+ string = '%s.%s' %(config, server)
+ value = weechat.config_get_plugin(string)
+ if not value:
+ value = weechat.config_get_plugin(config)
+ return value
+
+# -----------------------------------------------------------------------------
+# Utils
+
+now = lambda: int(time.time())
+
+def time_elapsed(elapsed, ret=None, level=2):
+ time_hour = 3600
+ time_day = 86400
+ time_year = 31536000
+
+ if ret is None:
+ ret = []
+
+ if not elapsed:
+ return ''
+
+ if elapsed > time_year:
+ years, elapsed = elapsed // time_year, elapsed % time_year
+ ret.append('%s%s' %(years, 'y'))
+ elif elapsed > time_day:
+ days, elapsed = elapsed // time_day, elapsed % time_day
+ ret.append('%s%s' %(days, 'd'))
+ elif elapsed > time_hour:
+ hours, elapsed = elapsed // time_hour, elapsed % time_hour
+ ret.append('%s%s' %(hours, 'h'))
+ elif elapsed > 60:
+ mins, elapsed = elapsed // 60, elapsed % 60
+ ret.append('%s%s' %(mins, 'm'))
+ else:
+ secs, elapsed = elapsed, 0
+ ret.append('%s%s' %(secs, 's'))
+
+ if len(ret) >= level or not elapsed:
+ return ' '.join(ret)
+
+ ret = time_elapsed(elapsed, ret, level)
+ return ret
+
+# -----------------------------------------------------------------------------
+# IRC utils
+
+_hostmaskRe = re.compile(r':?\S+!\S+@\S+') # poor but good enough
+def is_hostmask(s):
+ """Returns whether or not the string s starts with something like a hostmask."""
+ return _hostmaskRe.match(s) is not None
+
+def is_ip(s):
+ """Returns whether or not a given string is an IPV4 address."""
+ import socket
+ try:
+ return bool(socket.inet_aton(s))
+ except socket.error:
+ return False
+
+_reCache = {}
+def cachedPattern(f):
+ """Use cached regexp object or compile a new one from pattern."""
+ def getRegexp(pattern, *arg):
+ try:
+ regexp = _reCache[pattern]
+ except KeyError:
+ s = '^'
+ for c in pattern:
+ if c == '*':
+ s += '.*'
+ elif c == '?':
+ s += '.'
+ elif c in '[{':
+ s += r'[\[{]'
+ elif c in ']}':
+ s += r'[\]}]'
+ elif c in '|\\':
+ s += r'[|\\]'
+ else:
+ s += re.escape(c)
+ s += '$'
+ regexp = re.compile(s, re.I)
+ _reCache[pattern] = regexp
+ return f(regexp, *arg)
+ return getRegexp
+
+def hostmaskPattern(f):
+ """Check if pattern is for match a hostmask and remove ban forward if there's one."""
+ def checkPattern(pattern, arg):
+ # XXX this needs a refactor
+ if is_hostmask(pattern):
+ # nick!user@host$#channel
+ if '$' in pattern:
+ pattern = pattern.partition('$')[0]
+ if isinstance(arg, list):
+ arg = [ s for s in arg if is_hostmask(s) ]
+ elif not is_hostmask(arg):
+ return ''
+
+ rt = f(pattern, arg)
+ # this doesn't match any mask in args with a channel forward
+ pattern += '$*'
+ if isinstance(arg, list):
+ rt.extend(f(pattern, arg))
+ elif not rt:
+ rt = f(pattern, arg)
+ return rt
+
+ return ''
+ return checkPattern
+
+match_string = lambda r, s: r.match(s) is not None
+match_list = lambda r, L: [ s for s in L if r.match(s) is not None ]
+
+pattern_match = cachedPattern(match_string)
+pattern_match_list = cachedPattern(match_list)
+hostmask_match = hostmaskPattern(pattern_match)
+hostmask_match_list = hostmaskPattern(pattern_match_list)
+
+def get_nick(s):
+ """':nick!user@host' => 'nick'"""
+ return weechat.info_get('irc_nick_from_host', s)
+
+def get_user(s, trim=False):
+ """'nick!user@host' => 'user'"""
+ assert is_hostmask(s), "Invalid hostmask: %s" % s
+ s = s[s.find('!') + 1:s.find('@')]
+ if trim:
+ # remove the stuff not part of the username.
+ if s[0] == '~':
+ return s[1:]
+ elif s[:2] in ('i=', 'n='):
+ return s[2:]
+ return s
+
+def get_host(s):
+ """'nick!user@host' => 'host'"""
+ assert is_hostmask(s), "Invalid hostmask: %s" % s
+ if ' ' in s:
+ return s[s.find('@') + 1:s.find(' ')]
+ return s[s.find('@') + 1:]
+
+def is_channel(s):
+ return weechat.info_get('irc_is_channel', s)
+
+def is_nick(s):
+ return weechat.info_get('irc_is_nick', s)
+
+def irc_buffer(buffer):
+ """Returns pair (server, channel) or None if buffer isn't an irc channel"""
+ get_string = weechat.buffer_get_string
+ if get_string(buffer, 'plugin') == 'irc' \
+ and get_string(buffer, 'localvar_type') == 'channel':
+ channel = get_string(buffer, 'localvar_channel')
+ server = get_string(buffer, 'localvar_server')
+ return (server, channel)
+
+# -----------------------------------------------------------------------------
+# WeeChat classes
+
+class InvalidIRCBuffer(Exception):
+ pass
+
+def catchExceptions(f):
+ def function(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except Exception as e:
+ error(e)
+ return function
+
+def callback(method):
+ """This function will take a bound method or function and make it a callback."""
+ # try to create a descriptive and unique name.
+ func = method.func_name
+ try:
+ im_self = method.im_self
+ try:
+ inst = im_self.__name__
+ except AttributeError:
+ try:
+ inst = im_self.name
+ except AttributeError:
+ raise Exception("Instance of %s has no __name__ attribute" %type(im_self))
+ cls = type(im_self).__name__
+ name = '_'.join((cls, inst, func))
+ except AttributeError:
+ # not a bound method
+ name = func
+
+ method = catchExceptions(method)
+
+ # set our callback
+ import __main__
+ setattr(__main__, name, method)
+ return name
+
+def weechat_command(buffer, cmd):
+ """I want to always keep debug() calls for this"""
+ if buffer:
+ debug("%ssending: %r buffer: %s", COLOR_WHITE, cmd, buffer)
+ else:
+ debug("%ssending: %r", COLOR_WHITE, cmd)
+ weechat.command(buffer, cmd)
+
+class Infolist(object):
+ """Class for reading WeeChat's infolists."""
+
+ fields = {
+ 'name' :'string',
+ 'option_name' :'string',
+ 'value' :'string',
+ 'host' :'string',
+ 'flags' :'integer',
+ 'prefixes' :'string',
+ 'is_connected':'integer',
+ 'buffer' :'pointer',
+ }
+
+ _use_flags = False
+
+ def __init__(self, name, args='', pointer=''):
+ self.cursor = 0
+ #debug('Generating infolist %r %r', name, args)
+ self.pointer = weechat.infolist_get(name, pointer, args)
+ if self.pointer == '':
+ raise Exception("Infolist initialising failed (name:'%s' args:'%s')" %(name, args))
+
+ def __len__(self):
+ """True False evaluation."""
+ if self.pointer:
+ return 1
+ else:
+ return 0
+
+ def __del__(self):
+ """Purge infolist if is no longer referenced."""
+ self.free()
+
+ def __getitem__(self, name):
+ """Implement the evaluation of self[name]."""
+ if self._use_flags and name == 'prefixes':
+ name = 'flags'
+ value = getattr(weechat, 'infolist_%s' %self.fields[name])(self.pointer, name)
+ if self._use_flags and name == 'flags':
+ value = self._flagsAsString(value)
+ return value
+
+ def _flagsAsString(self, n):
+ s = ''
+ if n & 32:
+ s += '+'
+ if n & 8:
+ s += '@'
+ return s
+
+ def __iter__(self):
+ def generator():
+ while self.next():
+ yield self
+ return generator()
+
+ def next(self):
+ self.cursor = weechat.infolist_next(self.pointer)
+ return self.cursor
+
+ def prev(self):
+ self.cursor = weechat.infolist_prev(self.pointer)
+ return self.cursor
+
+ def reset(self):
+ """Moves cursor to beginning of infolist."""
+ if self.cursor == 1: # only if we aren't in the beginning already
+ while self.prev():
+ pass
+
+ def free(self):
+ if self.pointer:
+ weechat.infolist_free(self.pointer)
+ self.pointer = ''
+
+def nick_infolist(server, channel):
+ try:
+ return Infolist('irc_nick', '%s,%s' % (server, channel))
+ except:
+ raise InvalidIRCBuffer('%s.%s' % (server, channel))
+
+class NoArguments(Exception):
+ pass
+
+class ArgumentError(Exception):
+ pass
+
+class Command(object):
+ """Class for hook WeeChat commands."""
+ description, usage, help = "WeeChat command.", "[define usage template]", "detailed help here"
+ command = ''
+ completion = ''
+
+ def __init__(self):
+ assert self.command, "No command defined"
+ self.__name__ = self.command
+ self._pointer = ''
+ self._callback = ''
+
+ def __call__(self, *args):
+ return self.callback(*args)
+
+ def callback(self, data, buffer, args):
+ """Called by WeeChat when /command is used."""
+ self.data, self.buffer, self.args = data, buffer, args
+ debug("%s[%s] data: \"%s\" buffer: \"%s\" args: \"%s\"", COLOR_DARKGRAY,
+ self.command,
+ data,
+ buffer,
+ args)
+ try:
+ self.parser(args) # argument parsing
+ except ArgumentError as e:
+ error('Argument error, %s' %e)
+ except NoArguments:
+ pass
+ else:
+ self.execute()
+ return WEECHAT_RC_OK
+
+ def parser(self, args):
+ """Argument parsing, override if needed."""
+ pass
+
+ def execute(self):
+ """This method is called when the command is run, override this."""
+ pass
+
+ def hook(self):
+ assert not self._pointer, \
+ "There's already a hook pointer, unhook first (%s)" %self.command
+ self._callback = callback(self.callback)
+ pointer = weechat.hook_command(self.command,
+ self.description,
+ self.usage,
+ self.help,
+ self.completion,
+ self._callback, '')
+ if pointer == '':
+ raise Exception("hook_command failed: %s %s" % (SCRIPT_NAME, self.command))
+ self._pointer = pointer
+
+ def unhook(self):
+ if self._pointer:
+ weechat.unhook(self._pointer)
+ self._pointer = ''
+ self._callback = ''
+
+class Bar(object):
+ def __init__(self, name, hidden=False, items=''):
+ self.name = name
+ self.hidden = hidden
+ self._pointer = ''
+ self._items = items
+
+ def new(self):
+ assert not self._pointer, "Bar %s already created" % self.name
+ pointer = weechat.bar_search(self.name)
+ if not pointer:
+ pointer = weechat.bar_new(self.name, boolDict[self.hidden], '0', 'window',
+ 'active', 'bottom', 'horizontal', 'vertical',
+ '0', '1', 'default', 'cyan', 'blue', 'off',
+ self._items)
+ if not pointer:
+ raise Exception("bar_new failed: %s %s" % (SCRIPT_NAME, self.name))
+
+ self._pointer = pointer
+
+ def getPointer(self):
+ return weechat.bar_search(self.name)
+
+ def show(self):
+ pointer = self.getPointer()
+ if pointer and self.hidden:
+ weechat.bar_set(pointer, 'hidden', 'off')
+ self.hidden = False
+ return pointer
+
+ def hide(self):
+ pointer = self.getPointer()
+ if pointer and not self.hidden:
+ weechat.bar_set(pointer, 'hidden', 'on')
+ self.hidden = True
+
+ def remove(self):
+ pointer = self.getPointer()
+ if pointer:
+ weechat.bar_remove(pointer)
+ self._pointer = ''
+
+ def __len__(self):
+ """True False evaluation."""
+ if self.getPointer():
+ return 1
+ else:
+ return 0
+
+class PopupBar(Bar):
+ _timer_hook = ''
+ popup_mode = False
+
+ def popup(self, delay=10):
+ if self.show():
+ if self._timer_hook:
+ weechat.unhook(self._timer_hook)
+ self._timer_hook = weechat.hook_timer(delay * 1000, 0, 1, callback(self._timer), '')
+
+ def _timer(self, data, counter):
+ self.hide()
+ self._timer_hook = ''
+ return WEECHAT_RC_OK
+
+# -----------------------------------------------------------------------------
+# Per buffer variables
+
+class BufferVariables(dict):
+ """Keeps variables and objects of a specific buffer."""
+ def __init__(self, buffer):
+ self['buffer'] = buffer
+ self['irc'] = IrcCommands(buffer)
+ self['autodeop'] = True
+ self['deopHook'] = self.opHook = self.opTimeout = None
+ self['server'] = weechat.buffer_get_string(buffer, 'localvar_server')
+ self['channel'] = weechat.buffer_get_string(buffer, 'localvar_channel')
+ self['nick'] = weechat.info_get('irc_nick', self.server)
+
+ def __getattr__(self, k):
+ return self[k]
+
+ def __setattr__(self, k, v):
+ debug(' -- buffer[%s] %s ... %s => %s', self.buffer, k, self.get(k), v)
+ self[k] = v
+
+class ChanopBuffers(object):
+ """Keeps track of BuffersVariables instances in chanop."""
+ buffer = ''
+ _buffer = {} # must be shared across instances
+ def __getattr__(self, k):
+ return self._buffer[self.buffer][k]
+
+ def setup(self, buffer):
+ self.buffer = buffer
+ if buffer not in self._buffer:
+ self._buffer[buffer] = BufferVariables(buffer)
+ else:
+ # update nick, it might have changed.
+ self.vars.nick = weechat.info_get('irc_nick', self.vars.server)
+
+ @property
+ def vars(self):
+ return self._buffer[self.buffer]
+
+ def varsOf(self, buffer):
+ return self._buffer[buffer]
+
+ def replace_vars(self, s):
+ try:
+ return weechat.buffer_string_replace_local_var(self.buffer, s)
+ except AttributeError:
+ if '$channel' in s:
+ s = s.replace('$channel', self.channel)
+ if '$nick' in s:
+ s = s.replace('$nick', self.nick)
+ if '$server' in s:
+ s = s.replace('$server', self.server)
+ return s
+
+ def get_config(self, config):
+ #debug('config: %s' %config)
+ return get_config_specific(config, self.server, self.channel)
+
+ def get_config_boolean(self, config):
+ return get_config_boolean(config, self.get_config)
+
+ def get_config_int(self, config):
+ return get_config_int(config, self.get_config)
+
+# -----------------------------------------------------------------------------
+# IRC messages queue
+
+class Message(ChanopBuffers):
+ command = None
+ args = ()
+ wait = 0
+ def __init__(self, cmd=None, args=(), wait=0):
+ if cmd: self.command = cmd
+ if args: self.args = args
+ if wait: self.wait = wait
+
+ def payload(self):
+ cmd = self.command
+ if cmd[0] != '/':
+ cmd = '/' + cmd
+ if self.args:
+ cmd += ' ' + ' '.join(self.args)
+ if self.wait:
+ cmd = '/wait %s ' %self.wait + cmd
+ return cmd
+
+ def register(self, buffer):
+ self.buffer = buffer
+
+ def __call__(self):
+ cmd = self.payload()
+ if cmd:
+ self.send(cmd)
+
+ def send(self, cmd):
+ weechat_command(self.buffer, cmd)
+
+ def __repr__(self):
+ return '<Message(%s, %s)>' %(self.command, self.args)
+
+class IrcCommands(ChanopBuffers):
+ """Class that manages and sends the script's commands to WeeChat."""
+
+ # Special message classes
+ class OpMessage(Message):
+ def send(self, cmd):
+ if self.irc.checkOp():
+ # nothing to do
+ return
+
+ self.irc.interrupt = True
+ Message.send(self, cmd)
+
+ def modeOpCallback(buffer, signal, signal_data):
+ vars = self.varsOf(buffer)
+ data = 'MODE %s +o %s' % (vars.channel, vars.nick)
+ signal = signal_data.split(None, 1)[1]
+ if signal == data:
+ debug('GOT OP')
+ # add this channel to our watchlist
+ config = 'watchlist.%s' % vars.server
+ channels = CaseInsensibleSet(get_config_list(config))
+ if vars.channel not in channels:
+ channels.add(vars.channel)
+ value = ','.join(channels)
+ weechat.config_set_plugin(config, value)
+ weechat.unhook(vars.opHook)
+ weechat.unhook(vars.opTimeout)
+ vars.opTimeout = vars.opHook = None
+ vars.irc.interrupt = False
+ vars.irc.run()
+ return WEECHAT_RC_OK
+
+ def timeoutCallback(buffer, count):
+ vars = self.varsOf(buffer)
+ error("Couldn't get op in '%s', purging command queue..." % vars.channel)
+ weechat.unhook(vars.opHook)
+ if vars.deopHook:
+ weechat.unhook(vars.deopHook)
+ vars.deopHook = None
+ vars.opTimeout = vars.opHook = None
+ vars.irc.interrupt = False
+ vars.irc.clear()
+ return WEECHAT_RC_OK
+
+ # wait for a while before timing out.
+ self.vars.opTimeout = weechat.hook_timer(30*1000, 0, 1, callback(timeoutCallback),
+ self.buffer)
+
+ self.vars.opHook = weechat.hook_signal('%s,irc_in2_MODE' %self.server,
+ callback(modeOpCallback), self.buffer)
+
+ class UserhostMessage(Message):
+ def send(self, cmd):
+ self.irc.interrupt = True
+ Message.send(self, cmd)
+
+ def msgCallback(buffer, modifier, modifier_data, string):
+ vars = self.varsOf(buffer)
+ if vars.server != modifier_data:
+ return string
+ nick, host = string.rsplit(None, 1)[1].split('=')
+ nick, host = nick.strip(':*'), host[1:]
+ hostmask = '%s!%s' % (nick, host)
+ debug('USERHOST: %s %s', nick, hostmask)
+ userCache.remember(modifier_data, nick, hostmask)
+ weechat.unhook(vars.msgHook)
+ weechat.unhook(vars.msgTimeout)
+ vars.msgTimeout = vars.msgHook = None
+ vars.irc.interrupt = False
+ vars.irc.run()
+ return ''
+
+ def timeoutCallback(buffer, count):
+ vars = self.varsOf(buffer)
+ weechat.unhook(vars.msgHook)
+ vars.msgTimeout = vars.msgHook = None
+ vars.irc.interrupt = False
+ vars.irc.clear()
+ return WEECHAT_RC_OK
+
+ # wait for a while before timing out.
+ self.vars.msgTimeout = \
+ weechat.hook_timer(30*1000, 0, 1, callback(timeoutCallback), self.buffer)
+
+ self.vars.msgHook = weechat.hook_modifier('irc_in_302',
+ callback(msgCallback), self.buffer)
+
+
+ class ModeMessage(Message):
+ command = 'mode'
+ def __init__(self, char=None, args=None, **kwargs):
+ self.chars = [ char ]
+ self.charargs = [ args ]
+ self.args = (char, args)
+ Message.__init__(self, **kwargs)
+
+ def payload(self):
+ args = []
+ modeChar = []
+ prefix = ''
+ for m, a in zip(self.chars, self.charargs):
+ if a:
+ if callable(a):
+ a = a()
+ if not a:
+ continue
+ args.append(a)
+ if m[0] != prefix:
+ prefix = m[0]
+ modeChar.append(prefix)
+ modeChar.append(m[1])
+ args.insert(0, ''.join(modeChar))
+ if args:
+ self.args = args
+ return Message.payload(self)
+
+
+ class DeopMessage(ModeMessage):
+ def send(self, cmd):
+ if self.irc.checkOp():
+ Message.send(self, cmd)
+
+ # IrcCommands methods
+ def __init__(self, buffer):
+ self.interrupt = False
+ self.commands = []
+ self.buffer = buffer
+
+ def checkOp(self):
+ infolist = nick_infolist(self.server, self.channel)
+ while infolist.next():
+ if infolist['name'] == self.nick:
+ return '@' in infolist['prefixes']
+ return False
+
+ def Op(self):
+ if self.opHook and self.opTimeout:
+ # already send command, wait for timeout
+ return
+
+ value = self.replace_vars(self.get_config('op_command'))
+ if not value:
+ raise Exception("No command defined for get op.")
+ msg = self.OpMessage(value)
+ self.queue(msg, insert=True)
+
+ def Deop(self):
+ msg = self.DeopMessage('-o', self.nick)
+ self.queue(msg)
+
+ def Mode(self, mode, args=None, wait=0):
+ msg = self.ModeMessage(mode, args, wait=wait)
+ self.queue(msg)
+
+ def Kick(self, nick, reason=None, wait=0):
+ if not reason:
+ reason = self.get_config('kick_reason')
+ if self.get_config_boolean('enable_remove'):
+ cmd = '/quote remove %s %s :%s' %(self.channel, nick, reason)
+ msg = Message(cmd, wait=wait)
+ else:
+ msg = Message('kick', (nick, reason), wait=wait)
+ self.queue(msg)
+
+ def Voice(self, nick):
+ self.Mode('+v', nick)
+
+ def Devoice(self, nick):
+ self.Mode('-v', nick)
+
+ def Userhost(self, nick):
+ msg = self.UserhostMessage('USERHOST', (nick, ))
+ self.queue(msg, insert=True) # USERHOST should be sent first
+
+ def queue(self, message, insert=False):
+ debug('queuing: %s', message)
+ # merge /modes
+ if self.commands and message.command == 'mode':
+ max_modes = supported_maxmodes(self.server)
+ msg = self.commands[-1]
+ if msg.command == 'mode' and len(msg.chars) < max_modes:
+ msg.chars.append(message.chars[0])
+ msg.charargs.append(message.charargs[0])
+ return
+ if insert:
+ self.commands.insert(0, message)
+ else:
+ self.commands.append(message)
+
+ # it happened once and it wasn't pretty
+ def safe_check(f):
+ def abort_if_too_many_commands(self):
+ if len(self.commands) > 10:
+ error("Limit of 10 commands in queue reached, aborting.")
+ self.clear()
+ else:
+ f(self)
+ return abort_if_too_many_commands
+
+ @safe_check
+ def run(self):
+ while self.commands and not self.interrupt:
+ msg = self.commands.pop(0)
+ msg.register(self.buffer)
+ msg()
+ if self.interrupt:
+ #debug("Interrupting queue")
+ break
+
+ def clear(self):
+ debug('clear queue (%s messages)', len(self.commands))
+ self.commands = []
+
+ def __repr__(self):
+ return '<IrcCommands(%s)>' % ', '.join(map(repr, self.commands))
+
+# -----------------------------------------------------------------------------
+# User/Mask classes
+
+_rfc1459trans = string.maketrans(string.ascii_uppercase + r'\[]',
+ string.ascii_lowercase + r'|{}')
+def IRClower(s):
+ return s.translate(_rfc1459trans)
+
+class CaseInsensibleString(str):
+ def __init__(self, s=''):
+ self.lowered = IRClower(s)
+
+ lower = lambda self: self.lowered
+ translate = lambda self, trans: self.lowered
+ __eq__ = lambda self, s: self.lowered == IRClower(s)
+ __ne__ = lambda self, s: not self == s
+ __hash__ = lambda self: hash(self.lowered)
+
+def caseInsensibleKey(k):
+ if isinstance(k, str):
+ return CaseInsensibleString(k)
+ elif isinstance(k, tuple):
+ return tuple(map(caseInsensibleKey, k))
+ return k
+
+class CaseInsensibleDict(dict):
+ key = staticmethod(caseInsensibleKey)
+
+ def __init__(self, **kwargs):
+ for k, v in kwargs.items():
+ self[k] = v
+
+ def __setitem__(self, k, v):
+ dict.__setitem__(self, self.key(k), v)
+
+ def __getitem__(self, k):
+ return dict.__getitem__(self, self.key(k))
+
+ def __delitem__(self, k):
+ dict.__delitem__(self, self.key(k))
+
+ def __contains__(self, k):
+ return dict.__contains__(self, self.key(k))
+
+ def pop(self, k):
+ return dict.pop(self, self.key(k))
+
+class CaseInsensibleDefaultDict(defaultdict, CaseInsensibleDict):
+ pass
+
+class CaseInsensibleSet(set):
+ normalize = staticmethod(caseInsensibleKey)
+
+ def __init__(self, iterable=()):
+ iterable = map(self.normalize, iterable)
+ set.__init__(self, iterable)
+
+ def __contains__(self, v):
+ return set.__contains__(self, self.normalize(v))
+
+ def update(self, L):
+ set.update(self, map(self.normalize, L))
+
+ def add(self, v):
+ set.add(self, self.normalize(v))
+
+ def remove(self, v):
+ set.remove(self, self.normalize(v))
+
+class ChannelWatchlistSet(CaseInsensibleSet):
+ _updated = False
+ def __contains__(self, v):
+ if not self._updated:
+ self.__updateFromConfig()
+ return CaseInsensibleSet.__contains__(self, v)
+
+ def __updateFromConfig(self):
+ self._updated = True
+ infolist = Infolist('option', 'plugins.var.python.%s.watchlist.*' %SCRIPT_NAME)
+ n = len('python.%s.watchlist.' %SCRIPT_NAME)
+ while infolist.next():
+ name = infolist['option_name']
+ value = infolist['value']
+ server = name[n:]
+ if value:
+ channels = value.split(',')
+ else:
+ channels = []
+ self.update([ (server, channel) for channel in channels ])
+
+chanopChannels = ChannelWatchlistSet()
+
+class ServerChannelDict(CaseInsensibleDict):
+ def getChannels(self, server, item=None):
+ """Return a list of channels that match server and has item if given"""
+ if item:
+ return [ chan for serv, chan in self if serv == server and item in self[serv, chan] ]
+ else:
+ return [ chan for serv, chan in self if serv == server ]
+
+ def purge(self):
+ for key in self.keys():
+ if key not in chanopChannels:
+ debug('removing %s mask list, not in watchlist.', key)
+ del self[key]
+ for data in self.values():
+ data.purge()
+
+# -----------------------------------------------------------------------------
+# Channel Modes (bans)
+
+class MaskObject(object):
+ def __init__(self, mask, hostmask=[], operator='', date=0, expires=0):
+ self.mask = mask
+ self.operator = operator
+ if date:
+ date = int(date)
+ else:
+ date = now()
+ self.date = date
+ if isinstance(hostmask, str):
+ hostmask = [ hostmask ]
+ self.hostmask = hostmask
+ self.expires = int(expires)
+
+ def serialize(self):
+ data = ';'.join([ self.operator,
+ str(self.date),
+ str(self.expires),
+ ','.join(self.hostmask) ])
+ return data
+
+ def deserialize(self, data):
+ op, date, expires, hostmasks = data.split(';')
+ assert op and date, "Error reading chanmask option %s, missing operator or date" % self.mask
+ if not is_hostmask(op):
+ raise Exception('Error reading chanmask option %s, invalid usermask %r' \
+ % (self.mask, op))
+
+ self.operator = op
+ try:
+ self.date = int(date)
+ except ValueError:
+ self.date = int(time.mktime(time.strptime(date,'%Y-%m-%d %H:%M:%S')))
+ if expires:
+ self.expires = int(expires)
+ else:
+ self.expires = 0
+ if hostmasks:
+ hostmasks = hostmasks.split(',')
+ if not all(map(is_hostmask, hostmasks)):
+ raise Exception('Error reading chanmask option %s, a hostmask is invalid: %s' \
+ % (self.mask, hostmasks))
+
+ self.hostmask = hostmasks
+
+ def __repr__(self):
+ return "<MaskObject(%s)>" % self.mask
+
+class MaskList(CaseInsensibleDict):
+ """Single list of masks"""
+ def __init__(self, server, channel):
+ self.synced = 0
+
+ def add(self, mask, **kwargs):
+ if mask in self:
+ # mask exists, update it
+ ban = self[mask]
+ for attr, value in kwargs.items():
+ if value and not getattr(ban, attr):
+ setattr(ban, attr, value)
+ else:
+ ban = self[mask] = MaskObject(mask, **kwargs)
+ return ban
+
+# def searchByNick(self, nick):
+# try:
+# hostmask = userCache.getHostmask(nick, self.server, self.channel)
+# return self.searchByHostmask(hostmask)
+# except KeyError:
+# return []
+
+ def search(self, pattern, reverseMatch=False):
+ if reverseMatch:
+ L = [ mask for mask in self if hostmask_match(mask, pattern) ]
+ else:
+ L = pattern_match_list(pattern, self.keys())
+ return L
+
+ def purge(self):
+ pass
+
+class MaskCache(ServerChannelDict):
+ """Keeps a cache of masks for different channels."""
+ def add(self, server, channel, mask, **kwargs):
+ """Adds a ban to (server, channel) banlist."""
+ key = (server, channel)
+ if key not in self:
+ self[key] = MaskList(*key)
+ ban = self[key].add(mask, **kwargs)
+ return ban
+
+ def remove(self, server, channel, mask=None):#, hostmask=None):
+ key = (server, channel)
+ try:
+ if mask is None:
+ del self[key]
+ else:
+ del self[key][mask]
+ #debug("removing ban: %s" %banmask)
+ except KeyError:
+ pass
+
+class ChanopCache(Shelf):
+ def __init__(self, filename):
+ path = os.path.join(weechat.info_get('weechat_dir', ''), filename)
+ Shelf.__init__(self, path, writeback=True)
+
+class ModeCache(ChanopCache):
+ """class for store channel modes lists."""
+ def __init__(self, filename):
+ ChanopCache.__init__(self, filename)
+ self.modes = set()
+ self.map = CaseInsensibleDict()
+
+ # reset all sync timers
+ for cache in self.values():
+ for masklist in cache.values():
+ masklist.synced = 0
+
+ def registerMode(self, mode, *args):
+ if mode not in self:
+ cache = MaskCache()
+ self[mode] = cache
+
+ if mode not in self.modes:
+ self.modes.add(mode)
+
+ self.map[mode] = mode
+ for name in args:
+ self.map[name] = mode
+
+ def __getitem__(self, mode):
+ try:
+ return ChanopCache.__getitem__(self, mode)
+ except KeyError:
+ return ChanopCache.__getitem__(self, self.map[mode])
+
+ def add(self, server, channel, mode, mask, **kwargs):
+ assert mode in self.modes
+ self[mode].add(server, channel, mask, **kwargs)
+
+ def remove(self, server, channel, mode, mask):
+ self[mode].remove(server, channel, mask)
+
+ def purge(self):
+ for cache in self.values():
+ cache.purge()
+
+class MaskSync(object):
+ """Class for fetch and sync bans of any channel and mode."""
+ __name__ = ''
+ _hide_msg = False
+
+ _hook_mask = ''
+ _hook_end = ''
+
+ # freenode new signals for list quiet messages
+ _hook_quiet_mask = ''
+ _hook_quiet_end = ''
+
+ # sync queue stuff
+ queue = []
+ _maskbuffer = CaseInsensibleDefaultDict(list)
+ _callback = CaseInsensibleDict()
+
+ def hook(self):
+ # 367 - ban mask
+ # 368 - end of ban list
+ # 728 - quiet mask
+ # 729 - end of quiet list
+ self.unhook()
+ self._hook_mask = \
+ weechat.hook_modifier('irc_in_367', callback(self._maskCallback), '')
+ self._hook_end = \
+ weechat.hook_modifier('irc_in_368', callback(self._endCallback), '')
+ self._hook_quiet_mask = \
+ weechat.hook_modifier('irc_in_728', callback(self._maskCallback), '')
+ self._hook_quiet_end = \
+ weechat.hook_modifier('irc_in_729', callback(self._endCallback), '')
+
+ def unhook(self):
+ for hook in ('_hook_mask',
+ '_hook_end',
+ '_hook_quiet_mask',
+ '_hook_quiet_end'):
+ attr = getattr(self, hook)
+ if attr:
+ weechat.unhook(attr)
+ setattr(self, hook, '')
+
+ def fetch(self, server, channel, mode, callback=None):
+ """Fetches masks for a given server and channel."""
+ buffer = weechat.buffer_search('irc', 'server.%s' %server)
+ if not buffer or not weechat.info_get('irc_is_channel', channel):
+ # invalid server or channel
+ return
+
+ # check modes
+ if mode not in supported_modes(server):
+ return
+ maskCache = modeCache[mode]
+ key = (server, channel)
+ # check the last time we did this
+ try:
+ masklist = maskCache[key]
+ if (now() - masklist.synced) < 60:
+ # don't fetch again
+ return
+ except KeyError:
+ pass
+
+ if not self.queue:
+ self.queue.append((server, channel, mode))
+ self._fetch(server, channel, mode)
+ elif (server, channel, mode) not in self.queue:
+ self.queue.append((server, channel, mode))
+
+ if callback:
+ self._callback[server, channel] = callback
+
+ def _fetch(self, server, channel, mode):
+ buffer = weechat.buffer_search('irc', 'server.%s' %server)
+ if not buffer:
+ return
+ cmd = '/mode %s %s' %(channel, mode)
+ self._hide_msg = True
+ weechat_command(buffer, cmd)
+
+ def _maskCallback(self, data, modifier, modifier_data, string):
+ """callback for store a single mask."""
+ #debug("MASK %s: %s %s", modifier, modifier_data, string)
+ args = string.split()
+ if self.queue:
+ server, channel, _ = self.queue[0]
+ else:
+ server, channel = modifier_data, args[3]
+
+ if modifier == 'irc_in_367':
+ try:
+ mask, op, date = args[4:]
+ except IndexError:
+ mask = args[4]
+ op = date = None
+ elif modifier == 'irc_in_728':
+ mask, op, date = args[5:]
+
+ # store temporally until "end list" msg
+ self._maskbuffer[server, channel].append((mask, op, date))
+ if self._hide_msg:
+ string = ''
+ return string
+
+ def _endCallback(self, data, modifier, modifier_data, string):
+ """callback for end of channel's mask list."""
+ #debug("MASK END %s: %s %s", modifier, modifier_data, string)
+ if self.queue:
+ server, channel, mode = self.queue.pop(0)
+ else:
+ args = string.split()
+ server, channel = modifier_data, args[3]
+ if modifier == 'irc_in_368':
+ mode = args[7]
+ elif modifier == 'irc_in_729':
+ mode = args[4]
+ else:
+ return string
+
+ maskCache = modeCache[mode]
+
+ # delete old masks in cache
+ if (server, channel) in maskCache:
+ masklist = maskCache[server, channel]
+ banmasks = [ L[0] for L in self._maskbuffer[server, channel] ]
+ for mask in masklist.keys():
+ if mask not in banmasks:
+ del masklist[mask]
+
+ for banmask, op, date in self._maskbuffer[server, channel]:
+ maskCache.add(server, channel, banmask, operator=op, date=date)
+ del self._maskbuffer[server, channel]
+ try:
+ maskList = maskCache[server, channel]
+ except KeyError:
+ maskList = maskCache[server, channel] = MaskList(server, channel)
+ maskList.synced = now()
+
+ # run hooked functions if any
+ if (server, channel) in self._callback:
+ self._callback[server, channel]()
+ del self._callback[server, channel]
+
+ if self._hide_msg:
+ string = ''
+ if self.queue:
+ next = self.queue[0]
+ self._fetch(*next)
+ else:
+ assert not self._maskbuffer, "mask buffer not empty: %s" % self._maskbuffer.keys()
+ self._hide_msg = False
+ return string
+
+maskSync = MaskSync()
+
+# -----------------------------------------------------------------------------
+# User cache
+
+class UserObject(object):
+ def __init__(self, nick, hostmask=None):
+ self.nick = nick
+ if hostmask:
+ self._hostmask = [ hostmask ]
+ else:
+ self._hostmask = []
+ self.seen = now()
+ self._channels = 0
+
+ @property
+ def hostmask(self):
+ try:
+ return self._hostmask[-1]
+ except IndexError:
+ return ''
+
+ def update(self, hostmask=None):
+ if hostmask and hostmask != self.hostmask:
+ if hostmask in self._hostmask:
+ del self._hostmask[self._hostmask.index(hostmask)]
+ self._hostmask.append(hostmask)
+ self.seen = now()
+
+ def __len__(self):
+ return len(self.hostmask)
+
+ def __repr__(self):
+ return '<UserObject(%s)>' %(self.hostmask or self.nick)
+
+class ServerUserList(CaseInsensibleDict):
+ def __init__(self, server):
+ self.server = server
+ buffer = weechat.buffer_search('irc', 'server.%s' %server)
+ self.irc = IrcCommands(buffer)
+ self._purge_time = 3600*4 # 4 hours
+
+ def getHostmask(self, nick):
+ user = self[nick]
+ return user.hostmask
+
+ def purge(self):
+ """Purge old nicks"""
+ n = now()
+ for nick, user in self.items():
+ if user._channels < 1 and (n - user.seen) > self._purge_time:
+ #debug('purging old user: %s' % nick)
+ del self[nick]
+
+class UserList(ServerUserList):
+ def __init__(self, server, channel):
+ self.server = server
+ self.channel = channel
+ self._purge_list = CaseInsensibleDict()
+ self._purge_time = 3600*2 # 2 hours
+
+ def __setitem__(self, nick, user):
+ #debug('%s %s: join, %s', self.server, self.channel, nick)
+ if nick not in self:
+ user._channels += 1
+ if nick in self._purge_list:
+ #debug(' - removed from purge list')
+ del self._purge_list[nick]
+ ServerUserList.__setitem__(self, nick, user)
+
+ def part(self, nick):
+ try:
+ #debug('%s %s: part, %s', self.server, self.channel, nick)
+ user = self[nick]
+ self._purge_list[nick] = user
+ except KeyError:
+ pass
+
+ def values(self):
+ if not all(ServerUserList.values(self)):
+ userCache.who(self.server, self.channel)
+ return sorted(ServerUserList.values(self), key=lambda x:x.seen, reverse=True)
+
+ def hostmasks(self, sorted=False, all=False):
+ if sorted:
+ users = self.values()
+ else:
+ users = ServerUserList.values(self)
+ if all:
+ # return all known hostmasks
+ return [ hostmask for user in users for hostmask in user._hostmask ]
+ else:
+ # only current hostmasks
+ return [ user.hostmask for user in users if user._hostmask ]
+
+ def nicks(self, *args, **kwargs):
+# if not all(self.itervalues()):
+# userCache.who(self.server, self.channel)
+ L = list(self.items())
+ L.sort(key=lambda x:x[1].seen)
+ return reversed([x[0] for x in L])
+
+ def getHostmask(self, nick):
+ try:
+ user = self[nick]
+ except KeyError:
+ user = userCache[self.server][nick]
+ return user.hostmask
+
+ def purge(self):
+ """Purge old nicks"""
+ n = now()
+ for nick, user in self._purge_list.items():
+ if (n - user.seen) > self._purge_time:
+ #debug('%s %s: forgeting about %s', self.server, self.channel, nick)
+ user._channels -= 1
+ try:
+ del self._purge_list[nick]
+ del self[nick]
+ except KeyError:
+ pass
+
+class UserCache(ServerChannelDict):
+ __name__ = ''
+ servercache = CaseInsensibleDict()
+ _hook_who = _hook_who_end = None
+ _channels = CaseInsensibleSet()
+
+ def generateCache(self, server, channel):
+ debug('* building cache: %s %s', server, channel)
+ users = UserList(server, channel)
+ try:
+ infolist = nick_infolist(server, channel)
+ except:
+ # better to fail silently
+ #debug('invalid buffer')
+ return users
+
+ while infolist.next():
+ nick = infolist['name']
+ host = infolist['host']
+ if host:
+ hostmask = '%s!%s' %(nick, host)
+ else:
+ hostmask = ''
+ user = self.remember(server, nick, hostmask)
+ users[nick] = user
+ self[server, channel] = users
+ debug("new cache of %s users", len(users))
+ return users
+
+ def remember(self, server, nick, hostmask):
+ cache = self[server]
+ try:
+ user = cache[nick]
+ if hostmask:
+ user.update(hostmask)
+ except KeyError:
+ #debug("%s: new user %s %s", server, nick, hostmask)
+ user = UserObject(nick, hostmask)
+ cache[nick] = user
+ return user
+
+ def __getitem__(self, k):
+ if isinstance(k, tuple):
+ try:
+ return ServerChannelDict.__getitem__(self, k)
+ except KeyError:
+ return self.generateCache(*k)
+ elif isinstance(k, str):
+ try:
+ return self.servercache[k]
+ except KeyError:
+ cache = self.servercache[k] = ServerUserList(k)
+ return cache
+
+ def __delitem__(self, k):
+ # when we delete a channel, we need to reduce user._channels count
+ # so they can be purged later.
+ #debug('forgeting about %s', k)
+ for user in self[k].values():
+ user._channels -= 1
+ ServerChannelDict.__delitem__(self, k)
+
+ def getHostmask(self, nick, server, channel=None):
+ """Returns hostmask of nick."""
+ if channel:
+ return self[server, channel].getHostmask(nick)
+ return self[server].getHostmask(nick)
+
+ def who(self, server, channel):
+ if self._hook_who:
+ return
+
+ if (server, channel) in self._channels:
+ return
+
+ self._channels.add((server, channel))
+
+ key = ('%s.%s' %(server, channel)).lower()
+ self._hook_who = weechat.hook_modifier(
+ 'irc_in_352', callback(self._whoCallback), key)
+ self._hook_who_end = weechat.hook_modifier(
+ 'irc_in_315', callback(self._endWhoCallback), key)
+
+ buffer = weechat.buffer_search('irc', 'server.%s' %server)
+ weechat_command(buffer, '/who %s' % channel)
+
+ def _whoCallback(self, data, modifier, modifier_data, string):
+ #debug('%s %s %s', modifier, modifier_data, string)
+ args = string.split()
+ server, channel = modifier_data, args[3]
+ key = ('%s.%s' %(server, channel)).lower()
+ if key != data:
+ return string
+
+ nick, user, host = args[7], args[4], args[5]
+ hostmask = '%s!%s@%s' %(nick, user, host)
+ debug('WHO: %s', hostmask)
+ self.remember(server, nick, hostmask)
+ return ''
+
+ def _endWhoCallback(self, data, modifier, modifier_data, string):
+ args = string.split()
+ server, channel = modifier_data, args[3]
+ key = ('%s.%s' %(server, channel)).lower()
+ if key != data:
+ return string
+
+ debug('WHO: end.')
+ weechat.unhook(self._hook_who)
+ weechat.unhook(self._hook_who_end)
+ self._hook_who = self._hook_who_end = None
+ return ''
+
+ def purge(self):
+ ServerChannelDict.purge(self)
+ for cache in self.servercache.values():
+ cache.purge()
+
+userCache = UserCache()
+
+# -----------------------------------------------------------------------------
+# Chanop Command Classes
+
+# Base classes for chanop commands
+class CommandChanop(Command, ChanopBuffers):
+ """Base class for our commands, with config and general functions."""
+ infolist = None
+
+ def parser(self, args):
+ if not args:
+ weechat_command('', '/help %s' % self.command)
+ raise NoArguments
+ self.setup(self.buffer)
+
+ def execute(self):
+ self.users = userCache[self.server, self.channel]
+ try:
+ self.execute_chanop() # call our command and queue messages for WeeChat
+ self.irc.run() # run queued messages
+ except InvalidIRCBuffer as e:
+ error('Not in a IRC channel (%s)' % e)
+ self.irc.clear()
+ self.infolist = None # free irc_nick infolist
+
+ def execute_chanop(self):
+ pass
+
+ def nick_infolist(self):
+ # reuse the same infolist instead of creating it many times
+ if not self.infolist:
+ self.infolist = nick_infolist(self.server, self.channel)
+ else:
+ self.infolist.reset()
+ return self.infolist
+
+ def has_op(self, nick):
+ nicks = self.nick_infolist()
+ while nicks.next():
+ if nicks['name'] == nick:
+ return '@' in nicks['prefixes']
+
+ def has_voice(self, nick):
+ nicks = self.nick_infolist()
+ while nicks.next():
+ if nicks['name'] == nick:
+ return '+' in nicks['prefixes']
+
+ def isUser(self, nick):
+ return nick in self.users
+
+ def inChannel(self, nick):
+ return CaseInsensibleString(nick) in [ nick['name'] for nick in self.nick_infolist() ]
+
+ def getHostmask(self, name):
+ try:
+ hostmask = self.users.getHostmask(name)
+ if not hostmask:
+ self.irc.Userhost(name)
+ user = userCache[self.server][name]
+ return lambda: user.hostmask or user.nick
+ return hostmask
+ except KeyError:
+ pass
+
+ def set_mode(self, *nicks):
+ mode = self.prefix + self.mode
+ for nick in nicks:
+ self.irc.Mode(mode, nick)
+
+class CommandWithOp(CommandChanop):
+ """Base class for all the commands that requires op status for work."""
+ _enable_deopNow = True
+ deop_delay = 0
+
+ def __init__(self, *args, **kwargs):
+ CommandChanop.__init__(self, *args, **kwargs)
+ # update help so it adds --deop option
+ if self._enable_deopNow:
+ if self.usage:
+ self.usage += " "
+ if self.help:
+ self.help += "\n"
+ self.usage += "[--deop]"
+ self.help += " -o --deop: Forces deop immediately, without configured delay"\
+ " (option must be the last argument)."
+
+ def setup(self, buffer):
+ self.deopNow = False
+ CommandChanop.setup(self, buffer)
+
+ def parser(self, args):
+ CommandChanop.parser(self, args)
+ args = args.split()
+ if self._enable_deopNow and args[-1] in ('-o', '--deop'):
+ self.deopNow = True
+ del args[-1]
+ self.args = ' '.join(args)
+ if not self.args:
+ raise NoArguments
+
+ def execute_chanop(self, *args):
+ self.execute_op(*args)
+
+ if not self.irc.commands:
+ # nothing in queue, no reason to op.
+ return
+
+ self.irc.Op()
+ if (self.autodeop and self.get_config_boolean('autodeop')) or self.deopNow:
+ if self.deopNow:
+ delay = self.deop_delay
+ else:
+ delay = self.get_config_int('autodeop_delay')
+ if delay > 0:
+ if self.deopHook:
+ weechat.unhook(self.deopHook)
+ self.vars.deopHook = weechat.hook_timer(delay * 1000, 0, 1,
+ callback(self.deopCallback), self.buffer)
+ elif self.irc.commands: # only Deop if there are msgs in queue
+ self.irc.Deop()
+
+ def execute_op(self, *args):
+ """Commands in this method will be run with op privileges."""
+ pass
+
+ def deopCallback(self, buffer, count):
+ #debug('deop %s', buffer)
+ vars = self.varsOf(buffer)
+ if vars.autodeop:
+ if vars.irc.commands:
+ # there are commands in queue yet, wait some more
+ vars.deopHook = weechat.hook_timer(1000, 0, 1,
+ callback(self.deopCallback), buffer)
+ return WEECHAT_RC_OK
+ else:
+ vars.irc.Deop()
+ vars.irc.run()
+ vars.deopHook = None
+ return WEECHAT_RC_OK
+
+# Chanop commands
+class Op(CommandChanop):
+ description, usage = "Request operator privileges or give it to users.", "[nick [nick ... ]]",
+ help = \
+ "The command used for ask op is defined globally in plugins.var.python.%(name)s.op_command\n"\
+ "It can be defined per server or per channel in:\n"\
+ " plugins.var.python.%(name)s.op_command.<server>\n"\
+ " plugins.var.python.%(name)s.op_command.<server>.<#channel>\n"\
+ "\n"\
+ "After using this command, you won't be autodeoped." %{'name':SCRIPT_NAME}
+ command = 'oop'
+ completion = '%(nicks)'
+
+ prefix = '+'
+ mode = 'o'
+
+ def parser(self, args):
+ # dont show /help if no args
+ self.setup(self.buffer)
+
+ def execute_chanop(self):
+ self.irc.Op()
+ # /oop was used, we assume that the user wants
+ # to stay opped permanently
+ self.vars.autodeop = False
+ if self.args:
+ for nick in self.args.split():
+ if self.inChannel(nick) and not self.has_op(nick):
+ self.set_mode(nick)
+
+class Deop(Op, CommandWithOp):
+ description, usage, help = \
+ "Removes operator privileges from yourself or users.", "[nick [nick ... ]]", ""
+ command = 'odeop'
+ completion = '%(nicks)'
+
+ prefix = '-'
+ _enable_deopNow = False
+
+ def execute_chanop(self):
+ if self.args:
+ nicks = []
+ for nick in self.args.split():
+ if self.inChannel(nick) and self.has_op(nick):
+ nicks.append(nick)
+ if nicks:
+ CommandWithOp.execute_chanop(self, nicks)
+ else:
+ self.vars.autodeop = True
+ if self.has_op(self.nick):
+ self.irc.Deop()
+
+ def execute_op(self, nicks):
+ self.set_mode(*nicks)
+
+class Kick(CommandWithOp):
+ description, usage = "Kick nick.", "<nick> [<reason>]"
+ help = \
+ "On freenode, you can set this command to use /remove instead of /kick, users"\
+ " will see it as if the user parted and it can bypass autojoin-on-kick scripts."\
+ " See plugins.var.python.%s.enable_remove config option." %SCRIPT_NAME
+ command = 'okick'
+ completion = '%(nicks)'
+
+ def execute_op(self):
+ nick, s, reason = self.args.partition(' ')
+ if self.inChannel(nick):
+ self.irc.Kick(nick, reason)
+ else:
+ say("Nick not in %s (%s)" % (self.channel, nick), self.buffer)
+ self.irc.clear()
+
+class MultiKick(Kick):
+ description = "Kick one or more nicks."
+ usage = "<nick> [<nick> ... ] [:] [<reason>]"
+ help = Kick.help + "\n\n"\
+ "Note: Is not needed, but use ':' as a separator between nicks and "\
+ "the reason. Otherwise, if there's a nick in the channel matching the "\
+ "first word in reason it will be kicked."
+ completion = '%(nicks)|%*'
+
+ def execute_op(self):
+ args = self.args.split()
+ nicks = []
+ nicks_parted = []
+ #debug('multikick: %s' %str(args))
+ while(args):
+ nick = args[0]
+ if nick[0] == ':' or not self.isUser(nick):
+ break
+ nick = args.pop(0)
+ if self.inChannel(nick):
+ nicks.append(nick)
+ else:
+ nicks_parted.append(nick)
+
+ #debug('multikick: %s, %s' %(nicks, args))
+ reason = ' '.join(args).lstrip(':')
+ if nicks_parted:
+ say("Nick(s) not in %s (%s)" % (self.channel, ', '.join(nicks_parted)), self.buffer)
+ elif not nicks:
+ say("Unknown nick (%s)" % nick, self.buffer)
+ if nicks:
+ for nick in nicks:
+ self.irc.Kick(nick, reason)
+ else:
+ self.irc.clear()
+
+ban_help = \
+"Mask options:\n"\
+" -h --host: Match hostname (*!*@host)\n"\
+" -n --nick: Match nick (nick!*@*)\n"\
+" -u --user: Match username (*!user@*)\n"\
+" -e --exact: Use exact hostmask.\n"\
+"\n"\
+"If no mask options are supplied, configured defaults are used.\n"\
+"\n"\
+"Completer:\n"\
+"%(script)s will attempt to guess a complete banmask from current\n"\
+"users when using <tab> in an incomplete banmask. Using <tab> in a\n"\
+"complete banmask will generate variations of it. \n"\
+"\n"\
+"Examples:\n"\
+" /%(cmd)s somebody --user --host\n"\
+" will ban with *!user@hostname mask.\n"\
+" /%(cmd)s nick!*@<tab>\n"\
+" will autocomple with 'nick!*@host'.\n"\
+" /%(cmd)s nick!*@*<tab>\n"\
+" will cycle through different banmask variations for the same user.\n"
+
+class Ban(CommandWithOp):
+ description = "Ban user or hostmask."
+ usage = \
+ "<nick|mask> [<nick|mask> ... ] [ [--host] [--user] [--nick] | --exact ]"
+ command = 'oban'
+ help = ban_help % {'script': SCRIPT_NAME, 'cmd': command}
+ completion = '%(chanop_nicks)|%(chanop_ban_mask)|%*'
+
+ banmask = []
+ mode = 'b'
+ prefix = '+'
+
+ def __init__(self):
+ self.maskCache = modeCache[self.mode]
+ CommandWithOp.__init__(self)
+
+ def parser(self, args):
+ if not args:
+ showBans.callback(self.data, self.buffer, self.mode)
+ raise NoArguments
+ CommandWithOp.parser(self, args)
+ self._parser(self.args)
+
+ def _parser(self, args):
+ args = args.split()
+ try:
+ (opts, args) = getopt.gnu_getopt(args, 'hune', ('host', 'user', 'nick', 'exact'))
+ except getopt.GetoptError as e:
+ raise ArgumentError(e)
+ self.banmask = []
+ for k, v in opts:
+ if k in ('-h', '--host'):
+ self.banmask.append('host')
+ elif k in ('-u', '--user'):
+ self.banmask.append('user')
+ elif k in ('-n', '--nick'):
+ self.banmask.append('nick')
+ elif k in ('-e', '--exact'):
+ self.banmask = ['exact']
+ break
+ if not self.banmask:
+ self.banmask = self.get_default_banmask()
+ self.args = ' '.join(args)
+
+ def get_default_banmask(self):
+ return get_config_banmask(get_function=self.get_config)
+
+ def make_banmask(self, hostmask):
+ assert self.banmask
+ template = self.banmask
+
+ def banmask(s):
+ if not is_hostmask(s):
+ return s
+ if 'exact' in template:
+ return s
+ nick = user = host = '*'
+ if 'nick' in template:
+ nick = get_nick(s)
+ if 'user' in template:
+ user = get_user(s)
+ if 'host' in template:
+ host = get_host(s)
+ # check for freenode's webchat, and use a better mask.
+ if host.startswith('gateway/web/freenode'):
+ ip = host.partition('.')[2]
+ if is_ip(ip):
+ host = '*%s' % ip
+ s = '%s!%s@%s' %(nick, user, host)
+ assert is_hostmask(s), "Invalid hostmask: %s" % s
+ return s
+
+ if callable(hostmask):
+ return lambda: banmask(hostmask())
+ return banmask(hostmask)
+
+ def execute_op(self):
+ args = self.args.split()
+ banmasks = []
+ for arg in args:
+ if is_nick(arg):
+ hostmask = self.getHostmask(arg)
+ if not hostmask:
+ say("Unknown nick (%s)" % arg, self.buffer)
+ continue
+ mask = self.make_banmask(hostmask)
+ if self.has_voice(arg):
+ self.irc.Devoice(arg)
+ else:
+ # probably an extban
+ mask = arg
+ banmasks.append(mask)
+ banmasks = set(banmasks) # remove duplicates
+ self.ban(*banmasks)
+
+ def mode_is_supported(self):
+ return self.mode in supported_modes(self.server)
+
+ def ban(self, *banmasks, **kwargs):
+ if self.mode != 'b' and not self.mode_is_supported():
+ error("%s doesn't seem to support channel mode '%s', using regular ban." %(self.server,
+ self.mode))
+ mode = 'b'
+ else:
+ mode = self.mode
+ mode = self.prefix + mode
+ for mask in banmasks:
+ self.irc.Mode(mode, mask, **kwargs)
+
+class UnBan(Ban):
+ description, usage = "Remove bans.", "<nick|mask> [<nick|mask> ... ]"
+ command = 'ounban'
+ help = \
+ "Autocompletion will use channel's bans, patterns allowed for autocomplete multiple"\
+ " bans.\n"\
+ "\n"\
+ "Example:\n"\
+ "/%(cmd)s *192.168*<tab>\n"\
+ " Will autocomplete with all bans matching *192.168*" %{'cmd':command}
+ completion = '%(chanop_unban_mask)|%(chanop_nicks)|%*'
+ prefix = '-'
+
+ def search_masks(self, hostmask, **kwargs):
+ try:
+ masklist = self.maskCache[self.server, self.channel]
+ except KeyError:
+ return []
+
+ if callable(hostmask):
+ def banmask():
+ L = masklist.search(hostmask(), **kwargs)
+ if L: return L[0]
+
+ return [ banmask ]
+ return masklist.search(hostmask, **kwargs)
+
+ def execute_op(self):
+ args = self.args.split()
+ banmasks = []
+ for arg in args:
+ if is_hostmask(arg):
+ banmasks.extend(self.search_masks(arg))
+ elif is_nick(arg):
+ hostmask = self.getHostmask(arg)
+ if hostmask:
+ banmasks.extend(self.search_masks(hostmask, reverseMatch=True))
+ else:
+ # nick unknown to chanop
+ say("Unknown nick (%s)" % arg, self.buffer)
+ else:
+ banmasks.append(arg)
+ self.ban(*banmasks)
+
+class Quiet(Ban):
+ description = "Silence user or hostmask."
+ command = 'oquiet'
+ help = "This command is only for networks that support channel mode 'q'.\n\n" \
+ + ban_help % {'script': SCRIPT_NAME, 'cmd': command}
+ completion = '%(chanop_nicks)|%(chanop_ban_mask)|%*'
+
+ mode = 'q'
+
+class UnQuiet(UnBan):
+ command = 'ounquiet'
+ description = "Remove quiets."
+ help = "Works exactly like /ounban, but only for quiets. See /help ounban"
+ completion = '%(chanop_unquiet_mask)|%(chanop_nicks)|%*'
+
+ mode = 'q'
+
+class BanKick(Ban, Kick):
+ description = "Bankicks nick."
+ usage = "<nick> [<reason>] [ [--host] [--user] [--nick] | --exact ]"
+ help = "Combines /oban and /okick commands. See /help oban and /help okick."
+ command = 'obankick'
+ completion = '%(chanop_nicks)'
+ deop_delay = 2
+
+ def execute_op(self):
+ nick, s, reason = self.args.partition(' ')
+ if not self.isUser(nick):
+ say("Unknown nick (%s)" % nick, self.buffer)
+ self.irc.clear()
+ return
+
+ hostmask = self.getHostmask(nick)
+ # we already checked that nick is valid, so hostmask shouldn't be None
+ banmask = self.make_banmask(hostmask)
+ self.ban(banmask)
+ if self.inChannel(nick):
+ self.irc.Kick(nick, reason, wait=1)
+
+class MultiBanKick(BanKick):
+ description = "Bankicks one or more nicks."
+ usage = \
+ "<nick> [<nick> ... ] [:] [<reason>] [ [--host)] [--user] [--nick] | --exact ]"
+ completion = '%(chanop_nicks)|%*'
+
+ def execute_op(self):
+ args = self.args.split()
+ nicks = []
+ while(args):
+ nick = args[0]
+ if nick[0] == ':' or not self.isUser(nick):
+ break
+ nicks.append(args.pop(0))
+ reason = ' '.join(args).lstrip(':')
+ if not nicks:
+ say("Unknown nick (%s)" % nick, self.buffer)
+ self.irc.clear()
+ return
+
+ for nick in nicks:
+ hostmask = self.getHostmask(nick)
+ banmask = self.make_banmask(hostmask)
+ self.ban(banmask)
+
+ self.deop_delay = 1
+ for nick in nicks:
+ if self.inChannel(nick):
+ self.deop_delay += 1
+ self.irc.Kick(nick, reason, wait=1)
+
+class Topic(CommandWithOp):
+ description, usage = "Changes channel topic.", "[-delete | topic]"
+ help = "Clear topic if '-delete' is the new topic."
+ command = 'otopic'
+ completion = '%(irc_channel_topic)||-delete'
+
+ def execute_op(self):
+ self.irc.queue(Message('/topic %s' %self.args))
+
+class Voice(CommandWithOp):
+ description, usage, help = "Gives voice to somebody.", "nick [nick ... ]", ""
+ command = 'ovoice'
+ completion = '%(nicks)|%*'
+
+ prefix = '+'
+ mode = 'v'
+
+ def execute_op(self):
+ for nick in self.args.split():
+ if self.inChannel(nick) and not self.has_voice(nick):
+ self.set_mode(nick)
+
+class DeVoice(Voice):
+ description = "Removes voice from somebody."
+ command = 'odevoice'
+
+ prefix = '-'
+
+ def has_voice(self, nick):
+ return not Voice.has_voice(self, nick)
+
+class Mode(CommandWithOp):
+ description, usage, help = "Changes channel modes.", "<channel modes>", ""
+ command = 'omode'
+
+ def execute_op(self):
+ args = self.args.split()
+ modes = args.pop(0)
+ L = []
+ p = ''
+ for c in modes:
+ if c in '+-':
+ p = c
+ elif args:
+ L.append((p + c, args.pop(0)))
+ else:
+ L.append((p + c, None))
+ if not L:
+ return
+
+ for mode, arg in L:
+ self.irc.Mode(mode, arg)
+
+class ShowBans(CommandChanop):
+ description, usage, help = "Lists bans or quiets of a channel.", "(bans|quiets) [channel]", ""
+ command = 'olist'
+ completion = 'bans|quiets %(irc_server_channels)'
+ showbuffer = ''
+
+ padding = 40
+
+ def parser(self, args):
+ server = weechat.buffer_get_string(self.buffer, 'localvar_server')
+ channel = weechat.buffer_get_string(self.buffer, 'localvar_channel')
+ if server:
+ self.server = server
+ if channel:
+ self.channel = channel
+ type, _, args = args.partition(' ')
+ if not type:
+ raise ValueError('missing argument')
+ try:
+ mode = modeCache.map[type]
+ except KeyError:
+ raise ValueError('incorrect argument')
+
+ self.mode = mode
+ # fix self.type so is "readable" (ie, 'bans' instead of 'b')
+ if mode == 'b':
+ self.type = 'bans'
+ elif mode == 'q':
+ self.type = 'quiets'
+ args = args.strip()
+ if args:
+ self.channel = args
+
+ def get_buffer(self):
+ if self.showbuffer:
+ return self.showbuffer
+
+ buffer = weechat.buffer_search('python', SCRIPT_NAME)
+ if not buffer:
+ buffer = weechat.buffer_new(SCRIPT_NAME, '', '', '', '')
+ weechat.buffer_set(buffer, 'localvar_set_no_log', '1')
+ weechat.buffer_set(buffer, 'time_for_each_line', '0')
+ self.showbuffer = buffer
+ return buffer
+
+ def prnt(self, s):
+ weechat.prnt(self.get_buffer(), s)
+
+ def prnt_ban(self, banmask, op, when, hostmask=None):
+ padding = self.padding - len(banmask)
+ if padding < 0:
+ padding = 0
+ self.prnt('%s%s%s %sset by %s%s%s %s' %(color_mask,
+ banmask,
+ color_reset,
+ '.'*padding,
+ color_chat_nick,
+ op,
+ color_reset,
+ self.formatTime(when)))
+ if hostmask:
+ hostmasks = ' '.join(hostmask)
+ self.prnt(' %s%s' % (color_chat_host, hostmasks))
+
+ def clear(self):
+ b = self.get_buffer()
+ weechat.buffer_clear(b)
+ weechat.buffer_set(b, 'display', '1')
+ weechat.buffer_set(b, 'title', '%s' %SCRIPT_NAME)
+
+ def set_title(self, s):
+ weechat.buffer_set(self.get_buffer(), 'title', s)
+
+ def formatTime(self, t):
+ t = now() - int(t)
+ elapsed = time_elapsed(t, level=3)
+ return '%s ago' %elapsed
+
+ def execute(self):
+ self.showbuffer = ''
+ if self.mode not in supported_modes(self.server):
+ self.clear()
+ self.prnt("\n%sNetwork '%s' doesn't support %s" % (color_channel,
+ self.server,
+ self.type))
+ return
+
+ maskCache = modeCache[self.mode]
+ key = (self.server, self.channel)
+ try:
+ masklist = maskCache[key]
+ except KeyError:
+ if not (weechat.info_get('irc_is_channel', key[1]) and self.server):
+ error("Command /%s must be used in an IRC buffer." % self.command)
+ return
+
+ masklist = None
+ self.clear()
+ mask_count = 0
+ if masklist:
+ mask_count = len(masklist)
+ self.prnt('\n%s[%s %s]' %(color_channel, key[0], key[1]))
+ masks = [ m for m in masklist.values() ]
+ masks.sort(key=lambda x: x.date)
+ for ban in masks:
+ op = self.server
+ if ban.operator:
+ try:
+ op = get_nick(ban.operator)
+ except:
+ pass
+ self.prnt_ban(ban.mask, op, ban.date, ban.hostmask)
+ else:
+ self.prnt('No known %s for %s.%s' %(self.type, key[0], key[1]))
+ if masklist is None or not masklist.synced:
+ self.prnt("\n%sList not synced, please wait ..." %color_channel)
+ maskSync.fetch(key[0], key[1], self.mode, lambda: self.execute())
+ self.set_title('List of %s known by chanop in %s.%s (total: %s)' %(self.type,
+ key[0],
+ key[1],
+ mask_count))
+
+# -----------------------------------------------------------------------------
+# Script callbacks
+
+# Decorators
+def signal_parse(f):
+ @catchExceptions
+ def decorator(data, signal, signal_data):
+ server = signal[:signal.find(',')]
+ channel = signal_data.split()[2]
+ if channel[0] == ':':
+ channel = channel[1:]
+ if (server, channel) not in chanopChannels:
+ # signals only processed for channels in watchlist
+ return WEECHAT_RC_OK
+ nick = get_nick(signal_data)
+ hostmask = signal_data[1:signal_data.find(' ')]
+ #debug('%s %s', signal, signal_data)
+ return f(server, channel, nick, hostmask, signal_data)
+ decorator.func_name = f.func_name
+ return decorator
+
+def signal_parse_no_channel(f):
+ @catchExceptions
+ def decorator(data, signal, signal_data):
+ server = signal[:signal.find(',')]
+ nick = get_nick(signal_data)
+ channels = userCache.getChannels(server, nick)
+ if channels:
+ hostmask = signal_data[1:signal_data.find(' ')]
+ #debug('%s %s', signal, signal_data)
+ return f(server, channels, nick, hostmask, signal_data)
+ return WEECHAT_RC_OK
+ decorator.func_name = f.func_name
+ return decorator
+
+isupport = {}
+def get_isupport_value(server, feature):
+ #debug('isupport %s %s', server, feature)
+ try:
+ return isupport[server][feature]
+ except KeyError:
+ if not server:
+ return ''
+ elif server not in isupport:
+ isupport[server] = {}
+ v = weechat.info_get('irc_server_isupport_value', '%s,%s' %(server, feature.upper()))
+ if v:
+ isupport[server][feature] = v
+ else:
+ # old api
+ v = weechat.config_get_plugin('isupport.%s.%s' %(server, feature))
+ if not v:
+ # lets do a /VERSION (it should be done only once.)
+ if '/VERSION' in isupport[server]:
+ return ''
+ buffer = weechat.buffer_search('irc', 'server.%s' %server)
+ weechat_command(buffer, '/VERSION')
+ isupport[server]['/VERSION'] = True
+ return v
+
+_supported_modes = set('bq') # the script only support b,q masks
+def supported_modes(server):
+ """Returns modes supported by server."""
+ modes = get_isupport_value(server, 'chanmodes')
+ if not modes:
+ return 'b'
+ modes = modes.partition(',')[0] # we only care about the first type
+ modes = ''.join(_supported_modes.intersection(modes))
+ return modes
+
+def supported_maxmodes(server):
+ """Returns max modes number supported by server."""
+ max = get_isupport_value(server, 'modes')
+ try:
+ max = int(max)
+ if max <= 0:
+ max = 1
+ except ValueError:
+ return 1
+ return max
+
+def isupport_cb(data, signal, signal_data):
+ """Callback used for catch isupport msg if current version of WeeChat doesn't
+ support it."""
+ data = signal_data.split(' ', 3)[-1]
+ data, s, s = data.rpartition(' :')
+ data = data.split()
+ server = signal.partition(',')[0]
+ d = {}
+ #debug(data)
+ for s in data:
+ if '=' in s:
+ k, v = s.split('=')
+ else:
+ k, v = s, True
+ k = k.lower()
+ if k in ('chanmodes', 'modes', 'prefix'):
+ config = 'isupport.%s.%s' %(server, k)
+ weechat.config_set_plugin(config, v)
+ d[k] = v
+ isupport[server] = d
+ return WEECHAT_RC_OK
+
+def print_affected_users(buffer, *hostmasks):
+ """Print a list of users, max 8 hostmasks"""
+ def format_user(hostmask):
+ nick, host = hostmask.split('!', 1)
+ return '%s%s%s(%s%s%s)' %(color_chat_nick,
+ nick,
+ color_delimiter,
+ color_chat_host,
+ host,
+ color_delimiter)
+
+ max = 8
+ count = len(hostmasks)
+ if count > max:
+ hostmasks = hostmasks[:max]
+ say('Affects (%s): %s%s' %(count, ' '.join(map(format_user,
+ hostmasks)), count > max and ' %s...' %color_reset or ''), buffer=buffer)
+
+# Masks list tracking
+@signal_parse
+def mode_cb(server, channel, nick, opHostmask, signal_data):
+ """Keep the banmask list updated when somebody changes modes"""
+ #:m4v!~znc@unaffiliated/m4v MODE #test -bo+v asd!*@* m4v dude
+ pair = signal_data.split(' ', 4)[3:]
+ if len(pair) != 2:
+ # modes without argument, not interesting.
+ return WEECHAT_RC_OK
+ modes, args = pair
+
+ # check if there are interesting modes
+ servermodes = supported_modes(server)
+ s = modes.translate(chars, '+-') # remove + and -
+ if not set(servermodes).intersection(s):
+ return WEECHAT_RC_OK
+
+ # check if channel is in watchlist
+ key = (server, channel)
+ allkeys = CaseInsensibleSet()
+ for maskCache in modeCache.values():
+ allkeys.update(maskCache)
+ if key not in allkeys and key not in chanopChannels:
+ # from a channel we're not tracking
+ return WEECHAT_RC_OK
+
+ prefix = get_isupport_value(server, 'prefix')
+ chanmodes = get_isupport_value(server, 'chanmodes')
+ if not prefix or not chanmodes:
+ # we don't have ISUPPORT data, can't continue
+ return WEECHAT_RC_OK
+
+ # split chanmodes into tuples like ('+', 'b', 'asd!*@*')
+ action = ''
+ chanmode_list = []
+ args = args.split()
+
+ # user channel mode, such as +v or +o, get only the letters and not the prefixes
+ usermodes = ''.join(map(lambda c: c.isalpha() and c or '', prefix))
+ chanmodes = chanmodes.split(',')
+ # modes not supported by script, like +e +I
+ notsupported = chanmodes[0].translate(chars, servermodes)
+ modes_with_args = chanmodes[1] + usermodes + notsupported
+ modes_with_args_when_set = chanmodes[2]
+ for c in modes:
+ if c in '+-':
+ action = c
+ elif c in servermodes:
+ chanmode_list.append((action, c, args.pop(0)))
+ elif c in modes_with_args:
+ del args[0]
+ elif c in modes_with_args_when_set and action == '+':
+ del args[0]
+
+ affected_users = []
+ # update masks
+ for action, mode, mask in chanmode_list:
+ debug('MODE: %s%s %s %s', action, mode, mask, opHostmask)
+ if action == '+':
+ hostmask = hostmask_match_list(mask, userCache[key].hostmasks())
+ if hostmask:
+ affected_users.extend(hostmask)
+ if mask != '*!*@*':
+ # sending this signal with a *!*@* is annoying
+ weechat.hook_signal_send("%s,chanop_mode_%s" % (server, mode),
+ weechat.WEECHAT_HOOK_SIGNAL_STRING,
+ "%s %s %s %s" % (opHostmask, channel,
+ mask, ','.join(hostmask)))
+ modeCache.add(server, channel, mode, mask, operator=opHostmask, hostmask=hostmask)
+ elif action == '-':
+ modeCache.remove(server, channel, mode, mask)
+
+ if affected_users and get_config_boolean('display_affected',
+ get_function=get_config_specific, server=server, channel=channel):
+ buffer = weechat.buffer_search('irc', '%s.%s' %key)
+ print_affected_users(buffer, *set(affected_users))
+ return WEECHAT_RC_OK
+
+# User cache
+@signal_parse
+def join_cb(server, channel, nick, hostmask, signal_data):
+ if weechat.info_get('irc_nick', server) == nick:
+ # we're joining the channel, the cache is no longer valid
+ #userCache.generateCache(server, channel)
+ try:
+ del userCache[server, channel]
+ except KeyError:
+ pass
+ return WEECHAT_RC_OK
+ user = userCache.remember(server, nick, hostmask)
+ userCache[server, channel][nick] = user
+ return WEECHAT_RC_OK
+
+@signal_parse
+def part_cb(server, channel, nick, hostmask, signal_data):
+ userCache.remember(server, nick, hostmask)
+ userCache[server, channel].part(nick)
+ return WEECHAT_RC_OK
+
+@signal_parse_no_channel
+def quit_cb(server, channels, nick, hostmask, signal_data):
+ userCache.remember(server, nick, hostmask)
+ for channel in channels:
+ userCache[server, channel].part(nick)
+ return WEECHAT_RC_OK
+
+@signal_parse_no_channel
+def nick_cb(server, channels, oldNick, oldHostmask, signal_data):
+ newNick = signal_data[signal_data.rfind(' ') + 2:]
+ newHostmask = '%s!%s' % (newNick, oldHostmask[oldHostmask.find('!') + 1:])
+ userCache.remember(server, oldNick, oldHostmask)
+ user = userCache.remember(server, newNick, newHostmask)
+ for channel in channels:
+ userCache[server, channel].part(oldNick)
+ userCache[server, channel][newNick] = user
+ return WEECHAT_RC_OK
+
+# Garbage collector
+def garbage_collector_cb(data, counter):
+ """This takes care of purging users and masks from channels not in watchlist, and
+ expired users that parted.
+ """
+ debug('* flushing caches')
+ modeCache.purge()
+ userCache.purge()
+
+ if weechat.config_get_plugin('debug'):
+ # extra check that everything is right.
+ for serv, chan in userCache:
+ for nick in [ nick['name'] for nick in nick_infolist(serv, chan) ]:
+ if nick not in userCache[serv, chan]:
+ error('User cache out of sync, unknown nick. (%s - %s.%s)' % (nick, serv, chan))
+
+ return WEECHAT_RC_OK
+
+# -----------------------------------------------------------------------------
+# Config callbacks
+
+def enable_multi_kick_conf_cb(data, config, value):
+ global cmd_kick, cmd_bankick
+ cmd_kick.unhook()
+ cmd_bankick.unhook()
+ if boolDict[value]:
+ cmd_kick = MultiKick()
+ cmd_bankick = MultiBanKick()
+ else:
+ cmd_kick = Kick()
+ cmd_bankick = BanKick()
+ cmd_kick.hook()
+ cmd_bankick.hook()
+ return WEECHAT_RC_OK
+
+def update_chanop_watchlist_cb(data, config, value):
+ #debug('CONFIG: %s' %(' '.join((data, config, value))))
+ server = config[config.rfind('.')+1:]
+ if value:
+ L = value.split(',')
+ else:
+ L = []
+ for serv, chan in list(chanopChannels):
+ if serv == server:
+ chanopChannels.remove((serv, chan))
+ chanopChannels.update([ (server, channel) for channel in L ])
+ return WEECHAT_RC_OK
+
+def enable_bar_cb(data, config, value):
+ if boolDict[value]:
+ chanop_bar.new()
+ weechat.bar_item_new('chanop_ban_matches', 'item_ban_matches_cb', '')
+ weechat.bar_item_new('chanop_status', 'item_status_cb', '')
+ weechat.hook_modifier('input_text_content', 'input_content_cb', '')
+ else:
+ chanop_bar.remove()
+ return WEECHAT_RC_OK
+
+def enable_debug_cb(data, config, value):
+ global debug
+ if value and boolDict[value]:
+ try:
+ # custom debug module I use, allows me to inspect script's objects.
+ import pybuffer
+ debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME)
+ weechat.buffer_set(debug._getBuffer(), 'localvar_set_no_log', '0')
+ except:
+ def debug(s, *args):
+ if not isinstance(s, str):
+ s = str(s)
+ if args:
+ s = s % args
+ prnt('', '%s\t%s' % (script_nick, s))
+ else:
+ try:
+ if hasattr(debug, 'close'):
+ debug.close()
+ except NameError:
+ pass
+
+ debug = _no_debug
+
+ return WEECHAT_RC_OK
+
+# -----------------------------------------------------------------------------
+# Completers
+
+def cmpl_get_irc_users(f):
+ """Check if completion is done in a irc channel, and pass the buffer's user list."""
+ @catchExceptions
+ def decorator(data, completion_item, buffer, completion):
+ key = irc_buffer(buffer)
+ if not key:
+ return WEECHAT_RC_OK
+ users = userCache[key]
+ return f(users, data, completion_item, buffer, completion)
+ return decorator
+
+def unban_mask_cmpl(mode, completion_item, buffer, completion):
+ """Completion for applied banmasks, for commands like /ounban /ounquiet"""
+ maskCache = modeCache[mode]
+ key = irc_buffer(buffer)
+ if not key:
+ return WEECHAT_RC_OK
+ server, channel = key
+
+ def cmpl_unban(masklist):
+ input = weechat.buffer_get_string(buffer, 'input')
+ if input[-1] != ' ':
+ input, _, pattern = input.rpartition(' ')
+ else:
+ pattern = ''
+ #debug('%s %s', repr(input), repr(pattern))
+ if pattern and not is_nick(pattern): # FIXME nick completer interferes.
+ # NOTE masklist no longer accepts nicks.
+ L = masklist.search(pattern)
+ #debug('unban pattern %s => %s', pattern, L)
+ if L:
+ input = '%s %s ' % (input, ' '.join(L))
+ weechat.buffer_set(buffer, 'input', input)
+ weechat.buffer_set(buffer, 'input_pos', str(len(input)))
+ return
+ elif not masklist:
+ return
+ for mask in masklist.keys():
+ #debug('unban mask: %s', mask)
+ weechat.hook_completion_list_add(completion, mask, 0, weechat.WEECHAT_LIST_POS_END)
+
+ if key not in maskCache or not maskCache[key].synced:
+ # do completion after fetching marks
+ if not maskSync.queue:
+ def callback():
+ masklist = maskCache[key]
+ if chanop_bar:
+ global chanop_bar_status
+ if masklist:
+ chanop_bar_status = 'Got %s +%s masks.' % (len(masklist), mode)
+ else:
+ chanop_bar_status = 'No +%s masks found.' % mode
+ chanop_bar.popup()
+ weechat.bar_item_update('chanop_status')
+ else:
+ if masklist:
+ say('Got %s +%s masks.' % (len(masklist), mode), buffer)
+ else:
+ say('No +%s masks found.' % mode, buffer)
+ cmpl_unban(masklist)
+
+ maskSync.fetch(server, channel, mode, callback)
+ if chanop_bar:
+ global chanop_bar_status
+ chanop_bar_status = 'Fetching +%s masks in %s, please wait...' %(mode, channel)
+ weechat.bar_item_update('chanop_status')
+ chanop_bar.popup()
+ else:
+ say('Fetching +%s masks in %s, please wait...' %(mode, channel), buffer)
+ else:
+ # mask list is up to date, do completion
+ cmpl_unban(maskCache[key])
+ return WEECHAT_RC_OK
+
+banmask_cmpl_list = []
+@cmpl_get_irc_users
+def ban_mask_cmpl(users, data, completion_item, buffer, completion):
+ """Completion for banmasks, for commands like /oban /oquiet"""
+ input = weechat.buffer_get_string(buffer, 'input')
+ if input[-1] == ' ':
+ # no pattern, return
+ return WEECHAT_RC_OK
+
+ input, _, pattern = input.rpartition(' ')
+
+ global banmask_cmpl_list
+ if is_hostmask(pattern):
+ if not banmask_cmpl_list:
+ maskList = pattern_match_list(pattern, users.hostmasks(sorted=True, all=True))
+ if maskList:
+ banmask_cmpl_list = [ pattern ]
+
+ def add(mask):
+ if mask not in banmask_cmpl_list:
+ banmask_cmpl_list.append(mask)
+
+ for mask in maskList:
+ #debug('ban_mask_cmpl: Generating variations for %s', mask)
+ host = get_host(mask)
+ add('*!*@%s' % host)
+ add('%s!*@%s' % (get_nick(mask), host))
+ if host.startswith('gateway/web/freenode'):
+ ip = host.partition('.')[2]
+ if is_ip(ip):
+ add('*!*@*%s' % ip)
+ elif is_ip(host):
+ user = get_user(mask)
+ iprange = host.rsplit('.', 2)[0]
+ add('*!%s@%s.*' % (user, iprange))
+ add('*!*@%s.*' % iprange)
+ #debug('ban_mask_cmpl: variations: %s', banmask_cmpl_list)
+
+ if pattern in banmask_cmpl_list:
+ i = banmask_cmpl_list.index(pattern) + 1
+ if i == len(banmask_cmpl_list):
+ i = 0
+ mask = banmask_cmpl_list[i]
+ input = '%s %s' % (input, mask)
+ weechat.buffer_set(buffer, 'input', input)
+ weechat.buffer_set(buffer, 'input_pos', str(len(input)))
+ return WEECHAT_RC_OK
+
+ banmask_cmpl_list = []
+
+ if pattern[-1] != '*':
+ search_pattern = pattern + '*'
+ else:
+ search_pattern = pattern
+
+ if '@' in pattern:
+ # complete *!*@hostname
+ prefix = pattern[:pattern.find('@')]
+ make_mask = lambda mask: '%s@%s' %(prefix, mask[mask.find('@') + 1:])
+ get_list = users.hostmasks
+ elif '!' in pattern:
+ # complete *!username@*
+ prefix = pattern[:pattern.find('!')]
+ make_mask = lambda mask: '%s!%s@*' %(prefix, mask[mask.find('!') + 1:mask.find('@')])
+ get_list = users.hostmasks
+ else:
+ # complete nick!*@*
+ make_mask = lambda mask: '%s!*@*' %mask
+ get_list = users.nicks
+
+ for mask in pattern_match_list(search_pattern, get_list(sorted=True, all=True)):
+ mask = make_mask(mask)
+ weechat.hook_completion_list_add(completion, mask, 0, weechat.WEECHAT_LIST_POS_END)
+ return WEECHAT_RC_OK
+
+# Completions for nick, user and host parts of a usermask
+@cmpl_get_irc_users
+def nicks_cmpl(users, data, completion_item, buffer, completion):
+ for nick in users.nicks():
+ weechat.hook_completion_list_add(completion, nick, 0, weechat.WEECHAT_LIST_POS_END)
+ return WEECHAT_RC_OK
+
+@cmpl_get_irc_users
+def hosts_cmpl(users, data, completion_item, buffer, completion):
+ for hostmask in users.hostmasks(sorted=True, all=True):
+ weechat.hook_completion_list_add(completion, get_host(hostmask), 0,
+ weechat.WEECHAT_LIST_POS_SORT)
+ return WEECHAT_RC_OK
+
+@cmpl_get_irc_users
+def users_cmpl(users, data, completion_item, buffer, completion):
+ for hostmask in users.hostmasks(sorted=True, all=True):
+ user = get_user(hostmask)
+ weechat.hook_completion_list_add(completion, user, 0, weechat.WEECHAT_LIST_POS_END)
+ return WEECHAT_RC_OK
+
+# info hooks
+def info_hostmask_from_nick(data, info_name, arguments):
+ #debug('INFO: %s %s', info_name, arguments)
+ args = arguments.split(',')
+ channel = None
+ try:
+ nick, server, channel = args
+ except ValueError:
+ try:
+ nick, server = args
+ except ValueError:
+ return ''
+ try:
+ hostmask = userCache.getHostmask(nick, server, channel)
+ except KeyError:
+ return ''
+ return hostmask
+
+def info_pattern_match(data, info_name, arguments):
+ #debug('INFO: %s %s', info_name, arguments)
+ pattern, string = arguments.split(',')
+ if pattern_match(pattern, string):
+ return '1'
+ return ''
+
+# -----------------------------------------------------------------------------
+# Chanop bar callbacks
+
+chanop_bar_current_buffer = ''
+
+@catchExceptions
+def item_ban_matches_cb(data, item, window):
+ #debug('ban matches item: %s %s', item, window)
+ global chanop_bar_current_buffer
+ buffer = chanop_bar_current_buffer
+ if not buffer:
+ return ''
+
+ input = weechat.buffer_get_string(buffer, 'input')
+ if not input:
+ return ''
+
+ command, _, content = input.partition(' ')
+
+ if command[1:] not in ('oban', 'oquiet'):
+ return ''
+
+ def format(s):
+ return '%s affects: %s' % (command, s)
+
+ channel = weechat.buffer_get_string(buffer, 'localvar_channel')
+ if not channel or not is_channel(channel):
+ return format('(not an IRC channel)')
+
+ server = weechat.buffer_get_string(buffer, 'localvar_server')
+ users = userCache[server, channel]
+ content = content.split()
+ masks = [ mask for mask in content if is_hostmask(mask) or is_nick(mask) ]
+ if not masks:
+ return format('(no valid user mask or nick)')
+
+ #debug('ban matches item: %s', masks)
+
+ affected = []
+ hostmasks = users.hostmasks(all=True)
+ for mask in masks:
+ if is_hostmask(mask):
+ affected.extend(hostmask_match_list(mask, hostmasks))
+ elif mask in users:
+ affected.append(mask)
+ #debug('ban matches item: %s', affected)
+
+ if not affected:
+ return format('(nobody)')
+
+ L = set([ get_nick(h) for h in affected ])
+ return format('(%s) %s' % (len(L), ' '.join(L)))
+
+chanop_bar_status = ''
+def item_status_cb(data, item, window):
+ global chanop_bar_status
+ if chanop_bar_status:
+ return "%s[%s%s%s]%s %s" % (COLOR_BAR_DELIM,
+ COLOR_BAR_FG,
+ SCRIPT_NAME,
+ COLOR_BAR_DELIM,
+ color_reset,
+ chanop_bar_status)
+ else:
+ return "%s[%s%s%s]" % (COLOR_BAR_DELIM,
+ COLOR_BAR_FG,
+ SCRIPT_NAME,
+ COLOR_BAR_DELIM)
+
+@catchExceptions
+def input_content_cb(data, modifier, modifier_data, string):
+ #debug('input_content_cb: %s %s %r', modifier, modifier_data, string)
+ global chanop_bar_current_buffer, chanop_bar_status
+ if not chanop_bar:
+ return string
+
+ if string and not weechat.string_input_for_buffer(string):
+ command, _, content = string.partition(' ')
+ content = content.strip()
+ if content and command[1:] in ('oban', 'oquiet'):
+ chanop_bar.show()
+ chanop_bar_current_buffer = modifier_data
+ weechat.bar_item_update('chanop_ban_matches')
+ if chanop_bar_status:
+ chanop_bar_status = ''
+ weechat.bar_item_update('chanop_bar_status')
+ return string
+
+ if not chanop_bar._timer_hook:
+ chanop_bar.hide()
+ return string
+
+# -----------------------------------------------------------------------------
+# Main
+
+def unload_chanop():
+ if chanop_bar:
+ # we don't remove it, so custom options configs aren't lost
+ chanop_bar.hide()
+ bar_item = weechat.bar_item_search('chanop_ban_matches')
+ if bar_item:
+ weechat.bar_item_remove(bar_item)
+ return WEECHAT_RC_OK
+
+# Register script
+if __name__ == '__main__' and import_ok and \
+ weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+ SCRIPT_DESC, 'unload_chanop', ''):
+
+ # colors
+ color_delimiter = weechat.color('chat_delimiters')
+ color_chat_nick = weechat.color('chat_nick')
+ color_chat_host = weechat.color('chat_host')
+ color_mask = weechat.color('white')
+ color_channel = weechat.color('lightred')
+ color_reset = weechat.color('reset')
+ COLOR_WHITE = weechat.color('white')
+ COLOR_DARKGRAY = weechat.color('darkgray')
+ COLOR_BAR_DELIM = weechat.color('bar_delim')
+ COLOR_BAR_FG = weechat.color('bar_fg')
+
+ # pretty [chanop]
+ script_nick = '%s[%s%s%s]%s' %(color_delimiter,
+ color_chat_nick,
+ SCRIPT_NAME,
+ color_delimiter,
+ color_reset)
+
+ # -------------------------------------------------------------------------
+ # Debug
+
+ enable_debug_cb('', '', weechat.config_get_plugin('debug'))
+ weechat.hook_config('plugins.var.python.%s.debug' % SCRIPT_NAME, 'enable_debug_cb', '')
+
+ # -------------------------------------------------------------------------
+ # Init
+
+ # check weechat version
+ try:
+ version = int(weechat.info_get('version_number', ''))
+ except:
+ version = 0
+ if version < WEECHAT_VERSION[0]:
+ error("This version of WeeChat isn't supported. Use %s or later." % WEECHAT_VERSION[1])
+ raise Exception('unsupported weechat version')
+ if version < 0x30300: # prior to 0.3.3 didn't have support for ISUPPORT msg
+ error('WeeChat < 0.3.3: using ISUPPORT workaround.')
+ weechat.hook_signal('*,irc_in_005', 'isupport_cb', '')
+ if version < 0x30400: # irc_nick flags changed in 0.3.4
+ error('WeeChat < 0.3.4: using irc_nick infolist workaround.')
+ Infolist._use_flags = True
+
+ for opt, val in settings.items():
+ if not weechat.config_is_set_plugin(opt):
+ weechat.config_set_plugin(opt, val)
+
+ modeCache = ModeCache('chanop_mode_cache.dat')
+ modeCache.registerMode('b', 'ban', 'bans')
+ modeCache.registerMode('q', 'quiet', 'quiets')
+
+ # -------------------------------------------------------------------------
+ # remove old chanmask config and save them in shelf
+
+ prefix = 'python.%s.chanmask' % SCRIPT_NAME
+ infolist = Infolist('option', 'plugins.var.%s.*' % prefix)
+ n = len(prefix)
+ while infolist.next():
+ option = infolist['option_name'][n + 1:]
+ server, channel, mode, mask = option.split('.', 3)
+ if mode in modeCache:
+ cache = modeCache[mode]
+ if (server, channel) in cache:
+ masklist = cache[server, channel]
+ else:
+ masklist = cache[server, channel] = MaskList(server, channel)
+ if mask in masklist:
+ masklist[mask].deserialize(infolist['value'])
+ else:
+ obj = masklist[mask] = MaskObject(mask)
+ obj.deserialize(infolist['value'])
+ weechat.config_unset_plugin('chanmask.%s.%s.%s.%s' \
+ % (server, channel, mode, mask))
+ del infolist
+
+ # hook /oop /odeop
+ Op().hook()
+ Deop().hook()
+ # hook /okick /obankick
+ if get_config_boolean('enable_multi_kick'):
+ cmd_kick = MultiKick()
+ cmd_bankick = MultiBanKick()
+ else:
+ cmd_kick = Kick()
+ cmd_bankick = BanKick()
+ cmd_kick.hook()
+ cmd_bankick.hook()
+ # hook /oban /ounban /olist
+ Ban().hook()
+ UnBan().hook()
+ showBans = ShowBans()
+ showBans.hook()
+ # hook /oquiet /ounquiet
+ Quiet().hook()
+ UnQuiet().hook()
+ # hook /otopic /omode /ovoive /odevoice
+ Topic().hook()
+ Mode().hook()
+ Voice().hook()
+ DeVoice().hook()
+
+ maskSync.hook()
+
+ weechat.hook_config('plugins.var.python.%s.enable_multi_kick' % SCRIPT_NAME,
+ 'enable_multi_kick_conf_cb', '')
+ weechat.hook_config('plugins.var.python.%s.watchlist.*' % SCRIPT_NAME,
+ 'update_chanop_watchlist_cb', '')
+ weechat.hook_config('plugins.var.python.%s.enable_bar' % SCRIPT_NAME,
+ 'enable_bar_cb', '')
+
+ weechat.hook_completion('chanop_unban_mask', 'channelmode b masks', 'unban_mask_cmpl', 'b')
+ weechat.hook_completion('chanop_unquiet_mask', 'channelmode q masks', 'unban_mask_cmpl', 'q')
+ weechat.hook_completion('chanop_ban_mask', 'completes partial mask', 'ban_mask_cmpl', '')
+ weechat.hook_completion('chanop_nicks', 'nicks in cache', 'nicks_cmpl', '')
+ weechat.hook_completion('chanop_users', 'usernames in cache', 'users_cmpl', '')
+ weechat.hook_completion('chanop_hosts', 'hostnames in cache', 'hosts_cmpl', '')
+
+ weechat.hook_signal('*,irc_in_join', 'join_cb', '')
+ weechat.hook_signal('*,irc_in_part', 'part_cb', '')
+ weechat.hook_signal('*,irc_in_quit', 'quit_cb', '')
+ weechat.hook_signal('*,irc_in_nick', 'nick_cb', '')
+ weechat.hook_signal('*,irc_in_mode', 'mode_cb', '')
+
+ # run our cleaner function every 30 min.
+ weechat.hook_timer(1000 * 60 * 30, 0, 0, 'garbage_collector_cb', '')
+
+ chanop_bar = PopupBar('chanop_bar', hidden=True,
+ items='chanop_status,chanop_ban_matches')
+ if get_config_boolean('enable_bar'):
+ chanop_bar.new()
+ weechat.bar_item_new('chanop_ban_matches', 'item_ban_matches_cb', '')
+ weechat.bar_item_new('chanop_status', 'item_status_cb', '')
+ weechat.hook_modifier('input_text_content', 'input_content_cb', '')
+ else:
+ chanop_bar.remove()
+
+ weechat.hook_info("chanop_hostmask_from_nick",
+ "Returns nick's hostmask if is known. Returns '' otherwise.",
+ "nick,server[,channel]", "info_hostmask_from_nick", "")
+ weechat.hook_info("chanop_pattern_match",
+ "Test if pattern matches text, is case insensible with IRC case rules.",
+ "pattern,text", "info_pattern_match", "")
+
+
+# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: