diff --git a/setup.py b/setup.py index 4575a07..26160ca 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0092', + 'sleekxmpp/plugins/xep_0128', 'sleekxmpp/plugins/xep_0199', ] diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 1c967bd..83d7a9c 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -60,10 +60,13 @@ class xep_0030(base_plugin): disco_items_query -- Received a disco#items Iq query request. Attributes: - stanza -- A reference to the module containing the stanza classes - provided by this plugin. - static -- Object containing the default set of static node handlers. - xmpp -- The main SleekXMPP object. + stanza -- A reference to the module containing the + stanza classes provided by this plugin. + static -- Object containing the default set of + static node handlers. + default_handlers -- A dictionary mapping operations to the default + global handler (by default, the static handlers). + xmpp -- The main SleekXMPP object. Methods: set_node_handler -- Assign a handler to a JID/node combination. @@ -110,11 +113,10 @@ class xep_0030(base_plugin): 'add_identity', 'del_identity', 'add_feature', 'del_feature', 'add_item', 'del_item', 'del_identities', 'del_features'] + self.default_handlers = {} self._handlers = {} for op in self._disco_ops: - self._handlers[op] = {'global': getattr(self.static, op), - 'jid': {}, - 'node': {}} + self._add_disco_op(op, getattr(self.static, op)) def post_init(self): """Handle cross-plugin dependencies.""" @@ -123,6 +125,12 @@ class xep_0030(base_plugin): register_stanza_plugin(DiscoItems, self.xmpp['xep_0059'].stanza.Set) + def _add_disco_op(self, op, default_handler): + self.default_handlers[op] = default_handler + self._handlers[op] = {'global': default_handler, + 'jid': {}, + 'node': {}} + def set_node_handler(self, htype, jid=None, node=None, handler=None): """ Add a node handler for the given hierarchy level and @@ -205,26 +213,29 @@ class xep_0030(base_plugin): """ self.set_node_handler(htype, jid, node, None) - def make_static(self, jid=None, node=None, handlers=None): + def restore_defaults(self, jid=None, node=None, handlers=None): """ - Change all of a node's handlers to the default static + Change all or some of a node's handlers to the default handlers. Useful for manually overriding the contents of a node that would otherwise be handled by a JID level or global level dynamic handler. + The default is to use the built-in static handlers, but that + may be changed by modifying self.default_handlers. + 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 + default version. If provided, only these handlers will be changed. Otherwise, all - handlers will use the static version. + handlers will use the default 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)) + self.set_node_handler(op, jid, node, self.default_handlers[op]) def get_info(self, jid=None, node=None, local=False, **kwargs): """ @@ -609,3 +620,4 @@ class xep_0030(base_plugin): # Retain some backwards compatibility xep_0030.getInfo = xep_0030.get_info xep_0030.getItems = xep_0030.get_items +xep_0030.make_static = xep_0030.restore_defaults diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index 654a9bd..7e7f035 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -120,7 +120,8 @@ class StaticDisco(object): """ Replace the stored items data for a JID/node combination. - The data parameter is not used. + The data parameter may provided: + items -- A set of items in tuple format. """ items = data.get('items', set()) self.add_node(jid, node) diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py deleted file mode 100644 index 824977b..0000000 --- a/sleekxmpp/plugins/xep_0128.py +++ /dev/null @@ -1,51 +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 -from . xep_0030 import DiscoInfo, DiscoItems -from . xep_0004 import Form - - -class xep_0128(base.base_plugin): - """ - XEP-0128 Service Discovery Extensions - """ - - def plugin_init(self): - self.xep = '0128' - self.description = 'Service Discovery Extensions' - - registerStanzaPlugin(DiscoInfo, Form) - registerStanzaPlugin(DiscoItems, Form) - - def extend_info(self, node, data=None): - if data is None: - data = {} - node = self.xmpp['xep_0030'].nodes.get(node, None) - if node is None: - self.xmpp['xep_0030'].add_node(node) - - info = node.info - info['form']['type'] = 'result' - info['form'].setFields(data, default=None) - - def extend_items(self, node, data=None): - if data is None: - data = {} - node = self.xmpp['xep_0030'].nodes.get(node, None) - if node is None: - self.xmpp['xep_0030'].add_node(node) - - items = node.items - items['form']['type'] = 'result' - items['form'].setFields(data, default=None) diff --git a/sleekxmpp/plugins/xep_0128/__init__.py b/sleekxmpp/plugins/xep_0128/__init__.py new file mode 100644 index 0000000..3c6379a --- /dev/null +++ b/sleekxmpp/plugins/xep_0128/__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_0128.static import StaticExtendedDisco +from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128 diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py new file mode 100644 index 0000000..63b3cfe --- /dev/null +++ b/sleekxmpp/plugins/xep_0128/extended_disco.py @@ -0,0 +1,101 @@ +""" + 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.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0004 import Form +from sleekxmpp.plugins.xep_0030 import DiscoInfo +from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco + + +class xep_0128(base_plugin): + + """ + XEP-0128: Service Discovery Extensions + + Allow the use of data forms to add additional identity + information to disco#info results. + + Also see . + + Attributes: + disco -- A reference to the XEP-0030 plugin. + static -- Object containing the default set of static + node handlers. + xmpp -- The main SleekXMPP object. + + Methods: + set_extended_info -- Set extensions to a disco#info result. + add_extended_info -- Add an extension to a disco#info result. + del_extended_info -- Remove all extensions from a disco#info result. + """ + + def plugin_init(self): + """Start the XEP-0128 plugin.""" + self.xep = '0128' + self.description = 'Service Discovery Extensions' + + self._disco_ops = ['set_extended_info', + 'add_extended_info', + 'del_extended_info'] + + register_stanza_plugin(DiscoInfo, Form, iterable=True) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.disco = self.xmpp['xep_0030'] + self.static = StaticExtendedDisco(self.disco.static) + + self.disco.set_extended_info = self.set_extended_info + self.disco.add_extended_info = self.add_extended_info + self.disco.del_extended_info = self.del_extended_info + + for op in self._disco_ops: + self.disco._add_disco_op(op, getattr(self.static, op)) + + def set_extended_info(self, jid=None, node=None, **kwargs): + """ + Set additional, extended identity information to a node. + + Replaces any existing extended information. + + Arguments: + jid -- The JID to modify. + node -- The node to modify. + data -- Either a form, or a list of forms to use + as extended information, replacing any + existing extensions. + """ + self.disco._run_node_handler('set_extended_info', jid, node, kwargs) + + def add_extended_info(self, jid=None, node=None, **kwargs): + """ + Add additional, extended identity information to a node. + + Arguments: + jid -- The JID to modify. + node -- The node to modify. + data -- Either a form, or a list of forms to add + as extended information. + """ + self.disco._run_node_handler('add_extended_info', jid, node, kwargs) + + def del_extended_info(self, jid=None, node=None, **kwargs): + """ + Remove all extended identity information to a node. + + Arguments: + jid -- The JID to modify. + node -- The node to modify. + """ + self.disco._run_node_handler('del_extended_info', jid, node, kwargs) diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py new file mode 100644 index 0000000..493d937 --- /dev/null +++ b/sleekxmpp/plugins/xep_0128/static.py @@ -0,0 +1,72 @@ +""" + 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.plugins.xep_0030 import StaticDisco + + +log = logging.getLogger(__name__) + + +class StaticExtendedDisco(object): + + """ + Extend the default StaticDisco implementation to provide + support for extended identity information. + """ + + def __init__(self, static): + """ + Augment the default XEP-0030 static handler object. + + Arguments: + static -- The default static XEP-0030 handler object. + """ + self.static = static + + def set_extended_info(self, jid, node, data): + """ + Replace the extended identity data for a JID/node combination. + + The data parameter may provide: + data -- Either a single data form, or a list of data forms. + """ + self.del_extended_info(jid, node, data) + self.add_extended_info(jid, node, data) + + def add_extended_info(self, jid, node, data): + """ + Add additional extended identity data for a JID/node combination. + + The data parameter may provide: + data -- Either a single data form, or a list of data forms. + """ + self.static.add_node(jid, node) + + forms = data.get('data', []) + if not isinstance(forms, list): + forms = [forms] + + for form in forms: + self.static.nodes[(jid, node)]['info'].append(form) + + def del_extended_info(self, jid, node, data): + """ + Replace the extended identity data for a JID/node combination. + + The data parameter is not used. + """ + if (jid, node) not in self.static.nodes: + return + + info = self.static.nodes[(jid, node)]['info'] + + for form in info['substanza']: + info.xml.remove(form.xml) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 8b538d2..4da42cd 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -40,6 +40,7 @@ def register_stanza_plugin(stanza, plugin, iterable=False): stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin stanza.plugin_tag_map[tag] = plugin if iterable: + stanza.plugin_iterables = stanza.plugin_iterables.copy() stanza.plugin_iterables.add(plugin) @@ -206,7 +207,7 @@ class ElementBase(object): plugin_attrib_map = {} plugin_iterables = set() plugin_tag_map = {} - subitem = None + subitem = set() is_extension = False xml_ns = 'http://www.w3.org/XML/1998/namespace' @@ -231,6 +232,10 @@ class ElementBase(object): ElementBase.values = property(ElementBase._get_stanza_values, ElementBase._set_stanza_values) + if self.subitem is not None: + for sub in self.subitem: + self.plugin_iterables.add(sub) + if self.setup(xml): # If we generated our own XML, then everything is ready. return @@ -240,9 +245,6 @@ class ElementBase(object): if child.tag in self.plugin_tag_map: plugin = self.plugin_tag_map[child.tag] self.plugins[plugin.plugin_attrib] = plugin(child, self) - if self.subitem is not None: - for sub in self.subitem: - self.plugin_iterables.add(sub) for sub in self.plugin_iterables: if child.tag == "{%s}%s" % (sub.namespace, sub.name): self.iterables.append(sub(child, self)) @@ -333,11 +335,20 @@ class ElementBase(object): Plugin interfaces may accept a nested dictionary that will be used recursively. """ + iterable_interfaces = [p.plugin_attrib for \ + p in self.plugin_iterables] + for interface, value in values.items(): if interface == 'substanzas': + # Remove existing substanzas + for stanza in self.iterables: + self.xml.remove(stanza.xml) + self.iterables = [] + + # Add new substanzas for subdict in value: if '__childtag__' in subdict: - for subclass in self.subitem: + for subclass in self.plugin_iterables: child_tag = "{%s}%s" % (subclass.namespace, subclass.name) if subdict['__childtag__'] == child_tag: @@ -348,9 +359,10 @@ class ElementBase(object): elif interface in self.interfaces: self[interface] = value elif interface in self.plugin_attrib_map: - if interface not in self.plugins: - self.init_plugin(interface) - self.plugins[interface].values = value + if interface not in iterable_interfaces: + if interface not in self.plugins: + self.init_plugin(interface) + self.plugins[interface].values = value return self def __getitem__(self, attrib): diff --git a/tests/test_stream_xep_0128.py b/tests/test_stream_xep_0128.py new file mode 100644 index 0000000..6fee655 --- /dev/null +++ b/tests/test_stream_xep_0128.py @@ -0,0 +1,106 @@ +import sys +import time +import threading + +from sleekxmpp.test import * +from sleekxmpp.xmlstream import ElementBase + + +class TestStreamExtendedDisco(SleekTest): + + """ + Test using the XEP-0128 plugin. + """ + + def tearDown(self): + sys.excepthook = sys.__excepthook__ + self.stream_close() + + def testUsingExtendedInfo(self): + self.stream_start(mode='client', + jid='tester@localhost', + plugins=['xep_0030', + 'xep_0004', + 'xep_0128']) + + form = self.xmpp['xep_0004'].makeForm(ftype='result') + form.addField(var='FORM_TYPE', ftype='hidden', value='testing') + + info_ns = 'http://jabber.org/protocol/disco#info' + self.xmpp['xep_0030'].add_identity(node='test', + category='client', + itype='bot') + self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) + self.xmpp['xep_0128'].set_extended_info(node='test', data=form) + + self.recv(""" + + + + """) + + self.send(""" + + + + + + + testing + + + + + """) + + def testUsingMultipleExtendedInfo(self): + self.stream_start(mode='client', + jid='tester@localhost', + plugins=['xep_0030', + 'xep_0004', + 'xep_0128']) + + form1 = self.xmpp['xep_0004'].makeForm(ftype='result') + form1.addField(var='FORM_TYPE', ftype='hidden', value='testing') + + form2 = self.xmpp['xep_0004'].makeForm(ftype='result') + form2.addField(var='FORM_TYPE', ftype='hidden', value='testing_2') + + info_ns = 'http://jabber.org/protocol/disco#info' + self.xmpp['xep_0030'].add_identity(node='test', + category='client', + itype='bot') + self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) + self.xmpp['xep_0128'].set_extended_info(node='test', data=[form1, form2]) + + self.recv(""" + + + + """) + + self.send(""" + + + + + + + testing + + + + + testing_2 + + + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExtendedDisco)