| @@ -54,23 +54,13 @@ class Bot(object): | |||||
| self.frontend = None | self.frontend = None | ||||
| self.watcher = 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"): | if self.config.components.get("irc_frontend"): | ||||
| self.logger.info("Starting IRC frontend") | self.logger.info("Starting IRC frontend") | ||||
| self.frontend = Frontend(self) | self.frontend = Frontend(self) | ||||
| self.commands.load() | |||||
| threading.Thread(name=name, target=self.frontend.loop).start() | threading.Thread(name=name, target=self.frontend.loop).start() | ||||
| if self.config.components.get("irc_watcher"): | if self.config.components.get("irc_watcher"): | ||||
| @@ -78,17 +68,35 @@ class Bot(object): | |||||
| self.watcher = Watcher(self) | self.watcher = Watcher(self) | ||||
| threading.Thread(name=name, target=self.watcher.loop).start() | 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"): | if self.config.components.get("wiki_scheduler"): | ||||
| self.logger.info("Starting 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): | def _loop(self): | ||||
| while 1: | |||||
| with self._lock: | |||||
| while self._keep_looping: | |||||
| with self.component_lock: | |||||
| if self.frontend and self.frontend.is_stopped(): | 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(): | 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) | sleep(5) | ||||
| def run(self): | 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", "key") | ||||
| self.config.decrypt(config.wiki, "search", "credentials", "secret") | self.config.decrypt(config.wiki, "search", "credentials", "secret") | ||||
| self.config.decrypt(config.irc, "frontend", "nickservPassword") | 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() | 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() | self.config.load() | ||||
| #if self.config.components.get("irc_frontend"): | |||||
| # self.commands.load() | |||||
| self.commands.load() | |||||
| self._start_irc_components() | |||||
| def stop(self): | 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 | 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 imp | ||||
| import logging | |||||
| from os import listdir, path | from os import listdir, path | ||||
| from re import sub | from re import sub | ||||
| from threading import Lock | |||||
| __all__ = ["BaseCommand", "CommandManager"] | __all__ = ["BaseCommand", "CommandManager"] | ||||
| @@ -49,21 +49,24 @@ class BaseCommand(object): | |||||
| # command subclass: | # command subclass: | ||||
| hooks = ["msg"] | hooks = ["msg"] | ||||
| def __init__(self, connection): | |||||
| def __init__(self, bot): | |||||
| """Constructor for new commands. | """Constructor for new commands. | ||||
| This is called once when the command is loaded (from | 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): | 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 | 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. | 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 | return False. This is the default behavior of check(); you need only | ||||
| override it if you wish to change that. | 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): | def process(self, data): | ||||
| """Main entry point for doing a command. | """Main entry point for doing a command. | ||||
| Handle an activity (usually a message) on IRC. At this point, thanks | Handle an activity (usually a message) on IRC. At this point, thanks | ||||
| to self.check() which is called automatically by the command handler, | 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 | pass | ||||
| @@ -90,9 +92,9 @@ class BaseCommand(object): | |||||
| class CommandManager(object): | class CommandManager(object): | ||||
| def __init__(self, bot): | def __init__(self, bot): | ||||
| self.bot = 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._commands = {} | ||||
| self._command_access_lock = Lock() | |||||
| def _load_command(self, name, path): | def _load_command(self, name, path): | ||||
| """Load a specific command from a module, identified by name and path. | """Load a specific command from a module, identified by name and path. | ||||
| @@ -113,7 +115,7 @@ class CommandManager(object): | |||||
| f.close() | f.close() | ||||
| try: | try: | ||||
| command = module.Command(self.bot.frontend) | |||||
| command = module.Command(self.bot) | |||||
| 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): | ||||
| @@ -124,14 +126,16 @@ class CommandManager(object): | |||||
| def load(self): | def load(self): | ||||
| """Load (or reload) all valid commands into self._commands.""" | """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}" | msg = "Found {0} commands: {1}" | ||||
| commands = ", ".join(self._commands.keys()) | commands = ", ".join(self._commands.keys()) | ||||
| @@ -143,14 +147,13 @@ class CommandManager(object): | |||||
| def check(self, hook, data): | def check(self, hook, data): | ||||
| """Given an IRC event, check if there's anything we can respond to.""" | """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.""" | """Restart the bot. Only the owner can do this.""" | ||||
| name = "restart" | name = "restart" | ||||
| def check(self, data): | |||||
| commands = ["restart", "reload"] | |||||
| return data.is_command and data.command in commands | |||||
| def process(self, data): | def process(self, data): | ||||
| if data.host not in config.irc["permissions"]["owners"]: | if data.host not in config.irc["permissions"]["owners"]: | ||||
| msg = "you must be a bot owner to use this command." | msg = "you must be a bot owner to use this command." | ||||
| self.connection.reply(data, msg) | self.connection.reply(data, msg) | ||||
| return | 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. | # SOFTWARE. | ||||
| import socket | import socket | ||||
| import threading | |||||
| from threading import Lock | |||||
| from time import sleep | from time import sleep | ||||
| __all__ = ["BrokenSocketException", "IRCConnection"] | __all__ = ["BrokenSocketException", "IRCConnection"] | ||||
| @@ -36,17 +36,16 @@ class BrokenSocketException(Exception): | |||||
| class IRCConnection(object): | class IRCConnection(object): | ||||
| """A class to interface with IRC.""" | """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.host = host | ||||
| self.port = port | self.port = port | ||||
| self.nick = nick | self.nick = nick | ||||
| self.ident = ident | self.ident = ident | ||||
| self.realname = realname | self.realname = realname | ||||
| self.logger = logger | |||||
| self._is_running = False | self._is_running = False | ||||
| # A lock to prevent us from sending two messages at once: | # A lock to prevent us from sending two messages at once: | ||||
| self._lock = threading.Lock() | |||||
| self._send_lock = Lock() | |||||
| def _connect(self): | def _connect(self): | ||||
| """Connect to our IRC server.""" | """Connect to our IRC server.""" | ||||
| @@ -78,8 +77,7 @@ class IRCConnection(object): | |||||
| def _send(self, msg): | def _send(self, msg): | ||||
| """Send data to the server.""" | """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._sock.sendall(msg + "\r\n") | ||||
| self.logger.debug(msg) | self.logger.debug(msg) | ||||
| @@ -20,10 +20,9 @@ | |||||
| # 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. | ||||
| import logging | |||||
| import re | import re | ||||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | |||||
| from earwigbot.irc import IRCConnection, Data | |||||
| __all__ = ["Frontend"] | __all__ = ["Frontend"] | ||||
| @@ -41,13 +40,12 @@ class Frontend(IRCConnection): | |||||
| def __init__(self, bot): | def __init__(self, bot): | ||||
| self.bot = 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 = 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._connect() | self._connect() | ||||
| def _process_message(self, line): | def _process_message(self, line): | ||||
| @@ -58,36 +56,35 @@ class Frontend(IRCConnection): | |||||
| if line[1] == "JOIN": | if line[1] == "JOIN": | ||||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | ||||
| data.chan = line[2] | 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": | elif line[1] == "PRIVMSG": | ||||
| data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] | ||||
| data.msg = " ".join(line[3:])[1:] | data.msg = " ".join(line[3:])[1:] | ||||
| data.chan = line[2] | 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 | # This is a privmsg to us, so set 'chan' as the nick of the | ||||
| # sender, then check for private-only command hooks: | # sender, then check for private-only command hooks: | ||||
| data.chan = data.nick | data.chan = data.nick | ||||
| command_manager.check("msg_private", data) | |||||
| self.bot.commands.check("msg_private", data) | |||||
| else: | else: | ||||
| # Check for public-only command hooks: | # 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: | # 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]) | 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: | # If we're supposed to auth to NickServ, do that: | ||||
| try: | 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: | except KeyError: | ||||
| pass | pass | ||||
| else: | else: | ||||
| @@ -95,5 +92,5 @@ class Frontend(IRCConnection): | |||||
| self.say("NickServ", msg) | self.say("NickServ", msg) | ||||
| # Join all of our startup channels: | # 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) | self.join(chan) | ||||
| @@ -21,9 +21,8 @@ | |||||
| # SOFTWARE. | # SOFTWARE. | ||||
| import imp | import imp | ||||
| import logging | |||||
| from earwigbot.irc import IRCConnection, RC, BrokenSocketException | |||||
| from earwigbot.irc import IRCConnection, RC | |||||
| __all__ = ["Watcher"] | __all__ = ["Watcher"] | ||||
| @@ -40,14 +39,12 @@ class Watcher(IRCConnection): | |||||
| def __init__(self, bot): | def __init__(self, bot): | ||||
| self.bot = 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 = 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._prepare_process_hook() | self._prepare_process_hook() | ||||
| self._connect() | self._connect() | ||||
| @@ -60,7 +57,7 @@ class Watcher(IRCConnection): | |||||
| # Ignore messages originating from channels not in our list, to | # Ignore messages originating from channels not in our list, to | ||||
| # prevent someone PMing us false data: | # 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 | return | ||||
| msg = " ".join(line[3:])[1:] | msg = " ".join(line[3:])[1:] | ||||
| @@ -74,7 +71,7 @@ class Watcher(IRCConnection): | |||||
| # When we've finished starting up, join all watcher channels: | # When we've finished starting up, join all watcher channels: | ||||
| elif line[1] == "376": | elif line[1] == "376": | ||||
| for chan in self.config.irc["watcher"]["channels"]: | |||||
| for chan in self.bot.config.irc["watcher"]["channels"]: | |||||
| self.join(chan) | self.join(chan) | ||||
| def _prepare_process_hook(self): | def _prepare_process_hook(self): | ||||
| @@ -86,14 +83,15 @@ class Watcher(IRCConnection): | |||||
| # Set a default RC process hook that does nothing: | # Set a default RC process hook that does nothing: | ||||
| self._process_hook = lambda rc: () | self._process_hook = lambda rc: () | ||||
| try: | try: | ||||
| rules = self.config.data["rules"] | |||||
| rules = self.bot.config.data["rules"] | |||||
| except KeyError: | except KeyError: | ||||
| return | return | ||||
| module = imp.new_module("_rc_event_processing_rules") | module = imp.new_module("_rc_event_processing_rules") | ||||
| path = self.bot.config.path | |||||
| try: | try: | ||||
| exec compile(rules, self.config.path, "exec") in module.__dict__ | |||||
| exec compile(rules, path, "exec") in module.__dict__ | |||||
| except Exception: | 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) | self.logger.exception(e) | ||||
| return | return | ||||
| self._process_hook_module = module | self._process_hook_module = module | ||||
| @@ -113,7 +111,9 @@ class Watcher(IRCConnection): | |||||
| our config. | our config. | ||||
| """ | """ | ||||
| chans = self._process_hook(rc) | 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) | |||||