You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
9.2 KiB
291 lines
9.2 KiB
""" |
|
.. 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: |
|
"""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 |
|
:type require_args: bool |
|
:param allow_private: enable matching in private messages |
|
:type allow_private: bool |
|
""" |
|
|
|
prefix = "." |
|
""" |
|
Hotkey that must appear before commands |
|
""" |
|
|
|
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 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 msg.args[0][0] != "#" and not self.allow_private: |
|
return False |
|
for keyword in self.keywords: |
|
single = self._validate_one(msg, keyword) |
|
if single: |
|
return single |
|
return False |
|
|
|
def _validate_one(self, msg, keyword): |
|
with_prefix = "{}{}".format(self.prefix, keyword) |
|
return messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) |
|
|
|
|
|
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 |
|
"""
|
|
|