From 2f88dc28c6dc70dfada4afb5042531ecab0e062f Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 22 Nov 2017 22:09:25 -0800 Subject: [PATCH] Implement command help system --- docs/api/modules/modinfo.rst | 31 ++++++++++++ docs/api/modules/pubsubclient.rst | 1 - pyircbot/modulebase.py | 3 -- pyircbot/modules/Calc.py | 2 + pyircbot/modules/ModInfo.py | 81 +++++++++++++++++++++++++++++++ pyircbot/modules/Seen.py | 56 ++++++++++----------- pyircbot/pyircbot.py | 6 +-- 7 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 docs/api/modules/modinfo.rst create mode 100644 pyircbot/modules/ModInfo.py diff --git a/docs/api/modules/modinfo.rst b/docs/api/modules/modinfo.rst new file mode 100644 index 0000000..ee3aa1b --- /dev/null +++ b/docs/api/modules/modinfo.rst @@ -0,0 +1,31 @@ +:mod:`ModInfo` --- Module command help system +============================================= + +Implements global `help` and `helpindex` commands that print help information about all available modules. Modules must +import and use a decorator from this module. For example: + + +.. code-block:: python + + from pyircbot.modules.ModInfo import info + + # ... + + @info("help [command] show the manual for all or [commands]", cmds=["help"]) + @command("help") + def cmd_help(self, msg, cmd): + # ... + + +The `info` decorator takes a mandatory string parameter describing the command. The second, optional, list parameter +`cmds` is a list of short names thart are aliases for the function that aide in help lookup. In all cases, the cases, +commands will be prefixed with the default command prefix (`from pyircbot.modulebase.command.prefix`). + + +Class Reference +--------------- + +.. automodule:: pyircbot.modules.ModInfo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/modules/pubsubclient.rst b/docs/api/modules/pubsubclient.rst index af5d285..2b4873f 100644 --- a/docs/api/modules/pubsubclient.rst +++ b/docs/api/modules/pubsubclient.rst @@ -93,7 +93,6 @@ Client sending a message that the bot will relay pyircbot_send default privmsg ["#clonebot", "asdf1234"] -""" Example Programs ---------------- diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index 46a3529..00772c7 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -251,9 +251,6 @@ class regex(hook): def cmd_foobar(self, matches, msg): print("Someone's message was exactly "foobar" ({}) in channel {}".format(msg.message, msg.args[0])) - This stores a list of IRC actions each function is tagged for in method.__tag_regexes. This attribute is scanned - during module init and appropriate hooks are set up. - :param regexps: expressions to match for :type keywords: str :param allow_private: enable matching in private messages diff --git a/pyircbot/modules/Calc.py b/pyircbot/modules/Calc.py index 4cf0fc7..8fc2fed 100644 --- a/pyircbot/modules/Calc.py +++ b/pyircbot/modules/Calc.py @@ -1,5 +1,6 @@ from pyircbot.modulebase import ModuleBase, ModuleHook, MissingDependancyException, regex, command +from pyircbot.modules.ModInfo import info import datetime import time import math @@ -79,6 +80,7 @@ class Calc(ModuleBase): seconds = int(remaining - (minutes * 60)) return "Please wait %s minute(s) and %s second(s)." % (minutes, seconds) + @info("quote [key[ =[ value]]] set or update facts", cmds=["quote"]) @regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)', types=['PRIVMSG']) def cmd_calc(self, message, match): word, changeit, value = match.groups() diff --git a/pyircbot/modules/ModInfo.py b/pyircbot/modules/ModInfo.py new file mode 100644 index 0000000..642ec66 --- /dev/null +++ b/pyircbot/modules/ModInfo.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +""" +.. module::ModInfo + :synopsis: Provides manpage-like info for commands +""" + + +from pyircbot.modulebase import ModuleBase, command + + +class info(object): + """ + Decorator for tagging module methods with help text + + .. code-block:: python + from pyircbot.modules.ModInfo import info + + ... + + @info("help [command] show the manual for all or [commands]", cmds=["help", "rtfm"]) + @command("help") + def cmd_help(self, msg, cmd): + ... + + :param docstring: command help formatted as above + :type docstring: str + :param cmds: enable command names or aliases this function implements, as a list of strings. E.g. if the "help" + command has the alias "rtfm" + :type cmds: list + """ + def __init__(self, docstring, cmds=None): + self.docstring = docstring + self.commands = cmds or [] + + def __call__(self, func): + setattr(func, "irchelp", self.docstring) + setattr(func, "irchelpc", self.commands) + return func + + +class ModInfo(ModuleBase): + + @info("help [command] show the manual for all or [commands]", cmds=["help"]) + @command("help") + def cmd_help(self, msg, cmd): + """ + Get help on a command + """ + if cmd.args: + for modname, module, helptext, helpcommands in self.iter_modules(): + if cmd.args[0] in ["{}{}".format(command.prefix, i) for i in helpcommands]: + self.bot.act_PRIVMSG(msg.args[0], "RTFM: {}: {}".format(cmd.args[0], helptext)) + else: + for modname, module, helptext, helpcommands in self.iter_modules(): + self.bot.act_PRIVMSG(msg.args[0], "{}: {}{}".format(modname, command.prefix, helptext)) + + @command("helpindex") + def cmd_helpindex(self, msg, cmd): + """ + Short index of commands + """ + commands = [] + for modname, module, helptext, helpcommands in self.iter_modules(): + commands += ["{}{}".format(command.prefix, i) for i in helpcommands] + + self.bot.act_PRIVMSG(msg.args[0], "{}: commands: {}".format(msg.prefix.nick, ", ".join(commands))) + + def iter_modules(self): + """ + Iterator that cycles through module methods that are tagged with help information. The iterator yields tuples + of: + + (module_name, module_object, helptext, command_list) + """ + for modname, module in self.bot.moduleInstances.items(): + for attr_name in dir(module): + attr = getattr(module, attr_name) + if callable(attr) and hasattr(attr, "irchelp"): + yield (modname, module, getattr(attr, "irchelp"), getattr(attr, "irchelpc"), ) + raise StopIteration() diff --git a/pyircbot/modules/Seen.py b/pyircbot/modules/Seen.py index 8017aac..e7c4ac5 100755 --- a/pyircbot/modules/Seen.py +++ b/pyircbot/modules/Seen.py @@ -7,7 +7,9 @@ """ -from pyircbot.modulebase import ModuleBase, ModuleHook +from pyircbot.modules.ModInfo import info +from pyircbot.modulebase import ModuleBase, command, hook +from contextlib import closing import sqlite3 import time @@ -15,7 +17,6 @@ import time class Seen(ModuleBase): def __init__(self, bot, moduleName): ModuleBase.__init__(self, bot, moduleName) - self.hooks = [ModuleHook("PRIVMSG", self.lastSeen)] # if the database doesnt exist, it will be created sql = self.getSql() c = sql.cursor() @@ -27,31 +28,32 @@ class Seen(ModuleBase): c.execute("CREATE TABLE `seen` (`nick` VARCHAR(32), `date` INTEGER, PRIMARY KEY(`nick`))") self.x = "asdf" - def lastSeen(self, args, prefix, trailing): + @hook("PRIVMSG") + def recordSeen(self, message, command): # using a message to update last seen, also, the .seen query - prefixObj = self.bot.decodePrefix(prefix) - nick = prefixObj.nick - sql = self.getSql() - c = sql.cursor() - # update or add the user's row datest = str(time.time() + (int(self.config["add_hours"]) * 60 * 60)) - c.execute("REPLACE INTO `seen` (`nick`, `date`) VALUES (?, ?)", (nick.lower(), datest)) - # self.log.info("Seen: %s on %s" % (nick.lower(), datest)) - sql.commit() - if trailing.startswith(".seen"): - cmdargs = trailing.split(" ") + sql = self.getSql() + with closing(sql.cursor()) as c: + # update or add the user's row + c.execute("REPLACE INTO `seen` (`nick`, `date`) VALUES (?, ?)", (message.prefix.nick.lower(), datest)) + # self.log.info("Seen: %s on %s" % (nick.lower(), datest)) + sql.commit() + + @info("seen print last time user was seen", cmds=["seen"]) + @command("seen", require_args=True) + def lastSeen(self, message, command): + sql = self.getSql() + searchnic = command.args[0].lower() + with closing(sql.cursor()) as c: # query the DB for the user - if len(cmdargs) >= 2: - searchnic = cmdargs[1].lower() - c.execute("SELECT * FROM `seen` WHERE `nick`= ? ", [searchnic]) - rows = c.fetchall() - if len(rows) == 1: - self.bot.act_PRIVMSG(args[0], "I last saw %s on %s (%s)." % - (cmdargs[1], time.strftime("%m/%d/%y at %I:%M %p", - time.localtime(rows[0]['date'])), self.config["timezone"])) - else: - self.bot.act_PRIVMSG(args[0], "Sorry, I haven't seen %s!" % cmdargs[1]) - c.close() + c.execute("SELECT * FROM `seen` WHERE `nick`= ? ", [searchnic]) + rows = c.fetchall() + if len(rows) == 1: + self.bot.act_PRIVMSG(message.args[0], "I last saw %s on %s (%s)." % + (command.args[0], time.strftime("%m/%d/%y at %I:%M %p", + time.localtime(rows[0]['date'])), self.config["timezone"])) + else: + self.bot.act_PRIVMSG(message.args[0], "Sorry, I haven't seen %s!" % command.args[0]) def getSql(self): # return a SQL reference to the database @@ -66,9 +68,3 @@ class Seen(ModuleBase): for idx, col in enumerate(cursor.description): d[col[0]] = row[idx] return d - - def test(self, arg): - print("TEST: %s" % arg) - print("self.x = %s" % self.x) - return arg - diff --git a/pyircbot/pyircbot.py b/pyircbot/pyircbot.py index e352dee..82ab7a7 100644 --- a/pyircbot/pyircbot.py +++ b/pyircbot/pyircbot.py @@ -369,14 +369,14 @@ class PyIRCBot(object): argsStart = len(command) args = "" if argsStart > 0: - args = message[argsStart + 1:] + args = message[argsStart + 1:].strip() - if requireArgs and args.strip() == '': + if requireArgs and args == '': return False # Verified! Return the set. return ParsedCommand(command, - args.split(" "), + args.split(" ") if args else [], args, message)