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