From b09e675189f36c742c32b788fe0d37a36c010530 Mon Sep 17 00:00:00 2001 From: dave Date: Sun, 2 Jul 2017 14:48:34 -0700 Subject: [PATCH] add command decorator --- pyircbot/modulebase.py | 83 ++++++++++++++++++++++++++++++++++++++++-- pyircbot/pyircbot.py | 22 +++++++++-- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index dd8a424..f123591 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -8,6 +8,8 @@ import logging from .pyircbot import PyIRCBot +from inspect import getargspec +import os class ModuleBase: @@ -29,6 +31,9 @@ class ModuleBase: self.hooks = [] """Hooks (aka listeners) this module has""" + self.irchooks = [] + """IRC Hooks this module has""" + self.services = [] """If this module provides services usable by another module, they're listed here""" @@ -53,9 +58,14 @@ class ModuleBase: """ for attr_name in dir(self): attr = getattr(self, attr_name) - if callable(attr) and hasattr(attr, ATTR_ACTION_HOOK): + if not callable(attr): + continue + if hasattr(attr, ATTR_ACTION_HOOK): for action in getattr(attr, ATTR_ACTION_HOOK): self.hooks.append(ModuleHook(action, attr)) + if hasattr(attr, ATTR_COMMAND_HOOK): + for action in getattr(attr, ATTR_COMMAND_HOOK): + self.irchooks.append(IRCHook(action, attr)) def loadConfig(self): """ @@ -85,7 +95,7 @@ class ModuleBase: :type channel: str :Warning: .. Warning:: this does no error checking if the file exists or is\ writable. The bot's data dir *should* always be writable""" - return self.bot.getDataPath(self.moduleName) + (f if f else '') + return os.path.join(self.bot.getDataPath(self.moduleName), (f if f else '')) class ModuleHook: @@ -94,12 +104,22 @@ class ModuleHook: self.method = method +class IRCHook: + def __init__(self, hook, method): + self.hook = hook + self.method = method + + def call(self, msg): + self.hook.call(self.method, msg) + + ATTR_ACTION_HOOK = "__tag_hooks" +ATTR_COMMAND_HOOK = "__tag_commands" class hook(object): """ - Decorating for calling module methods in response to IRC actions. Example: + Decorator for calling module methods in response to IRC actions. Example: ``` @hook("PRIVMSG") def mymyethod(self, message): @@ -120,3 +140,60 @@ class hook(object): else: getattr(func, ATTR_ACTION_HOOK).extend(self.commands) return func + + +class irchook(object): + def __call__(self, func): + if not hasattr(func, ATTR_COMMAND_HOOK): + setattr(func, ATTR_COMMAND_HOOK, [self]) + else: + getattr(func, ATTR_COMMAND_HOOK).extend(self) + return func + + def validate(self, msg, bot): + """ + Return True if the message should be passed on. False otherwise. + """ + return True + + +class command(irchook): + """ + Decorating for calling module methods in response to IRC actions. Example: + ``` + @hook("PRIVMSG") + def mymyethod(self, message): + print("IRC server sent PRIVMSG") + ``` + This stores a list of IRC actions each function is tagged for in method.__tag_hooks. This attribute is scanned + during module init and appropriate hooks are set up. + """ + prefix = "." + + def __init__(self, *keywords, require_args=False, allow_private=False): + self.keywords = keywords + self.require_args = require_args + self.allow_private = allow_private + self.parsed_cmd = None + + def call(self, method, msg): + if len(getargspec(method).args) == 3: + method(self.parsed_cmd, msg) + else: + method(self.parsed_cmd) + + def validate(self, msg, bot): + if not self.allow_private and msg.args[0] == "#": + return False + for keyword in self.keywords: + if self._validate_one(msg, keyword): + return True + return False + + def _validate_one(self, msg, keyword): + with_prefix = "{}{}".format(self.prefix, keyword) + cmd = PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) + if cmd: + self.parsed_cmd = cmd + return True + return False diff --git a/pyircbot/pyircbot.py b/pyircbot/pyircbot.py index 3764b54..225a6c8 100644 --- a/pyircbot/pyircbot.py +++ b/pyircbot/pyircbot.py @@ -15,6 +15,7 @@ from socket import AF_INET, AF_INET6 from json import load import os.path import asyncio +import traceback ParsedCommand = namedtuple("ParsedCommand", "command args args_str message") @@ -67,6 +68,9 @@ class PyIRCBot(object): # Load modules self.initModules() + # Internal usage hook + self.irc.addHook("PRIVMSG", self._hook_privmsg_internal) + def run(self): self.loop = asyncio.get_event_loop() @@ -111,6 +115,16 @@ class PyIRCBot(object): for modulename in self.botconfig["modules"]: self.loadmodule(modulename) + def _hook_privmsg_internal(self, msg): + """ + IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a PRIVMSG is + received. It tests all hooks in all modules against the message can calls the hooked function on hits. + """ + for module_name, module in self.moduleInstances.items(): + for hook in module.irchooks: + if hook.hook.validate(msg, self): + hook.call(msg) + def importmodule(self, name): """Import a module @@ -128,6 +142,7 @@ class PyIRCBot(object): " on failure (usually syntax error in Module code) print an error " self.log.error("Module %s failed to load: " % name) self.log.error("Module load failure reason: " + str(e)) + traceback.print_exc() return (False, str(e)) else: self.log.warning("Module %s already imported" % name) @@ -299,9 +314,10 @@ class PyIRCBot(object): :param moduleName: the module who's data dir we want :type moduleName: str""" - if not os.path.exists("%s/data/%s" % (self.botconfig["bot"]["datadir"], moduleName)): - os.mkdir("%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName)) - return "%s/data/%s/" % (self.botconfig["bot"]["datadir"], moduleName) + module_dir = os.path.join(self.botconfig["bot"]["datadir"], "data", moduleName) + if not os.path.exists(module_dir): + os.mkdir(module_dir) + return module_dir def getConfigPath(self, moduleName): """Return the absolute path for a module's config file