| @@ -23,10 +23,13 @@ | |||
| import threading | |||
| from time import sleep, time | |||
| from earwigbot.commands import CommandManager | |||
| from earwigbot.config import BotConfig | |||
| from earwigbot.irc import Frontend, Watcher | |||
| from earwigbot.tasks import task_manager | |||
| __all__ = ["Bot"] | |||
| class Bot(object): | |||
| """ | |||
| The Bot class is the core of EarwigBot, essentially responsible for | |||
| @@ -46,16 +49,14 @@ class Bot(object): | |||
| def __init__(self, root_dir): | |||
| self.config = BotConfig(root_dir) | |||
| self.logger = logging.getLogger("earwigbot") | |||
| self.commands = CommandManager(self) | |||
| self.tasks = None | |||
| self.frontend = None | |||
| self.watcher = None | |||
| self._keep_scheduling = True | |||
| self._lock = threading.Lock() | |||
| def _start_thread(self, name, target): | |||
| thread = threading.Thread(name=name, target=target) | |||
| thread.start() | |||
| def _wiki_scheduler(self): | |||
| while self._keep_scheduling: | |||
| time_start = time() | |||
| @@ -68,17 +69,18 @@ class Bot(object): | |||
| def _start_components(self): | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| self.frontend = Frontend(self.config) | |||
| self._start_thread(name, self.frontend.loop) | |||
| self.frontend = Frontend(self) | |||
| self.commands.load() | |||
| threading.Thread(name=name, target=self.frontend.loop).start() | |||
| if self.config.components.get("irc_watcher"): | |||
| self.logger.info("Starting IRC watcher") | |||
| self.watcher = Watcher(self.config, self.frontend) | |||
| self._start_thread(name, self.watcher.loop) | |||
| self.watcher = Watcher(self) | |||
| threading.Thread(name=name, target=self.watcher.loop).start() | |||
| if self.config.components.get("wiki_scheduler"): | |||
| self.logger.info("Starting wiki scheduler") | |||
| self._start_thread(name, self._wiki_scheduler) | |||
| threading.Thread(name=name, target=self._wiki_scheduler).start() | |||
| def _loop(self): | |||
| while 1: | |||
| @@ -104,6 +106,7 @@ class Bot(object): | |||
| with self._lock: | |||
| self.config.load() | |||
| #if self.config.components.get("irc_frontend"): | |||
| # self.commands.load() | |||
| def stop(self): | |||
| if self.frontend: | |||
| @@ -111,3 +114,4 @@ class Bot(object): | |||
| if self.watcher: | |||
| self.watcher.stop() | |||
| self._keep_scheduling = False | |||
| sleep(3) # Give a few seconds to finish closing IRC connections | |||
| @@ -25,17 +25,16 @@ EarwigBot's IRC Command Manager | |||
| This package provides the IRC "commands" used by the bot's front-end component. | |||
| This module contains the BaseCommand class (import with | |||
| `from earwigbot.commands import BaseCommand`) and an internal _CommandManager | |||
| class. This can be accessed through the `command_manager` singleton. | |||
| `from earwigbot.commands import BaseCommand`) and an internal CommandManager | |||
| class. This can be accessed through `bot.commands`. | |||
| """ | |||
| import imp | |||
| import logging | |||
| import os | |||
| import sys | |||
| from os import listdir, path | |||
| from re import sub | |||
| from earwigbot.config import config | |||
| __all__ = ["BaseCommand", "command_manager"] | |||
| __all__ = ["BaseCommand", "CommandManager"] | |||
| class BaseCommand(object): | |||
| """A base class for commands on IRC. | |||
| @@ -88,32 +87,33 @@ class BaseCommand(object): | |||
| pass | |||
| class _CommandManager(object): | |||
| def __init__(self): | |||
| class CommandManager(object): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = logging.getLogger("earwigbot.tasks") | |||
| self._base_dir = os.path.dirname(os.path.abspath(__file__)) | |||
| self._connection = None | |||
| self._dirs = [path.dirname(__file__), bot.config.root_dir] | |||
| self._commands = {} | |||
| def _load_command(self, filename): | |||
| """Load a specific command from a module, identified by filename. | |||
| def _load_command(self, name, path): | |||
| """Load a specific command from a module, identified by name and path. | |||
| Given a Connection object and a filename, we'll first try to import | |||
| it, and if that works, make an instance of the 'Command' class inside | |||
| (assuming it is an instance of BaseCommand), add it to self._commands, | |||
| and log the addition. Any problems along the way will either be | |||
| ignored or logged. | |||
| We'll first try to import it using imp magic, and if that works, make | |||
| an instance of the 'Command' class inside (assuming it is an instance | |||
| of BaseCommand), add it to self._commands, and log the addition. Any | |||
| problems along the way will either be ignored or logged. | |||
| """ | |||
| # Strip .py from the filename's end and join with our package name: | |||
| name = ".".join(("commands", filename[:-3])) | |||
| f, path, desc = imp.find_module(name, [path]) | |||
| try: | |||
| __import__(name) | |||
| except: | |||
| self.logger.exception("Couldn't load file {0}".format(filename)) | |||
| module = imp.load_module(name, f, path, desc) | |||
| except Exception: | |||
| e = "Couldn't load module {0} from {1}" | |||
| self.logger.exception(e.format(name, path)) | |||
| return | |||
| finally: | |||
| f.close() | |||
| try: | |||
| command = sys.modules[name].Command(self._connection) | |||
| command = module.Command(self.bot.frontend) | |||
| except AttributeError: | |||
| return # No command in this module | |||
| if not isinstance(command, BaseCommand): | |||
| @@ -122,20 +122,16 @@ class _CommandManager(object): | |||
| self._commands[command.name] = command | |||
| self.logger.debug("Added command {0}".format(command.name)) | |||
| def load(self, connection): | |||
| """Load all valid commands into self._commands. | |||
| `connection` is a Connection object that is given to each command's | |||
| constructor. | |||
| """ | |||
| self._connection = connection | |||
| files = os.listdir(self._base_dir) | |||
| files.sort() | |||
| for filename in files: | |||
| if filename.startswith("_") or not filename.endswith(".py"): | |||
| continue | |||
| self._load_command(filename) | |||
| def load(self): | |||
| """Load (or reload) all valid commands into self._commands.""" | |||
| dirs = [path.join(path.dirname(__file__), "commands"), | |||
| path.join(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}" | |||
| commands = ", ".join(self._commands.keys()) | |||
| @@ -158,6 +154,3 @@ class _CommandManager(object): | |||
| e = "Error executing command '{0}'" | |||
| self.logger.exception(e.format(data.command)) | |||
| break | |||
| command_manager = _CommandManager() | |||
| @@ -23,7 +23,6 @@ | |||
| import logging | |||
| import re | |||
| from earwigbot.commands import command_manager | |||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | |||
| __all__ = ["Frontend"] | |||
| @@ -40,14 +39,15 @@ class Frontend(IRCConnection): | |||
| """ | |||
| sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | |||
| def __init__(self, config): | |||
| self.config = config | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.config = bot.config | |||
| self.logger = logging.getLogger("earwigbot.frontend") | |||
| cf = config.irc["frontend"] | |||
| base = super(Frontend, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| command_manager.load(self) | |||
| self._connect() | |||
| def _process_message(self, line): | |||
| @@ -38,14 +38,16 @@ class Watcher(IRCConnection): | |||
| to channels on the IRC frontend. | |||
| """ | |||
| def __init__(self, config, frontend=None): | |||
| self.config = config | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.config = bot.config | |||
| self.frontend = bot.frontend | |||
| self.logger = logging.getLogger("earwigbot.watcher") | |||
| cf = config.irc["watcher"] | |||
| base = super(Watcher, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| self.frontend = frontend | |||
| self._prepare_process_hook() | |||
| self._connect() | |||
| @@ -36,12 +36,22 @@ class BotUtility(object): | |||
| return __version__ | |||
| def run(self): | |||
| print "EarwigBot v{0}\n".format(self.version()) | |||
| def main(self): | |||
| root_dir = path.abspath(path.curdir()) | |||
| bot = Bot(root_dir) | |||
| bot.run() | |||
| try: | |||
| bot.run() | |||
| finally: | |||
| bot.stop() | |||
| def main(self): | |||
| print "EarwigBot v{0}\n".format(self.version()) | |||
| parser = argparse.ArgumentParser(description=BotUtility.__doc__) | |||
| parser.add_argument("-V", "--version", action="version", | |||
| version=self.version()) | |||
| args = parser.parse_args() | |||
| # args.func(args) | |||
| main = BotUtility().main | |||
| @@ -30,8 +30,10 @@ from setuptools import setup | |||
| setup( | |||
| name = "earwigbot", | |||
| entry_points = {"console_scripts": ["earwigbot = earwigbot.util:main"]}, | |||
| install_requires = ["PyYAML>=3.10", "oursql>=0.9.3", "oauth2>=1.5.211", | |||
| "numpy>=1.6.1", "matplotlib>=1.1.0"], | |||
| install_requires = ["PyYAML>=3.10", | |||
| "oursql>=0.9.3", | |||
| "oauth2>=1.5.211", | |||
| "matplotlib>=1.1.0"], | |||
| version = "0.1.dev", | |||
| author = "Ben Kurtovic", | |||
| author_email = "ben.kurtovic@verizon.net", | |||