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