Refactor modulebase hooks to reduce internal complexity

This commit is contained in:
dave 2017-11-22 20:20:52 -08:00
parent f000194af4
commit 5c8f6b02fd
5 changed files with 81 additions and 83 deletions

View File

@ -10,7 +10,6 @@ import re
import os
import logging
from .pyircbot import PyIRCBot
from inspect import getargspec
class ModuleBase:
@ -30,7 +29,7 @@ class ModuleBase:
"""Reference to the master PyIRCBot object"""
self.hooks = []
"""Hooks (aka listeners) this module has"""
"""Low-level protocol hooks this module has"""
self.irchooks = []
"""IRC Hooks this module has"""
@ -61,12 +60,9 @@ class ModuleBase:
attr = getattr(self, attr_name)
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))
if hasattr(attr, ATTR_ALL_HOOKS):
for hook in getattr(attr, ATTR_ALL_HOOKS):
self.irchooks.append(IRCHook(hook.validate, attr))
def loadConfig(self):
"""
@ -110,19 +106,61 @@ class ModuleHook:
class IRCHook:
def __init__(self, hook, method):
self.hook = hook
def __init__(self, validator, method):
"""
:param validator: method accpeting an IRCEvent and returning false-like or true-like depending on match
:param method: module method
"""
self.validator = validator
self.method = method
def call(self, msg):
self.hook.call(self.method, msg)
ATTR_ALL_HOOKS = "__hooks"
ATTR_ACTION_HOOK = "__tag_hooks"
ATTR_COMMAND_HOOK = "__tag_commands"
class AbstractHook(object):
"""
Decorator for calling module methods in response to arbitrary IRC actions. Example:
.. code-block:: python
@myhooksubclass(<conditions>)
def mymyethod(self, message, extra):
print("IRC server sent something that matched <conditions>")
This stores some record of the above filtering in an attribute of the decorated method, such as method.__tag_hooks.
This attribute is scanned during module init and appropriate hooks are set up.
Hooks implement a validate() method that return a true-like or false-like item, dictating whether the hooked method
will be called (with the message and true-like object as parameters)
:param args: irc protocol event to listen for. See :py:meth:`pyircbot.irccore.IRCCore.initHooks` for a complete list
:type args: str
"""
def __init__(self):
# todo do i need this here for the docstring?
pass
def __call__(self, func):
"""
Store a list of such hooks in an attribute of the decorated method
"""
if not hasattr(func, ATTR_ALL_HOOKS):
setattr(func, ATTR_ALL_HOOKS, [self])
else:
getattr(func, ATTR_ALL_HOOKS).extend(self)
return func
def validate(self, msg, bot):
"""
Return a true-like item if the hook matched. Otherwise, false.
:param msg: IRCEvent instance of the message
:param bot: reference to the bot TODO remove this
"""
return True
class hook(object):
class hook(AbstractHook):
"""
Decorator for calling module methods in response to IRC actions. Example:
@ -141,36 +179,12 @@ class hook(object):
def __init__(self, *args):
self.commands = args
def __call__(self, func):
if not hasattr(func, ATTR_ACTION_HOOK):
setattr(func, ATTR_ACTION_HOOK, list(self.commands))
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 call(self, method, msg):
"""
Call the hooked function
"""
method(msg) # TODO is this actually a sane base?
def validate(self, msg, bot):
"""
Return True if the message should be passed on. False otherwise.
"""
return True
if msg.command in self.commands:
return True
class command(irchook):
class command(hook):
"""
Decorator for calling module methods when a command is parsed from chat
@ -197,20 +211,12 @@ class command(irchook):
"""
def __init__(self, *keywords, require_args=False, allow_private=False):
super().__init__("PRIVMSG")
self.keywords = keywords
self.require_args = require_args
self.allow_private = allow_private
self.parsed_cmd = None
def call(self, method, msg):
"""
Overridden to make the msg param optional
"""
if len(getargspec(method).args) == 3:
return method(self.parsed_cmd, msg)
else:
return method(self.parsed_cmd)
def validate(self, msg, bot):
"""
Test a message and return true if matched.
@ -220,23 +226,22 @@ class command(irchook):
:param bot: reference to main pyircbot
:type bot: pyircbot.pyircbot.PyIRCBot
"""
if not super().validate(msg, bot):
return False
if not self.allow_private and msg.args[0] == "#":
return False
for keyword in self.keywords:
if self._validate_one(msg, keyword):
return True
single = self._validate_one(msg, keyword)
if single:
return single
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
return PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args)
class regex(irchook):
class regex(hook):
"""
Decorator for calling module methods when a message matches a regex.
@ -258,20 +263,11 @@ class regex(irchook):
"""
def __init__(self, *regexps, allow_private=False, types=None):
super().__init__("PRIVMSG")
self.regexps = [re.compile(r) for r in regexps]
self.allow_private = allow_private
self.matches = None
self.types = types
def call(self, method, msg):
"""
Overridden to pass matches and make an arg optional
"""
if len(getargspec(method).args) == 3:
return method(self.matches, msg)
else:
return method(self.matches)
def validate(self, msg, bot):
"""
Test a message and return true if matched.
@ -281,6 +277,8 @@ class regex(irchook):
:param bot: reference to main pyircbot
:type bot: pyircbot.pyircbot.PyIRCBot
"""
if not super().validate(msg, bot):
return False
if self.types and msg.command not in self.types:
return False
if not self.allow_private and msg.args[0] == "#":
@ -288,8 +286,7 @@ class regex(irchook):
for exp in self.regexps:
matches = exp.search(msg.trailing)
if matches:
self.matches = matches
return True
return matches
return False

View File

@ -33,7 +33,7 @@ class ASCII(ModuleBase):
# @hook("PRIVMSG")
@command("listascii")
def cmd_listascii(self, cmd, msg):
def cmd_listascii(self, msg, cmd):
"""
List available asciis
"""
@ -48,7 +48,7 @@ class ASCII(ModuleBase):
return
@command("ascii", require_args=True)
def cmd_ascii(self, cmd, msg):
def cmd_ascii(self, msg, cmd):
# import ipdb
# ipdb.set_trace()
# Send out an ascii
@ -70,7 +70,7 @@ class ASCII(ModuleBase):
return
@command("stopascii")
def cmd_stopascii(self, cmd, msg):
def cmd_stopascii(self, msg, cmd):
"""
Command to stop the running ascii in a given channel
"""
@ -120,7 +120,7 @@ class ASCII(ModuleBase):
del self.killed_channels[channel]
@command("asciiedit", require_args=True)
def cmd_asciiedit(self, cmd, msg):
def cmd_asciiedit(self, msg, cmd):
ascii_name = cmd.args.pop(0)
try:
with open(self.getFilePath(ascii_name + ".json")) as f:

View File

@ -80,7 +80,7 @@ class Calc(ModuleBase):
return "Please wait %s minute(s) and %s second(s)." % (minutes, seconds)
@regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)', types=['PRIVMSG'])
def cmd_calc(self, match, message):
def cmd_calc(self, message, match):
word, changeit, value = match.groups()
if word:
word = word.strip()
@ -134,7 +134,7 @@ class Calc(ModuleBase):
self.updateTimeSince(channel, "calc")
@command("match", require_args=True)
def cmd_match(self, cmd, msg):
def cmd_match(self, msg, cmd):
if self.config["delayMatch"] > 0 and self.timeSince(msg.args[0], "match") < self.config["delayMatch"]:
self.bot.act_PRIVMSG(msg.prefix.nick, self.remainingToStr(self.config["delayMatch"],
self.timeSince(msg.args[0], "match")))

View File

@ -134,7 +134,7 @@ class SMS(ModuleBase):
cherrypy.engine.exit()
@regex(r'(?:^\.text\-([a-zA-Z0-9]+)(?:\s+(.+))?)', types=['PRIVMSG'])
def cmd_text(self, match, msg):
def cmd_text(self, msg, match):
"""
Text somebody
"""

View File

@ -69,7 +69,7 @@ class PyIRCBot(object):
self.initModules()
# Internal usage hook
self.irc.addHook("PRIVMSG", self._hook_privmsg_internal)
self.irc.addHook("PRIVMSG", self._irchook_internal)
def run(self):
self.loop = asyncio.get_event_loop()
@ -116,15 +116,16 @@ class PyIRCBot(object):
for modulename in self.botconfig["modules"]:
self.loadmodule(modulename)
def _hook_privmsg_internal(self, msg):
def _irchook_internal(self, msg):
"""
IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a PRIVMSG is
IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a message 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)
validation = hook.validator(msg, self)
if validation:
hook.method(msg, validation)
def importmodule(self, name):
"""Import a module