1 # -*- encoding: utf-8 -*-
7 This module provides XMPP functionality that
8 is specific to client connections.
10 Part of Slixmpp: The Slick XMPP Library
12 :copyright: (c) 2012 Nathanael C. Fritz
13 :license: MIT, see LICENSE for more details
22 if sys
.version_info
>= (3, 0):
26 log
= logging
.getLogger(__name__
)
29 #: Associate short string names of plugins with implementations. The
30 #: plugin names are based on the spec used by the plugin, such as
31 #: `'xep_0030'` for a plugin that implements XEP-0030.
34 #: In order to do cascading plugin disabling, reverse dependencies
36 PLUGIN_DEPENDENTS
= {}
38 #: Only allow one thread to manipulate the plugin registry at a time.
39 REGISTRY_LOCK
= threading
.RLock()
42 class PluginNotFound(Exception):
43 """Raised if an unknown plugin is accessed."""
46 def register_plugin(impl
, name
=None):
47 """Add a new plugin implementation to the registry.
49 :param class impl: The plugin class.
51 The implementation class must provide a :attr:`~BasePlugin.name`
52 value that will be used as a short name for enabling and disabling
53 the plugin. The name should be based on the specification used by
54 the plugin. For example, a plugin implementing XEP-0030 would be
60 PLUGIN_REGISTRY
[name
] = impl
61 if name
not in PLUGIN_DEPENDENTS
:
62 PLUGIN_DEPENDENTS
[name
] = set()
63 for dep
in impl
.dependencies
:
64 if dep
not in PLUGIN_DEPENDENTS
:
65 PLUGIN_DEPENDENTS
[dep
] = set()
66 PLUGIN_DEPENDENTS
[dep
].add(name
)
69 def load_plugin(name
, module
=None):
70 """Find and import a plugin module so that it can be registered.
72 This function is called to import plugins that have selected for
73 enabling, but no matching registered plugin has been found.
75 :param str name: The name of the plugin. It is expected that
76 plugins are in packages matching their name,
77 even though the plugin class name does not
79 :param str module: The name of the base module to search
85 module
= 'slixmpp.plugins.%s' % name
87 mod
= sys
.modules
[module
]
89 module
= 'slixmpp.features.%s' % name
91 mod
= sys
.modules
[module
]
92 elif isinstance(module
, (str, unicode)):
94 mod
= sys
.modules
[module
]
98 # Add older style plugins to the registry.
99 if hasattr(mod
, name
):
100 plugin
= getattr(mod
, name
)
101 if hasattr(plugin
, 'xep') or hasattr(plugin
, 'rfc'):
103 # Mark the plugin as an older style plugin so
104 # we can work around dependency issues.
105 plugin
.old_style
= True
106 register_plugin(plugin
, name
)
108 log
.exception("Unable to load plugin: %s", name
)
111 class PluginManager(object):
112 def __init__(self
, xmpp
, config
=None):
113 #: We will track all enabled plugins in a set so that we
114 #: can enable plugins in batches and pull in dependencies
116 self
._enabled
= set()
118 #: Maintain references to active plugins.
121 self
._plugin
_lock
= threading
.RLock()
123 #: Globally set default plugin configuration. This will
124 #: be used for plugins that are auto-enabled through
125 #: dependency loading.
126 self
.config
= config
if config
else {}
130 def register(self
, plugin
, enable
=True):
131 """Register a new plugin, and optionally enable it.
133 :param class plugin: The implementation class of the plugin
135 :param bool enable: If ``True``, immediately enable the
136 plugin after registration.
138 register_plugin(plugin
)
140 self
.enable(plugin
.name
)
142 def enable(self
, name
, config
=None, enabled
=None):
143 """Enable a plugin, including any dependencies.
145 :param string name: The short name of the plugin.
146 :param dict config: Optional settings dictionary for
147 configuring plugin behaviour.
153 with self
._plugin
_lock
:
154 if name
not in self
._enabled
:
156 self
._enabled
.add(name
)
157 if not self
.registered(name
):
160 plugin_class
= PLUGIN_REGISTRY
.get(name
, None)
162 raise PluginNotFound(name
)
165 config
= self
.config
.get(name
, None)
167 plugin
= plugin_class(self
.xmpp
, config
)
168 self
._plugins
[name
] = plugin
169 for dep
in plugin
.dependencies
:
170 self
.enable(dep
, enabled
=enabled
)
175 if hasattr(self
.plugins
[name
], 'old_style'):
176 # Older style plugins require post_init()
177 # to run just before stream processing begins,
178 # so we don't call it here.
180 self
.plugins
[name
].post_init()
182 def enable_all(self
, names
=None, config
=None):
183 """Enable all registered plugins.
185 :param list names: A list of plugin names to enable. If
186 none are provided, all registered plugins
188 :param dict config: A dictionary mapping plugin names to
189 configuration dictionaries, as used by
190 :meth:`~PluginManager.enable`.
192 names
= names
if names
else PLUGIN_REGISTRY
.keys()
196 self
.enable(name
, config
.get(name
, {}))
198 def enabled(self
, name
):
199 """Check if a plugin has been enabled.
201 :param string name: The name of the plugin to check.
204 return name
in self
._enabled
206 def registered(self
, name
):
207 """Check if a plugin has been registered.
209 :param string name: The name of the plugin to check.
212 return name
in PLUGIN_REGISTRY
214 def disable(self
, name
, _disabled
=None):
215 """Disable a plugin, including any dependent upon it.
217 :param string name: The name of the plugin to disable.
218 :param set _disabled: Private set used to track the
219 disabled status of plugins during
220 the cascading process.
222 if _disabled
is None:
224 with self
._plugin
_lock
:
225 if name
not in _disabled
and name
in self
._enabled
:
227 plugin
= self
._plugins
.get(name
, None)
229 raise PluginNotFound(name
)
230 for dep
in PLUGIN_DEPENDENTS
[name
]:
231 self
.disable(dep
, _disabled
)
233 if name
in self
._enabled
:
234 self
._enabled
.remove(name
)
235 del self
._plugins
[name
]
238 """Return the set of enabled plugins."""
239 return self
._plugins
.keys()
241 def __getitem__(self
, name
):
243 Allow plugins to be accessed through the manager as if
244 it were a dictionary.
246 plugin
= self
._plugins
.get(name
, None)
248 raise PluginNotFound(name
)
252 """Return an iterator over the set of enabled plugins."""
253 return self
._plugins
.__iter
__()
256 """Return the number of enabled plugins."""
257 return len(self
._plugins
)
260 class BasePlugin(object):
262 #: A short name for the plugin based on the implemented specification.
263 #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
266 #: A longer name for the plugin, describing its purpose. For example,
267 #: a plugin for XEP-0030 would use `'Service Discovery'` as its
268 #: description value.
271 #: Some plugins may depend on others in order to function properly.
272 #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
273 #: be initialized as needed if this plugin is enabled.
276 #: The basic, standard configuration for the plugin, which may
277 #: be overridden when initializing the plugin. The configuration
278 #: fields included here may be accessed directly as attributes of
279 #: the plugin. For example, including the configuration field 'foo'
280 #: would mean accessing `plugin.foo` returns the current value of
281 #: `plugin.config['foo']`.
284 def __init__(self
, xmpp
, config
=None):
287 self
.api
= self
.xmpp
.api
.wrap(self
.name
)
289 #: A plugin's behaviour may be configurable, in which case those
290 #: configuration settings will be provided as a dictionary.
291 self
.config
= copy
.copy(self
.default_config
)
293 self
.config
.update(config
)
295 def __getattr__(self
, key
):
296 """Provide direct access to configuration fields.
298 If the standard configuration includes the option `'foo'`, then
299 accessing `self.foo` should be the same as `self.config['foo']`.
301 if key
in self
.default_config
:
302 return self
.config
.get(key
, None)
304 return object.__getattribute
__(self
, key
)
306 def __setattr__(self
, key
, value
):
307 """Provide direct assignment to configuration fields.
309 If the standard configuration includes the option `'foo'`, then
310 assigning to `self.foo` should be the same as assigning to
311 `self.config['foo']`.
313 if key
in self
.default_config
:
314 self
.config
[key
] = value
316 super(BasePlugin
, self
).__setattr
__(key
, value
)
319 """Initialize plugin state, such as registering event handlers.
321 Also sets up required event handlers.
323 if self
.xmpp
is not None:
324 self
.xmpp
.add_event_handler('session_bind', self
.session_bind
)
325 if self
.xmpp
.session_bind_event
.is_set():
326 self
.session_bind(self
.xmpp
.boundjid
.full
)
328 log
.debug('Loaded Plugin: %s', self
.description
)
331 """Cleanup plugin state, and prepare for plugin removal.
333 Also removes required event handlers.
335 if self
.xmpp
is not None:
336 self
.xmpp
.del_event_handler('session_bind', self
.session_bind
)
338 log
.debug('Disabled Plugin: %s' % self
.description
)
340 def plugin_init(self
):
341 """Initialize plugin state, such as registering event handlers."""
344 def plugin_end(self
):
345 """Cleanup plugin state, and prepare for plugin removal."""
348 def session_bind(self
, jid
):
349 """Initialize plugin state based on the bound JID."""
353 """Initialize any cross-plugin state.
355 Only needed if the plugin has circular dependencies.
360 base_plugin
= BasePlugin