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