diff --git a/earwigbot/classes/__init__.py b/earwigbot/classes/__init__.py index 3373679..96079e1 100644 --- a/earwigbot/classes/__init__.py +++ b/earwigbot/classes/__init__.py @@ -20,5 +20,4 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes.base_command import * from earwigbot.classes.base_task import * diff --git a/earwigbot/classes/base_command.py b/earwigbot/classes/base_command.py deleted file mode 100644 index 10ccbc8..0000000 --- a/earwigbot/classes/base_command.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2009-2012 by Ben Kurtovic -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import logging - -__all__ = ["BaseCommand"] - -class BaseCommand(object): - """A base class for commands on IRC. - - This docstring is reported to the user when they use !help . - """ - # This is the command's name, as reported to the user when they use !help: - name = None - - # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the - # default behavior; if you wish to override that, change the value in your - # command subclass: - hooks = ["msg"] - - def __init__(self, connection): - """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. - """ - self.connection = connection - logger_name = ".".join(("earwigbot", "commands", self.name)) - self.logger = logging.getLogger(logger_name) - self.logger.setLevel(logging.DEBUG) - - def check(self, data): - """Returns 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. - - Most commands return True if data.command == self.name, otherwise they - 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 - - 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. - """ - pass diff --git a/earwigbot/commands/__init__.py b/earwigbot/commands/__init__.py index fda7072..8af0ce8 100644 --- a/earwigbot/commands/__init__.py +++ b/earwigbot/commands/__init__.py @@ -24,88 +24,140 @@ EarwigBot's IRC Command Manager This package provides the IRC "commands" used by the bot's front-end component. -In __init__, you can find some functions used to load and run these commands. +This module contains the BaseCommand class (import with +`from earwigbot.commands import BaseCommand`) and an internal _CommandManager +class. This can be accessed through the singleton `command_manager`. """ import logging import os import sys -from earwigbot.classes import BaseCommand from earwigbot.config import config -__all__ = ["load", "get_all", "check"] +__all__ = ["BaseCommand", "command_manager"] -# Base directory when searching for commands: -base_dir = os.path.dirname(os.path.abspath(__file__)) +class BaseCommand(object): + """A base class for commands on IRC. -# Store commands in a dict, where the key is the command's name and the value -# is an instance of the command's class: -_commands = {} - -# Logger for this module: -logger = logging.getLogger("earwigbot.tasks") - -def _load_command(connection, filename): - """Try to load a specific command from a module, identified by file name. - - Given a Connection object and a filename, we'll first try to import it, - and if that works, make an instance of the 'Command' class inside (assuming - it is an instance of BaseCommand), add it to _commands, and report the - addition to the user. Any problems along the way will either be ignored or - reported. - """ - global _commands - - # Strip .py from the end of the filename and join with our package name: - name = ".".join(("commands", filename[:-3])) - try: - __import__(name) - except: - logger.exception("Couldn't load file {0}".format(filename)) - return - - command = sys.modules[name].Command(connection) - if not isinstance(command, BaseCommand): - return - - _commands[command.name] = command - logger.debug("Added command {0}".format(command.name)) - -def load(connection): - """Load all valid commands into the _commands global variable. - - `connection` is a Connection object that is given to each command's - constructor. + This docstring is reported to the user when they use !help . """ - files = os.listdir(base_dir) - files.sort() + # This is the command's name, as reported to the user when they use !help: + name = None + + # Hooks are "msg", "msg_private", "msg_public", and "join". "msg" is the + # default behavior; if you wish to override that, change the value in your + # command subclass: + hooks = ["msg"] + + def __init__(self, connection): + """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. + """ + self.connection = connection + logger_name = ".".join(("earwigbot", "commands", self.name)) + self.logger = logging.getLogger(logger_name) + self.logger.setLevel(logging.DEBUG) + + def check(self, data): + """Returns 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. + + Most commands return True if data.command == self.name, otherwise they + 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 + + 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. + """ + pass + + +class _CommandManager(object): + def __init__(self): + self.logger = logging.getLogger("earwigbot.tasks") + self._base_dir = os.path.dirname(os.path.abspath(__file__)) + self._connection = None + self._commands = {} + + def _load_command(self, filename): + """Load a specific command from a module, identified by filename. + + Given a Connection object and a filename, we'll first try to import + it, and if that works, make an instance of the 'Command' class inside + (assuming it is an instance of BaseCommand), add it to self._commands, + and log the addition. Any problems along the way will either be + ignored or logged. + """ + # Strip .py from the filename's end and join with our package name: + name = ".".join(("commands", filename[:-3])) + try: + __import__(name) + except: + self.logger.exception("Couldn't load file {0}".format(filename)) + return - for filename in files: - if filename.startswith("_") or not filename.endswith(".py"): - continue try: - _load_command(connection, filename) + command = sys.modules[name].Command(self._connection) except AttributeError: - pass # The file is doesn't contain a command, so just move on - - msg = "Found {0} commands: {1}" - logger.info(msg.format(len(_commands), ", ".join(_commands.keys()))) - -def get_all(): - """Return our dict of all loaded commands.""" - return _commands - -def check(hook, data): - """Given an event on IRC, check if there's anything we can respond to.""" - # Parse command arguments into data.command and data.args: - data.parse_args() - - for command in _commands.values(): - if hook in command.hooks: - if command.check(data): - try: - command.process(data) - except: - logger.exception("Error executing command '{0}'".format(data.command)) - break + return # No command in this module + if not isinstance(command, BaseCommand): + return + + self._commands[command.name] = command + self.logger.debug("Added command {0}".format(command.name)) + + def load(self, connection): + """Load all valid commands into self._commands. + + `connection` is a Connection object that is given to each command's + constructor. + """ + self._connection = connection + + files = os.listdir(self._base_dir) + files.sort() + for filename in files: + if filename.startswith("_") or not filename.endswith(".py"): + continue + self._load_command(filename) + + msg = "Found {0} commands: {1}" + commands = ", ".join(self._commands.keys()) + self.logger.info(msg.format(len(self._commands), commands)) + + def get_all(self): + """Return our dict of all loaded commands.""" + return self._commands + + 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 + + +command_manager = _CommandManager() diff --git a/earwigbot/commands/afc_report.py b/earwigbot/commands/afc_report.py index fe3cd8d..3c3bc24 100644 --- a/earwigbot/commands/afc_report.py +++ b/earwigbot/commands/afc_report.py @@ -22,9 +22,9 @@ import re -from earwigbot.classes import BaseCommand from earwigbot import tasks from earwigbot import wiki +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Get information about an AFC submission by name.""" diff --git a/earwigbot/commands/afc_status.py b/earwigbot/commands/afc_status.py index ab32259..a475caf 100644 --- a/earwigbot/commands/afc_status.py +++ b/earwigbot/commands/afc_status.py @@ -23,7 +23,7 @@ import re from earwigbot import wiki -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand from earwigbot.config import config class Command(BaseCommand): diff --git a/earwigbot/commands/calc.py b/earwigbot/commands/calc.py index 4b3d25b..f6d3177 100644 --- a/earwigbot/commands/calc.py +++ b/earwigbot/commands/calc.py @@ -23,7 +23,7 @@ import re import urllib -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp diff --git a/earwigbot/commands/chanops.py b/earwigbot/commands/chanops.py index 0a36966..dd59353 100644 --- a/earwigbot/commands/chanops.py +++ b/earwigbot/commands/chanops.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand from earwigbot.config import config class Command(BaseCommand): diff --git a/earwigbot/commands/crypt.py b/earwigbot/commands/crypt.py index c0fb432..e71e139 100644 --- a/earwigbot/commands/crypt.py +++ b/earwigbot/commands/crypt.py @@ -22,8 +22,8 @@ import hashlib -from earwigbot.classes import BaseCommand from earwigbot import blowfish +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Provides hash functions with !hash (!hash list for supported algorithms) diff --git a/earwigbot/commands/ctcp.py b/earwigbot/commands/ctcp.py index 5c0a352..80b56e2 100644 --- a/earwigbot/commands/ctcp.py +++ b/earwigbot/commands/ctcp.py @@ -23,8 +23,8 @@ import platform import time -import earwigbot -from earwigbot.classes import BaseCommand +from earwigbot import __version__ +from earwigbot.commands import BaseCommand from earwigbot.config import config class Command(BaseCommand): @@ -64,6 +64,6 @@ class Command(BaseCommand): elif command == "VERSION": default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" vers = config.irc.get("version", default) - vers = vers.replace("$1", earwigbot.__version__) + vers = vers.replace("$1", __version__) vers = vers.replace("$2", platform.python_version()) self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) diff --git a/earwigbot/commands/editcount.py b/earwigbot/commands/editcount.py index 7417ff9..9c58726 100644 --- a/earwigbot/commands/editcount.py +++ b/earwigbot/commands/editcount.py @@ -22,8 +22,8 @@ from urllib import quote_plus -from earwigbot.classes import BaseCommand from earwigbot import wiki +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Return a user's edit count.""" diff --git a/earwigbot/commands/git.py b/earwigbot/commands/git.py index e465e93..dfd9aba 100644 --- a/earwigbot/commands/git.py +++ b/earwigbot/commands/git.py @@ -24,7 +24,7 @@ import shlex import subprocess import re -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand from earwigbot.config import config class Command(BaseCommand): diff --git a/earwigbot/commands/help.py b/earwigbot/commands/help.py index f8129a0..5a6f9dd 100644 --- a/earwigbot/commands/help.py +++ b/earwigbot/commands/help.py @@ -22,8 +22,7 @@ import re -from earwigbot import commands -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand, command_manager from earwigbot.irc import Data class Command(BaseCommand): @@ -31,7 +30,7 @@ class Command(BaseCommand): name = "help" def process(self, data): - self.cmnds = commands.get_all() + self.cmnds = command_manager.get_all() if not data.args: self.do_main_help(data) else: diff --git a/earwigbot/commands/link.py b/earwigbot/commands/link.py index 24a4b7a..675096e 100644 --- a/earwigbot/commands/link.py +++ b/earwigbot/commands/link.py @@ -23,7 +23,7 @@ import re from urllib import quote -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Convert a Wikipedia page name into a URL.""" diff --git a/earwigbot/commands/praise.py b/earwigbot/commands/praise.py index e82247c..c9e3950 100644 --- a/earwigbot/commands/praise.py +++ b/earwigbot/commands/praise.py @@ -22,7 +22,7 @@ import random -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Praise people!""" diff --git a/earwigbot/commands/registration.py b/earwigbot/commands/registration.py index b21a6ac..55b762f 100644 --- a/earwigbot/commands/registration.py +++ b/earwigbot/commands/registration.py @@ -22,8 +22,8 @@ import time -from earwigbot.classes import BaseCommand from earwigbot import wiki +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Return when a user registered.""" diff --git a/earwigbot/commands/remind.py b/earwigbot/commands/remind.py index 73a4a1e..115cb4c 100644 --- a/earwigbot/commands/remind.py +++ b/earwigbot/commands/remind.py @@ -23,7 +23,7 @@ import threading import time -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Set a message to be repeated to you in a certain amount of time.""" diff --git a/earwigbot/commands/replag.py b/earwigbot/commands/replag.py index 8347dc7..32b664d 100644 --- a/earwigbot/commands/replag.py +++ b/earwigbot/commands/replag.py @@ -24,7 +24,7 @@ from os.path import expanduser import oursql -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Return the replag for a specific database on the Toolserver.""" diff --git a/earwigbot/commands/restart.py b/earwigbot/commands/restart.py index 0f47f49..527fc82 100644 --- a/earwigbot/commands/restart.py +++ b/earwigbot/commands/restart.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand from earwigbot.config import config class Command(BaseCommand): diff --git a/earwigbot/commands/rights.py b/earwigbot/commands/rights.py index 31c56db..1a9dd99 100644 --- a/earwigbot/commands/rights.py +++ b/earwigbot/commands/rights.py @@ -20,8 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -from earwigbot.classes import BaseCommand from earwigbot import wiki +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Retrieve a list of rights for a given username.""" diff --git a/earwigbot/commands/test.py b/earwigbot/commands/test.py index 0c94fa7..edc4567 100644 --- a/earwigbot/commands/test.py +++ b/earwigbot/commands/test.py @@ -22,7 +22,7 @@ import random -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand class Command(BaseCommand): """Test the bot!""" diff --git a/earwigbot/commands/threads.py b/earwigbot/commands/threads.py index a7c5a1b..5b5ce0f 100644 --- a/earwigbot/commands/threads.py +++ b/earwigbot/commands/threads.py @@ -24,7 +24,7 @@ import threading import re from earwigbot import tasks -from earwigbot.classes import BaseCommand +from earwigbot.commands import BaseCommand from earwigbot.config import config from earwigbot.irc import KwargParseException diff --git a/earwigbot/irc/frontend.py b/earwigbot/irc/frontend.py index 027c73f..2b7e7d1 100644 --- a/earwigbot/irc/frontend.py +++ b/earwigbot/irc/frontend.py @@ -23,7 +23,7 @@ import logging import re -from earwigbot import commands +from earwigbot.commands import command_manager from earwigbot.irc import IRCConnection, Data, BrokenSocketException from earwigbot.config import config @@ -47,7 +47,7 @@ class Frontend(IRCConnection): base = super(Frontend, self) base.__init__(cf["host"], cf["port"], cf["nick"], cf["ident"], cf["realname"], self.logger) - commands.load(self) + command_manager.load(self) self._connect() def _process_message(self, line): @@ -59,7 +59,7 @@ class Frontend(IRCConnection): data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] data.chan = line[2] # Check for 'join' hooks in our commands: - commands.check("join", data) + command_manager.check("join", data) elif line[1] == "PRIVMSG": data.nick, data.ident, data.host = self.sender_regex.findall(line[0])[0] @@ -70,13 +70,13 @@ class Frontend(IRCConnection): # 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 - commands.check("msg_private", data) + command_manager.check("msg_private", data) else: # Check for public-only command hooks: - commands.check("msg_public", data) + command_manager.check("msg_public", data) # Check for command hooks that apply to all messages: - commands.check("msg", data) + command_manager.check("msg", data) # If we are pinged, pong back: elif line[0] == "PING":