| @@ -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 * | |||
| @@ -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 | |||
| 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 <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: | |||
| _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() | |||
| @@ -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.""" | |||
| @@ -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): | |||
| @@ -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 | |||
| @@ -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): | |||
| @@ -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) | |||
| @@ -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)) | |||
| @@ -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.""" | |||
| @@ -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): | |||
| @@ -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: | |||
| @@ -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.""" | |||
| @@ -22,7 +22,7 @@ | |||
| import random | |||
| from earwigbot.classes import BaseCommand | |||
| from earwigbot.commands import BaseCommand | |||
| class Command(BaseCommand): | |||
| """Praise people!""" | |||
| @@ -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.""" | |||
| @@ -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.""" | |||
| @@ -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.""" | |||
| @@ -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): | |||
| @@ -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.""" | |||
| @@ -22,7 +22,7 @@ | |||
| import random | |||
| from earwigbot.classes import BaseCommand | |||
| from earwigbot.commands import BaseCommand | |||
| class Command(BaseCommand): | |||
| """Test the bot!""" | |||
| @@ -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 | |||
| @@ -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": | |||