@@ -23,10 +23,13 @@ | |||||
import threading | import threading | ||||
from time import sleep, time | from time import sleep, time | ||||
from earwigbot.commands import CommandManager | |||||
from earwigbot.config import BotConfig | from earwigbot.config import BotConfig | ||||
from earwigbot.irc import Frontend, Watcher | from earwigbot.irc import Frontend, Watcher | ||||
from earwigbot.tasks import task_manager | from earwigbot.tasks import task_manager | ||||
__all__ = ["Bot"] | |||||
class Bot(object): | class Bot(object): | ||||
""" | """ | ||||
The Bot class is the core of EarwigBot, essentially responsible for | The Bot class is the core of EarwigBot, essentially responsible for | ||||
@@ -46,16 +49,14 @@ class Bot(object): | |||||
def __init__(self, root_dir): | def __init__(self, root_dir): | ||||
self.config = BotConfig(root_dir) | self.config = BotConfig(root_dir) | ||||
self.logger = logging.getLogger("earwigbot") | self.logger = logging.getLogger("earwigbot") | ||||
self.commands = CommandManager(self) | |||||
self.tasks = None | |||||
self.frontend = None | self.frontend = None | ||||
self.watcher = None | self.watcher = None | ||||
self._keep_scheduling = True | self._keep_scheduling = True | ||||
self._lock = threading.Lock() | self._lock = threading.Lock() | ||||
def _start_thread(self, name, target): | |||||
thread = threading.Thread(name=name, target=target) | |||||
thread.start() | |||||
def _wiki_scheduler(self): | def _wiki_scheduler(self): | ||||
while self._keep_scheduling: | while self._keep_scheduling: | ||||
time_start = time() | time_start = time() | ||||
@@ -68,17 +69,18 @@ class Bot(object): | |||||
def _start_components(self): | def _start_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") | ||||
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"): | if self.config.components.get("irc_watcher"): | ||||
self.logger.info("Starting 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"): | if self.config.components.get("wiki_scheduler"): | ||||
self.logger.info("Starting 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): | def _loop(self): | ||||
while 1: | while 1: | ||||
@@ -104,6 +106,7 @@ class Bot(object): | |||||
with self._lock: | with self._lock: | ||||
self.config.load() | self.config.load() | ||||
#if self.config.components.get("irc_frontend"): | #if self.config.components.get("irc_frontend"): | ||||
# self.commands.load() | |||||
def stop(self): | def stop(self): | ||||
if self.frontend: | if self.frontend: | ||||
@@ -111,3 +114,4 @@ class Bot(object): | |||||
if self.watcher: | if self.watcher: | ||||
self.watcher.stop() | self.watcher.stop() | ||||
self._keep_scheduling = False | 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 package provides the IRC "commands" used by the bot's front-end component. | ||||
This module contains the BaseCommand class (import with | 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 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): | class BaseCommand(object): | ||||
"""A base class for commands on IRC. | """A base class for commands on IRC. | ||||
@@ -88,32 +87,33 @@ class BaseCommand(object): | |||||
pass | pass | ||||
class _CommandManager(object): | |||||
def __init__(self): | |||||
class CommandManager(object): | |||||
def __init__(self, bot): | |||||
self.bot = bot | |||||
self.logger = logging.getLogger("earwigbot.tasks") | 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 = {} | 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: | 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 | return | ||||
finally: | |||||
f.close() | |||||
try: | try: | ||||
command = sys.modules[name].Command(self._connection) | |||||
command = module.Command(self.bot.frontend) | |||||
except AttributeError: | except AttributeError: | ||||
return # No command in this module | return # No command in this module | ||||
if not isinstance(command, BaseCommand): | if not isinstance(command, BaseCommand): | ||||
@@ -122,20 +122,16 @@ class _CommandManager(object): | |||||
self._commands[command.name] = command | self._commands[command.name] = command | ||||
self.logger.debug("Added command {0}".format(command.name)) | 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}" | msg = "Found {0} commands: {1}" | ||||
commands = ", ".join(self._commands.keys()) | commands = ", ".join(self._commands.keys()) | ||||
@@ -158,6 +154,3 @@ class _CommandManager(object): | |||||
e = "Error executing command '{0}'" | e = "Error executing command '{0}'" | ||||
self.logger.exception(e.format(data.command)) | self.logger.exception(e.format(data.command)) | ||||
break | break | ||||
command_manager = _CommandManager() |
@@ -23,7 +23,6 @@ | |||||
import logging | import logging | ||||
import re | import re | ||||
from earwigbot.commands import command_manager | |||||
from earwigbot.irc import IRCConnection, Data, BrokenSocketException | from earwigbot.irc import IRCConnection, Data, BrokenSocketException | ||||
__all__ = ["Frontend"] | __all__ = ["Frontend"] | ||||
@@ -40,14 +39,15 @@ class Frontend(IRCConnection): | |||||
""" | """ | ||||
sender_regex = re.compile(":(.*?)!(.*?)@(.*?)\Z") | 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") | self.logger = logging.getLogger("earwigbot.frontend") | ||||
cf = config.irc["frontend"] | cf = config.irc["frontend"] | ||||
base = super(Frontend, self) | base = super(Frontend, self) | ||||
base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | ||||
cf["realname"], self.logger) | cf["realname"], self.logger) | ||||
command_manager.load(self) | |||||
self._connect() | self._connect() | ||||
def _process_message(self, line): | def _process_message(self, line): | ||||
@@ -38,14 +38,16 @@ class Watcher(IRCConnection): | |||||
to channels on the IRC frontend. | 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") | self.logger = logging.getLogger("earwigbot.watcher") | ||||
cf = config.irc["watcher"] | cf = config.irc["watcher"] | ||||
base = super(Watcher, self) | base = super(Watcher, self) | ||||
base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | ||||
cf["realname"], self.logger) | cf["realname"], self.logger) | ||||
self.frontend = frontend | |||||
self._prepare_process_hook() | self._prepare_process_hook() | ||||
self._connect() | self._connect() | ||||
@@ -36,12 +36,22 @@ class BotUtility(object): | |||||
return __version__ | return __version__ | ||||
def run(self): | def run(self): | ||||
print "EarwigBot v{0}\n".format(self.version()) | |||||
def main(self): | |||||
root_dir = path.abspath(path.curdir()) | root_dir = path.abspath(path.curdir()) | ||||
bot = Bot(root_dir) | 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 | main = BotUtility().main | ||||
@@ -30,8 +30,10 @@ from setuptools import setup | |||||
setup( | setup( | ||||
name = "earwigbot", | name = "earwigbot", | ||||
entry_points = {"console_scripts": ["earwigbot = earwigbot.util:main"]}, | 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", | version = "0.1.dev", | ||||
author = "Ben Kurtovic", | author = "Ben Kurtovic", | ||||
author_email = "ben.kurtovic@verizon.net", | author_email = "ben.kurtovic@verizon.net", | ||||