add command decorator

This commit is contained in:
dave 2017-07-02 14:48:34 -07:00
parent 65459051da
commit b09e675189
2 changed files with 99 additions and 6 deletions

View File

@ -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

View File

@ -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