| @@ -20,5 +20,4 @@ | |||||
| # 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. | ||||
| from earwigbot.classes.base_command import * | |||||
| from earwigbot.classes.base_task import * | from earwigbot.classes.base_task import * | ||||
| @@ -1,75 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # | |||||
| # Copyright (C) 2009-2012 by Ben Kurtovic <ben.kurtovic@verizon.net> | |||||
| # | |||||
| # 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 <command>. | |||||
| """ | |||||
| # 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 | |||||
| @@ -24,88 +24,140 @@ | |||||
| EarwigBot's IRC Command Manager | EarwigBot's IRC Command Manager | ||||
| This package provides the IRC "commands" used by the bot's front-end component. | 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 logging | ||||
| import os | import os | ||||
| import sys | import sys | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.config import config | 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 <command>. | |||||
| """ | """ | ||||
| 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: | try: | ||||
| _load_command(connection, filename) | |||||
| command = sys.modules[name].Command(self._connection) | |||||
| except AttributeError: | 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() | |||||
| @@ -22,9 +22,9 @@ | |||||
| import re | import re | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import tasks | from earwigbot import tasks | ||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Get information about an AFC submission by name.""" | """Get information about an AFC submission by name.""" | ||||
| @@ -23,7 +23,7 @@ | |||||
| import re | import re | ||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -23,7 +23,7 @@ | |||||
| import re | import re | ||||
| import urllib | import urllib | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp | """A somewhat advanced calculator: see http://futureboy.us/fsp/frink.fsp | ||||
| @@ -20,7 +20,7 @@ | |||||
| # 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. | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -22,8 +22,8 @@ | |||||
| import hashlib | import hashlib | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import blowfish | from earwigbot import blowfish | ||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Provides hash functions with !hash (!hash list for supported algorithms) | """Provides hash functions with !hash (!hash list for supported algorithms) | ||||
| @@ -23,8 +23,8 @@ | |||||
| import platform | import platform | ||||
| import time | import time | ||||
| import earwigbot | |||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import __version__ | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -64,6 +64,6 @@ class Command(BaseCommand): | |||||
| elif command == "VERSION": | elif command == "VERSION": | ||||
| default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | default = "EarwigBot - $1 - Python/$2 https://github.com/earwig/earwigbot" | ||||
| vers = config.irc.get("version", default) | vers = config.irc.get("version", default) | ||||
| vers = vers.replace("$1", earwigbot.__version__) | |||||
| vers = vers.replace("$1", __version__) | |||||
| vers = vers.replace("$2", platform.python_version()) | vers = vers.replace("$2", platform.python_version()) | ||||
| self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) | self.connection.notice(target, "\x01VERSION {0}\x01".format(vers)) | ||||
| @@ -22,8 +22,8 @@ | |||||
| from urllib import quote_plus | from urllib import quote_plus | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Return a user's edit count.""" | """Return a user's edit count.""" | ||||
| @@ -24,7 +24,7 @@ import shlex | |||||
| import subprocess | import subprocess | ||||
| import re | import re | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -22,8 +22,7 @@ | |||||
| import re | import re | ||||
| from earwigbot import commands | |||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand, command_manager | |||||
| from earwigbot.irc import Data | from earwigbot.irc import Data | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -31,7 +30,7 @@ class Command(BaseCommand): | |||||
| name = "help" | name = "help" | ||||
| def process(self, data): | def process(self, data): | ||||
| self.cmnds = commands.get_all() | |||||
| self.cmnds = command_manager.get_all() | |||||
| if not data.args: | if not data.args: | ||||
| self.do_main_help(data) | self.do_main_help(data) | ||||
| else: | else: | ||||
| @@ -23,7 +23,7 @@ | |||||
| import re | import re | ||||
| from urllib import quote | from urllib import quote | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Convert a Wikipedia page name into a URL.""" | """Convert a Wikipedia page name into a URL.""" | ||||
| @@ -22,7 +22,7 @@ | |||||
| import random | import random | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Praise people!""" | """Praise people!""" | ||||
| @@ -22,8 +22,8 @@ | |||||
| import time | import time | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Return when a user registered.""" | """Return when a user registered.""" | ||||
| @@ -23,7 +23,7 @@ | |||||
| import threading | import threading | ||||
| import time | import time | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Set a message to be repeated to you in a certain amount of time.""" | """Set a message to be repeated to you in a certain amount of time.""" | ||||
| @@ -24,7 +24,7 @@ from os.path import expanduser | |||||
| import oursql | import oursql | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Return the replag for a specific database on the Toolserver.""" | """Return the replag for a specific database on the Toolserver.""" | ||||
| @@ -20,7 +20,7 @@ | |||||
| # 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. | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| @@ -20,8 +20,8 @@ | |||||
| # 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. | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot import wiki | from earwigbot import wiki | ||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Retrieve a list of rights for a given username.""" | """Retrieve a list of rights for a given username.""" | ||||
| @@ -22,7 +22,7 @@ | |||||
| import random | import random | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| class Command(BaseCommand): | class Command(BaseCommand): | ||||
| """Test the bot!""" | """Test the bot!""" | ||||
| @@ -24,7 +24,7 @@ import threading | |||||
| import re | import re | ||||
| from earwigbot import tasks | from earwigbot import tasks | ||||
| from earwigbot.classes import BaseCommand | |||||
| from earwigbot.commands import BaseCommand | |||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| from earwigbot.irc import KwargParseException | from earwigbot.irc import KwargParseException | ||||
| @@ -23,7 +23,7 @@ | |||||
| import logging | import logging | ||||
| import re | import re | ||||
| from earwigbot import commands | |||||
| from earwigbot.commands import command_manager | |||||
| from earwigbot.irc import IRCConnection, Data, BrokenSocketException | from earwigbot.irc import IRCConnection, Data, BrokenSocketException | ||||
| from earwigbot.config import config | from earwigbot.config import config | ||||
| @@ -47,7 +47,7 @@ class Frontend(IRCConnection): | |||||
| 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.logger) | ||||
| commands.load(self) | |||||
| command_manager.load(self) | |||||
| self._connect() | self._connect() | ||||
| def _process_message(self, line): | 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.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: | # Check for 'join' hooks in our commands: | ||||
| commands.check("join", data) | |||||
| command_manager.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] | ||||
| @@ -70,13 +70,13 @@ class Frontend(IRCConnection): | |||||
| # 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 | ||||
| commands.check("msg_private", data) | |||||
| command_manager.check("msg_private", data) | |||||
| else: | else: | ||||
| # Check for public-only command hooks: | # 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: | # Check for command hooks that apply to all messages: | ||||
| commands.check("msg", data) | |||||
| command_manager.check("msg", data) | |||||
| # If we are pinged, pong back: | # If we are pinged, pong back: | ||||
| elif line[0] == "PING": | elif line[0] == "PING": | ||||