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 __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 __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)
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)
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()
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()
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()