summaryrefslogtreecommitdiff
path: root/sleekxmpp/plugins/xep_0030
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/plugins/xep_0030')
-rw-r--r--sleekxmpp/plugins/xep_0030/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py800
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/info.py276
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py136
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py441
6 files changed, 1675 insertions, 0 deletions
diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py
new file mode 100644
index 00000000..2e183852
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0030 import stanza
+from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
+from sleekxmpp.plugins.xep_0030.static import StaticDisco
+from sleekxmpp.plugins.xep_0030.disco import xep_0030
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
new file mode 100644
index 00000000..2267401e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -0,0 +1,800 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0030(base_plugin):
+
+ """
+ XEP-0030: Service Discovery
+
+ Service discovery in XMPP allows entities to discover information about
+ other agents in the network, such as the feature sets supported by a
+ client, or signposts to other, related entities.
+
+ Also see <http://www.xmpp.org/extensions/xep-0030.html>.
+
+ The XEP-0030 plugin works using a hierarchy of dynamic
+ node handlers, ranging from global handlers to specific
+ JID+node handlers. The default set of handlers operate
+ in a static manner, storing disco information in memory.
+ However, custom handlers may use any available backend
+ storage mechanism desired, such as SQLite or Redis.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Stream Handlers:
+ Disco Info -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#info.
+ Disco Items -- Any Iq stanze that includes a query with the
+ namespace http://jabber.org/protocol/disco#items.
+
+ Events:
+ disco_info -- Received a disco#info Iq query result.
+ disco_items -- Received a disco#items Iq query result.
+ disco_info_query -- Received a disco#info Iq query request.
+ disco_items_query -- Received a disco#items Iq query request.
+
+ Attributes:
+ stanza -- A reference to the module containing the
+ stanza classes provided by this plugin.
+ static -- Object containing the default set of
+ static node handlers.
+ default_handlers -- A dictionary mapping operations to the default
+ global handler (by default, the static handlers).
+ xmpp -- The main SleekXMPP object.
+
+ Methods:
+ set_node_handler -- Assign a handler to a JID/node combination.
+ del_node_handler -- Remove a handler from a JID/node combination.
+ get_info -- Retrieve disco#info data, locally or remote.
+ get_items -- Retrieve disco#items data, locally or remote.
+ set_identities --
+ set_features --
+ set_items --
+ del_items --
+ del_identity --
+ del_feature --
+ del_item --
+ add_identity --
+ add_feature --
+ add_item --
+ """
+
+ def plugin_init(self):
+ """
+ Start the XEP-0030 plugin.
+ """
+ self.xep = '0030'
+ self.description = 'Service Discovery'
+ self.stanza = sleekxmpp.plugins.xep_0030.stanza
+
+ self.xmpp.register_handler(
+ Callback('Disco Info',
+ StanzaPath('iq/disco_info'),
+ self._handle_disco_info))
+
+ self.xmpp.register_handler(
+ Callback('Disco Items',
+ StanzaPath('iq/disco_items'),
+ self._handle_disco_items))
+
+ register_stanza_plugin(Iq, DiscoInfo)
+ register_stanza_plugin(Iq, DiscoItems)
+
+ self.static = StaticDisco(self.xmpp, self)
+
+ self.use_cache = self.config.get('use_cache', True)
+ self.wrap_results = self.config.get('wrap_results', False)
+
+ self._disco_ops = [
+ 'get_info', 'set_info', 'set_identities', 'set_features',
+ 'get_items', 'set_items', 'del_items', 'add_identity',
+ 'del_identity', 'add_feature', 'del_feature', 'add_item',
+ 'del_item', 'del_identities', 'del_features', 'cache_info',
+ 'get_cached_info', 'supports', 'has_identity']
+
+ self.default_handlers = {}
+ self._handlers = {}
+ for op in self._disco_ops:
+ self._add_disco_op(op, getattr(self.static, op))
+
+ def post_init(self):
+ """Handle cross-plugin dependencies."""
+ base_plugin.post_init(self)
+ if 'xep_0059' in self.xmpp.plugin:
+ register_stanza_plugin(DiscoItems,
+ self.xmpp['xep_0059'].stanza.Set)
+
+ def _add_disco_op(self, op, default_handler):
+ self.default_handlers[op] = default_handler
+ self._handlers[op] = {'global': default_handler,
+ 'jid': {},
+ 'node': {}}
+
+ def set_node_handler(self, htype, jid=None, node=None, handler=None):
+ """
+ Add a node handler for the given hierarchy level and
+ handler type.
+
+ Node handlers are ordered in a hierarchy where the
+ most specific handler is executed. Thus, a fallback,
+ global handler can be used for the majority of cases
+ with a few node specific handler that override the
+ global behavior.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Handler types:
+ get_info
+ get_items
+ set_identities
+ set_features
+ set_items
+ del_items
+ del_identities
+ del_identity
+ del_feature
+ del_features
+ del_item
+ add_identity
+ add_feature
+ add_item
+
+ Arguments:
+ htype -- The operation provided by the handler.
+ jid -- The JID the handler applies to. May be narrowed
+ further if a node is given.
+ node -- The particular node the handler is for. If no JID
+ is given, then the self.xmpp.boundjid.full is
+ assumed.
+ handler -- The handler function to use.
+ """
+ if htype not in self._disco_ops:
+ return
+ if jid is None and node is None:
+ self._handlers[htype]['global'] = handler
+ elif node is None:
+ self._handlers[htype]['jid'][jid] = handler
+ elif jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.boundjid.full
+ else:
+ jid = self.xmpp.boundjid.bare
+ self._handlers[htype]['node'][(jid, node)] = handler
+ else:
+ self._handlers[htype]['node'][(jid, node)] = handler
+
+ def del_node_handler(self, htype, jid, node):
+ """
+ Remove a handler type for a JID and node combination.
+
+ The next handler in the hierarchy will be used if one
+ exists. If removing the global handler, make sure that
+ other handlers exist to process existing nodes.
+
+ Node handler hierarchy:
+ JID | Node | Level
+ ---------------------
+ None | None | Global
+ Given | None | All nodes for the JID
+ None | Given | Node on self.xmpp.boundjid
+ Given | Given | A single node
+
+ Arguments:
+ htype -- The type of handler to remove.
+ jid -- The JID from which to remove the handler.
+ node -- The node from which to remove the handler.
+ """
+ self.set_node_handler(htype, jid, node, None)
+
+ def restore_defaults(self, jid=None, node=None, handlers=None):
+ """
+ Change all or some of a node's handlers to the default
+ handlers. Useful for manually overriding the contents
+ of a node that would otherwise be handled by a JID level
+ or global level dynamic handler.
+
+ The default is to use the built-in static handlers, but that
+ may be changed by modifying self.default_handlers.
+
+ Arguments:
+ jid -- The JID owning the node to modify.
+ node -- The node to change to using static handlers.
+ handlers -- Optional list of handlers to change to the
+ default version. If provided, only these
+ handlers will be changed. Otherwise, all
+ handlers will use the default version.
+ """
+ if handlers is None:
+ handlers = self._disco_ops
+ for op in handlers:
+ self.del_node_handler(op, jid, node)
+ self.set_node_handler(op, jid, node, self.default_handlers[op])
+
+ def supports(self, jid=None, node=None, feature=None, local=False,
+ cached=True, ifrom=None):
+ """
+ Check if a JID supports a given feature.
+
+ Return values:
+ True -- The feature is supported
+ False -- The feature is not listed as supported
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ feature -- The name of the feature to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'feature': feature,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('supports', jid, node, ifrom, data)
+
+ def has_identity(self, jid=None, node=None, category=None, itype=None,
+ lang=None, local=False, cached=True, ifrom=None):
+ """
+ Check if a JID provides a given identity.
+
+ Return values:
+ True -- The identity is provided
+ False -- The identity is not listed
+ None -- Nothing could be found due to a timeout
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ """
+ data = {'category': category,
+ 'itype': itype,
+ 'lang': lang,
+ 'local': local,
+ 'cached': cached}
+ return self._run_node_handler('has_identity', jid, node, ifrom, data)
+
+ def get_info(self, jid=None, node=None, local=False,
+ cached=None, **kwargs):
+ """
+ Retrieve the disco#info results from a given JID/node combination.
+
+ Info may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the information should be gathered
+ by executing the local node handlers, or if a disco#info stanza
+ must be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoInfo
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely. The
+ timeout value is only used when block=True.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+
+ if local or jid in (None, ''):
+ log.debug("Looking up local disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self._run_node_handler('get_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ info = self._fix_default_info(info)
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ if cached:
+ log.debug("Looking up cached disco#info data " + \
+ "for %s, node %s.", jid, node)
+ info = self._run_node_handler('get_cached_info',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ if info is not None:
+ return self._wrap(kwargs.get('ifrom', None), jid, info)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_info']['node'] = node if node else ''
+ return iq.send(timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None))
+
+ def set_info(self, jid=None, node=None, info=None):
+ """
+ Set the disco#info data for a JID/node based on an existing
+ disco#info stanza.
+ """
+ if isinstance(info, Iq):
+ info = info['disco_info']
+ self._run_node_handler('set_info', jid, node, None, info)
+
+ def get_items(self, jid=None, node=None, local=False, **kwargs):
+ """
+ Retrieve the disco#items results from a given JID/node combination.
+
+ Items may be retrieved from both local resources and remote agents;
+ the local parameter indicates if the items should be gathered by
+ executing the local node handlers, or if a disco#items stanza must
+ be generated and sent.
+
+ If requesting items from a local JID/node, then only a DiscoItems
+ stanza will be returned. Otherwise, an Iq stanza will be returned.
+
+ Arguments:
+ jid -- Request info from this JID.
+ node -- The particular node to query.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the items.
+ ifrom -- Specifiy the sender's JID.
+ block -- If true, block and wait for the stanzas' reply.
+ timeout -- The time in seconds to block while waiting for
+ a reply. If None, then wait indefinitely.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ iterator -- If True, return a result set iterator using
+ the XEP-0059 plugin, if the plugin is loaded.
+ Otherwise the parameter is ignored.
+ """
+ if local or jid is None:
+ items = self._run_node_handler('get_items',
+ jid, node, kwargs.get('ifrom', None), kwargs)
+ return self._wrap(kwargs.get('ifrom', None), jid, items)
+
+ iq = self.xmpp.Iq()
+ # Check dfrom parameter for backwards compatibility
+ iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
+ iq['to'] = jid
+ iq['type'] = 'get'
+ iq['disco_items']['node'] = node if node else ''
+ if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
+ return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
+ else:
+ return iq.send(timeout=kwargs.get('timeout', None),
+ block=kwargs.get('block', True),
+ callback=kwargs.get('callback', None))
+
+ def set_items(self, jid=None, node=None, **kwargs):
+ """
+ Set or replace all items for the specified JID/node combination.
+
+ The given items must be in a list or set where each item is a
+ tuple of the form: (jid, node, name).
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ items -- A series of items in tuple format.
+ """
+ self._run_node_handler('set_items', jid, node, None, kwargs)
+
+ def del_items(self, jid=None, node=None, **kwargs):
+ """
+ Remove all items from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- Optional node to modify.
+ """
+ self._run_node_handler('del_items', jid, node, None, kwargs)
+
+ def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
+ """
+ Add a new item element to the given JID/node combination.
+
+ Each item is required to have a JID, but may also specify
+ a node value to reference non-addressable entities.
+
+ Arguments:
+ jid -- The JID for the item.
+ name -- Optional name for the item.
+ node -- The node to modify.
+ subnode -- Optional node for the item.
+ ijid -- The JID to modify.
+ """
+ if not jid:
+ jid = self.xmpp.boundjid.full
+ kwargs = {'ijid': jid,
+ 'name': name,
+ 'inode': subnode}
+ self._run_node_handler('add_item', ijid, node, None, kwargs)
+
+ def del_item(self, jid=None, node=None, **kwargs):
+ """
+ Remove a single item from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ ijid -- The item's JID.
+ inode -- The item's node.
+ """
+ self._run_node_handler('del_item', jid, node, None, kwargs)
+
+ def add_identity(self, category='', itype='', name='',
+ node=None, jid=None, lang=None):
+ """
+ Add a new identity to the given JID/node combination.
+
+ Each identity must be unique in terms of all four identity
+ components: category, type, name, and language.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the
+ names are different. A category and type is always required.
+
+ Arguments:
+ category -- The identity's category.
+ itype -- The identity's type.
+ name -- Optional name for the identity.
+ lang -- Optional two-letter language code.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'category': category,
+ 'itype': itype,
+ 'name': name,
+ 'lang': lang}
+ self._run_node_handler('add_identity', jid, node, None, kwargs)
+
+ def add_feature(self, feature, node=None, jid=None):
+ """
+ Add a feature to a JID/node combination.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ node -- The node to modify.
+ jid -- The JID to modify.
+ """
+ kwargs = {'feature': feature}
+ self._run_node_handler('add_feature', jid, node, None, kwargs)
+
+ def del_identity(self, jid=None, node=None, **kwargs):
+ """
+ Remove an identity from the given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ category -- The identity's category.
+ itype -- The identity's type value.
+ name -- Optional, human readable name for the identity.
+ lang -- Optional, the identity's xml:lang value.
+ """
+ self._run_node_handler('del_identity', jid, node, None, kwargs)
+
+ def del_feature(self, jid=None, node=None, **kwargs):
+ """
+ Remove a feature from a given JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ feature -- The feature's namespace.
+ """
+ self._run_node_handler('del_feature', jid, node, None, kwargs)
+
+ def set_identities(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace all identities for the given JID/node combination.
+
+ The identities must be in a set where each identity is a tuple
+ of the form: (category, type, lang, name)
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ identities -- A set of identities in tuple form.
+ lang -- Optional, xml:lang value.
+ """
+ self._run_node_handler('set_identities', jid, node, None, kwargs)
+
+ def del_identities(self, jid=None, node=None, **kwargs):
+ """
+ Remove all identities for a JID/node combination.
+
+ If a language is specified, only identities using that
+ language will be removed.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ lang -- Optional. If given, only remove identities
+ using this xml:lang value.
+ """
+ self._run_node_handler('del_identities', jid, node, None, kwargs)
+
+ def set_features(self, jid=None, node=None, **kwargs):
+ """
+ Add or replace the set of supported features
+ for a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ features -- The new set of supported features.
+ """
+ self._run_node_handler('set_features', jid, node, None, kwargs)
+
+ def del_features(self, jid=None, node=None, **kwargs):
+ """
+ Remove all features from a JID/node combination.
+
+ Arguments:
+ jid -- The JID to modify.
+ node -- The node to modify.
+ """
+ self._run_node_handler('del_features', jid, node, None, kwargs)
+
+ def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
+ """
+ Execute the most specific node handler for the given
+ JID/node combination.
+
+ Arguments:
+ htype -- The handler type to execute.
+ jid -- The JID requested.
+ node -- The node requested.
+ data -- Optional, custom data to pass to the handler.
+ """
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if jid in (None, ''):
+ if self.xmpp.is_component:
+ jid = self.xmpp.boundjid.full
+ else:
+ jid = self.xmpp.boundjid.bare
+ if node is None:
+ node = ''
+
+ try:
+ args = (jid, node, ifrom, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
+ except TypeError:
+ # To preserve backward compatibility, drop the ifrom parameter
+ # for existing handlers that don't understand it.
+ args = (jid, node, data)
+ if self._handlers[htype]['node'].get((jid, node), False):
+ return self._handlers[htype]['node'][(jid, node)](*args)
+ elif self._handlers[htype]['jid'].get(jid, False):
+ return self._handlers[htype]['jid'][jid](*args)
+ elif self._handlers[htype]['global']:
+ return self._handlers[htype]['global'](*args)
+ else:
+ return None
+
+ def _handle_disco_info(self, iq):
+ """
+ Process an incoming disco#info stanza. If it is a get
+ request, find and return the appropriate identities
+ and features. If it is an info result, fire the
+ disco_info event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco info query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ jid = iq['to'].full
+ else:
+ jid = iq['to'].bare
+ info = self._run_node_handler('get_info',
+ jid,
+ iq['disco_info']['node'],
+ iq['from'],
+ iq)
+ if isinstance(info, Iq):
+ info.send()
+ else:
+ iq.reply()
+ if info:
+ info = self._fix_default_info(info)
+ iq.set_payload(info.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco info result from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.use_cache:
+ log.debug("Caching disco info result from " \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ ito = iq['to'].full
+ else:
+ ito = None
+ self._run_node_handler('cache_info',
+ iq['from'].full,
+ iq['disco_info']['node'],
+ ito,
+ iq)
+ self.xmpp.event('disco_info', iq)
+
+ def _handle_disco_items(self, iq):
+ """
+ Process an incoming disco#items stanza. If it is a get
+ request, find and return the appropriate items. If it
+ is an items result, fire the disco_items event.
+
+ Arguments:
+ iq -- The incoming disco#items stanza.
+ """
+ if iq['type'] == 'get':
+ log.debug("Received disco items query from " + \
+ "<%s> to <%s>.", iq['from'], iq['to'])
+ if self.xmpp.is_component:
+ jid = iq['to'].full
+ else:
+ jid = iq['to'].bare
+ items = self._run_node_handler('get_items',
+ jid,
+ iq['disco_items']['node'],
+ iq['from'].full,
+ iq)
+ if isinstance(items, Iq):
+ items.send()
+ else:
+ iq.reply()
+ if items:
+ iq.set_payload(items.xml)
+ iq.send()
+ elif iq['type'] == 'result':
+ log.debug("Received disco items result from " + \
+ "%s to %s.", iq['from'], iq['to'])
+ self.xmpp.event('disco_items', iq)
+
+ def _fix_default_info(self, info):
+ """
+ Disco#info results for a JID are required to include at least
+ one identity and feature. As a default, if no other identity is
+ provided, SleekXMPP will use either the generic component or the
+ bot client identity. A the standard disco#info feature will also be
+ added if no features are provided.
+
+ Arguments:
+ info -- The disco#info quest (not the full Iq stanza) to modify.
+ """
+ result = info
+ if isinstance(info, Iq):
+ info = iq['disco_info']
+ if not info['node']:
+ if not info['identities']:
+ if self.xmpp.is_component:
+ log.debug("No identity found for this entity. " + \
+ "Using default component identity.")
+ info.add_identity('component', 'generic')
+ else:
+ log.debug("No identity found for this entity. " + \
+ "Using default client identity.")
+ info.add_identity('client', 'bot')
+ if not info['features']:
+ log.debug("No features found for this entity. " + \
+ "Using default disco#info feature.")
+ info.add_feature(info.namespace)
+ return result
+
+ def _wrap(self, ito, ifrom, payload, force=False):
+ """
+ Ensure that results are wrapped in an Iq stanza
+ if self.wrap_results has been set to True.
+
+ Arguments:
+ ito -- The JID to use as the 'to' value
+ ifrom -- The JID to use as the 'from' value
+ payload -- The disco data to wrap
+ force -- Force wrapping, regardless of self.wrap_results
+ """
+ if (force or self.wrap_results) and not isinstance(payload, Iq):
+ iq = self.xmpp.Iq()
+ # Since we're simulating a result, we have to treat
+ # the 'from' and 'to' values opposite the normal way.
+ iq['to'] = self.xmpp.boundjid if ito is None else ito
+ iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
+ iq['type'] = 'result'
+ iq.append(payload)
+ return iq
+ return payload
+
+
+# Retain some backwards compatibility
+xep_0030.getInfo = xep_0030.get_info
+xep_0030.getItems = xep_0030.get_items
+xep_0030.make_static = xep_0030.restore_defaults
diff --git a/sleekxmpp/plugins/xep_0030/stanza/__init__.py b/sleekxmpp/plugins/xep_0030/stanza/__init__.py
new file mode 100644
index 00000000..0d97cf3d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo
+from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems
diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py
new file mode 100644
index 00000000..25d1d07f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/info.py
@@ -0,0 +1,276 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class DiscoInfo(ElementBase):
+
+ """
+ XMPP allows for users and agents to find the identities and features
+ supported by other entities in the XMPP network through service discovery,
+ or "disco". In particular, the "disco#info" query type for <iq> stanzas is
+ used to request the list of identities and features offered by a JID.
+
+ An identity is a combination of a category and type, such as the 'client'
+ category with a type of 'pc' to indicate the agent is a human operated
+ client with a GUI, or a category of 'gateway' with a type of 'aim' to
+ identify the agent as a gateway for the legacy AIM protocol. See
+ <http://xmpp.org/registrar/disco-categories.html> for a full list of
+ accepted category and type combinations.
+
+ Features are simply a set of the namespaces that identify the supported
+ features. For example, a client that supports service discovery will
+ include the feature 'http://jabber.org/protocol/disco#info'.
+
+ Since clients and components may operate in several roles at once, identity
+ and feature information may be grouped into "nodes". If one were to write
+ all of the identities and features used by a client, then node names would
+ be like section headings.
+
+ Example disco#info stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#info">
+ <identity category="client" type="bot" name="SleekXMPP Bot" />
+ <feature var="http://jabber.org/protocol/disco#info" />
+ <feature var="jabber:x:data" />
+ <feature var="urn:xmpp:ping" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ identities -- A set of 4-tuples, where each tuple contains
+ the category, type, xml:lang, and name
+ of an identity.
+ features -- A set of namespaces for features.
+
+ Methods:
+ add_identity -- Add a new, single identity.
+ del_identity -- Remove a single identity.
+ get_identities -- Return all identities in tuple form.
+ set_identities -- Use multiple identities, each given in tuple form.
+ del_identities -- Remove all identities.
+ add_feature -- Add a single feature.
+ del_feature -- Remove a single feature.
+ get_features -- Return a list of all features.
+ set_features -- Use a given list of features.
+ del_features -- Remove all features.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#info'
+ plugin_attrib = 'disco_info'
+ interfaces = set(('node', 'features', 'identities'))
+ lang_interfaces = set(('identities',))
+
+ # Cache identities and features
+ _identities = set()
+ _features = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches identity and feature information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+
+ self._identities = set([id[0:3] for id in self['identities']])
+ self._features = self['features']
+
+ def add_identity(self, category, itype, name=None, lang=None):
+ """
+ Add a new identity element. Each identity must be unique
+ in terms of all four identity components.
+
+ Multiple, identical category/type pairs are allowed only
+ if the xml:lang values are different. Likewise, multiple
+ category/type/xml:lang pairs are allowed so long as the names
+ are different. In any case, a category and type are required.
+
+ Arguments:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity not in self._identities:
+ self._identities.add(identity)
+ id_xml = ET.Element('{%s}identity' % self.namespace)
+ id_xml.attrib['category'] = category
+ id_xml.attrib['type'] = itype
+ if lang:
+ id_xml.attrib['{%s}lang' % self.xml_ns] = lang
+ if name:
+ id_xml.attrib['name'] = name
+ self.xml.append(id_xml)
+ return True
+ return False
+
+ def del_identity(self, category, itype, name=None, lang=None):
+ """
+ Remove a given identity.
+
+ Arguments:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ identity = (category, itype, lang)
+ if identity in self._identities:
+ self._identities.remove(identity)
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
+ if id == identity:
+ self.xml.remove(id_xml)
+ return True
+ return False
+
+ def get_identities(self, lang=None, dedupe=True):
+ """
+ Return a set of all identities in tuple form as so:
+ (category, type, lang, name)
+
+ If a language was specified, only return identities using
+ that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ dedupe -- If True, de-duplicate identities, otherwise
+ return a list of all identities.
+ """
+ if dedupe:
+ identities = set()
+ else:
+ identities = []
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
+ if lang is None or xml_lang == lang:
+ id = (id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
+ id_xml.attrib.get('name', None))
+ if dedupe:
+ identities.add(id)
+ else:
+ identities.append(id)
+ return identities
+
+ def set_identities(self, identities, lang=None):
+ """
+ Add or replace all identities. The identities must be a in set
+ where each identity is a tuple of the form:
+ (category, type, lang, name)
+
+ If a language is specifified, any identities using that language
+ will be removed to be replaced with the given identities.
+
+ NOTE: An identity's language will not be changed regardless of
+ the value of lang.
+
+ Arguments:
+ identities -- A set of identities in tuple form.
+ lang -- Optional, standard xml:lang value.
+ """
+ self.del_identities(lang)
+ for identity in identities:
+ category, itype, lang, name = identity
+ self.add_identity(category, itype, name, lang)
+
+ def del_identities(self, lang=None):
+ """
+ Remove all identities. If a language was specified, only
+ remove identities using that language.
+
+ Arguments:
+ lang -- Optional, standard xml:lang value.
+ """
+ for id_xml in self.findall('{%s}identity' % self.namespace):
+ if lang is None:
+ self.xml.remove(id_xml)
+ elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
+ self._identities.remove((
+ id_xml.attrib['category'],
+ id_xml.attrib['type'],
+ id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
+ self.xml.remove(id_xml)
+
+ def add_feature(self, feature):
+ """
+ Add a single, new feature.
+
+ Arguments:
+ feature -- The namespace of the supported feature.
+ """
+ if feature not in self._features:
+ self._features.add(feature)
+ feature_xml = ET.Element('{%s}feature' % self.namespace)
+ feature_xml.attrib['var'] = feature
+ self.xml.append(feature_xml)
+ return True
+ return False
+
+ def del_feature(self, feature):
+ """
+ Remove a single feature.
+
+ Arguments:
+ feature -- The namespace of the removed feature.
+ """
+ if feature in self._features:
+ self._features.remove(feature)
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if feature_xml.attrib['var'] == feature:
+ self.xml.remove(feature_xml)
+ return True
+ return False
+
+ def get_features(self, dedupe=True):
+ """Return the set of all supported features."""
+ if dedupe:
+ features = set()
+ else:
+ features = []
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ if dedupe:
+ features.add(feature_xml.attrib['var'])
+ else:
+ features.append(feature_xml.attrib['var'])
+ return features
+
+ def set_features(self, features):
+ """
+ Add or replace the set of supported features.
+
+ Arguments:
+ features -- The new set of supported features.
+ """
+ self.del_features()
+ for feature in features:
+ self.add_feature(feature)
+
+ def del_features(self):
+ """Remove all features."""
+ self._features = set()
+ for feature_xml in self.findall('{%s}feature' % self.namespace):
+ self.xml.remove(feature_xml)
diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py
new file mode 100644
index 00000000..a1fb819c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/stanza/items.py
@@ -0,0 +1,136 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class DiscoItems(ElementBase):
+
+ """
+ Example disco#items stanzas:
+ <iq type="get">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq type="result">
+ <query xmlns="http://jabber.org/protocol/disco#items">
+ <item jid="chat.example.com"
+ node="xmppdev"
+ name="XMPP Dev" />
+ <item jid="chat.example.com"
+ node="sleekdev"
+ name="SleekXMPP Dev" />
+ </query>
+ </iq>
+
+ Stanza Interface:
+ node -- The name of the node to either
+ query or return info from.
+ items -- A list of 3-tuples, where each tuple contains
+ the JID, node, and name of an item.
+
+ Methods:
+ add_item -- Add a single new item.
+ del_item -- Remove a single item.
+ get_items -- Return all items.
+ set_items -- Set or replace all items.
+ del_items -- Remove all items.
+ """
+
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/disco#items'
+ plugin_attrib = 'disco_items'
+ interfaces = set(('node', 'items'))
+
+ # Cache items
+ _items = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._items = set([item[0:2] for item in self['items']])
+
+ def add_item(self, jid, node=None, name=None):
+ """
+ Add a new item element. Each item is required to have a
+ JID, but may also specify a node value to reference
+ non-addressable entitities.
+
+ Arguments:
+ jid -- The JID for the item.
+ node -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ if (jid, node) not in self._items:
+ self._items.add((jid, node))
+ item_xml = ET.Element('{%s}item' % self.namespace)
+ item_xml.attrib['jid'] = jid
+ if name:
+ item_xml.attrib['name'] = name
+ if node:
+ item_xml.attrib['node'] = node
+ self.xml.append(item_xml)
+ return True
+ return False
+
+ def del_item(self, jid, node=None):
+ """
+ Remove a single item.
+
+ Arguments:
+ jid -- JID of the item to remove.
+ node -- Optional extra identifying information.
+ """
+ if (jid, node) in self._items:
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ item = (item_xml.attrib['jid'],
+ item_xml.attrib.get('node', None))
+ if item == (jid, node):
+ self.xml.remove(item_xml)
+ return True
+ return False
+
+ def get_items(self):
+ """Return all items."""
+ items = set()
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ item = (item_xml.attrib['jid'],
+ item_xml.attrib.get('node'),
+ item_xml.attrib.get('name'))
+ items.add(item)
+ return items
+
+ def set_items(self, items):
+ """
+ Set or replace all items. The given items must be in a
+ list or set where each item is a tuple of the form:
+ (jid, node, name)
+
+ Arguments:
+ items -- A series of items in tuple format.
+ """
+ self.del_items()
+ for item in items:
+ jid, node, name = item
+ self.add_item(jid, node, name)
+
+ def del_items(self):
+ """Remove all items."""
+ self._items = set()
+ for item_xml in self.findall('{%s}item' % self.namespace):
+ self.xml.remove(item_xml)
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
new file mode 100644
index 00000000..e0ac29c6
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030/static.py
@@ -0,0 +1,441 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+
+import sleekxmpp
+from sleekxmpp import Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
+
+
+log = logging.getLogger(__name__)
+
+
+class StaticDisco(object):
+
+ """
+ While components will likely require fully dynamic handling
+ of service discovery information, most clients and simple bots
+ only need to manage a few disco nodes that will remain mostly
+ static.
+
+ StaticDisco provides a set of node handlers that will store
+ static sets of disco info and items in memory.
+
+ Attributes:
+ nodes -- A dictionary mapping (JID, node) tuples to a dict
+ containing a disco#info and a disco#items stanza.
+ xmpp -- The main SleekXMPP object.
+ """
+
+ def __init__(self, xmpp, disco):
+ """
+ Create a static disco interface. Sets of disco#info and
+ disco#items are maintained for every given JID and node
+ combination. These stanzas are used to store disco
+ information in memory without any additional processing.
+
+ Arguments:
+ xmpp -- The main SleekXMPP object.
+ """
+ self.nodes = {}
+ self.xmpp = xmpp
+ self.disco = disco
+ self.lock = threading.RLock()
+
+ def add_node(self, jid=None, node=None, ifrom=None):
+ """
+ Create a new set of stanzas for the provided
+ JID and node combination.
+
+ Arguments:
+ jid -- The JID that will own the new stanzas.
+ node -- The node that will own the new stanzas.
+ """
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
+ 'items': DiscoItems()}
+ self.nodes[(jid, node, ifrom)]['info']['node'] = node
+ self.nodes[(jid, node, ifrom)]['items']['node'] = node
+
+ def get_node(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ self.add_node(jid, node, ifrom)
+ return self.nodes[(jid, node, ifrom)]
+
+ def node_exists(self, jid=None, node=None, ifrom=None):
+ with self.lock:
+ if jid is None:
+ jid = self.xmpp.boundjid.full
+ if node is None:
+ node = ''
+ if ifrom is None:
+ ifrom = ''
+ if isinstance(ifrom, JID):
+ ifrom = ifrom.full
+ if (jid, node, ifrom) not in self.nodes:
+ return False
+ return True
+
+ # =================================================================
+ # Node Handlers
+ #
+ # Each handler accepts four arguments: jid, node, ifrom, and data.
+ # The jid and node parameters together determine the set of info
+ # and items stanzas that will be retrieved or added. Additionally,
+ # the ifrom value allows for cached results when results vary based
+ # on the requester's JID. The data parameter is a dictionary with
+ # additional parameters that will be passed to other calls.
+ #
+ # This implementation does not allow different responses based on
+ # the requester's JID, except for cached results. To do that,
+ # register a custom node handler.
+
+ def supports(self, jid, node, ifrom, data):
+ """
+ Check if a JID supports a given feature.
+
+ The data parameter may provide:
+ feature -- The feature to check for support.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ feature = data.get('feature', None)
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if not feature:
+ return False
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ features = info['disco_info']['features']
+ return feature in features
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+ def has_identity(self, jid, node, ifrom, data):
+ """
+ Check if a JID has a given identity.
+
+ The data parameter may provide:
+ category -- The category of the identity to check.
+ itype -- The type of the identity to check.
+ lang -- The language of the identity to check.
+ local -- If true, then the query is for a JID/node
+ combination handled by this Sleek instance and
+ no stanzas need to be sent.
+ Otherwise, a disco stanza must be sent to the
+ remove JID to retrieve the info.
+ cached -- If true, then look for the disco info data from
+ the local cache system. If no results are found,
+ send the query as usual. The self.use_cache
+ setting must be set to true for this option to
+ be useful. If set to false, then the cache will
+ be skipped, even if a result has already been
+ cached. Defaults to false.
+ """
+ identity = (data.get('category', None),
+ data.get('itype', None),
+ data.get('lang', None))
+
+ data = {'local': data.get('local', False),
+ 'cached': data.get('cached', True)}
+
+ if node in (None, ''):
+ info = self.caps.get_caps(jid)
+ if info and identity in info['identities']:
+ return True
+
+ try:
+ info = self.disco.get_info(jid=jid, node=node,
+ ifrom=ifrom, **data)
+ info = self.disco._wrap(ifrom, jid, info, True)
+ trunc = lambda i: (i[0], i[1], i[2])
+ return identity in map(trunc, info['disco_info']['identities'])
+ except IqError:
+ return False
+ except IqTimeout:
+ return None
+
+
+ def get_info(self, jid, node, ifrom, data):
+ """
+ Return the stored info data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['info']
+
+ def set_info(self, jid, node, ifrom, data):
+ """
+ Set the entire info stanza for a JID/node at once.
+
+ The data parameter is a disco#info substanza.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'] = data
+
+ def del_info(self, jid, node, ifrom, data):
+ """
+ Reset the info stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'] = DiscoInfo()
+
+ def get_items(self, jid, node, ifrom, data):
+ """
+ Return the stored items data for the requested JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ if not node:
+ return DiscoInfo()
+ else:
+ raise XMPPError(condition='item-not-found')
+ else:
+ return self.get_node(jid, node)['items']
+
+ def set_items(self, jid, node, ifrom, data):
+ """
+ Replace the stored items data for a JID/node combination.
+
+ The data parameter may provide:
+ items -- A set of items in tuple format.
+ """
+ with self.lock:
+ items = data.get('items', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items']['items'] = items
+
+ def del_items(self, jid, node, ifrom, data):
+ """
+ Reset the items stanza for a given JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'] = DiscoItems()
+
+ def add_identity(self, jid, node, ifrom, data):
+ """
+ Add a new identity to te JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belongs.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional standard xml:lang value.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def set_identities(self, jid, node, ifrom, data):
+ """
+ Add or replace all identities for a JID/node combination.
+
+ The data parameter should include:
+ identities -- A list of identities in tuple form:
+ (category, type, name, lang)
+ """
+ with self.lock:
+ identities = data.get('identities', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['identities'] = identities
+
+ def del_identity(self, jid, node, ifrom, data):
+ """
+ Remove an identity from a JID/node combination.
+
+ The data parameter may provide:
+ category -- The general category to which the agent belonged.
+ itype -- A more specific designation with the category.
+ name -- Optional human readable name for this identity.
+ lang -- Optional, standard xml:lang value.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_identity(
+ data.get('category', ''),
+ data.get('itype', ''),
+ data.get('name', None),
+ data.get('lang', None))
+
+ def del_identities(self, jid, node, ifrom, data):
+ """
+ Remove all identities from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ del self.get_node(jid, node)['info']['identities']
+
+ def add_feature(self, jid, node, ifrom, data):
+ """
+ Add a feature to a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the supported feature.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info'].add_feature(data.get('feature', ''))
+
+ def set_features(self, jid, node, ifrom, data):
+ """
+ Add or replace all features for a JID/node combination.
+
+ The data parameter should include:
+ features -- The new set of supported features.
+ """
+ with self.lock:
+ features = data.get('features', set())
+ self.add_node(jid, node)
+ self.get_node(jid, node)['info']['features'] = features
+
+ def del_feature(self, jid, node, ifrom, data):
+ """
+ Remove a feature from a JID/node combination.
+
+ The data parameter should include:
+ feature -- The namespace of the removed feature.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['info'].del_feature(data.get('feature', ''))
+
+ def del_features(self, jid, node, ifrom, data):
+ """
+ Remove all features from a JID/node combination.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if not self.node_exists(jid, node):
+ return
+ del self.get_node(jid, node)['info']['features']
+
+ def add_item(self, jid, node, ifrom, data):
+ """
+ Add an item to a JID/node combination.
+
+ The data parameter may include:
+ ijid -- The JID for the item.
+ inode -- Optional additional information to reference
+ non-addressable items.
+ name -- Optional human readable name for the item.
+ """
+ with self.lock:
+ self.add_node(jid, node)
+ self.get_node(jid, node)['items'].add_item(
+ data.get('ijid', ''),
+ node=data.get('inode', ''),
+ name=data.get('name', ''))
+
+ def del_item(self, jid, node, ifrom, data):
+ """
+ Remove an item from a JID/node combination.
+
+ The data parameter may include:
+ ijid -- JID of the item to remove.
+ inode -- Optional extra identifying information.
+ """
+ with self.lock:
+ if self.node_exists(jid, node):
+ self.get_node(jid, node)['items'].del_item(
+ data.get('ijid', ''),
+ node=data.get('inode', None))
+
+ def cache_info(self, jid, node, ifrom, data):
+ """
+ Cache disco information for an external JID.
+
+ The data parameter is the Iq result stanza
+ containing the disco info to cache, or
+ the disco#info substanza itself.
+ """
+ with self.lock:
+ if isinstance(data, Iq):
+ data = data['disco_info']
+
+ self.add_node(jid, node, ifrom)
+ self.get_node(jid, node, ifrom)['info'] = data
+
+ def get_cached_info(self, jid, node, ifrom, data):
+ """
+ Retrieve cached disco info data.
+
+ The data parameter is not used.
+ """
+ with self.lock:
+ if isinstance(jid, JID):
+ jid = jid.full
+
+ if not self.node_exists(jid, node, ifrom):
+ return None
+ else:
+ return self.get_node(jid, node, ifrom)['info']