| @@ -1,32 +1,41 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| class BaseCommand(object): | class BaseCommand(object): | ||||
| """A base class for commands on IRC.""" | |||||
| """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 = "base_command" | |||||
| # 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): | def __init__(self, connection): | ||||
| self.connection = connection | self.connection = connection | ||||
| def get_hooks(self): | |||||
| """Hooks are: 'msg', 'msg_private', 'msg_public', and 'join'. Return | |||||
| the hooks you want this command to be called on.""" | |||||
| return [] | |||||
| def check(self, data): | |||||
| """Returns whether this command should be called in response to 'data'. | |||||
| def get_help(self, command): | |||||
| """Return help information for the command, used by !help. return None | |||||
| for no help. If a given class handles multiple commands, the command | |||||
| variable can be used to return different help for each one.""" | |||||
| return None | |||||
| 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. | |||||
| def check(self, data): | |||||
| """Given a Data() object, return True if we should respond to this | |||||
| activity, or False if we should ignore it/it doesn't apply to us. Most | |||||
| commands return True if data.command == 'command_name', otherwise | |||||
| they return False.""" | |||||
| 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 | return False | ||||
| def process(self, data): | def process(self, data): | ||||
| """Handle an activity (usually a message) on IRC. At this point, thanks | |||||
| """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 command_handler, we | to self.check() which is called automatically by command_handler, we | ||||
| know this is something we should respond to, so (usually) a | know this is something we should respond to, so (usually) a | ||||
| 'if data.command != "command_name": return' is unnecessary.""" | |||||
| 'if data.command != "command_name": return' is unnecessary. | |||||
| """ | |||||
| pass | pass | ||||
| @@ -5,15 +5,23 @@ class BaseTask(object): | |||||
| task_name = None | task_name = None | ||||
| def __init__(self): | def __init__(self): | ||||
| """This is called once immediately after the task class is loaded by | |||||
| the task manager (in wiki.task_manager.load_class_from_file()).""" | |||||
| """Constructor for new tasks. | |||||
| This is called once immediately after the task class is loaded by | |||||
| the task manager (in tasks._load_class_from_file()). | |||||
| """ | |||||
| pass | pass | ||||
| def run(self, **kwargs): | def run(self, **kwargs): | ||||
| """This is called directly by task_manager.start_task() and is the main | |||||
| way to make a task do stuff. kwargs will be any keyword arguments | |||||
| passed to start_task(), which are (of course) optional. The same task | |||||
| instance is preserved between runs, so you can theoretically store data | |||||
| in self (e.g. start_task('mytask', action='store', data='foo')) and | |||||
| then use it later (e.g. start_task('mytask', action='save')).""" | |||||
| """Main entry point to run a given task. | |||||
| This is called directly by tasks.start() and is the main way to make a | |||||
| task do stuff. kwargs will be any keyword arguments passed to start() | |||||
| which are entirely optional. | |||||
| The same task instance is preserved between runs, so you can | |||||
| theoretically store data in self (e.g. | |||||
| start('mytask', action='store', data='foo')) and then use it later | |||||
| (e.g. start('mytask', action='save')). | |||||
| """ | |||||
| pass | pass | ||||
| @@ -19,7 +19,7 @@ class Connection(object): | |||||
| self.ident = ident | self.ident = ident | ||||
| self.realname = realname | self.realname = realname | ||||
| # A lock to prevent us from sending two messages at once. | |||||
| # A lock to prevent us from sending two messages at once: | |||||
| self.lock = threading.Lock() | self.lock = threading.Lock() | ||||
| def connect(self): | def connect(self): | ||||
| @@ -58,8 +58,7 @@ class Connection(object): | |||||
| self.send(message) | self.send(message) | ||||
| def reply(self, data, msg): | def reply(self, data, msg): | ||||
| """Send a private message as a reply to a user on the server. `data` is | |||||
| a Data object (or anything with chan and nick attributes).""" | |||||
| """Send a private message as a reply to a user on the server.""" | |||||
| message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg)) | message = "".join((chr(2), data.nick, chr(0x0f), ": ", msg)) | ||||
| self.say(data.chan, message) | self.say(data.chan, message) | ||||
| @@ -13,11 +13,7 @@ class Data(object): | |||||
| def __init__(self, line): | def __init__(self, line): | ||||
| self.line = line | self.line = line | ||||
| self.chan = str() | |||||
| self.nick = str() | |||||
| self.ident = str() | |||||
| self.host = str() | |||||
| self.msg = str() | |||||
| self.chan = self.nick = self.ident = self.host = self.msg = "" | |||||
| def parse_args(self): | def parse_args(self): | ||||
| """Parse command args from self.msg into self.command and self.args.""" | """Parse command args from self.msg into self.command and self.args.""" | ||||
| @@ -1,16 +1,23 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| # A module to manage IRC commands. | |||||
| """ | |||||
| 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. | |||||
| """ | |||||
| import os | import os | ||||
| import traceback | import traceback | ||||
| __all__ = ["load", "get_all", "check"] | __all__ = ["load", "get_all", "check"] | ||||
| _commands = [] | |||||
| # 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 = {} | |||||
| def _process_module(connection, module): | |||||
| """go through all objects in a module and add valid command classes to the commands variable""" | |||||
| def _load_class_from_file(connection, module): | |||||
| """Add.""" | |||||
| global commands | global commands | ||||
| objects = dir(module) | objects = dir(module) | ||||
| @@ -30,9 +37,9 @@ def _process_module(connection, module): | |||||
| continue | continue | ||||
| def load(connection): | def load(connection): | ||||
| """load all valid command classes from irc/commmands/ into the commands variable""" | |||||
| files = os.listdir(os.path.join("irc", "commands")) # get all files in irc/commands/ | |||||
| files.sort() # alphabetically sort list of files | |||||
| """Load all valid commands into the _commands global variable.""" | |||||
| files = os.listdir(os.path.join("bot", "commands")) | |||||
| files.sort() | |||||
| for f in files: | for f in files: | ||||
| if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" | if f.startswith("_") or not f.endswith(".py"): # ignore non-python files or files beginning with "_" | ||||
| @@ -50,12 +57,11 @@ def load(connection): | |||||
| print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) | print "Found %s command classes: %s." % (len(commands), ', '.join(pretty_cmnds)) | ||||
| def get_all(): | def get_all(): | ||||
| """Return our list of all commands.""" | |||||
| """Return our dict of all loaded commands.""" | |||||
| return _commands | return _commands | ||||
| def check(hook, data): | def check(hook, data): | ||||
| """Given an event on IRC, check if there's anything we can respond to by | |||||
| calling each command class""" | |||||
| """Given an event on IRC, check if there's anything we can respond to.""" | |||||
| # parse command arguments into data.command and data.args | # parse command arguments into data.command and data.args | ||||
| data.parse_args() | data.parse_args() | ||||
| @@ -1,54 +1,44 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| # Generates help information. | |||||
| from classes import BaseCommand, Data | |||||
| import commands | |||||
| from irc.classes import BaseCommand, Data | |||||
| from irc import command_handler | |||||
| class Help(BaseCommand): | |||||
| def get_hooks(self): | |||||
| return ["msg"] | |||||
| def get_help(self, command): | |||||
| return "Generates help information." | |||||
| def check(self, data): | |||||
| if data.is_command and data.command == "help": | |||||
| return True | |||||
| return False | |||||
| class Command(BaseCommand): | |||||
| """Generates help information.""" | |||||
| name = "help" | |||||
| def process(self, data): | def process(self, data): | ||||
| self.cmnds = commands.get_all().keys() | |||||
| if not data.args: | if not data.args: | ||||
| self.do_general_help(data) | |||||
| self.do_main_help(data) | |||||
| else: | else: | ||||
| if data.args[0] == "list": | |||||
| self.do_list_help(data) | |||||
| else: | |||||
| self.do_command_help(data) | |||||
| def do_general_help(self, data): | |||||
| self.connection.reply(data, "I am a bot! You can get help for any command with '!help <command>', or a list of all loaded modules with '!help list'.") | |||||
| self.do_command_help(data) | |||||
| def do_list_help(self, data): | |||||
| commands = command_handler.get_commands() | |||||
| cmnds = map(lambda c: c.__class__.__name__, commands) | |||||
| pretty_cmnds = ', '.join(cmnds) | |||||
| self.connection.reply(data, "%s command classes loaded: %s." % (len(cmnds), pretty_cmnds)) | |||||
| def do_main_help(self, data): | |||||
| """Give the user a general help message with a list of all commands.""" | |||||
| msg = "I am a bot! I have {0} commands loaded: {1}. You can get help for any command with '!help <command>'." | |||||
| msg.format(len(self.cmnds), ', '.join(self.cmnds)) | |||||
| self.connection.reply(data, msg) | |||||
| def do_command_help(self, data): | def do_command_help(self, data): | ||||
| """Give the user help for a specific command.""" | |||||
| command = data.args[0] | command = data.args[0] | ||||
| commands = command_handler.get_commands() | |||||
| dummy = Data() # dummy message to test which command classes pick up this command | |||||
| dummy.command = command.lower() # lowercase command name | |||||
| # Create a dummy message to test which commands pick up the user's | |||||
| # input: | |||||
| dummy = Data() | |||||
| dummy.command = command.lower() | |||||
| dummy.is_command = True | dummy.is_command = True | ||||
| for cmnd in commands: | |||||
| for cmnd in self.cmnds: | |||||
| if cmnd.check(dummy): | if cmnd.check(dummy): | ||||
| help = cmnd.get_help(command) | |||||
| doc = cmnd.__doc__ | |||||
| if doc: | |||||
| msg = "info for command \x0303{0}\x0301: \"{1}\"" | |||||
| msg.format(command, doc) | |||||
| self.connection.reply(data, msg) | |||||
| return | |||||
| break | break | ||||
| try: | |||||
| self.connection.reply(data, "info for command \x0303%s\x0301: \"%s\"" % (command, help)) | |||||
| except UnboundLocalError: | |||||
| self.connection.reply(data, "sorry, no help for \x0303%s\x0301." % command) | |||||
| msg = "sorry, no help for \x0303{0}\x0301.".format(command) | |||||
| self.connection.reply(data, msg) | |||||
| @@ -1,22 +1,12 @@ | |||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||
| # A very simple command to test the bot. | |||||
| import random | import random | ||||
| from irc.classes import BaseCommand | |||||
| class Test(BaseCommand): | |||||
| def get_hooks(self): | |||||
| return ["msg"] | |||||
| def get_help(self, command): | |||||
| return "Test the bot!" | |||||
| from classes import BaseCommand | |||||
| def check(self, data): | |||||
| if data.is_command and data.command == "test": | |||||
| return True | |||||
| return False | |||||
| class Command(BaseCommand): | |||||
| """Test the bot!""" | |||||
| name = "test" | |||||
| def process(self, data): | def process(self, data): | ||||
| hey = random.randint(0, 1) | hey = random.randint(0, 1) | ||||
| @@ -16,9 +16,9 @@ import config | |||||
| __all__ = ["load", "schedule", "start"] | __all__ = ["load", "schedule", "start"] | ||||
| # store loaded tasks as a dict where the key is the task name and the value is | |||||
| # an instance of the task class (wiki.tasks.task_file.Task()) | |||||
| _tasks = dict() | |||||
| # Store loaded tasks as a dict where the key is the task name and the value is | |||||
| # an instance of the task class: | |||||
| _tasks = {} | |||||
| def _load_class_from_file(f): | def _load_class_from_file(f): | ||||
| """Look in a given file for the task class.""" | """Look in a given file for the task class.""" | ||||