|
- # -*- coding: utf-8 -*-
-
- """
- EarwigBot's JSON Config File Parser
-
- 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 import config" and access config data
- from within config's global variables and functions:
-
- * config.components - a list of enabled components
- * config.wiki - a dict of information about wiki-editing
- * config.tasks - a dict of information for bot tasks
- * config.irc - a dict of information about IRC
- * config.metadata - a dict of miscellaneous information
- * config.schedule() - returns a list of tasks scheduled to run at a given time
-
- Additionally, there are functions used in config loading:
- * config.load() - loads and parses our config file, returning True if
- passwords are stored encrypted or False otherwise
- * config.decrypt() - given a key, decrypts passwords inside our config
- variables; won't work if passwords aren't encrypted
- """
-
- import json
- import logging
- import logging.handlers
- from os import mkdir, path
-
- import blowfish
-
- script_dir = path.dirname(path.abspath(__file__))
- root_dir = path.split(script_dir)[0]
- config_path = path.join(root_dir, "config.json")
- log_dir = path.join(root_dir, "logs")
-
- _config = None # Holds data loaded from our config file
-
- # Set our easy-config-access global variables to None
- components, wiki, tasks, irc, metadata = None, None, None, None, None
-
- def _load():
- """Load data from our JSON config file (config.json) into _config."""
- global _config
- with open(config_path, 'r') as fp:
- try:
- _config = json.load(fp)
- except ValueError as error:
- print "Error parsing config file {0}:".format(config_path)
- print error
- exit(1)
-
- def _setup_logging():
- """Configures the logging module so it works the way we want it to."""
- logger = logging.getLogger()
- logger.setLevel(logging.DEBUG)
-
- if metadata.get("enableLogging"):
- hand = logging.handlers.TimedRotatingFileHandler
- formatter = BotFormatter()
- color_formatter = BotFormatter(color=True)
-
- logfile = lambda f: path.join(log_dir, f)
-
- if not path.isdir(log_dir):
- if not path.exists(log_dir):
- mkdir(log_dir, 0700)
- else:
- msg = "log_dir ({0}) exists but is not a directory!"
- print msg.format(log_dir)
- exit(1)
-
- main_handler = hand(logfile("bot.log"), "midnight", 1, 7)
- error_handler = hand(logfile("error.log"), "W6", 1, 4)
- debug_handler = hand(logfile("debug.log"), "H", 1, 6)
-
- main_handler.setLevel(logging.INFO)
- error_handler.setLevel(logging.WARNING)
- debug_handler.setLevel(logging.DEBUG)
-
- for h in (main_handler, error_handler, debug_handler):
- h.setFormatter(formatter)
- logger.addHandler(h)
-
- stream_handler = logging.StreamHandler()
- stream_handler.setLevel(logging.DEBUG)
- stream_handler.setFormatter(color_formatter)
- logger.addHandler(stream_handler)
-
- else:
- logger.addHandler(logging.NullHandler())
-
- def _make_new():
- """Make a new config file based on the user's input."""
- encrypt = raw_input("Would you like to encrypt passwords stored in config.json? [y/n] ")
- if encrypt.lower().startswith("y"):
- is_encrypted = True
- else:
- is_encrypted = False
-
- return is_encrypted
-
- def is_loaded():
- """Return True if our config file has been loaded, otherwise False."""
- return _config is not None
-
- def load():
- """Load, or reload, our config file.
-
- First, check if we have a valid config file, and if not, notify the user.
- If there is no config file at all, offer to make one, otherwise exit.
-
- Store data from our config file in five global variables (components, wiki,
- tasks, irc, metadata) for easy access (as well as the internal _config
- variable).
-
- If everything goes well, return True if stored passwords are
- encrypted in the file, or False if they are not.
- """
- global components, wiki, tasks, irc, metadata
-
- if not path.exists(config_path):
- print "You haven't configured the bot yet!"
- choice = raw_input("Would you like to do this now? [y/n] ")
- if choice.lower().startswith("y"):
- return _make_new()
- else:
- exit(1)
-
- _load()
-
- components = _config.get("components", [])
- wiki = _config.get("wiki", {})
- tasks = _config.get("tasks", {})
- irc = _config.get("irc", {})
- metadata = _config.get("metadata", {})
-
- _setup_logging()
-
- # Are passwords encrypted?
- return metadata.get("encryptPasswords", False)
-
- def decrypt(key):
- """Use the key to decrypt passwords in our config file.
-
- Call this if load() returns True. Catch password decryption errors and
- report them to the user.
- """
- global irc, wiki
-
- try:
- item = wiki.get("password")
- if item:
- wiki["password"] = blowfish.decrypt(key, item)
-
- item = irc.get("frontend").get("nickservPassword")
- if item:
- irc["frontend"]["nickservPassword"] = blowfish.decrypt(key, item)
-
- item = irc.get("watcher").get("nickservPassword")
- if item:
- irc["watcher"]["nickservPassword"] = blowfish.decrypt(key, item)
-
- except blowfish.BlowfishError as error:
- print "\nError decrypting passwords:"
- print "{0}: {1}.".format(error.__class__.__name__, error)
- exit(1)
-
- def schedule(minute, hour, month_day, month, week_day):
- """Return a list of tasks scheduled to run at the specified time.
-
- The schedule data comes from our config file's 'schedule' field, which is
- stored as _config["schedule"]. Call this function as config.schedule(args).
- """
- # Tasks to run this turn, each as a list of either [task_name, kwargs], or
- # just the task_name:
- tasks = []
-
- now = {"minute": minute, "hour": hour, "month_day": month_day,
- "month": month, "week_day": week_day}
-
- data = _config.get("schedule", [])
- for event in data:
- do = True
- for key, value in now.items():
- try:
- requirement = event[key]
- except KeyError:
- continue
- if requirement != value:
- do = False
- break
- if do:
- try:
- tasks.extend(event["tasks"])
- except KeyError:
- pass
-
- return tasks
-
-
- class BotFormatter(logging.Formatter):
- def __init__(self, color=False):
- self._format = super(BotFormatter, self).format
- if color:
- fmt = "[%(asctime)s %(lvl)s] %(name)s: %(message)s"
- self.format = lambda record: self._format(self.format_color(record))
- else:
- fmt = "[%(asctime)s %(levelname)-8s] %(name)s: %(message)s"
- self.format = self._format
- datefmt = "%Y-%m-%d %H:%M:%S"
- super(BotFormatter, self).__init__(fmt=fmt, datefmt=datefmt)
-
- def format_color(self, record):
- l = record.levelname.ljust(8)
- if record.levelno == logging.DEBUG:
- record.lvl = l.join(("\x1b[34m", "\x1b[0m")) # Blue
- if record.levelno == logging.INFO:
- record.lvl = l.join(("\x1b[32m", "\x1b[0m")) # Green
- if record.levelno == logging.WARNING:
- record.lvl = l.join(("\x1b[33m", "\x1b[0m")) # Yellow
- if record.levelno == logging.ERROR:
- record.lvl = l.join(("\x1b[31m", "\x1b[0m")) # Red
- if record.levelno == logging.CRITICAL:
- record.lvl = l.join(("\x1b[1m\x1b[31m", "\x1b[0m")) # Bold red
- return record
|