summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLance Stout <lancestout@gmail.com>2012-01-05 11:33:47 -0500
committerLance Stout <lancestout@gmail.com>2012-01-05 11:33:47 -0500
commit8fd2efa2fa50751af7b5182ad7793295c540d294 (patch)
tree7639768de4d93b00e998f3575b02830b2ac97b44
parent79f1aa0e1ba7dd29bf597beeae924b96950f9416 (diff)
parent6b6995bb0b80c91eda72bc92974f68133cef93a3 (diff)
downloadslixmpp-8fd2efa2fa50751af7b5182ad7793295c540d294.tar.gz
slixmpp-8fd2efa2fa50751af7b5182ad7793295c540d294.tar.bz2
slixmpp-8fd2efa2fa50751af7b5182ad7793295c540d294.tar.xz
slixmpp-8fd2efa2fa50751af7b5182ad7793295c540d294.zip
Merge branch 'develop-1.1' into develop
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/field.py8
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py246
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/info.py36
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py354
-rw-r--r--sleekxmpp/plugins/xep_0115/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0115/caps.py290
-rw-r--r--sleekxmpp/plugins/xep_0115/stanza.py19
-rw-r--r--sleekxmpp/plugins/xep_0115/static.py147
-rw-r--r--sleekxmpp/plugins/xep_0128/extended_disco.py6
-rw-r--r--sleekxmpp/plugins/xep_0128/static.py37
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py5
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py44
-rw-r--r--tests/test_stream_filters.py88
-rw-r--r--tests/test_stream_xep_0030.py16
14 files changed, 1130 insertions, 177 deletions
diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py
index 8131233c..6814e157 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:
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index 53086d4e..10f9ef4e 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._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
diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py
index 6764acbb..25d1d07f 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):
diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py
index 7e7f0353..0b196b40 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,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']
diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py
new file mode 100644
index 00000000..f4892f84
--- /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 00000000..d3e62abb
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0115/caps.py
@@ -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)
diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py
new file mode 100644
index 00000000..af02949b
--- /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 00000000..204181d5
--- /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)
diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py
index 63b3cfee..5bb78320 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 493d9370..427011c0 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/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 721181a8..d1409706 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)
@@ -1251,7 +1252,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 fb9f91bc..d5029928 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,28 @@ 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.
+
+ 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)
+ 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 +1017,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.")
@@ -1246,8 +1277,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)
@@ -1255,6 +1284,15 @@ 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
+
+ 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.
diff --git a/tests/test_stream_filters.py b/tests/test_stream_filters.py
new file mode 100644
index 00000000..ef4d5dc8
--- /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("""
+ <message>
+ <body>no filter</body>
+ </message>
+ """)
+
+ self.recv("""
+ <message>
+ <body>testing</body>
+ </message>
+ """)
+
+ 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("""
+ <message>
+ <body>changed!</body>
+ </message>
+ """)
+
+ self.send("""
+ <message>
+ <body>blah</body>
+ </message>
+ """)
+
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestFilters)
diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py
index 1666d3a1..dd43778a 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')