Browse Source

More cleanup for IRC stuff

tags/v0.1^2
Ben Kurtovic 12 years ago
parent
commit
d901a252bb
6 changed files with 125 additions and 111 deletions
  1. +38
    -31
      earwigbot/bot.py
  2. +40
    -37
      earwigbot/commands/__init__.py
  3. +11
    -2
      earwigbot/commands/restart.py
  4. +4
    -6
      earwigbot/irc/connection.py
  5. +16
    -19
      earwigbot/irc/frontend.py
  6. +16
    -16
      earwigbot/irc/watcher.py

+ 38
- 31
earwigbot/bot.py View File

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

+ 40
- 37
earwigbot/commands/__init__.py View File

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

+ 11
- 2
earwigbot/commands/restart.py View File

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

+ 4
- 6
earwigbot/irc/connection.py View File

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




+ 16
- 19
earwigbot/irc/frontend.py View File

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

+ 16
- 16
earwigbot/irc/watcher.py View File

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

Loading…
Cancel
Save