Ver código fonte

More cleanup for IRC stuff

tags/v0.1^2
Ben Kurtovic 12 anos atrás
pai
commit
d901a252bb
6 arquivos alterados com 125 adições e 111 exclusões
  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 Ver arquivo

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

+ 40
- 37
earwigbot/commands/__init__.py Ver arquivo

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

+ 11
- 2
earwigbot/commands/restart.py Ver arquivo

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

+ 4
- 6
earwigbot/irc/connection.py Ver arquivo

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



+ 16
- 19
earwigbot/irc/frontend.py Ver arquivo

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

+ 16
- 16
earwigbot/irc/watcher.py Ver arquivo

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

Carregando…
Cancelar
Salvar