- Fixed loading bugs in CommandLoader and TaskLoadertags/v0.1^2
| @@ -53,8 +53,8 @@ class Bot(object): | |||
| wiki toolset with bot.wiki.get_site(). | |||
| """ | |||
| def __init__(self, root_dir): | |||
| self.config = BotConfig(root_dir) | |||
| def __init__(self, root_dir, level=logging.INFO): | |||
| self.config = BotConfig(root_dir, level) | |||
| self.logger = logging.getLogger("earwigbot") | |||
| self.commands = CommandManager(self) | |||
| self.tasks = TaskManager(self) | |||
| @@ -65,6 +65,10 @@ class Bot(object): | |||
| self.component_lock = Lock() | |||
| self._keep_looping = True | |||
| self.config.load() | |||
| self.commands.load() | |||
| self.tasks.load() | |||
| def _start_irc_components(self): | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| @@ -107,18 +111,10 @@ class Bot(object): | |||
| self.logger.warn("IRC watcher has stopped; restarting") | |||
| self.watcher = Watcher(self) | |||
| Thread(name=name, target=self.watcher.loop).start() | |||
| sleep(5) | |||
| sleep(3) | |||
| def run(self): | |||
| config = self.config | |||
| config.load() | |||
| config.decrypt(config.wiki, "password") | |||
| config.decrypt(config.wiki, "search", "credentials", "key") | |||
| config.decrypt(config.wiki, "search", "credentials", "secret") | |||
| config.decrypt(config.irc, "frontend", "nickservPassword") | |||
| config.decrypt(config.irc, "watcher", "nickservPassword") | |||
| self.commands.load() | |||
| self.tasks.load() | |||
| self.logger.info("Starting bot") | |||
| self._start_irc_components() | |||
| self._start_wiki_scheduler() | |||
| self._loop() | |||
| @@ -137,4 +133,3 @@ class Bot(object): | |||
| with self.component_lock: | |||
| self._stop_irc_components() | |||
| self._keep_looping = False | |||
| sleep(3) # Give a few seconds to finish closing IRC connections | |||
| @@ -135,22 +135,31 @@ class CommandManager(object): | |||
| return | |||
| self._commands[command.name] = command | |||
| self.logger.debug("Added command {0}".format(command.name)) | |||
| self.logger.debug("Loaded command {0}".format(command.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid commands in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_command(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid commands into self._commands.""" | |||
| with self._command_access_lock: | |||
| self._commands.clear() | |||
| dirs = [path.join(path.dirname(__file__), "commands"), | |||
| path.join(self.bot.config.root_dir, "commands")] | |||
| for dir in dirs: | |||
| files = listdir(dir) | |||
| files = [sub("\.pyc?$", "", f) for f in files if f[0] != "_"] | |||
| files = list(set(files)) # Remove duplicates | |||
| for filename in sorted(files): | |||
| self._load_command(filename, dir) | |||
| msg = "Found {0} commands: {1}" | |||
| builtin_dir = path.dirname(__file__) | |||
| plugins_dir = path.join(self.bot.config.root_dir, "commands") | |||
| self._load_directory(builtin_dir) # Built-in commands | |||
| self._load_directory(plugins_dir) # Custom commands, aka plugins | |||
| msg = "Loaded {0} commands: {1}" | |||
| commands = ", ".join(self._commands.keys()) | |||
| self.logger.info(msg.format(len(self._commands), commands)) | |||
| @@ -59,8 +59,9 @@ class BotConfig(object): | |||
| aren't encrypted | |||
| """ | |||
| def __init__(self, root_dir): | |||
| def __init__(self, root_dir, level): | |||
| self._root_dir = root_dir | |||
| self._logging_level = level | |||
| self._config_path = path.join(self._root_dir, "config.yml") | |||
| self._log_dir = path.join(self._root_dir, "logs") | |||
| self._decryption_key = None | |||
| @@ -74,7 +75,14 @@ class BotConfig(object): | |||
| self._nodes = [self._components, self._wiki, self._tasks, self._irc, | |||
| self._metadata] | |||
| self._decryptable_nodes = [] | |||
| self._decryptable_nodes = [ # Default nodes to decrypt | |||
| (self._wiki, ("password")), | |||
| (self._wiki, ("search", "credentials", "key")), | |||
| (self._wiki, ("search", "credentials", "secret")), | |||
| (self._irc, ("frontend", "nickservPassword")), | |||
| (self._irc, ("watcher", "nickservPassword")), | |||
| ] | |||
| def _load(self): | |||
| """Load data from our JSON config file (config.yml) into self._data.""" | |||
| @@ -119,10 +127,10 @@ class BotConfig(object): | |||
| h.setFormatter(formatter) | |||
| logger.addHandler(h) | |||
| stream_handler = logging.StreamHandler() | |||
| stream_handler.setLevel(logging.DEBUG) | |||
| stream_handler.setFormatter(color_formatter) | |||
| logger.addHandler(stream_handler) | |||
| self._stream_handler = stream = logging.StreamHandler() | |||
| stream.setLevel(self._logging_level) | |||
| stream.setFormatter(color_formatter) | |||
| logger.addHandler(stream) | |||
| def _decrypt(self, node, nodes): | |||
| """Try to decrypt the contents of a config node. Use self.decrypt().""" | |||
| @@ -148,6 +156,15 @@ class BotConfig(object): | |||
| return self._root_dir | |||
| @property | |||
| def logging_level(self): | |||
| return self._logging_level | |||
| @logging_level.setter | |||
| def logging_level(self, level): | |||
| self._logging_level = level | |||
| self._stream_handler.setLevel(level) | |||
| @property | |||
| def path(self): | |||
| return self._config_path | |||
| @@ -213,7 +230,7 @@ class BotConfig(object): | |||
| if choice.lower().startswith("y"): | |||
| self._make_new() | |||
| else: | |||
| exit(1) | |||
| exit(1) # TODO: raise an exception instead | |||
| self._load() | |||
| data = self._data | |||
| @@ -30,6 +30,7 @@ internal TaskManager class. This can be accessed through `bot.tasks`. | |||
| import imp | |||
| from os import listdir, path | |||
| from re import sub | |||
| from threading import Lock, Thread | |||
| from time import gmtime, strftime | |||
| @@ -186,22 +187,31 @@ class TaskManager(object): | |||
| return | |||
| self._tasks[task.name] = task | |||
| self.logger.debug("Added task {0}".format(task.name)) | |||
| self.logger.debug("Loaded task {0}".format(task.name)) | |||
| def _load_directory(self, dir): | |||
| """Load all valid tasks in a given directory.""" | |||
| processed = [] | |||
| for name in listdir(dir): | |||
| if not name.endswith(".py") and not name.endswith(".pyc"): | |||
| continue | |||
| if name.startswith("_") or name.startswith("."): | |||
| continue | |||
| modname = sub("\.pyc?$", "", name) # Remove extension | |||
| if modname not in processed: | |||
| self._load_task(modname, dir) | |||
| processed.append(modname) | |||
| def load(self): | |||
| """Load (or reload) all valid tasks into self._tasks.""" | |||
| with self._task_access_lock: | |||
| self._tasks.clear() | |||
| dirs = [path.join(path.dirname(__file__), "tasks"), | |||
| path.join(self.bot.config.root_dir, "tasks")] | |||
| for dir in dirs: | |||
| files = listdir(dir) | |||
| files = [sub("\.pyc?$", "", f) for f in files if f[0] != "_"] | |||
| files = list(set(files)) # Remove duplicates | |||
| for filename in sorted(files): | |||
| self._load_task(filename) | |||
| msg = "Found {0} tasks: {1}" | |||
| builtin_dir = path.dirname(__file__) | |||
| plugins_dir = path.join(self.bot.config.root_dir, "tasks") | |||
| self._load_directory(builtin_dir) # Built-in tasks | |||
| self._load_directory(plugins_dir) # Custom tasks, aka plugins | |||
| msg = "Loaded {0} tasks: {1}" | |||
| tasks = ', '.join(self._tasks.keys()) | |||
| self.logger.info(msg.format(len(self._tasks), tasks)) | |||
| @@ -21,46 +21,54 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| """ | |||
| This is EarwigBot's command-line utility, enabling you to easily start the | |||
| bot or run specific tasks. | |||
| """ | |||
| import argparse | |||
| import logging | |||
| from os import path | |||
| from earwigbot import __version__ | |||
| from earwigbot.bot import Bot | |||
| __all__ = ["BotUtility", "main"] | |||
| class BotUtility(object): | |||
| """ | |||
| This is a command-line utility for EarwigBot that enables you to easily | |||
| start the bot without writing generally unnecessary three-line bootstrap | |||
| scripts. It supports starting the bot from any directory, as well as | |||
| starting individual tasks instead of the entire bot. | |||
| """ | |||
| def version(self): | |||
| return "EarwigBot v{0}".format(__version__) | |||
| def run(self, root_dir): | |||
| bot = Bot(root_dir) | |||
| print self.version() | |||
| #try: | |||
| # bot.run() | |||
| #finally: | |||
| # bot.stop() | |||
| def main(self): | |||
| parser = argparse.ArgumentParser(description=BotUtility.__doc__) | |||
| parser.add_argument("-v", "--version", action="version", | |||
| version=self.version()) | |||
| parser.add_argument("root_dir", metavar="path", nargs="?", default=path.curdir) | |||
| args = parser.parse_args() | |||
| __all__ = ["main"] | |||
| root_dir = path.abspath(args.root_dir) | |||
| self.run(root_dir) | |||
| def main(): | |||
| version = "EarwigBot v{0}".format(__version__) | |||
| parser = argparse.ArgumentParser(description=__doc__) | |||
| parser.add_argument("path", nargs="?", metavar="PATH", default=path.curdir, | |||
| help="path to the bot's working directory, which will be created if it doesn't exist; current directory assumed if not specified") | |||
| parser.add_argument("-v", "--version", action="version", version=version) | |||
| parser.add_argument("-d", "--debug", action="store_true", | |||
| help="print all logs, including DEBUG-level messages") | |||
| parser.add_argument("-q", "--quiet", action="store_true", | |||
| help="don't print any logs except warnings and errors") | |||
| parser.add_argument("-t", "--task", metavar="NAME", | |||
| help="given the name of a task, the bot will run it instead of the main bot and then exit") | |||
| args = parser.parse_args() | |||
| if args.debug and args.quiet: | |||
| parser.print_usage() | |||
| print "earwigbot: error: cannot show debug messages and be quiet at the same time" | |||
| return | |||
| level = logging.INFO | |||
| if args.debug: | |||
| level = logging.DEBUG | |||
| elif args.quiet: | |||
| level = logging.WARNING | |||
| main = BotUtility().main | |||
| print version | |||
| bot = Bot(path.abspath(args.path), level=level) | |||
| try: | |||
| if args.task: | |||
| bot.tasks.start(args.task) | |||
| else: | |||
| bot.run() | |||
| finally: | |||
| bot.stop() | |||
| if __name__ == "__main__": | |||
| main() | |||