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