@@ -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", | |||