pyircbot/pyircbot/modulebase.py

230 lines
7.1 KiB
Python
Raw Normal View History

"""
.. module:: ModuleBase
2015-11-01 18:03:11 -08:00
:synopsis: Base class that modules will extend
.. moduleauthor:: Dave Pedu <dave@davepedu.com>
"""
2013-12-28 09:58:20 -08:00
import logging
2015-06-18 20:43:17 -07:00
from .pyircbot import PyIRCBot
2017-07-02 14:48:34 -07:00
from inspect import getargspec
import os
2013-12-28 09:58:20 -08:00
2017-01-01 14:01:23 -08:00
2013-12-28 09:58:20 -08:00
class ModuleBase:
2015-11-01 18:03:11 -08:00
"""All modules will extend this class
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
: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
"""
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
def __init__(self, bot, moduleName):
2017-01-01 14:59:01 -08:00
self.moduleName = moduleName
2015-11-01 18:03:11 -08:00
"""Assigned name of this module"""
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
self.bot = bot
"""Reference to the master PyIRCBot object"""
2017-01-01 14:01:23 -08:00
2017-01-01 14:59:01 -08:00
self.hooks = []
2015-11-01 18:03:11 -08:00
"""Hooks (aka listeners) this module has"""
2017-01-01 14:01:23 -08:00
2017-07-02 14:48:34 -07:00
self.irchooks = []
"""IRC Hooks this module has"""
2017-01-01 14:59:01 -08:00
self.services = []
2015-11-01 18:03:11 -08:00
"""If this module provides services usable by another module, they're listed
here"""
2017-01-01 14:01:23 -08:00
2017-01-01 14:59:01 -08:00
self.config = {}
2015-11-01 18:03:11 -08:00
"""Configuration dictionary. Autoloaded from `%(datadir)s/%(modulename)s.json`"""
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
self.log = logging.getLogger("Module.%s" % self.moduleName)
"""Logger object for this module"""
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
# Autoload config if available
self.loadConfig()
2017-01-01 14:01:23 -08:00
2017-05-14 12:44:14 -07:00
# Prepare any function hooking
self.init_hooks()
2015-11-01 18:03:11 -08:00
self.log.info("Loaded module %s" % self.moduleName)
2017-01-01 14:01:23 -08:00
2017-05-14 12:44:14 -07:00
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)
2017-07-02 14:48:34 -07:00
if not callable(attr):
continue
if hasattr(attr, ATTR_ACTION_HOOK):
2017-05-14 12:44:14 -07:00
for action in getattr(attr, ATTR_ACTION_HOOK):
self.hooks.append(ModuleHook(action, attr))
2017-07-02 14:48:34 -07:00
if hasattr(attr, ATTR_COMMAND_HOOK):
for action in getattr(attr, ATTR_COMMAND_HOOK):
self.irchooks.append(IRCHook(action, attr))
2017-05-14 12:44:14 -07:00
2015-11-01 18:03:11 -08:00
def loadConfig(self):
2017-04-30 23:27:40 -07:00
"""
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 = PyIRCBot.load(configPath)
2017-01-01 14:01:23 -08:00
2017-09-18 19:56:22 -07:00
def onenable(self):
"""Called when the module is enabled"""
pass
2015-11-01 18:03:11 -08:00
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
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
def getConfigPath(self):
"""Returns the absolute path of this module's json config file"""
return self.bot.getConfigPath(self.moduleName)
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
def getFilePath(self, f=None):
"""Returns the absolute path to a file in this Module's data dir
2017-01-01 14:01:23 -08:00
2015-11-01 18:03:11 -08:00
: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"""
2017-07-02 14:48:34 -07:00
return os.path.join(self.bot.getDataPath(self.moduleName), (f if f else ''))
2015-06-20 00:30:12 -07:00
2017-01-01 14:59:01 -08:00
2013-12-28 09:58:20 -08:00
class ModuleHook:
2015-11-01 18:03:11 -08:00
def __init__(self, hook, method):
2017-01-01 14:59:01 -08:00
self.hook = hook
self.method = method
2017-05-14 12:44:14 -07:00
2017-07-02 14:48:34 -07:00
class IRCHook:
def __init__(self, hook, method):
self.hook = hook
self.method = method
def call(self, msg):
self.hook.call(self.method, msg)
2017-05-14 12:44:14 -07:00
ATTR_ACTION_HOOK = "__tag_hooks"
2017-07-02 14:48:34 -07:00
ATTR_COMMAND_HOOK = "__tag_commands"
2017-05-14 12:44:14 -07:00
class hook(object):
"""
2017-07-02 14:48:34 -07:00
Decorator for calling module methods in response to IRC actions. Example:
2017-07-02 15:24:14 -07:00
.. code-block:: python
@hook("PRIVMSG")
def mymyethod(self, message):
print("IRC server sent PRIVMSG")
2017-05-14 12:44:14 -07:00
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.
2017-07-02 15:24:14 -07:00
:param args: irc protocol event to listen for. See :py:meth:`pyircbot.irccore.IRCCore.initHooks` for a complete list
:type args: str
2017-05-14 12:44:14 -07:00
"""
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
2017-07-02 14:48:34 -07:00
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):
"""
2017-07-02 15:24:14 -07:00
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
2017-07-02 14:48:34 -07:00
during module init and appropriate hooks are set up.
2017-07-02 15:24:14 -07:00
: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
2017-07-02 14:48:34 -07:00
"""
2017-07-02 15:24:14 -07:00
2017-07-02 14:48:34 -07:00
prefix = "."
2017-07-02 15:24:14 -07:00
"""
Hotkey that must appear before commands
"""
2017-07-02 14:48:34 -07:00
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):
2017-07-02 15:24:14 -07:00
"""
Internal use. Triggers the hooked function
"""
2017-07-02 14:48:34 -07:00
if len(getargspec(method).args) == 3:
2017-07-02 15:24:14 -07:00
return method(self.parsed_cmd, msg)
2017-07-02 14:48:34 -07:00
else:
2017-07-02 15:24:14 -07:00
return method(self.parsed_cmd)
2017-07-02 14:48:34 -07:00
def validate(self, msg, bot):
2017-07-02 15:24:14 -07:00
"""
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
"""
2017-07-02 14:48:34 -07:00
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