From eff3330e754cfbb496d6ba761d0a821375f575c8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 12 Dec 2011 22:17:07 -0800 Subject: [PATCH 01/18] Add support for incoming/outgoing filters. A filter accepts and returns a stanza, but potentially modified. To prevent sending/receiving a stanza, a filter may return None. Incoming: self.add_filter('in', in_filter) Outgoing: self.add_filter('out', out_filter) Filters are applied in the order thay are added. However, you may add an order parameter, which is the place in the list to insert the filter: self.add_filter('in', in_filter, order=0) --- sleekxmpp/xmlstream/stanzabase.py | 2 +- sleekxmpp/xmlstream/xmlstream.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 389fe20..2f86430 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1251,7 +1251,7 @@ class StanzaBase(ElementBase): stanza sent immediately. Useful for stream initialization. Defaults to ``False``. """ - self.stream.send_raw(self.__str__(), now=now) + self.stream.send(self, now=now) def __copy__(self): """Return a copy of the stanza object that does not share the diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 3e56908..b690103 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -35,7 +35,7 @@ except ImportError: import sleekxmpp from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring -from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase from sleekxmpp.xmlstream.handler import Waiter, XMLCallback from sleekxmpp.xmlstream.matcher import MatchXMLMask @@ -268,6 +268,7 @@ class XMLStream(object): self.__handlers = [] self.__event_handlers = {} self.__event_handlers_lock = threading.Lock() + self.__filters = {'in': [], 'out': []} self._id = 0 self._id_lock = threading.Lock() @@ -743,6 +744,18 @@ class XMLStream(object): """ del self.__root_stanza[stanza_class] + def add_filter(self, mode, handler, order=None): + """Add a filter for incoming or outgoing stanzas. + + These filters are applied before incoming stanzas are + passed to any handlers, and before outgoing stanzas + are put in the send queue. + """ + if order: + self.__filters[mode].insert(order, handler) + else: + self.__filters[mode].append(handler) + def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): """A shortcut method for registering a handler using XML masks. @@ -994,6 +1007,14 @@ class XMLStream(object): timeout = self.response_timeout if hasattr(mask, 'xml'): mask = mask.xml + + if isinstance(data, ElementBase): + for filter in self.__filters['out']: + if data is not None: + data = filter(data) + if data is None: + return + data = str(data) if mask is not None: log.warning("Use of send mask waiters is deprecated.") @@ -1254,6 +1275,12 @@ class XMLStream(object): # stanza type applies, a generic StanzaBase stanza will be used. stanza = self._build_stanza(xml) + for filter in self.__filters['in']: + if stanza is not None: + stanza = filter(stanza) + if stanza is None: + return + # Match the stanza against registered handlers. Handlers marked # to run "in stream" will be executed immediately; the rest will # be queued. From cb635dcd5ab94a1cbc104788b33abefea12d0040 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 12 Dec 2011 22:37:19 -0800 Subject: [PATCH 02/18] Add parameter docs for add_filter. --- sleekxmpp/xmlstream/xmlstream.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index b690103..8f662e8 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -750,6 +750,16 @@ class XMLStream(object): These filters are applied before incoming stanzas are passed to any handlers, and before outgoing stanzas are put in the send queue. + + Each filter must accept a single stanza, and return + either a stanza or ``None``. If the filter returns + ``None``, then the stanza will be dropped from being + processed for events or from being sent. + + :param mode: One of ``'in'`` or ``'out'``. + :param handler: The filter function. + :param int order: The position to insert the filter in + the list of active filters. """ if order: self.__filters[mode].insert(order, handler) From 45ed68006f579ac48ce1a67a37beaf146d3b92be Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 13 Dec 2011 20:05:31 -0800 Subject: [PATCH 03/18] Add tests for filters. --- tests/test_stream_filters.py | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/test_stream_filters.py diff --git a/tests/test_stream_filters.py b/tests/test_stream_filters.py new file mode 100644 index 0000000..ef4d5dc --- /dev/null +++ b/tests/test_stream_filters.py @@ -0,0 +1,88 @@ +import time + +from sleekxmpp import Message +from sleekxmpp.test import * +from sleekxmpp.xmlstream.handler import * +from sleekxmpp.xmlstream.matcher import * + + +class TestFilters(SleekTest): + + """ + Test using incoming and outgoing filters. + """ + + def setUp(self): + self.stream_start() + + def tearDown(self): + self.stream_close() + + def testIncoming(self): + + data = [] + + def in_filter(stanza): + if isinstance(stanza, Message): + if stanza['body'] == 'testing': + stanza['subject'] = stanza['body'] + ' filter' + print('>>> %s' % stanza['subject']) + return stanza + + def on_message(msg): + print('<<< %s' % msg['subject']) + data.append(msg['subject']) + + self.xmpp.add_filter('in', in_filter) + self.xmpp.add_event_handler('message', on_message) + + self.recv(""" + + no filter + + """) + + self.recv(""" + + testing + + """) + + time.sleep(0.5) + + self.assertEqual(data, ['', 'testing filter'], + 'Incoming filter did not apply %s' % data) + + def testOutgoing(self): + + def out_filter(stanza): + if isinstance(stanza, Message): + if stanza['body'] == 'testing': + stanza['body'] = 'changed!' + return stanza + + self.xmpp.add_filter('out', out_filter) + + m1 = self.Message() + m1['body'] = 'testing' + m1.send() + + m2 = self.Message() + m2['body'] = 'blah' + m2.send() + + self.send(""" + + changed! + + """) + + self.send(""" + + blah + + """) + + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters) From f6e30edbc4a717b34bbf01d3d0a9fe884c6b37da Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 14 Dec 2011 21:14:27 -0800 Subject: [PATCH 04/18] Log received data AFTER filtering. This allows applications to filter out sensitive information, such as passwords, so that it won't appear in the logs. It does mean that the debug logs will not show the actual received data, and there will be no indication of tampering, unless the filter author explicitly logs and notes that a change was made. --- sleekxmpp/xmlstream/xmlstream.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 8f662e8..66dd657 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -1276,8 +1276,6 @@ class XMLStream(object): :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza to analyze. """ - log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns, - stream=self)) # Apply any preprocessing filters. xml = self.incoming_filter(xml) @@ -1291,6 +1289,9 @@ class XMLStream(object): if stanza is None: return + log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns, + stream=self)) + # Match the stanza against registered handlers. Handlers marked # to run "in stream" will be executed immediately; the rest will # be queued. From 1a61bdb302e48d29888ab9b9cc23421bbb3fbead Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 28 Dec 2011 09:53:22 -0500 Subject: [PATCH 05/18] Ensure that stanza plugins work as expected if the XML is appended. --- sleekxmpp/xmlstream/stanzabase.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 2f86430..8678ca1 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -345,7 +345,8 @@ class ElementBase(object): """ if attrib not in self.plugins: plugin_class = self.plugin_attrib_map[attrib] - plugin = plugin_class(parent=self) + existing_xml = self.xml.find(plugin_class.tag_name()) + plugin = plugin_class(parent=self, xml=existing_xml) self.plugins[attrib] = plugin if plugin_class in self.plugin_iterables: self.iterables.append(plugin) From d979b5f2b9cb38bdd145c95526358238449fa067 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 28 Dec 2011 10:07:33 -0500 Subject: [PATCH 06/18] Add caching support to xep_0030. New plugin configuration options: use_cache - Enable caching disco info results. Defaults to True wrap_results - Always return disco results in an Iq stanza. Defaults to False Node handler changes: Handlers now take four arguments: jid, node, ifrom, data Most older style handlers will still work, depending on if they raise a TypeError for incorrect number of arguments. Handlers that used *args may not work. New get_info options: cached - Passing cached=True to get_info() will attempt to load results from the cache. If nothing is found, a query will be sent as normal. If set to False, the cache will be skipped, even if it contains results. New method: supports() - Given a JID/node pair and a feature, return True if the feature is supported, False if not, and None if there was a timeout. By default, the search will use the cache. --- sleekxmpp/plugins/xep_0030/disco.py | 173 ++++++++++--- sleekxmpp/plugins/xep_0030/static.py | 253 ++++++++++++------- sleekxmpp/plugins/xep_0128/extended_disco.py | 6 +- sleekxmpp/plugins/xep_0128/static.py | 37 +-- tests/test_stream_xep_0030.py | 16 +- 5 files changed, 339 insertions(+), 146 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 53086d4..503631e 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -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.use_cache = self.config.get('use_cache', True) + self.wrap_results = self.config.get('wrap_results', False) + 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'] + 'del_identities', 'del_features', + 'cache_info', 'get_cached_info'] + self.default_handlers = {} self._handlers = {} for op in self._disco_ops: @@ -237,7 +242,47 @@ 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. + """ + try: + info = self.get_info(jid=jid, node=node, local=local, + cached=cached, ifrom=ifrom) + info = self._wrap(ifrom, jid, info, True) + features = info['disco_info']['features'] + return feature in features + except IqError: + return False + except IqTimeout: + return None + + 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 +302,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 @@ -269,9 +321,19 @@ class xep_0030(base_plugin): if local or jid is 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', '')) @@ -314,7 +376,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 +405,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 +415,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 +436,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 +448,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 +475,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 +487,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 +501,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 +512,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 +527,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 +542,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 +554,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 +564,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, ifrom, data={}): """ Execute the most specific node handler for the given JID/node combination. @@ -521,14 +585,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 +628,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 +639,16 @@ 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']) + self._run_node_handler('cache_info', + iq['from'].full, + iq['disco_info']['node'], + iq['to'].full, + iq) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): @@ -583,6 +670,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() @@ -607,6 +695,9 @@ 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: @@ -621,7 +712,29 @@ class xep_0030(base_plugin): 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 diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index 7e7f035..09ce685 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -7,6 +7,7 @@ """ import logging +import threading import sleekxmpp from sleekxmpp import Iq @@ -50,8 +51,9 @@ class StaticDisco(object): """ self.nodes = {} self.xmpp = xmpp + 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,38 +62,77 @@ 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 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): """ @@ -99,44 +140,48 @@ class StaticDisco(object): 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: 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 +191,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 +207,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 +222,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 +295,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 +310,35 @@ 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 not self.node_exists(jid, node, ifrom): + return None + else: + return self.get_node(jid, node, ifrom)['info'] diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py index 63b3cfe..5bb7832 100644 --- a/sleekxmpp/plugins/xep_0128/extended_disco.py +++ b/sleekxmpp/plugins/xep_0128/extended_disco.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py index 493d937..427011c 100644 --- a/sleekxmpp/plugins/xep_0128/static.py +++ b/sleekxmpp/plugins/xep_0128/static.py @@ -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) diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py index 1666d3a..dd43778 100644 --- a/tests/test_stream_xep_0030.py +++ b/tests/test_stream_xep_0030.py @@ -122,7 +122,7 @@ class TestStreamDisco(SleekTest): self.stream_start(mode='client', plugins=['xep_0030']) - def dynamic_jid(jid, node, iq): + def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') @@ -158,7 +158,7 @@ class TestStreamDisco(SleekTest): jid='tester.localhost', plugins=['xep_0030']) - def dynamic_global(jid, node, iq): + def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') @@ -194,7 +194,7 @@ class TestStreamDisco(SleekTest): self.stream_start(mode='client', plugins=['xep_0030']) - def dynamic_jid(jid, node, iq): + def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') @@ -236,7 +236,7 @@ class TestStreamDisco(SleekTest): jid='tester.localhost', plugins=['xep_0030']) - def dynamic_global(jid, node, iq): + def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') @@ -325,7 +325,7 @@ class TestStreamDisco(SleekTest): self.stream_start(mode='client', plugins=['xep_0030']) - def dynamic_jid(jid, node, iq): + def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='JID') @@ -359,7 +359,7 @@ class TestStreamDisco(SleekTest): jid='tester.localhost', plugins=['xep_0030']) - def dynamic_global(jid, node, iq): + def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') @@ -393,7 +393,7 @@ class TestStreamDisco(SleekTest): self.stream_start(mode='client', plugins=['xep_0030']) - def dynamic_jid(jid, node, iq): + def dynamic_jid(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') @@ -435,7 +435,7 @@ class TestStreamDisco(SleekTest): jid='tester.localhost', plugins=['xep_0030']) - def dynamic_global(jid, node, iq): + def dynamic_global(jid, node, ifrom, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester.localhost', node='foo', name='Global') From 5ef0b96d5c038d70fd563c18d592a4637106879b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 28 Dec 2011 11:37:05 -0500 Subject: [PATCH 07/18] Fix caching for clients. --- sleekxmpp/plugins/xep_0030/disco.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 503631e..e1eec70 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -644,10 +644,14 @@ class xep_0030(base_plugin): 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'], - iq['to'].full, + ito, iq) self.xmpp.event('disco_info', iq) From 4df1641689da53e3f9c2e49ee3f0e5e92a7c4d06 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 28 Dec 2011 11:46:13 -0500 Subject: [PATCH 08/18] Add set_info disco handler. --- sleekxmpp/plugins/xep_0030/disco.py | 21 +++++++++++++++------ sleekxmpp/plugins/xep_0030/static.py | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index e1eec70..ccfee8b 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -111,12 +111,12 @@ class xep_0030(base_plugin): self.use_cache = self.config.get('use_cache', True) self.wrap_results = self.config.get('wrap_results', False) - 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', - 'cache_info', 'get_cached_info'] + 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'] self.default_handlers = {} self._handlers = {} @@ -344,6 +344,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. diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index 09ce685..d48b564 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -134,7 +134,17 @@ class StaticDisco(object): else: 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. @@ -163,7 +173,7 @@ class StaticDisco(object): """ 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. """ with self.lock: From 1bb0b3886800ffcb442626b85b1af1ddb4eab0f6 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 20:50:15 -0500 Subject: [PATCH 09/18] Make the disco logs nicer. --- sleekxmpp/plugins/xep_0030/disco.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index ccfee8b..d99a7ca 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -693,7 +693,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) @@ -714,15 +714,15 @@ class xep_0030(base_plugin): 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 result From a11e6c0b773169dd395f7456bba5b1b8dd5971f4 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 20:51:02 -0500 Subject: [PATCH 10/18] Be more lenient on required arguments to disco node handlers. --- sleekxmpp/plugins/xep_0030/disco.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index d99a7ca..58d5b7f 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -575,7 +575,7 @@ class xep_0030(base_plugin): """ self._run_node_handler('del_features', jid, node, None, kwargs) - def _run_node_handler(self, htype, jid, node, ifrom, 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. @@ -586,7 +586,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: From efae8f33691516282f3ada20f2a17e35162a51ae Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 20:51:41 -0500 Subject: [PATCH 11/18] Automatically use local disco based on the JID. --- sleekxmpp/plugins/xep_0030/disco.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 58d5b7f..65c5ecc 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -318,7 +318,16 @@ 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', From a7df76a275e0e68176008cd6ae3a5ca2955667b2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 20:52:44 -0500 Subject: [PATCH 12/18] Add 'supports' and 'has_identity' node handlers. --- sleekxmpp/plugins/xep_0030/disco.py | 53 +++++++++++++---- sleekxmpp/plugins/xep_0030/static.py | 87 ++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 11 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 65c5ecc..10f9ef4 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -116,7 +116,7 @@ class xep_0030(base_plugin): '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'] + 'get_cached_info', 'supports', 'has_identity'] self.default_handlers = {} self._handlers = {} @@ -270,17 +270,48 @@ class xep_0030(base_plugin): cached. Defaults to false. ifrom -- Specifiy the sender's JID. """ - try: - info = self.get_info(jid=jid, node=node, local=local, - cached=cached, ifrom=ifrom) - info = self._wrap(ifrom, jid, info, True) - features = info['disco_info']['features'] - return feature in features - except IqError: - return False - except IqTimeout: - return None + 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): """ diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index d48b564..0b196b4 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -51,6 +51,7 @@ 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, ifrom=None): @@ -119,6 +120,89 @@ class StaticDisco(object): # 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. @@ -348,6 +432,9 @@ class StaticDisco(object): 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: From 8eb225bdecd33e85b5b0e6c447f0ae4eee47201f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 20:53:18 -0500 Subject: [PATCH 13/18] Add option for disabling identity and feature deduplication. XEP-0115 requires detecting duplicates, so we can't always silently ignore them. --- sleekxmpp/plugins/xep_0030/stanza/info.py | 36 ++++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py index 6764acb..25d1d07 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/info.py +++ b/sleekxmpp/plugins/xep_0030/stanza/info.py @@ -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): From 6722b0224ae7673e3754552bdcbad714dc00b529 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 21:43:39 -0500 Subject: [PATCH 14/18] Add option to disable condensing and converting form values. XEP-0115 needs to use the raw XML character data. --- sleekxmpp/plugins/xep_0004/stanza/field.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py index 8131233..6814e15 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/field.py +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -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: From 8a29ec67ac91528747c2a954cc8f89b5bbe71e58 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 21:45:25 -0500 Subject: [PATCH 15/18] Add XEP-0115 plugin. Finally --- sleekxmpp/plugins/xep_0115/__init__.py | 11 ++ sleekxmpp/plugins/xep_0115/caps.py | 260 +++++++++++++++++++++++++ sleekxmpp/plugins/xep_0115/stanza.py | 19 ++ sleekxmpp/plugins/xep_0115/static.py | 147 ++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0115/__init__.py create mode 100644 sleekxmpp/plugins/xep_0115/caps.py create mode 100644 sleekxmpp/plugins/xep_0115/stanza.py create mode 100644 sleekxmpp/plugins/xep_0115/static.py diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py new file mode 100644 index 0000000..f4892f8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py new file mode 100644 index 0000000..19570d1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -0,0 +1,260 @@ +""" + 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 import Presence, Iq +from sleekxmpp.xmlstream import register_stanza_plugin +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) + + 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) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) + + self.disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, self.disco.static) + + for op in self._disco_ops: + self.disco._add_disco_op(op, getattr(self.static, op)) + + self._run_node_handler = self.disco._run_node_handler + + self.disco.cache_caps = self.cache_caps + self.disco.update_caps = self.update_caps + self.disco.assign_verstring = self.assign_verstring + self.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 _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.disco.get_info(jid=pres['from']) + return + except XMPPError: + return + + log.debug("New caps verification string: %s", pres['caps']['ver']) + try: + caps = self.disco.get_info( + jid=pres['from'], + 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) + + def update_caps(self, jid=None, node=None): + info = self.disco.get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.cache_caps(ver, info) + self.assign_verstring(jid, ver) + + def get_verstring(self, jid=None): + 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 + 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 + + data = {'verstring': verstring} + return self._run_node_handler('get_caps', jid, None, None, data) diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py new file mode 100644 index 0000000..af02949 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/stanza.py @@ -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')) diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py new file mode 100644 index 0000000..204181d --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/static.py @@ -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) From d817d64c65c62976481690c0bfc3882621ac0455 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Dec 2011 22:34:57 -0500 Subject: [PATCH 16/18] Enable caps stream feature. --- sleekxmpp/plugins/xep_0115/caps.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 19570d1..306629f 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -11,7 +11,7 @@ import hashlib import base64 import sleekxmpp -from sleekxmpp import Presence, Iq +from sleekxmpp.stanza import StreamFeatures, Presence, Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -46,6 +46,7 @@ class xep_0115(base_plugin): 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', @@ -64,6 +65,11 @@ class xep_0115(base_plugin): 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) @@ -96,6 +102,16 @@ class xep_0115(base_plugin): 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.") From 35954cdc90f1f404c81b7ede47c2fae420e182e8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 31 Dec 2011 19:18:00 -0500 Subject: [PATCH 17/18] Fix a few holes in caps. Protip: Don't test using a custom disco handler that always returns the same feature set :p --- sleekxmpp/plugins/xep_0115/caps.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 306629f..db5b9bf 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -245,14 +245,23 @@ class xep_0115(base_plugin): return base64.b64encode(binary) def update_caps(self, jid=None, node=None): - info = self.disco.get_info(jid, node, local=True) - if isinstance(info, Iq): - info = info['disco_info'] - ver = self.generate_verstring(info, self.hash) - self.cache_caps(ver, info) - self.assign_verstring(jid, ver) + try: + info = self.disco.get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.disco.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 return self._run_node_handler('get_verstring', jid) def assign_verstring(self, jid=None, verstring=None): From 27c658922e51aaae722880e18878cb2964cb4bd0 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 31 Dec 2011 21:15:40 -0500 Subject: [PATCH 18/18] Fix handing caps in Python3, allow update_caps() call before process() --- sleekxmpp/plugins/xep_0115/caps.py | 37 +++++++++++++++++------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index db5b9bf..d3e62ab 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -12,7 +12,7 @@ import base64 import sleekxmpp from sleekxmpp.stanza import StreamFeatures, Presence, Iq -from sleekxmpp.xmlstream import register_stanza_plugin +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 @@ -74,18 +74,18 @@ class xep_0115(base_plugin): base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) - self.disco = self.xmpp['xep_0030'] - self.static = StaticCaps(self.xmpp, self.disco.static) + disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, disco.static) for op in self._disco_ops: - self.disco._add_disco_op(op, getattr(self.static, op)) + disco._add_disco_op(op, getattr(self.static, op)) - self._run_node_handler = self.disco._run_node_handler + self._run_node_handler = disco._run_node_handler - self.disco.cache_caps = self.cache_caps - self.disco.update_caps = self.update_caps - self.disco.assign_verstring = self.assign_verstring - self.disco.get_verstring = self.get_verstring + 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: @@ -125,15 +125,15 @@ class xep_0115(base_plugin): if pres['caps']['hash'] not in self.hashes: try: log.debug("Unknown caps hash: %s", pres['caps']['hash']) - self.disco.get_info(jid=pres['from']) + 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.disco.get_info( - jid=pres['from'], + caps = self.xmpp['xep_0030'].get_info( + jid=pres['from'].full, node='%s#%s' % (pres['caps']['node'], pres['caps']['ver'])) @@ -242,15 +242,15 @@ class xep_0115(base_plugin): S += '<'.join(sorted(vals)) + '<' binary = hash(S.encode('utf8')).digest() - return base64.b64encode(binary) + return base64.b64encode(binary).decode('utf-8') def update_caps(self, jid=None, node=None): try: - info = self.disco.get_info(jid, node, local=True) + 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.disco.set_info( + self.xmpp['xep_0030'].set_info( jid=jid, node='%s#%s' % (self.caps_node, ver), info=info) @@ -262,11 +262,15 @@ class xep_0115(base_plugin): 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}) @@ -280,6 +284,7 @@ class xep_0115(base_plugin): 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)