- Fixed loading bugs in CommandLoader and TaskLoadertags/v0.1^2
@@ -53,8 +53,8 @@ class Bot(object): | |||||
wiki toolset with bot.wiki.get_site(). | 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.logger = logging.getLogger("earwigbot") | ||||
self.commands = CommandManager(self) | self.commands = CommandManager(self) | ||||
self.tasks = TaskManager(self) | self.tasks = TaskManager(self) | ||||
@@ -65,6 +65,10 @@ class Bot(object): | |||||
self.component_lock = Lock() | self.component_lock = Lock() | ||||
self._keep_looping = True | self._keep_looping = True | ||||
self.config.load() | |||||
self.commands.load() | |||||
self.tasks.load() | |||||
def _start_irc_components(self): | def _start_irc_components(self): | ||||
if self.config.components.get("irc_frontend"): | if self.config.components.get("irc_frontend"): | ||||
self.logger.info("Starting IRC frontend") | self.logger.info("Starting IRC frontend") | ||||
@@ -107,18 +111,10 @@ class Bot(object): | |||||
self.logger.warn("IRC watcher has stopped; restarting") | self.logger.warn("IRC watcher has stopped; restarting") | ||||
self.watcher = Watcher(self) | self.watcher = Watcher(self) | ||||
Thread(name=name, target=self.watcher.loop).start() | Thread(name=name, target=self.watcher.loop).start() | ||||
sleep(5) | |||||
sleep(3) | |||||
def run(self): | 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_irc_components() | ||||
self._start_wiki_scheduler() | self._start_wiki_scheduler() | ||||
self._loop() | self._loop() | ||||
@@ -137,4 +133,3 @@ class Bot(object): | |||||
with self.component_lock: | with self.component_lock: | ||||
self._stop_irc_components() | self._stop_irc_components() | ||||
self._keep_looping = False | self._keep_looping = False | ||||
sleep(3) # Give a few seconds to finish closing IRC connections |
@@ -135,22 +135,31 @@ class CommandManager(object): | |||||
return | return | ||||
self._commands[command.name] = command | 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): | def load(self): | ||||
"""Load (or reload) all valid commands into self._commands.""" | """Load (or reload) all valid commands into self._commands.""" | ||||
with self._command_access_lock: | with self._command_access_lock: | ||||
self._commands.clear() | 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()) | commands = ", ".join(self._commands.keys()) | ||||
self.logger.info(msg.format(len(self._commands), commands)) | self.logger.info(msg.format(len(self._commands), commands)) | ||||
@@ -59,8 +59,9 @@ class BotConfig(object): | |||||
aren't encrypted | aren't encrypted | ||||
""" | """ | ||||
def __init__(self, root_dir): | |||||
def __init__(self, root_dir, level): | |||||
self._root_dir = root_dir | self._root_dir = root_dir | ||||
self._logging_level = level | |||||
self._config_path = path.join(self._root_dir, "config.yml") | self._config_path = path.join(self._root_dir, "config.yml") | ||||
self._log_dir = path.join(self._root_dir, "logs") | self._log_dir = path.join(self._root_dir, "logs") | ||||
self._decryption_key = None | self._decryption_key = None | ||||
@@ -74,7 +75,14 @@ class BotConfig(object): | |||||
self._nodes = [self._components, self._wiki, self._tasks, self._irc, | self._nodes = [self._components, self._wiki, self._tasks, self._irc, | ||||
self._metadata] | 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): | def _load(self): | ||||
"""Load data from our JSON config file (config.yml) into self._data.""" | """Load data from our JSON config file (config.yml) into self._data.""" | ||||
@@ -119,10 +127,10 @@ class BotConfig(object): | |||||
h.setFormatter(formatter) | h.setFormatter(formatter) | ||||
logger.addHandler(h) | 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): | def _decrypt(self, node, nodes): | ||||
"""Try to decrypt the contents of a config node. Use self.decrypt().""" | """Try to decrypt the contents of a config node. Use self.decrypt().""" | ||||
@@ -148,6 +156,15 @@ class BotConfig(object): | |||||
return self._root_dir | return self._root_dir | ||||
@property | @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): | def path(self): | ||||
return self._config_path | return self._config_path | ||||
@@ -213,7 +230,7 @@ class BotConfig(object): | |||||
if choice.lower().startswith("y"): | if choice.lower().startswith("y"): | ||||
self._make_new() | self._make_new() | ||||
else: | else: | ||||
exit(1) | |||||
exit(1) # TODO: raise an exception instead | |||||
self._load() | self._load() | ||||
data = self._data | data = self._data | ||||
@@ -30,6 +30,7 @@ internal TaskManager class. This can be accessed through `bot.tasks`. | |||||
import imp | import imp | ||||
from os import listdir, path | from os import listdir, path | ||||
from re import sub | |||||
from threading import Lock, Thread | from threading import Lock, Thread | ||||
from time import gmtime, strftime | from time import gmtime, strftime | ||||
@@ -186,22 +187,31 @@ class TaskManager(object): | |||||
return | return | ||||
self._tasks[task.name] = task | 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): | def load(self): | ||||
"""Load (or reload) all valid tasks into self._tasks.""" | """Load (or reload) all valid tasks into self._tasks.""" | ||||
with self._task_access_lock: | with self._task_access_lock: | ||||
self._tasks.clear() | 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()) | tasks = ', '.join(self._tasks.keys()) | ||||
self.logger.info(msg.format(len(self._tasks), tasks)) | 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 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
# SOFTWARE. | # SOFTWARE. | ||||
""" | |||||
This is EarwigBot's command-line utility, enabling you to easily start the | |||||
bot or run specific tasks. | |||||
""" | |||||
import argparse | import argparse | ||||
import logging | |||||
from os import path | from os import path | ||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
from earwigbot.bot import Bot | 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__": | if __name__ == "__main__": | ||||
main() | main() |