Merge branch 'develop' into roster

Conflicts:
	sleekxmpp/basexmpp.py
This commit is contained in:
Lance Stout 2010-12-13 14:36:53 -05:00
commit c16913c999
19 changed files with 1951 additions and 450 deletions

View file

@ -111,6 +111,9 @@ class BaseXMPP(XMLStream):
self.boundjid = JID(jid)
self.plugin = {}
self.plugin_config = {}
self.plugin_whitelist = []
self.roster = roster.Roster(self)
self.roster.add(self.boundjid.bare)

View file

@ -1,337 +0,0 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
log = logging.getLogger(__name__)
class DiscoInfo(ElementBase):
namespace = 'http://jabber.org/protocol/disco#info'
name = 'query'
plugin_attrib = 'disco_info'
interfaces = set(('node', 'features', 'identities'))
def getFeatures(self):
features = []
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
features.append(feature.attrib['var'])
return features
def setFeatures(self, features):
self.delFeatures()
for name in features:
self.addFeature(name)
def delFeatures(self):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for feature in featuresXML:
self.xml.remove(feature)
def addFeature(self, feature):
featureXML = ET.Element('{%s}feature' % self.namespace,
{'var': feature})
self.xml.append(featureXML)
def delFeature(self, feature):
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
for featureXML in featuresXML:
if featureXML.attrib['var'] == feature:
self.xml.remove(featureXML)
def getIdentities(self):
ids = []
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'],
idXML.attrib.get('name', ''))
ids.append(idData)
return ids
def setIdentities(self, ids):
self.delIdentities()
for idData in ids:
self.addIdentity(*idData)
def delIdentities(self):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
self.xml.remove(idXML)
def addIdentity(self, category, id_type, name=''):
idXML = ET.Element('{%s}identity' % self.namespace,
{'category': category,
'type': id_type,
'name': name})
self.xml.append(idXML)
def delIdentity(self, category, id_type, name=''):
idsXML = self.xml.findall('{%s}identity' % self.namespace)
for idXML in idsXML:
idData = (idXML.attrib['category'],
idXML.attrib['type'])
delId = (category, id_type)
if idData == delId:
self.xml.remove(idXML)
class DiscoItems(ElementBase):
namespace = 'http://jabber.org/protocol/disco#items'
name = 'query'
plugin_attrib = 'disco_items'
interfaces = set(('node', 'items'))
def getItems(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
itemData = (item.attrib['jid'],
item.attrib.get('node'),
item.attrib.get('name'))
items.append(itemData)
return items
def setItems(self, items):
self.delItems()
for item in items:
self.addItem(*item)
def delItems(self):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for item in itemsXML:
self.xml.remove(item)
def addItem(self, jid, node='', name=''):
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
if name:
itemXML.attrib['name'] = name
if node:
itemXML.attrib['node'] = node
self.xml.append(itemXML)
def delItem(self, jid, node=''):
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
itemData = (itemXML.attrib['jid'],
itemXML.attrib.get('node', ''))
itemDel = (jid, node)
if itemData == itemDel:
self.xml.remove(itemXML)
class DiscoNode(object):
"""
Collection object for grouping info and item information
into nodes.
"""
def __init__(self, name):
self.name = name
self.info = DiscoInfo()
self.items = DiscoItems()
self.info['node'] = name
self.items['node'] = name
# This is a bit like poor man's inheritance, but
# to simplify adding information to the node we
# map node functions to either the info or items
# stanza objects.
#
# We don't want to make DiscoNode inherit from
# DiscoInfo and DiscoItems because DiscoNode is
# not an actual stanza, and doing so would create
# confusion and potential bugs.
self._map(self.items, 'items', ['get', 'set', 'del'])
self._map(self.items, 'item', ['add', 'del'])
self._map(self.info, 'identities', ['get', 'set', 'del'])
self._map(self.info, 'identity', ['add', 'del'])
self._map(self.info, 'features', ['get', 'set', 'del'])
self._map(self.info, 'feature', ['add', 'del'])
def isEmpty(self):
"""
Test if the node contains any information. Useful for
determining if a node can be deleted.
"""
ids = self.getIdentities()
features = self.getFeatures()
items = self.getItems()
if not ids and not features and not items:
return True
return False
def _map(self, obj, interface, access):
"""
Map functions of the form obj.accessInterface
to self.accessInterface for each given access type.
"""
interface = interface.title()
for access_type in access:
method = access_type + interface
if hasattr(obj, method):
setattr(self, method, getattr(obj, method))
class xep_0030(base.base_plugin):
"""
XEP-0030 Service Discovery
"""
def plugin_init(self):
self.xep = '0030'
self.description = 'Service Discovery'
self.xmpp.registerHandler(
Callback('Disco Items',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoItems.namespace)),
self.handle_item_query))
self.xmpp.registerHandler(
Callback('Disco Info',
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
DiscoInfo.namespace)),
self.handle_info_query))
registerStanzaPlugin(Iq, DiscoInfo)
registerStanzaPlugin(Iq, DiscoItems)
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
self.nodes = {'main': DiscoNode('main')}
def add_node(self, node):
if node not in self.nodes:
self.nodes[node] = DiscoNode(node)
def del_node(self, node):
if node in self.nodes:
del self.nodes[node]
def rename_node(self, node, new_name):
if new_name not in self.nodes and node in self.nodes:
self.nodes[new_name] = self.nodes[node]
self.nodes[new_name].name = new_name
self.nodes[new_name].info['node'] = new_name
self.nodes[new_name].items['node'] = new_name
self.del_node(node)
def handle_item_query(self, iq):
if iq['type'] == 'get':
log.debug("Items requested by %s" % iq['from'])
self.xmpp.event('disco_items_request', iq)
elif iq['type'] == 'result':
log.debug("Items result from %s" % iq['from'])
self.xmpp.event('disco_items', iq)
def handle_info_query(self, iq):
if iq['type'] == 'get':
log.debug("Info requested by %s" % iq['from'])
self.xmpp.event('disco_info_request', iq)
elif iq['type'] == 'result':
log.debug("Info result from %s" % iq['from'])
self.xmpp.event('disco_info', iq)
def handle_disco_info(self, iq, forwarded=False):
"""
A default handler for disco#info requests. If another
handler is registered, this one will defer and not run.
"""
if not forwarded and self.xmpp.event_handled('disco_info_request'):
return
node_name = iq['disco_info']['node']
if not node_name:
node_name = 'main'
log.debug("Using default handler for disco#info on node '%s'." % node_name)
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.info.xml).send()
else:
log.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_info'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
def handle_disco_items(self, iq, forwarded=False):
"""
A default handler for disco#items requests. If another
handler is registered, this one will defer and not run.
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('disco_items_request'):
return
node_name = iq['disco_items']['node']
if not node_name:
node_name = 'main'
log.debug("Using default handler for disco#items on node '%s'." % node_name)
if node_name in self.nodes:
node = self.nodes[node_name]
iq.reply().setPayload(node.items.xml).send()
else:
log.debug("Node %s requested, but does not exist." % node_name)
iq.reply().error().setPayload(iq['disco_items'].xml)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
iq.send()
# Older interface methods for backwards compatibility
def getInfo(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = dfrom
iq['disco_info']['node'] = node
return iq.send()
def getItems(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = dfrom
iq['disco_items']['node'] = node
return iq.send()
def add_feature(self, feature, node='main'):
self.add_node(node)
self.nodes[node].addFeature(feature)
def add_identity(self, category='', itype='', name='', node='main'):
self.add_node(node)
self.nodes[node].addIdentity(category=category,
id_type=itype,
name=name)
def add_item(self, jid=None, name='', node='main', subnode=''):
self.add_node(node)
self.add_node(subnode)
if jid is None:
jid = self.xmpp.fulljid
self.nodes[node].addItem(jid=jid, name=name, node=subnode)

View file

@ -0,0 +1,12 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.xep_0030 import stanza
from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
from sleekxmpp.plugins.xep_0030.static import StaticDisco
from sleekxmpp.plugins.xep_0030.disco import xep_0030

View file

@ -0,0 +1,314 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 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 import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
log = logging.getLogger(__name__)
class xep_0030(base_plugin):
"""
XEP-0030: Service Discovery
Stream Handlers:
Disco Info --
Disco Items --
Events:
disco_info --
disco_items --
disco_info_query --
disco_items_query --
Methods:
set_node_handler --
del_node_handler --
add_identity --
del_identity --
add_feature --
del_feature --
add_item --
del_item --
get_info --
get_items --
"""
def plugin_init(self):
self.xep = '0030'
self.description = 'Service Discovery'
self.stanza = sleekxmpp.plugins.xep_0030.stanza
self.xmpp.register_handler(
Callback('Disco Info',
StanzaPath('iq/disco_info'),
self._handle_disco_info))
self.xmpp.register_handler(
Callback('Disco Items',
StanzaPath('iq/disco_items'),
self._handle_disco_items))
register_stanza_plugin(Iq, DiscoInfo)
register_stanza_plugin(Iq, DiscoItems)
self.static = StaticDisco(self.xmpp)
self._disco_ops = ['get_info', 'set_identities', 'set_features',
'del_info', 'get_items', 'set_items', 'del_items',
'add_identity', 'del_identity', 'add_feature',
'del_feature', 'add_item', 'del_item']
self.handlers = {}
for op in self._disco_ops:
self.handlers[op] = {'global': getattr(self.static, op),
'jid': {},
'node': {}}
def set_node_handler(self, htype, jid=None, node=None, handler=None):
"""
Arguments:
htype
jid
node
handler
"""
if htype not in self._disco_ops:
return
if jid is None and node is None:
self.handlers[htype]['global'] = handler
elif node is None:
self.handlers[htype]['jid'][jid] = handler
elif jid is None:
jid = self.xmpp.boundjid.full
self.handlers[htype]['node'][(jid, node)] = handler
else:
self.handlers[htype]['node'][(jid, node)] = handler
def del_node_handler(self, htype, jid, node):
"""
Arguments:
htype
jid
node
"""
self.set_node_handler(htype, jid, node, None)
def make_static(self, jid=None, node=None, handlers=None):
"""
Change all of a node's handlers to the default static
handlers. Useful for manually overriding the contents
of a node that would otherwise be handled by a JID level
or global level dynamic handler.
Arguments:
jid -- The JID owning the node to modify.
node -- The node to change to using static handlers.
handlers -- Optional list of handlers to change to the
static version. If provided, only these
handlers will be changed. Otherwise, all
handlers will use the static version.
"""
if handlers is None:
handlers = self._disco_ops
for op in handlers:
self.del_node_handler(op, jid, node)
self.set_node_handler(op, jid, node, getattr(self.static, op))
def get_info(self, jid=None, node=None, local=False, **kwargs):
"""
Arguments:
jid --
node --
local --
dfrom --
block --
timeout --
callback --
"""
if local or jid is None:
log.debug("Looking up local disco#info data " + \
"for %s, node %s." % (jid, node))
info = self._run_node_handler('get_info', jid, node, kwargs)
return self._fix_default_info(info)
iq = self.xmpp.Iq()
iq['from'] = kwargs.get('dfrom', '')
iq['to'] = jid
iq['type'] = 'get'
iq['disco_info']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', None),
callback=kwargs.get('callback', None))
def get_items(self, jid=None, node=None, local=False, **kwargs):
"""
Arguments:
jid --
node --
local --
dfrom --
block --
timeout --
callback --
"""
if local or jid is None:
return self._run_node_handler('get_items', jid, node, kwargs)
iq = self.xmpp.Iq()
iq['from'] = kwargs.get('dfrom', '')
iq['to'] = jid
iq['type'] = 'get'
iq['disco_items']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', None),
callback=kwargs.get('callback', None))
def set_info(self, jid=None, node=None, **kwargs):
self._run_node_handler('set_info', jid, node, kwargs)
def del_info(self, jid=None, node=None, **kwargs):
self._run_node_handler('del_info', jid, node, kwargs)
def set_items(self, jid=None, node=None, **kwargs):
self._run_node_handler('set_items', jid, node, kwargs)
def del_items(self, jid=None, node=None, **kwargs):
self._run_node_handler('del_items', jid, node, kwargs)
def add_identity(self, jid=None, node=None, **kwargs):
self._run_node_handler('add_identity', jid, node, kwargs)
def add_feature(self, jid=None, node=None, **kwargs):
self._run_node_handler('add_feature', jid, node, kwargs)
def del_identity(self, jid=None, node=None, **kwargs):
self._run_node_handler('del_identity', jid, node, kwargs)
def del_feature(self, jid=None, node=None, **kwargs):
self._run_node_handler('del_feature', jid, node, kwargs)
def add_item(self, jid=None, node=None, **kwargs):
self._run_node_handler('add_item', jid, node, kwargs)
def del_item(self, jid=None, node=None, **kwargs):
self._run_node_handler('del_item', jid, node, kwargs)
def _run_node_handler(self, htype, jid, node, data=None):
"""
Execute the most specific node handler for the given
JID/node combination.
Arguments:
htype -- The handler type to execute.
jid -- The JID requested.
node -- The node requested.
dat -- Optional, custom data to pass to the handler.
"""
if jid is None:
jid = self.xmpp.boundjid.full
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
def _handle_disco_info(self, iq):
"""
Process an incoming disco#info stanza. If it is a get
request, find and return the appropriate identities
and features. If it is an info result, fire the
disco_info event.
Arguments:
iq -- The incoming disco#items stanza.
"""
if iq['type'] == 'get':
log.debug("Received disco info query from " + \
"<%s> to <%s>." % (iq['from'], iq['to']))
info = self._run_node_handler('get_info',
iq['to'].full,
iq['disco_info']['node'],
iq)
iq.reply()
if info:
info = self._fix_default_info(info)
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']))
self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq):
"""
Process an incoming disco#items stanza. If it is a get
request, find and return the appropriate items. If it
is an items result, fire the disco_items event.
Arguments:
iq -- The incoming disco#items stanza.
"""
if iq['type'] == 'get':
log.debug("Received disco items query from " + \
"<%s> to <%s>." % (iq['from'], iq['to']))
items = self._run_node_handler('get_items',
iq['to'].full,
iq['disco_items']['node'])
iq.reply()
if items:
iq.set_payload(items.xml)
iq.send()
elif iq['type'] == 'result':
log.debug("Received disco items result from" + \
"%s to %s." % (iq['from'], iq['to']))
self.xmpp.event('disco_items', iq)
def _fix_default_info(self, info):
"""
Disco#info results for a JID are required to include at least
one identity and feature. As a default, if no other identity is
provided, SleekXMPP will use either the generic component or the
bot client identity. A the standard disco#info feature will also be
added if no features are provided.
Arguments:
info -- The disco#info quest (not the full Iq stanza) to modify.
"""
if not info['node']:
if not info['identities']:
if self.xmpp.is_component:
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." + \
"Using default client identity.")
info.add_identity('client', 'bot')
if not info['features']:
log.debug("No features found for this entity." + \
"Using default disco#info feature.")
info.add_feature(info.namespace)
return info

View file

@ -0,0 +1,10 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo
from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems

View file

@ -0,0 +1,262 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase, ET
class DiscoInfo(ElementBase):
"""
XMPP allows for users and agents to find the identities and features
supported by other entities in the XMPP network through service discovery,
or "disco". In particular, the "disco#info" query type for <iq> stanzas is
used to request the list of identities and features offered by a JID.
An identity is a combination of a category and type, such as the 'client'
category with a type of 'pc' to indicate the agent is a human operated
client with a GUI, or a category of 'gateway' with a type of 'aim' to
identify the agent as a gateway for the legacy AIM protocol. See
<http://xmpp.org/registrar/disco-categories.html> for a full list of
accepted category and type combinations.
Features are simply a set of the namespaces that identify the supported
features. For example, a client that supports service discovery will
include the feature 'http://jabber.org/protocol/disco#info'.
Since clients and components may operate in several roles at once, identity
and feature information may be grouped into "nodes". If one were to write
all of the identities and features used by a client, then node names would
be like section headings.
Example disco#info stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" name="SleekXMPP Bot" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="jabber:x:data" />
<feature var="urn:xmpp:ping" />
</query>
</iq>
Stanza Interface:
node -- The name of the node to either
query or return info from.
identities -- A set of 4-tuples, where each tuple contains
the category, type, xml:lang, and name
of an identity.
features -- A set of namespaces for features.
Methods:
add_identity -- Add a new, single identity.
del_identity -- Remove a single identity.
get_identities -- Return all identities in tuple form.
set_identities -- Use multiple identities, each given in tuple form.
del_identities -- Remove all identities.
add_feature -- Add a single feature.
del_feature -- Remove a single feature.
get_features -- Return a list of all features.
set_features -- Use a given list of features.
del_features -- Remove all features.
"""
name = 'query'
namespace = 'http://jabber.org/protocol/disco#info'
plugin_attrib = 'disco_info'
interfaces = set(('node', 'features', 'identities'))
lang_interfaces = set(('identities',))
# Cache identities and features
_identities = set()
_features = set()
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides ElementBase.setup
Caches identity and feature information.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
ElementBase.setup(self, xml)
self._identities = set([id[0:3] for id in self['identities']])
self._features = self['features']
def add_identity(self, category, itype, name=None, lang=None):
"""
Add a new identity element. Each identity must be unique
in terms of all four identity components.
Multiple, identical category/type pairs are allowed only
if the xml:lang values are different. Likewise, multiple
category/type/xml:lang pairs are allowed so long as the names
are different. In any case, a category and type are required.
Arguments:
category -- The general category to which the agent belongs.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
identity = (category, itype, lang)
if identity not in self._identities:
self._identities.add(identity)
id_xml = ET.Element('{%s}identity' % self.namespace)
id_xml.attrib['category'] = category
id_xml.attrib['type'] = itype
if lang:
id_xml.attrib['{%s}lang' % self.xml_ns] = lang
if name:
id_xml.attrib['name'] = name
self.xml.append(id_xml)
return True
return False
def del_identity(self, category, itype, name=None, lang=None):
"""
Remove a given identity.
Arguments:
category -- The general category to which the agent belonged.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
identity = (category, itype, lang)
if identity in self._identities:
self._identities.remove(identity)
for id_xml in self.findall('{%s}identity' % self.namespace):
id = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
if id == identity:
self.xml.remove(id_xml)
return True
return False
def get_identities(self, lang=None):
"""
Return a set of all identities in tuple form as so:
(category, type, lang, name)
If a language was specified, only return identities using
that language.
Arguments:
lang -- Optional, standard xml:lang value.
"""
identities = set()
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)))
return identities
def set_identities(self, identities, lang=None):
"""
Add or replace all identities. The identities must be a in set
where each identity is a tuple of the form:
(category, type, lang, name)
If a language is specifified, any identities using that language
will be removed to be replaced with the given identities.
NOTE: An identity's language will not be changed regardless of
the value of lang.
Arguments:
identities -- A set of identities in tuple form.
lang -- Optional, standard xml:lang value.
"""
self.del_identities(lang)
for identity in identities:
category, itype, lang, name = identity
self.add_identity(category, itype, name, lang)
def del_identities(self, lang=None):
"""
Remove all identities. If a language was specified, only
remove identities using that language.
Arguments:
lang -- Optional, standard xml:lang value.
"""
for id_xml in self.findall('{%s}identity' % self.namespace):
if lang is None:
self.xml.remove(id_xml)
elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
self._identities.remove((
id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
self.xml.remove(id_xml)
def add_feature(self, feature):
"""
Add a single, new feature.
Arguments:
feature -- The namespace of the supported feature.
"""
if feature not in self._features:
self._features.add(feature)
feature_xml = ET.Element('{%s}feature' % self.namespace)
feature_xml.attrib['var'] = feature
self.xml.append(feature_xml)
return True
return False
def del_feature(self, feature):
"""
Remove a single feature.
Arguments:
feature -- The namespace of the removed feature.
"""
if feature in self._features:
self._features.remove(feature)
for feature_xml in self.findall('{%s}feature' % self.namespace):
if feature_xml.attrib['var'] == feature:
self.xml.remove(feature_xml)
return True
return False
def get_features(self):
"""Return the set of all supported features."""
features = set()
for feature_xml in self.findall('{%s}feature' % self.namespace):
features.add(feature_xml.attrib['var'])
return features
def set_features(self, features):
"""
Add or replace the set of supported features.
Arguments:
features -- The new set of supported features.
"""
self.del_features()
for feature in features:
self.add_feature(feature)
def del_features(self):
"""Remove all features."""
self._features = set()
for feature_xml in self.findall('{%s}feature' % self.namespace):
self.xml.remove(feature_xml)

View file

@ -0,0 +1,138 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
See the file LICENSE for copying permission.
"""
from sleekxmpp.xmlstream import ElementBase, ET
class DiscoItems(ElementBase):
"""
Example disco#items stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="chat.example.com"
node="xmppdev"
name="XMPP Dev" />
<item jid="chat.example.com"
node="sleekdev"
name="SleekXMPP Dev" />
</query>
</iq>
Stanza Interface:
node -- The name of the node to either
query or return info from.
items -- A list of 3-tuples, where each tuple contains
the JID, node, and name of an item.
Methods:
add_item -- Add a single new item.
del_item -- Remove a single item.
get_items -- Return all items.
set_items -- Set or replace all items.
del_items -- Remove all items.
"""
name = 'query'
namespace = 'http://jabber.org/protocol/disco#items'
plugin_attrib = 'disco_items'
interfaces = set(('node', 'items'))
# Cache items
_items = set()
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides ElementBase.setup
Caches item information.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
ElementBase.setup(self, xml)
self._items = set([item[0:2] for item in self['items']])
def add_item(self, jid, node=None, name=None):
"""
Add a new item element. Each item is required to have a
JID, but may also specify a node value to reference
non-addressable entitities.
Arguments:
jid -- The JID for the item.
node -- Optional additional information to reference
non-addressable items.
name -- Optional human readable name for the item.
"""
if (jid, node) not in self._items:
self._items.add((jid, node))
item_xml = ET.Element('{%s}item' % self.namespace)
item_xml.attrib['jid'] = jid
if name:
item_xml.attrib['name'] = name
if node:
item_xml.attrib['node'] = node
self.xml.append(item_xml)
return True
return False
def del_item(self, jid, node=None):
"""
Remove a single item.
Arguments:
jid -- JID of the item to remove.
node -- Optional extra identifying information.
"""
if (jid, node) in self._items:
for item_xml in self.findall('{%s}item' % self.namespace):
item = (item_xml.attrib['jid'],
item_xml.attrib.get('node', None))
if item == (jid, node):
self.xml.remove(item_xml)
return True
return False
def get_items(self):
"""Return all items."""
items = set()
for item_xml in self.findall('{%s}item' % self.namespace):
item = (item_xml.attrib['jid'],
item_xml.attrib.get('node'),
item_xml.attrib.get('name'))
items.add(item)
return items
def set_items(self, items):
"""
Set or replace all items. The given items must be in a
list or set where each item is a tuple of the form:
(jid, node, name)
Arguments:
items -- A series of items in tuple format.
"""
self.del_items()
for item in items:
jid, node, name = item
self.add_item(jid, node, name)
def del_items(self):
"""Remove all items."""
self._items = set()
for item_xml in self.findall('{%s}item' % self.namespace):
self.xml.remove(item_xml)

View file

@ -0,0 +1,127 @@
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 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 import Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.plugins.base import base_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
log = logging.getLogger(__name__)
class StaticDisco(object):
"""
While components will likely require fully dynamic handling
of service discovery information, most clients and simple bots
only need to manage a few disco nodes that will remain mostly
static.
StaticDisco provides a set of node handlers that will store
static sets of disco info and items in memory.
"""
def __init__(self, xmpp):
"""
Arguments:
xmpp -- The main SleekXMPP object.
"""
self.nodes = {}
self.xmpp = xmpp
def add_node(self, jid=None, node=None):
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
def get_info(self, jid, node, data=None):
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
else:
raise XMPPError(condition='item-not-found')
else:
return self.nodes[(jid, node)]['info']
def del_info(self, jid, node, data=None):
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['info'] = DiscoInfo()
def get_items(self, jid, node, data=None):
if (jid, node) not in self.nodes:
if not node:
return DiscoInfo()
else:
raise XMPPError(condition='item-not-found')
else:
return self.nodes[(jid, node)]['items']
def set_items(self, jid, node, data=None):
pass
def del_items(self, jid, node, data=None):
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'] = DiscoItems()
def add_identity(self, jid, node, data={}):
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))
def set_identities(self, jid, node, data=None):
pass
def del_identity(self, jid, node, data=None):
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))
def add_feature(self, jid, node, data=None):
self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
def set_features(self, jid, node, data=None):
pass
def del_feature(self, jid, node, data=None):
if (jid, node) not in self.nodes:
return
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
def add_item(self, jid, node, data=None):
self.add_node(jid, node)
self.nodes[(jid, node)]['items'].add_item(
data.get('ijid', ''),
node=data.get('inode', None),
name=data.get('name', None))
def del_item(self, jid, node, data=None):
if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'].del_item(**data)

View file

@ -9,7 +9,7 @@
from sleekxmpp.stanza import Error
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.handler import Waiter
from sleekxmpp.xmlstream.handler import Waiter, Callback
from sleekxmpp.xmlstream.matcher import MatcherId
@ -157,28 +157,44 @@ class Iq(RootStanza):
StanzaBase.reply(self)
return self
def send(self, block=True, timeout=None):
def send(self, block=True, timeout=None, callback=None):
"""
Send an <iq> stanza over the XML stream.
The send call can optionally block until a response is received or
a timeout occurs. Be aware that using blocking in non-threaded event
handlers can drastically impact performance.
handlers can drastically impact performance. Otherwise, a callback
handler can be provided that will be executed when the Iq stanza's
result reply is received. Be aware though that that the callback
handler will not be executed in its own thread.
Using both block and callback is not recommended, and only the
callback argument will be used in that case.
Overrides StanzaBase.send
Arguments:
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
before exiting the send call if blocking is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
block -- Specify if the send call will block until a response
is received, or a timeout occurs. Defaults to True.
timeout -- The length of time (in seconds) to wait for a response
before exiting the send call if blocking is used.
Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
if timeout is None:
timeout = self.stream.response_timeout
if block and self['type'] in ('get', 'set'):
if callback is not None and self['type'] in ('get', 'set'):
handler = Callback('IqCallback_%s' % self['id'],
MatcherId(self['id']),
callback,
once=True)
self.stream.register_handler(handler)
StanzaBase.send(self)
return None
elif block and self['type'] in ('get', 'set'):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.registerHandler(waitfor)
self.stream.register_handler(waitfor)
StanzaBase.send(self)
return waitfor.wait(timeout)
else:

View file

@ -52,6 +52,10 @@ class SleekTest(unittest.TestCase):
compare -- Compare XML objects against each other.
"""
def __init__(self, *args, **kwargs):
unittest.TestCase.__init__(self, *args, **kwargs)
self.xmpp = None
def runTest(self):
pass
@ -73,6 +77,8 @@ class SleekTest(unittest.TestCase):
xml = self.parse_xml(xml_string)
xml = xml.getchildren()[0]
return xml
else:
self.fail("XML data was mal-formed:\n%s" % xml_string)
# ------------------------------------------------------------------
# Shortcut methods for creating stanza objects
@ -86,7 +92,7 @@ class SleekTest(unittest.TestCase):
Arguments:
xml -- An XML object to use for the Message's values.
"""
return Message(None, *args, **kwargs)
return Message(self.xmpp, *args, **kwargs)
def Iq(self, *args, **kwargs):
"""
@ -97,7 +103,7 @@ class SleekTest(unittest.TestCase):
Arguments:
xml -- An XML object to use for the Iq's values.
"""
return Iq(None, *args, **kwargs)
return Iq(self.xmpp, *args, **kwargs)
def Presence(self, *args, **kwargs):
"""
@ -108,7 +114,7 @@ class SleekTest(unittest.TestCase):
Arguments:
xml -- An XML object to use for the Iq's values.
"""
return Presence(None, *args, **kwargs)
return Presence(self.xmpp, *args, **kwargs)
def check_jid(self, jid, user=None, domain=None, resource=None,
bare=None, full=None, string=None):
@ -194,7 +200,7 @@ class SleekTest(unittest.TestCase):
Arguments:
stanza -- The stanza object to test.
criteria -- An expression the stanza must match against.
method -- The type of matching to use; one of:
method -- The type of matching to use; one of:
'exact', 'mask', 'id', 'xpath', and 'stanzapath'.
Defaults to the value of self.match_method.
defaults -- A list of stanza interfaces that have default
@ -283,7 +289,7 @@ class SleekTest(unittest.TestCase):
def stream_start(self, mode='client', skip=True, header=None,
socket='mock', jid='tester@localhost',
password='test', server='localhost',
port=5222):
port=5222, plugins=None):
"""
Initialize an XMPP client or component using a dummy XML stream.
@ -303,6 +309,8 @@ class SleekTest(unittest.TestCase):
server -- The name of the XMPP server. Defaults to 'localhost'.
port -- The port to use when connecting to the server.
Defaults to 5222.
plugins -- List of plugins to register. By default, all plugins
are loaded.
"""
if mode == 'client':
self.xmpp = ClientXMPP(jid, password)
@ -338,7 +346,11 @@ class SleekTest(unittest.TestCase):
else:
raise ValueError("Unknown socket type.")
self.xmpp.register_plugins()
if plugins is None:
self.xmpp.register_plugins()
else:
for plugin in plugins:
self.xmpp.register_plugin(plugin)
self.xmpp.process(threaded=True)
if skip:
if socket != 'live':
@ -387,7 +399,7 @@ class SleekTest(unittest.TestCase):
return header % ' '.join(parts)
def recv(self, data, defaults=[], method='exact',
use_values=True, timeout=1):
use_values=True, timeout=1):
"""
Pass data to the dummy XMPP client as if it came from an XMPP server.
@ -415,7 +427,7 @@ class SleekTest(unittest.TestCase):
# receiving data.
recv_data = self.xmpp.socket.next_recv(timeout)
if recv_data is None:
return False
self.fail("No stanza was received.")
xml = self.parse_xml(recv_data)
self.fix_namespaces(xml, 'jabber:client')
stanza = self.xmpp._build_stanza(xml, 'jabber:client')
@ -510,14 +522,14 @@ class SleekTest(unittest.TestCase):
xml = self.parse_xml(data)
recv_xml = self.parse_xml(recv_data)
if recv_data is None:
return False
self.fail("No stanza was received.")
if method == 'exact':
self.failUnless(self.compare(xml, recv_xml),
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
tostring(xml), tostring(recv_xml)))
elif method == 'mask':
matcher = MatchXMLMask(xml)
self.failUnless(matcher.match(recv_xml),
self.failUnless(matcher.match(recv_xml),
"Stanza did not match using %s method:\n" % method + \
"Criteria:\n%s\n" % tostring(xml) + \
"Stanza:\n%s" % tostring(recv_xml))
@ -580,14 +592,14 @@ class SleekTest(unittest.TestCase):
xml = self.parse_xml(data)
sent_xml = self.parse_xml(sent_data)
if sent_data is None:
return False
self.fail("No stanza was sent.")
if method == 'exact':
self.failUnless(self.compare(xml, sent_xml),
"Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
tostring(xml), tostring(sent_xml)))
elif method == 'mask':
matcher = MatchXMLMask(xml)
self.failUnless(matcher.match(sent_xml),
self.failUnless(matcher.match(sent_xml),
"Stanza did not match using %s method:\n" % method + \
"Criteria:\n%s\n" % tostring(xml) + \
"Stanza:\n%s" % tostring(sent_xml))
@ -618,7 +630,7 @@ class SleekTest(unittest.TestCase):
"""
sent = self.xmpp.socket.next_sent(timeout)
if sent is None:
return False
self.fail("No stanza was sent.")
xml = self.parse_xml(sent)
self.fix_namespaces(xml, 'jabber:client')
sent = self.xmpp._build_stanza(xml, 'jabber:client')

View file

@ -116,6 +116,9 @@ class ElementBase(object):
associated plugin stanza classes.
plugin_tag_map -- A mapping of plugin stanza tag names with
the associated plugin stanza classes.
xml_ns -- The XML namespace,
http://www.w3.org/XML/1998/namespace,
for use with xml:lang values.
Instance Attributes:
xml -- The stanza's XML contents.
@ -144,7 +147,7 @@ class ElementBase(object):
_get_attr -- Return an attribute's value from the main
stanza element.
_get_sub_text -- Return the text contents of a subelement.
_set_sub_ext -- Set the text contents of a subelement.
_set_sub_text -- Set the text contents of a subelement.
_del_sub -- Remove a subelement.
match -- Compare the stanza against an XPath expression.
find -- Return subelement matching an XPath expression.
@ -170,6 +173,7 @@ class ElementBase(object):
plugin_attrib_map = {}
plugin_tag_map = {}
subitem = None
xml_ns = 'http://www.w3.org/XML/1998/namespace'
def __init__(self, xml=None, parent=None):
"""

View file

@ -52,9 +52,18 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
# Output escaped attribute values.
for attrib, value in xml.attrib.items():
if '{' not in attrib:
value = xml_escape(value)
value = xml_escape(value)
if '}' not in attrib:
output.append(' %s="%s"' % (attrib, value))
else:
attrib_ns = attrib.split('}')[0][1:]
attrib = attrib.split('}')[1]
if stream and attrib_ns in stream.namespace_map:
mapped_ns = stream.namespace_map[attrib_ns]
if mapped_ns:
output.append(' %s:%s="%s"' % (mapped_ns,
attrib,
value))
if len(xml) or xml.text:
# If there are additional child elements to serialize.

View file

@ -55,9 +55,18 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
# Output escaped attribute values.
for attrib, value in xml.attrib.items():
if '{' not in attrib:
value = xml_escape(value)
output.append(u' %s="%s"' % (attrib, value))
value = xml_escape(value)
if '}' not in attrib:
output.append(' %s="%s"' % (attrib, value))
else:
attrib_ns = attrib.split('}')[0][1:]
attrib = attrib.split('}')[1]
if stream and attrib_ns in stream.namespace_map:
mapped_ns = stream.namespace_map[attrib_ns]
if mapped_ns:
output.append(' %s:%s="%s"' % (mapped_ns,
attrib,
value))
if len(xml) or xml.text:
# If there are additional child elements to serialize.

View file

@ -192,7 +192,7 @@ class XMLStream(object):
self.send_queue = queue.Queue()
self.scheduler = Scheduler(self.event_queue, self.stop)
self.namespace_map = {}
self.namespace_map = {StanzaBase.xml_ns: 'xml'}
self.__thread = {}
self.__root_stanza = []

View file

@ -4,6 +4,11 @@ import sleekxmpp.plugins.xep_0030 as xep_0030
class TestDisco(SleekTest):
"""
Test creating and manipulating the disco#info and
disco#items stanzas from the XEP-0030 plugin.
"""
def setUp(self):
register_stanza_plugin(Iq, xep_0030.DiscoInfo)
register_stanza_plugin(Iq, xep_0030.DiscoItems)
@ -11,11 +16,10 @@ class TestDisco(SleekTest):
def testCreateInfoQueryNoNode(self):
"""Testing disco#info query with no node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = ''
self.check(iq, """
<iq id="0">
<iq>
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
@ -23,23 +27,22 @@ class TestDisco(SleekTest):
def testCreateInfoQueryWithNode(self):
"""Testing disco#info query with a node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo" />
<iq>
<query xmlns="http://jabber.org/protocol/disco#info"
node="foo" />
</iq>
""")
def testCreateInfoQueryNoNode(self):
def testCreateItemsQueryNoNode(self):
"""Testing disco#items query with no node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = ''
self.check(iq, """
<iq id="0">
<iq>
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
""")
@ -47,130 +50,467 @@ class TestDisco(SleekTest):
def testCreateItemsQueryWithNode(self):
"""Testing disco#items query with a node."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#items" node="foo" />
<iq>
<query xmlns="http://jabber.org/protocol/disco#items"
node="foo" />
</iq>
""")
def testInfoIdentities(self):
def testIdentities(self):
"""Testing adding identities to disco#info."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
iq['disco_info'].add_identity('conference', 'text',
name='Chatroom',
lang='en')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
<identity category="conference" type="text" name="Chatroom" />
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="conference"
type="text"
name="Chatroom"
xml:lang="en" />
</query>
</iq>
""")
def testInfoFeatures(self):
"""Testing adding features to disco#info."""
def testDuplicateIdentities(self):
"""
Test adding multiple copies of the same category
and type combination. Only the first identity should
be kept.
"""
iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
iq['disco_info'].addFeature('foo')
iq['disco_info'].addFeature('bar')
iq['disco_info'].add_identity('conference', 'text',
name='Chatroom')
iq['disco_info'].add_identity('conference', 'text',
name='MUC')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="conference"
type="text"
name="Chatroom" />
</query>
</iq>
""")
def testDuplicateIdentitiesWithLangs(self):
"""
Test adding multiple copies of the same category,
type, and language combination. Only the first identity
should be kept.
"""
iq = self.Iq()
iq['disco_info'].add_identity('conference', 'text',
name='Chatroom',
lang='en')
iq['disco_info'].add_identity('conference', 'text',
name='MUC',
lang='en')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="conference"
type="text"
name="Chatroom"
xml:lang="en" />
</query>
</iq>
""")
def testRemoveIdentitiesNoLang(self):
"""Test removing identities from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'pc')
iq['disco_info'].add_identity('client', 'bot')
iq['disco_info'].del_identity('client', 'pc')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#info" node="foo">
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" />
</query>
</iq>
""")
def testRemoveIdentitiesWithLang(self):
"""Test removing identities from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'pc')
iq['disco_info'].add_identity('client', 'bot')
iq['disco_info'].add_identity('client', 'pc', lang='no')
iq['disco_info'].del_identity('client', 'pc')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" />
<identity category="client"
type="pc"
xml:lang="no" />
</query>
</iq>
""")
def testRemoveAllIdentitiesNoLang(self):
"""Test removing all identities from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'bot', name='Bot')
iq['disco_info'].add_identity('client', 'bot', lang='no')
iq['disco_info'].add_identity('client', 'console')
del iq['disco_info']['identities']
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
def testRemoveAllIdentitiesWithLang(self):
"""Test removing all identities from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'bot', name='Bot')
iq['disco_info'].add_identity('client', 'bot', lang='no')
iq['disco_info'].add_identity('client', 'console')
iq['disco_info'].del_identities(lang='no')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" name="Bot" />
<identity category="client" type="console" />
</query>
</iq>
""")
def testAddBatchIdentitiesNoLang(self):
"""Test adding multiple identities at once to a disco#info stanza."""
iq = self.Iq()
identities = [('client', 'pc', 'no', 'PC Client'),
('client', 'bot', None, 'Bot'),
('client', 'console', None, None)]
iq['disco_info']['identities'] = identities
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client"
type="pc"
xml:lang="no"
name="PC Client" />
<identity category="client" type="bot" name="Bot" />
<identity category="client" type="console" />
</query>
</iq>
""")
def testAddBatchIdentitiesWithLang(self):
"""Test selectively replacing identities based on language."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'pc', lang='no')
iq['disco_info'].add_identity('client', 'pc', lang='en')
iq['disco_info'].add_identity('client', 'pc', lang='fr')
identities = [('client', 'bot', 'fr', 'Bot'),
('client', 'bot', 'en', 'Bot')]
iq['disco_info'].set_identities(identities, lang='fr')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="pc" xml:lang="no" />
<identity category="client" type="pc" xml:lang="en" />
<identity category="client"
type="bot"
xml:lang="fr"
name="Bot" />
<identity category="client"
type="bot"
xml:lang="en"
name="Bot" />
</query>
</iq>
""")
def testGetIdentitiesNoLang(self):
"""Test getting all identities from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'pc')
iq['disco_info'].add_identity('client', 'pc', lang='no')
iq['disco_info'].add_identity('client', 'pc', lang='en')
iq['disco_info'].add_identity('client', 'pc', lang='fr')
expected = set([('client', 'pc', None, None),
('client', 'pc', 'no', None),
('client', 'pc', 'en', None),
('client', 'pc', 'fr', None)])
self.failUnless(iq['disco_info']['identities'] == expected,
"Identities do not match:\n%s\n%s" % (
expected,
iq['disco_info']['identities']))
def testGetIdentitiesWithLang(self):
"""
Test getting all identities of a given
lang from a disco#info stanza.
"""
iq = self.Iq()
iq['disco_info'].add_identity('client', 'pc')
iq['disco_info'].add_identity('client', 'pc', lang='no')
iq['disco_info'].add_identity('client', 'pc', lang='en')
iq['disco_info'].add_identity('client', 'pc', lang='fr')
expected = set([('client', 'pc', 'no', None)])
result = iq['disco_info'].get_identities(lang='no')
self.failUnless(result == expected,
"Identities do not match:\n%s\n%s" % (
expected, result))
def testFeatures(self):
"""Testing adding features to disco#info."""
iq = self.Iq()
iq['disco_info'].add_feature('foo')
iq['disco_info'].add_feature('bar')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<feature var="foo" />
<feature var="bar" />
</query>
</iq>
""")
def testFeaturesDuplicate(self):
"""Test adding duplicate features to disco#info."""
iq = self.Iq()
iq['disco_info'].add_feature('foo')
iq['disco_info'].add_feature('bar')
iq['disco_info'].add_feature('foo')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<feature var="foo" />
<feature var="bar" />
</query>
</iq>
""")
def testRemoveFeature(self):
"""Test removing a feature from disco#info."""
iq = self.Iq()
iq['disco_info'].add_feature('foo')
iq['disco_info'].add_feature('bar')
iq['disco_info'].add_feature('baz')
iq['disco_info'].del_feature('foo')
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<feature var="bar" />
<feature var="baz" />
</query>
</iq>
""")
def testGetFeatures(self):
"""Test getting all features from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_feature('foo')
iq['disco_info'].add_feature('bar')
iq['disco_info'].add_feature('baz')
expected = set(['foo', 'bar', 'baz'])
self.failUnless(iq['disco_info']['features'] == expected,
"Features do not match:\n%s\n%s" % (
expected,
iq['disco_info']['features']))
def testRemoveAllFeatures(self):
"""Test removing all features from a disco#info stanza."""
iq = self.Iq()
iq['disco_info'].add_feature('foo')
iq['disco_info'].add_feature('bar')
iq['disco_info'].add_feature('baz')
del iq['disco_info']['features']
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
def testAddBatchFeatures(self):
"""Test adding multiple features at once to a disco#info stanza."""
iq = self.Iq()
features = ['foo', 'bar', 'baz']
iq['disco_info']['features'] = features
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#info">
<feature var="foo" />
<feature var="bar" />
<feature var="baz" />
</query>
</iq>
""")
def testItems(self):
"""Testing adding features to disco#info."""
iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
iq['disco_items'].addItem('user@localhost')
iq['disco_items'].addItem('user@localhost', 'foo')
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
iq['disco_items'].add_item('user@localhost')
iq['disco_items'].add_item('user@localhost', 'foo')
iq['disco_items'].add_item('user@localhost', 'bar', name='Testing')
self.check(iq, """
<iq id="0">
<query xmlns="http://jabber.org/protocol/disco#items" node="foo">
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" />
<item node="foo" jid="user@localhost" />
<item node="bar" jid="user@localhost" name="Testing" />
<item jid="user@localhost"
node="foo" />
<item jid="user@localhost"
node="bar"
name="Testing" />
</query>
</iq>
""")
def testAddRemoveIdentities(self):
"""Test adding and removing identities to disco#info stanza"""
ids = [('automation', 'commands', 'AdHoc'),
('conference', 'text', 'ChatRoom')]
def testDuplicateItems(self):
"""Test adding items with the same JID without any nodes."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost', name='First')
iq['disco_items'].add_item('user@localhost', name='Second')
info = xep_0030.DiscoInfo()
info.addIdentity(*ids[0])
self.failUnless(info.getIdentities() == [ids[0]])
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" name="First" />
</query>
</iq>
""")
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [])
info.setIdentities(ids)
self.failUnless(info.getIdentities() == ids)
def testDuplicateItemsWithNodes(self):
"""Test adding items with the same JID/node combination."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost',
node='foo',
name='First')
iq['disco_items'].add_item('user@localhost',
node='foo',
name='Second')
info.delIdentity('automation', 'commands')
self.failUnless(info.getIdentities() == [ids[1]])
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" node="foo" name="First" />
</query>
</iq>
""")
info.delIdentities()
self.failUnless(info.getIdentities() == [])
def testRemoveItemsNoNode(self):
"""Test removing items without nodes from a disco#items stanza."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost')
iq['disco_items'].add_item('user@localhost', node='foo')
iq['disco_items'].add_item('test@localhost')
def testAddRemoveFeatures(self):
"""Test adding and removing features to disco#info stanza"""
features = ['foo', 'bar', 'baz']
iq['disco_items'].del_item('user@localhost')
info = xep_0030.DiscoInfo()
info.addFeature(features[0])
self.failUnless(info.getFeatures() == [features[0]])
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" node="foo" />
<item jid="test@localhost" />
</query>
</iq>
""")
info.delFeature('foo')
self.failUnless(info.getFeatures() == [])
def testRemoveItemsWithNode(self):
"""Test removing items with nodes from a disco#items stanza."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost')
iq['disco_items'].add_item('user@localhost', node='foo')
iq['disco_items'].add_item('test@localhost')
info.setFeatures(features)
self.failUnless(info.getFeatures() == features)
iq['disco_items'].del_item('user@localhost', node='foo')
info.delFeature('bar')
self.failUnless(info.getFeatures() == ['foo', 'baz'])
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" />
<item jid="test@localhost" />
</query>
</iq>
""")
info.delFeatures()
self.failUnless(info.getFeatures() == [])
def testGetItems(self):
"""Test retrieving items from disco#items stanza."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost')
iq['disco_items'].add_item('user@localhost', node='foo')
iq['disco_items'].add_item('test@localhost',
node='bar',
name='Tester')
def testAddRemoveItems(self):
"""Test adding and removing items to disco#items stanza"""
items = [('user@localhost', None, None),
('user@localhost', 'foo', None),
('user@localhost', 'bar', 'Test')]
expected = set([('user@localhost', None, None),
('user@localhost', 'foo', None),
('test@localhost', 'bar', 'Tester')])
self.failUnless(iq['disco_items']['items'] == expected,
"Items do not match:\n%s\n%s" % (
expected,
iq['disco_items']['items']))
info = xep_0030.DiscoItems()
self.failUnless(True, ""+str(items[0]))
def testRemoveAllItems(self):
"""Test removing all items from a disco#items stanza."""
iq = self.Iq()
iq['disco_items'].add_item('user@localhost')
iq['disco_items'].add_item('user@localhost', node='foo')
iq['disco_items'].add_item('test@localhost',
node='bar',
name='Tester')
info.addItem(*(items[0]))
self.failUnless(info.getItems() == [items[0]], info.getItems())
del iq['disco_items']['items']
info.delItem('user@localhost')
self.failUnless(info.getItems() == [])
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
""")
info.setItems(items)
self.failUnless(info.getItems() == items)
def testAddBatchItems(self):
"""Test adding multiple items to a disco#items stanza."""
iq = self.Iq()
items = [('user@localhost', 'foo', 'Test'),
('test@localhost', None, None),
('other@localhost', None, 'Other')]
info.delItem('user@localhost', 'foo')
self.failUnless(info.getItems() == [items[0], items[2]])
info.delItems()
self.failUnless(info.getItems() == [])
iq['disco_items']['items'] = items
self.check(iq, """
<iq>
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="user@localhost" node="foo" name="Test" />
<item jid="test@localhost" />
<item jid="other@localhost" name="Other" />
</query>
</iq>
""")
suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco)

View file

@ -1,3 +1,5 @@
import time
from sleekxmpp.test import *
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream.matcher import *
@ -108,5 +110,41 @@ class TestHandlers(SleekTest):
self.failUnless(waiter_exists == False,
"Waiter handler was not removed.")
def testIqCallback(self):
"""Test that iq.send(callback=handle_foo) works."""
events = []
def handle_foo(iq):
events.append('foo')
iq = self.Iq()
iq['type'] = 'get'
iq['id'] = 'test-foo'
iq['to'] = 'user@localhost'
iq['query'] = 'foo'
iq.send(callback=handle_foo)
self.send("""
<iq type="get" id="test-foo" to="user@localhost">
<query xmlns="foo" />
</iq>
""")
self.recv("""
<iq type="result" id="test-foo"
to="test@localhost"
from="user@localhost">
<query xmlns="foo">
<data />
</query>
</iq>
""")
# Give event queue time to process
time.sleep(0.1)
self.failUnless(events == ['foo'],
"Iq callback was not executed: %s" % events)
suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers)

View file

@ -0,0 +1,528 @@
import time
import threading
from sleekxmpp.test import *
class TestStreamDisco(SleekTest):
"""
Test using the XEP-0030 plugin.
"""
def tearDown(self):
self.stream_close()
def testInfoEmptyDefaultNode(self):
"""
Info query result from an entity MUST have at least one identity
and feature, namely http://jabber.org/protocol/disco#info.
Since the XEP-0030 plugin is loaded, a disco response should
be generated and not an error result.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
self.recv("""
<iq type="get" id="test">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" />
<feature var="http://jabber.org/protocol/disco#info" />
</query>
</iq>
""")
def testInfoEmptyDefaultNodeComponent(self):
"""
Test requesting an empty, default node using a Component.
"""
self.stream_start(mode='component',
jid='tester.localhost',
plugins=['xep_0030'])
self.recv("""
<iq type="get" id="test">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="component" type="generic" />
<feature var="http://jabber.org/protocol/disco#info" />
</query>
</iq>
""")
def testInfoIncludeNode(self):
"""
Results for info queries directed to a particular node MUST
include the node in the query response.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
self.xmpp['xep_0030'].static.add_node(node='testing')
self.recv("""
<iq to="tester@localhost" type="get" id="test">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing">
</query>
</iq>""",
method='mask')
def testItemsIncludeNode(self):
"""
Results for items queries directed to a particular node MUST
include the node in the query response.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
self.xmpp['xep_0030'].static.add_node(node='testing')
self.recv("""
<iq to="tester@localhost" type="get" id="test">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing">
</query>
</iq>""",
method='mask')
def testDynamicInfoJID(self):
"""
Test using a dynamic info handler for a particular JID.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
def dynamic_jid(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info')
return result
self.xmpp['xep_0030'].set_node_handler('get_info',
jid='tester@localhost',
handler=dynamic_jid)
self.recv("""
<iq type="get" id="test" to="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing">
<identity category="client"
type="console"
name="Dynamic Info" />
</query>
</iq>
""")
def testDynamicInfoGlobal(self):
"""
Test using a dynamic info handler for all requests.
"""
self.stream_start(mode='component',
jid='tester.localhost',
plugins=['xep_0030'])
def dynamic_global(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info')
return result
self.xmpp['xep_0030'].set_node_handler('get_info',
handler=dynamic_global)
self.recv("""
<iq type="get" id="test"
to="user@tester.localhost"
from="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test"
to="tester@localhost"
from="user@tester.localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing">
<identity category="component"
type="generic"
name="Dynamic Info" />
</query>
</iq>
""")
def testOverrideJIDInfoHandler(self):
"""Test overriding a JID info handler."""
self.stream_start(mode='client',
plugins=['xep_0030'])
def dynamic_jid(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('client', 'console', name='Dynamic Info')
return result
self.xmpp['xep_0030'].set_node_handler('get_info',
jid='tester@localhost',
handler=dynamic_jid)
self.xmpp['xep_0030'].make_static(jid='tester@localhost',
node='testing')
self.xmpp['xep_0030'].add_identity(jid='tester@localhost',
node='testing',
category='automation',
itype='command-list')
self.recv("""
<iq type="get" id="test" to="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing">
<identity category="automation"
type="command-list" />
</query>
</iq>
""")
def testOverrideGlobalInfoHandler(self):
"""Test overriding the global JID info handler."""
self.stream_start(mode='component',
jid='tester.localhost',
plugins=['xep_0030'])
def dynamic_global(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoInfo()
result['node'] = node
result.add_identity('component', 'generic', name='Dynamic Info')
return result
self.xmpp['xep_0030'].set_node_handler('get_info',
handler=dynamic_global)
self.xmpp['xep_0030'].make_static(jid='user@tester.localhost',
node='testing')
self.xmpp['xep_0030'].add_feature(jid='user@tester.localhost',
node='testing',
feature='urn:xmpp:ping')
self.recv("""
<iq type="get" id="test"
to="user@tester.localhost"
from="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test"
to="tester@localhost"
from="user@tester.localhost">
<query xmlns="http://jabber.org/protocol/disco#info"
node="testing">
<feature var="urn:xmpp:ping" />
</query>
</iq>
""")
def testGetInfoRemote(self):
"""
Test sending a disco#info query to another entity
and receiving the result.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
events = set()
def handle_disco_info(iq):
events.add('disco_info')
self.xmpp.add_event_handler('disco_info', handle_disco_info)
t = threading.Thread(name="get_info",
target=self.xmpp['xep_0030'].get_info,
args=('user@localhost', 'foo'))
t.start()
self.send("""
<iq type="get" to="user@localhost" id="1">
<query xmlns="http://jabber.org/protocol/disco#info"
node="foo" />
</iq>
""")
self.recv("""
<iq type="result" to="tester@localhost" id="1">
<query xmlns="http://jabber.org/protocol/disco#info"
node="foo">
<identity category="client" type="bot" />
<feature var="urn:xmpp:ping" />
</query>
</iq>
""")
# Wait for disco#info request to be received.
t.join()
time.sleep(0.1)
self.assertEqual(events, set(('disco_info',)),
"Disco info event was not triggered: %s" % events)
def testDynamicItemsJID(self):
"""
Test using a dynamic items handler for a particular JID.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
def dynamic_jid(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='JID')
return result
self.xmpp['xep_0030'].set_node_handler('get_items',
jid='tester@localhost',
handler=dynamic_jid)
self.recv("""
<iq type="get" id="test" to="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing">
<item jid="tester@localhost" node="foo" name="JID" />
</query>
</iq>
""")
def testDynamicItemsGlobal(self):
"""
Test using a dynamic items handler for all requests.
"""
self.stream_start(mode='component',
jid='tester.localhost',
plugins=['xep_0030'])
def dynamic_global(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global')
return result
self.xmpp['xep_0030'].set_node_handler('get_items',
handler=dynamic_global)
self.recv("""
<iq type="get" id="test"
to="user@tester.localhost"
from="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test"
to="tester@localhost"
from="user@tester.localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing">
<item jid="tester@localhost" node="foo" name="Global" />
</query>
</iq>
""")
def testOverrideJIDItemsHandler(self):
"""Test overriding a JID items handler."""
self.stream_start(mode='client',
plugins=['xep_0030'])
def dynamic_jid(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester@localhost', node='foo', name='Global')
return result
self.xmpp['xep_0030'].set_node_handler('get_items',
jid='tester@localhost',
handler=dynamic_jid)
self.xmpp['xep_0030'].make_static(jid='tester@localhost',
node='testing')
self.xmpp['xep_0030'].add_item(jid='tester@localhost',
node='testing',
ijid='tester@localhost',
inode='foo',
name='Test')
self.recv("""
<iq type="get" id="test" to="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing">
<item jid="tester@localhost" node="foo" name="Test" />
</query>
</iq>
""")
def testOverrideGlobalItemsHandler(self):
"""Test overriding the global JID items handler."""
self.stream_start(mode='component',
jid='tester.localhost',
plugins=['xep_0030'])
def dynamic_global(jid, node, iq):
result = self.xmpp['xep_0030'].stanza.DiscoItems()
result['node'] = node
result.add_item('tester.localhost', node='foo', name='Global')
return result
self.xmpp['xep_0030'].set_node_handler('get_items',
handler=dynamic_global)
self.xmpp['xep_0030'].make_static(jid='user@tester.localhost',
node='testing')
self.xmpp['xep_0030'].add_item(jid='user@tester.localhost',
node='testing',
ijid='user@tester.localhost',
inode='foo',
name='Test')
self.recv("""
<iq type="get" id="test"
to="user@tester.localhost"
from="tester@localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing" />
</iq>
""")
self.send("""
<iq type="result" id="test"
to="tester@localhost"
from="user@tester.localhost">
<query xmlns="http://jabber.org/protocol/disco#items"
node="testing">
<item jid="user@tester.localhost" node="foo" name="Test" />
</query>
</iq>
""")
def testGetItemsRemote(self):
"""
Test sending a disco#items query to another entity
and receiving the result.
"""
self.stream_start(mode='client',
plugins=['xep_0030'])
events = set()
results = set()
def handle_disco_items(iq):
events.add('disco_items')
results.update(iq['disco_items']['items'])
self.xmpp.add_event_handler('disco_items', handle_disco_items)
t = threading.Thread(name="get_items",
target=self.xmpp['xep_0030'].get_items,
args=('user@localhost', 'foo'))
t.start()
self.send("""
<iq type="get" to="user@localhost" id="1">
<query xmlns="http://jabber.org/protocol/disco#items"
node="foo" />
</iq>
""")
self.recv("""
<iq type="result" to="tester@localhost" id="1">
<query xmlns="http://jabber.org/protocol/disco#items"
node="foo">
<item jid="user@localhost" node="bar" name="Test" />
<item jid="user@localhost" node="baz" name="Test 2" />
</query>
</iq>
""")
# Wait for disco#items request to be received.
t.join()
time.sleep(0.1)
items = set([('user@localhost', 'bar', 'Test'),
('user@localhost', 'baz', 'Test 2')])
self.assertEqual(events, set(('disco_items',)),
"Disco items event was not triggered: %s" % events)
self.assertEqual(results, items,
"Unexpected items: %s" % results)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDisco)

View file

@ -1,6 +1,6 @@
from sleekxmpp.test import *
from sleekxmpp.stanza import Message
from sleekxmpp.xmlstream.stanzabase import ET
from sleekxmpp.xmlstream.stanzabase import ET, ElementBase
from sleekxmpp.xmlstream.tostring import tostring, xml_escape
@ -10,6 +10,9 @@ class TestToString(SleekTest):
Test the implementation of sleekxmpp.xmlstream.tostring
"""
def tearDown(self):
self.stream_close()
def tryTostring(self, original='', expected=None, message='', **kwargs):
"""
Compare the result of calling tostring against an
@ -110,5 +113,18 @@ class TestToString(SleekTest):
self.failUnless(result == expected,
"Stanza Unicode handling is incorrect: %s" % result)
def testXMLLang(self):
"""Test that serializing xml:lang works."""
self.stream_start()
msg = self.Message()
msg._set_attr('{%s}lang' % msg.xml_ns, "no")
expected = '<message xml:lang="no" />'
result = msg.__str__()
self.failUnless(expected == result,
"Serialization with xml:lang failed: %s" % result)
suite = unittest.TestLoader().loadTestsFromTestCase(TestToString)