diff --git a/core/config.py b/core/config.py index 3607502..486367a 100644 --- a/core/config.py +++ b/core/config.py @@ -7,8 +7,14 @@ This handles all tasks involving reading and writing to our config file, including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. -Usually you'll just want to do "from core.config import config" and access -config data from within that object. +Usually you'll just want to do "from core import config" and access config data +from within config's five global variables: + +* config.components +* config.wiki +* config.irc +* config.schedule +* config.watcher """ from collections import defaultdict @@ -23,7 +29,9 @@ root_dir = path.split(script_dir)[0] config_path = path.join(root_dir, "config.xml") _config = None # holds the parsed DOM object for our config file -config = None # holds an instance of Container() with our config data + +# initialize our five global variables to store config data +components, wiki, irc, schedule, watcher = (None, None, None, None, None) class ConfigParseError(Exception): """Base exception for when we could not parse the config file.""" @@ -84,27 +92,14 @@ def make_new_config(): return is_encrypted def are_passwords_encrypted(): - """Determine if the passwords in our config file are encrypted, returning - either True or False.""" + """Determine if the passwords in our config file are encrypted; return + either True or False, or raise an exception if there was a problem reading + the config file.""" element = _config.getElementsByTagName("config")[0] - return attribute_to_bool(element, "encrypt-passwords", default=False) - -def attribute_to_bool(element, attribute, default=None): - """Return True if the value of element's attribute is 'true', '1', or 'on'; - return False if it is 'false', '0', or 'off' (regardless of - capitalization); return default if it is empty; raise TypeMismatchError if - it does match any of those.""" - value = element.getAttribute(attribute).lower() - if value in ["true", "1", "on"]: - return True - elif value in ["false", "0", "off"]: + attribute = element.getAttribute("encrypt-passwords") + if not attribute: return False - elif value == '': - return default - else: - e = ("Expected a bool in attribute '{0}' of element '{1}', but " + - "got '{2}'.").format(attribute, element.tagName, value) - raise TypeMismatchError(e) + return attribute_to_bool(attribute, element, "encrypt-passwords") def get_first_element(parent, tag_name): """Return the first child of the parent element with the given tag name, or @@ -134,6 +129,32 @@ def get_required_attribute(element, attr_name): raise MissingAttributeError(e) return attribute +def attribute_to_bool(value, element, attr_name): + """Return True if 'value' is 'true', '1', or 'on', return False if it is + 'false', '0', or 'off' (regardless of capitalization), or raise + TypeMismatchError() if it does match any of those. 'element' and + 'attr_name' are only used to generate the error message.""" + lcase = value.lower() + if lcase in ["true", "1", "on"]: + return True + elif lcase in ["false", "0", "off"]: + return False + else: + e = ("Expected a bool in attribute '{0}' of tag '{1}', but got '{2}'." + ).format(attr_name, element.tagName, value) + raise TypeMismatchError(e) + +def attribute_to_int(value, element, attr_name): + """Return 'value' after it is converted to an integer. If it could not be + converted, raise TypeMismatchError() using 'element' and 'attr_name' only + to give the user information about what happened.""" + try: + return int(value) + except ValueError: + e = ("Expected an integer in attribute '{0}' of tag '{1}', but got " + + "'{2}'.").format(attr_name, element.tagName, value) + raise TypeMismatchError(e) + def parse_config(key): """A thin wrapper for the actual config parser in _parse_config(): catch parsing exceptions and report them to the user cleanly.""" @@ -149,21 +170,20 @@ def parse_config(key): exit(1) def _parse_config(key): - """Parse config data from a DOM object into the 'config' global variable. - The key is used to unencrypt passwords stored in the XML config file.""" + """Parse config data from a DOM object into the five global variables that + store our config info. The key is used to unencrypt passwords stored in the + XML config file.""" + global components, wiki, irc, schedule, watcher + _load_config() # we might be re-loading unnecessarily here, but no harm in # that! data = _config.getElementsByTagName("config")[0] - cfg = Container() - cfg.components = parse_components(data) - cfg.wiki = parse_wiki(data, key) - cfg.irc = parse_irc(data, key) - cfg.schedule = parse_schedule(data) - cfg.watcher = parse_watcher(data) - - global config - config = cfg + components = parse_components(data) + wiki = parse_wiki(data, key) + irc = parse_irc(data, key) + schedule = parse_schedule(data) + watcher = parse_watcher(data) def parse_components(data): """Parse everything within the XML tag of our config file. @@ -187,14 +207,17 @@ def parse_wiki(data, key): def parse_irc_server(data, key): """Parse everything within a tag.""" server = Container() - connection = get_required_element(data, "connection") + server.host = get_required_attribute(connection, "host") server.port = get_required_attribute(connection, "port") server.nick = get_required_attribute(connection, "nick") server.ident = get_required_attribute(connection, "ident") server.realname = get_required_attribute(connection, "realname") + # convert the port from a string to an int + server.port = attribute_to_int(server.port, connection, "port") + nickserv = get_first_element(data, "nickserv") if nickserv: server.nickserv = Container() @@ -204,10 +227,12 @@ def parse_irc_server(data, key): server.nickserv.password = blowfish.decrypt(key, password) else: server.nickserv.password = password + else: + server.nickserv = None + server.channels = list() channels = get_first_element(data, "channels") if channels: - server.channels = list() for channel in channels.getElementsByTagName("channel"): name = get_required_attribute(channel, "name") server.channels.append(name) diff --git a/core/main.py b/core/main.py index 9f0cc7f..31d6149 100644 --- a/core/main.py +++ b/core/main.py @@ -42,7 +42,7 @@ root_dir = os.path.split(script_dir)[0] # the bot's "root" directory relative sys.path.append(root_dir) # make sure we look in the root dir for modules from core import config -#from irc import frontend, watcher +from irc import frontend#, watcher #from wiki import task_manager f_conn = None @@ -87,7 +87,7 @@ def irc_frontend(components): f_conn = frontend.get_connection() frontend.startup(f_conn) - if enable_wiki_schedule: + if config.components["wiki_schedule"]: print "\nStarting wiki scheduler..." task_manager.load_tasks() t_scheduler = threading.Thread(target=wiki_scheduler) @@ -95,7 +95,7 @@ def irc_frontend(components): t_scheduler.daemon = True t_scheduler.start() - if enable_irc_watcher: + if config.components["irc_watcher"]: print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" @@ -104,7 +104,7 @@ def irc_frontend(components): frontend.main() - if enable_irc_watcher: + if config.components["irc_watcher"]: w_conn.close() f_conn.close() @@ -115,7 +115,7 @@ def run(): key = None config.parse_config(key) # load data from the config file and parse it # using the unlock key - components = config.config.components + components = config.components if components["irc_frontend"]: # make the frontend run on our primary irc_frontend(components) # thread if enabled, and enable additional @@ -124,7 +124,7 @@ def run(): elif components["wiki_schedule"]: # run the scheduler on the main print "Starting wiki scheduler..." # thread, but also run the IRC task_manager.load_tasks() # watcher on another thread iff it - if enable_irc_watcher: # is enabled + if components["irc_watcher"]: # is enabled print "\nStarting IRC watcher..." t_watcher = threading.Thread(target=irc_watcher, args=(f_conn,)) t_watcher.name = "irc-watcher" diff --git a/irc/frontend.py b/irc/frontend.py index 293bc0a..d8d4991 100644 --- a/irc/frontend.py +++ b/irc/frontend.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- -## Imports -import re, time +""" +EarwigBot's Front-end IRC Component -from config.irc import * -from config.secure import * +The IRC frontend runs on a normal IRC server and expects users to interact with +it and give it commands. Commands are stored as "command classes", subclasses +of BaseCommand in irc/base_command.py. All command classes are automatically +imported by irc/command_handler.py if they are in irc/commands. +""" +from re import findall + +from core import config from irc import command_handler from irc.connection import * from irc.data import Data @@ -13,16 +19,24 @@ from irc.data import Data connection = None def get_connection(): - connection = Connection(HOST, PORT, NICK, IDENT, REALNAME) + """Return a new Connection() instance with information about our server + connection, but don't actually connect yet.""" + cf = config.irc.frontend + connection = Connection(cf.host, cf.port, cf.nick, cf.nick, cf.realname) return connection def startup(conn): + """Accept a single arg, a Connection() object, and set our global variable + 'connection' to it. Load all command classes in irc/commands with + command_handler, and then establish a connection with the IRC server.""" global connection connection = conn command_handler.load_commands(connection) connection.connect() def main(): + """Main loop for the Frontend IRC Bot component. get_connection() and + startup() should have already been called.""" read_buffer = str() while 1: @@ -35,41 +49,52 @@ def main(): lines = read_buffer.split("\n") read_buffer = lines.pop() - for line in lines: + for line in lines: # handle a single message from IRC line = line.strip().split() - data = Data() + data = Data() # new Data() instance to store info about this line data.line = line if line[1] == "JOIN": - data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] + data.nick, data.ident, data.host = findall( + ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] data.chan = line[2][1:] - - command_handler.check("join", data) # check if there's anything we can respond to, and if so, respond + command_handler.check("join", data) # check for 'join' hooks in + # our commands if line[1] == "PRIVMSG": - data.nick, data.ident, data.host = re.findall(":(.*?)!(.*?)@(.*?)\Z", line[0])[0] + data.nick, data.ident, data.host = findall( + ":(.*?)!(.*?)@(.*?)\Z", line[0])[0] data.msg = ' '.join(line[3:])[1:] data.chan = line[2] - if data.chan == NICK: # this is a privmsg to us, so set 'chan' as the nick of the sender + if data.chan == config.irc.frontend.nick: + # this is a privmsg to us, so set 'chan' as the nick of the + # sender, then check for private-only command hooks data.chan = data.nick - command_handler.check("msg_private", data) # only respond if it's a private message + command_handler.check("msg_private", data) else: - command_handler.check("msg_public", data) # only respond if it's a public (channel) message + # check for public-only command hooks + command_handler.check("msg_public", data) - command_handler.check("msg", data) # check for general messages + # check for command hooks that apply to all messages + command_handler.check("msg", data) - if data.msg.startswith("!restart"): # hardcode the !restart command (we can't restart from within an ordinary command) - if data.host in OWNERS: + # hardcode the !restart command (we can't restart from within + # an ordinary command) + if data.msg in ["!restart", ".restart"]: + if data.host in config.irc.permissions["owners"]: print "Restarting bot per owner request..." return - if line[0] == "PING": # If we are pinged, pong back to the server + if line[0] == "PING": # if we are pinged, pong back to the server connection.send("PONG %s" % line[1]) - if line[1] == "376": - if NS_AUTH: # if we're supposed to auth to nickserv, do that - connection.say("NickServ", "IDENTIFY %s %s" % (NS_USER, NS_PASS)) - time.sleep(3) # sleep for a bit so we don't join channels un-authed - for chan in CHANS: # join all of our startup channels + if line[1] == "376": # we've successfully connected to the network + ns = config.irc.frontend.nickserv + if ns: # if we're supposed to auth to nickserv, do that + connection.say("NickServ", "IDENTIFY %s %s" % (ns.username, + ns.password)) + + # join all of our startup channels + for chan in config.irc.frontend.channels: connection.join(chan)