| @@ -54,23 +54,13 @@ class Bot(object): | |||
| self.frontend = None | |||
| self.watcher = None | |||
| self._keep_scheduling = True | |||
| self._lock = threading.Lock() | |||
| def _wiki_scheduler(self): | |||
| while self._keep_scheduling: | |||
| time_start = time() | |||
| task_manager.schedule() | |||
| time_end = time() | |||
| time_diff = time_start - time_end | |||
| if time_diff < 60: # Sleep until the next minute | |||
| sleep(60 - time_diff) | |||
| def _start_components(self): | |||
| self.component_lock = threading.Lock() | |||
| self._keep_looping = True | |||
| def _start_irc_components(self): | |||
| if self.config.components.get("irc_frontend"): | |||
| self.logger.info("Starting IRC frontend") | |||
| self.frontend = Frontend(self) | |||
| self.commands.load() | |||
| threading.Thread(name=name, target=self.frontend.loop).start() | |||
| if self.config.components.get("irc_watcher"): | |||
| @@ -78,17 +68,35 @@ class Bot(object): | |||
| self.watcher = Watcher(self) | |||
| threading.Thread(name=name, target=self.watcher.loop).start() | |||
| def _start_wiki_scheduler(self): | |||
| def wiki_scheduler(): | |||
| while self._keep_looping: | |||
| time_start = time() | |||
| task_manager.schedule() | |||
| time_end = time() | |||
| time_diff = time_start - time_end | |||
| if time_diff < 60: # Sleep until the next minute | |||
| sleep(60 - time_diff) | |||
| if self.config.components.get("wiki_scheduler"): | |||
| self.logger.info("Starting wiki scheduler") | |||
| threading.Thread(name=name, target=self._wiki_scheduler).start() | |||
| threading.Thread(name=name, target=wiki_scheduler).start() | |||
| def _stop_irc_components(self): | |||
| if self.frontend: | |||
| self.frontend.stop() | |||
| if self.watcher: | |||
| self.watcher.stop() | |||
| def _loop(self): | |||
| while 1: | |||
| with self._lock: | |||
| while self._keep_looping: | |||
| with self.component_lock: | |||
| if self.frontend and self.frontend.is_stopped(): | |||
| self.frontend._connect() | |||
| self.frontend = Frontend(self) | |||
| threading.Thread(name=name, target=self.frontend.loop).start() | |||
| if self.watcher and self.watcher.is_stopped(): | |||
| self.watcher._connect() | |||
| self.watcher = Watcher(self) | |||
| threading.Thread(name=name, target=self.watcher.loop).start() | |||
| sleep(5) | |||
| def run(self): | |||
| @@ -97,21 +105,20 @@ class Bot(object): | |||
| self.config.decrypt(config.wiki, "search", "credentials", "key") | |||
| self.config.decrypt(config.wiki, "search", "credentials", "secret") | |||
| self.config.decrypt(config.irc, "frontend", "nickservPassword") | |||
| self.config.decrypt(config.irc, "watcher", "nickservPassword") | |||
| self._start_components() | |||
| self.config.decrypt(config.irc, "watcher", "nickservPassword") | |||
| self.commands.load() | |||
| self._start_irc_components() | |||
| self._start_wiki_scheduler() | |||
| self._loop() | |||
| def reload(self): | |||
| #components = self.config.components | |||
| with self._lock: | |||
| def restart(self): | |||
| with self.component_lock: | |||
| self._stop_irc_components() | |||
| self.config.load() | |||
| #if self.config.components.get("irc_frontend"): | |||
| # self.commands.load() | |||
| self.commands.load() | |||
| self._start_irc_components() | |||
| def stop(self): | |||
| if self.frontend: | |||
| self.frontend.stop() | |||
| if self.watcher: | |||
| self.watcher.stop() | |||
| self._keep_scheduling = False | |||
| self._stop_irc_components() | |||
| self._keep_looping = False | |||
| sleep(3) # Give a few seconds to finish closing IRC connections | |||
| @@ -30,9 +30,9 @@ class. This can be accessed through `bot.commands`. | |||
| """ | |||
| import imp | |||
| import logging | |||
| from os import listdir, path | |||
| from re import sub | |||
| from threading import Lock | |||
| __all__ = ["BaseCommand", "CommandManager"] | |||
| @@ -49,21 +49,24 @@ class BaseCommand(object): | |||
| # command subclass: | |||
| hooks = ["msg"] | |||
| def __init__(self, connection): | |||
| def __init__(self, bot): | |||
| """Constructor for new commands. | |||
| This is called once when the command is loaded (from | |||
| commands._load_command()). `connection` is a Connection object, | |||
| allowing us to do self.connection.say(), self.connection.send(), etc, | |||
| from within a method. | |||
| commands._load_command()). `bot` is out base Bot object. Generally you | |||
| shouldn't need to override this; if you do, call | |||
| super(Command, self).__init__() first. | |||
| """ | |||
| self.connection = connection | |||
| logger_name = ".".join(("earwigbot", "commands", self.name)) | |||
| self.logger = logging.getLogger(logger_name) | |||
| self.logger.setLevel(logging.DEBUG) | |||
| self.bot = bot | |||
| self.logger = bot.commands.getLogger(self.name) | |||
| def _execute(self, data): | |||
| """Make a quick connection alias and then process() the message.""" | |||
| self.connection = self.bot.frontend | |||
| self.process(data) | |||
| def check(self, data): | |||
| """Returns whether this command should be called in response to 'data'. | |||
| """Return whether this command should be called in response to 'data'. | |||
| Given a Data() instance, return True if we should respond to this | |||
| activity, or False if we should ignore it or it doesn't apply to us. | |||
| @@ -72,17 +75,16 @@ class BaseCommand(object): | |||
| return False. This is the default behavior of check(); you need only | |||
| override it if you wish to change that. | |||
| """ | |||
| if data.is_command and data.command == self.name: | |||
| return True | |||
| return False | |||
| return data.is_command and data.command == self.name | |||
| def process(self, data): | |||
| """Main entry point for doing a command. | |||
| Handle an activity (usually a message) on IRC. At this point, thanks | |||
| to self.check() which is called automatically by the command handler, | |||
| we know this is something we should respond to, so (usually) something | |||
| like 'if data.command != "command_name": return' is unnecessary. | |||
| we know this is something we should respond to, so something like | |||
| `if data.command != "command_name": return` is usually unnecessary. | |||
| Note that | |||
| """ | |||
| pass | |||
| @@ -90,9 +92,9 @@ class BaseCommand(object): | |||
| class CommandManager(object): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.logger = logging.getLogger("earwigbot.tasks") | |||
| self._dirs = [path.dirname(__file__), bot.config.root_dir] | |||
| self.logger = bot.logger.getLogger("commands") | |||
| self._commands = {} | |||
| self._command_access_lock = Lock() | |||
| def _load_command(self, name, path): | |||
| """Load a specific command from a module, identified by name and path. | |||
| @@ -113,7 +115,7 @@ class CommandManager(object): | |||
| f.close() | |||
| try: | |||
| command = module.Command(self.bot.frontend) | |||
| command = module.Command(self.bot) | |||
| except AttributeError: | |||
| return # No command in this module | |||
| if not isinstance(command, BaseCommand): | |||
| @@ -124,14 +126,16 @@ class CommandManager(object): | |||
| 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) | |||
| self._commands = {} | |||
| with self._command_access_lock: | |||
| 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()) | |||
| @@ -143,14 +147,13 @@ class CommandManager(object): | |||
| def check(self, hook, data): | |||
| """Given an IRC event, check if there's anything we can respond to.""" | |||
| # Parse command arguments into data.command and data.args: | |||
| data.parse_args() | |||
| for command in self._commands.values(): | |||
| if hook in command.hooks: | |||
| if command.check(data): | |||
| try: | |||
| command.process(data) | |||
| except Exception: | |||
| e = "Error executing command '{0}'" | |||
| self.logger.exception(e.format(data.command)) | |||
| break | |||
| with self._command_access_lock: | |||
| for command in self._commands.values(): | |||
| if hook in command.hooks: | |||
| if command.check(data): | |||
| try: | |||
| command._execute(data) | |||
| except Exception: | |||
| e = "Error executing command '{0}':" | |||
| self.logger.exception(e.format(data.command)) | |||
| break | |||
| @@ -27,11 +27,20 @@ class Command(BaseCommand): | |||
| """Restart the bot. Only the owner can do this.""" | |||
| name = "restart" | |||
| def check(self, data): | |||
| commands = ["restart", "reload"] | |||
| return data.is_command and data.command in commands | |||
| def process(self, data): | |||
| if data.host not in config.irc["permissions"]["owners"]: | |||
| msg = "you must be a bot owner to use this command." | |||
| self.connection.reply(data, msg) | |||
| return | |||
| self.connection.logger.info("Restarting bot per owner request") | |||
| self.connection.stop() | |||
| if data.command == "restart": | |||
| self.connection.logger.info("Restarting bot per owner request") | |||
| self.connection.bot.restart() | |||
| elif data.command == "reload": | |||
| self.connection.bot.commands.load() | |||
| self.connection.logger.info("IRC commands reloaded") | |||
| @@ -21,7 +21,7 @@ | |||
| # SOFTWARE. | |||
| import socket | |||
| import threading | |||
| from threading import Lock | |||
| from time import sleep | |||
| __all__ = ["BrokenSocketException", "IRCConnection"] | |||
| @@ -36,17 +36,16 @@ class BrokenSocketException(Exception): | |||
| class IRCConnection(object): | |||
| """A class to interface with IRC.""" | |||
| def __init__(self, host, port, nick, ident, realname, logger): | |||
| def __init__(self, host, port, nick, ident, realname): | |||
| self.host = host | |||
| self.port = port | |||
| self.nick = nick | |||
| self.ident = ident | |||
| self.realname = realname | |||
| self.logger = logger | |||
| self._is_running = False | |||
| # A lock to prevent us from sending two messages at once: | |||
| self._lock = threading.Lock() | |||
| self._send_lock = Lock() | |||
| def _connect(self): | |||
| """Connect to our IRC server.""" | |||
| @@ -78,8 +77,7 @@ class IRCConnection(object): | |||
| def _send(self, msg): | |||
| """Send data to the server.""" | |||
| # Ensure that we only send one message at a time with a blocking lock: | |||
| with self._lock: | |||
| with self._send_lock: | |||
| self._sock.sendall(msg + "\r\n") | |||
| self.logger.debug(msg) | |||
| @@ -20,10 +20,9 @@ | |||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| # SOFTWARE. | |||
| import logging | |||
| import re | |||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | |||
| from earwigbot.irc import IRCConnection, Data | |||
| __all__ = ["Frontend"] | |||
| @@ -41,13 +40,12 @@ class Frontend(IRCConnection): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.config = bot.config | |||
| self.logger = logging.getLogger("earwigbot.frontend") | |||
| self.logger = bot.logger.getLogger("frontend") | |||
| cf = config.irc["frontend"] | |||
| cf = bot.config.irc["frontend"] | |||
| base = super(Frontend, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| cf["realname"]) | |||
| self._connect() | |||
| def _process_message(self, line): | |||
| @@ -58,36 +56,35 @@ class Frontend(IRCConnection): | |||
| if line[1] == "JOIN": | |||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | |||
| data.chan = line[2] | |||
| # Check for 'join' hooks in our commands: | |||
| command_manager.check("join", data) | |||
| data.parse_args() | |||
| self.bot.commands.check("join", data) | |||
| elif line[1] == "PRIVMSG": | |||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | |||
| data.msg = " ".join(line[3:])[1:] | |||
| data.chan = line[2] | |||
| data.parse_args() | |||
| if data.chan == self.config.irc["frontend"]["nick"]: | |||
| if data.chan == self.bot.config.irc["frontend"]["nick"]: | |||
| # This is a privmsg to us, so set 'chan' as the nick of the | |||
| # sender, then check for private-only command hooks: | |||
| data.chan = data.nick | |||
| command_manager.check("msg_private", data) | |||
| self.bot.commands.check("msg_private", data) | |||
| else: | |||
| # Check for public-only command hooks: | |||
| command_manager.check("msg_public", data) | |||
| self.bot.commands.check("msg_public", data) | |||
| # Check for command hooks that apply to all messages: | |||
| command_manager.check("msg", data) | |||
| self.bot.commands.check("msg", data) | |||
| # If we are pinged, pong back: | |||
| elif line[0] == "PING": | |||
| elif line[0] == "PING": # If we are pinged, pong back | |||
| self.pong(line[1]) | |||
| # On successful connection to the server: | |||
| elif line[1] == "376": | |||
| elif line[1] == "376": # On successful connection to the server | |||
| # If we're supposed to auth to NickServ, do that: | |||
| try: | |||
| username = self.config.irc["frontend"]["nickservUsername"] | |||
| password = self.config.irc["frontend"]["nickservPassword"] | |||
| username = self.bot.config.irc["frontend"]["nickservUsername"] | |||
| password = self.bot.config.irc["frontend"]["nickservPassword"] | |||
| except KeyError: | |||
| pass | |||
| else: | |||
| @@ -95,5 +92,5 @@ class Frontend(IRCConnection): | |||
| self.say("NickServ", msg) | |||
| # Join all of our startup channels: | |||
| for chan in self.config.irc["frontend"]["channels"]: | |||
| for chan in self.bot.config.irc["frontend"]["channels"]: | |||
| self.join(chan) | |||
| @@ -21,9 +21,8 @@ | |||
| # SOFTWARE. | |||
| import imp | |||
| import logging | |||
| from earwigbot.irc import IRCConnection, RC, BrokenSocketException | |||
| from earwigbot.irc import IRCConnection, RC | |||
| __all__ = ["Watcher"] | |||
| @@ -40,14 +39,12 @@ class Watcher(IRCConnection): | |||
| def __init__(self, bot): | |||
| self.bot = bot | |||
| self.config = bot.config | |||
| self.frontend = bot.frontend | |||
| self.logger = logging.getLogger("earwigbot.watcher") | |||
| self.logger = bot.logger.getLogger("watcher") | |||
| cf = config.irc["watcher"] | |||
| cf = bot.config.irc["watcher"] | |||
| base = super(Watcher, self) | |||
| base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], | |||
| cf["realname"], self.logger) | |||
| cf["realname"]) | |||
| self._prepare_process_hook() | |||
| self._connect() | |||
| @@ -60,7 +57,7 @@ class Watcher(IRCConnection): | |||
| # Ignore messages originating from channels not in our list, to | |||
| # prevent someone PMing us false data: | |||
| if chan not in self.config.irc["watcher"]["channels"]: | |||
| if chan not in self.bot.config.irc["watcher"]["channels"]: | |||
| return | |||
| msg = " ".join(line[3:])[1:] | |||
| @@ -74,7 +71,7 @@ class Watcher(IRCConnection): | |||
| # When we've finished starting up, join all watcher channels: | |||
| elif line[1] == "376": | |||
| for chan in self.config.irc["watcher"]["channels"]: | |||
| for chan in self.bot.config.irc["watcher"]["channels"]: | |||
| self.join(chan) | |||
| def _prepare_process_hook(self): | |||
| @@ -86,14 +83,15 @@ class Watcher(IRCConnection): | |||
| # Set a default RC process hook that does nothing: | |||
| self._process_hook = lambda rc: () | |||
| try: | |||
| rules = self.config.data["rules"] | |||
| rules = self.bot.config.data["rules"] | |||
| except KeyError: | |||
| return | |||
| module = imp.new_module("_rc_event_processing_rules") | |||
| path = self.bot.config.path | |||
| try: | |||
| exec compile(rules, self.config.path, "exec") in module.__dict__ | |||
| exec compile(rules, path, "exec") in module.__dict__ | |||
| except Exception: | |||
| e = "Could not compile config file's RC event rules" | |||
| e = "Could not compile config file's RC event rules:" | |||
| self.logger.exception(e) | |||
| return | |||
| self._process_hook_module = module | |||
| @@ -113,7 +111,9 @@ class Watcher(IRCConnection): | |||
| our config. | |||
| """ | |||
| chans = self._process_hook(rc) | |||
| if chans and self.frontend: | |||
| pretty = rc.prettify() | |||
| for chan in chans: | |||
| self.frontend.say(chan, pretty) | |||
| with self.bot.component_lock: | |||
| frontend = self.bot.frontend | |||
| if chans and frontend and not frontend.is_stopped(): | |||
| pretty = rc.prettify() | |||
| for chan in chans: | |||
| frontend.say(chan, pretty) | |||