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 os
import logging import logging
from .pyircbot import PyIRCBot from .pyircbot import PyIRCBot
from inspect import getargspec
class ModuleBase: class ModuleBase:
@ -30,7 +29,7 @@ class ModuleBase:
"""Reference to the master PyIRCBot object""" """Reference to the master PyIRCBot object"""
self.hooks = [] self.hooks = []
"""Hooks (aka listeners) this module has""" """Low-level protocol hooks this module has"""
self.irchooks = [] self.irchooks = []
"""IRC Hooks this module has""" """IRC Hooks this module has"""
@ -61,12 +60,9 @@ class ModuleBase:
attr = getattr(self, attr_name) attr = getattr(self, attr_name)
if not callable(attr): if not callable(attr):
continue continue
if hasattr(attr, ATTR_ACTION_HOOK): if hasattr(attr, ATTR_ALL_HOOKS):
for action in getattr(attr, ATTR_ACTION_HOOK): for hook in getattr(attr, ATTR_ALL_HOOKS):
self.hooks.append(ModuleHook(action, attr)) self.irchooks.append(IRCHook(hook.validate, attr))
if hasattr(attr, ATTR_COMMAND_HOOK):
for action in getattr(attr, ATTR_COMMAND_HOOK):
self.irchooks.append(IRCHook(action, attr))
def loadConfig(self): def loadConfig(self):
""" """
@ -110,19 +106,61 @@ class ModuleHook:
class IRCHook: class IRCHook:
def __init__(self, hook, method): def __init__(self, validator, method):
self.hook = hook """
: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 self.method = method
def call(self, msg):
self.hook.call(self.method, msg) ATTR_ALL_HOOKS = "__hooks"
ATTR_ACTION_HOOK = "__tag_hooks" class AbstractHook(object):
ATTR_COMMAND_HOOK = "__tag_commands" """
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: Decorator for calling module methods in response to IRC actions. Example:
@ -141,36 +179,12 @@ class hook(object):
def __init__(self, *args): def __init__(self, *args):
self.commands = 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): def validate(self, msg, bot):
""" if msg.command in self.commands:
Return True if the message should be passed on. False otherwise. return True
"""
return True
class command(irchook): class command(hook):
""" """
Decorator for calling module methods when a command is parsed from chat 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): def __init__(self, *keywords, require_args=False, allow_private=False):
super().__init__("PRIVMSG")
self.keywords = keywords self.keywords = keywords
self.require_args = require_args self.require_args = require_args
self.allow_private = allow_private self.allow_private = allow_private
self.parsed_cmd = None 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): def validate(self, msg, bot):
""" """
Test a message and return true if matched. Test a message and return true if matched.
@ -220,23 +226,22 @@ class command(irchook):
:param bot: reference to main pyircbot :param bot: reference to main pyircbot
:type bot: pyircbot.pyircbot.PyIRCBot :type bot: pyircbot.pyircbot.PyIRCBot
""" """
if not super().validate(msg, bot):
return False
if not self.allow_private and msg.args[0] == "#": if not self.allow_private and msg.args[0] == "#":
return False return False
for keyword in self.keywords: for keyword in self.keywords:
if self._validate_one(msg, keyword): single = self._validate_one(msg, keyword)
return True if single:
return single
return False return False
def _validate_one(self, msg, keyword): def _validate_one(self, msg, keyword):
with_prefix = "{}{}".format(self.prefix, keyword) with_prefix = "{}{}".format(self.prefix, keyword)
cmd = PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) return PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args)
if cmd:
self.parsed_cmd = cmd
return True
return False
class regex(irchook): class regex(hook):
""" """
Decorator for calling module methods when a message matches a regex. 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): def __init__(self, *regexps, allow_private=False, types=None):
super().__init__("PRIVMSG")
self.regexps = [re.compile(r) for r in regexps] self.regexps = [re.compile(r) for r in regexps]
self.allow_private = allow_private self.allow_private = allow_private
self.matches = None
self.types = types 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): def validate(self, msg, bot):
""" """
Test a message and return true if matched. Test a message and return true if matched.
@ -281,6 +277,8 @@ class regex(irchook):
:param bot: reference to main pyircbot :param bot: reference to main pyircbot
:type bot: pyircbot.pyircbot.PyIRCBot :type bot: pyircbot.pyircbot.PyIRCBot
""" """
if not super().validate(msg, bot):
return False
if self.types and msg.command not in self.types: if self.types and msg.command not in self.types:
return False return False
if not self.allow_private and msg.args[0] == "#": if not self.allow_private and msg.args[0] == "#":
@ -288,8 +286,7 @@ class regex(irchook):
for exp in self.regexps: for exp in self.regexps:
matches = exp.search(msg.trailing) matches = exp.search(msg.trailing)
if matches: if matches:
self.matches = matches return matches
return True
return False return False

View File

@ -33,7 +33,7 @@ class ASCII(ModuleBase):
# @hook("PRIVMSG") # @hook("PRIVMSG")
@command("listascii") @command("listascii")
def cmd_listascii(self, cmd, msg): def cmd_listascii(self, msg, cmd):
""" """
List available asciis List available asciis
""" """
@ -48,7 +48,7 @@ class ASCII(ModuleBase):
return return
@command("ascii", require_args=True) @command("ascii", require_args=True)
def cmd_ascii(self, cmd, msg): def cmd_ascii(self, msg, cmd):
# import ipdb # import ipdb
# ipdb.set_trace() # ipdb.set_trace()
# Send out an ascii # Send out an ascii
@ -70,7 +70,7 @@ class ASCII(ModuleBase):
return return
@command("stopascii") @command("stopascii")
def cmd_stopascii(self, cmd, msg): def cmd_stopascii(self, msg, cmd):
""" """
Command to stop the running ascii in a given channel Command to stop the running ascii in a given channel
""" """
@ -120,7 +120,7 @@ class ASCII(ModuleBase):
del self.killed_channels[channel] del self.killed_channels[channel]
@command("asciiedit", require_args=True) @command("asciiedit", require_args=True)
def cmd_asciiedit(self, cmd, msg): def cmd_asciiedit(self, msg, cmd):
ascii_name = cmd.args.pop(0) ascii_name = cmd.args.pop(0)
try: try:
with open(self.getFilePath(ascii_name + ".json")) as f: 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) return "Please wait %s minute(s) and %s second(s)." % (minutes, seconds)
@regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)', types=['PRIVMSG']) @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() word, changeit, value = match.groups()
if word: if word:
word = word.strip() word = word.strip()
@ -134,7 +134,7 @@ class Calc(ModuleBase):
self.updateTimeSince(channel, "calc") self.updateTimeSince(channel, "calc")
@command("match", require_args=True) @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"]: 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.bot.act_PRIVMSG(msg.prefix.nick, self.remainingToStr(self.config["delayMatch"],
self.timeSince(msg.args[0], "match"))) self.timeSince(msg.args[0], "match")))

View File

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

View File

@ -69,7 +69,7 @@ class PyIRCBot(object):
self.initModules() self.initModules()
# Internal usage hook # Internal usage hook
self.irc.addHook("PRIVMSG", self._hook_privmsg_internal) self.irc.addHook("PRIVMSG", self._irchook_internal)
def run(self): def run(self):
self.loop = asyncio.get_event_loop() self.loop = asyncio.get_event_loop()
@ -116,15 +116,16 @@ class PyIRCBot(object):
for modulename in self.botconfig["modules"]: for modulename in self.botconfig["modules"]:
self.loadmodule(modulename) 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. 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 module_name, module in self.moduleInstances.items():
for hook in module.irchooks: for hook in module.irchooks:
if hook.hook.validate(msg, self): validation = hook.validator(msg, self)
hook.call(msg) if validation:
hook.method(msg, validation)
def importmodule(self, name): def importmodule(self, name):
"""Import a module """Import a module