summaryrefslogtreecommitdiff
path: root/slixmpp/plugins/base.py
diff options
context:
space:
mode:
Diffstat (limited to 'slixmpp/plugins/base.py')
-rw-r--r--slixmpp/plugins/base.py352
1 files changed, 352 insertions, 0 deletions
diff --git a/slixmpp/plugins/base.py b/slixmpp/plugins/base.py
new file mode 100644
index 00000000..0fe083bc
--- /dev/null
+++ b/slixmpp/plugins/base.py
@@ -0,0 +1,352 @@
+# -*- encoding: utf-8 -*-
+
+"""
+ slixmpp.plugins.base
+ ~~~~~~~~~~~~~~~~~~~~~~
+
+ This module provides XMPP functionality that
+ is specific to client connections.
+
+ Part of Slixmpp: The Slick XMPP Library
+
+ :copyright: (c) 2012 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+import sys
+import copy
+import logging
+import threading
+
+
+log = logging.getLogger(__name__)
+
+
+#: Associate short string names of plugins with implementations. The
+#: plugin names are based on the spec used by the plugin, such as
+#: `'xep_0030'` for a plugin that implements XEP-0030.
+PLUGIN_REGISTRY = {}
+
+#: In order to do cascading plugin disabling, reverse dependencies
+#: must be tracked.
+PLUGIN_DEPENDENTS = {}
+
+#: Only allow one thread to manipulate the plugin registry at a time.
+REGISTRY_LOCK = threading.RLock()
+
+
+class PluginNotFound(Exception):
+ """Raised if an unknown plugin is accessed."""
+
+
+def register_plugin(impl, name=None):
+ """Add a new plugin implementation to the registry.
+
+ :param class impl: The plugin class.
+
+ The implementation class must provide a :attr:`~BasePlugin.name`
+ value that will be used as a short name for enabling and disabling
+ the plugin. The name should be based on the specification used by
+ the plugin. For example, a plugin implementing XEP-0030 would be
+ named `'xep_0030'`.
+ """
+ if name is None:
+ name = impl.name
+ with REGISTRY_LOCK:
+ PLUGIN_REGISTRY[name] = impl
+ if name not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[name] = set()
+ for dep in impl.dependencies:
+ if dep not in PLUGIN_DEPENDENTS:
+ PLUGIN_DEPENDENTS[dep] = set()
+ PLUGIN_DEPENDENTS[dep].add(name)
+
+
+def load_plugin(name, module=None):
+ """Find and import a plugin module so that it can be registered.
+
+ This function is called to import plugins that have selected for
+ enabling, but no matching registered plugin has been found.
+
+ :param str name: The name of the plugin. It is expected that
+ plugins are in packages matching their name,
+ even though the plugin class name does not
+ have to match.
+ :param str module: The name of the base module to search
+ for the plugin.
+ """
+ try:
+ if not module:
+ try:
+ module = 'slixmpp.plugins.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ except ImportError:
+ module = 'slixmpp.features.%s' % name
+ __import__(module)
+ mod = sys.modules[module]
+ elif isinstance(module, str):
+ __import__(module)
+ mod = sys.modules[module]
+ else:
+ mod = module
+
+ # Add older style plugins to the registry.
+ if hasattr(mod, name):
+ plugin = getattr(mod, name)
+ if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
+ plugin.name = name
+ # Mark the plugin as an older style plugin so
+ # we can work around dependency issues.
+ plugin.old_style = True
+ register_plugin(plugin, name)
+ except ImportError:
+ log.exception("Unable to load plugin: %s", name)
+
+
+class PluginManager(object):
+ def __init__(self, xmpp, config=None):
+ #: We will track all enabled plugins in a set so that we
+ #: can enable plugins in batches and pull in dependencies
+ #: without problems.
+ self._enabled = set()
+
+ #: Maintain references to active plugins.
+ self._plugins = {}
+
+ self._plugin_lock = threading.RLock()
+
+ #: Globally set default plugin configuration. This will
+ #: be used for plugins that are auto-enabled through
+ #: dependency loading.
+ self.config = config if config else {}
+
+ self.xmpp = xmpp
+
+ def register(self, plugin, enable=True):
+ """Register a new plugin, and optionally enable it.
+
+ :param class plugin: The implementation class of the plugin
+ to register.
+ :param bool enable: If ``True``, immediately enable the
+ plugin after registration.
+ """
+ register_plugin(plugin)
+ if enable:
+ self.enable(plugin.name)
+
+ def enable(self, name, config=None, enabled=None):
+ """Enable a plugin, including any dependencies.
+
+ :param string name: The short name of the plugin.
+ :param dict config: Optional settings dictionary for
+ configuring plugin behaviour.
+ """
+ if enabled is None:
+ enabled = set()
+
+ with self._plugin_lock:
+ if name not in self._enabled:
+ enabled.add(name)
+ self._enabled.add(name)
+ if not self.registered(name):
+ load_plugin(name)
+
+ plugin_class = PLUGIN_REGISTRY.get(name, None)
+ if not plugin_class:
+ raise PluginNotFound(name)
+
+ if config is None:
+ config = self.config.get(name, None)
+
+ plugin = plugin_class(self.xmpp, config)
+ self._plugins[name] = plugin
+ for dep in plugin.dependencies:
+ self.enable(dep, enabled=enabled)
+ plugin._init()
+
+ for name in enabled:
+ if hasattr(self._plugins[name], 'old_style'):
+ # Older style plugins require post_init()
+ # to run just before stream processing begins,
+ # so we don't call it here.
+ pass
+ else:
+ self._plugins[name].post_init()
+
+ def enable_all(self, names=None, config=None):
+ """Enable all registered plugins.
+
+ :param list names: A list of plugin names to enable. If
+ none are provided, all registered plugins
+ will be enabled.
+ :param dict config: A dictionary mapping plugin names to
+ configuration dictionaries, as used by
+ :meth:`~PluginManager.enable`.
+ """
+ names = names if names else PLUGIN_REGISTRY.keys()
+ if config is None:
+ config = {}
+ for name in names:
+ self.enable(name, config.get(name, {}))
+
+ def enabled(self, name):
+ """Check if a plugin has been enabled.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in self._enabled
+
+ def registered(self, name):
+ """Check if a plugin has been registered.
+
+ :param string name: The name of the plugin to check.
+ :return: boolean
+ """
+ return name in PLUGIN_REGISTRY
+
+ def disable(self, name, _disabled=None):
+ """Disable a plugin, including any dependent upon it.
+
+ :param string name: The name of the plugin to disable.
+ :param set _disabled: Private set used to track the
+ disabled status of plugins during
+ the cascading process.
+ """
+ if _disabled is None:
+ _disabled = set()
+ with self._plugin_lock:
+ if name not in _disabled and name in self._enabled:
+ _disabled.add(name)
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ for dep in PLUGIN_DEPENDENTS[name]:
+ self.disable(dep, _disabled)
+ plugin._end()
+ if name in self._enabled:
+ self._enabled.remove(name)
+ del self._plugins[name]
+
+ def __keys__(self):
+ """Return the set of enabled plugins."""
+ return self._plugins.keys()
+
+ def __getitem__(self, name):
+ """
+ Allow plugins to be accessed through the manager as if
+ it were a dictionary.
+ """
+ plugin = self._plugins.get(name, None)
+ if plugin is None:
+ raise PluginNotFound(name)
+ return plugin
+
+ def __iter__(self):
+ """Return an iterator over the set of enabled plugins."""
+ return self._plugins.__iter__()
+
+ def __len__(self):
+ """Return the number of enabled plugins."""
+ return len(self._plugins)
+
+
+class BasePlugin(object):
+
+ #: A short name for the plugin based on the implemented specification.
+ #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
+ name = ''
+
+ #: A longer name for the plugin, describing its purpose. For example,
+ #: a plugin for XEP-0030 would use `'Service Discovery'` as its
+ #: description value.
+ description = ''
+
+ #: Some plugins may depend on others in order to function properly.
+ #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
+ #: be initialized as needed if this plugin is enabled.
+ dependencies = set()
+
+ #: The basic, standard configuration for the plugin, which may
+ #: be overridden when initializing the plugin. The configuration
+ #: fields included here may be accessed directly as attributes of
+ #: the plugin. For example, including the configuration field 'foo'
+ #: would mean accessing `plugin.foo` returns the current value of
+ #: `plugin.config['foo']`.
+ default_config = {}
+
+ def __init__(self, xmpp, config=None):
+ self.xmpp = xmpp
+ if self.xmpp:
+ self.api = self.xmpp.api.wrap(self.name)
+
+ #: A plugin's behaviour may be configurable, in which case those
+ #: configuration settings will be provided as a dictionary.
+ self.config = copy.copy(self.default_config)
+ if config:
+ self.config.update(config)
+
+ def __getattr__(self, key):
+ """Provide direct access to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ accessing `self.foo` should be the same as `self.config['foo']`.
+ """
+ if key in self.default_config:
+ return self.config.get(key, None)
+ else:
+ return object.__getattribute__(self, key)
+
+ def __setattr__(self, key, value):
+ """Provide direct assignment to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ assigning to `self.foo` should be the same as assigning to
+ `self.config['foo']`.
+ """
+ if key in self.default_config:
+ self.config[key] = value
+ else:
+ super(BasePlugin, self).__setattr__(key, value)
+
+ def _init(self):
+ """Initialize plugin state, such as registering event handlers.
+
+ Also sets up required event handlers.
+ """
+ if self.xmpp is not None:
+ self.xmpp.add_event_handler('session_bind', self.session_bind)
+ if self.xmpp.session_bind_event.is_set():
+ self.session_bind(self.xmpp.boundjid.full)
+ self.plugin_init()
+ log.debug('Loaded Plugin: %s', self.description)
+
+ def _end(self):
+ """Cleanup plugin state, and prepare for plugin removal.
+
+ Also removes required event handlers.
+ """
+ if self.xmpp is not None:
+ self.xmpp.del_event_handler('session_bind', self.session_bind)
+ self.plugin_end()
+ log.debug('Disabled Plugin: %s' % self.description)
+
+ def plugin_init(self):
+ """Initialize plugin state, such as registering event handlers."""
+ pass
+
+ def plugin_end(self):
+ """Cleanup plugin state, and prepare for plugin removal."""
+ pass
+
+ def session_bind(self, jid):
+ """Initialize plugin state based on the bound JID."""
+ pass
+
+ def post_init(self):
+ """Initialize any cross-plugin state.
+
+ Only needed if the plugin has circular dependencies.
+ """
+ pass