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) reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None return reqXML is not None
def get_value(self): def get_value(self, convert=True):
valsXML = self.xml.findall('{%s}value' % self.namespace) valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0: if len(valsXML) == 0:
return None return None
elif self._type == 'boolean': elif self._type == 'boolean':
if convert:
return valsXML[0].text in self.true_values return valsXML[0].text in self.true_values
return valsXML[0].text
elif self._type in self.multi_value_types or len(valsXML) > 1: elif self._type in self.multi_value_types or len(valsXML) > 1:
values = [] values = []
for valXML in valsXML: for valXML in valsXML:
if valXML.text is None: if valXML.text is None:
valXML.text = '' valXML.text = ''
values.append(valXML.text) values.append(valXML.text)
if self._type == 'text-multi': if self._type == 'text-multi' and condense:
values = "\n".join(values) values = "\n".join(values)
return values return values
else: else:

View file

@ -10,7 +10,7 @@ import logging
import sleekxmpp import sleekxmpp
from sleekxmpp import Iq 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.plugins.base import base_plugin
from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.matcher import StanzaPath
@ -108,11 +108,16 @@ class xep_0030(base_plugin):
self.static = StaticDisco(self.xmpp) self.static = StaticDisco(self.xmpp)
self._disco_ops = ['get_info', 'set_identities', 'set_features', self.use_cache = self.config.get('use_cache', True)
'get_items', 'set_items', 'del_items', self.wrap_results = self.config.get('wrap_results', False)
'add_identity', 'del_identity', 'add_feature',
'del_feature', 'add_item', 'del_item', self._disco_ops = [
'del_identities', 'del_features'] '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.default_handlers = {}
self._handlers = {} self._handlers = {}
for op in self._disco_ops: for op in self._disco_ops:
@ -237,7 +242,78 @@ class xep_0030(base_plugin):
self.del_node_handler(op, jid, node) self.del_node_handler(op, jid, node)
self.set_node_handler(op, jid, node, self.default_handlers[op]) 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. 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. no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info. 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. ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply. block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for timeout -- The time in seconds to block while waiting for
@ -266,11 +349,30 @@ class xep_0030(base_plugin):
received instead of blocking and waiting for received instead of blocking and waiting for
the reply. 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 " + \ log.debug("Looking up local disco#info data " + \
"for %s, node %s.", jid, node) "for %s, node %s.", jid, node)
info = self._run_node_handler('get_info', jid, node, kwargs) info = self._run_node_handler('get_info',
return self._fix_default_info(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() iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility # Check dfrom parameter for backwards compatibility
@ -282,6 +384,15 @@ class xep_0030(base_plugin):
block=kwargs.get('block', True), block=kwargs.get('block', True),
callback=kwargs.get('callback', None)) 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): def get_items(self, jid=None, node=None, local=False, **kwargs):
""" """
Retrieve the disco#items results from a given JID/node combination. 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. Otherwise the parameter is ignored.
""" """
if local or jid is None: 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() iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility # Check dfrom parameter for backwards compatibility
@ -341,7 +454,7 @@ class xep_0030(base_plugin):
node -- Optional node to modify. node -- Optional node to modify.
items -- A series of items in tuple format. 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): def del_items(self, jid=None, node=None, **kwargs):
""" """
@ -351,7 +464,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify. jid -- The JID to modify.
node -- Optional node 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): def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
""" """
@ -372,7 +485,7 @@ class xep_0030(base_plugin):
kwargs = {'ijid': jid, kwargs = {'ijid': jid,
'name': name, 'name': name,
'inode': subnode} '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): def del_item(self, jid=None, node=None, **kwargs):
""" """
@ -384,7 +497,7 @@ class xep_0030(base_plugin):
ijid -- The item's JID. ijid -- The item's JID.
inode -- The item's node. 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='', def add_identity(self, category='', itype='', name='',
node=None, jid=None, lang=None): node=None, jid=None, lang=None):
@ -411,7 +524,7 @@ class xep_0030(base_plugin):
'itype': itype, 'itype': itype,
'name': name, 'name': name,
'lang': lang} '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): def add_feature(self, feature, node=None, jid=None):
""" """
@ -423,7 +536,7 @@ class xep_0030(base_plugin):
jid -- The JID to modify. jid -- The JID to modify.
""" """
kwargs = {'feature': feature} 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): 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. name -- Optional, human readable name for the identity.
lang -- Optional, the identity's xml:lang value. 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): def del_feature(self, jid=None, node=None, **kwargs):
""" """
@ -448,7 +561,7 @@ class xep_0030(base_plugin):
node -- The node to modify. node -- The node to modify.
feature -- The feature's namespace. 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): 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. identities -- A set of identities in tuple form.
lang -- Optional, xml:lang value. 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): 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 lang -- Optional. If given, only remove identities
using this xml:lang value. 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): def set_features(self, jid=None, node=None, **kwargs):
""" """
@ -490,7 +603,7 @@ class xep_0030(base_plugin):
node -- The node to modify. node -- The node to modify.
features -- The new set of supported features. 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): def del_features(self, jid=None, node=None, **kwargs):
""" """
@ -500,9 +613,9 @@ class xep_0030(base_plugin):
jid -- The JID to modify. jid -- The JID to modify.
node -- The node 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 Execute the most specific node handler for the given
JID/node combination. JID/node combination.
@ -513,7 +626,7 @@ class xep_0030(base_plugin):
node -- The node requested. node -- The node requested.
data -- Optional, custom data to pass to the handler. data -- Optional, custom data to pass to the handler.
""" """
if jid is None: if jid in (None, ''):
if self.xmpp.is_component: if self.xmpp.is_component:
jid = self.xmpp.boundjid.full jid = self.xmpp.boundjid.full
else: else:
@ -521,12 +634,26 @@ class xep_0030(base_plugin):
if node is None: if node is None:
node = '' node = ''
try:
args = (jid, node, ifrom, data)
if self._handlers[htype]['node'].get((jid, node), False): if self._handlers[htype]['node'].get((jid, node), False):
return self._handlers[htype]['node'][(jid, node)](jid, node, data) return self._handlers[htype]['node'][(jid, node)](*args)
elif self._handlers[htype]['jid'].get(jid, False): elif self._handlers[htype]['jid'].get(jid, False):
return self._handlers[htype]['jid'][jid](jid, node, data) return self._handlers[htype]['jid'][jid](*args)
elif self._handlers[htype]['global']: elif self._handlers[htype]['global']:
return self._handlers[htype]['global'](jid, node, data) 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: else:
return None return None
@ -550,6 +677,7 @@ class xep_0030(base_plugin):
info = self._run_node_handler('get_info', info = self._run_node_handler('get_info',
jid, jid,
iq['disco_info']['node'], iq['disco_info']['node'],
iq['from'],
iq) iq)
if isinstance(info, Iq): if isinstance(info, Iq):
info.send() info.send()
@ -561,7 +689,19 @@ class xep_0030(base_plugin):
iq.send() iq.send()
elif iq['type'] == 'result': elif iq['type'] == 'result':
log.debug("Received disco info result from " + \ log.debug("Received disco info result from " + \
"%s to %s.", iq['from'], iq['to']) "<%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) self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq): def _handle_disco_items(self, iq):
@ -583,6 +723,7 @@ class xep_0030(base_plugin):
items = self._run_node_handler('get_items', items = self._run_node_handler('get_items',
jid, jid,
iq['disco_items']['node'], iq['disco_items']['node'],
iq['from'].full,
iq) iq)
if isinstance(items, Iq): if isinstance(items, Iq):
items.send() items.send()
@ -607,6 +748,9 @@ class xep_0030(base_plugin):
Arguments: Arguments:
info -- The disco#info quest (not the full Iq stanza) to modify. 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['node']:
if not info['identities']: if not info['identities']:
if self.xmpp.is_component: if self.xmpp.is_component:
@ -621,7 +765,29 @@ class xep_0030(base_plugin):
log.debug("No features found for this entity. " + \ log.debug("No features found for this entity. " + \
"Using default disco#info feature.") "Using default disco#info feature.")
info.add_feature(info.namespace) 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 # Retain some backwards compatibility

View file

@ -146,7 +146,7 @@ class DiscoInfo(ElementBase):
return True return True
return False 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: Return a set of all identities in tuple form as so:
(category, type, lang, name) (category, type, lang, name)
@ -156,16 +156,24 @@ class DiscoInfo(ElementBase):
Arguments: 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.
""" """
if dedupe:
identities = set() identities = set()
else:
identities = []
for id_xml in self.findall('{%s}identity' % self.namespace): for id_xml in self.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
if lang is None or xml_lang == lang: if lang is None or xml_lang == lang:
identities.add(( id = (id_xml.attrib['category'],
id_xml.attrib['category'],
id_xml.attrib['type'], id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None), id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None))) id_xml.attrib.get('name', None))
if dedupe:
identities.add(id)
else:
identities.append(id)
return identities return identities
def set_identities(self, identities, lang=None): def set_identities(self, identities, lang=None):
@ -237,11 +245,17 @@ class DiscoInfo(ElementBase):
return True return True
return False return False
def get_features(self): def get_features(self, dedupe=True):
"""Return the set of all supported features.""" """Return the set of all supported features."""
if dedupe:
features = set() features = set()
else:
features = []
for feature_xml in self.findall('{%s}feature' % self.namespace): for feature_xml in self.findall('{%s}feature' % self.namespace):
if dedupe:
features.add(feature_xml.attrib['var']) features.add(feature_xml.attrib['var'])
else:
features.append(feature_xml.attrib['var'])
return features return features
def set_features(self, features): def set_features(self, features):

View file

@ -7,6 +7,7 @@
""" """
import logging import logging
import threading
import sleekxmpp import sleekxmpp
from sleekxmpp import Iq from sleekxmpp import Iq
@ -50,8 +51,10 @@ class StaticDisco(object):
""" """
self.nodes = {} self.nodes = {}
self.xmpp = xmpp 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 Create a new set of stanzas for the provided
JID and node combination. JID and node combination.
@ -60,83 +63,219 @@ class StaticDisco(object):
jid -- The JID that will own the new stanzas. jid -- The JID that will own the new stanzas.
node -- The node that will own the new stanzas. node -- The node that will own the new stanzas.
""" """
with self.lock:
if jid is None: if jid is None:
jid = self.xmpp.boundjid.full jid = self.xmpp.boundjid.full
if node is None: if node is None:
node = '' node = ''
if (jid, node) not in self.nodes: if ifrom is None:
self.nodes[(jid, node)] = {'info': DiscoInfo(), 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()} 'items': DiscoItems()}
self.nodes[(jid, node)]['info']['node'] = node self.nodes[(jid, node, ifrom)]['info']['node'] = node
self.nodes[(jid, node)]['items']['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 # Node Handlers
# #
# Each handler accepts three arguments: jid, node, and data. # Each handler accepts four arguments: jid, node, ifrom, and data.
# The jid and node parameters together determine the set of # The jid and node parameters together determine the set of info
# info and items stanzas that will be retrieved or added. # and items stanzas that will be retrieved or added. Additionally,
# The data parameter is a dictionary with additional paramters # the ifrom value allows for cached results when results vary based
# that will be passed to other calls. # 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. Return the stored info data for the requested JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) not in self.nodes: with self.lock:
if not self.node_exists(jid, node):
if not node: if not node:
return DiscoInfo() return DiscoInfo()
else: else:
raise XMPPError(condition='item-not-found') raise XMPPError(condition='item-not-found')
else: 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. Reset the info stanza for a given JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) in self.nodes: with self.lock:
self.nodes[(jid, node)]['info'] = DiscoInfo() 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. Return the stored items data for the requested JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) not in self.nodes: with self.lock:
if not self.node_exists(jid, node):
if not node: if not node:
return DiscoInfo() return DiscoInfo()
else: else:
raise XMPPError(condition='item-not-found') raise XMPPError(condition='item-not-found')
else: 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. 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 -- A set of items in tuple format.
""" """
with self.lock:
items = data.get('items', set()) items = data.get('items', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['items']['items'] = items 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. Reset the items stanza for a given JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) in self.nodes: with self.lock:
self.nodes[(jid, node)]['items'] = DiscoItems() 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. Add a new identity to te JID/node combination.
@ -146,14 +285,15 @@ class StaticDisco(object):
name -- Optional human readable name for this identity. name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value. lang -- Optional standard xml:lang value.
""" """
with self.lock:
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_identity( self.get_node(jid, node)['info'].add_identity(
data.get('category', ''), data.get('category', ''),
data.get('itype', ''), data.get('itype', ''),
data.get('name', None), data.get('name', None),
data.get('lang', 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. 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: identities -- A list of identities in tuple form:
(category, type, name, lang) (category, type, name, lang)
""" """
with self.lock:
identities = data.get('identities', set()) identities = data.get('identities', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info']['identities'] = identities 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. Remove an identity from a JID/node combination.
@ -175,67 +316,70 @@ class StaticDisco(object):
name -- Optional human readable name for this identity. name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value. lang -- Optional, standard xml:lang value.
""" """
if (jid, node) not in self.nodes: with self.lock:
return if self.node_exists(jid, node):
self.nodes[(jid, node)]['info'].del_identity( self.get_node(jid, node)['info'].del_identity(
data.get('category', ''), data.get('category', ''),
data.get('itype', ''), data.get('itype', ''),
data.get('name', None), data.get('name', None),
data.get('lang', 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. Remove all identities from a JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) not in self.nodes: with self.lock:
return if self.node_exists(jid, node):
del self.nodes[(jid, node)]['info']['identities'] 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. Add a feature to a JID/node combination.
The data parameter should include: The data parameter should include:
feature -- The namespace of the supported feature. feature -- The namespace of the supported feature.
""" """
with self.lock:
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) 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. Add or replace all features for a JID/node combination.
The data parameter should include: The data parameter should include:
features -- The new set of supported features. features -- The new set of supported features.
""" """
with self.lock:
features = data.get('features', set()) features = data.get('features', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info']['features'] = features 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. Remove a feature from a JID/node combination.
The data parameter should include: The data parameter should include:
feature -- The namespace of the removed feature. feature -- The namespace of the removed feature.
""" """
if (jid, node) not in self.nodes: with self.lock:
return if self.node_exists(jid, node):
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) 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. Remove all features from a JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) not in self.nodes: with self.lock:
if not self.node_exists(jid, node):
return return
del self.nodes[(jid, node)]['info']['features'] 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. Add an item to a JID/node combination.
@ -245,13 +389,14 @@ class StaticDisco(object):
non-addressable items. non-addressable items.
name -- Optional human readable name for the item. name -- Optional human readable name for the item.
""" """
with self.lock:
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['items'].add_item( self.get_node(jid, node)['items'].add_item(
data.get('ijid', ''), data.get('ijid', ''),
node=data.get('inode', ''), node=data.get('inode', ''),
name=data.get('name', '')) 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. Remove an item from a JID/node combination.
@ -259,7 +404,38 @@ class StaticDisco(object):
ijid -- JID of the item to remove. ijid -- JID of the item to remove.
inode -- Optional extra identifying information. inode -- Optional extra identifying information.
""" """
if (jid, node) in self.nodes: with self.lock:
self.nodes[(jid, node)]['items'].del_item( if self.node_exists(jid, node):
self.get_node(jid, node)['items'].del_item(
data.get('ijid', ''), data.get('ijid', ''),
node=data.get('inode', None)) 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 as extended information, replacing any
existing extensions. 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): 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 data -- Either a form, or a list of forms to add
as extended information. 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): def del_extended_info(self, jid=None, node=None, **kwargs):
""" """
@ -98,4 +98,4 @@ class xep_0128(base_plugin):
jid -- The JID to modify. jid -- The JID to modify.
node -- The node 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 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. Replace the extended identity data for a JID/node combination.
The data parameter may provide: The data parameter may provide:
data -- Either a single data form, or a list of data forms. data -- Either a single data form, or a list of data forms.
""" """
self.del_extended_info(jid, node, data) with self.static.lock:
self.add_extended_info(jid, node, data) 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. Add additional extended identity data for a JID/node combination.
The data parameter may provide: The data parameter may provide:
data -- Either a single data form, or a list of data forms. data -- Either a single data form, or a list of data forms.
""" """
with self.static.lock:
self.static.add_node(jid, node) self.static.add_node(jid, node)
forms = data.get('data', []) forms = data.get('data', [])
if not isinstance(forms, list): if not isinstance(forms, list):
forms = [forms] forms = [forms]
info = self.static.get_node(jid, node)['info']
for form in forms: for form in forms:
self.static.nodes[(jid, node)]['info'].append(form) 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. Replace the extended identity data for a JID/node combination.
The data parameter is not used. The data parameter is not used.
""" """
if (jid, node) not in self.static.nodes: with self.static.lock:
return if self.static.node_exists(jid, node):
info = self.static.get_node(jid, node)['info']
info = self.static.nodes[(jid, node)]['info']
for form in info['substanza']: for form in info['substanza']:
info.xml.remove(form.xml) info.xml.remove(form.xml)

View file

@ -345,7 +345,8 @@ class ElementBase(object):
""" """
if attrib not in self.plugins: if attrib not in self.plugins:
plugin_class = self.plugin_attrib_map[attrib] 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 self.plugins[attrib] = plugin
if plugin_class in self.plugin_iterables: if plugin_class in self.plugin_iterables:
self.iterables.append(plugin) self.iterables.append(plugin)
@ -1251,7 +1252,7 @@ class StanzaBase(ElementBase):
stanza sent immediately. Useful for stream stanza sent immediately. Useful for stream
initialization. Defaults to ``False``. initialization. Defaults to ``False``.
""" """
self.stream.send_raw(self.__str__(), now=now) self.stream.send(self, now=now)
def __copy__(self): def __copy__(self):
"""Return a copy of the stanza object that does not share the """Return a copy of the stanza object that does not share the

View file

@ -35,7 +35,7 @@ except ImportError:
import sleekxmpp import sleekxmpp
from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.thirdparty.statemachine import StateMachine
from sleekxmpp.xmlstream import Scheduler, tostring 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.handler import Waiter, XMLCallback
from sleekxmpp.xmlstream.matcher import MatchXMLMask from sleekxmpp.xmlstream.matcher import MatchXMLMask
@ -268,6 +268,7 @@ class XMLStream(object):
self.__handlers = [] self.__handlers = []
self.__event_handlers = {} self.__event_handlers = {}
self.__event_handlers_lock = threading.Lock() self.__event_handlers_lock = threading.Lock()
self.__filters = {'in': [], 'out': []}
self._id = 0 self._id = 0
self._id_lock = threading.Lock() self._id_lock = threading.Lock()
@ -743,6 +744,28 @@ class XMLStream(object):
""" """
del self.__root_stanza[stanza_class] 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, def add_handler(self, mask, pointer, name=None, disposable=False,
threaded=False, filter=False, instream=False): threaded=False, filter=False, instream=False):
"""A shortcut method for registering a handler using XML masks. """A shortcut method for registering a handler using XML masks.
@ -994,6 +1017,14 @@ class XMLStream(object):
timeout = self.response_timeout timeout = self.response_timeout
if hasattr(mask, 'xml'): if hasattr(mask, 'xml'):
mask = 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) data = str(data)
if mask is not None: if mask is not None:
log.warning("Use of send mask waiters is deprecated.") log.warning("Use of send mask waiters is deprecated.")
@ -1246,8 +1277,6 @@ class XMLStream(object):
:param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
stanza to analyze. stanza to analyze.
""" """
log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns,
stream=self))
# Apply any preprocessing filters. # Apply any preprocessing filters.
xml = self.incoming_filter(xml) xml = self.incoming_filter(xml)
@ -1255,6 +1284,15 @@ class XMLStream(object):
# stanza type applies, a generic StanzaBase stanza will be used. # stanza type applies, a generic StanzaBase stanza will be used.
stanza = self._build_stanza(xml) 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 # Match the stanza against registered handlers. Handlers marked
# to run "in stream" will be executed immediately; the rest will # to run "in stream" will be executed immediately; the rest will
# be queued. # 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', self.stream_start(mode='client',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_jid(jid, node, iq): def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo() result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info') result.add_identity('client', 'console', name='Dynamic Info')
@ -158,7 +158,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost', jid='tester.localhost',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_global(jid, node, iq): def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo() result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info') result.add_identity('component', 'generic', name='Dynamic Info')
@ -194,7 +194,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client', self.stream_start(mode='client',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_jid(jid, node, iq): def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo() result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info') result.add_identity('client', 'console', name='Dynamic Info')
@ -236,7 +236,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost', jid='tester.localhost',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_global(jid, node, iq): def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo() result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info') result.add_identity('component', 'generic', name='Dynamic Info')
@ -325,7 +325,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client', self.stream_start(mode='client',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_jid(jid, node, iq): def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems() result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node result['node'] = node
result.add_item('tester@localhost', node='foo', name='JID') result.add_item('tester@localhost', node='foo', name='JID')
@ -359,7 +359,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost', jid='tester.localhost',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_global(jid, node, iq): def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems() result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global') result.add_item('tester@localhost', node='foo', name='Global')
@ -393,7 +393,7 @@ class TestStreamDisco(SleekTest):
self.stream_start(mode='client', self.stream_start(mode='client',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_jid(jid, node, iq): def dynamic_jid(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems() result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global') result.add_item('tester@localhost', node='foo', name='Global')
@ -435,7 +435,7 @@ class TestStreamDisco(SleekTest):
jid='tester.localhost', jid='tester.localhost',
plugins=['xep_0030']) plugins=['xep_0030'])
def dynamic_global(jid, node, iq): def dynamic_global(jid, node, ifrom, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems() result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node result['node'] = node
result.add_item('tester.localhost', node='foo', name='Global') result.add_item('tester.localhost', node='foo', name='Global')