298 lines
9.7 KiB
Python
298 lines
9.7 KiB
Python
"""
|
|
.. module:: ModuleBase
|
|
:synopsis: Base class that modules will extend
|
|
|
|
.. moduleauthor:: Dave Pedu <dave@davepedu.com>
|
|
|
|
"""
|
|
|
|
import re
|
|
import os
|
|
import logging
|
|
from .common import load as pload
|
|
from .common import messageHasCommand
|
|
|
|
|
|
class ModuleBase(object):
|
|
"""All modules will extend this class
|
|
|
|
:param bot: A reference to the main bot passed when this module is created
|
|
:type bot: PyIRCBot
|
|
:param moduleName: The name assigned to this module
|
|
:type moduleName: str
|
|
"""
|
|
|
|
def __init__(self, bot, moduleName):
|
|
self.moduleName = moduleName
|
|
"""Assigned name of this module"""
|
|
|
|
self.bot = bot
|
|
"""Reference to the master PyIRCBot object"""
|
|
|
|
self.irchooks = []
|
|
"""IRC Hooks this module has"""
|
|
|
|
self.services = []
|
|
"""If this module provides services usable by another module, they're listed
|
|
here"""
|
|
|
|
self.config = {}
|
|
"""Configuration dictionary. Autoloaded from `%(datadir)s/%(modulename)s.json`"""
|
|
|
|
self.log = logging.getLogger("Module.%s" % self.moduleName)
|
|
"""Logger object for this module"""
|
|
|
|
# Autoload config if available
|
|
self.loadConfig()
|
|
|
|
# Prepare any function hooking
|
|
self.init_hooks()
|
|
|
|
self.log.info("Loaded module %s" % self.moduleName)
|
|
|
|
def init_hooks(self):
|
|
"""
|
|
Scan the module for tagged methods and set up appropriate protocol hooks.
|
|
"""
|
|
for attr_name in dir(self):
|
|
attr = getattr(self, attr_name)
|
|
if not callable(attr):
|
|
continue
|
|
if hasattr(attr, ATTR_ALL_HOOKS):
|
|
for hook in getattr(attr, ATTR_ALL_HOOKS):
|
|
self.irchooks.append(IRCHook(hook.validate, attr))
|
|
|
|
def loadConfig(self):
|
|
"""
|
|
Loads this module's config into self.config. The bot's main config is checked for a section matching the module
|
|
name, which will be preferred. If not found, an individual config file will be loaded from the data dir
|
|
"""
|
|
self.config = self.bot.botconfig.get("module_configs", {}).get(self.__class__.__name__, {})
|
|
if not self.config:
|
|
configPath = self.getConfigPath()
|
|
if configPath is not None:
|
|
self.config = pload(configPath)
|
|
|
|
def onenable(self):
|
|
"""Called when the module is enabled"""
|
|
pass
|
|
|
|
def ondisable(self):
|
|
"""Called when the module should be disabled. Your module should do any sort
|
|
of clean-up operations here like ending child threads or saving data files.
|
|
"""
|
|
pass
|
|
|
|
def getConfigPath(self):
|
|
"""Returns the absolute path of this module's json config file"""
|
|
return self.bot.getConfigPath(self.moduleName)
|
|
|
|
def getFilePath(self, f=None):
|
|
"""Returns the absolute path to a file in this Module's data dir
|
|
|
|
:param f: The file name included in the path
|
|
: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 os.path.join(self.bot.getDataPath(self.moduleName), (f if f else ''))
|
|
|
|
|
|
class ModuleHook:
|
|
def __init__(self, hook, method):
|
|
self.hook = hook
|
|
self.method = method
|
|
|
|
|
|
class IRCHook:
|
|
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
|
|
|
|
|
|
ATTR_ALL_HOOKS = "__hooks"
|
|
|
|
|
|
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(AbstractHook):
|
|
"""
|
|
Decorator for calling module methods in response to IRC actions. Example:
|
|
|
|
.. code-block:: python
|
|
|
|
@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.
|
|
|
|
: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, *args):
|
|
self.commands = args
|
|
|
|
def validate(self, msg, bot):
|
|
if msg.command in self.commands:
|
|
return True
|
|
|
|
|
|
class command(hook):
|
|
"""
|
|
Decorator for calling module methods when a command is parsed from chat
|
|
|
|
.. code-block:: python
|
|
|
|
@command("ascii")
|
|
def cmd_ascii(self, cmd, msg):
|
|
print("Somebody typed .ascii with params {} in channel {}".format(str(cmd.args), msg.args[0]))
|
|
|
|
This stores a list of IRC actions each function is tagged for in method.__tag_commands. This attribute is scanned
|
|
during module init and appropriate hooks are set up.
|
|
|
|
:param keywords: commands to listen for
|
|
:type keywords: str
|
|
:param require_args: only match if trailing data is passed with the command used. False-like values disable This
|
|
requirement. True-like values require any number of args greater than one. Int values require a specific
|
|
number of args
|
|
:type require_args: bool, int
|
|
:param allow_private: enable matching in private messages
|
|
:type allow_private: bool
|
|
:param allow_highlight: treat 'Nick[:,] command args' the same as '.command args'
|
|
"""
|
|
|
|
prefix = "."
|
|
"""
|
|
Hotkey that must appear before commands
|
|
"""
|
|
|
|
def __init__(self, *keywords, require_args=False, allow_private=False, allow_highlight=True):
|
|
super().__init__("PRIVMSG")
|
|
self.keywords = keywords
|
|
self.require_args = require_args
|
|
self.allow_private = allow_private
|
|
self.allow_highlight = allow_highlight
|
|
|
|
def validate(self, msg, bot):
|
|
"""
|
|
Test a message and return true if matched.
|
|
|
|
:param msg: message to test against
|
|
:type msg: pyircbot.irccore.IRCEvent
|
|
:param bot: reference to main pyircbot
|
|
:type bot: pyircbot.pyircbot.PyIRCBot
|
|
"""
|
|
bot_nick = bot.get_nick()
|
|
if not super().validate(msg, bot):
|
|
return False
|
|
if msg.args[0][0] != "#" and not self.allow_private:
|
|
return False
|
|
for keyword in self.keywords:
|
|
single = self._validate_prefixedcommand(msg, keyword, bot_nick)
|
|
if single:
|
|
return single
|
|
return False
|
|
|
|
def _validate_prefixedcommand(self, msg, keyword, nick):
|
|
with_prefix = "{}{}".format(self.prefix, keyword)
|
|
return messageHasCommand(with_prefix, msg.trailing,
|
|
requireArgs=self.require_args,
|
|
withHighlight=nick if self.allow_highlight else False)
|
|
|
|
|
|
class regex(hook):
|
|
"""
|
|
Decorator for calling module methods when a message matches a regex.
|
|
|
|
.. code-block:: python
|
|
|
|
@regex(r'^foobar$')
|
|
def cmd_foobar(self, matches, msg):
|
|
print("Someone's message was exactly "foobar" ({}) in channel {}".format(msg.message, msg.args[0]))
|
|
|
|
:param regexps: expressions to match for
|
|
:type keywords: str
|
|
:param allow_private: enable matching in private messages
|
|
:type allow_private: bool
|
|
:param types: list of irc commands such as PRIVMSG to accept
|
|
:type types: list
|
|
"""
|
|
|
|
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.types = types
|
|
|
|
def validate(self, msg, bot):
|
|
"""
|
|
Test a message and return true if matched.
|
|
|
|
:param msg: message to test against
|
|
:type msg: pyircbot.irccore.IRCEvent
|
|
: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] == "#":
|
|
return False
|
|
for exp in self.regexps:
|
|
matches = exp.search(msg.trailing)
|
|
if matches:
|
|
return matches
|
|
return False
|
|
|
|
|
|
class MissingDependancyException(Exception):
|
|
"""
|
|
Exception expressing that a pyricbot module could not find a required module
|
|
"""
|