<reponame>sckasturi/.weechat
# -*- coding: utf-8 -*-
###
# Copyright (c) 2009-2013 by <NAME> <<EMAIL>>
#
# 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  = "<NAME> <<EMAIL>>"
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:

# -*- coding: utf-8 -*-
###
# Copyright (c) 2009-2013 by <NAME> <<EMAIL>>
#
# 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  = "<NAME> <<EMAIL>>"
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:
# -*- coding: utf-8 -*- ### # Copyright (c) 2009-2013 by <NAME> <<EMAIL>> # # 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 ### # default settings # ----------------------------------------------------------------------------- # Messages Error msg normal msg # ----------------------------------------------------------------------------- # Config # TODO Need to refactor all this too #debug("default banmask: %s" %values) Gets config defined for either server or channel. # ----------------------------------------------------------------------------- # Utils # ----------------------------------------------------------------------------- # IRC utils # poor but good enough Returns whether or not the string s starts with something like a hostmask. Returns whether or not a given string is an IPV4 address. Use cached regexp object or compile a new one from pattern. Check if pattern is for match a hostmask and remove ban forward if there's one. # XXX this needs a refactor # nick!user@host$#channel # this doesn't match any mask in args with a channel forward ':nick!user@host' => 'nick' 'nick!user@host' => 'user' # remove the stuff not part of the username. 'nick!user@host' => 'host' Returns pair (server, channel) or None if buffer isn't an irc channel # ----------------------------------------------------------------------------- # WeeChat classes This function will take a bound method or function and make it a callback. # try to create a descriptive and unique name. # not a bound method # set our callback I want to always keep debug() calls for this Class for reading WeeChat's infolists. #debug('Generating infolist %r %r', name, args) True False evaluation. Purge infolist if is no longer referenced. Implement the evaluation of self[name]. Moves cursor to beginning of infolist. # only if we aren't in the beginning already Class for hook WeeChat commands. Called by WeeChat when /command is used. # argument parsing Argument parsing, override if needed. This method is called when the command is run, override this. True False evaluation. # ----------------------------------------------------------------------------- # Per buffer variables Keeps variables and objects of a specific buffer. Keeps track of BuffersVariables instances in chanop. # must be shared across instances # update nick, it might have changed. #debug('config: %s' %config) # ----------------------------------------------------------------------------- # IRC messages queue Class that manages and sends the script's commands to WeeChat. # Special message classes # nothing to do # add this channel to our watchlist # wait for a while before timing out. # wait for a while before timing out. # IrcCommands methods # already send command, wait for timeout # USERHOST should be sent first # merge /modes # it happened once and it wasn't pretty #debug("Interrupting queue") # ----------------------------------------------------------------------------- # User/Mask classes Return a list of channels that match server and has item if given # ----------------------------------------------------------------------------- # Channel Modes (bans) Single list of masks # mask exists, update it #    def searchByNick(self, nick): #        try: #            hostmask = userCache.getHostmask(nick, self.server, self.channel) #            return self.searchByHostmask(hostmask) #        except KeyError: #            return [] Keeps a cache of masks for different channels. Adds a ban to (server, channel) banlist. #, hostmask=None): #debug("removing ban: %s" %banmask) class for store channel modes lists. # reset all sync timers Class for fetch and sync bans of any channel and mode. # freenode new signals for list quiet messages # sync queue stuff # 367 - ban mask # 368 - end of ban list # 728 - quiet mask # 729 - end of quiet list Fetches masks for a given server and channel. # invalid server or channel # check modes # check the last time we did this # don't fetch again callback for store a single mask. #debug("MASK %s: %s %s", modifier, modifier_data, string) # store temporally until "end list" msg callback for end of channel's mask list. #debug("MASK END %s: %s %s", modifier, modifier_data, string) # delete old masks in cache # run hooked functions if any # ----------------------------------------------------------------------------- # User cache # 4 hours Purge old nicks #debug('purging old user: %s' % nick) # 2 hours #debug('%s %s: join, %s', self.server, self.channel, nick) #debug(' - removed from purge list') #debug('%s %s: part, %s', self.server, self.channel, nick) # return all known hostmasks # only current hostmasks #        if not all(self.itervalues()): #            userCache.who(self.server, self.channel) Purge old nicks #debug('%s %s: forgeting about %s', self.server, self.channel, nick) # better to fail silently #debug('invalid buffer') #debug("%s: new user %s %s", server, nick, hostmask) # when we delete a channel, we need to reduce user._channels count # so they can be purged later. #debug('forgeting about %s', k) Returns hostmask of nick. #debug('%s %s %s', modifier, modifier_data, string) # ----------------------------------------------------------------------------- # Chanop Command Classes # Base classes for chanop commands Base class for our commands, with config and general functions. # call our command and queue messages for WeeChat # run queued messages # free irc_nick infolist # reuse the same infolist instead of creating it many times Base class for all the commands that requires op status for work. # update help so it adds --deop option # nothing in queue, no reason to op. # only Deop if there are msgs in queue Commands in this method will be run with op privileges. #debug('deop %s', buffer) # there are commands in queue yet, wait some more # Chanop commands #channel>\n"\ # dont show /help if no args # /oop was used, we assume that the user wants # to stay opped permanently #debug('multikick: %s' %str(args)) #debug('multikick: %s, %s' %(nicks, args)) # check for freenode's webchat, and use a better mask. # probably an extban # remove duplicates # nick unknown to chanop # we already checked that nick is valid, so hostmask shouldn't be None # fix self.type so is "readable" (ie, 'bans' instead of 'b') # ----------------------------------------------------------------------------- # Script callbacks # Decorators # signals only processed for channels in watchlist #debug('%s %s', signal, signal_data) #debug('%s %s', signal, signal_data) #debug('isupport %s %s', server, feature) # old api # lets do a /VERSION (it should be done only once.) # the script only support b,q masks Returns modes supported by server. # we only care about the first type Returns max modes number supported by server. Callback used for catch isupport msg if current version of WeeChat doesn't     support it. #debug(data) Print a list of users, max 8 hostmasks # Masks list tracking Keep the banmask list updated when somebody changes modes #:m4v!~znc@unaffiliated/m4v MODE #test -bo+v asd!*@* m4v dude # modes without argument, not interesting. # check if there are interesting modes # remove + and - # check if channel is in watchlist # from a channel we're not tracking # we don't have ISUPPORT data, can't continue # split chanmodes into tuples like ('+', 'b', 'asd!*@*') # user channel mode, such as +v or +o, get only the letters and not the prefixes # modes not supported by script, like +e +I # update masks # sending this signal with a *!*@* is annoying # User cache # we're joining the channel, the cache is no longer valid #userCache.generateCache(server, channel) # Garbage collector This takes care of purging users and masks from channels not in watchlist, and     expired users that parted. # extra check that everything is right. # ----------------------------------------------------------------------------- # Config callbacks #debug('CONFIG: %s' %(' '.join((data, config, value)))) # custom debug module I use, allows me to inspect script's objects. # ----------------------------------------------------------------------------- # Completers Check if completion is done in a irc channel, and pass the buffer's user list. Completion for applied banmasks, for commands like /ounban /ounquiet #debug('%s %s', repr(input), repr(pattern)) # FIXME nick completer interferes. # NOTE masklist no longer accepts nicks. #debug('unban pattern %s => %s', pattern, L) #debug('unban mask: %s', mask) # do completion after fetching marks # mask list is up to date, do completion Completion for banmasks, for commands like /oban /oquiet # no pattern, return #debug('ban_mask_cmpl: Generating variations for %s', mask) #debug('ban_mask_cmpl: variations: %s', banmask_cmpl_list) # complete *!*@hostname # complete *!username@* # complete nick!*@* # Completions for nick, user and host parts of a usermask # info hooks #debug('INFO: %s %s', info_name, arguments) #debug('INFO: %s %s', info_name, arguments) # ----------------------------------------------------------------------------- # Chanop bar callbacks #debug('ban matches item: %s %s', item, window) #debug('ban matches item: %s', masks) #debug('ban matches item: %s', affected) #debug('input_content_cb: %s %s %r', modifier, modifier_data, string) # ----------------------------------------------------------------------------- # Main # we don't remove it, so custom options configs aren't lost # Register script # colors # pretty [chanop] # ------------------------------------------------------------------------- # Debug # ------------------------------------------------------------------------- # Init # check weechat version # prior to 0.3.3 didn't have support for ISUPPORT msg # irc_nick flags changed in 0.3.4 # ------------------------------------------------------------------------- # remove old chanmask config and save them in shelf # hook /oop /odeop # hook /okick /obankick # hook /oban /ounban /olist # hook /oquiet /ounquiet # hook /otopic /omode /ovoive /odevoice # run our cleaner function every 30 min. # vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: