diff options
author | Lance Stout <lancestout@gmail.com> | 2010-12-13 14:36:53 -0500 |
---|---|---|
committer | Lance Stout <lancestout@gmail.com> | 2010-12-13 14:36:53 -0500 |
commit | c16913c99929a6a5a57611ec8a1757e3e82d4a45 (patch) | |
tree | d92b98975b3dc012c734997e6fddc0ac9e2b82d8 /sleekxmpp | |
parent | 12b61365adbbc910841475ea3cf8204f26ccd9c7 (diff) | |
parent | f4451fe6b72f7cfb9680ead7a608d5ca1bc7e753 (diff) | |
download | slixmpp-c16913c99929a6a5a57611ec8a1757e3e82d4a45.tar.gz slixmpp-c16913c99929a6a5a57611ec8a1757e3e82d4a45.tar.bz2 slixmpp-c16913c99929a6a5a57611ec8a1757e3e82d4a45.tar.xz slixmpp-c16913c99929a6a5a57611ec8a1757e3e82d4a45.zip |
Merge branch 'develop' into roster
Conflicts:
sleekxmpp/basexmpp.py
Diffstat (limited to 'sleekxmpp')
-rw-r--r-- | sleekxmpp/basexmpp.py | 3 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030.py | 337 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/__init__.py | 12 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/disco.py | 314 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/__init__.py | 10 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/disco.py | 0 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/info.py | 262 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/stanza/items.py | 138 | ||||
-rw-r--r-- | sleekxmpp/plugins/xep_0030/static.py | 127 | ||||
-rw-r--r-- | sleekxmpp/stanza/iq.py | 36 | ||||
-rw-r--r-- | sleekxmpp/test/sleektest.py | 38 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/stanzabase.py | 6 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/tostring/tostring.py | 13 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/tostring/tostring26.py | 15 | ||||
-rw-r--r-- | sleekxmpp/xmlstream/xmlstream.py | 2 |
15 files changed, 946 insertions, 367 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index c60cb567..5a232d14 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py deleted file mode 100644 index e3589077..00000000 --- a/sleekxmpp/plugins/xep_0030.py +++ /dev/null @@ -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) diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py new file mode 100644 index 00000000..2e183852 --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py new file mode 100644 index 00000000..c323ba7c --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -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 + diff --git a/sleekxmpp/plugins/xep_0030/stanza/__init__.py b/sleekxmpp/plugins/xep_0030/stanza/__init__.py new file mode 100644 index 00000000..0d97cf3d --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/__init__.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0030/stanza/disco.py b/sleekxmpp/plugins/xep_0030/stanza/disco.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/disco.py diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py new file mode 100644 index 00000000..6764acbb --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/info.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py new file mode 100644 index 00000000..319e666f --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -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) diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py new file mode 100644 index 00000000..f3693228 --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -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) + diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 150baa00..906e6648 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -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: diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index 3afd9a62..ce582e3a 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -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') diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index aabd3864..5551d439 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -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): """ diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py index d8f5c5b2..38b08d82 100644 --- a/sleekxmpp/xmlstream/tostring/tostring.py +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -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. diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py index 0ee432cc..11501780 100644 --- a/sleekxmpp/xmlstream/tostring/tostring26.py +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -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. diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 9ae31a20..fc7aff34 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -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 = [] |