Merge branch 'develop-1.1' into develop
This commit is contained in:
		| @@ -79,19 +79,21 @@ class FormField(ElementBase): | ||||
|         reqXML = self.xml.find('{%s}required' % self.namespace) | ||||
|         return reqXML is not None | ||||
|  | ||||
|     def get_value(self): | ||||
|     def get_value(self, convert=True): | ||||
|         valsXML = self.xml.findall('{%s}value' % self.namespace) | ||||
|         if len(valsXML) == 0: | ||||
|             return None | ||||
|         elif self._type == 'boolean': | ||||
|             return valsXML[0].text in self.true_values | ||||
|             if convert: | ||||
|                 return valsXML[0].text in self.true_values | ||||
|             return valsXML[0].text | ||||
|         elif self._type in self.multi_value_types or len(valsXML) > 1: | ||||
|             values = [] | ||||
|             for valXML in valsXML: | ||||
|                 if valXML.text is None: | ||||
|                     valXML.text = '' | ||||
|                 values.append(valXML.text) | ||||
|             if self._type == 'text-multi': | ||||
|             if self._type == 'text-multi' and condense: | ||||
|                 values = "\n".join(values) | ||||
|             return values | ||||
|         else: | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import logging | ||||
|  | ||||
| import sleekxmpp | ||||
| from sleekxmpp import Iq | ||||
| from sleekxmpp.exceptions import XMPPError | ||||
| 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 | ||||
| @@ -108,11 +108,16 @@ class xep_0030(base_plugin): | ||||
|  | ||||
|         self.static = StaticDisco(self.xmpp) | ||||
|  | ||||
|         self._disco_ops = ['get_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'] | ||||
|         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: | ||||
| @@ -237,7 +242,78 @@ class xep_0030(base_plugin): | ||||
|             self.del_node_handler(op, jid, node) | ||||
|             self.set_node_handler(op, jid, node, self.default_handlers[op]) | ||||
|  | ||||
|     def get_info(self, jid=None, node=None, local=False, **kwargs): | ||||
|     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. | ||||
|  | ||||
| @@ -257,6 +333,13 @@ class xep_0030(base_plugin): | ||||
|                         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 | ||||
| @@ -266,12 +349,31 @@ class xep_0030(base_plugin): | ||||
|                         received instead of blocking and waiting for | ||||
|                         the reply. | ||||
|         """ | ||||
|         if local or jid is None: | ||||
|         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 | ||||
|  | ||||
|         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) | ||||
|             return self._fix_default_info(info) | ||||
|             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', '')) | ||||
| @@ -282,6 +384,15 @@ class xep_0030(base_plugin): | ||||
|                        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. | ||||
| @@ -314,7 +425,9 @@ class xep_0030(base_plugin): | ||||
|                         Otherwise the parameter is ignored. | ||||
|         """ | ||||
|         if local or jid is None: | ||||
|             return self._run_node_handler('get_items', jid, node, kwargs) | ||||
|             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 | ||||
| @@ -341,7 +454,7 @@ class xep_0030(base_plugin): | ||||
|             node  -- Optional node to modify. | ||||
|             items -- A series of items in tuple format. | ||||
|         """ | ||||
|         self._run_node_handler('set_items', jid, node, kwargs) | ||||
|         self._run_node_handler('set_items', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_items(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -351,7 +464,7 @@ class xep_0030(base_plugin): | ||||
|             jid  -- The JID to modify. | ||||
|             node -- Optional node to modify. | ||||
|         """ | ||||
|         self._run_node_handler('del_items', jid, node, kwargs) | ||||
|         self._run_node_handler('del_items', jid, node, None, kwargs) | ||||
|  | ||||
|     def add_item(self, jid='', name='', node=None, subnode='', ijid=None): | ||||
|         """ | ||||
| @@ -372,7 +485,7 @@ class xep_0030(base_plugin): | ||||
|         kwargs = {'ijid': jid, | ||||
|                   'name': name, | ||||
|                   'inode': subnode} | ||||
|         self._run_node_handler('add_item', ijid, node, kwargs) | ||||
|         self._run_node_handler('add_item', ijid, node, None, kwargs) | ||||
|  | ||||
|     def del_item(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -384,7 +497,7 @@ class xep_0030(base_plugin): | ||||
|             ijid  -- The item's JID. | ||||
|             inode -- The item's node. | ||||
|         """ | ||||
|         self._run_node_handler('del_item', jid, node, kwargs) | ||||
|         self._run_node_handler('del_item', jid, node, None, kwargs) | ||||
|  | ||||
|     def add_identity(self, category='', itype='', name='', | ||||
|                      node=None, jid=None, lang=None): | ||||
| @@ -411,7 +524,7 @@ class xep_0030(base_plugin): | ||||
|                   'itype': itype, | ||||
|                   'name': name, | ||||
|                   'lang': lang} | ||||
|         self._run_node_handler('add_identity', jid, node, kwargs) | ||||
|         self._run_node_handler('add_identity', jid, node, None, kwargs) | ||||
|  | ||||
|     def add_feature(self, feature, node=None, jid=None): | ||||
|         """ | ||||
| @@ -423,7 +536,7 @@ class xep_0030(base_plugin): | ||||
|             jid     -- The JID to modify. | ||||
|         """ | ||||
|         kwargs = {'feature': feature} | ||||
|         self._run_node_handler('add_feature', jid, node, kwargs) | ||||
|         self._run_node_handler('add_feature', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_identity(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -437,7 +550,7 @@ class xep_0030(base_plugin): | ||||
|             name     -- Optional, human readable name for the identity. | ||||
|             lang     -- Optional, the identity's xml:lang value. | ||||
|         """ | ||||
|         self._run_node_handler('del_identity', jid, node, kwargs) | ||||
|         self._run_node_handler('del_identity', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_feature(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -448,7 +561,7 @@ class xep_0030(base_plugin): | ||||
|             node    -- The node to modify. | ||||
|             feature -- The feature's namespace. | ||||
|         """ | ||||
|         self._run_node_handler('del_feature', jid, node, kwargs) | ||||
|         self._run_node_handler('del_feature', jid, node, None, kwargs) | ||||
|  | ||||
|     def set_identities(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -463,7 +576,7 @@ class xep_0030(base_plugin): | ||||
|             identities -- A set of identities in tuple form. | ||||
|             lang       -- Optional, xml:lang value. | ||||
|         """ | ||||
|         self._run_node_handler('set_identities', jid, node, kwargs) | ||||
|         self._run_node_handler('set_identities', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_identities(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -478,7 +591,7 @@ class xep_0030(base_plugin): | ||||
|             lang -- Optional. If given, only remove identities | ||||
|                     using this xml:lang value. | ||||
|         """ | ||||
|         self._run_node_handler('del_identities', jid, node, kwargs) | ||||
|         self._run_node_handler('del_identities', jid, node, None, kwargs) | ||||
|  | ||||
|     def set_features(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -490,7 +603,7 @@ class xep_0030(base_plugin): | ||||
|             node     -- The node to modify. | ||||
|             features -- The new set of supported features. | ||||
|         """ | ||||
|         self._run_node_handler('set_features', jid, node, kwargs) | ||||
|         self._run_node_handler('set_features', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_features(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -500,9 +613,9 @@ class xep_0030(base_plugin): | ||||
|             jid  -- The JID to modify. | ||||
|             node -- The node to modify. | ||||
|         """ | ||||
|         self._run_node_handler('del_features', jid, node, kwargs) | ||||
|         self._run_node_handler('del_features', jid, node, None, kwargs) | ||||
|  | ||||
|     def _run_node_handler(self, htype, jid, node, data={}): | ||||
|     def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): | ||||
|         """ | ||||
|         Execute the most specific node handler for the given | ||||
|         JID/node combination. | ||||
| @@ -513,7 +626,7 @@ class xep_0030(base_plugin): | ||||
|             node  -- The node requested. | ||||
|             data  -- Optional, custom data to pass to the handler. | ||||
|         """ | ||||
|         if jid is None: | ||||
|         if jid in (None, ''): | ||||
|             if self.xmpp.is_component: | ||||
|                 jid = self.xmpp.boundjid.full | ||||
|             else: | ||||
| @@ -521,14 +634,28 @@ class xep_0030(base_plugin): | ||||
|         if node is None: | ||||
|             node = '' | ||||
|  | ||||
|         if self._handlers[htype]['node'].get((jid, node), False): | ||||
|             return self._handlers[htype]['node'][(jid, node)](jid, node, data) | ||||
|         elif self._handlers[htype]['jid'].get(jid, False): | ||||
|             return self._handlers[htype]['jid'][jid](jid, node, data) | ||||
|         elif self._handlers[htype]['global']: | ||||
|             return self._handlers[htype]['global'](jid, node, data) | ||||
|         else: | ||||
|             return None | ||||
|         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): | ||||
|         """ | ||||
| @@ -550,6 +677,7 @@ class xep_0030(base_plugin): | ||||
|             info = self._run_node_handler('get_info', | ||||
|                                           jid, | ||||
|                                           iq['disco_info']['node'], | ||||
|                                           iq['from'], | ||||
|                                           iq) | ||||
|             if isinstance(info, Iq): | ||||
|                 info.send() | ||||
| @@ -560,8 +688,20 @@ class xep_0030(base_plugin): | ||||
|                     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']) | ||||
|             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): | ||||
| @@ -583,6 +723,7 @@ class xep_0030(base_plugin): | ||||
|             items = self._run_node_handler('get_items', | ||||
|                                           jid, | ||||
|                                           iq['disco_items']['node'], | ||||
|                                           iq['from'].full, | ||||
|                                           iq) | ||||
|             if isinstance(items, Iq): | ||||
|                 items.send() | ||||
| @@ -592,7 +733,7 @@ class xep_0030(base_plugin): | ||||
|                     iq.set_payload(items.xml) | ||||
|                 iq.send() | ||||
|         elif iq['type'] == 'result': | ||||
|             log.debug("Received disco items result from" + \ | ||||
|             log.debug("Received disco items result from " + \ | ||||
|                       "%s to %s.", iq['from'], iq['to']) | ||||
|             self.xmpp.event('disco_items', iq) | ||||
|  | ||||
| @@ -607,21 +748,46 @@ class xep_0030(base_plugin): | ||||
|         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." + \ | ||||
|                     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." + \ | ||||
|                     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." + \ | ||||
|                 log.debug("No features found for this entity. " + \ | ||||
|                           "Using default disco#info feature.") | ||||
|                 info.add_feature(info.namespace) | ||||
|         return info | ||||
|         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 | ||||
|   | ||||
| @@ -146,7 +146,7 @@ class DiscoInfo(ElementBase): | ||||
|                     return True | ||||
|         return False | ||||
|  | ||||
|     def get_identities(self, lang=None): | ||||
|     def get_identities(self, lang=None, dedupe=True): | ||||
|         """ | ||||
|         Return a set of all identities in tuple form as so: | ||||
|             (category, type, lang, name) | ||||
| @@ -155,17 +155,25 @@ class DiscoInfo(ElementBase): | ||||
|         that language. | ||||
|  | ||||
|         Arguments: | ||||
|             lang -- Optional, standard xml:lang value. | ||||
|             lang   -- Optional, standard xml:lang value. | ||||
|             dedupe -- If True, de-duplicate identities, otherwise | ||||
|                       return a list of all identities. | ||||
|         """ | ||||
|         identities = set() | ||||
|         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: | ||||
|                 identities.add(( | ||||
|                     id_xml.attrib['category'], | ||||
|                     id_xml.attrib['type'], | ||||
|                     id_xml.attrib.get('{%s}lang' % self.xml_ns, None), | ||||
|                     id_xml.attrib.get('name', None))) | ||||
|                 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): | ||||
| @@ -237,11 +245,17 @@ class DiscoInfo(ElementBase): | ||||
|                     return True | ||||
|         return False | ||||
|  | ||||
|     def get_features(self): | ||||
|     def get_features(self, dedupe=True): | ||||
|         """Return the set of all supported features.""" | ||||
|         features = set() | ||||
|         if dedupe: | ||||
|             features = set() | ||||
|         else: | ||||
|             features = [] | ||||
|         for feature_xml in self.findall('{%s}feature' % self.namespace): | ||||
|             features.add(feature_xml.attrib['var']) | ||||
|             if dedupe: | ||||
|                 features.add(feature_xml.attrib['var']) | ||||
|             else: | ||||
|                 features.append(feature_xml.attrib['var']) | ||||
|         return features | ||||
|  | ||||
|     def set_features(self, features): | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| import threading | ||||
|  | ||||
| import sleekxmpp | ||||
| from sleekxmpp import Iq | ||||
| @@ -50,8 +51,10 @@ class StaticDisco(object): | ||||
|         """ | ||||
|         self.nodes = {} | ||||
|         self.xmpp = xmpp | ||||
|         self.disco = xmpp['xep_0030'] | ||||
|         self.lock = threading.RLock() | ||||
|  | ||||
|     def add_node(self, jid=None, node=None): | ||||
|     def add_node(self, jid=None, node=None, ifrom=None): | ||||
|         """ | ||||
|         Create a new set of stanzas for the provided | ||||
|         JID and node combination. | ||||
| @@ -60,83 +63,219 @@ class StaticDisco(object): | ||||
|             jid  -- The JID that will own the new stanzas. | ||||
|             node -- The node that will own the new stanzas. | ||||
|         """ | ||||
|         if jid is None: | ||||
|             jid = self.xmpp.boundjid.full | ||||
|         if node is None: | ||||
|             node = '' | ||||
|         if (jid, node) not in self.nodes: | ||||
|             self.nodes[(jid, node)] = {'info': DiscoInfo(), | ||||
|                                        'items': DiscoItems()} | ||||
|             self.nodes[(jid, node)]['info']['node'] = node | ||||
|             self.nodes[(jid, node)]['items']['node'] = node | ||||
|         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 three arguments: jid, node, and data. | ||||
|     # The jid and node parameters together determine the set of | ||||
|     # info and items stanzas that will be retrieved or added. | ||||
|     # The data parameter is a dictionary with additional paramters | ||||
|     # that will be passed to other calls. | ||||
|     # 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 get_info(self, jid, node, data): | ||||
|     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. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             if not node: | ||||
|                 return DiscoInfo() | ||||
|         with self.lock: | ||||
|             if not self.node_exists(jid, node): | ||||
|                 if not node: | ||||
|                     return DiscoInfo() | ||||
|                 else: | ||||
|                     raise XMPPError(condition='item-not-found') | ||||
|             else: | ||||
|                 raise XMPPError(condition='item-not-found') | ||||
|         else: | ||||
|             return self.nodes[(jid, node)]['info'] | ||||
|                 return self.get_node(jid, node)['info'] | ||||
|  | ||||
|     def del_info(self, jid, node, data): | ||||
|     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. | ||||
|         """ | ||||
|         if (jid, node) in self.nodes: | ||||
|             self.nodes[(jid, node)]['info'] = DiscoInfo() | ||||
|         with self.lock: | ||||
|             if self.node_exists(jid, node): | ||||
|                 self.get_node(jid, node)['info'] = DiscoInfo() | ||||
|  | ||||
|     def get_items(self, jid, node, data): | ||||
|     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. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             if not node: | ||||
|                 return DiscoInfo() | ||||
|         with self.lock: | ||||
|             if not self.node_exists(jid, node): | ||||
|                 if not node: | ||||
|                     return DiscoInfo() | ||||
|                 else: | ||||
|                     raise XMPPError(condition='item-not-found') | ||||
|             else: | ||||
|                 raise XMPPError(condition='item-not-found') | ||||
|         else: | ||||
|             return self.nodes[(jid, node)]['items'] | ||||
|                 return self.get_node(jid, node)['items'] | ||||
|  | ||||
|     def set_items(self, jid, node, data): | ||||
|     def set_items(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Replace the stored items data for a JID/node combination. | ||||
|  | ||||
|         The data parameter may provided: | ||||
|         The data parameter may provide: | ||||
|             items -- A set of items in tuple format. | ||||
|         """ | ||||
|         items = data.get('items', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['items']['items'] = items | ||||
|         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, data): | ||||
|     def del_items(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Reset the items stanza for a given JID/node combination. | ||||
|  | ||||
|         The data parameter is not used. | ||||
|         """ | ||||
|         if (jid, node) in self.nodes: | ||||
|             self.nodes[(jid, node)]['items'] = DiscoItems() | ||||
|         with self.lock: | ||||
|             if self.node_exists(jid, node): | ||||
|                 self.get_node(jid, node)['items'] = DiscoItems() | ||||
|  | ||||
|     def add_identity(self, jid, node, data): | ||||
|     def add_identity(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Add a new identity to te JID/node combination. | ||||
|  | ||||
| @@ -146,14 +285,15 @@ class StaticDisco(object): | ||||
|             name     -- Optional human readable name for this identity. | ||||
|             lang     -- Optional standard xml:lang value. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['info'].add_identity( | ||||
|                 data.get('category', ''), | ||||
|                 data.get('itype', ''), | ||||
|                 data.get('name', None), | ||||
|                 data.get('lang', None)) | ||||
|         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, data): | ||||
|     def set_identities(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Add or replace all identities for a JID/node combination. | ||||
|  | ||||
| @@ -161,11 +301,12 @@ class StaticDisco(object): | ||||
|             identities -- A list of identities in tuple form: | ||||
|                             (category, type, name, lang) | ||||
|         """ | ||||
|         identities = data.get('identities', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['info']['identities'] = identities | ||||
|         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, data): | ||||
|     def del_identity(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Remove an identity from a JID/node combination. | ||||
|  | ||||
| @@ -175,67 +316,70 @@ class StaticDisco(object): | ||||
|             name     -- Optional human readable name for this identity. | ||||
|             lang     -- Optional, standard xml:lang value. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             return | ||||
|         self.nodes[(jid, node)]['info'].del_identity( | ||||
|                 data.get('category', ''), | ||||
|                 data.get('itype', ''), | ||||
|                 data.get('name', None), | ||||
|                 data.get('lang', None)) | ||||
|         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, data): | ||||
|     def del_identities(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Remove all identities from a JID/node combination. | ||||
|  | ||||
|         The data parameter is not used. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             return | ||||
|         del self.nodes[(jid, node)]['info']['identities'] | ||||
|         with self.lock: | ||||
|             if self.node_exists(jid, node): | ||||
|                 del self.get_node(jid, node)['info']['identities'] | ||||
|  | ||||
|     def add_feature(self, jid, node, data): | ||||
|     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. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['info'].add_feature(data.get('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, data): | ||||
|     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. | ||||
|         """ | ||||
|         features = data.get('features', set()) | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['info']['features'] = 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, data): | ||||
|     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. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             return | ||||
|         self.nodes[(jid, node)]['info'].del_feature(data.get('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, data): | ||||
|     def del_features(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Remove all features from a JID/node combination. | ||||
|  | ||||
|         The data parameter is not used. | ||||
|         """ | ||||
|         if (jid, node) not in self.nodes: | ||||
|             return | ||||
|         del self.nodes[(jid, node)]['info']['features'] | ||||
|         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, data): | ||||
|     def add_item(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Add an item to a JID/node combination. | ||||
|  | ||||
| @@ -245,13 +389,14 @@ class StaticDisco(object): | ||||
|                      non-addressable items. | ||||
|             name  -- Optional human readable name for the item. | ||||
|         """ | ||||
|         self.add_node(jid, node) | ||||
|         self.nodes[(jid, node)]['items'].add_item( | ||||
|                 data.get('ijid', ''), | ||||
|                 node=data.get('inode', ''), | ||||
|                 name=data.get('name', '')) | ||||
|         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, data): | ||||
|     def del_item(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Remove an item from a JID/node combination. | ||||
|  | ||||
| @@ -259,7 +404,38 @@ class StaticDisco(object): | ||||
|             ijid  -- JID of the item to remove. | ||||
|             inode -- Optional extra identifying information. | ||||
|         """ | ||||
|         if (jid, node) in self.nodes: | ||||
|             self.nodes[(jid, node)]['items'].del_item( | ||||
|                     data.get('ijid', ''), | ||||
|                     node=data.get('inode', None)) | ||||
|         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'] | ||||
|   | ||||
							
								
								
									
										11
									
								
								sleekxmpp/plugins/xep_0115/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								sleekxmpp/plugins/xep_0115/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| """ | ||||
|     SleekXMPP: The Sleek XMPP Library | ||||
|     Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of SleekXMPP. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from sleekxmpp.plugins.xep_0115.stanza import Capabilities | ||||
| from sleekxmpp.plugins.xep_0115.static import StaticCaps | ||||
| from sleekxmpp.plugins.xep_0115.caps import xep_0115 | ||||
							
								
								
									
										290
									
								
								sleekxmpp/plugins/xep_0115/caps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										290
									
								
								sleekxmpp/plugins/xep_0115/caps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,290 @@ | ||||
| """ | ||||
|     SleekXMPP: The Sleek XMPP Library | ||||
|     Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of SleekXMPP. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| import hashlib | ||||
| import base64 | ||||
|  | ||||
| import sleekxmpp | ||||
| from sleekxmpp.stanza import StreamFeatures, Presence, Iq | ||||
| from sleekxmpp.xmlstream import register_stanza_plugin, JID | ||||
| from sleekxmpp.xmlstream.handler import Callback | ||||
| from sleekxmpp.xmlstream.matcher import StanzaPath | ||||
| from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout | ||||
| from sleekxmpp.plugins.base import base_plugin | ||||
| from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class xep_0115(base_plugin): | ||||
|  | ||||
|     """ | ||||
|     XEP-0115: Entity Capabalities | ||||
|     """ | ||||
|  | ||||
|     def plugin_init(self): | ||||
|         self.xep = '0115' | ||||
|         self.description = 'Entity Capabilities' | ||||
|         self.stanza = stanza | ||||
|  | ||||
|         self.hashes = {'sha-1': hashlib.sha1,  | ||||
|                        'md5': hashlib.md5} | ||||
|  | ||||
|         self.hash = self.config.get('hash', 'sha-1') | ||||
|         self.caps_node = self.config.get('caps_node', None) | ||||
|         self.broadcast = self.config.get('broadcast', True) | ||||
|  | ||||
|         if self.caps_node is None: | ||||
|             ver = sleekxmpp.__version__ | ||||
|             self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver | ||||
|  | ||||
|         register_stanza_plugin(Presence, stanza.Capabilities) | ||||
|         register_stanza_plugin(StreamFeatures, stanza.Capabilities) | ||||
|  | ||||
|         self._disco_ops = ['cache_caps', | ||||
|                            'get_caps', | ||||
|                            'assign_verstring', | ||||
|                            'get_verstring', | ||||
|                            'supports', | ||||
|                            'has_identity'] | ||||
|  | ||||
|         self.xmpp.register_handler( | ||||
|                 Callback('Entity Capabilites', | ||||
|                          StanzaPath('presence/caps'), | ||||
|                          self._handle_caps)) | ||||
|  | ||||
|         self.xmpp.add_filter('out', self._filter_add_caps) | ||||
|  | ||||
|         self.xmpp.add_event_handler('entity_caps', self._process_caps, | ||||
|                                     threaded=True) | ||||
|  | ||||
|         self.xmpp.register_feature('caps', | ||||
|                 self._handle_caps_feature, | ||||
|                 restart=False, | ||||
|                 order=10010) | ||||
|  | ||||
|     def post_init(self): | ||||
|         base_plugin.post_init(self) | ||||
|         self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) | ||||
|  | ||||
|         disco = self.xmpp['xep_0030'] | ||||
|         self.static = StaticCaps(self.xmpp, disco.static) | ||||
|  | ||||
|         for op in self._disco_ops: | ||||
|             disco._add_disco_op(op, getattr(self.static, op)) | ||||
|  | ||||
|         self._run_node_handler = disco._run_node_handler | ||||
|  | ||||
|         disco.cache_caps = self.cache_caps | ||||
|         disco.update_caps = self.update_caps | ||||
|         disco.assign_verstring = self.assign_verstring | ||||
|         disco.get_verstring = self.get_verstring | ||||
|  | ||||
|     def _filter_add_caps(self, stanza): | ||||
|         if isinstance(stanza, Presence) and self.broadcast: | ||||
|             ver = self.get_verstring(stanza['from']) | ||||
|             if ver: | ||||
|                 stanza['caps']['node'] = self.caps_node | ||||
|                 stanza['caps']['hash'] = self.hash | ||||
|                 stanza['caps']['ver'] = ver | ||||
|         return stanza | ||||
|  | ||||
|     def _handle_caps(self, presence): | ||||
|         if not self.xmpp.is_component: | ||||
|             if presence['from'] == self.xmpp.boundjid: | ||||
|                 return | ||||
|         self.xmpp.event('entity_caps', presence) | ||||
|  | ||||
|     def _handle_caps_feature(self, features): | ||||
|         # We already have a method to process presence with | ||||
|         # caps, so wrap things up and use that. | ||||
|         p = Presence() | ||||
|         p['from'] = self.xmpp.boundjid.domain | ||||
|         p.append(features['caps']) | ||||
|         self.xmpp.features.add('caps') | ||||
|  | ||||
|         self.xmpp.event('entity_caps', p) | ||||
|  | ||||
|     def _process_caps(self, pres): | ||||
|         if not pres['caps']['hash']: | ||||
|             log.debug("Received unsupported legacy caps.") | ||||
|             self.xmpp.event('entity_caps_legacy', pres) | ||||
|             return | ||||
|  | ||||
|         existing_verstring = self.get_verstring(pres['from'].full) | ||||
|         if str(existing_verstring) == str(pres['caps']['ver']): | ||||
|             return | ||||
|       | ||||
|         if pres['caps']['hash'] not in self.hashes: | ||||
|             try: | ||||
|                 log.debug("Unknown caps hash: %s", pres['caps']['hash']) | ||||
|                 self.xmpp['xep_003'].get_info(jid=pres['from'].full) | ||||
|                 return | ||||
|             except XMPPError: | ||||
|                 return | ||||
|     | ||||
|         log.debug("New caps verification string: %s", pres['caps']['ver']) | ||||
|         try: | ||||
|             caps = self.xmpp['xep_0030'].get_info( | ||||
|                     jid=pres['from'].full, | ||||
|                     node='%s#%s' % (pres['caps']['node'], | ||||
|                                     pres['caps']['ver'])) | ||||
|                      | ||||
|             if self._validate_caps(caps['disco_info'],  | ||||
|                                    pres['caps']['hash'], | ||||
|                                    pres['caps']['ver']): | ||||
|                 self.assign_verstring(pres['from'], pres['caps']['ver']) | ||||
|         except XMPPError: | ||||
|             log.debug("Could not retrieve disco#info results for caps") | ||||
|  | ||||
|     def _validate_caps(self, caps, hash, check_verstring): | ||||
|         # Check Identities | ||||
|         full_ids = caps.get_identities(dedupe=False) | ||||
|         deduped_ids = caps.get_identities() | ||||
|         if len(full_ids) != len(deduped_ids): | ||||
|             log.debug("Duplicate disco identities found, invalid for caps") | ||||
|             return False | ||||
|  | ||||
|         # Check Features | ||||
|  | ||||
|         full_features = caps.get_features(dedupe=False) | ||||
|         deduped_features = caps.get_features() | ||||
|         if len(full_features) != len(deduped_features): | ||||
|             log.debug("Duplicate disco features found, invalid for caps") | ||||
|             return False | ||||
|  | ||||
|         # Check Forms | ||||
|         form_types = [] | ||||
|         deduped_form_types = set() | ||||
|         for stanza in caps['substanzas']: | ||||
|             if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): | ||||
|                 if 'FORM_TYPE' in stanza['fields']: | ||||
|                     f_type = tuple(stanza['fields']['FORM_TYPE']['value']) | ||||
|                     form_types.append(f_type) | ||||
|                     deduped_form_types.add(f_type) | ||||
|                     if len(form_types) != len(deduped_form_types): | ||||
|                         log.debug("Duplicated FORM_TYPE values, invalid for caps") | ||||
|                         return False | ||||
|  | ||||
|                     if len(f_type) > 1: | ||||
|                         deduped_type = set(f_type) | ||||
|                         if len(f_type) != len(deduped_type): | ||||
|                             log.debug("Extra FORM_TYPE data, invalid for caps") | ||||
|                             return False | ||||
|  | ||||
|                     if stanza['fields']['FORM_TYPE']['type'] != 'hidden': | ||||
|                         log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps") | ||||
|                         caps.xml.remove(stanza.xml) | ||||
|                 else: | ||||
|                     log.debug("No FORM_TYPE found, ignoring form for caps") | ||||
|                     caps.xml.remove(stanza.xml) | ||||
|  | ||||
|         verstring = self.generate_verstring(caps, hash) | ||||
|         if verstring != check_verstring: | ||||
|             log.debug("Verification strings do not match: %s, %s" % ( | ||||
|                 verstring, check_verstring)) | ||||
|             return False | ||||
|  | ||||
|         self.cache_caps(verstring, caps) | ||||
|         return True | ||||
|  | ||||
|     def generate_verstring(self, info, hash): | ||||
|         hash = self.hashes.get(hash, None) | ||||
|         if hash is None: | ||||
|             return None | ||||
|  | ||||
|         S = '' | ||||
|  | ||||
|         # Convert None to '' in the identities | ||||
|         def clean_identity(id): | ||||
|             return map(lambda i: i or '', id) | ||||
|         identities = map(clean_identity, info['identities']) | ||||
|  | ||||
|         identities = sorted(('/'.join(i) for i in identities)) | ||||
|         features = sorted(info['features']) | ||||
|   | ||||
|         S += '<'.join(identities) + '<' | ||||
|         S += '<'.join(features) + '<' | ||||
|  | ||||
|         form_types = {} | ||||
|  | ||||
|         for stanza in info['substanzas']: | ||||
|             if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): | ||||
|                 if 'FORM_TYPE' in stanza['fields']: | ||||
|                     f_type = stanza['values']['FORM_TYPE'] | ||||
|                     if len(f_type): | ||||
|                         f_type = f_type[0] | ||||
|                     if f_type not in form_types: | ||||
|                         form_types[f_type] = [] | ||||
|                     form_types[f_type].append(stanza) | ||||
|  | ||||
|         sorted_forms = sorted(form_types.keys()) | ||||
|         for f_type in sorted_forms: | ||||
|             for form in form_types[f_type]: | ||||
|                 S += '%s<' % f_type | ||||
|                 fields = sorted(form['fields'].keys()) | ||||
|                 fields.remove('FORM_TYPE') | ||||
|                 for field in fields: | ||||
|                     S += '%s<' % field | ||||
|                     vals = form['fields'][field].get_value(convert=False) | ||||
|                     if vals is None: | ||||
|                         S += '<' | ||||
|                     else: | ||||
|                         if not isinstance(vals, list): | ||||
|                             vals = [vals] | ||||
|                         S += '<'.join(sorted(vals)) + '<' | ||||
|  | ||||
|         binary = hash(S.encode('utf8')).digest() | ||||
|         return base64.b64encode(binary).decode('utf-8') | ||||
|  | ||||
|     def update_caps(self, jid=None, node=None): | ||||
|         try: | ||||
|             info = self.xmpp['xep_0030'].get_info(jid, node, local=True) | ||||
|             if isinstance(info, Iq): | ||||
|                 info = info['disco_info'] | ||||
|             ver = self.generate_verstring(info, self.hash) | ||||
|             self.xmpp['xep_0030'].set_info( | ||||
|                     jid=jid,  | ||||
|                     node='%s#%s' % (self.caps_node, ver), | ||||
|                     info=info) | ||||
|             self.cache_caps(ver, info) | ||||
|             self.assign_verstring(jid, ver) | ||||
|         except XMPPError: | ||||
|             return | ||||
|  | ||||
|     def get_verstring(self, jid=None): | ||||
|         if jid in ('', None): | ||||
|             jid = self.xmpp.boundjid.full | ||||
|         if isinstance(jid, JID): | ||||
|             jid = jid.full | ||||
|         return self._run_node_handler('get_verstring', jid) | ||||
|  | ||||
|     def assign_verstring(self, jid=None, verstring=None): | ||||
|         if jid in (None, ''): | ||||
|             jid = self.xmpp.boundjid.full | ||||
|         if isinstance(jid, JID): | ||||
|             jid = jid.full | ||||
|         return self._run_node_handler('assign_verstring', jid,  | ||||
|                                       data={'verstring': verstring}) | ||||
|  | ||||
|     def cache_caps(self, verstring=None, info=None): | ||||
|         data = {'verstring': verstring, 'info': info} | ||||
|         return self._run_node_handler('cache_caps', None, None, data=data) | ||||
|  | ||||
|     def get_caps(self, jid=None, verstring=None): | ||||
|         if verstring is None: | ||||
|             if jid is not None: | ||||
|                 verstring = self.get_verstring(jid) | ||||
|             else: | ||||
|                 return None | ||||
|         if isinstance(jid, JID): | ||||
|             jid = jid.full | ||||
|         data = {'verstring': verstring} | ||||
|         return self._run_node_handler('get_caps', jid, None, None, data) | ||||
							
								
								
									
										19
									
								
								sleekxmpp/plugins/xep_0115/stanza.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								sleekxmpp/plugins/xep_0115/stanza.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| """ | ||||
|     SleekXMPP: The Sleek XMPP Library | ||||
|     Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout | ||||
|     This file is part of SleekXMPP. | ||||
|  | ||||
|     See the file LICENSE for copying permission. | ||||
| """ | ||||
|  | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| from sleekxmpp.xmlstream import ElementBase, ET | ||||
|  | ||||
|  | ||||
| class Capabilities(ElementBase): | ||||
|  | ||||
|     namespace = 'http://jabber.org/protocol/caps' | ||||
|     name = 'c' | ||||
|     plugin_attrib = 'caps' | ||||
|     interfaces = set(('hash', 'node', 'ver', 'ext')) | ||||
							
								
								
									
										147
									
								
								sleekxmpp/plugins/xep_0115/static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								sleekxmpp/plugins/xep_0115/static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| """ | ||||
|     SleekXMPP: The Sleek XMPP Library | ||||
|     Copyright (C) 2011 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.xmlstream import JID | ||||
| from sleekxmpp.plugins.xep_0030 import StaticDisco | ||||
|  | ||||
|  | ||||
| log = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class StaticCaps(object): | ||||
|  | ||||
|     """ | ||||
|     Extend the default StaticDisco implementation to provide | ||||
|     support for extended identity information. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, xmpp, static): | ||||
|         """ | ||||
|         Augment the default XEP-0030 static handler object. | ||||
|  | ||||
|         Arguments: | ||||
|             static -- The default static XEP-0030 handler object. | ||||
|         """ | ||||
|         self.xmpp = xmpp | ||||
|         self.disco = self.xmpp['xep_0030'] | ||||
|         self.caps = self.xmpp['xep_0115'] | ||||
|         self.static = static | ||||
|         self.ver_cache = {} | ||||
|         self.jid_vers = {} | ||||
|  | ||||
|     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 | ||||
|  | ||||
|         if node in (None, ''): | ||||
|             info = self.caps.get_caps(jid) | ||||
|             if info and feature in info['features']: | ||||
|                 return True | ||||
|  | ||||
|         try: | ||||
|             info = self.disco.get_info(jid=jid, node=node,  | ||||
|                                        ifrom=ifrom, **data) | ||||
|             info = self.disco._wrap(ifrom, jid, info, True) | ||||
|             return feature in info['disco_info']['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)} | ||||
|  | ||||
|         trunc = lambda i: (i[0], i[1], i[2]) | ||||
|  | ||||
|         if node in (None, ''): | ||||
|             info = self.caps.get_caps(jid) | ||||
|             if info and identity in map(trunc, 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) | ||||
|             return identity in map(trunc, info['disco_info']['identities']) | ||||
|         except IqError: | ||||
|             return False | ||||
|         except IqTimeout: | ||||
|             return None | ||||
|  | ||||
|     def cache_caps(self, jid, node, ifrom, data): | ||||
|         with self.static.lock: | ||||
|             verstring = data.get('verstring', None) | ||||
|             info = data.get('info', None) | ||||
|             if not verstring or not info: | ||||
|                 return | ||||
|             self.ver_cache[verstring] = info | ||||
|  | ||||
|     def assign_verstring(self, jid, node, ifrom, data): | ||||
|         with self.static.lock: | ||||
|             if isinstance(jid, JID): | ||||
|                 jid = jid.full | ||||
|             self.jid_vers[jid] = data.get('verstring', None) | ||||
|  | ||||
|     def get_verstring(self, jid, node, ifrom, data): | ||||
|         with self.static.lock: | ||||
|             return self.jid_vers.get(jid, None) | ||||
|  | ||||
|     def get_caps(self, jid, node, ifrom, data): | ||||
|         with self.static.lock: | ||||
|             return self.ver_cache.get(data.get('verstring', None), None) | ||||
| @@ -76,7 +76,7 @@ class xep_0128(base_plugin): | ||||
|                     as extended information, replacing any | ||||
|                     existing extensions. | ||||
|         """ | ||||
|         self.disco._run_node_handler('set_extended_info', jid, node, kwargs) | ||||
|         self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs) | ||||
|  | ||||
|     def add_extended_info(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -88,7 +88,7 @@ class xep_0128(base_plugin): | ||||
|             data -- Either a form, or a list of forms to add | ||||
|                     as extended information. | ||||
|         """ | ||||
|         self.disco._run_node_handler('add_extended_info', jid, node, kwargs) | ||||
|         self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs) | ||||
|  | ||||
|     def del_extended_info(self, jid=None, node=None, **kwargs): | ||||
|         """ | ||||
| @@ -98,4 +98,4 @@ class xep_0128(base_plugin): | ||||
|             jid  -- The JID to modify. | ||||
|             node -- The node to modify. | ||||
|         """ | ||||
|         self.disco._run_node_handler('del_extended_info', jid, node, kwargs) | ||||
|         self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs) | ||||
|   | ||||
| @@ -31,42 +31,43 @@ class StaticExtendedDisco(object): | ||||
|         """ | ||||
|         self.static = static | ||||
|  | ||||
|     def set_extended_info(self, jid, node, data): | ||||
|     def set_extended_info(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Replace the extended identity data for a JID/node combination. | ||||
|  | ||||
|         The data parameter may provide: | ||||
|             data -- Either a single data form, or a list of data forms. | ||||
|         """ | ||||
|         self.del_extended_info(jid, node, data) | ||||
|         self.add_extended_info(jid, node, data) | ||||
|         with self.static.lock: | ||||
|             self.del_extended_info(jid, node, ifrom, data) | ||||
|             self.add_extended_info(jid, node, ifrom, data) | ||||
|  | ||||
|     def add_extended_info(self, jid, node, data): | ||||
|     def add_extended_info(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Add additional extended identity data for a JID/node combination. | ||||
|  | ||||
|         The data parameter may provide: | ||||
|             data -- Either a single data form, or a list of data forms. | ||||
|         """ | ||||
|         self.static.add_node(jid, node) | ||||
|         with self.static.lock: | ||||
|             self.static.add_node(jid, node) | ||||
|  | ||||
|         forms = data.get('data', []) | ||||
|         if not isinstance(forms, list): | ||||
|             forms = [forms] | ||||
|             forms = data.get('data', []) | ||||
|             if not isinstance(forms, list): | ||||
|                 forms = [forms] | ||||
|  | ||||
|         for form in forms: | ||||
|             self.static.nodes[(jid, node)]['info'].append(form) | ||||
|             info = self.static.get_node(jid, node)['info'] | ||||
|             for form in forms: | ||||
|                 info.append(form) | ||||
|  | ||||
|     def del_extended_info(self, jid, node, data): | ||||
|     def del_extended_info(self, jid, node, ifrom, data): | ||||
|         """ | ||||
|         Replace the extended identity data for a JID/node combination. | ||||
|  | ||||
|         The data parameter is not used. | ||||
|         """ | ||||
|         if (jid, node) not in self.static.nodes: | ||||
|             return | ||||
|  | ||||
|         info = self.static.nodes[(jid, node)]['info'] | ||||
|  | ||||
|         for form in info['substanza']: | ||||
|             info.xml.remove(form.xml) | ||||
|         with self.static.lock: | ||||
|             if self.static.node_exists(jid, node): | ||||
|                 info = self.static.get_node(jid, node)['info'] | ||||
|                 for form in info['substanza']: | ||||
|                     info.xml.remove(form.xml) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Lance Stout
					Lance Stout