| @@ -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 <components> 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 <server> 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) | |||
| @@ -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" | |||
| @@ -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) | |||