base.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. # -*- encoding: utf-8 -*-
  2. """
  3. sleekxmpp.plugins.base
  4. ~~~~~~~~~~~~~~~~~~~~~~
  5. This module provides XMPP functionality that
  6. is specific to client connections.
  7. Part of SleekXMPP: The Sleek XMPP Library
  8. :copyright: (c) 2012 Nathanael C. Fritz
  9. :license: MIT, see LICENSE for more details
  10. """
  11. import sys
  12. import copy
  13. import logging
  14. import threading
  15. if sys.version_info >= (3, 0):
  16. unicode = str
  17. log = logging.getLogger(__name__)
  18. #: Associate short string names of plugins with implementations. The
  19. #: plugin names are based on the spec used by the plugin, such as
  20. #: `'xep_0030'` for a plugin that implements XEP-0030.
  21. PLUGIN_REGISTRY = {}
  22. #: In order to do cascading plugin disabling, reverse dependencies
  23. #: must be tracked.
  24. PLUGIN_DEPENDENTS = {}
  25. #: Only allow one thread to manipulate the plugin registry at a time.
  26. REGISTRY_LOCK = threading.RLock()
  27. class PluginNotFound(Exception):
  28. """Raised if an unknown plugin is accessed."""
  29. def register_plugin(impl, name=None):
  30. """Add a new plugin implementation to the registry.
  31. :param class impl: The plugin class.
  32. The implementation class must provide a :attr:`~BasePlugin.name`
  33. value that will be used as a short name for enabling and disabling
  34. the plugin. The name should be based on the specification used by
  35. the plugin. For example, a plugin implementing XEP-0030 would be
  36. named `'xep_0030'`.
  37. """
  38. if name is None:
  39. name = impl.name
  40. with REGISTRY_LOCK:
  41. PLUGIN_REGISTRY[name] = impl
  42. if name not in PLUGIN_DEPENDENTS:
  43. PLUGIN_DEPENDENTS[name] = set()
  44. for dep in impl.dependencies:
  45. if dep not in PLUGIN_DEPENDENTS:
  46. PLUGIN_DEPENDENTS[dep] = set()
  47. PLUGIN_DEPENDENTS[dep].add(name)
  48. def load_plugin(name, module=None):
  49. """Find and import a plugin module so that it can be registered.
  50. This function is called to import plugins that have selected for
  51. enabling, but no matching registered plugin has been found.
  52. :param str name: The name of the plugin. It is expected that
  53. plugins are in packages matching their name,
  54. even though the plugin class name does not
  55. have to match.
  56. :param str module: The name of the base module to search
  57. for the plugin.
  58. """
  59. try:
  60. if not module:
  61. try:
  62. module = 'sleekxmpp.plugins.%s' % name
  63. __import__(module)
  64. mod = sys.modules[module]
  65. except ImportError:
  66. module = 'sleekxmpp.features.%s' % name
  67. __import__(module)
  68. mod = sys.modules[module]
  69. elif isinstance(module, (str, unicode)):
  70. __import__(module)
  71. mod = sys.modules[module]
  72. else:
  73. mod = module
  74. # Add older style plugins to the registry.
  75. if hasattr(mod, name):
  76. plugin = getattr(mod, name)
  77. if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
  78. plugin.name = name
  79. # Mark the plugin as an older style plugin so
  80. # we can work around dependency issues.
  81. plugin.old_style = True
  82. register_plugin(plugin, name)
  83. except ImportError:
  84. log.exception("Unable to load plugin: %s", name)
  85. class PluginManager(object):
  86. def __init__(self, xmpp, config=None):
  87. #: We will track all enabled plugins in a set so that we
  88. #: can enable plugins in batches and pull in dependencies
  89. #: without problems.
  90. self._enabled = set()
  91. #: Maintain references to active plugins.
  92. self._plugins = {}
  93. self._plugin_lock = threading.RLock()
  94. #: Globally set default plugin configuration. This will
  95. #: be used for plugins that are auto-enabled through
  96. #: dependency loading.
  97. self.config = config if config else {}
  98. self.xmpp = xmpp
  99. def register(self, plugin, enable=True):
  100. """Register a new plugin, and optionally enable it.
  101. :param class plugin: The implementation class of the plugin
  102. to register.
  103. :param bool enable: If ``True``, immediately enable the
  104. plugin after registration.
  105. """
  106. register_plugin(plugin)
  107. if enable:
  108. self.enable(plugin.name)
  109. def enable(self, name, config=None, enabled=None):
  110. """Enable a plugin, including any dependencies.
  111. :param string name: The short name of the plugin.
  112. :param dict config: Optional settings dictionary for
  113. configuring plugin behaviour.
  114. """
  115. top_level = False
  116. if enabled is None:
  117. enabled = set()
  118. with self._plugin_lock:
  119. if name not in self._enabled:
  120. enabled.add(name)
  121. self._enabled.add(name)
  122. if not self.registered(name):
  123. load_plugin(name)
  124. plugin_class = PLUGIN_REGISTRY.get(name, None)
  125. if not plugin_class:
  126. raise PluginNotFound(name)
  127. if config is None:
  128. config = self.config.get(name, None)
  129. plugin = plugin_class(self.xmpp, config)
  130. self._plugins[name] = plugin
  131. for dep in plugin.dependencies:
  132. self.enable(dep, enabled=enabled)
  133. plugin._init()
  134. if top_level:
  135. for name in enabled:
  136. if hasattr(self.plugins[name], 'old_style'):
  137. # Older style plugins require post_init()
  138. # to run just before stream processing begins,
  139. # so we don't call it here.
  140. pass
  141. self.plugins[name].post_init()
  142. def enable_all(self, names=None, config=None):
  143. """Enable all registered plugins.
  144. :param list names: A list of plugin names to enable. If
  145. none are provided, all registered plugins
  146. will be enabled.
  147. :param dict config: A dictionary mapping plugin names to
  148. configuration dictionaries, as used by
  149. :meth:`~PluginManager.enable`.
  150. """
  151. names = names if names else PLUGIN_REGISTRY.keys()
  152. if config is None:
  153. config = {}
  154. for name in names:
  155. self.enable(name, config.get(name, {}))
  156. def enabled(self, name):
  157. """Check if a plugin has been enabled.
  158. :param string name: The name of the plugin to check.
  159. :return: boolean
  160. """
  161. return name in self._enabled
  162. def registered(self, name):
  163. """Check if a plugin has been registered.
  164. :param string name: The name of the plugin to check.
  165. :return: boolean
  166. """
  167. return name in PLUGIN_REGISTRY
  168. def disable(self, name, _disabled=None):
  169. """Disable a plugin, including any dependent upon it.
  170. :param string name: The name of the plugin to disable.
  171. :param set _disabled: Private set used to track the
  172. disabled status of plugins during
  173. the cascading process.
  174. """
  175. if _disabled is None:
  176. _disabled = set()
  177. with self._plugin_lock:
  178. if name not in _disabled and name in self._enabled:
  179. _disabled.add(name)
  180. plugin = self._plugins.get(name, None)
  181. if plugin is None:
  182. raise PluginNotFound(name)
  183. for dep in PLUGIN_DEPENDENTS[name]:
  184. self.disable(dep, _disabled)
  185. plugin._end()
  186. if name in self._enabled:
  187. self._enabled.remove(name)
  188. del self._plugins[name]
  189. def __keys__(self):
  190. """Return the set of enabled plugins."""
  191. return self._plugins.keys()
  192. def __getitem__(self, name):
  193. """
  194. Allow plugins to be accessed through the manager as if
  195. it were a dictionary.
  196. """
  197. plugin = self._plugins.get(name, None)
  198. if plugin is None:
  199. raise PluginNotFound(name)
  200. return plugin
  201. def __iter__(self):
  202. """Return an iterator over the set of enabled plugins."""
  203. return self._plugins.__iter__()
  204. def __len__(self):
  205. """Return the number of enabled plugins."""
  206. return len(self._plugins)
  207. class BasePlugin(object):
  208. #: A short name for the plugin based on the implemented specification.
  209. #: For example, a plugin for XEP-0030 would use `'xep_0030'`.
  210. name = ''
  211. #: A longer name for the plugin, describing its purpose. For example,
  212. #: a plugin for XEP-0030 would use `'Service Discovery'` as its
  213. #: description value.
  214. description = ''
  215. #: Some plugins may depend on others in order to function properly.
  216. #: Any plugin names included in :attr:`~BasePlugin.dependencies` will
  217. #: be initialized as needed if this plugin is enabled.
  218. dependencies = set()
  219. #: The basic, standard configuration for the plugin, which may
  220. #: be overridden when initializing the plugin. The configuration
  221. #: fields included here may be accessed directly as attributes of
  222. #: the plugin. For example, including the configuration field 'foo'
  223. #: would mean accessing `plugin.foo` returns the current value of
  224. #: `plugin.config['foo']`.
  225. default_config = {}
  226. def __init__(self, xmpp, config=None):
  227. self.xmpp = xmpp
  228. if self.xmpp:
  229. self.api = self.xmpp.api.wrap(self.name)
  230. #: A plugin's behaviour may be configurable, in which case those
  231. #: configuration settings will be provided as a dictionary.
  232. self.config = copy.copy(self.default_config)
  233. if config:
  234. self.config.update(config)
  235. def __getattr__(self, key):
  236. """Provide direct access to configuration fields.
  237. If the standard configuration includes the option `'foo'`, then
  238. accessing `self.foo` should be the same as `self.config['foo']`.
  239. """
  240. if key in self.default_config:
  241. return self.config.get(key, None)
  242. else:
  243. return object.__getattribute__(self, key)
  244. def __setattr__(self, key, value):
  245. """Provide direct assignment to configuration fields.
  246. If the standard configuration includes the option `'foo'`, then
  247. assigning to `self.foo` should be the same as assigning to
  248. `self.config['foo']`.
  249. """
  250. if key in self.default_config:
  251. self.config[key] = value
  252. else:
  253. super(BasePlugin, self).__setattr__(key, value)
  254. def _init(self):
  255. """Initialize plugin state, such as registering event handlers.
  256. Also sets up required event handlers.
  257. """
  258. if self.xmpp is not None:
  259. self.xmpp.add_event_handler('session_bind', self.session_bind)
  260. if self.xmpp.session_bind_event.is_set():
  261. self.session_bind(self.xmpp.boundjid.full)
  262. self.plugin_init()
  263. log.debug('Loaded Plugin: %s', self.description)
  264. def _end(self):
  265. """Cleanup plugin state, and prepare for plugin removal.
  266. Also removes required event handlers.
  267. """
  268. if self.xmpp is not None:
  269. self.xmpp.del_event_handler('session_bind', self.session_bind)
  270. self.plugin_end()
  271. log.debug('Disabled Plugin: %s' % self.description)
  272. def plugin_init(self):
  273. """Initialize plugin state, such as registering event handlers."""
  274. pass
  275. def plugin_end(self):
  276. """Cleanup plugin state, and prepare for plugin removal."""
  277. pass
  278. def session_bind(self, jid):
  279. """Initialize plugin state based on the bound JID."""
  280. pass
  281. def post_init(self):
  282. """Initialize any cross-plugin state.
  283. Only needed if the plugin has circular dependencies.
  284. """
  285. pass
  286. base_plugin = BasePlugin