@@ -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) |