Merge branch 'develop-1.1' into develop

This commit is contained in:
Lance Stout 2012-01-05 11:33:47 -05:00
commit 8fd2efa2fa
14 changed files with 1130 additions and 177 deletions

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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']

View file

@ -0,0 +1,11 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.xep_0115.stanza import Capabilities
from sleekxmpp.plugins.xep_0115.static import StaticCaps
from sleekxmpp.plugins.xep_0115.caps import xep_0115

View file

@ -0,0 +1,290 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
import hashlib
import base64
import sleekxmpp
from sleekxmpp.stanza import StreamFeatures, Presence, Iq
from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps
log = logging.getLogger(__name__)
class xep_0115(base_plugin):
"""
XEP-0115: Entity Capabalities
"""
def plugin_init(self):
self.xep = '0115'
self.description = 'Entity Capabilities'
self.stanza = stanza
self.hashes = {'sha-1': hashlib.sha1,
'md5': hashlib.md5}
self.hash = self.config.get('hash', 'sha-1')
self.caps_node = self.config.get('caps_node', None)
self.broadcast = self.config.get('broadcast', True)
if self.caps_node is None:
ver = sleekxmpp.__version__
self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
register_stanza_plugin(Presence, stanza.Capabilities)
register_stanza_plugin(StreamFeatures, stanza.Capabilities)
self._disco_ops = ['cache_caps',
'get_caps',
'assign_verstring',
'get_verstring',
'supports',
'has_identity']
self.xmpp.register_handler(
Callback('Entity Capabilites',
StanzaPath('presence/caps'),
self._handle_caps))
self.xmpp.add_filter('out', self._filter_add_caps)
self.xmpp.add_event_handler('entity_caps', self._process_caps,
threaded=True)
self.xmpp.register_feature('caps',
self._handle_caps_feature,
restart=False,
order=10010)
def post_init(self):
base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
disco = self.xmpp['xep_0030']
self.static = StaticCaps(self.xmpp, disco.static)
for op in self._disco_ops:
disco._add_disco_op(op, getattr(self.static, op))
self._run_node_handler = disco._run_node_handler
disco.cache_caps = self.cache_caps
disco.update_caps = self.update_caps
disco.assign_verstring = self.assign_verstring
disco.get_verstring = self.get_verstring
def _filter_add_caps(self, stanza):
if isinstance(stanza, Presence) and self.broadcast:
ver = self.get_verstring(stanza['from'])
if ver:
stanza['caps']['node'] = self.caps_node
stanza['caps']['hash'] = self.hash
stanza['caps']['ver'] = ver
return stanza
def _handle_caps(self, presence):
if not self.xmpp.is_component:
if presence['from'] == self.xmpp.boundjid:
return
self.xmpp.event('entity_caps', presence)
def _handle_caps_feature(self, features):
# We already have a method to process presence with
# caps, so wrap things up and use that.
p = Presence()
p['from'] = self.xmpp.boundjid.domain
p.append(features['caps'])
self.xmpp.features.add('caps')
self.xmpp.event('entity_caps', p)
def _process_caps(self, pres):
if not pres['caps']['hash']:
log.debug("Received unsupported legacy caps.")
self.xmpp.event('entity_caps_legacy', pres)
return
existing_verstring = self.get_verstring(pres['from'].full)
if str(existing_verstring) == str(pres['caps']['ver']):
return
if pres['caps']['hash'] not in self.hashes:
try:
log.debug("Unknown caps hash: %s", pres['caps']['hash'])
self.xmpp['xep_003'].get_info(jid=pres['from'].full)
return
except XMPPError:
return
log.debug("New caps verification string: %s", pres['caps']['ver'])
try:
caps = self.xmpp['xep_0030'].get_info(
jid=pres['from'].full,
node='%s#%s' % (pres['caps']['node'],
pres['caps']['ver']))
if self._validate_caps(caps['disco_info'],
pres['caps']['hash'],
pres['caps']['ver']):
self.assign_verstring(pres['from'], pres['caps']['ver'])
except XMPPError:
log.debug("Could not retrieve disco#info results for caps")
def _validate_caps(self, caps, hash, check_verstring):
# Check Identities
full_ids = caps.get_identities(dedupe=False)
deduped_ids = caps.get_identities()
if len(full_ids) != len(deduped_ids):
log.debug("Duplicate disco identities found, invalid for caps")
return False
# Check Features
full_features = caps.get_features(dedupe=False)
deduped_features = caps.get_features()
if len(full_features) != len(deduped_features):
log.debug("Duplicate disco features found, invalid for caps")
return False
# Check Forms
form_types = []
deduped_form_types = set()
for stanza in caps['substanzas']:
if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
if 'FORM_TYPE' in stanza['fields']:
f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
form_types.append(f_type)
deduped_form_types.add(f_type)
if len(form_types) != len(deduped_form_types):
log.debug("Duplicated FORM_TYPE values, invalid for caps")
return False
if len(f_type) > 1:
deduped_type = set(f_type)
if len(f_type) != len(deduped_type):
log.debug("Extra FORM_TYPE data, invalid for caps")
return False
if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps")
caps.xml.remove(stanza.xml)
else:
log.debug("No FORM_TYPE found, ignoring form for caps")
caps.xml.remove(stanza.xml)
verstring = self.generate_verstring(caps, hash)
if verstring != check_verstring:
log.debug("Verification strings do not match: %s, %s" % (
verstring, check_verstring))
return False
self.cache_caps(verstring, caps)
return True
def generate_verstring(self, info, hash):
hash = self.hashes.get(hash, None)
if hash is None:
return None
S = ''
# Convert None to '' in the identities
def clean_identity(id):
return map(lambda i: i or '', id)
identities = map(clean_identity, info['identities'])
identities = sorted(('/'.join(i) for i in identities))
features = sorted(info['features'])
S += '<'.join(identities) + '<'
S += '<'.join(features) + '<'
form_types = {}
for stanza in info['substanzas']:
if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
if 'FORM_TYPE' in stanza['fields']:
f_type = stanza['values']['FORM_TYPE']
if len(f_type):
f_type = f_type[0]
if f_type not in form_types:
form_types[f_type] = []
form_types[f_type].append(stanza)
sorted_forms = sorted(form_types.keys())
for f_type in sorted_forms:
for form in form_types[f_type]:
S += '%s<' % f_type
fields = sorted(form['fields'].keys())
fields.remove('FORM_TYPE')
for field in fields:
S += '%s<' % field
vals = form['fields'][field].get_value(convert=False)
if vals is None:
S += '<'
else:
if not isinstance(vals, list):
vals = [vals]
S += '<'.join(sorted(vals)) + '<'
binary = hash(S.encode('utf8')).digest()
return base64.b64encode(binary).decode('utf-8')
def update_caps(self, jid=None, node=None):
try:
info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
if isinstance(info, Iq):
info = info['disco_info']
ver = self.generate_verstring(info, self.hash)
self.xmpp['xep_0030'].set_info(
jid=jid,
node='%s#%s' % (self.caps_node, ver),
info=info)
self.cache_caps(ver, info)
self.assign_verstring(jid, ver)
except XMPPError:
return
def get_verstring(self, jid=None):
if jid in ('', None):
jid = self.xmpp.boundjid.full
if isinstance(jid, JID):
jid = jid.full
return self._run_node_handler('get_verstring', jid)
def assign_verstring(self, jid=None, verstring=None):
if jid in (None, ''):
jid = self.xmpp.boundjid.full
if isinstance(jid, JID):
jid = jid.full
return self._run_node_handler('assign_verstring', jid,
data={'verstring': verstring})
def cache_caps(self, verstring=None, info=None):
data = {'verstring': verstring, 'info': info}
return self._run_node_handler('cache_caps', None, None, data=data)
def get_caps(self, jid=None, verstring=None):
if verstring is None:
if jid is not None:
verstring = self.get_verstring(jid)
else:
return None
if isinstance(jid, JID):
jid = jid.full
data = {'verstring': verstring}
return self._run_node_handler('get_caps', jid, None, None, data)

View file

@ -0,0 +1,19 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from __future__ import unicode_literals
from sleekxmpp.xmlstream import ElementBase, ET
class Capabilities(ElementBase):
namespace = 'http://jabber.org/protocol/caps'
name = 'c'
plugin_attrib = 'caps'
interfaces = set(('hash', 'node', 'ver', 'ext'))

View file

@ -0,0 +1,147 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
import sleekxmpp
from sleekxmpp.xmlstream import JID
from sleekxmpp.plugins.xep_0030 import StaticDisco
log = logging.getLogger(__name__)
class StaticCaps(object):
"""
Extend the default StaticDisco implementation to provide
support for extended identity information.
"""
def __init__(self, xmpp, static):
"""
Augment the default XEP-0030 static handler object.
Arguments:
static -- The default static XEP-0030 handler object.
"""
self.xmpp = xmpp
self.disco = self.xmpp['xep_0030']
self.caps = self.xmpp['xep_0115']
self.static = static
self.ver_cache = {}
self.jid_vers = {}
def supports(self, jid, node, ifrom, data):
"""
Check if a JID supports a given feature.
The data parameter may provide:
feature -- The feature to check for support.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
feature = data.get('feature', None)
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
if not feature:
return False
if node in (None, ''):
info = self.caps.get_caps(jid)
if info and feature in info['features']:
return True
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
return feature in info['disco_info']['features']
except IqError:
return False
except IqTimeout:
return None
def has_identity(self, jid, node, ifrom, data):
"""
Check if a JID has a given identity.
The data parameter may provide:
category -- The category of the identity to check.
itype -- The type of the identity to check.
lang -- The language of the identity to check.
local -- If true, then the query is for a JID/node
combination handled by this Sleek instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
identity = (data.get('category', None),
data.get('itype', None),
data.get('lang', None))
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
trunc = lambda i: (i[0], i[1], i[2])
if node in (None, ''):
info = self.caps.get_caps(jid)
if info and identity in map(trunc, info['identities']):
return True
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
return identity in map(trunc, info['disco_info']['identities'])
except IqError:
return False
except IqTimeout:
return None
def cache_caps(self, jid, node, ifrom, data):
with self.static.lock:
verstring = data.get('verstring', None)
info = data.get('info', None)
if not verstring or not info:
return
self.ver_cache[verstring] = info
def assign_verstring(self, jid, node, ifrom, data):
with self.static.lock:
if isinstance(jid, JID):
jid = jid.full
self.jid_vers[jid] = data.get('verstring', None)
def get_verstring(self, jid, node, ifrom, data):
with self.static.lock:
return self.jid_vers.get(jid, None)
def get_caps(self, jid, node, ifrom, data):
with self.static.lock:
return self.ver_cache.get(data.get('verstring', None), None)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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.

View file

@ -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)

View file

@ -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')