Beispiel #1
0
    def __init__(self):
        logging.info('Bot __init__')
        self.last_msg = -1
        self.msg_flood_limit = 0.25

        with open(os.path.join(os.path.dirname(__file__), 'ircbot.conf')) as f:
            data = json.load(f)
            self.servers = data['servers']

        self.select_server(0)

        self.db = sqlite3.connect(os.path.join(os.path.dirname(__file__),
                                               'ircbot.sqlite3'),
                                  check_same_thread=False)
        cursor = self.db.cursor()
        try:
            cursor.execute('select * from config limit 1')
        except sqlite3.OperationalError:  # table no exist
            cursor.execute(
                'create table config ( `group` varchar(100), `key` varchar(100), `value` varchar(100) NULL )'
            )
        cursor.close()
        modules_blacklist = data.get('blacklist', None)
        self.modules = ModuleManager(self, modules_blacklist)

        self.channel_ops = {}

        server = self.current_server['host']
        port = self.current_server[
            'port'] if 'port' in self.current_server else 6667
        ssl_enabled = self.current_server[
            'ssl'] if 'ssl' in self.current_server else False
        ipv6_enabled = self.current_server[
            'ipv6'] if 'ipv6' in self.current_server else False
        password = self.current_server[
            'password'] if 'password' in self.current_server else ''
        nickname = self.current_server['nickname']

        factory = irc.connection.Factory(
            wrapper=ssl.wrap_socket if ssl_enabled else lambda x: x,
            ipv6=ipv6_enabled)

        super(Bot, self).__init__([irc.bot.ServerSpec(server, port, password)],
                                  nickname,
                                  nickname,
                                  connect_factory=factory)

        self.connection.set_rate_limit(30)

        for module_name in self.modules.get_available_modules():
            self.modules.enable_module(module_name)
Beispiel #2
0
    def __init__(self, config_file):
        self.config_file = config_file
        self.config = json.load(open(self.config_file))
        self.me = self.config["me"]
        self.net = self.config["network"]
        self.module_manager = ModuleManager(self)
        self.hook_manager = HookManager(self)
        self.perms = Permissions(self)
        self.connection = IRCConnection(self.net["address"], self.net["port"], self.net["ssl"], self.config["proxies"].get(self.net.get("proxy", "none"), None), self.net.get("flood_interval", 0.0))
        self.running = True
        self.state = {}  # Dict used to hold stuff like last line received and last message etc...
        self.db = Database("etc/buhirc.db")
        self.db.connect()
        logging.basicConfig(level=getattr(logging, self.config["misc"]["loglevel"]), format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
        self.requests_session = requests.session()
        if self.config["misc"].get("http_proxy", "none") != "none":
            proxy = self.config["proxies"].get(self.config["misc"]["http_proxy"], "none")
            if proxy != "none":
                self.requests_session.proxies = {"http": proxy, "https": proxy}

        self.flood_verbs = [x.lower() for x in self.net.get("flood_verbs", [])]
        self.help = {}
Beispiel #3
0
    def __init__(self):
        logging.info("Bot __init__")
        self.last_msg = -1
        self.msg_flood_limit = 0.25

        with open(os.path.join(os.path.dirname(__file__), "ircbot.conf")) as f:
            data = json.load(f)
            self.servers = data["servers"]

        self.select_server(0)

        self.db = sqlite3.connect(os.path.join(os.path.dirname(__file__), "ircbot.sqlite3"), check_same_thread=False)
        cursor = self.db.cursor()
        try:
            cursor.execute("select * from config limit 1")
        except sqlite3.OperationalError:  # table no exist
            cursor.execute(
                "create table config ( `group` varchar(100), `key` varchar(100), `value` varchar(100) NULL )"
            )
        cursor.close()
        modules_blacklist = data.get("blacklist", None)
        self.modules = ModuleManager(self, modules_blacklist)

        self.channel_ops = {}

        server = self.current_server["host"]
        port = self.current_server["port"] if "port" in self.current_server else 6667
        ssl_enabled = self.current_server["ssl"] if "ssl" in self.current_server else False
        ipv6_enabled = self.current_server["ipv6"] if "ipv6" in self.current_server else False
        password = self.current_server["password"] if "password" in self.current_server else ""
        nickname = self.current_server["nickname"]

        factory = irc.connection.Factory(wrapper=ssl.wrap_socket if ssl_enabled else lambda x: x, ipv6=ipv6_enabled)

        super(Bot, self).__init__(
            [irc.bot.ServerSpec(server, port, password)], nickname, nickname, connect_factory=factory
        )

        self.connection.set_rate_limit(30)

        for module_name in self.modules.get_available_modules():
            self.modules.enable_module(module_name)
Beispiel #4
0
class BuhIRC:
    def __init__(self, config_file):
        self.config_file = config_file
        self.config = json.load(open(self.config_file))
        self.me = self.config["me"]
        self.net = self.config["network"]
        self.module_manager = ModuleManager(self)
        self.hook_manager = HookManager(self)
        self.perms = Permissions(self)
        self.connection = IRCConnection(self.net["address"], self.net["port"], self.net["ssl"], self.config["proxies"].get(self.net.get("proxy", "none"), None), self.net.get("flood_interval", 0.0))
        self.running = True
        self.state = {}  # Dict used to hold stuff like last line received and last message etc...
        self.db = Database("etc/buhirc.db")
        self.db.connect()
        logging.basicConfig(level=getattr(logging, self.config["misc"]["loglevel"]), format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p')
        self.requests_session = requests.session()
        if self.config["misc"].get("http_proxy", "none") != "none":
            proxy = self.config["proxies"].get(self.config["misc"]["http_proxy"], "none")
            if proxy != "none":
                self.requests_session.proxies = {"http": proxy, "https": proxy}

        self.flood_verbs = [x.lower() for x in self.net.get("flood_verbs", [])]
        self.help = {}

    def run(self):
        self.connection.connect()
        if self.config["network"]["sasl"]["use"]:
            self.raw("CAP REQ :sasl")
        self.raw("NICK %s" % self.me["nicks"][0])  # Nicks thing is a temp hack
        self.raw("USER %s * * :%s" % (self.me["ident"], self.me["gecos"]))

        for module in self.config["modules"]:
            self.module_manager.load_module(module)

        while self.running:
            if not self.loop():
                self.stop()

    def raw(self, line):
        """
        Send a raw IRC line to the server.
        @param line: The raw line to send, without a trailing carriage return or newline.
        """
        logging.debug("[IRC] <- %s" % line)
        ln = Line.parse(line)
        force = True  # Whether we bypass flood protection or not.
        if ln.command.lower() in self.flood_verbs:
            force = False

        self.connection.write_line(line, force)

    def parse_line(self, ln):
        logging.debug("[IRC] -> %s" % ln.linestr)
        if ln.command == "PING":
            self.raw(ln.linestr.replace("PING", "PONG"))
        elif ln.command == "376":
            for channel in self.net["channels"]:
                self.join(channel)
        elif ln.command == "CAP":
            if ln.params[1] == "ACK" and ln.params[-1] == "sasl" and self.net["sasl"]["use"]:
                self.raw("AUTHENTICATE PLAIN")
        elif ln.command == "AUTHENTICATE":
            magic = "%s\x00%s\x00%s" % (self.net["sasl"]["username"], self.net["sasl"]["username"], self.net["sasl"]["password"])
            magic = base64.b64encode(magic.encode("ascii"))
            self.raw("AUTHENTICATE %s" % magic)
        elif ln.command == "903":
            self.raw("CAP END")
        elif ln.command == "904":
            logging.warning("SASL authentication failed, continuing login anyways...")
            self.raw("CAP END")

    def loop(self):
        if not self.connection.loop():
            return False

        for line in self.connection.buffer:
            ln = Line.parse(line)
            self.state["last_line"] = ln
            self.parse_line(ln)
            self.hook_manager.run_irc_hooks(ln)

        return True

    def stop(self):
        self.raw("QUIT :Bye!")
        self.connection.disconnect()
        self.running = False

    def rehash(self):
        """
        Rehash (reread and reparse) the bot's configuration file.
        """
        self.config = json.load(open(self.config_file))

    # Helper functions
    def hook_command(self, cmd, callback, help_text=None):
        """
        Register a command hook to the bot.
        @param cmd: Command name to hook.
        @param callback: Event callback function to call when this command is ran.
        @param help_text: Help text for this command, no help if not specified.
        @return: ID of the new hook. (Used for removal later)
        """
        cmd = cmd.lower()

        if help_text:
            self.help[cmd] = help_text

        return self.hook_manager.add_hook(Hook("command_%s" % cmd, callback))

    def hook_numeric(self, numeric, callback):
        """
        Register a raw numeric hook to the bot.
        @param numeric: The raw IRC numeric (or command, such as PRIVMSG) to hook.
        @param callback: Event callback function to call when this numeric/command is received from the server.
        @return: ID of the new hook. (Used for removal later)
        """
        return self.hook_manager.add_hook(Hook("irc_raw_%s" % numeric, callback))

    def list_commands(self):
        """
        Get a list of all commands the bot knows about.
        @return: list of command names
        """
        return [hook[8:] for hook in self.hook_manager.hooks.keys() if hook.startswith("command_")]  # len("command_") == 8

    def unhook_something(self, the_id):
        """
        Unhook any sort of hook. (Command, numeric, or event.)
        @param the_id: The ID of the hook to remove, returned by a hook adding function.
        """
        self.hook_manager.remove_hook(the_id)

    def is_admin(self, hostmask=None):
        """
        Check if a hostmask is a bot admin.
        @param hostmask: The hostmask to check.
        @return: True if admin, False if not.
        """
        if hostmask is None:
            hostmask = self.state["last_line"].hostmask

        return self.perms.check_permission(hostmask, "admin")

    def check_condition(self, condition, false_message="Sorry, you may not do that.", reply_func=None):
        """
        Check a condition and return it, calling reply_func with false_message if the condition is False.
        @param condition: The condition to check.
        @param false_message: The message to be passed to reply_func
        @param reply_func: The function to call with false_message as argument if condition is False.
        @return:
        """
        if reply_func is None:
            reply_func = self.reply

        if condition:
            return True

        reply_func(false_message)
        return False

    def check_permission(self, permission="admin", error_reply="Sorry, you do not have permission to do that!",
                         reply_func=None):
        """
        Check a bot permission against the hostmask of the last line received, and return whether it matches.
        Calls reply_func with error_reply as argument if condition is False
        @param permission: The permission to check.
        @param error_reply: The message to be passed to reply_func
        @param reply_func: The function to call with error_reply as argument if condition is False.
        @return:
        """
        if reply_func is None:
            reply_func = self.reply_notice

        return self.check_condition(self.perms.check_permission(self.state["last_line"].hostmask, permission),
                                    error_reply, reply_func)

    # IRC-related stuff begins here
    def _msg_like(self, verb, target, message):
        self.raw("%s %s :%s" % (verb, target, message))

    def privmsg(self, target, message):
        """
        Send a PRIVMSG (channel or user message) to a user/channel.
        @param target: The target to send this message to. (Can be nickname or channel.)
        @param message: The actual message to send.
        """
        self._msg_like("PRIVMSG", target, message)

    def act(self, target, action):
        """
        Send a CTCP ACTION (/me) to a user/channel.
        @param target: The target to send this ACTION to. (Can be nickname or channel.)
        @param action: The actual action to send.
        """
        self.privmsg(target, "\x01ACTION %s\x01" % action)

    def notice(self, target, message):
        """
        Send a NOTICE to a user/channel.
        @param target: The user or channel to send this notice to.
        @param message: The actual notice text.
        """
        self._msg_like("NOTICE", target, message)

    def join(self, channel):
        """
        Send a raw channel JOIN message to the server. (Join a channel)
        @param channel: The channel to join. (Key can be passed in the same argument, separated by a space.)
        """
        self.raw("JOIN %s" % channel)

    def part(self, channel):
        """
        Send a raw channel PART to the server. (Leave a channel)
        @param channel: The channel to leave.
        """
        self.raw("PART %s" % channel)

    # IRC-related stuff that involves state.
    def reply(self, message):
        """
        Send a PRIVMSG (channel or user message) to the last channel or user we received a message in.
        @param message: The reply message to send.
        """
        ln = self.state["last_line"]
        reply_to = ln.hostmask.nick

        if ln.params[0][0] == "#":
            reply_to = ln.params[0]

        self.privmsg(reply_to, message)

    def reply_act(self, action):
        """
        Send a CTCP ACTION (/me) to the last channel or user we received a message in.
        @param action: The action to send.
        """
        self.reply("\x01ACTION %s\x01" % action)

    def reply_notice(self, message):
        """
        Send a NOTICE to the last channel or user we received a message in.
        @param message: The notice text to send.
        """
        ln = self.state["last_line"]
        self.notice(ln.hostmask.nick, message)

    # Web stuff.
    def http_get(self, url, **kwargs):
        """
        Perform an HTTP GET using requests.
        @param url: The URL to GET.
        @param kwargs: Any arguments to pass to requests.get()
        @return: requests.Response object.
        """
        return self.requests_session.get(url, **kwargs)

    def http_post(self, url, **kwargs):
        """
        Perform an HTTP POST using requests.
        @param url: The URL to POST to.
        @param kwargs: Any arguments to pass to requests.get()
        @return: requests.Response object.
        """
        return self.requests_session.post(url, **kwargs)
Beispiel #5
0
class Bot(irc.bot.SingleServerIRCBot):
    """The main brain of the IRC bot."""

    BOT_CFG_KEY = '_Bot'

    def __init__(self):
        logging.info('Bot __init__')
        self.last_msg = -1
        self.msg_flood_limit = 0.25

        with open(os.path.join(os.path.dirname(__file__), 'ircbot.conf')) as f:
            data = json.load(f)
            self.servers = data['servers']

        self.select_server(0)

        self.db = sqlite3.connect(os.path.join(os.path.dirname(__file__),
                                               'ircbot.sqlite3'),
                                  check_same_thread=False)
        cursor = self.db.cursor()
        try:
            cursor.execute('select * from config limit 1')
        except sqlite3.OperationalError:  # table no exist
            cursor.execute(
                'create table config ( `group` varchar(100), `key` varchar(100), `value` varchar(100) NULL )'
            )
        cursor.close()
        modules_blacklist = data.get('blacklist', None)
        self.modules = ModuleManager(self, modules_blacklist)

        self.channel_ops = {}

        server = self.current_server['host']
        port = self.current_server[
            'port'] if 'port' in self.current_server else 6667
        ssl_enabled = self.current_server[
            'ssl'] if 'ssl' in self.current_server else False
        ipv6_enabled = self.current_server[
            'ipv6'] if 'ipv6' in self.current_server else False
        password = self.current_server[
            'password'] if 'password' in self.current_server else ''
        nickname = self.current_server['nickname']

        factory = irc.connection.Factory(
            wrapper=ssl.wrap_socket if ssl_enabled else lambda x: x,
            ipv6=ipv6_enabled)

        super(Bot, self).__init__([irc.bot.ServerSpec(server, port, password)],
                                  nickname,
                                  nickname,
                                  connect_factory=factory)

        self.connection.set_rate_limit(30)

        for module_name in self.modules.get_available_modules():
            self.modules.enable_module(module_name)

    def select_server(self, index):
        self.current_server = self.servers[index]

        self.admin = self.current_server['global_admins']
        self.admin_channels = self.current_server['admin_channels']

    def start(self):
        logging.debug('start()')
        super(Bot, self).start()

    def die(self):
        logging.debug('die()')
        self.modules.unload()
        self.connection.disconnect('Bye, cruel world!')
        #super(Bot, self).die()

    def __process_message(self, message):
        for char in '\r\n':
            message = message.replace(char, '')
        MAX_MESSAGE_COUNT = 5
        MAX_LINE_LEN = 256
        m = []
        for i in range(0, len(message), MAX_LINE_LEN):
            if len(m) >= MAX_MESSAGE_COUNT:
                m.append('(message truncated) ...')
                break
            m.append(message[i:i + MAX_LINE_LEN])
        return m

    def notice(self, target, message):
        for m in self.__process_message(message):
            self.connection.notice(target, m)

    def privmsg(self, target, message):
        for m in self.__process_message(message):
            self.connection.privmsg(target, m)

    def action(self, target, message):
        for m in self.__process_message(message):
            self.connection.action(target, m)

    def __module_handle(self, handler, **kwargs):
        """Passed the "on_*" handlers through to the modules that support them"""
        handler = 'on_' + handler
        for (_, module) in self.modules.get_loaded_modules():
            if hasattr(module, handler):
                try:
                    getattr(module, handler)(**kwargs)
                except Exception as e:
                    logging.debug('Module handler %s.%s failed: %s', _,
                                  handler, e)

    def __process_command(self, c, e):
        """Process a message coming from the server."""
        message = e.arguments[0]
        # commands have to start with !
        if message[0] != '!':
            return
        # strip the ! off, and split the message into command and arguments
        split_message = message[1:].split(None, 1)
        cmd = split_message.pop(0).strip()
        raw_args = split_message[0] if len(split_message) else ''
        arglist = raw_args.split()

        # test for admin
        admin = e.source.userhost in self.admin
        if not admin:
            if e.target in self.admin_channels and e.target in self.channel_ops and e.source.nick in self.channel_ops[
                    e.target]:
                admin = True

        # nick is the sender of the message, target is either a channel or the sender.
        source = e.source.nick
        target = e.target if is_channel(e.target) else source

        # see if there is a module that is willing to handle this, and make it so.
        logging.debug(
            '__process_command (src: %s; tgt: %s; cmd: %s; args: %s; admin: %s)',
            source, target, cmd, raw_args, admin)

        # handle die outside of module (in case module is dead :( )
        if admin:
            if cmd == 'die':
                self.notice(source, 'Goodbye cruel world!')
                raise BotExitException
            elif cmd == 'jump':
                self.jump_server()
            elif cmd == 'restart_class':
                self.notice(source, 'Restarting...')
                raise BotReloadException
            # config commands
            elif cmd == 'get_config' and len(arglist) <= 2:
                if len(arglist) == 2:
                    try:
                        value = self.get_config(arglist[0], arglist[1])
                        self.notice(
                            source, 'config[{0}][{1}] = {2}'.format(
                                arglist[0], arglist[1], value))
                    except:
                        self.notice(
                            source,
                            'config[{0}][{1}] not set'.format(*arglist))
                elif len(arglist) == 1:
                    try:
                        values = self.get_config(arglist[0])
                        if len(values) > 0:
                            self.notice(
                                source,
                                'config[{}]: '.format(arglist[0]) + ', '.join([
                                    '{}: "{}"'.format(k, v)
                                    for (k, v) in values.items()
                                ]))
                        else:
                            self.notice(
                                source,
                                'config[{}] is empty'.format(arglist[0]))
                    except:
                        self.notice(source,
                                    'config[{}] not set'.format(arglist[0]))
                else:
                    try:
                        self.notice(
                            source, 'config groups: ' +
                            ', '.join(self.get_config_groups()))
                    except Exception as e:
                        self.notice(source, 'No config groups: {}'.format(e))
            elif cmd == 'set_config' and len(arglist) >= 2:
                if len(arglist) >= 3:
                    config_val = ' '.join(arglist[2:])
                else:
                    config_val = None
                try:
                    self.set_config(arglist[0], arglist[1], config_val)
                    self.notice(
                        source, 'Set config setting'
                        if config_val else 'Cleared config setting')
                except Exception as e:
                    self.notice(
                        source,
                        'Failed setting/clearing config setting: {0}'.format(
                            e))
            # other base admin commands
            elif cmd == 'raw':
                self.connection.send_raw(raw_args)
                return
            elif cmd == 'admins':
                self.notice(source, 'Current operators:')
                self.notice(source,
                            ' - global: {0}'.format(' '.join(self.admin)))
                for chan in [
                        chan for chan in self.admin_channels
                        if chan in self.channel_ops
                ]:
                    self.notice(
                        source,
                        ' - {0}: {1}'.format(chan,
                                             ' '.join(self.channel_ops[chan])))
                return

        if False and cmd == 'help':
            if len(arglist) > 0:
                if arglist[0] == 'module':
                    if len(arglist) < 2:
                        pass
                    elif self.modules.module_is_loaded(arglist[1]):
                        module = self.modules.get_module(arglist[1])
                        self.notice(target, module.__doc__)
                else:
                    for (module_name,
                         module) in self.modules.get_loaded_modules():
                        if module.has_cmd(arglist[0]):
                            self.notice(target,
                                        module.get_cmd(arglist[0]).__doc__)
            else:
                self.notice(
                    target,
                    '!help: this help text (send !help <command> for command help, send !help module <module> for module help)'
                )
                for (module_name, module) in [
                        lst for lst in self.modules.get_loaded_modules()
                        if lst[1].has_commands and not lst[1].admin_only
                ]:
                    cmds = module.get_cmd_list()
                    self.notice(
                        target, ' * {0}: {1}'.format(
                            module_name, ', '.join(cmds)
                            if len(cmds) > 0 else 'No commands'))

        elif False and admin and cmd == 'admin_help':
            if len(arglist) > 0:
                for (module_name, module) in self.modules.get_loaded_modules():
                    if module.has_admin_cmd(arglist[0]):
                        self.notice(source,
                                    module.get_admin_cmd(arglist[0]).__doc__)
            else:
                self.notice(
                    source,
                    '!admin_help: this help text (send !admin_help <command> for command help'
                )
                self.notice(
                    source,
                    '!die:                                   kill the bot')
                self.notice(
                    source,
                    '!raw:                                   send raw irc command'
                )
                self.notice(
                    source,
                    '!admins:                                see who are admin'
                )
                self.notice(
                    source,
                    '!restart_class:                         restart the main Bot class'
                )
                for (module_name, module) in self.modules.get_loaded_modules():
                    cmds = module.get_admin_cmd_list()
                    if len(cmds) > 0:
                        self.notice(
                            source,
                            ' * {0}: {1}'.format(module_name, ', '.join(cmds)))
        else:
            for (module_name, module) in self.modules.get_loaded_modules():
                try:
                    if module.has_cmd(cmd):
                        lines = module.get_cmd(cmd)(args=arglist,
                                                    arglist=arglist,
                                                    raw_args=raw_args,
                                                    source=source,
                                                    target=target,
                                                    admin=admin)
                        if lines:
                            for line in lines:
                                self.notice(target, line)
                    elif admin and module.has_admin_cmd(cmd):
                        lines = module.get_admin_cmd(cmd)(args=arglist,
                                                          arglist=arglist,
                                                          raw_args=raw_args,
                                                          source=source,
                                                          target=target,
                                                          admin=admin)
                        if lines:
                            for line in lines:
                                self.notice(source, line)
                except Exception as e:
                    logging.exception("Module '{0}' handle error: {1}".format(
                        module_name, e))

    def on_privmsg(self, c, e):
        logging.debug("on_privmsg")

        source = e.source.nick
        target = e.target if is_channel(e.target) else source
        message = e.arguments[0]

        self.__module_handle('privmsg',
                             source=source,
                             target=target,
                             message=message)
        try:
            self.__process_command(c, e)
        except BotExitException as e:
            raise e
        except BotReloadException as e:
            self.connection.disconnect("Reloading bot...")
            self.modules.unload()
            raise e
        except Exception as e:
            logging.exception('Error in __process_command: %s', e)

    def on_pubmsg(self, c, e):
        logging.debug("on_pubmsg")
        self.on_privmsg(c, e)

    def on_pubnotice(self, c, e):
        self.on_notice(c, e)

    def on_privnotice(self, c, e):
        self.on_notice(c, e)

    def on_notice(self, c, e):
        source = e.source
        target = e.target
        message = e.arguments[0]
        logging.debug('notice! source: {}, target: {}, message: {}'.format(
            source, target, message))
        self.__module_handle('notice',
                             source=source,
                             target=target,
                             message=message)

    def on_join(self, connection, event):
        self.connection.names([event.target])
        self.__module_handle('join', connection=connection, event=event)

    def on_part(self, c, e):
        self.connection.names([e.target])

    def on_kick(self, c, e):
        self.connection.names([e.target])

    def on_mode(self, c, e):
        self.connection.names([e.target])

    def on_endofnames(self, c, e):
        channel, text = e.arguments
        if not channel in self.channels:
            return
        self.channel_ops[channel] = list(self.channels[channel].opers())

    # def on_nick(self, c, e):
    #     self.connection.names(self.channels.keys())

    def on_nicknameinuse(self, c, e):
        """Gets called if the server complains about the name being in use. Tries to set the nick to nick + '_'"""
        logging.debug("on_nicknameinuse")
        c.nick(c.get_nickname() + "_")

    def on_welcome(self, connection, event):
        for chan in self.current_server['channels']:
            connection.join(chan)
        self.__module_handle('welcome', connection=connection, event=event)

    def get_config_groups(self):
        resultset = self.db.execute('select distinct `group` from config')
        return [g for (g, ) in resultset.fetchall()]

    def get_config(self, group, key=None, default=None):
        """gets a config value"""
        logging.info('get config %s.%s', group, key)
        if key == None:
            resultset = self.db.execute(
                'select `key`, `value` from config where `group` = :group',
                {'group': group})
            values = {}
            for (key, value) in resultset.fetchall():
                values[key] = value
            return values
        else:
            resultset = self.db.execute(
                'select `value` from config where `group` = :group and `key` = :key',
                {
                    'group': group,
                    'key': key
                })
            value = resultset.fetchone()
            if value == None:
                if default != None:
                    return default
                raise Exception('Value not found')
            return value[0]

    def set_config(self, group, key, value):
        """sets a config value"""
        logging.info('set config %s.%s to "%s"', group, key, value)
        cursor = self.db.cursor()
        data = {'group': group, 'key': key, 'value': value}
        if value == None:
            cursor.execute(
                'delete from config where `group` = :group and `key` = :key',
                data)
        else:
            try:
                self.get_config(group, key)
                cursor.execute(
                    'update config set `value` = :value where `group` = :group and `key` = :key',
                    data)
            except:
                cursor.execute(
                    'insert into config ( `group`, `key`, `value` ) values( :group, :key, :value )',
                    data)
        cursor.close()
        self.db.commit()
Beispiel #6
0
class Bot(irc.bot.SingleServerIRCBot):
    """The main brain of the IRC bot."""

    BOT_CFG_KEY = "_Bot"

    def __init__(self):
        logging.info("Bot __init__")
        self.last_msg = -1
        self.msg_flood_limit = 0.25

        with open(os.path.join(os.path.dirname(__file__), "ircbot.conf")) as f:
            data = json.load(f)
            self.servers = data["servers"]

        self.select_server(0)

        self.db = sqlite3.connect(os.path.join(os.path.dirname(__file__), "ircbot.sqlite3"), check_same_thread=False)
        cursor = self.db.cursor()
        try:
            cursor.execute("select * from config limit 1")
        except sqlite3.OperationalError:  # table no exist
            cursor.execute(
                "create table config ( `group` varchar(100), `key` varchar(100), `value` varchar(100) NULL )"
            )
        cursor.close()
        modules_blacklist = data.get("blacklist", None)
        self.modules = ModuleManager(self, modules_blacklist)

        self.channel_ops = {}

        server = self.current_server["host"]
        port = self.current_server["port"] if "port" in self.current_server else 6667
        ssl_enabled = self.current_server["ssl"] if "ssl" in self.current_server else False
        ipv6_enabled = self.current_server["ipv6"] if "ipv6" in self.current_server else False
        password = self.current_server["password"] if "password" in self.current_server else ""
        nickname = self.current_server["nickname"]

        factory = irc.connection.Factory(wrapper=ssl.wrap_socket if ssl_enabled else lambda x: x, ipv6=ipv6_enabled)

        super(Bot, self).__init__(
            [irc.bot.ServerSpec(server, port, password)], nickname, nickname, connect_factory=factory
        )

        self.connection.set_rate_limit(30)

        for module_name in self.modules.get_available_modules():
            self.modules.enable_module(module_name)

    def select_server(self, index):
        self.current_server = self.servers[index]

        self.admin = self.current_server["global_admins"]
        self.admin_channels = self.current_server["admin_channels"]

    def start(self):
        logging.debug("start()")
        super(Bot, self).start()

    def die(self):
        logging.debug("die()")
        self.modules.unload()
        self.connection.disconnect("Bye, cruel world!")
        # super(Bot, self).die()

    def __process_message(self, message):
        for char in "\r\n":
            message = message.replace(char, "")
        MAX_MESSAGE_COUNT = 5
        MAX_LINE_LEN = 256
        m = []
        for i in range(0, len(message), MAX_LINE_LEN):
            if len(m) >= MAX_MESSAGE_COUNT:
                m.append("(message truncated) ...")
                break
            m.append(message[i : i + MAX_LINE_LEN])
        return m

    def notice(self, target, message):
        for m in self.__process_message(message):
            self.connection.notice(target, m)

    def privmsg(self, target, message):
        for m in self.__process_message(message):
            self.connection.privmsg(target, m)

    def action(self, target, message):
        for m in self.__process_message(message):
            self.connection.action(target, m)

    def __module_handle(self, handler, **kwargs):
        """Passed the "on_*" handlers through to the modules that support them"""
        handler = "on_" + handler
        for (_, module) in self.modules.get_loaded_modules():
            if hasattr(module, handler):
                try:
                    getattr(module, handler)(**kwargs)
                except Exception as e:
                    logging.debug("Module handler %s.%s failed: %s", _, handler, e)

    def __process_command(self, c, e):
        """Process a message coming from the server."""
        message = e.arguments[0]
        # commands have to start with !
        if message[0] != "!":
            return
        # strip the ! off, and split the message into command and arguments
        split_message = message[1:].split(None, 1)
        cmd = split_message.pop(0).strip()
        raw_args = split_message[0] if len(split_message) else ""
        arglist = raw_args.split()

        # test for admin
        admin = e.source.userhost in self.admin
        if not admin:
            if (
                e.target in self.admin_channels
                and e.target in self.channel_ops
                and e.source.nick in self.channel_ops[e.target]
            ):
                admin = True

        # nick is the sender of the message, target is either a channel or the sender.
        source = e.source.nick
        target = e.target if is_channel(e.target) else source

        # see if there is a module that is willing to handle this, and make it so.
        logging.debug(
            "__process_command (src: %s; tgt: %s; cmd: %s; args: %s; admin: %s)", source, target, cmd, raw_args, admin
        )

        # handle die outside of module (in case module is dead :( )
        if admin:
            if cmd == "die":
                self.notice(source, "Goodbye cruel world!")
                raise BotExitException
            elif cmd == "jump":
                self.jump_server()
            elif cmd == "restart_class":
                self.notice(source, "Restarting...")
                raise BotReloadException
            # config commands
            elif cmd == "get_config" and len(arglist) <= 2:
                if len(arglist) == 2:
                    try:
                        value = self.get_config(arglist[0], arglist[1])
                        self.notice(source, "config[{0}][{1}] = {2}".format(arglist[0], arglist[1], value))
                    except:
                        self.notice(source, "config[{0}][{1}] not set".format(*arglist))
                elif len(arglist) == 1:
                    try:
                        values = self.get_config(arglist[0])
                        if len(values) > 0:
                            self.notice(
                                source,
                                "config[{}]: ".format(arglist[0])
                                + ", ".join(['{}: "{}"'.format(k, v) for (k, v) in values.items()]),
                            )
                        else:
                            self.notice(source, "config[{}] is empty".format(arglist[0]))
                    except:
                        self.notice(source, "config[{}] not set".format(arglist[0]))
                else:
                    try:
                        self.notice(source, "config groups: " + ", ".join(self.get_config_groups()))
                    except Exception as e:
                        self.notice(source, "No config groups: {}".format(e))
            elif cmd == "set_config" and len(arglist) >= 2:
                if len(arglist) >= 3:
                    config_val = " ".join(arglist[2:])
                else:
                    config_val = None
                try:
                    self.set_config(arglist[0], arglist[1], config_val)
                    self.notice(source, "Set config setting" if config_val else "Cleared config setting")
                except Exception as e:
                    self.notice(source, "Failed setting/clearing config setting: {0}".format(e))
            # other base admin commands
            elif cmd == "raw":
                self.connection.send_raw(raw_args)
                return
            elif cmd == "admins":
                self.notice(source, "Current operators:")
                self.notice(source, " - global: {0}".format(" ".join(self.admin)))
                for chan in [chan for chan in self.admin_channels if chan in self.channel_ops]:
                    self.notice(source, " - {0}: {1}".format(chan, " ".join(self.channel_ops[chan])))
                return

        if False and cmd == "help":
            if len(arglist) > 0:
                if arglist[0] == "module":
                    if len(arglist) < 2:
                        pass
                    elif self.modules.module_is_loaded(arglist[1]):
                        module = self.modules.get_module(arglist[1])
                        self.notice(target, module.__doc__)
                else:
                    for (module_name, module) in self.modules.get_loaded_modules():
                        if module.has_cmd(arglist[0]):
                            self.notice(target, module.get_cmd(arglist[0]).__doc__)
            else:
                self.notice(
                    target,
                    "!help: this help text (send !help <command> for command help, send !help module <module> for module help)",
                )
                for (module_name, module) in [
                    lst for lst in self.modules.get_loaded_modules() if lst[1].has_commands and not lst[1].admin_only
                ]:
                    cmds = module.get_cmd_list()
                    self.notice(
                        target, " * {0}: {1}".format(module_name, ", ".join(cmds) if len(cmds) > 0 else "No commands")
                    )

        elif False and admin and cmd == "admin_help":
            if len(arglist) > 0:
                for (module_name, module) in self.modules.get_loaded_modules():
                    if module.has_admin_cmd(arglist[0]):
                        self.notice(source, module.get_admin_cmd(arglist[0]).__doc__)
            else:
                self.notice(source, "!admin_help: this help text (send !admin_help <command> for command help")
                self.notice(source, "!die:                                   kill the bot")
                self.notice(source, "!raw:                                   send raw irc command")
                self.notice(source, "!admins:                                see who are admin")
                self.notice(source, "!restart_class:                         restart the main Bot class")
                for (module_name, module) in self.modules.get_loaded_modules():
                    cmds = module.get_admin_cmd_list()
                    if len(cmds) > 0:
                        self.notice(source, " * {0}: {1}".format(module_name, ", ".join(cmds)))
        else:
            for (module_name, module) in self.modules.get_loaded_modules():
                try:
                    if module.has_cmd(cmd):
                        lines = module.get_cmd(cmd)(
                            args=arglist, arglist=arglist, raw_args=raw_args, source=source, target=target, admin=admin
                        )
                        if lines:
                            for line in lines:
                                self.notice(target, line)
                    elif admin and module.has_admin_cmd(cmd):
                        lines = module.get_admin_cmd(cmd)(
                            args=arglist, arglist=arglist, raw_args=raw_args, source=source, target=target, admin=admin
                        )
                        if lines:
                            for line in lines:
                                self.notice(source, line)
                except Exception as e:
                    logging.exception("Module '{0}' handle error: {1}".format(module_name, e))

    def on_privmsg(self, c, e):
        logging.debug("on_privmsg")

        source = e.source.nick
        target = e.target if is_channel(e.target) else source
        message = e.arguments[0]

        self.__module_handle("privmsg", source=source, target=target, message=message)
        try:
            self.__process_command(c, e)
        except BotExitException as e:
            raise e
        except BotReloadException as e:
            self.connection.disconnect("Reloading bot...")
            self.modules.unload()
            raise e
        except Exception as e:
            logging.exception("Error in __process_command: %s", e)

    def on_pubmsg(self, c, e):
        logging.debug("on_pubmsg")
        self.on_privmsg(c, e)

    def on_pubnotice(self, c, e):
        self.on_notice(c, e)

    def on_privnotice(self, c, e):
        self.on_notice(c, e)

    def on_notice(self, c, e):
        source = e.source
        target = e.target
        message = e.arguments[0]
        logging.debug("notice! source: {}, target: {}, message: {}".format(source, target, message))
        self.__module_handle("notice", source=source, target=target, message=message)

    def on_join(self, connection, event):
        self.connection.names([event.target])
        self.__module_handle("join", connection=connection, event=event)

    def on_part(self, c, e):
        self.connection.names([e.target])

    def on_kick(self, c, e):
        self.connection.names([e.target])

    def on_mode(self, c, e):
        self.connection.names([e.target])

    def on_endofnames(self, c, e):
        channel, text = e.arguments
        if not channel in self.channels:
            return
        self.channel_ops[channel] = list(self.channels[channel].opers())

    # def on_nick(self, c, e):
    #     self.connection.names(self.channels.keys())

    def on_nicknameinuse(self, c, e):
        """Gets called if the server complains about the name being in use. Tries to set the nick to nick + '_'"""
        logging.debug("on_nicknameinuse")
        c.nick(c.get_nickname() + "_")

    def on_welcome(self, connection, event):
        for chan in self.current_server["channels"]:
            connection.join(chan)
        self.__module_handle("welcome", connection=connection, event=event)

    def get_config_groups(self):
        resultset = self.db.execute("select distinct `group` from config")
        return [g for (g,) in resultset.fetchall()]

    def get_config(self, group, key=None, default=None):
        """gets a config value"""
        logging.info("get config %s.%s", group, key)
        if key == None:
            resultset = self.db.execute("select `key`, `value` from config where `group` = :group", {"group": group})
            values = {}
            for (key, value) in resultset.fetchall():
                values[key] = value
            return values
        else:
            resultset = self.db.execute(
                "select `value` from config where `group` = :group and `key` = :key", {"group": group, "key": key}
            )
            value = resultset.fetchone()
            if value == None:
                if default != None:
                    return default
                raise Exception("Value not found")
            return value[0]

    def set_config(self, group, key, value):
        """sets a config value"""
        logging.info('set config %s.%s to "%s"', group, key, value)
        cursor = self.db.cursor()
        data = {"group": group, "key": key, "value": value}
        if value == None:
            cursor.execute("delete from config where `group` = :group and `key` = :key", data)
        else:
            try:
                self.get_config(group, key)
                cursor.execute("update config set `value` = :value where `group` = :group and `key` = :key", data)
            except:
                cursor.execute("insert into config ( `group`, `key`, `value` ) values( :group, :key, :value )", data)
        cursor.close()
        self.db.commit()
Beispiel #7
0
class Bot(irc.bot.SingleServerIRCBot):
    """The main brain of the IRC bot."""

    BOT_CFG_KEY = '_Bot'

    def __init__( self ):
        logging.info('Bot __init__')
        self.last_msg = -1
        self.msg_flood_limit = 0.25

        with open(os.path.join(os.path.dirname(__file__), 'ircbot.conf')) as f:
            data = json.load(f)
            self.servers = data['servers']

        self.select_server(0)

        self.db = sqlite3.connect( os.path.join( os.path.dirname( __file__ ), 'ircbot.sqlite3' ), check_same_thread = False )
        cursor = self.db.cursor()
        try:
            cursor.execute( 'select * from config limit 1' )
        except sqlite3.OperationalError: # table no exist
            cursor.execute( 'create table config ( `group` varchar(100), `key` varchar(100), `value` varchar(100) NULL )' )
        cursor.close()
        modules_blacklist = data.get('blacklist', None)
        self.modules = ModuleManager(self, modules_blacklist)

        self.channel_ops = {}

        server = self.current_server['host']
        port = self.current_server['port'] if 'port' in self.current_server else 6667
        ssl_enabled = self.current_server['ssl'] if 'ssl' in self.current_server else False
        ipv6_enabled = self.current_server['ipv6'] if 'ipv6' in self.current_server else False
        password = self.current_server['password'] if 'password' in self.current_server else ''
        nickname = self.current_server['nickname']

        factory = irc.connection.Factory(wrapper=ssl.wrap_socket if ssl_enabled else lambda x: x, ipv6=ipv6_enabled)

        super(Bot, self).__init__([irc.bot.ServerSpec(server, port, password)], nickname, nickname, connect_factory=factory)
        
        self.connection.set_rate_limit(30)

        for module_name in self.modules.get_available_modules():
            self.modules.enable_module( module_name )

    def select_server(self, index):
        self.current_server = self.servers[index]

        self.admin = self.current_server['global_admins']
        self.admin_channels = self.current_server['admin_channels']

    def start( self ):
        logging.debug( 'start()' )
        super(Bot, self).start()

    def die( self ):
        logging.debug( 'die()' )
        self.modules.unload()
        self.connection.disconnect( 'Bye, cruel world!' )
        #super(Bot, self).die()

    def __process_message(self, message):
        for char in '\r\n': message = message.replace(char, '')
        MAX_MESSAGE_COUNT = 5
        MAX_LINE_LEN = 256
        m = []
        for i in range(0, len(message), MAX_LINE_LEN):
            if len(m) >= MAX_MESSAGE_COUNT:
                m.append('(message truncated) ...')
                break
            m.append(message[i:i + MAX_LINE_LEN])
        return m
    def notice( self, target, message ):
        for m in self.__process_message(message):
            self.connection.notice(target, m)
    def privmsg( self, target, message ):
        for m in self.__process_message(message):
            self.connection.privmsg(target, m)
    def action( self, target, message ):
        for m in self.__process_message(message):
            self.connection.action(target, m)

    def __module_handle(self, handler, **kwargs):
        """Passed the "on_*" handlers through to the modules that support them"""
        handler = 'on_' + handler
        for (_ , module) in self.modules.get_loaded_modules():
            if hasattr(module, handler):
                try:
                    getattr(module, handler)(**kwargs)
                except Exception as e:
                    logging.debug('Module handler %s.%s failed: %s', _, handler, e)

    def __process_command( self, c, e ):
        """Process a message coming from the server."""
        message = e.arguments[0]
        # commands have to start with !
        if message[0] != '!':
            return
        # strip the ! off, and split the message into command and arguments
        split_message = message[1:].split(None, 1)
        cmd = split_message.pop(0).strip()
        raw_args = split_message[0] if len(split_message) else ''
        arglist = raw_args.split()

        # test for admin
        admin = e.source.userhost in self.admin
        if not admin:
            if e.target in self.admin_channels and e.target in self.channel_ops and e.source.nick in self.channel_ops[ e.target ]:
                admin = True

        # nick is the sender of the message, target is either a channel or the sender.
        source = e.source.nick
        target = e.target if is_channel(e.target) else source

        # see if there is a module that is willing to handle this, and make it so.
        logging.debug( '__process_command (src: %s; tgt: %s; cmd: %s; args: %s; admin: %s)', source, target, cmd, raw_args, admin )

        # handle die outside of module (in case module is dead :( )
        if admin:
            if cmd == 'die':
                self.notice( source, 'Goodbye cruel world!' )
                raise BotExitException
            elif cmd == 'jump':
                self.jump_server()
            elif cmd == 'restart_class':
                self.notice(source, 'Restarting...')
                raise BotReloadException
            # config commands
            elif cmd == 'get_config' and len( arglist ) <= 2:
                if len( arglist ) == 2:
                    try:
                        value = self.get_config( arglist[0], arglist[1] )
                        self.notice( source, 'config[{0}][{1}] = {2}'.format( arglist[0], arglist[1], value ) )
                    except:
                        self.notice( source, 'config[{0}][{1}] not set'.format( *arglist ) )
                elif len( arglist ) == 1:
                    try:
                        values = self.get_config( arglist[0] )
                        if len( values ) > 0:
                            self.notice( source, 'config[{}]: '.format( arglist[0] ) + ', '.join( [ '{}: "{}"'.format( k,v ) for ( k, v ) in values.items() ] ) )
                        else:
                            self.notice( source, 'config[{}] is empty'.format( arglist[0] ) )
                    except:
                        self.notice( source, 'config[{}] not set'.format( arglist[0] ) )
                else:
                    try:
                        self.notice( source, 'config groups: ' + ', '.join( self.get_config_groups() ) )
                    except Exception as e:
                        self.notice( source, 'No config groups: {}'.format( e ) )
            elif cmd == 'set_config' and len( arglist ) >= 2:
                if len( arglist ) >= 3:
                    config_val = ' '.join( arglist[2:] )
                else:
                    config_val = None
                try:
                    self.set_config( arglist[0], arglist[1], config_val )
                    self.notice( source, 'Set config setting' if config_val else 'Cleared config setting' )
                except Exception as e:
                    self.notice( source, 'Failed setting/clearing config setting: {0}'.format( e ) )
            # other base admin commands
            elif cmd == 'raw':
                self.connection.send_raw(raw_args)
                return
            elif cmd == 'admins':
                self.notice( source, 'Current operators:' )
                self.notice( source, ' - global: {0}'.format( ' '.join( self.admin ) ) )
                for chan in [ chan for chan in self.admin_channels if chan in self.channel_ops ]:
                    self.notice( source, ' - {0}: {1}'.format( chan, ' '.join( self.channel_ops[ chan ] ) ) )
                return

        if cmd == 'help':
            if len( arglist ) > 0:
                if arglist[0] == 'module':
                    if len( arglist ) < 2:
                        pass
                    elif self.modules.module_is_loaded( arglist[1] ):
                        module = self.modules.get_module( arglist[1] )
                        self.notice( target, module.__doc__ )
                else:
                    for ( module_name, module ) in self.modules.get_loaded_modules():
                        if module.has_cmd( arglist[0] ):
                            self.notice( target, module.get_cmd( arglist[0] ).__doc__ )
            else:
                self.notice( target, '!help: this help text (send !help <command> for command help, send !help module <module> for module help)' )
                for ( module_name, module ) in [ lst for lst in self.modules.get_loaded_modules() if lst[1].has_commands and not lst[1].admin_only ]:
                    cmds = module.get_cmd_list()
                    self.notice( target, ' * {0}: {1}'.format( module_name, ', '.join( cmds ) if len( cmds ) > 0 else 'No commands' ) )

        elif admin and cmd == 'admin_help':
            if len( arglist ) > 0:
                for ( module_name, module ) in self.modules.get_loaded_modules():
                    if module.has_admin_cmd( arglist[0] ):
                        self.notice( source, module.get_admin_cmd( arglist[0] ).__doc__ )
            else:
                self.notice( source, '!admin_help: this help text (send !admin_help <command> for command help' )
                self.notice( source, '!die:                                   kill the bot' )
                self.notice( source, '!raw:                                   send raw irc command' )
                self.notice( source, '!admins:                                see who are admin' )
                self.notice( source, '!restart_class:                         restart the main Bot class' )
                for ( module_name, module ) in self.modules.get_loaded_modules():
                    cmds = module.get_admin_cmd_list()
                    if len( cmds ) > 0:
                        self.notice( source, ' * {0}: {1}'.format( module_name, ', '.join( cmds ) ) )
        else:
            for ( module_name, module ) in self.modules.get_loaded_modules():
                try:
                    if module.has_cmd( cmd ):
                        lines = module.get_cmd( cmd )(args=arglist, arglist=arglist, raw_args=raw_args, source=source, target=target, admin=admin)
                        if lines:
                            for line in lines:
                                self.notice( target, line )
                    elif admin and module.has_admin_cmd( cmd ):
                        lines = module.get_admin_cmd(cmd)(args=arglist, arglist=arglist, raw_args=raw_args, source=source, target=target, admin=admin)
                        if lines:
                            for line in lines:
                                self.notice( source, line )
                except Exception as e:
                    logging.exception( "Module '{0}' handle error: {1}".format( module_name, e ) )

    def on_privmsg(self, c, e):
        logging.debug("on_privmsg")

        source = e.source.nick
        target = e.target if is_channel( e.target ) else source
        message = e.arguments[0]

        self.__module_handle('privmsg', source=source, target=target, message=message)
        try:
            self.__process_command( c, e )
        except BotExitException as e:
            raise e
        except BotReloadException as e:
            self.connection.disconnect( "Reloading bot..." )
            self.modules.unload()
            raise e
        except Exception as e:
            logging.exception( 'Error in __process_command: %s', e )

    def on_pubmsg(self, c, e):
        logging.debug("on_pubmsg")
        self.on_privmsg(c, e)

    def on_pubnotice(self, c, e):
        self.on_notice( c, e )
    def on_privnotice(self, c, e):
        self.on_notice(c, e)

    def on_notice(self, c, e):
        source = e.source
        target = e.target
        message = e.arguments[0]
        logging.debug('notice! source: {}, target: {}, message: {}'.format(source, target, message))
        self.__module_handle('notice', source=source, target=target, message=message)

    def on_join(self, connection, event):
        self.connection.names([event.target])
        self.__module_handle('join', connection=connection, event=event)

    def on_part(self, c, e):
        self.connection.names([e.target])

    def on_kick(self, c, e):
        self.connection.names([e.target])

    def on_mode( self, c, e ):
        self.connection.names( [e.target] )

    def on_endofnames(self, c, e):
        channel, text = e.arguments
        if not channel in self.channels:
            return
        self.channel_ops[channel] = list(self.channels[channel].opers())

    # def on_nick(self, c, e):
    #     self.connection.names(self.channels.keys())

    def on_nicknameinuse( self, c, e ):
        """Gets called if the server complains about the name being in use. Tries to set the nick to nick + '_'"""
        logging.debug( "on_nicknameinuse" )
        c.nick( c.get_nickname() + "_" )

    def on_welcome(self, connection, event):
        for chan in self.current_server['channels']:
            connection.join( chan )
        self.__module_handle('welcome', connection=connection, event=event)

    def get_config_groups( self ):
        resultset = self.db.execute( 'select distinct `group` from config' )
        return [ g for ( g, ) in resultset.fetchall() ]

    def get_config( self, group, key = None, default = None ):
        """gets a config value"""
        logging.info( 'get config %s.%s', group, key )
        if key == None:
            resultset = self.db.execute( 'select `key`, `value` from config where `group` = :group', { 'group': group } )
            values = {}
            for ( key, value ) in resultset.fetchall():
                values[ key ] = value
            return values
        else:
            resultset = self.db.execute( 'select `value` from config where `group` = :group and `key` = :key', { 'group': group, 'key': key } )
            value = resultset.fetchone()
            if value == None:
                if default != None:
                    return default
                raise Exception('Value not found')
            return value[0]

    def set_config( self, group, key, value ):
        """sets a config value"""
        logging.info( 'set config %s.%s to "%s"', group, key, value )
        cursor = self.db.cursor()
        data = { 'group': group, 'key': key, 'value': value }
        if value == None:
            cursor.execute( 'delete from config where `group` = :group and `key` = :key', data )
        else:
            try:
                self.get_config( group, key )
                cursor.execute( 'update config set `value` = :value where `group` = :group and `key` = :key', data )
            except:
                cursor.execute( 'insert into config ( `group`, `key`, `value` ) values( :group, :key, :value )', data )
        cursor.close()
        self.db.commit()