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