Refactor modulebase hooks to reduce internal complexity
This commit is contained in:
parent
f000194af4
commit
5c8f6b02fd
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")))
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue