From 629f6e76a9c003ef8befdfdf023f2e5b9eadc718 Mon Sep 17 00:00:00 2001 From: Lance stout Date: Mon, 31 May 2010 13:24:14 -0400 Subject: [PATCH 01/99] Added implementation and tests for XEP-0085 - Chat State Notifications. Chat states may be set using: msg['chat_state'].active() msg['chat_state'].composing() msg['chat_state'].gone() msg['chat_state'].inactive() msg['chat_state'].paused() Checking a chat state can be done with either: msg['chat_state'].getState() msg['chat_state'].name When a message with a chat state is receieved, the following events may occur: chatstate_active chatstate_composing chatstate_gone chatstate_inactive chatstate_paused where the event data is the message stanza. Note that currently these events are also triggered for messages sent by SleekXMPP, not just those received. --- sleekxmpp/plugins/__init__.py | 2 +- sleekxmpp/plugins/xep_0085.py | 100 ++++++++++++++++++++++++++++++++++ tests/test_chatstates.py | 47 ++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 sleekxmpp/plugins/xep_0085.py create mode 100644 tests/test_chatstates.py diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 1868365..674c3de 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -17,4 +17,4 @@ along with SleekXMPP; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ -__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] +__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py new file mode 100644 index 0000000..e183ec2 --- /dev/null +++ b/sleekxmpp/plugins/xep_0085.py @@ -0,0 +1,100 @@ +""" + 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.txt for copying permissio +""" + +import logging +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class ChatState(ElementBase): + namespace = 'http://jabber.org/protocol/chatstates' + plugin_attrib = 'chat_state' + interface = set(('state',)) + states = set(('active', 'composing', 'gone', 'inactive', 'paused')) + + def active(self): + self.setState('active') + + def composing(self): + self.setState('composing') + + def gone(self): + self.setState('gone') + + def inactive(self): + self.setState('inactive') + + def paused(self): + self.setState('paused') + + def setState(self, state): + if state in self.states: + self.name = state + self.xml.tag = state + self.xml.attrib['xmlns'] = self.namespace + + def getState(self): + return self.name + +# In order to match the various chat state elements, +# we need one stanza object per state, even though +# they are all the same except for the initial name +# value. Do not depend on the type of the chat state +# stanza object for the actual state. + +class Active(ChatState): + name = 'active' +class Composing(ChatState): + name = 'composing' +class Gone(ChatState): + name = 'gone' +class Inactive(ChatState): + name = 'inactive' +class Paused(ChatState): + name = 'paused' + + +class xep_0085(base.base_plugin): + """ + XEP-0085 Chat State Notifications + """ + + def plugin_init(self): + self.xep = '0085' + self.description = 'Chat State Notifications' + + handlers = [('Active Chat State', 'active'), + ('Composing Chat State', 'composing'), + ('Gone Chat State', 'gone'), + ('Inactive Chat State', 'inactive'), + ('Paused Chat State', 'paused')] + for handler in handlers: + self.xmpp.registerHandler( + Callback(handler[0], + MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns, + ChatState.namespace, + handler[1])), + self._handleChatState)) + + self.xmpp.stanzaPlugin(Message, Active) + self.xmpp.stanzaPlugin(Message, Composing) + self.xmpp.stanzaPlugin(Message, Gone) + self.xmpp.stanzaPlugin(Message, Inactive) + self.xmpp.stanzaPlugin(Message, Paused) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates') + + def _handleChatState(self, msg): + state = msg['chat_state'].name + logging.debug("Chat State: %s, %s" % (state, msg['from'].jid)) + self.xmpp.event('chatstate_%s' % state, msg) diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py new file mode 100644 index 0000000..8878e31 --- /dev/null +++ b/tests/test_chatstates.py @@ -0,0 +1,47 @@ +import unittest +from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +from . import xmlcompare + +import sleekxmpp.plugins.xep_0085 as cs + +def stanzaPlugin(stanza, plugin): + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin + +class testchatstates(unittest.TestCase): + + def setUp(self): + self.cs = cs + stanzaPlugin(self.cs.Message, self.cs.Active) + stanzaPlugin(self.cs.Message, self.cs.Composing) + stanzaPlugin(self.cs.Message, self.cs.Gone) + stanzaPlugin(self.cs.Message, self.cs.Inactive) + stanzaPlugin(self.cs.Message, self.cs.Paused) + + def try2Methods(self, xmlstring, msg): + msg2 = self.cs.Message(None, self.cs.ET.fromstring(xmlstring)) + self.failUnless(xmlstring == str(msg) == str(msg2), + "Two methods for creating stanza don't match") + + def testCreateChatState(self): + """Testing creating chat states.""" + xmlstring = """<%s xmlns="http://jabber.org/protocol/chatstates" />""" + + msg = self.cs.Message() + msg['chat_state'].active() + self.try2Methods(xmlstring % 'active', msg) + + msg['chat_state'].composing() + self.try2Methods(xmlstring % 'composing', msg) + + msg['chat_state'].gone() + self.try2Methods(xmlstring % 'gone', msg) + + msg['chat_state'].inactive() + self.try2Methods(xmlstring % 'inactive', msg) + + msg['chat_state'].paused() + self.try2Methods(xmlstring % 'paused', msg) + +suite = unittest.TestLoader().loadTestsFromTestCase(testchatstates) From 332eea3b3bae719c707e99031660197b22fe41ce Mon Sep 17 00:00:00 2001 From: Lance stout Date: Mon, 31 May 2010 13:35:15 -0400 Subject: [PATCH 02/99] Make sure that the node is alway set in disco responses. --- sleekxmpp/plugins/xep_0030.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 6a31d24..2c8d435 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -138,6 +138,9 @@ class DiscoNode(object): 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 From 01e8040a077d4fefdec2c599e75ea93918e9f538 Mon Sep 17 00:00:00 2001 From: Lance stout Date: Tue, 1 Jun 2010 10:51:03 -0400 Subject: [PATCH 03/99] Added additional parameter to xep_0030's getInfo and getItems methods. By using dfrom, a server component may send disco requests using any of its JIDS. --- sleekxmpp/plugins/xep_0030.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 2c8d435..9fcc8b1 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -293,19 +293,19 @@ class xep_0030(base.base_plugin): # Older interface methods for backwards compatibility - def getInfo(self, jid, node=''): + def getInfo(self, jid, node='', dfrom=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid - iq['from'] = self.xmpp.fulljid + iq['from'] = dfrom iq['disco_info']['node'] = node iq.send() - def getItems(self, jid, node=''): + def getItems(self, jid, node='', dfrom=None): iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = jid - iq['from'] = self.xmpp.fulljid + iq['from'] = dfrom iq['disco_items']['node'] = node iq.send() From f5cae85af5c2f9c79ade1e6413436c1206689bdd Mon Sep 17 00:00:00 2001 From: Lance stout Date: Tue, 1 Jun 2010 10:52:37 -0400 Subject: [PATCH 04/99] Make sure that the id parameter used in xmpp.makeIq is converted to a string. Otherwise, SleekXMPP will barf on trying to serialize an integer when it expects text. --- sleekxmpp/basexmpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 907067f..d2dd3fb 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -152,7 +152,7 @@ class basexmpp(object): return waitfor.wait(timeout) def makeIq(self, id=0, ifrom=None): - return self.Iq().setValues({'id': id, 'from': ifrom}) + return self.Iq().setValues({'id': str(id), 'from': ifrom}) def makeIqGet(self, queryxmlns = None): iq = self.Iq().setValues({'type': 'get'}) From e700a54d11a01c0ed24cddb14ceac0511fbc8372 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 2 Jun 2010 15:59:10 -0400 Subject: [PATCH 05/99] Return result of iq.send() for disco requests. Events are still triggered, but now the caller can determine if there was a timeout. --- sleekxmpp/plugins/xep_0030.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 9fcc8b1..73f4636 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -299,7 +299,7 @@ class xep_0030(base.base_plugin): iq['to'] = jid iq['from'] = dfrom iq['disco_info']['node'] = node - iq.send() + return iq.send() def getItems(self, jid, node='', dfrom=None): iq = self.xmpp.Iq() @@ -307,7 +307,7 @@ class xep_0030(base.base_plugin): iq['to'] = jid iq['from'] = dfrom iq['disco_items']['node'] = node - iq.send() + return iq.send() def add_feature(self, feature, node='main'): self.add_node(node) From 253de8518cfb2461f2172fbffd9b2a252924cb00 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 3 Jun 2010 22:42:11 -0400 Subject: [PATCH 06/99] Modified xmlstream.py to pass a clean stanza object to each stream handler. The previous version passed the same stanza object to each registered handler, which can cause issues when the stanza object is modified by one handler. The next handler receives the stanza with the modifications, not the original stanza. --- sleekxmpp/xmlstream/xmlstream.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6b92abc..003ead1 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -21,6 +21,7 @@ import threading import time import traceback import types +import copy import xml.sax.saxutils from . import scheduler @@ -305,19 +306,18 @@ class XMLStream(object): #convert XML into Stanza logging.debug("RECV: %s" % cElementTree.tostring(xmlobj)) xmlobj = self.incoming_filter(xmlobj) - stanza = None + stanza_type = StanzaBase for stanza_class in self.__root_stanza: if xmlobj.tag == "{%s}%s" % (self.default_ns, stanza_class.name): - #if self.__root_stanza[stanza_class].match(xmlobj): - stanza = stanza_class(self, xmlobj) + stanza_type = stanza_class break - if stanza is None: - stanza = StanzaBase(self, xmlobj) unhandled = True + stanza = stanza_type(self, xmlobj) for handler in self.__handlers: if handler.match(stanza): - handler.prerun(stanza) - self.eventqueue.put(('stanza', handler, stanza)) + stanza_copy = stanza_type(self, copy.deepcopy(xmlobj)) + handler.prerun(stanza_copy) + self.eventqueue.put(('stanza', handler, stanza_copy)) if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler)) unhandled = False if unhandled: From 9962f1a664cfe72b88d78d8541f269f290de8643 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 6 Jun 2010 23:12:54 -0400 Subject: [PATCH 07/99] Added a __copy__ method to both ElementBase and StanzaBase. Stanzas may now be copied using copy.copy(), which will be useful to prevent stanza objects from being shared between event handlers. --- sleekxmpp/xmlstream/stanzabase.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 64020c8..a0d8f5d 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -10,6 +10,7 @@ import logging import traceback import sys import weakref +import copy if sys.version_info < (3,0): from . import tostring26 as tostring @@ -308,6 +309,9 @@ class ElementBase(tostring.ToString): def appendxml(self, xml): self.xml.append(xml) return self + + def __copy__(self): + return self.__class__(xml=copy.copy(self.xml), parent=self.parent) #def __del__(self): #prevents garbage collection of reference cycle # if self.parent is not None: @@ -386,3 +390,6 @@ class StanzaBase(ElementBase): def send(self): self.stream.sendRaw(self.__str__()) + def __copy__(self): + return self.__class__(xml=copy.copy(self.xml), stream=self.stream) + From 3c939313d2381accf4fe449dd569df2be6fe3c55 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 6 Jun 2010 23:19:07 -0400 Subject: [PATCH 08/99] Modified basexmpp.event() to pass a copy of the event data to each handler. --- sleekxmpp/basexmpp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index d2dd3fb..9728c3f 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -27,6 +27,7 @@ from . stanza.error import Error import logging import threading +import copy import sys @@ -205,12 +206,13 @@ class basexmpp(object): def event(self, name, eventdata = {}): # called on an event for handler in self.event_handlers.get(name, []): + handlerdata = copy.copy(eventdata) if handler[1]: #if threaded #thread.start_new(handler[0], (eventdata,)) - x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,)) + x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(handlerdata,)) x.start() else: - handler[0](eventdata) + handler[0](handlerdata) if handler[2]: #disposable with self.lock: self.event_handlers[name].pop(self.event_handlers[name].index(handler)) From 8bb0f5e34c443a9efc05dadb2a77a95ac94c2c98 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 7 Jun 2010 19:55:39 -0400 Subject: [PATCH 09/99] Needed to use copy.deepcopy() to copy XML objects to make sure that the entire tree is copied. --- sleekxmpp/xmlstream/stanzabase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index a0d8f5d..024fe6c 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -311,7 +311,7 @@ class ElementBase(tostring.ToString): return self def __copy__(self): - return self.__class__(xml=copy.copy(self.xml), parent=self.parent) + return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) #def __del__(self): #prevents garbage collection of reference cycle # if self.parent is not None: @@ -391,5 +391,5 @@ class StanzaBase(ElementBase): self.stream.sendRaw(self.__str__()) def __copy__(self): - return self.__class__(xml=copy.copy(self.xml), stream=self.stream) + return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) From 646a609c0b4e19203b4e2190df9b58fbe40c43fb Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 22 Jun 2010 23:09:50 -0400 Subject: [PATCH 10/99] Added plugin and tests for XEP-0033, Extended Stanza Addresses. XEP-0033 can be useful for interacting with XMPP<->Email gateways. --- sleekxmpp/plugins/__init__.py | 2 +- sleekxmpp/plugins/xep_0033.py | 159 ++++++++++++++++++++++++++++++++++ tests/test_addresses.py | 83 ++++++++++++++++++ 3 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 sleekxmpp/plugins/xep_0033.py create mode 100644 tests/test_addresses.py diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 674c3de..fbc5e01 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -17,4 +17,4 @@ along with SleekXMPP; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ -__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] +__all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py new file mode 100644 index 0000000..09d17ae --- /dev/null +++ b/sleekxmpp/plugins/xep_0033.py @@ -0,0 +1,159 @@ +""" + 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.txt for copying permission. +""" + +import logging +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class Addresses(ElementBase): + namespace = 'http://jabber.org/protocol/address' + name = 'addresses' + plugin_attrib = 'addresses' + interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): + address = Address(parent=self) + address['type'] = atype + address['jid'] = jid + address['node'] = node + address['uri'] = uri + address['desc'] = desc + address['delivered'] = delivered + return address + + def getAddresses(self, atype=None): + addresses = [] + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if atype is not None and addrXML.get('type') == atype: + self.xml.remove(addrXML) + addresses.append(Address(xml=addrXML, parent=None)) + return addresses + + def setAddresses(self, addresses, set_type=None): + self.delAddresses(set_type) + for addr in addresses: + # Remap 'type' to 'atype' to match the add method + if set_type is not None: + addr['type'] = set_type + curr_type = addr.get('type', None) + if curr_type is not None: + del addr['type'] + addr['atype'] = curr_type + self.addAddress(**addr) + + def delAddresses(self, atype=None): + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if atype is not None and addrXML.get('type') == atype: + self.xml.remove(addrXML) + + # -------------------------------------------------------------- + + def delBcc(self): + self.delAddresses('bcc') + + def delCc(self): + self.delAddresses('cc') + + def delNoreply(self): + self.delAddresses('noreply') + + def delReplyroom(self): + self.delAddresses('replyroom') + + def delReplyto(self): + self.delAddresses('replyto') + + def delTo(self): + self.delAddresses('to') + + # -------------------------------------------------------------- + + def getBcc(self): + return self.getAddresses('bcc') + + def getCc(self): + return self.getAddresses('cc') + + def getNoreply(self): + return self.getAddresses('noreply') + + def getReplyroom(self): + return self.getAddresses('replyroom') + + def getReplyto(self): + return self.getAddresses('replyto') + + def getTo(self): + return self.getAddresses('to') + + # -------------------------------------------------------------- + + def setBcc(self, addresses): + self.setAddresses(addresses, 'bcc') + + def setCc(self, addresses): + self.setAddresses(addresses, 'cc') + + def setNoreply(self, addresses): + self.setAddresses(addresses, 'noreply') + + def setReplyroom(self, addresses): + self.setAddresses(addresses, 'replyroom') + + def setReplyto(self, addresses): + self.setAddresses(addresses, 'replyto') + + def setTo(self, addresses): + self.setAddresses(addresses, 'to') + + +class Address(ElementBase): + namespace = 'http://jabber.org/protocol/address' + name = 'address' + plugin_attrib = 'address' + interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def getDelivered(self): + return self.attrib.get('delivered', False) + + def setDelivered(self, delivered): + if delivered: + self.xml.attrib['delivered'] = "true" + else: + del self['delivered'] + + def setUri(self, uri): + if uri: + del self['jid'] + del self['node'] + self.xml.attrib['uri'] = uri + elif 'uri' in self.xml.attrib: + del self.xml.attrib['uri'] + + +class xep_0030(base.base_plugin): + """ + XEP-0033: Extended Stanza Addressing + """ + + def plugin_init(self): + self.xep = '0033' + self.description = 'Extended Stanza Addressing' + + self.xmpp.stanzaPlugin(Message, Addresses) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) diff --git a/tests/test_addresses.py b/tests/test_addresses.py new file mode 100644 index 0000000..eb57537 --- /dev/null +++ b/tests/test_addresses.py @@ -0,0 +1,83 @@ +import unittest +from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +from . import xmlcompare + +import sleekxmpp.plugins.xep_0033 as addr + + +def stanzaPlugin(stanza, plugin): + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin + + +class testaddresses(unittest.TestCase): + def setUp(self): + self.addr = addr + stanzaPlugin(self.addr.Message, self.addr.Addresses) + + def try2Methods(self, xmlstring, msg): + msg2 = self.addr.Message(None, self.addr.ET.fromstring(xmlstring)) + self.failUnless(xmlstring == str(msg) == str(msg2), + """Three methods for creating stanza don't match:\n%s\n%s\n%s""" % (xmlstring, str(msg), str(msg2))) + + def testAddAddress(self): + """Testing adding extended stanza address.""" + xmlstring = """
""" + + msg = self.addr.Message() + msg['addresses'].addAddress(atype='to', jid='to@header1.org') + self.try2Methods(xmlstring, msg) + + xmlstring = """
""" + + msg = self.addr.Message() + msg['addresses'].addAddress(atype='replyto', jid='replyto@header1.org', desc='Reply address') + self.try2Methods(xmlstring, msg) + + def testAddAddresses(self): + """Testing adding multiple extended stanza addresses.""" + + xmlstring = """
""" + + msg = self.addr.Message() + msg['addresses'].setAddresses([{'type':'replyto', 'jid':'replyto@header1.org', 'desc':'Reply address'}, + {'type':'cc', 'jid':'cc@header2.org'}, + {'type':'bcc', 'jid':'bcc@header2.org'}]) + self.try2Methods(xmlstring, msg) + + msg = self.addr.Message() + msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', 'desc':'Reply address'}] + msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] + msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] + self.try2Methods(xmlstring, msg) + + def testAddURI(self): + """Testing adding URI attribute to extended stanza address.""" + + xmlstring = """
""" + msg = self.addr.Message() + addr = msg['addresses'].addAddress(atype='to', jid='to@header1.org', node='foo') + self.try2Methods(xmlstring, msg) + + xmlstring = """
""" + addr['uri'] = 'mailto:to@header2.org' + self.try2Methods(xmlstring, msg) + + def testDelivered(self): + """Testing delivered attribute of extended stanza addresses.""" + + xmlstring = """
""" + + msg = self.addr.Message() + addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') + self.try2Methods(xmlstring % '', msg) + + addr['delivered'] = True + self.try2Methods(xmlstring % 'delivered="true" ', msg) + + addr['delivered'] = False + self.try2Methods(xmlstring % '', msg) + + +suite = unittest.TestLoader().loadTestsFromTestCase(testaddresses) From acb53ba371952e70a53e96d7c7b71961b68b36a8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 10:14:21 -0400 Subject: [PATCH 11/99] Fixed tab and spacing issue to please the Tab Nanny during unit tests. --- sleekxmpp/plugins/xep_0033.py | 44 +++++++++++++++++------------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index 09d17ae..76bea6a 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -30,7 +30,7 @@ class Addresses(ElementBase): address['delivered'] = delivered return address - def getAddresses(self, atype=None): + def getAddresses(self, atype=None): addresses = [] for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall @@ -39,7 +39,7 @@ class Addresses(ElementBase): addresses.append(Address(xml=addrXML, parent=None)) return addresses - def setAddresses(self, addresses, set_type=None): + def setAddresses(self, addresses, set_type=None): self.delAddresses(set_type) for addr in addresses: # Remap 'type' to 'atype' to match the add method @@ -51,7 +51,7 @@ class Addresses(ElementBase): addr['atype'] = curr_type self.addAddress(**addr) - def delAddresses(self, atype=None): + def delAddresses(self, atype=None): for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall if atype is not None and addrXML.get('type') == atype: @@ -62,59 +62,59 @@ class Addresses(ElementBase): def delBcc(self): self.delAddresses('bcc') - def delCc(self): + def delCc(self): self.delAddresses('cc') - def delNoreply(self): + def delNoreply(self): self.delAddresses('noreply') - def delReplyroom(self): + def delReplyroom(self): self.delAddresses('replyroom') - def delReplyto(self): + def delReplyto(self): self.delAddresses('replyto') - def delTo(self): + def delTo(self): self.delAddresses('to') # -------------------------------------------------------------- - def getBcc(self): + def getBcc(self): return self.getAddresses('bcc') def getCc(self): return self.getAddresses('cc') - def getNoreply(self): + def getNoreply(self): return self.getAddresses('noreply') - def getReplyroom(self): + def getReplyroom(self): return self.getAddresses('replyroom') - def getReplyto(self): + def getReplyto(self): return self.getAddresses('replyto') - def getTo(self): + def getTo(self): return self.getAddresses('to') # -------------------------------------------------------------- - def setBcc(self, addresses): + def setBcc(self, addresses): self.setAddresses(addresses, 'bcc') - def setCc(self, addresses): + def setCc(self, addresses): self.setAddresses(addresses, 'cc') - def setNoreply(self, addresses): + def setNoreply(self, addresses): self.setAddresses(addresses, 'noreply') - def setReplyroom(self, addresses): + def setReplyroom(self, addresses): self.setAddresses(addresses, 'replyroom') - def setReplyto(self, addresses): + def setReplyto(self, addresses): self.setAddresses(addresses, 'replyto') - def setTo(self, addresses): + def setTo(self, addresses): self.setAddresses(addresses, 'to') @@ -123,9 +123,9 @@ class Address(ElementBase): name = 'address' plugin_attrib = 'address' interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) - address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) - def getDelivered(self): + def getDelivered(self): return self.attrib.get('delivered', False) def setDelivered(self, delivered): @@ -134,7 +134,7 @@ class Address(ElementBase): else: del self['delivered'] - def setUri(self, uri): + def setUri(self, uri): if uri: del self['jid'] del self['node'] From 6041cd1952cbe3a54354eaa6395d49986632e871 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 16:33:59 -0400 Subject: [PATCH 12/99] Fixed typo --- sleekxmpp/plugins/xep_0030.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 73f4636..93e094f 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permissio + See the file license.txt for copying permission. """ import logging From 309c9e74eb906af42841d2ab783aa18843c08878 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 16:34:48 -0400 Subject: [PATCH 13/99] Fixed error in setState() method. --- sleekxmpp/plugins/xep_0085.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py index e183ec2..e24e9db 100644 --- a/sleekxmpp/plugins/xep_0085.py +++ b/sleekxmpp/plugins/xep_0085.py @@ -38,8 +38,9 @@ class ChatState(ElementBase): def setState(self, state): if state in self.states: self.name = state - self.xml.tag = state - self.xml.attrib['xmlns'] = self.namespace + self.xml.tag = '{%s}%s' % (self.namespace, state) + else: + raise ValueError('Invalid chat state') def getState(self): return self.name From 059cc9ccc4c41942f6a9b2ba3baab443a91d2a60 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 17:32:16 -0400 Subject: [PATCH 14/99] Fixed several errors in xep_0033 plugin. The method getAddresses was removing addresses by mistake. Several instances of using self.attrib instead of self.xml.attrib. --- sleekxmpp/plugins/xep_0033.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index 76bea6a..80d58a4 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -34,14 +34,14 @@ class Addresses(ElementBase): addresses = [] for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall - if atype is not None and addrXML.get('type') == atype: - self.xml.remove(addrXML) - addresses.append(Address(xml=addrXML, parent=None)) + if atype is None or addrXML.attrib.get('type') == atype: + addresses.append(Address(xml=addrXML, parent=None)) return addresses def setAddresses(self, addresses, set_type=None): self.delAddresses(set_type) for addr in addresses: + addr = dict(addr) # Remap 'type' to 'atype' to match the add method if set_type is not None: addr['type'] = set_type @@ -52,9 +52,11 @@ class Addresses(ElementBase): self.addAddress(**addr) def delAddresses(self, atype=None): + if atype is None: + return for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall - if atype is not None and addrXML.get('type') == atype: + if addrXML.attrib.get('type') == atype: self.xml.remove(addrXML) # -------------------------------------------------------------- @@ -126,7 +128,7 @@ class Address(ElementBase): address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) def getDelivered(self): - return self.attrib.get('delivered', False) + return self.xml.attrib.get('delivered', False) def setDelivered(self, delivered): if delivered: From 5c76d969f79936ce56828dba2c5ab59214feec12 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 17:33:43 -0400 Subject: [PATCH 15/99] Added a new SleekTest class that provides useful methods for test cases. Can now use: (where self is a SleekTest instance) self.stanzaPlugin(stanza, plugin) self.Message() \ self.Iq() > Just like basexmpp.Message(), etc. self.Presence() / self.checkMessage(msg, xmlstring) self.checkIq(iq, xmlstring) self.checkPresence(pres, xmlstring) <- Not implemented yet, but stub is there. The check* methods also accept a use_values keyword argument that defaults to True. When this value is True, an additional test is executed by creating a stanza using getValues() and setValues(). Since some stanza objects can override these two methods, disabling this test is sometimes required. --- tests/sleektest.py | 159 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 tests/sleektest.py diff --git a/tests/sleektest.py b/tests/sleektest.py new file mode 100644 index 0000000..eed52ec --- /dev/null +++ b/tests/sleektest.py @@ -0,0 +1,159 @@ +""" + 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.txt for copying permission. +""" + +import unittest +from xml.etree import cElementTree as ET +from sleekxmpp import Message, Iq +from sleekxmpp.stanza.presence import Presence +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath + + +class SleekTest(unittest.TestCase): + """ + A SleekXMPP specific TestCase class that provides + methods for comparing message, iq, and presence stanzas. + """ + + def stanzaPlugin(self, stanza, plugin): + """ + Associate a stanza object as a plugin for another stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin + + def Message(self, *args, **kwargs): + """Create a message stanza.""" + return Message(None, *args, **kwargs) + + def Iq(self, *args, **kwargs): + """Create an iq stanza.""" + return Iq(None, *args, **kwargs) + + def Presence(self, *args, **kwargs): + """Create a presence stanza.""" + return Presence(None, *args, **kwargs) + + def checkMessage(self, msg, xml_string, use_values=True): + """ + Create and compare several message stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + + debug = "Given Stanza:\n%s\n" % ET.tostring(msg.xml) + + xml = ET.fromstring(xml_string) + xml.tag = '{jabber:client}message' + debug += "XML String:\n%s\n" % ET.tostring(xml) + + msg2 = self.Message(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) + + if use_values: + # Ugly, but need to make sure the type attribute is set. + msg['type'] = msg['type'] + if xml.attrib.get('type', None) is None: + xml.attrib['type'] = 'normal' + + values = msg2.getValues() + msg3 = self.Message() + msg3.setValues(values) + + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) + debug = "Three methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, msg.xml, + msg2.xml, msg3.xml]), + debug) + else: + debug = "Two methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, msg.xml, msg2.xml]), debug) + + def checkIq(self, iq, xml_string, use_values=True): + """ + Create and compare several iq stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + debug = "Given Stanza:\n%s\n" % ET.tostring(iq.xml) + + xml = ET.fromstring(xml_string) + xml.tag = '{jabber:client}iq' + debug += "XML String:\n%s\n" % ET.tostring(xml) + + iq2 = self.Iq(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(iq2.xml) + + if use_values: + values = iq.getValues() + iq3 = self.Iq() + iq3.setValues(values) + + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(iq3.xml) + debug = "Three methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), + debug) + else: + debug = "Two methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, iq.xml, iq2.xml]), debug) + + def checkPresence(self, pres, xml_string, use_values=True): + """ + Create and compare several presence stanza objects to a + correct XML string. + + If use_values is False, the test using getValues() and + setValues() will not be used. + """ + pass + + def compare(self, xml1, xml2=None): + """ + Compare XML objects. + + If given a list of XML objects, then + all of the elements in the list will be + compared. + """ + + # Compare multiple objects + if type(xml1) is list: + xmls = xml1 + xml1 = xmls[0] + for xml in xmls[1:]: + xml2 = xml + if not self.compare(xml1, xml2): + return False + return True + + # Step 1: Check tags + if xml1.tag != xml2.tag: + print xml1.tag, xml2.tag + return False + + # Step 2: Check attributes + if xml1.attrib != xml2.attrib: + return False + + # Step 3: Recursively check children + for child in xml1: + child2s = xml2.findall("%s" % child.tag) + if child2s is None: + return False + for child2 in child2s: + if self.compare(child, child2): + break + else: + return False + + # Everything matches + return True From 37ada498029c12459e5724ed27db92c828ce2a38 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 17:39:16 -0400 Subject: [PATCH 16/99] Fixed indentation to please tab nanny during unit tests. --- sleekxmpp/plugins/xep_0033.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index 80d58a4..89770dc 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -35,7 +35,7 @@ class Addresses(ElementBase): for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall if atype is None or addrXML.attrib.get('type') == atype: - addresses.append(Address(xml=addrXML, parent=None)) + addresses.append(Address(xml=addrXML, parent=None)) return addresses def setAddresses(self, addresses, set_type=None): From 7f8179d91e4f8ae3d58448e57f2e350ed6269fd0 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 27 Jun 2010 17:40:06 -0400 Subject: [PATCH 17/99] Refactored unit tests for XEP-0030, XEP-0033, and XEP-0085 to use the new SleekTest class. --- tests/test_addresses.py | 123 ++++++++++++++++++++++++--------------- tests/test_chatstates.py | 57 +++++++++--------- tests/test_disco.py | 113 ++++++++++++++++++++--------------- 3 files changed, 169 insertions(+), 124 deletions(-) diff --git a/tests/test_addresses.py b/tests/test_addresses.py index eb57537..2718bb1 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -1,83 +1,110 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare - -import sleekxmpp.plugins.xep_0033 as addr +from sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin +class TestAddresses(SleekTest): - -class testaddresses(unittest.TestCase): def setUp(self): - self.addr = addr - stanzaPlugin(self.addr.Message, self.addr.Addresses) - - def try2Methods(self, xmlstring, msg): - msg2 = self.addr.Message(None, self.addr.ET.fromstring(xmlstring)) - self.failUnless(xmlstring == str(msg) == str(msg2), - """Three methods for creating stanza don't match:\n%s\n%s\n%s""" % (xmlstring, str(msg), str(msg2))) + self.stanzaPlugin(Message, xep_0033.Addresses) def testAddAddress(self): """Testing adding extended stanza address.""" - xmlstring = """
""" - - msg = self.addr.Message() + msg = self.Message() msg['addresses'].addAddress(atype='to', jid='to@header1.org') - self.try2Methods(xmlstring, msg) + self.checkMessage(msg, """ + + +
+ + + """) - xmlstring = """
""" - - msg = self.addr.Message() - msg['addresses'].addAddress(atype='replyto', jid='replyto@header1.org', desc='Reply address') - self.try2Methods(xmlstring, msg) + msg = self.Message() + msg['addresses'].addAddress(atype='replyto', + jid='replyto@header1.org', + desc='Reply address') + self.checkMessage(msg, """ + + +
+ + + """) def testAddAddresses(self): """Testing adding multiple extended stanza addresses.""" - xmlstring = """
""" + xmlstring = """ + + +
+
+
+ + + """ - msg = self.addr.Message() - msg['addresses'].setAddresses([{'type':'replyto', 'jid':'replyto@header1.org', 'desc':'Reply address'}, - {'type':'cc', 'jid':'cc@header2.org'}, - {'type':'bcc', 'jid':'bcc@header2.org'}]) - self.try2Methods(xmlstring, msg) + msg = self.Message() + msg['addresses'].setAddresses([{'type':'replyto', + 'jid':'replyto@header1.org', + 'desc':'Reply address'}, + {'type':'cc', + 'jid':'cc@header2.org'}, + {'type':'bcc', + 'jid':'bcc@header2.org'}]) + self.checkMessage(msg, xmlstring) - msg = self.addr.Message() - msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', 'desc':'Reply address'}] + msg = self.Message() + msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', + 'desc':'Reply address'}] msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] - self.try2Methods(xmlstring, msg) + self.checkMessage(msg, xmlstring) def testAddURI(self): """Testing adding URI attribute to extended stanza address.""" - xmlstring = """
""" - msg = self.addr.Message() - addr = msg['addresses'].addAddress(atype='to', jid='to@header1.org', node='foo') - self.try2Methods(xmlstring, msg) + msg = self.Message() + addr = msg['addresses'].addAddress(atype='to', + jid='to@header1.org', + node='foo') + self.checkMessage(msg, """ + + +
+ + + """) - xmlstring = """
""" addr['uri'] = 'mailto:to@header2.org' - self.try2Methods(xmlstring, msg) + self.checkMessage(msg, """ + + +
+ + + """) def testDelivered(self): """Testing delivered attribute of extended stanza addresses.""" - xmlstring = """
""" + xmlstring = """ + + +
+ + + """ - msg = self.addr.Message() + msg = self.Message() addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') - self.try2Methods(xmlstring % '', msg) + self.checkMessage(msg, xmlstring % '') addr['delivered'] = True - self.try2Methods(xmlstring % 'delivered="true" ', msg) + self.checkMessage(msg, xmlstring % 'delivered="true"') addr['delivered'] = False - self.try2Methods(xmlstring % '', msg) + self.checkMessage(msg, xmlstring % '') -suite = unittest.TestLoader().loadTestsFromTestCase(testaddresses) +suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py index 8878e31..1e585be 100644 --- a/tests/test_chatstates.py +++ b/tests/test_chatstates.py @@ -1,47 +1,44 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from sleektest import * +import sleekxmpp.plugins.xep_0085 as xep_0085 -import sleekxmpp.plugins.xep_0085 as cs - -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin - -class testchatstates(unittest.TestCase): +class TestChatStates(SleekTest): def setUp(self): - self.cs = cs - stanzaPlugin(self.cs.Message, self.cs.Active) - stanzaPlugin(self.cs.Message, self.cs.Composing) - stanzaPlugin(self.cs.Message, self.cs.Gone) - stanzaPlugin(self.cs.Message, self.cs.Inactive) - stanzaPlugin(self.cs.Message, self.cs.Paused) - - def try2Methods(self, xmlstring, msg): - msg2 = self.cs.Message(None, self.cs.ET.fromstring(xmlstring)) - self.failUnless(xmlstring == str(msg) == str(msg2), - "Two methods for creating stanza don't match") + self.stanzaPlugin(Message, xep_0085.Active) + self.stanzaPlugin(Message, xep_0085.Composing) + self.stanzaPlugin(Message, xep_0085.Gone) + self.stanzaPlugin(Message, xep_0085.Inactive) + self.stanzaPlugin(Message, xep_0085.Paused) def testCreateChatState(self): """Testing creating chat states.""" - xmlstring = """<%s xmlns="http://jabber.org/protocol/chatstates" />""" + + xmlstring = """ + + <%s xmlns="http://jabber.org/protocol/chatstates" /> + + """ - msg = self.cs.Message() + msg = self.Message() msg['chat_state'].active() - self.try2Methods(xmlstring % 'active', msg) + self.checkMessage(msg, xmlstring % 'active', + use_values=False) msg['chat_state'].composing() - self.try2Methods(xmlstring % 'composing', msg) + self.checkMessage(msg, xmlstring % 'composing', + use_values=False) + msg['chat_state'].gone() - self.try2Methods(xmlstring % 'gone', msg) + self.checkMessage(msg, xmlstring % 'gone', + use_values=False) msg['chat_state'].inactive() - self.try2Methods(xmlstring % 'inactive', msg) + self.checkMessage(msg, xmlstring % 'inactive', + use_values=False) msg['chat_state'].paused() - self.try2Methods(xmlstring % 'paused', msg) + self.checkMessage(msg, xmlstring % 'paused', + use_values=False) -suite = unittest.TestLoader().loadTestsFromTestCase(testchatstates) +suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) diff --git a/tests/test_disco.py b/tests/test_disco.py index bbe285a..6daad13 100644 --- a/tests/test_disco.py +++ b/tests/test_disco.py @@ -1,96 +1,118 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from sleektest import * +import sleekxmpp.plugins.xep_0030 as xep_0030 -import sleekxmpp.plugins.xep_0030 as sd -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin - -class testdisco(unittest.TestCase): +class TestDisco(SleekTest): def setUp(self): - self.sd = sd - stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo) - stanzaPlugin(self.sd.Iq, self.sd.DiscoItems) - - def try3Methods(self, xmlstring, iq): - iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring)) - values = iq2.getValues() - iq3 = self.sd.Iq() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match") + self.stanzaPlugin(Iq, xep_0030.DiscoInfo) + self.stanzaPlugin(Iq, xep_0030.DiscoItems) def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = '' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateInfoQueryWithNode(self): """Testing disco#info query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateInfoQueryNoNode(self): """Testing disco#items query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = '' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateItemsQueryWithNode(self): """Testing disco#items query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = 'foo' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testInfoIdentities(self): """Testing adding identities to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + + + """) def testInfoFeatures(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' iq['disco_info'].addFeature('foo') iq['disco_info'].addFeature('bar') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + + + + """) def testItems(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + 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') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + + + + + """) def testAddRemoveIdentities(self): """Test adding and removing identities to disco#info stanza""" ids = [('automation', 'commands', 'AdHoc'), ('conference', 'text', 'ChatRoom')] - info = self.sd.DiscoInfo() + info = xep_0030.DiscoInfo() info.addIdentity(*ids[0]) self.failUnless(info.getIdentities() == [ids[0]]) @@ -110,7 +132,7 @@ class testdisco(unittest.TestCase): """Test adding and removing features to disco#info stanza""" features = ['foo', 'bar', 'baz'] - info = self.sd.DiscoInfo() + info = xep_0030.DiscoInfo() info.addFeature(features[0]) self.failUnless(info.getFeatures() == [features[0]]) @@ -132,7 +154,7 @@ class testdisco(unittest.TestCase): ('user@localhost', 'foo', None), ('user@localhost', 'bar', 'Test')] - info = self.sd.DiscoItems() + info = xep_0030.DiscoItems() self.failUnless(True, ""+str(items[0])) info.addItem(*(items[0])) @@ -151,5 +173,4 @@ class testdisco(unittest.TestCase): self.failUnless(info.getItems() == []) - -suite = unittest.TestLoader().loadTestsFromTestCase(testdisco) +suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) From d0cb400c544abb432fa9b67038381be8172c2efb Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 11 Jul 2010 21:43:51 -0400 Subject: [PATCH 18/99] Fixed tabs to please tab nanny. --- sleekxmpp/plugins/xep_0033.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index 89770dc..df8bb88 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -41,7 +41,7 @@ class Addresses(ElementBase): def setAddresses(self, addresses, set_type=None): self.delAddresses(set_type) for addr in addresses: - addr = dict(addr) + addr = dict(addr) # Remap 'type' to 'atype' to match the add method if set_type is not None: addr['type'] = set_type @@ -52,8 +52,8 @@ class Addresses(ElementBase): self.addAddress(**addr) def delAddresses(self, atype=None): - if atype is None: - return + if atype is None: + return for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall if addrXML.attrib.get('type') == atype: From b1c997be1d7d7d456cad06ef436c01527e04df5d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 11 Jul 2010 22:01:51 -0400 Subject: [PATCH 19/99] Reworked the Gmail notification plugin to use stanza objects and expose more information. --- sleekxmpp/plugins/gmail_notify.py | 191 ++++++++++++++++++++++-------- tests/test_gmail.py | 88 ++++++++++++++ 2 files changed, 228 insertions(+), 51 deletions(-) create mode 100644 tests/test_gmail.py diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index b709ef6..acfc38b 100644 --- a/sleekxmpp/plugins/gmail_notify.py +++ b/sleekxmpp/plugins/gmail_notify.py @@ -1,57 +1,146 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + See the file license.txt for copying permission. """ -from __future__ import with_statement -from . import base + import logging -from xml.etree import cElementTree as ET -import traceback -import time +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.iq import Iq + + +class GmailQuery(ElementBase): + namespace = 'google:mail:notify' + name = 'query' + plugin_attrib = 'gmail' + interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search')) + + def getSearch(self): + return self['q'] + + def setSearch(self, search): + self['q'] = search + + def delSearch(self): + del self['q'] + + +class MailBox(ElementBase): + namespace = 'google:mail:notify' + name = 'mailbox' + plugin_attrib = 'mailbox' + interfaces = set(('result-time', 'total-matched', 'total-estimate', + 'url', 'threads', 'matched', 'estimate')) + + def getThreads(self): + threads = [] + for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace, + MailThread.name)): + threads.append(MailThread(xml=threadXML, parent=None)) + return threads + + def getMatched(self): + return self['total-matched'] + + def getEstimate(self): + return self['total-estimate'] == '1' + + +class MailThread(ElementBase): + namespace = 'google:mail:notify' + name = 'mail-thread-info' + plugin_attrib = 'thread' + interfaces = set(('tid', 'participation', 'messages', 'date', + 'senders', 'url', 'labels', 'subject', 'snippet')) + sub_interfaces = set(('labels', 'subject', 'snippet')) + + def getSenders(self): + senders = [] + sendersXML = self.xml.find('{%s}senders' % self.namespace) + if sendersXML is not None: + for senderXML in sendersXML.findall('{%s}sender' % self.namespace): + senders.append(MailSender(xml=senderXML, parent=None)) + return senders + + +class MailSender(ElementBase): + namespace = 'google:mail:notify' + name = 'sender' + plugin_attrib = 'sender' + interfaces = set(('address', 'name', 'originator', 'unread')) + + def getOriginator(self): + return self.xml.attrib.get('originator', '0') == '1' + + def getUnread(self): + return self.xml.attrib.get('unread', '0') == '1' + + +class NewMail(ElementBase): + namespace = 'google:mail:notify' + name = 'new-mail' + plugin_attrib = 'new-mail' + class gmail_notify(base.base_plugin): - - def plugin_init(self): - self.description = 'Google Talk Gmail Notification' - self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True) - self.emails = [] - - def handler_gmailcheck(self, payload): - #TODO XEP 30 should cache results and have getFeature - result = self.xmpp['xep_0030'].getInfo(self.xmpp.server) - features = [] - for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'): - features.append(feature.get('var')) - if 'google:mail:notify' in features: - logging.debug("Server supports Gmail Notify") - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_notify) - self.getEmail() - - def handler_notify(self, xml): - logging.info("New Gmail recieved!") - self.xmpp.event('gmail_notify') - - def getEmail(self): - iq = self.xmpp.makeIqGet() - iq.attrib['from'] = self.xmpp.fulljid - iq.attrib['to'] = self.xmpp.jid - self.xmpp.makeIqQuery(iq, 'google:mail:notify') - emails = iq.send() - mailbox = emails.find('{google:mail:notify}mailbox') - total = int(mailbox.get('total-matched', 0)) - logging.info("%s New Gmail Messages" % total) + """ + Google Talk: Gmail Notifications + """ + + def plugin_init(self): + self.description = 'Google Talk: Gmail Notifications' + + self.xmpp.registerHandler( + Callback('Gmail Result', + MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, + MailBox.namespace, + MailBox.name)), + self.handle_gmail)) + + self.xmpp.registerHandler( + Callback('Gmail New Mail', + MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, + NewMail.namespace, + NewMail.name)), + self.handle_new_mail)) + + self.xmpp.stanzaPlugin(Iq, GmailQuery) + self.xmpp.stanzaPlugin(Iq, MailBox) + self.xmpp.stanzaPlugin(Iq, NewMail) + + self.last_result_time = None + + def handle_gmail(self, iq): + mailbox = iq['mailbox'] + approx = ' approximately' if mailbox['estimated'] else '' + logging.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) + self.last_result_time = mailbox['result-time'] + self.xmpp.event('gmail_messages', iq) + + def handle_new_mail(self, iq): + logging.info("Gmail: New emails received!") + self.xmpp.event('gmail_notify') + self.checkEmail() + + def getEmail(self, query=None): + return self.search(query) + + def checkEmail(self): + return self.search(newer=self.last_result_time) + + def search(self, query=None, newer=None): + if query is None: + logging.info("Gmail: Checking for new emails") + else: + logging.info('Gmail: Searching for emails matching: "%s"' % query) + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = self.xmpp.jid + iq['gmail']['q'] = query + iq['gmail']['newer-than-time'] = newer + return iq.send() diff --git a/tests/test_gmail.py b/tests/test_gmail.py new file mode 100644 index 0000000..199b76a --- /dev/null +++ b/tests/test_gmail.py @@ -0,0 +1,88 @@ +from sleektest import * +import sleekxmpp.plugins.gmail_notify as gmail + + +class TestGmail(SleekTest): + + def setUp(self): + self.stanzaPlugin(Iq, gmail.GmailQuery) + self.stanzaPlugin(Iq, gmail.MailBox) + self.stanzaPlugin(Iq, gmail.NewMail) + + def testCreateQuery(self): + """Testing querying Gmail for emails.""" + + iq = self.Iq() + iq['type'] = 'get' + iq['gmail']['search'] = 'is:starred' + iq['gmail']['newer-than-time'] = '1140638252542' + iq['gmail']['newer-than-tid'] = '11134623426430234' + + self.checkIq(iq, """ + + + + """) + + def testMailBox(self): + """Testing reading from Gmail mailbox result""" + + # Use the example from Google's documentation at + # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications + xml = ET.fromstring(""" + + + + + + + + + act1scene3 + Put thy rapier up. + Ay, ay, a scratch, a scratch; marry, 'tis enough. + + + + """) + + iq = self.Iq(xml=xml) + mailbox = iq['mailbox'] + self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match") + self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match") + self.failUnless(mailbox['matched'] == '95', "total-matched incorrect") + self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect") + self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads") + + thread = mailbox['threads'][0] + self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match") + self.failUnless(thread['participation'] == '1', "thread participation incorrect") + self.failUnless(thread['messages'] == '28', "thread message count incorrect") + self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match") + self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match") + self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect") + self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match") + self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match") + self.failUnless(len(thread['senders']) == 3, "could not extract senders") + + sender1 = thread['senders'][0] + self.failUnless(sender1['name'] == 'Me', "sender name doesn't match") + self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match") + self.failUnless(sender1['originator'] == True, "sender originator incorrect") + self.failUnless(sender1['unread'] == False, "sender unread incorrectly True") + + sender2 = thread['senders'][2] + self.failUnless(sender2['unread'] == True, "sender unread incorrectly False") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail) From 48f0843ace5a8d383abe493b999e7bd4699598d7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 14 Jul 2010 11:59:58 -0400 Subject: [PATCH 20/99] Added initial stanza object version of the xep_0004 plugin. Items/reported elements still need to be unit tested --- sleekxmpp/plugins/alt_0004.py | 330 ++++++++++++++++++++++++++++++++++ tests/test_forms.py | 90 ++++++++++ 2 files changed, 420 insertions(+) create mode 100644 sleekxmpp/plugins/alt_0004.py create mode 100644 tests/test_forms.py diff --git a/sleekxmpp/plugins/alt_0004.py b/sleekxmpp/plugins/alt_0004.py new file mode 100644 index 0000000..b38a491 --- /dev/null +++ b/sleekxmpp/plugins/alt_0004.py @@ -0,0 +1,330 @@ +""" + 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.txt for copying permission. +""" + +import logging +import copy +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class Form(ElementBase): + namespace = 'jabber:x:data' + name = 'x' + plugin_attrib = 'form' + interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) + sub_interfaces = set(('title',)) + form_types = set(('cancel', 'form', 'result', 'submit')) + + def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): + field = FormField(parent=self) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + field['required'] = required + field['value'] = value + if options is not None: + field['options'] = options + return field + + def addItem(self, values): + itemXML = ET.Element('{%s}item' % self.namespace) + self.xml.append(itemXML) + reported_vars = self['reported'].keys() + for var in reported_vars: + fieldXML = ET.Element('{%s}field' % FormField.namespace) + itemXML.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['value'] = values.get(var, None) + + def addReported(self, var, ftype='text-single', label='', desc=''): + reported = self.xml.find('{%s}reported' % self.namespace) + if reported is None: + reported = ET.Element('{%s}reported' % self.namespace) + self.xml.append(reported) + fieldXML = ET.Element('{%s}field' % FormField.namespace) + reported.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + return field + + def cancel(self): + self['type'] = 'cancel' + + def delFields(self): + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + self.xml.remove(fieldXML) + + def delInstructions(self): + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + self.xml.remove(instXML) + + def delItems(self): + itemsXML = self.xml.find('{%s}item' % self.namespace) + for itemXML in itemsXML: + self.xml.remove(itemXML) + + def delReported(self): + reportedXML = self.xml.find('{%s}reported' % self.namespace) + if reportedXML is not None: + self.xml.remove(reportedXML) + + def getFields(self): + fields = {} + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getInstructions(self): + instructions = '' + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + instructions += instXML.text + + def getItems(self): + items = [] + itemsXML = self.xml.findall('{%s}item' % self.namespace) + for itemXML in itemsXML: + item = {} + fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + item[field['var']] = field['value'] + items.append(item) + return items + + def getReported(self): + fields = {} + fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, + FormField.namespace)) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getValues(self): + values = {} + fields = self.getFields() + for var in fields: + values[var] = fields[var]['value'] + return values + + def reply(self): + if self['type'] == 'form': + self['type'] = 'submit' + elif self['type'] == 'submit': + self['type'] = 'result' + + def setFields(self, fields): + del self['fields'] + for var in fields: + field = fields[var] + + # Remap 'type' to 'ftype' to match the addField method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addField(var, **field) + + def setInstructions(self, instructions): + instructions = instructions.split('\n') + for instruction in instructions: + inst = ET.Element('{%s}instructions' % self.namespace) + inst.text = instruction + self.xml.append(inst) + + def setItems(self, items): + for item in items: + self.addItem(item) + + def setReported(self, reported): + for var in reported: + field = reported[var] + + # Remap 'type' to 'ftype' to match the addReported method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addReported(var, **field) + + def setValues(self, values): + fields = self.getFields() + for field in values: + fields[field]['value'] = values[field] + + +class FormField(ElementBase): + namespace = 'jabber:x:data' + name = 'field' + plugin_attrib = 'field' + interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) + sub_interfaces = set(('desc',)) + field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', + 'list-single', 'text-multi', 'text-private', 'text-single')) + multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) + multi_line_types = set(('hidden', 'text-multi')) + option_types = set(('list-multi', 'list-single')) + true_values = set((True, '1', 'true')) + + def addOption(self, label='', value=''): + if self['type'] in self.option_types: + opt = FieldOption(parent=self) + opt['label'] = label + opt['value'] = value + else: + raise ValueError("Cannot add options to a %s field." % self['type']) + + def delOptions(self): + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + self.xml.remove(optXML) + + def delRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + if reqXML is not None: + self.xml.remove(reqXML) + + def delValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + for valXML in valsXML: + self.xml.remove(valXML) + + def getAnswer(self): + return self.getValue() + + def getOptions(self): + options = [] + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + opt = FieldOption(xml=optXML) + options.append({'label': opt['label'], 'value':opt['value']}) + return options + + def getRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + return reqXML is not None + + def getValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + if len(valsXML) == 0: + return None + elif self['type'] == 'boolean': + return valsXML[0].text in self.true_values + elif self['type'] in self.multi_value_types: + values = [] + for valXML in valsXML: + if valXML.text is None: + valXML.text = '' + values.append(valXML.text) + if self['type'] == 'text-multi': + values = "\n".join(values) + return values + else: + return valsXML[0].text + + def setAnswer(self, answer): + self.setValue(answer) + + def setFalse(self): + self.setValue(False) + + def setOptions(self, options): + for value in options: + if isinstance(value, dict): + self.addOption(**value) + else: + self.addOption(value=value) + + def setRequired(self, required): + exists = self.getRequired() + if not exists and required: + self.xml.append(ET.Element('{%s}required' % self.namespace)) + elif exists and not required: + self.delRequired() + + def setTrue(self): + self.setValue(True) + + def setValue(self, value): + self.delValue() + valXMLName = '{%s}value' % self.namespace + + if self['type'] == 'boolean': + if value in self.true_values: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + else: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + if self['type'] in self.multi_value_types: + if self['type'] in self.multi_line_types and isinstance(value, str): + value = value.split('\n') + if not isinstance(value, list): + value = [value] + for val in value: + valXML = ET.Element(valXMLName) + valXML.text = val + self.xml.append(valXML) + else: + if isinstance(value, list): + raise ValueError("Cannot add multiple values to a %s field." % self['type']) + valXML = ET.Element(valXMLName) + valXML.text = value + self.xml.append(valXML) + + +class FieldOption(ElementBase): + namespace = 'jabber:x:data' + name = 'option' + plugin_attrib = 'option' + interfaces = set(('label', 'value')) + sub_interfaces = set(('value',)) + + +class alt_0004(base.base_plugin): + """ + XEP-0004: Data Forms + """ + + def plugin_init(self): + self.xep = '0004' + self.description = 'Data Forms' + + self.xmpp.registerHandler( + Callback('Data Form', + MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, + Form.namespace)), + self.handle_form)) + + self.xmpp.stanzaPlugin(FormField, FieldOption) + self.xmpp.stanzaPlugin(Form, FormField) + self.xmpp.stanzaPlugin(Message, Form) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') + + def handle_form(self, message): + self.xmpp.event("message_xform", message) diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 0000000..981d887 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,90 @@ +from sleektest import * +import sleekxmpp.plugins.alt_0004 as xep_0004 + + +class TestDataForms(SleekTest): + + def setUp(self): + self.stanzaPlugin(Message, xep_0004.Form) + self.stanzaPlugin(xep_0004.Form, xep_0004.FormField) + self.stanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) + + def testMultipleInstructions(self): + """Testing using multiple instructions elements in a data form.""" + msg = self.Message() + msg['form']['instructions'] = "Instructions\nSecond batch" + + self.checkMessage(msg, """ + + + Instructions + Second batch + + + """, use_values=False) + + def testAddField(self): + """Testing adding fields to a data form.""" + + msg = self.Message() + form = msg['form'] + form.addField('f1', + ftype='text-single', + label='Text', + desc='A text field', + required=True, + value='Some text!') + + self.checkMessage(msg, """ + + + + A text field + + Some text! + + + + """, use_values=False) + + form['fields'] = {'f1': {'type': 'text-single', + 'label': 'Username', + 'required': True}, + 'f2': {'type': 'text-private', + 'label': 'Password', + 'required': True}, + 'f3': {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'}, + 'f4': {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]}} + self.checkMessage(msg, """ + + + + + + + + + + Enter message. + A long one even. + + + + + + + + """, use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) From 35212c7991312b26e813afde3bf0bbe002058c11 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 14 Jul 2010 15:24:37 -0400 Subject: [PATCH 21/99] Updated SleekTest to be able to simulate and test interactions with an XML stream. --- tests/sleektest.py | 150 +++++++++++++++++++++++++++++++++++++++++-- tests/test_stream.py | 34 ++++++++++ 2 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 tests/test_stream.py diff --git a/tests/sleektest.py b/tests/sleektest.py index eed52ec..eef3b90 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -7,11 +7,80 @@ """ import unittest +import socket +try: + import queue +except ImportError: + import Queue as queue from xml.etree import cElementTree as ET +from sleekxmpp import ClientXMPP from sleekxmpp import Message, Iq from sleekxmpp.stanza.presence import Presence from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +class TestSocket(object): + + def __init__(self, *args, **kwargs): + self.socket = socket.socket(*args, **kwargs) + self.recv_queue = queue.Queue() + self.send_queue = queue.Queue() + + def __getattr__(self, name): + """Pass requests through to actual socket""" + # Override a few methods to prevent actual socket connections + overrides = {'connect': lambda *args: None, + 'close': lambda *args: None, + 'shutdown': lambda *args: None} + return overrides.get(name, getattr(self.socket, name)) + + # ------------------------------------------------------------------ + # Testing Interface + + def nextSent(self, timeout=None): + """Get the next stanza that has been 'sent'""" + args = {'block': False} + if timeout is not None: + args = {'block': True, 'timeout': timeout} + try: + return self.send_queue.get(**args) + except: + return None + + def recvData(self, data): + """Add data to the receiving queue""" + self.recv_queue.put(data) + + # ------------------------------------------------------------------ + # Socket Interface + + def recv(self, *args, **kwargs): + return self.read(block=True) + + def send(self, data): + self.send_queue.put(data) + + # ------------------------------------------------------------------ + # File Socket + + def makefile(self, mode='r', bufsize=-1): + """File socket version to use with ElementTree""" + return self + + def read(self, size=4096, block=True, timeout=None): + """Implement the file socket interface""" + if timeout is not None: + block = True + try: + return self.recv_queue.get(block, timeout) + except: + return None + +class TestStream(object): + """Dummy class to pass a stream object to created stanzas""" + + def __init__(self): + self.default_ns = 'jabber:client' + class SleekTest(unittest.TestCase): """ @@ -27,6 +96,9 @@ class SleekTest(unittest.TestCase): stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin stanza.plugin_tag_map[tag] = plugin + # ------------------------------------------------------------------ + # Shortcut methods for creating stanza objects + def Message(self, *args, **kwargs): """Create a message stanza.""" return Message(None, *args, **kwargs) @@ -39,6 +111,9 @@ class SleekTest(unittest.TestCase): """Create a presence stanza.""" return Presence(None, *args, **kwargs) + # ------------------------------------------------------------------ + # Methods for comparing stanza objects to XML strings + def checkMessage(self, msg, xml_string, use_values=True): """ Create and compare several message stanza objects to a @@ -48,10 +123,12 @@ class SleekTest(unittest.TestCase): setValues() will not be used. """ + self.fix_namespaces(msg.xml, 'jabber:client') debug = "Given Stanza:\n%s\n" % ET.tostring(msg.xml) xml = ET.fromstring(xml_string) - xml.tag = '{jabber:client}message' + self.fix_namespaces(xml, 'jabber:client') + debug += "XML String:\n%s\n" % ET.tostring(xml) msg2 = self.Message(xml) @@ -69,8 +146,7 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, msg.xml, - msg2.xml, msg3.xml]), + self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug @@ -84,10 +160,12 @@ class SleekTest(unittest.TestCase): If use_values is False, the test using getValues() and setValues() will not be used. """ + + self.fix_namespaces(iq.xml, 'jabber:client') debug = "Given Stanza:\n%s\n" % ET.tostring(iq.xml) xml = ET.fromstring(xml_string) - xml.tag = '{jabber:client}iq' + self.fix_namespaces(xml, 'jabber:client') debug += "XML String:\n%s\n" % ET.tostring(xml) iq2 = self.Iq(xml) @@ -116,6 +194,69 @@ class SleekTest(unittest.TestCase): """ pass + # ------------------------------------------------------------------ + # Methods for simulating stanza streams. + + def streamStart(self, mode='client', skip=True): + if mode == 'client': + self.xmpp = ClientXMPP('tester@localhost', 'test') + self.xmpp.setSocket(TestSocket()) + + self.xmpp.state.set('reconnect', False) + self.xmpp.state.set('is client', True) + self.xmpp.state.set('connected', True) + + # Must have the stream header ready for xmpp.process() to work + self.xmpp.socket.recvData(self.xmpp.stream_header) + + self.xmpp.connectTCP = lambda a, b, c, d: True + self.xmpp.startTLS = lambda: True + self.xmpp.process(threaded=True) + if skip: + # Clear startup stanzas + self.xmpp.socket.nextSent(timeout=1) + + def streamRecv(self, data): + data = str(data) + self.xmpp.socket.recvData(data) + + def streamSendMessage(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Message(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout=1) + self.checkMessage(data, sent, use_values) + + def streamSendIq(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Iq(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkIq(data, sent, use_values) + + def streamSendPresence(self, data, use_values=True, timeout=.5): + if isinstance(data, str): + data = self.Presence(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkPresence(data, sent, use_values) + + def streamClose(self): + if self.xmpp is not None: + self.xmpp.disconnect() + self.xmpp.socket.recvData(self.xmpp.stream_footer) + + # ------------------------------------------------------------------ + # XML Comparison and Cleanup + + def fix_namespaces(self, xml, ns): + """ + Assign a namespace to an element and any children that + don't have a namespace. + """ + if xml.tag.startswith('{'): + return + xml.tag = '{%s}%s' % (ns, xml.tag) + for child in xml.getchildren(): + self.fix_namespaces(child, ns) + def compare(self, xml1, xml2=None): """ Compare XML objects. @@ -137,7 +278,6 @@ class SleekTest(unittest.TestCase): # Step 1: Check tags if xml1.tag != xml2.tag: - print xml1.tag, xml2.tag return False # Step 2: Check attributes diff --git a/tests/test_stream.py b/tests/test_stream.py new file mode 100644 index 0000000..eb4aaa5 --- /dev/null +++ b/tests/test_stream.py @@ -0,0 +1,34 @@ +from sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 + + +class TestStreamTester(SleekTest): + """ + Test that we can simulate and test a stanza stream. + """ + + def setUp(self): + self.streamStart() + + def tearDown(self): + self.streamClose() + + def testEcho(self): + def echo(msg): + msg.reply('Thanks for sending: %(body)s' % msg).send() + + self.xmpp.add_event_handler('message', echo) + + self.streamRecv(""" + + Hi! + + """) + + self.streamSendMessage(""" + + Thanks for sending: Hi! + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester) From bae082f4370c37c4aca0afc303b2d98b22582a34 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Thu, 15 Jul 2010 11:53:35 -0700 Subject: [PATCH 22/99] fixed updateRoster and delRosterItem --- sleekxmpp/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index ccb4352..35df620 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -146,10 +146,17 @@ class ClientXMPP(basexmpp, XMLStream): def updateRoster(self, jid, name=None, subscription=None, groups=[]): """Add or change a roster item.""" iq = self.Iq().setValues({'type': 'set'}) - iq['roster'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} + iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} #self.send(iq, self.Iq().setValues({'id': iq['id']})) + return r = iq.send() return r['type'] == 'result' + + def delRosterItem(self, jid): + iq = self.Iq() + iq['type'] = 'set' + iq['roster']['items'] = {jid: {'subscription': 'remove'}} + return iq.send()['type'] == 'result' def getRoster(self): """Request the roster be sent.""" From 078c71ed3fd550812461795149f6ffca35397871 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Thu, 15 Jul 2010 14:25:10 -0700 Subject: [PATCH 23/99] accidental debugging return left in the code from last commit --- sleekxmpp/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 35df620..df0af09 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -148,7 +148,6 @@ class ClientXMPP(basexmpp, XMLStream): iq = self.Iq().setValues({'type': 'set'}) iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} #self.send(iq, self.Iq().setValues({'id': iq['id']})) - return r = iq.send() return r['type'] == 'result' From 797e92a6a3cf1534f6ecb0f30019b0135d0ffacb Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 04:12:54 -0400 Subject: [PATCH 24/99] Fixed error in updateRoster when the name keyword parameter is left out. The Roster stanza object builds item elements manually, and did not handle the case where the name attribute is set to None, which would crash SleekXMPP. --- sleekxmpp/stanza/roster.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 1fefc18..69027b6 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -23,7 +23,9 @@ class Roster(ElementBase): if 'subscription' in items[jid]: item.attrib['subscription'] = items[jid]['subscription'] if 'name' in items[jid]: - item.attrib['name'] = items[jid]['name'] + name = items[jid]['name'] + if name is not None: + item.attrib['name'] = name if 'groups' in items[jid]: for group in items[jid]['groups']: groupxml = ET.Element('{jabber:iq:roster}group') From e6bec8681e07ced607db1fbcbc5e356c3936f1d1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 04:22:31 -0400 Subject: [PATCH 25/99] Added implementation for XEP-0128 Service Discovery Extensions. Uses the alt_0004 plugin for jabber:x:data stanza objects. --- sleekxmpp/plugins/xep_0128.py | 51 +++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 sleekxmpp/plugins/xep_0128.py diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py new file mode 100644 index 0000000..3e660b1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0128.py @@ -0,0 +1,51 @@ +""" + 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.txt for copying permission. +""" + +import logging +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.iq import Iq +from . xep_0030 import DiscoInfo, DiscoItems +from . alt_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' + + self.xmpp.stanzaPlugin(DiscoInfo, Form) + self.xmpp.stanzaPlugin(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) From d5e42ac0e7282500583bf17f21eb2f944600ce76 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 13:58:53 -0400 Subject: [PATCH 26/99] Condensed all of the stanzaPlugin functions into a single registerStanzaPlugin function. Updated plugins and tests to use new function. --- sleekxmpp/basexmpp.py | 17 ++----- sleekxmpp/plugins/alt_0004.py | 8 ++-- sleekxmpp/plugins/gmail_notify.py | 8 ++-- sleekxmpp/plugins/stanza_pubsub.py | 73 ++++++++++++++---------------- sleekxmpp/plugins/xep_0030.py | 6 +-- sleekxmpp/plugins/xep_0033.py | 4 +- sleekxmpp/plugins/xep_0045.py | 4 +- sleekxmpp/plugins/xep_0060.py | 2 +- sleekxmpp/plugins/xep_0085.py | 12 ++--- sleekxmpp/plugins/xep_0128.py | 6 +-- sleekxmpp/stanza/atom.py | 2 +- sleekxmpp/stanza/error.py | 2 +- sleekxmpp/stanza/htmlim.py | 2 +- sleekxmpp/stanza/nick.py | 2 +- sleekxmpp/stanza/roster.py | 2 +- sleekxmpp/xmlstream/stanzabase.py | 12 ++++- tests/sleektest.py | 10 +--- tests/test_addresses.py | 2 +- tests/test_chatstates.py | 10 ++-- tests/test_disco.py | 4 +- tests/test_forms.py | 6 +-- tests/test_gmail.py | 6 +-- tests/test_messagestanzas.py | 4 +- 23 files changed, 98 insertions(+), 106 deletions(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 9728c3f..c9439ea 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -16,6 +16,7 @@ from . xmlstream.handler.xmlcallback import XMLCallback from . xmlstream.handler.xmlwaiter import XMLWaiter from . xmlstream.handler.waiter import Waiter from . xmlstream.handler.callback import Callback +from . xmlstream.stanzabase import registerStanzaPlugin from . import plugins from . stanza.message import Message from . stanza.iq import Iq @@ -35,12 +36,6 @@ if sys.version_info < (3,0): reload(sys) sys.setdefaultencoding('utf8') - -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin - - class basexmpp(object): def __init__(self): self.id = 0 @@ -62,13 +57,9 @@ class basexmpp(object): self.registerStanza(Message) self.registerStanza(Iq) self.registerStanza(Presence) - self.stanzaPlugin(Iq, Roster) - self.stanzaPlugin(Message, Nick) - self.stanzaPlugin(Message, HTMLIM) - - def stanzaPlugin(self, stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin + registerStanzaPlugin(Iq, Roster) + registerStanzaPlugin(Message, Nick) + registerStanzaPlugin(Message, HTMLIM) def Message(self, *args, **kwargs): return Message(self, *args, **kwargs) diff --git a/sleekxmpp/plugins/alt_0004.py b/sleekxmpp/plugins/alt_0004.py index b38a491..ff9b7ef 100644 --- a/sleekxmpp/plugins/alt_0004.py +++ b/sleekxmpp/plugins/alt_0004.py @@ -11,7 +11,7 @@ import copy from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.message import Message @@ -318,9 +318,9 @@ class alt_0004(base.base_plugin): Form.namespace)), self.handle_form)) - self.xmpp.stanzaPlugin(FormField, FieldOption) - self.xmpp.stanzaPlugin(Form, FormField) - self.xmpp.stanzaPlugin(Message, Form) + registerStanzaPlugin(FormField, FieldOption) + registerStanzaPlugin(Form, FormField) + registerStanzaPlugin(Message, Form) def post_init(self): base.base_plugin.post_init(self) diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index acfc38b..fb1ecb3 100644 --- a/sleekxmpp/plugins/gmail_notify.py +++ b/sleekxmpp/plugins/gmail_notify.py @@ -10,7 +10,7 @@ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq @@ -109,9 +109,9 @@ class gmail_notify(base.base_plugin): NewMail.name)), self.handle_new_mail)) - self.xmpp.stanzaPlugin(Iq, GmailQuery) - self.xmpp.stanzaPlugin(Iq, MailBox) - self.xmpp.stanzaPlugin(Iq, NewMail) + registerStanzaPlugin(Iq, GmailQuery) + registerStanzaPlugin(Iq, MailBox) + registerStanzaPlugin(Iq, NewMail) self.last_result_time = None diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py index 1a1526f..e04f1a7 100644 --- a/sleekxmpp/plugins/stanza_pubsub.py +++ b/sleekxmpp/plugins/stanza_pubsub.py @@ -1,4 +1,4 @@ -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq from .. stanza.message import Message from .. basexmpp import basexmpp @@ -6,9 +6,6 @@ from .. xmlstream.xmlstream import XMLStream import logging from . import xep_0004 -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin class PubsubState(ElementBase): namespace = 'http://jabber.org/protocol/psstate' @@ -30,7 +27,7 @@ class PubsubState(ElementBase): for child in self.xml.getchildren(): self.xml.remove(child) -stanzaPlugin(Iq, PubsubState) +registerStanzaPlugin(Iq, PubsubState) class PubsubStateEvent(ElementBase): namespace = 'http://jabber.org/protocol/psstate#event' @@ -40,8 +37,8 @@ class PubsubStateEvent(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Message, PubsubStateEvent) -stanzaPlugin(PubsubStateEvent, PubsubState) +registerStanzaPlugin(Message, PubsubStateEvent) +registerStanzaPlugin(PubsubStateEvent, PubsubState) class Pubsub(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -51,7 +48,7 @@ class Pubsub(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Iq, Pubsub) +registerStanzaPlugin(Iq, Pubsub) class PubsubOwner(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -61,7 +58,7 @@ class PubsubOwner(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Iq, PubsubOwner) +registerStanzaPlugin(Iq, PubsubOwner) class Affiliation(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -86,7 +83,7 @@ class Affiliations(ElementBase): self.xml.append(affiliation.xml) return self.iterables.append(affiliation) -stanzaPlugin(Pubsub, Affiliations) +registerStanzaPlugin(Pubsub, Affiliations) class Subscription(ElementBase): @@ -103,7 +100,7 @@ class Subscription(ElementBase): def getjid(self): return jid(self._getattr('jid')) -stanzaPlugin(Pubsub, Subscription) +registerStanzaPlugin(Pubsub, Subscription) class Subscriptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -114,7 +111,7 @@ class Subscriptions(ElementBase): plugin_tag_map = {} subitem = (Subscription,) -stanzaPlugin(Pubsub, Subscriptions) +registerStanzaPlugin(Pubsub, Subscriptions) class OptionalSetting(object): interfaces = set(('required',)) @@ -147,7 +144,7 @@ class SubscribeOptions(ElementBase, OptionalSetting): plugin_tag_map = {} interfaces = set(('required',)) -stanzaPlugin(Subscription, SubscribeOptions) +registerStanzaPlugin(Subscription, SubscribeOptions) class Item(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -178,7 +175,7 @@ class Items(ElementBase): plugin_tag_map = {} subitem = (Item,) -stanzaPlugin(Pubsub, Items) +registerStanzaPlugin(Pubsub, Items) class Create(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -188,7 +185,7 @@ class Create(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Pubsub, Create) +registerStanzaPlugin(Pubsub, Create) #class Default(ElementBase): # namespace = 'http://jabber.org/protocol/pubsub' @@ -203,7 +200,7 @@ stanzaPlugin(Pubsub, Create) # if not t: t == 'leaf' # return t # -#stanzaPlugin(Pubsub, Default) +#registerStanzaPlugin(Pubsub, Default) class Publish(Items): namespace = 'http://jabber.org/protocol/pubsub' @@ -214,7 +211,7 @@ class Publish(Items): plugin_tag_map = {} subitem = (Item,) -stanzaPlugin(Pubsub, Publish) +registerStanzaPlugin(Pubsub, Publish) class Retract(Items): namespace = 'http://jabber.org/protocol/pubsub' @@ -224,7 +221,7 @@ class Retract(Items): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Pubsub, Retract) +registerStanzaPlugin(Pubsub, Retract) class Unsubscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -254,7 +251,7 @@ class Subscribe(ElementBase): def getJid(self): return JID(self._getAttr('jid')) -stanzaPlugin(Pubsub, Subscribe) +registerStanzaPlugin(Pubsub, Subscribe) class Configure(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -284,7 +281,7 @@ class Configure(ElementBase): config = self.xml.find('{jabber:x:data}x') self.xml.remove(config) -stanzaPlugin(Pubsub, Configure) +registerStanzaPlugin(Pubsub, Configure) class DefaultConfig(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -317,7 +314,7 @@ class DefaultConfig(ElementBase): if not t: t = 'leaf' return t -stanzaPlugin(PubsubOwner, DefaultConfig) +registerStanzaPlugin(PubsubOwner, DefaultConfig) class Options(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -351,8 +348,8 @@ class Options(ElementBase): def getJid(self): return JID(self._getAttr('jid')) -stanzaPlugin(Pubsub, Options) -stanzaPlugin(Subscribe, Options) +registerStanzaPlugin(Pubsub, Options) +registerStanzaPlugin(Subscribe, Options) class OwnerAffiliations(Affiliations): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -366,7 +363,7 @@ class OwnerAffiliations(Affiliations): self.xml.append(affiliation.xml) return self.affiliations.append(affiliation) -stanzaPlugin(PubsubOwner, OwnerAffiliations) +registerStanzaPlugin(PubsubOwner, OwnerAffiliations) class OwnerAffiliation(Affiliation): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -380,7 +377,7 @@ class OwnerConfigure(Configure): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(PubsubOwner, OwnerConfigure) +registerStanzaPlugin(PubsubOwner, OwnerConfigure) class OwnerDefault(OwnerConfigure): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -388,7 +385,7 @@ class OwnerDefault(OwnerConfigure): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(PubsubOwner, OwnerDefault) +registerStanzaPlugin(PubsubOwner, OwnerDefault) class OwnerDelete(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -398,7 +395,7 @@ class OwnerDelete(ElementBase, OptionalSetting): plugin_tag_map = {} interfaces = set(('node',)) -stanzaPlugin(PubsubOwner, OwnerDelete) +registerStanzaPlugin(PubsubOwner, OwnerDelete) class OwnerPurge(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -407,7 +404,7 @@ class OwnerPurge(ElementBase, OptionalSetting): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(PubsubOwner, OwnerPurge) +registerStanzaPlugin(PubsubOwner, OwnerPurge) class OwnerRedirect(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -423,7 +420,7 @@ class OwnerRedirect(ElementBase): def getJid(self): return JID(self._getAttr('jid')) -stanzaPlugin(OwnerDelete, OwnerRedirect) +registerStanzaPlugin(OwnerDelete, OwnerRedirect) class OwnerSubscriptions(Subscriptions): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -437,7 +434,7 @@ class OwnerSubscriptions(Subscriptions): self.xml.append(subscription.xml) return self.subscriptions.append(subscription) -stanzaPlugin(PubsubOwner, OwnerSubscriptions) +registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) class OwnerSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -461,7 +458,7 @@ class Event(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Message, Event) +registerStanzaPlugin(Message, Event) class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -501,7 +498,7 @@ class EventItems(ElementBase): plugin_tag_map = {} subitem = (EventItem, EventRetract) -stanzaPlugin(Event, EventItems) +registerStanzaPlugin(Event, EventItems) class EventCollection(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -511,7 +508,7 @@ class EventCollection(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Event, EventCollection) +registerStanzaPlugin(Event, EventCollection) class EventAssociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -521,7 +518,7 @@ class EventAssociate(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(EventCollection, EventAssociate) +registerStanzaPlugin(EventCollection, EventAssociate) class EventDisassociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -531,7 +528,7 @@ class EventDisassociate(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(EventCollection, EventDisassociate) +registerStanzaPlugin(EventCollection, EventDisassociate) class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -556,7 +553,7 @@ class EventConfiguration(ElementBase): config = self.xml.find('{jabber:x:data}x') self.xml.remove(config) -stanzaPlugin(Event, EventConfiguration) +registerStanzaPlugin(Event, EventConfiguration) class EventPurge(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -566,7 +563,7 @@ class EventPurge(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} -stanzaPlugin(Event, EventPurge) +registerStanzaPlugin(Event, EventPurge) class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' @@ -582,4 +579,4 @@ class EventSubscription(ElementBase): def getJid(self): return JID(self._getAttr('jid')) -stanzaPlugin(Event, EventSubscription) +registerStanzaPlugin(Event, EventSubscription) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 93e094f..1e04fe4 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -10,7 +10,7 @@ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq class DiscoInfo(ElementBase): @@ -204,8 +204,8 @@ class xep_0030(base.base_plugin): DiscoInfo.namespace)), self.handle_info_query)) - self.xmpp.stanzaPlugin(Iq, DiscoInfo) - self.xmpp.stanzaPlugin(Iq, DiscoItems) + 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) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index df8bb88..9af27e3 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -10,7 +10,7 @@ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.message import Message @@ -154,7 +154,7 @@ class xep_0030(base.base_plugin): self.xep = '0033' self.description = 'Extended Stanza Addressing' - self.xmpp.stanzaPlugin(Message, Addresses) + registerStanzaPlugin(Message, Addresses) def post_init(self): base.base_plugin.post_init(self) diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index 937c6f9..88ada19 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -21,7 +21,7 @@ from __future__ import with_statement from . import base import logging from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import ElementBase, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID from .. stanza.presence import Presence from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath @@ -125,7 +125,7 @@ class xep_0045(base.base_plugin): self.xep = '0045' self.description = 'Multi User Chat' # load MUC support in presence stanzas - self.xmpp.stanzaPlugin(Presence, MUCPresence) + registerStanzaPlugin(Presence, MUCPresence) self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_message)) diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/xep_0060.py index bff158a..a92a384 100644 --- a/sleekxmpp/plugins/xep_0060.py +++ b/sleekxmpp/plugins/xep_0060.py @@ -2,7 +2,7 @@ from __future__ import with_statement from . import base import logging #from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import ElementBase, ET +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET from . import stanza_pubsub class xep_0060(base.base_plugin): diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py index e24e9db..66940af 100644 --- a/sleekxmpp/plugins/xep_0085.py +++ b/sleekxmpp/plugins/xep_0085.py @@ -10,7 +10,7 @@ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.message import Message @@ -85,11 +85,11 @@ class xep_0085(base.base_plugin): handler[1])), self._handleChatState)) - self.xmpp.stanzaPlugin(Message, Active) - self.xmpp.stanzaPlugin(Message, Composing) - self.xmpp.stanzaPlugin(Message, Gone) - self.xmpp.stanzaPlugin(Message, Inactive) - self.xmpp.stanzaPlugin(Message, Paused) + registerStanzaPlugin(Message, Active) + registerStanzaPlugin(Message, Composing) + registerStanzaPlugin(Message, Gone) + registerStanzaPlugin(Message, Inactive) + registerStanzaPlugin(Message, Paused) def post_init(self): base.base_plugin.post_init(self) diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py index 3e660b1..7ba00bf 100644 --- a/sleekxmpp/plugins/xep_0128.py +++ b/sleekxmpp/plugins/xep_0128.py @@ -10,7 +10,7 @@ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq from . xep_0030 import DiscoInfo, DiscoItems from . alt_0004 import Form @@ -25,8 +25,8 @@ class xep_0128(base.base_plugin): self.xep = '0128' self.description = 'Service Discovery Extensions' - self.xmpp.stanzaPlugin(DiscoInfo, Form) - self.xmpp.stanzaPlugin(DiscoItems, Form) + registerStanzaPlugin(DiscoInfo, Form) + registerStanzaPlugin(DiscoItems, Form) def extend_info(self, node, data=None): if data is None: diff --git a/sleekxmpp/stanza/atom.py b/sleekxmpp/stanza/atom.py index 5e82cb9..9df85a2 100644 --- a/sleekxmpp/stanza/atom.py +++ b/sleekxmpp/stanza/atom.py @@ -1,4 +1,4 @@ -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from xml.etree import cElementTree as ET class AtomEntry(ElementBase): diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index ee46722..b9ab267 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -5,7 +5,7 @@ See the file license.txt for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET class Error(ElementBase): namespace = 'jabber:client' diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 60686e4..14595e2 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -5,7 +5,7 @@ See the file license.txt for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET class HTMLIM(ElementBase): namespace = 'http://jabber.org/protocol/xhtml-im' diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index ac7e360..ec29070 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -5,7 +5,7 @@ See the file license.txt for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET class Nick(ElementBase): namespace = 'http://jabber.org/nick/nick' diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 69027b6..708b8d4 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -5,7 +5,7 @@ See the file license.txt for copying permission. """ -from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID import logging class Roster(ElementBase): diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 024fe6c..7592e1f 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -19,6 +19,16 @@ else: xmltester = type(ET.Element('xml')) + +def registerStanzaPlugin(stanza, plugin): + """ + Associate a stanza object as a plugin for another stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin + + class JID(object): def __init__(self, jid): self.jid = jid @@ -392,4 +402,4 @@ class StanzaBase(ElementBase): def __copy__(self): return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) - + diff --git a/tests/sleektest.py b/tests/sleektest.py index eef3b90..3c270eb 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -17,6 +17,8 @@ from sleekxmpp import ClientXMPP from sleekxmpp import Message, Iq from sleekxmpp.stanza.presence import Presence from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin + class TestSocket(object): @@ -88,14 +90,6 @@ class SleekTest(unittest.TestCase): methods for comparing message, iq, and presence stanzas. """ - def stanzaPlugin(self, stanza, plugin): - """ - Associate a stanza object as a plugin for another stanza. - """ - tag = "{%s}%s" % (plugin.namespace, plugin.name) - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map[tag] = plugin - # ------------------------------------------------------------------ # Shortcut methods for creating stanza objects diff --git a/tests/test_addresses.py b/tests/test_addresses.py index 2718bb1..63d1100 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -5,7 +5,7 @@ import sleekxmpp.plugins.xep_0033 as xep_0033 class TestAddresses(SleekTest): def setUp(self): - self.stanzaPlugin(Message, xep_0033.Addresses) + registerStanzaPlugin(Message, xep_0033.Addresses) def testAddAddress(self): """Testing adding extended stanza address.""" diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py index 1e585be..bcacb9e 100644 --- a/tests/test_chatstates.py +++ b/tests/test_chatstates.py @@ -4,11 +4,11 @@ import sleekxmpp.plugins.xep_0085 as xep_0085 class TestChatStates(SleekTest): def setUp(self): - self.stanzaPlugin(Message, xep_0085.Active) - self.stanzaPlugin(Message, xep_0085.Composing) - self.stanzaPlugin(Message, xep_0085.Gone) - self.stanzaPlugin(Message, xep_0085.Inactive) - self.stanzaPlugin(Message, xep_0085.Paused) + registerStanzaPlugin(Message, xep_0085.Active) + registerStanzaPlugin(Message, xep_0085.Composing) + registerStanzaPlugin(Message, xep_0085.Gone) + registerStanzaPlugin(Message, xep_0085.Inactive) + registerStanzaPlugin(Message, xep_0085.Paused) def testCreateChatState(self): """Testing creating chat states.""" diff --git a/tests/test_disco.py b/tests/test_disco.py index 6daad13..96a12e2 100644 --- a/tests/test_disco.py +++ b/tests/test_disco.py @@ -5,8 +5,8 @@ import sleekxmpp.plugins.xep_0030 as xep_0030 class TestDisco(SleekTest): def setUp(self): - self.stanzaPlugin(Iq, xep_0030.DiscoInfo) - self.stanzaPlugin(Iq, xep_0030.DiscoItems) + registerStanzaPlugin(Iq, xep_0030.DiscoInfo) + registerStanzaPlugin(Iq, xep_0030.DiscoItems) def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" diff --git a/tests/test_forms.py b/tests/test_forms.py index 981d887..1616024 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -5,9 +5,9 @@ import sleekxmpp.plugins.alt_0004 as xep_0004 class TestDataForms(SleekTest): def setUp(self): - self.stanzaPlugin(Message, xep_0004.Form) - self.stanzaPlugin(xep_0004.Form, xep_0004.FormField) - self.stanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) + registerStanzaPlugin(Message, xep_0004.Form) + registerStanzaPlugin(xep_0004.Form, xep_0004.FormField) + registerStanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) def testMultipleInstructions(self): """Testing using multiple instructions elements in a data form.""" diff --git a/tests/test_gmail.py b/tests/test_gmail.py index 199b76a..b2e70d2 100644 --- a/tests/test_gmail.py +++ b/tests/test_gmail.py @@ -5,9 +5,9 @@ import sleekxmpp.plugins.gmail_notify as gmail class TestGmail(SleekTest): def setUp(self): - self.stanzaPlugin(Iq, gmail.GmailQuery) - self.stanzaPlugin(Iq, gmail.MailBox) - self.stanzaPlugin(Iq, gmail.NewMail) + registerStanzaPlugin(Iq, gmail.GmailQuery) + registerStanzaPlugin(Iq, gmail.MailBox) + registerStanzaPlugin(Iq, gmail.NewMail) def testCreateQuery(self): """Testing querying Gmail for emails.""" diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index 08488ce..026a578 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -5,9 +5,9 @@ class testmessagestanzas(unittest.TestCase): def setUp(self): import sleekxmpp.stanza.message as m - from sleekxmpp.basexmpp import stanzaPlugin + from sleekxmpp.basexmpp import registerStanzaPlugin from sleekxmpp.stanza.htmlim import HTMLIM - stanzaPlugin(m.Message, HTMLIM) + registerStanzaPlugin(m.Message, HTMLIM) self.m = m def testGroupchatReplyRegression(self): From 16104b6e56892b65d91abe467b124ca1e86ccfcf Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 13:36:28 -0700 Subject: [PATCH 27/99] made Lance's new XEP-4 stanzas the default, and put xep-0004 as old_0004 --- sleekxmpp/plugins/alt_0004.py | 330 ---------------- sleekxmpp/plugins/old_0004.py | 427 ++++++++++++++++++++ sleekxmpp/plugins/xep_0004.py | 723 +++++++++++++++------------------- 3 files changed, 740 insertions(+), 740 deletions(-) delete mode 100644 sleekxmpp/plugins/alt_0004.py create mode 100644 sleekxmpp/plugins/old_0004.py diff --git a/sleekxmpp/plugins/alt_0004.py b/sleekxmpp/plugins/alt_0004.py deleted file mode 100644 index ff9b7ef..0000000 --- a/sleekxmpp/plugins/alt_0004.py +++ /dev/null @@ -1,330 +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.txt for copying permission. -""" - -import logging -import copy -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.message import Message - - -class Form(ElementBase): - namespace = 'jabber:x:data' - name = 'x' - plugin_attrib = 'form' - interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) - sub_interfaces = set(('title',)) - form_types = set(('cancel', 'form', 'result', 'submit')) - - def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): - field = FormField(parent=self) - field['var'] = var - field['type'] = ftype - field['label'] = label - field['desc'] = desc - field['required'] = required - field['value'] = value - if options is not None: - field['options'] = options - return field - - def addItem(self, values): - itemXML = ET.Element('{%s}item' % self.namespace) - self.xml.append(itemXML) - reported_vars = self['reported'].keys() - for var in reported_vars: - fieldXML = ET.Element('{%s}field' % FormField.namespace) - itemXML.append(fieldXML) - field = FormField(xml=fieldXML) - field['var'] = var - field['value'] = values.get(var, None) - - def addReported(self, var, ftype='text-single', label='', desc=''): - reported = self.xml.find('{%s}reported' % self.namespace) - if reported is None: - reported = ET.Element('{%s}reported' % self.namespace) - self.xml.append(reported) - fieldXML = ET.Element('{%s}field' % FormField.namespace) - reported.append(fieldXML) - field = FormField(xml=fieldXML) - field['var'] = var - field['type'] = ftype - field['label'] = label - field['desc'] = desc - return field - - def cancel(self): - self['type'] = 'cancel' - - def delFields(self): - fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) - for fieldXML in fieldsXML: - self.xml.remove(fieldXML) - - def delInstructions(self): - instsXML = self.xml.findall('{%s}instructions') - for instXML in instsXML: - self.xml.remove(instXML) - - def delItems(self): - itemsXML = self.xml.find('{%s}item' % self.namespace) - for itemXML in itemsXML: - self.xml.remove(itemXML) - - def delReported(self): - reportedXML = self.xml.find('{%s}reported' % self.namespace) - if reportedXML is not None: - self.xml.remove(reportedXML) - - def getFields(self): - fields = {} - fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) - for fieldXML in fieldsXML: - field = FormField(xml=fieldXML) - fields[field['var']] = field - return fields - - def getInstructions(self): - instructions = '' - instsXML = self.xml.findall('{%s}instructions') - for instXML in instsXML: - instructions += instXML.text - - def getItems(self): - items = [] - itemsXML = self.xml.findall('{%s}item' % self.namespace) - for itemXML in itemsXML: - item = {} - fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) - for fieldXML in fieldsXML: - field = FormField(xml=fieldXML) - item[field['var']] = field['value'] - items.append(item) - return items - - def getReported(self): - fields = {} - fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, - FormField.namespace)) - for fieldXML in fieldsXML: - field = FormField(xml=fieldXML) - fields[field['var']] = field - return fields - - def getValues(self): - values = {} - fields = self.getFields() - for var in fields: - values[var] = fields[var]['value'] - return values - - def reply(self): - if self['type'] == 'form': - self['type'] = 'submit' - elif self['type'] == 'submit': - self['type'] = 'result' - - def setFields(self, fields): - del self['fields'] - for var in fields: - field = fields[var] - - # Remap 'type' to 'ftype' to match the addField method - ftype = field.get('type', 'text-single') - field['type'] = ftype - del field['type'] - field['ftype'] = ftype - - self.addField(var, **field) - - def setInstructions(self, instructions): - instructions = instructions.split('\n') - for instruction in instructions: - inst = ET.Element('{%s}instructions' % self.namespace) - inst.text = instruction - self.xml.append(inst) - - def setItems(self, items): - for item in items: - self.addItem(item) - - def setReported(self, reported): - for var in reported: - field = reported[var] - - # Remap 'type' to 'ftype' to match the addReported method - ftype = field.get('type', 'text-single') - field['type'] = ftype - del field['type'] - field['ftype'] = ftype - - self.addReported(var, **field) - - def setValues(self, values): - fields = self.getFields() - for field in values: - fields[field]['value'] = values[field] - - -class FormField(ElementBase): - namespace = 'jabber:x:data' - name = 'field' - plugin_attrib = 'field' - interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) - sub_interfaces = set(('desc',)) - field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', - 'list-single', 'text-multi', 'text-private', 'text-single')) - multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) - multi_line_types = set(('hidden', 'text-multi')) - option_types = set(('list-multi', 'list-single')) - true_values = set((True, '1', 'true')) - - def addOption(self, label='', value=''): - if self['type'] in self.option_types: - opt = FieldOption(parent=self) - opt['label'] = label - opt['value'] = value - else: - raise ValueError("Cannot add options to a %s field." % self['type']) - - def delOptions(self): - optsXML = self.xml.findall('{%s}option' % self.namespace) - for optXML in optsXML: - self.xml.remove(optXML) - - def delRequired(self): - reqXML = self.xml.find('{%s}required' % self.namespace) - if reqXML is not None: - self.xml.remove(reqXML) - - def delValue(self): - valsXML = self.xml.findall('{%s}value' % self.namespace) - for valXML in valsXML: - self.xml.remove(valXML) - - def getAnswer(self): - return self.getValue() - - def getOptions(self): - options = [] - optsXML = self.xml.findall('{%s}option' % self.namespace) - for optXML in optsXML: - opt = FieldOption(xml=optXML) - options.append({'label': opt['label'], 'value':opt['value']}) - return options - - def getRequired(self): - reqXML = self.xml.find('{%s}required' % self.namespace) - return reqXML is not None - - def getValue(self): - valsXML = self.xml.findall('{%s}value' % self.namespace) - if len(valsXML) == 0: - return None - elif self['type'] == 'boolean': - return valsXML[0].text in self.true_values - elif self['type'] in self.multi_value_types: - values = [] - for valXML in valsXML: - if valXML.text is None: - valXML.text = '' - values.append(valXML.text) - if self['type'] == 'text-multi': - values = "\n".join(values) - return values - else: - return valsXML[0].text - - def setAnswer(self, answer): - self.setValue(answer) - - def setFalse(self): - self.setValue(False) - - def setOptions(self, options): - for value in options: - if isinstance(value, dict): - self.addOption(**value) - else: - self.addOption(value=value) - - def setRequired(self, required): - exists = self.getRequired() - if not exists and required: - self.xml.append(ET.Element('{%s}required' % self.namespace)) - elif exists and not required: - self.delRequired() - - def setTrue(self): - self.setValue(True) - - def setValue(self, value): - self.delValue() - valXMLName = '{%s}value' % self.namespace - - if self['type'] == 'boolean': - if value in self.true_values: - valXML = ET.Element(valXMLName) - valXML.text = 'true' - self.xml.append(valXML) - else: - valXML = ET.Element(valXMLName) - valXML.text = 'true' - self.xml.append(valXML) - if self['type'] in self.multi_value_types: - if self['type'] in self.multi_line_types and isinstance(value, str): - value = value.split('\n') - if not isinstance(value, list): - value = [value] - for val in value: - valXML = ET.Element(valXMLName) - valXML.text = val - self.xml.append(valXML) - else: - if isinstance(value, list): - raise ValueError("Cannot add multiple values to a %s field." % self['type']) - valXML = ET.Element(valXMLName) - valXML.text = value - self.xml.append(valXML) - - -class FieldOption(ElementBase): - namespace = 'jabber:x:data' - name = 'option' - plugin_attrib = 'option' - interfaces = set(('label', 'value')) - sub_interfaces = set(('value',)) - - -class alt_0004(base.base_plugin): - """ - XEP-0004: Data Forms - """ - - def plugin_init(self): - self.xep = '0004' - self.description = 'Data Forms' - - self.xmpp.registerHandler( - Callback('Data Form', - MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, - Form.namespace)), - self.handle_form)) - - registerStanzaPlugin(FormField, FieldOption) - registerStanzaPlugin(Form, FormField) - registerStanzaPlugin(Message, Form) - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - - def handle_form(self, message): - self.xmpp.event("message_xform", message) diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py new file mode 100644 index 0000000..353e722 --- /dev/null +++ b/sleekxmpp/plugins/old_0004.py @@ -0,0 +1,427 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2007 Nathanael C. Fritz + This file is part of SleekXMPP. + + SleekXMPP is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + SleekXMPP is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SleekXMPP; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +""" +from . import base +import logging +from xml.etree import cElementTree as ET +import copy +#TODO support item groups and results + +class old_0004(base.base_plugin): + + def plugin_init(self): + self.xep = '0004' + self.description = '*Deprecated Data Forms' + self.xmpp.add_handler("", self.handler_message_xform) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') + + def handler_message_xform(self, xml): + object = self.handle_form(xml) + self.xmpp.event("message_form", object) + + def handler_presence_xform(self, xml): + object = self.handle_form(xml) + self.xmpp.event("presence_form", object) + + def handle_form(self, xml): + xmlform = xml.find('{jabber:x:data}x') + object = self.buildForm(xmlform) + self.xmpp.event("message_xform", object) + return object + + def buildForm(self, xml): + form = Form(ftype=xml.attrib['type']) + form.fromXML(xml) + return form + + def makeForm(self, ftype='form', title='', instructions=''): + return Form(self.xmpp, ftype, title, instructions) + +class FieldContainer(object): + def __init__(self, stanza = 'form'): + self.fields = [] + self.field = {} + self.stanza = stanza + + def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): + self.field[var] = FormField(var, ftype, label, desc, required, value) + self.fields.append(self.field[var]) + return self.field[var] + + def buildField(self, xml): + self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) + self.fields.append(self.field[xml.get('var', '__unnamed__')]) + self.field[xml.get('var', '__unnamed__')].buildField(xml) + + def buildContainer(self, xml): + self.stanza = xml.tag + for field in xml.findall('{jabber:x:data}field'): + self.buildField(field) + + def getXML(self, ftype): + container = ET.Element(self.stanza) + for field in self.fields: + container.append(field.getXML(ftype)) + return container + +class Form(FieldContainer): + types = ('form', 'submit', 'cancel', 'result') + def __init__(self, xmpp=None, ftype='form', title='', instructions=''): + if not ftype in self.types: + raise ValueError("Invalid Form Type") + FieldContainer.__init__(self) + self.xmpp = xmpp + self.type = ftype + self.title = title + self.instructions = instructions + self.reported = [] + self.items = [] + + def merge(self, form2): + form1 = Form(ftype=self.type) + form1.fromXML(self.getXML(self.type)) + for field in form2.fields: + if not field.var in form1.field: + form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value) + else: + form1.field[field.var].value = field.value + for option, label in field.options: + if (option, label) not in form1.field[field.var].options: + form1.fields[field.var].addOption(option, label) + return form1 + + def copy(self): + newform = Form(ftype=self.type) + newform.fromXML(self.getXML(self.type)) + return newform + + def update(self, form): + values = form.getValues() + for var in values: + if var in self.fields: + self.fields[var].setValue(self.fields[var]) + + def getValues(self): + result = {} + for field in self.fields: + value = field.value + if len(value) == 1: + value = value[0] + result[field.var] = value + return result + + def setValues(self, values={}): + for field in values: + if field in self.field: + if isinstance(values[field], list) or isinstance(values[field], tuple): + for value in values[field]: + self.field[field].setValue(value) + else: + self.field[field].setValue(values[field]) + + def fromXML(self, xml): + self.buildForm(xml) + + def addItem(self): + newitem = FieldContainer('item') + self.items.append(newitem) + return newitem + + def buildItem(self, xml): + newitem = self.addItem() + newitem.buildContainer(xml) + + def addReported(self): + reported = FieldContainer('reported') + self.reported.append(reported) + return reported + + def buildReported(self, xml): + reported = self.addReported() + reported.buildContainer(xml) + + def setTitle(self, title): + self.title = title + + def setInstructions(self, instructions): + self.instructions = instructions + + def setType(self, ftype): + self.type = ftype + + def getXMLMessage(self, to): + msg = self.xmpp.makeMessage(to) + msg.append(self.getXML()) + return msg + + def buildForm(self, xml): + self.type = xml.get('type', 'form') + if xml.find('{jabber:x:data}title') is not None: + self.setTitle(xml.find('{jabber:x:data}title').text) + if xml.find('{jabber:x:data}instructions') is not None: + self.setInstructions(xml.find('{jabber:x:data}instructions').text) + for field in xml.findall('{jabber:x:data}field'): + self.buildField(field) + for reported in xml.findall('{jabber:x:data}reported'): + self.buildReported(reported) + for item in xml.findall('{jabber:x:data}item'): + self.buildItem(item) + + #def getXML(self, tostring = False): + def getXML(self, ftype=None): + if ftype: + self.type = ftype + form = ET.Element('{jabber:x:data}x') + form.attrib['type'] = self.type + if self.title and self.type in ('form', 'result'): + title = ET.Element('{jabber:x:data}title') + title.text = self.title + form.append(title) + if self.instructions and self.type == 'form': + instructions = ET.Element('{jabber:x:data}instructions') + instructions.text = self.instructions + form.append(instructions) + for field in self.fields: + form.append(field.getXML(self.type)) + for reported in self.reported: + form.append(reported.getXML('{jabber:x:data}reported')) + for item in self.items: + form.append(item.getXML(self.type)) + #if tostring: + # form = self.xmpp.tostring(form) + return form + + def getXHTML(self): + form = ET.Element('{http://www.w3.org/1999/xhtml}form') + if self.title: + title = ET.Element('h2') + title.text = self.title + form.append(title) + if self.instructions: + instructions = ET.Element('p') + instructions.text = self.instructions + form.append(instructions) + for field in self.fields: + form.append(field.getXHTML()) + for field in self.reported: + form.append(field.getXHTML()) + for field in self.items: + form.append(field.getXHTML()) + return form + + + def makeSubmit(self): + self.setType('submit') + +class FormField(object): + types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single') + listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single') + lbtypes = ('fixed', 'text-multi') + def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None): + if not ftype in self.types: + raise ValueError("Invalid Field Type") + self.type = ftype + self.var = var + self.label = label + self.desc = desc + self.options = [] + self.required = False + self.value = [] + if self.type in self.listtypes: + self.islist = True + else: + self.islist = False + if self.type in self.lbtypes: + self.islinebreak = True + else: + self.islinebreak = False + if value: + self.setValue(value) + + def addOption(self, value, label): + if self.islist: + self.options.append((value, label)) + else: + raise ValueError("Cannot add options to non-list type field.") + + def setTrue(self): + if self.type == 'boolean': + self.value = [True] + + def setFalse(self): + if self.type == 'boolean': + self.value = [False] + + def require(self): + self.required = True + + def setDescription(self, desc): + self.desc = desc + + def setValue(self, value): + if self.type == 'boolean': + if value in ('1', 1, True, 'true', 'True', 'yes'): + value = True + else: + value = False + if self.islinebreak and value is not None: + self.value += value.split('\n') + else: + if len(self.value) and (not self.islist or self.type == 'list-single'): + self.value = [value] + else: + self.value.append(value) + + def delValue(self, value): + if type(self.value) == type([]): + try: + idx = self.value.index(value) + if idx != -1: + self.value.pop(idx) + except ValueError: + pass + else: + self.value = '' + + def setAnswer(self, value): + self.setValue(value) + + def buildField(self, xml): + self.type = xml.get('type', 'text-single') + self.label = xml.get('label', '') + for option in xml.findall('{jabber:x:data}option'): + self.addOption(option.find('{jabber:x:data}value').text, option.get('label', '')) + for value in xml.findall('{jabber:x:data}value'): + self.setValue(value.text) + if xml.find('{jabber:x:data}required') is not None: + self.require() + if xml.find('{jabber:x:data}desc') is not None: + self.setDescription(xml.find('{jabber:x:data}desc').text) + + def getXML(self, ftype): + field = ET.Element('{jabber:x:data}field') + if ftype != 'result': + field.attrib['type'] = self.type + if self.type != 'fixed': + if self.var: + field.attrib['var'] = self.var + if self.label: + field.attrib['label'] = self.label + if ftype == 'form': + for option in self.options: + optionxml = ET.Element('{jabber:x:data}option') + optionxml.attrib['label'] = option[1] + optionval = ET.Element('{jabber:x:data}value') + optionval.text = option[0] + optionxml.append(optionval) + field.append(optionxml) + if self.required: + required = ET.Element('{jabber:x:data}required') + field.append(required) + if self.desc: + desc = ET.Element('{jabber:x:data}desc') + desc.text = self.desc + field.append(desc) + for value in self.value: + valuexml = ET.Element('{jabber:x:data}value') + if value is True or value is False: + if value: + valuexml.text = '1' + else: + valuexml.text = '0' + else: + valuexml.text = value + field.append(valuexml) + return field + + def getXHTML(self): + field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) + if self.label: + label = ET.Element('p') + label.text = "%s: " % self.label + else: + label = ET.Element('p') + label.text = "%s: " % self.var + field.append(label) + if self.type == 'boolean': + formf = ET.Element('input', {'type': 'checkbox', 'name': self.var}) + if len(self.value) and self.value[0] in (True, 'true', '1'): + formf.attrib['checked'] = 'checked' + elif self.type == 'fixed': + formf = ET.Element('p') + try: + formf.text = ', '.join(self.value) + except: + pass + field.append(formf) + formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) + try: + formf.text = ', '.join(self.value) + except: + pass + elif self.type == 'hidden': + formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) + try: + formf.text = ', '.join(self.value) + except: + pass + elif self.type in ('jid-multi', 'list-multi'): + formf = ET.Element('select', {'name': self.var}) + for option in self.options: + optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'}) + optf.text = option[1] + if option[1] in self.value: + optf.attrib['selected'] = 'selected' + formf.append(option) + elif self.type in ('jid-single', 'text-single'): + formf = ET.Element('input', {'type': 'text', 'name': self.var}) + try: + formf.attrib['value'] = ', '.join(self.value) + except: + pass + elif self.type == 'list-single': + formf = ET.Element('select', {'name': self.var}) + for option in self.options: + optf = ET.Element('option', {'value': option[0]}) + optf.text = option[1] + if not optf.text: + optf.text = option[0] + if option[1] in self.value: + optf.attrib['selected'] = 'selected' + formf.append(optf) + elif self.type == 'text-multi': + formf = ET.Element('textarea', {'name': self.var}) + try: + formf.text = ', '.join(self.value) + except: + pass + if not formf.text: + formf.text = ' ' + elif self.type == 'text-private': + formf = ET.Element('input', {'type': 'password', 'name': self.var}) + try: + formf.attrib['value'] = ', '.join(self.value) + except: + pass + label.append(formf) + return field + diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py index 015bd8b..e3d2b55 100644 --- a/sleekxmpp/plugins/xep_0004.py +++ b/sleekxmpp/plugins/xep_0004.py @@ -1,427 +1,330 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + See the file license.txt for copying permission. """ -from . import base + import logging -from xml.etree import cElementTree as ET import copy -#TODO support item groups and results +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.message import Message + + +class Form(ElementBase): + namespace = 'jabber:x:data' + name = 'x' + plugin_attrib = 'form' + interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) + sub_interfaces = set(('title',)) + form_types = set(('cancel', 'form', 'result', 'submit')) + + def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): + field = FormField(parent=self) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + field['required'] = required + field['value'] = value + if options is not None: + field['options'] = options + return field + + def addItem(self, values): + itemXML = ET.Element('{%s}item' % self.namespace) + self.xml.append(itemXML) + reported_vars = self['reported'].keys() + for var in reported_vars: + fieldXML = ET.Element('{%s}field' % FormField.namespace) + itemXML.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['value'] = values.get(var, None) + + def addReported(self, var, ftype='text-single', label='', desc=''): + reported = self.xml.find('{%s}reported' % self.namespace) + if reported is None: + reported = ET.Element('{%s}reported' % self.namespace) + self.xml.append(reported) + fieldXML = ET.Element('{%s}field' % FormField.namespace) + reported.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + return field + + def cancel(self): + self['type'] = 'cancel' + + def delFields(self): + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + self.xml.remove(fieldXML) + + def delInstructions(self): + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + self.xml.remove(instXML) + + def delItems(self): + itemsXML = self.xml.find('{%s}item' % self.namespace) + for itemXML in itemsXML: + self.xml.remove(itemXML) + + def delReported(self): + reportedXML = self.xml.find('{%s}reported' % self.namespace) + if reportedXML is not None: + self.xml.remove(reportedXML) + + def getFields(self): + fields = {} + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getInstructions(self): + instructions = '' + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + instructions += instXML.text + + def getItems(self): + items = [] + itemsXML = self.xml.findall('{%s}item' % self.namespace) + for itemXML in itemsXML: + item = {} + fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + item[field['var']] = field['value'] + items.append(item) + return items + + def getReported(self): + fields = {} + fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, + FormField.namespace)) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getValues(self): + values = {} + fields = self.getFields() + for var in fields: + values[var] = fields[var]['value'] + return values + + def reply(self): + if self['type'] == 'form': + self['type'] = 'submit' + elif self['type'] == 'submit': + self['type'] = 'result' + + def setFields(self, fields): + del self['fields'] + for var in fields: + field = fields[var] + + # Remap 'type' to 'ftype' to match the addField method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addField(var, **field) + + def setInstructions(self, instructions): + instructions = instructions.split('\n') + for instruction in instructions: + inst = ET.Element('{%s}instructions' % self.namespace) + inst.text = instruction + self.xml.append(inst) + + def setItems(self, items): + for item in items: + self.addItem(item) + + def setReported(self, reported): + for var in reported: + field = reported[var] + + # Remap 'type' to 'ftype' to match the addReported method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addReported(var, **field) + + def setValues(self, values): + fields = self.getFields() + for field in values: + fields[field]['value'] = values[field] + + +class FormField(ElementBase): + namespace = 'jabber:x:data' + name = 'field' + plugin_attrib = 'field' + interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) + sub_interfaces = set(('desc',)) + field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', + 'list-single', 'text-multi', 'text-private', 'text-single')) + multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) + multi_line_types = set(('hidden', 'text-multi')) + option_types = set(('list-multi', 'list-single')) + true_values = set((True, '1', 'true')) + + def addOption(self, label='', value=''): + if self['type'] in self.option_types: + opt = FieldOption(parent=self) + opt['label'] = label + opt['value'] = value + else: + raise ValueError("Cannot add options to a %s field." % self['type']) + + def delOptions(self): + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + self.xml.remove(optXML) + + def delRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + if reqXML is not None: + self.xml.remove(reqXML) + + def delValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + for valXML in valsXML: + self.xml.remove(valXML) + + def getAnswer(self): + return self.getValue() + + def getOptions(self): + options = [] + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + opt = FieldOption(xml=optXML) + options.append({'label': opt['label'], 'value':opt['value']}) + return options + + def getRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + return reqXML is not None + + def getValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + if len(valsXML) == 0: + return None + elif self['type'] == 'boolean': + return valsXML[0].text in self.true_values + elif self['type'] in self.multi_value_types: + values = [] + for valXML in valsXML: + if valXML.text is None: + valXML.text = '' + values.append(valXML.text) + if self['type'] == 'text-multi': + values = "\n".join(values) + return values + else: + return valsXML[0].text + + def setAnswer(self, answer): + self.setValue(answer) + + def setFalse(self): + self.setValue(False) + + def setOptions(self, options): + for value in options: + if isinstance(value, dict): + self.addOption(**value) + else: + self.addOption(value=value) + + def setRequired(self, required): + exists = self.getRequired() + if not exists and required: + self.xml.append(ET.Element('{%s}required' % self.namespace)) + elif exists and not required: + self.delRequired() + + def setTrue(self): + self.setValue(True) + + def setValue(self, value): + self.delValue() + valXMLName = '{%s}value' % self.namespace + + if self['type'] == 'boolean': + if value in self.true_values: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + else: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + if self['type'] in self.multi_value_types: + if self['type'] in self.multi_line_types and isinstance(value, str): + value = value.split('\n') + if not isinstance(value, list): + value = [value] + for val in value: + valXML = ET.Element(valXMLName) + valXML.text = val + self.xml.append(valXML) + else: + if isinstance(value, list): + raise ValueError("Cannot add multiple values to a %s field." % self['type']) + valXML = ET.Element(valXMLName) + valXML.text = value + self.xml.append(valXML) + + +class FieldOption(ElementBase): + namespace = 'jabber:x:data' + name = 'option' + plugin_attrib = 'option' + interfaces = set(('label', 'value')) + sub_interfaces = set(('value',)) + class xep_0004(base.base_plugin): - + """ + XEP-0004: Data Forms + """ + def plugin_init(self): self.xep = '0004' self.description = 'Data Forms' - self.xmpp.add_handler("", self.handler_message_xform) + + self.xmpp.registerHandler( + Callback('Data Form', + MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, + Form.namespace)), + self.handle_form)) + + registerStanzaPlugin(FormField, FieldOption) + registerStanzaPlugin(Form, FormField) + registerStanzaPlugin(Message, Form) def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - - def handler_message_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("message_form", object) - - def handler_presence_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("presence_form", object) - - def handle_form(self, xml): - xmlform = xml.find('{jabber:x:data}x') - object = self.buildForm(xmlform) - self.xmpp.event("message_xform", object) - return object - - def buildForm(self, xml): - form = Form(ftype=xml.attrib['type']) - form.fromXML(xml) - return form - def makeForm(self, ftype='form', title='', instructions=''): - return Form(self.xmpp, ftype, title, instructions) - -class FieldContainer(object): - def __init__(self, stanza = 'form'): - self.fields = [] - self.field = {} - self.stanza = stanza - - def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): - self.field[var] = FormField(var, ftype, label, desc, required, value) - self.fields.append(self.field[var]) - return self.field[var] - - def buildField(self, xml): - self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) - self.fields.append(self.field[xml.get('var', '__unnamed__')]) - self.field[xml.get('var', '__unnamed__')].buildField(xml) - - def buildContainer(self, xml): - self.stanza = xml.tag - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - - def getXML(self, ftype): - container = ET.Element(self.stanza) - for field in self.fields: - container.append(field.getXML(ftype)) - return container - -class Form(FieldContainer): - types = ('form', 'submit', 'cancel', 'result') - def __init__(self, xmpp=None, ftype='form', title='', instructions=''): - if not ftype in self.types: - raise ValueError("Invalid Form Type") - FieldContainer.__init__(self) - self.xmpp = xmpp - self.type = ftype - self.title = title - self.instructions = instructions - self.reported = [] - self.items = [] - - def merge(self, form2): - form1 = Form(ftype=self.type) - form1.fromXML(self.getXML(self.type)) - for field in form2.fields: - if not field.var in form1.field: - form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value) - else: - form1.field[field.var].value = field.value - for option, label in field.options: - if (option, label) not in form1.field[field.var].options: - form1.fields[field.var].addOption(option, label) - return form1 - - def copy(self): - newform = Form(ftype=self.type) - newform.fromXML(self.getXML(self.type)) - return newform - - def update(self, form): - values = form.getValues() - for var in values: - if var in self.fields: - self.fields[var].setValue(self.fields[var]) - - def getValues(self): - result = {} - for field in self.fields: - value = field.value - if len(value) == 1: - value = value[0] - result[field.var] = value - return result - - def setValues(self, values={}): - for field in values: - if field in self.field: - if isinstance(values[field], list) or isinstance(values[field], tuple): - for value in values[field]: - self.field[field].setValue(value) - else: - self.field[field].setValue(values[field]) - - def fromXML(self, xml): - self.buildForm(xml) - - def addItem(self): - newitem = FieldContainer('item') - self.items.append(newitem) - return newitem - - def buildItem(self, xml): - newitem = self.addItem() - newitem.buildContainer(xml) - - def addReported(self): - reported = FieldContainer('reported') - self.reported.append(reported) - return reported - - def buildReported(self, xml): - reported = self.addReported() - reported.buildContainer(xml) - - def setTitle(self, title): - self.title = title - - def setInstructions(self, instructions): - self.instructions = instructions - - def setType(self, ftype): - self.type = ftype - - def getXMLMessage(self, to): - msg = self.xmpp.makeMessage(to) - msg.append(self.getXML()) - return msg - - def buildForm(self, xml): - self.type = xml.get('type', 'form') - if xml.find('{jabber:x:data}title') is not None: - self.setTitle(xml.find('{jabber:x:data}title').text) - if xml.find('{jabber:x:data}instructions') is not None: - self.setInstructions(xml.find('{jabber:x:data}instructions').text) - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - for reported in xml.findall('{jabber:x:data}reported'): - self.buildReported(reported) - for item in xml.findall('{jabber:x:data}item'): - self.buildItem(item) - - #def getXML(self, tostring = False): - def getXML(self, ftype=None): - if ftype: - self.type = ftype - form = ET.Element('{jabber:x:data}x') - form.attrib['type'] = self.type - if self.title and self.type in ('form', 'result'): - title = ET.Element('{jabber:x:data}title') - title.text = self.title - form.append(title) - if self.instructions and self.type == 'form': - instructions = ET.Element('{jabber:x:data}instructions') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXML(self.type)) - for reported in self.reported: - form.append(reported.getXML('{jabber:x:data}reported')) - for item in self.items: - form.append(item.getXML(self.type)) - #if tostring: - # form = self.xmpp.tostring(form) - return form - - def getXHTML(self): - form = ET.Element('{http://www.w3.org/1999/xhtml}form') - if self.title: - title = ET.Element('h2') - title.text = self.title - form.append(title) - if self.instructions: - instructions = ET.Element('p') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXHTML()) - for field in self.reported: - form.append(field.getXHTML()) - for field in self.items: - form.append(field.getXHTML()) - return form - - - def makeSubmit(self): - self.setType('submit') - -class FormField(object): - types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single') - listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single') - lbtypes = ('fixed', 'text-multi') - def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None): - if not ftype in self.types: - raise ValueError("Invalid Field Type") - self.type = ftype - self.var = var - self.label = label - self.desc = desc - self.options = [] - self.required = False - self.value = [] - if self.type in self.listtypes: - self.islist = True - else: - self.islist = False - if self.type in self.lbtypes: - self.islinebreak = True - else: - self.islinebreak = False - if value: - self.setValue(value) - - def addOption(self, value, label): - if self.islist: - self.options.append((value, label)) - else: - raise ValueError("Cannot add options to non-list type field.") - - def setTrue(self): - if self.type == 'boolean': - self.value = [True] - - def setFalse(self): - if self.type == 'boolean': - self.value = [False] - - def require(self): - self.required = True - - def setDescription(self, desc): - self.desc = desc - - def setValue(self, value): - if self.type == 'boolean': - if value in ('1', 1, True, 'true', 'True', 'yes'): - value = True - else: - value = False - if self.islinebreak and value is not None: - self.value += value.split('\n') - else: - if len(self.value) and (not self.islist or self.type == 'list-single'): - self.value = [value] - else: - self.value.append(value) - - def delValue(self, value): - if type(self.value) == type([]): - try: - idx = self.value.index(value) - if idx != -1: - self.value.pop(idx) - except ValueError: - pass - else: - self.value = '' - - def setAnswer(self, value): - self.setValue(value) - - def buildField(self, xml): - self.type = xml.get('type', 'text-single') - self.label = xml.get('label', '') - for option in xml.findall('{jabber:x:data}option'): - self.addOption(option.find('{jabber:x:data}value').text, option.get('label', '')) - for value in xml.findall('{jabber:x:data}value'): - self.setValue(value.text) - if xml.find('{jabber:x:data}required') is not None: - self.require() - if xml.find('{jabber:x:data}desc') is not None: - self.setDescription(xml.find('{jabber:x:data}desc').text) - - def getXML(self, ftype): - field = ET.Element('{jabber:x:data}field') - if ftype != 'result': - field.attrib['type'] = self.type - if self.type != 'fixed': - if self.var: - field.attrib['var'] = self.var - if self.label: - field.attrib['label'] = self.label - if ftype == 'form': - for option in self.options: - optionxml = ET.Element('{jabber:x:data}option') - optionxml.attrib['label'] = option[1] - optionval = ET.Element('{jabber:x:data}value') - optionval.text = option[0] - optionxml.append(optionval) - field.append(optionxml) - if self.required: - required = ET.Element('{jabber:x:data}required') - field.append(required) - if self.desc: - desc = ET.Element('{jabber:x:data}desc') - desc.text = self.desc - field.append(desc) - for value in self.value: - valuexml = ET.Element('{jabber:x:data}value') - if value is True or value is False: - if value: - valuexml.text = '1' - else: - valuexml.text = '0' - else: - valuexml.text = value - field.append(valuexml) - return field - - def getXHTML(self): - field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) - if self.label: - label = ET.Element('p') - label.text = "%s: " % self.label - else: - label = ET.Element('p') - label.text = "%s: " % self.var - field.append(label) - if self.type == 'boolean': - formf = ET.Element('input', {'type': 'checkbox', 'name': self.var}) - if len(self.value) and self.value[0] in (True, 'true', '1'): - formf.attrib['checked'] = 'checked' - elif self.type == 'fixed': - formf = ET.Element('p') - try: - formf.text = ', '.join(self.value) - except: - pass - field.append(formf) - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type == 'hidden': - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type in ('jid-multi', 'list-multi'): - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'}) - optf.text = option[1] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(option) - elif self.type in ('jid-single', 'text-single'): - formf = ET.Element('input', {'type': 'text', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - elif self.type == 'list-single': - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0]}) - optf.text = option[1] - if not optf.text: - optf.text = option[0] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(optf) - elif self.type == 'text-multi': - formf = ET.Element('textarea', {'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - if not formf.text: - formf.text = ' ' - elif self.type == 'text-private': - formf = ET.Element('input', {'type': 'password', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - label.append(formf) - return field - + def handle_form(self, message): + self.xmpp.event("message_xform", message) From 130a148d3434b001e24a544022ee4df8597dbe96 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 13:53:41 -0700 Subject: [PATCH 28/99] added fromXML/getXML compatiblity to the new xep-0004 w/ deprecated warnings --- sleekxmpp/plugins/old_0004.py | 2 ++ sleekxmpp/plugins/xep_0004.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py index 353e722..263f8b2 100644 --- a/sleekxmpp/plugins/old_0004.py +++ b/sleekxmpp/plugins/old_0004.py @@ -21,6 +21,7 @@ from . import base import logging from xml.etree import cElementTree as ET import copy +import logging #TODO support item groups and results class old_0004(base.base_plugin): @@ -33,6 +34,7 @@ class old_0004(base.base_plugin): def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') + logging.warning("This implementation of XEP-0004 is deprecated.") def handler_message_xform(self, xml): object = self.handle_form(xml) diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py index e3d2b55..712e84a 100644 --- a/sleekxmpp/plugins/xep_0004.py +++ b/sleekxmpp/plugins/xep_0004.py @@ -35,6 +35,15 @@ class Form(ElementBase): field['options'] = options return field + def getXML(self): + logging.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py") + return self.xml + + def fromXML(self, xml): + logging.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py") + n = Form(xml=xml) + return n + def addItem(self, values): itemXML = ET.Element('{%s}item' % self.namespace) self.xml.append(itemXML) From f80b3285d49a2ca395369a98cb0f7cf1fda4e218 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 14:57:21 -0700 Subject: [PATCH 29/99] indent problem on stanzabase --- sleekxmpp/xmlstream/stanzabase.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 7592e1f..6508c0a 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -21,10 +21,10 @@ xmltester = type(ET.Element('xml')) def registerStanzaPlugin(stanza, plugin): - """ - Associate a stanza object as a plugin for another stanza. - """ - tag = "{%s}%s" % (plugin.namespace, plugin.name) + """ + Associate a stanza object as a plugin for another stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin stanza.plugin_tag_map[tag] = plugin From fec8578cf61696d8ca85a6fe85a55be71d7109fd Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 15:38:48 -0700 Subject: [PATCH 30/99] stanza should not have setValues/getValues because that conflicts with attribute accessors --- sleekxmpp/__init__.py | 6 +++--- sleekxmpp/basexmpp.py | 12 ++++++------ sleekxmpp/plugins/xep_0045.py | 2 +- sleekxmpp/xmlstream/stanzabase.py | 14 +++++++------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index df0af09..3d659a8 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -145,7 +145,7 @@ class ClientXMPP(basexmpp, XMLStream): def updateRoster(self, jid, name=None, subscription=None, groups=[]): """Add or change a roster item.""" - iq = self.Iq().setValues({'type': 'set'}) + iq = self.Iq().setStanzaValues({'type': 'set'}) iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} #self.send(iq, self.Iq().setValues({'id': iq['id']})) r = iq.send() @@ -159,7 +159,7 @@ class ClientXMPP(basexmpp, XMLStream): def getRoster(self): """Request the roster be sent.""" - iq = self.Iq().setValues({'type': 'get'}).enable('roster').send() + iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send() self._handleRoster(iq, request=True) def _handleStreamFeatures(self, features): @@ -254,5 +254,5 @@ class ClientXMPP(basexmpp, XMLStream): self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True} self.roster[jid].update(iq['roster']['items'][jid]) if iq['type'] == 'set': - self.send(self.Iq().setValues({'type': 'result', 'id': iq['id']}).enable('roster')) + self.send(self.Iq().setStanzaValues({'type': 'result', 'id': iq['id']}).enable('roster')) self.event("roster_update", iq) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index c9439ea..12dc2a1 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -144,26 +144,26 @@ class basexmpp(object): return waitfor.wait(timeout) def makeIq(self, id=0, ifrom=None): - return self.Iq().setValues({'id': str(id), 'from': ifrom}) + return self.Iq().setStanzaValues({'id': str(id), 'from': ifrom}) def makeIqGet(self, queryxmlns = None): - iq = self.Iq().setValues({'type': 'get'}) + iq = self.Iq().setStanzaValues({'type': 'get'}) if queryxmlns: iq.append(ET.Element("{%s}query" % queryxmlns)) return iq def makeIqResult(self, id): - return self.Iq().setValues({'id': id, 'type': 'result'}) + return self.Iq().setStanzaValues({'id': id, 'type': 'result'}) def makeIqSet(self, sub=None): - iq = self.Iq().setValues({'type': 'set'}) + iq = self.Iq().setStanzaValues({'type': 'set'}) if sub != None: iq.append(sub) return iq def makeIqError(self, id, type='cancel', condition='feature-not-implemented', text=None): - iq = self.Iq().setValues({'id': id}) - iq['error'].setValues({'type': type, 'condition': condition, 'text': text}) + iq = self.Iq().setStanzaValues({'id': id}) + iq['error'].setStanzaValues({'type': type, 'condition': condition, 'text': text}) return iq def makeIqQuery(self, iq, xmlns): diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index 88ada19..cc676a6 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -134,7 +134,7 @@ class xep_0045(base.base_plugin): """ if pr['muc']['room'] not in self.rooms.keys(): return - entry = pr['muc'].getValues() + entry = pr['muc'].getStanzaValues() if pr['type'] == 'unavailable': del self.rooms[entry['room']][entry['nick']] else: diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 6508c0a..4e6afee 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -241,7 +241,7 @@ class ElementBase(tostring.ToString): def __eq__(self, other): if not isinstance(other, ElementBase): return False - values = self.getValues() + values = self.getStanzaValues() for key in other: if key not in values or values[key] != other[key]: return False @@ -283,21 +283,21 @@ class ElementBase(tostring.ToString): if child.tag == "{%s}%s" % (self.namespace, name): self.xml.remove(child) - def getValues(self): + def getStanzaValues(self): out = {} for interface in self.interfaces: out[interface] = self[interface] for pluginkey in self.plugins: - out[pluginkey] = self.plugins[pluginkey].getValues() + out[pluginkey] = self.plugins[pluginkey].getStanzaValues() if self.iterables: iterables = [] for stanza in self.iterables: - iterables.append(stanza.getValues()) + iterables.append(stanza.getStanzaValues()) iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) out['substanzas'] = iterables return out - def setValues(self, attrib): + def setStanzaValues(self, attrib): for interface in attrib: if interface == 'substanzas': for subdict in attrib['substanzas']: @@ -305,7 +305,7 @@ class ElementBase(tostring.ToString): for subclass in self.subitem: if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): sub = subclass(parent=self) - sub.setValues(subdict) + sub.setStanzaValues(subdict) self.iterables.append(sub) break elif interface in self.interfaces: @@ -313,7 +313,7 @@ class ElementBase(tostring.ToString): elif interface in self.plugin_attrib_map and interface not in self.plugins: self.initPlugin(interface) if interface in self.plugins: - self.plugins[interface].setValues(attrib[interface]) + self.plugins[interface].setStanzaValues(attrib[interface]) return self def appendxml(self, xml): From b5a14a0190f6ea45bfbc0e18a7ff6c61b6415865 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 19:19:33 -0400 Subject: [PATCH 31/99] Can now pass a name to add_handler so that the handler can be reliably removed later. Updated uses of add_handler to include a name. --- sleekxmpp/__init__.py | 6 +++--- sleekxmpp/basexmpp.py | 8 +++++--- sleekxmpp/plugins/old_0004.py | 2 +- sleekxmpp/plugins/xep_0009.py | 9 ++++++--- sleekxmpp/plugins/xep_0050.py | 10 +++++----- sleekxmpp/plugins/xep_0092.py | 2 +- sleekxmpp/plugins/xep_0199.py | 2 +- sleekxmpp/tests/testpubsub.py | 4 ++-- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 3d659a8..86b74fc 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -175,7 +175,7 @@ class ClientXMPP(basexmpp, XMLStream): def handler_starttls(self, xml): if not self.authenticated and self.ssl_support: - self.add_handler("", self.handler_tls_start, instream=True) + self.add_handler("", self.handler_tls_start, name='TLS Proceed', instream=True) self.sendXML(xml) return True else: @@ -191,8 +191,8 @@ class ClientXMPP(basexmpp, XMLStream): if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: return False logging.debug("Starting SASL Auth") - self.add_handler("", self.handler_auth_success, instream=True) - self.add_handler("", self.handler_auth_fail, instream=True) + self.add_handler("", self.handler_auth_success, name='SASL Sucess', instream=True) + self.add_handler("", self.handler_auth_fail, name='SASL Failure', instream=True) sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism') if len(sasl_mechs): for sasl_mech in sasl_mechs: diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 12dc2a1..8489b24 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -118,9 +118,11 @@ class basexmpp(object): self.id += 1 return self.getId() - def add_handler(self, mask, pointer, disposable=False, threaded=False, filter=False, instream=False): - #logging.warning("Deprecated add_handler used for %s: %s." % (mask, pointer)) - self.registerHandler(XMLCallback('add_handler_%s' % self.getNewId(), MatchXMLMask(mask), pointer, threaded, disposable, instream)) + def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): + # threaded is no longer needed, but leaving it for backwards compatibility for now + if name is None: + name = 'add_handler_%s' % self.getNewId() + self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream)) def getId(self): return "%x".upper() % self.id diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py index 263f8b2..2d4203a 100644 --- a/sleekxmpp/plugins/old_0004.py +++ b/sleekxmpp/plugins/old_0004.py @@ -29,7 +29,7 @@ class old_0004(base.base_plugin): def plugin_init(self): self.xep = '0004' self.description = '*Deprecated Data Forms' - self.xmpp.add_handler("", self.handler_message_xform) + self.xmpp.add_handler("", self.handler_message_xform, name='Old Message Form') def post_init(self): base.base_plugin.post_init(self) diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/xep_0009.py index 49ffac4..625b03f 100644 --- a/sleekxmpp/plugins/xep_0009.py +++ b/sleekxmpp/plugins/xep_0009.py @@ -178,9 +178,12 @@ class xep_0009(base.base_plugin): def plugin_init(self): self.xep = '0009' self.description = 'Jabber-RPC' - self.xmpp.add_handler("", self._callMethod) - self.xmpp.add_handler("", self._callResult) - self.xmpp.add_handler("", self._callError) + self.xmpp.add_handler("", + self._callMethod, name='Jabber RPC Call') + self.xmpp.add_handler("", + self._callResult, name='Jabber RPC Result') + self.xmpp.add_handler("", + self._callError, name='Jabber RPC Error') self.entries = {} self.activeCalls = [] diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py index 2f356e1..11a1772 100644 --- a/sleekxmpp/plugins/xep_0050.py +++ b/sleekxmpp/plugins/xep_0050.py @@ -32,11 +32,11 @@ class xep_0050(base.base_plugin): def plugin_init(self): self.xep = '0050' self.description = 'Ad-Hoc Commands' - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, threaded=True) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_complete) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete') self.commands = {} self.sessions = {} self.sd = self.xmpp.plugin['xep_0030'] diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py index aeebbe0..77a6d9d 100644 --- a/sleekxmpp/plugins/xep_0092.py +++ b/sleekxmpp/plugins/xep_0092.py @@ -30,7 +30,7 @@ class xep_0092(base.base_plugin): self.xep = "0092" self.name = self.config.get('name', 'SleekXMPP') self.version = self.config.get('version', '0.1-dev') - self.xmpp.add_handler("" % self.xmpp.default_ns, self.report_version) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.report_version, name='Sofware Version') def post_init(self): base.base_plugin.post_init(self) diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py index ccaf0b3..1da57fe 100644 --- a/sleekxmpp/plugins/xep_0199.py +++ b/sleekxmpp/plugins/xep_0199.py @@ -29,7 +29,7 @@ class xep_0199(base.base_plugin): def plugin_init(self): self.description = "XMPP Ping" self.xep = "0199" - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_ping) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping') self.running = False #if self.config.get('keepalive', True): #self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True) diff --git a/sleekxmpp/tests/testpubsub.py b/sleekxmpp/tests/testpubsub.py index ed9dd5c..d8f3ff4 100755 --- a/sleekxmpp/tests/testpubsub.py +++ b/sleekxmpp/tests/testpubsub.py @@ -34,9 +34,9 @@ class testps(sleekxmpp.ClientXMPP): self.registerPlugin('xep_0030') self.registerPlugin('xep_0060') self.registerPlugin('xep_0092') - self.add_handler("", self.pubsubEventHandler, threaded=True) + self.add_handler("", self.pubsubEventHandler, name='Pubsub Event', threaded=True) self.add_event_handler("session_start", self.start, threaded=True) - self.add_handler("", self.handleError) + self.add_handler("", self.handleError, name='Iq Error') self.events = Queue.Queue() self.default_config = None self.ps = self.plugin['xep_0060'] From f74baf1c23af79c6e732fffc7691cba97ee79715 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 16:25:01 -0700 Subject: [PATCH 32/99] updated sleektest to use new stanza get/set values api --- tests/sleektest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index 3c270eb..d7a9d0a 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -134,9 +134,9 @@ class SleekTest(unittest.TestCase): if xml.attrib.get('type', None) is None: xml.attrib['type'] = 'normal' - values = msg2.getValues() + values = msg2.getStanzaValues() msg3 = self.Message() - msg3.setValues(values) + msg3.setStanzaValues(values) debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) debug = "Three methods for creating stanza do not match:\n" + debug From 85ee30539d750740cd88c0ec85247815e7eea8bb Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 19 Jul 2010 16:26:25 -0700 Subject: [PATCH 33/99] more set/get Values changes --- tests/sleektest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index d7a9d0a..d5696d0 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -166,9 +166,9 @@ class SleekTest(unittest.TestCase): debug += "Constructed Stanza:\n%s\n" % ET.tostring(iq2.xml) if use_values: - values = iq.getValues() + values = iq.getStanzaValues() iq3 = self.Iq() - iq3.setValues(values) + iq3.setStanzaValues(values) debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(iq3.xml) debug = "Three methods for creating stanza do not match:\n" + debug From 278a8bb443e1fda262fec1cd3999137b7971d61b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 23:57:21 -0400 Subject: [PATCH 34/99] Removed outdated MANIFEST file. Setuptools will generate a new one when needed. --- MANIFEST | 39 --------------------------------------- 1 file changed, 39 deletions(-) delete mode 100644 MANIFEST diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 3a39d83..0000000 --- a/MANIFEST +++ /dev/null @@ -1,39 +0,0 @@ -setup.py -sleekxmpp/__init__.py -sleekxmpp/basexmpp.py -sleekxmpp/clientxmpp.py -sleekxmpp/example.py -sleekxmpp/plugins/__init__.py -sleekxmpp/plugins/base.py -sleekxmpp/plugins/gmail_notify.py -sleekxmpp/plugins/xep_0004.py -sleekxmpp/plugins/xep_0009.py -sleekxmpp/plugins/xep_0030.py -sleekxmpp/plugins/xep_0045.py -sleekxmpp/plugins/xep_0050.py -sleekxmpp/plugins/xep_0060.py -sleekxmpp/plugins/xep_0078.py -sleekxmpp/plugins/xep_0086.py -sleekxmpp/plugins/xep_0092.py -sleekxmpp/plugins/xep_0199.py -sleekxmpp/stanza/__init__.py -sleekxmpp/stanza/iq.py -sleekxmpp/stanza/message.py -sleekxmpp/stanza/presence.py -sleekxmpp/xmlstream/__init__.py -sleekxmpp/xmlstream/stanzabase.py -sleekxmpp/xmlstream/statemachine.py -sleekxmpp/xmlstream/test.py -sleekxmpp/xmlstream/testclient.py -sleekxmpp/xmlstream/xmlstream.py -sleekxmpp/xmlstream/handler/__init__.py -sleekxmpp/xmlstream/handler/base.py -sleekxmpp/xmlstream/handler/callback.py -sleekxmpp/xmlstream/handler/waiter.py -sleekxmpp/xmlstream/handler/xmlcallback.py -sleekxmpp/xmlstream/handler/xmlwaiter.py -sleekxmpp/xmlstream/matcher/__init__.py -sleekxmpp/xmlstream/matcher/base.py -sleekxmpp/xmlstream/matcher/many.py -sleekxmpp/xmlstream/matcher/xmlmask.py -sleekxmpp/xmlstream/matcher/xpath.py From 14f1c3ba512b17942e1925cc4e610d3558356ea7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 19 Jul 2010 23:58:33 -0400 Subject: [PATCH 35/99] Updated SleekTest to implement the checkPresence method. Also, removed unnecessary TestStream class and shortened timeout during stream connection. --- tests/sleektest.py | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index d5696d0..9f4198e 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -77,12 +77,6 @@ class TestSocket(object): except: return None -class TestStream(object): - """Dummy class to pass a stream object to created stanzas""" - - def __init__(self): - self.default_ns = 'jabber:client' - class SleekTest(unittest.TestCase): """ @@ -186,7 +180,34 @@ class SleekTest(unittest.TestCase): If use_values is False, the test using getValues() and setValues() will not be used. """ - pass + self.fix_namespaces(pres.xml, 'jabber:client') + debug = "Given Stanza:\n%s\n" % ET.tostring(pres.xml) + + xml = ET.fromstring(xml_string) + self.fix_namespaces(xml, 'jabber:client') + debug += "XML String:\n%s\n" % ET.tostring(xml) + + pres2 = self.Presence(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(pres2.xml) + + # Ugly, but 'priority' has a default value and need to make + # sure it is set + pres['priority'] = pres['priority'] + pres2['priority'] = pres2['priority'] + + if use_values: + values = pres.getStanzaValues() + pres3 = self.Presence() + pres3.setStanzaValues(values) + + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(pres3.xml) + debug = "Three methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, pres.xml, pres2.xml, pres3.xml]), + debug) + else: + debug = "Two methods for creating stanza do not match:\n" + debug + self.failUnless(self.compare([xml, pres.xml, pres2.xml]), debug) + # ------------------------------------------------------------------ # Methods for simulating stanza streams. @@ -208,7 +229,7 @@ class SleekTest(unittest.TestCase): self.xmpp.process(threaded=True) if skip: # Clear startup stanzas - self.xmpp.socket.nextSent(timeout=1) + self.xmpp.socket.nextSent(timeout=0.1) def streamRecv(self, data): data = str(data) From bb927c7e6ad75b190ab3aeea7fd71d8cd2118eed Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 00:04:34 -0400 Subject: [PATCH 36/99] Updated presence stanza to include a 'show' interface. Presence stanza tests updated accordingly. --- sleekxmpp/stanza/presence.py | 28 ++++++++----------- tests/test_presencestanzas.py | 52 +++++++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 31 deletions(-) diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index c66246c..0da7ffc 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -5,36 +5,34 @@ See the file license.txt for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET + from . error import Error from . rootstanza import RootStanza +from .. xmlstream.stanzabase import StanzaBase, ET + class Presence(RootStanza): - interfaces = set(('type', 'to', 'from', 'id', 'status', 'priority')) + interfaces = set(('type', 'to', 'from', 'id', 'show', 'status', 'priority')) types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed')) showtypes = set(('dnd', 'chat', 'xa', 'away')) - sub_interfaces = set(('status', 'priority')) + sub_interfaces = set(('show', 'status', 'priority')) name = 'presence' plugin_attrib = name namespace = 'jabber:client' - def getShowElement(self): - return self.xml.find("{%s}show" % self.namespace) + def setShow(self, show): + if show in self.showtypes: + self._setSubText('show', text=show) + return self def setType(self, value): - show = self.getShowElement() if value in self.types: - if show is not None: - self.xml.remove(show) + self['show'] = None if value == 'available': value = '' self._setAttr('type', value) elif value in self.showtypes: - if show is None: - show = ET.Element("{%s}show" % self.namespace) - self.xml.append(show) - show.text = value + self['show'] = value return self def setPriority(self, value): @@ -48,9 +46,7 @@ class Presence(RootStanza): def getType(self): out = self._getAttr('type') if not out: - show = self.getShowElement() - if show is not None: - out = show.text + out = self['show'] if not out or out is None: out = 'available' return out diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index 23eb911..02799c8 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -1,31 +1,53 @@ -import unittest +import sleekxmpp +from sleektest import * +from sleekxmpp.stanza.presence import Presence -class testpresencestanzas(unittest.TestCase): - - def setUp(self): - import sleekxmpp.stanza.presence as p - self.p = p +class TestPresenceStanzas(SleekTest): - def testPresenceShowRegression(self): - "Regression check presence['type'] = 'dnd' show value working" - p = self.p.Presence() + def testPresenceShowRegression(self): + """Regression check presence['type'] = 'dnd' show value working""" + p = self.Presence() p['type'] = 'dnd' - self.failUnless(str(p) == "dnd") - + self.checkPresence(p, """ + dnd + """) + + def testPresenceType(self): + """Test manipulating presence['type']""" + p = self.Presence() + p['type'] = 'available' + self.checkPresence(p, """ + + """) + self.failUnless(p['type'] == 'available', "Incorrect presence['type'] for type 'available'") + + for showtype in ['away', 'chat', 'dnd', 'xa']: + p['type'] = showtype + self.checkPresence(p, """ + %s + """ % showtype) + self.failUnless(p['type'] == showtype, "Incorrect presence['type'] for type '%s'" % showtype) + + p['type'] = None + self.checkPresence(p, """ + + """) + def testPresenceUnsolicitedOffline(self): - "Unsolicted offline presence does not spawn changed_status or update roster" - p = self.p.Presence() + """Unsolicted offline presence does not spawn changed_status or update roster""" + p = self.Presence() p['type'] = 'unavailable' p['from'] = 'bill@chadmore.com/gmail15af' - import sleekxmpp + c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handlechangedpresence(event): happened.append(True) c.add_event_handler("changed_status", handlechangedpresence) c._handlePresence(p) + self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence") self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence") -suite = unittest.TestLoader().loadTestsFromTestCase(testpresencestanzas) +suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) From 9ca4bba2def8cffe1c079ce98304f1fa89b95b75 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 00:34:24 -0400 Subject: [PATCH 37/99] Update XEP-0128 to use new xep_0004 --- sleekxmpp/plugins/xep_0128.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py index 7ba00bf..dfe5829 100644 --- a/sleekxmpp/plugins/xep_0128.py +++ b/sleekxmpp/plugins/xep_0128.py @@ -13,7 +13,7 @@ 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 . alt_0004 import Form +from . xep_0004 import Form class xep_0128(base.base_plugin): From f505e229d6af009ba926292e3947346614df89f9 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 01:55:44 -0400 Subject: [PATCH 38/99] Updated message stanza tests. --- tests/test_messagestanzas.py | 37 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index 026a578..c83b59a 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -1,18 +1,15 @@ -import unittest -from xml.etree import cElementTree as ET +from sleektest import * +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.htmlim import HTMLIM -class testmessagestanzas(unittest.TestCase): +class TestMessageStanzas(SleekTest): def setUp(self): - import sleekxmpp.stanza.message as m - from sleekxmpp.basexmpp import registerStanzaPlugin - from sleekxmpp.stanza.htmlim import HTMLIM - registerStanzaPlugin(m.Message, HTMLIM) - self.m = m - + registerStanzaPlugin(Message, HTMLIM) + def testGroupchatReplyRegression(self): "Regression groupchat reply should be to barejid" - msg = self.m.Message() + msg = self.Message() msg['to'] = 'me@myserver.tld' msg['from'] = 'room@someservice.someserver.tld/somenick' msg['type'] = 'groupchat' @@ -22,23 +19,27 @@ class testmessagestanzas(unittest.TestCase): def testAttribProperty(self): "Test attrib property returning self" - msg = self.m.Message() + msg = self.Message() msg.attrib.attrib.attrib['to'] = 'usr@server.tld' self.failUnless(str(msg['to']) == 'usr@server.tld') def testHTMLPlugin(self): "Test message/html/html stanza" - msgtxt = """this is the plaintext message

This is the htmlim message

""" - msg = self.m.Message() + msg = self.Message() msg['to'] = "fritzy@netflint.net/sleekxmpp" msg['body'] = "this is the plaintext message" msg['type'] = 'chat' p = ET.Element('{http://www.w3.org/1999/xhtml}p') p.text = "This is the htmlim message" msg['html']['html'] = p - msg2 = self.m.Message() - values = msg.getValues() - msg2.setValues(values) - self.failUnless(msgtxt == str(msg) == str(msg2)) + self.checkMessage(msg, """ + + this is the plaintext message + + +

This is the htmlim message

+ + +
""") -suite = unittest.TestLoader().loadTestsFromTestCase(testmessagestanzas) +suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) From 690eaf8d3c3856c6242612da22e6c6d323f193ed Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 11:19:49 -0400 Subject: [PATCH 39/99] Updated license notices to use the correct MIT format. Also corrected references to nonexistant license.txt to LICENSE. --- sleekxmpp/__init__.py | 2 +- sleekxmpp/basexmpp.py | 2 +- sleekxmpp/componentxmpp.py | 2 +- sleekxmpp/exceptions.py | 2 +- sleekxmpp/plugins/__init__.py | 18 +++------------- sleekxmpp/plugins/base.py | 24 +++++++--------------- sleekxmpp/plugins/gmail_notify.py | 2 +- sleekxmpp/plugins/old_0004.py | 22 +++++--------------- sleekxmpp/plugins/xep_0004.py | 2 +- sleekxmpp/plugins/xep_0030.py | 2 +- sleekxmpp/plugins/xep_0033.py | 2 +- sleekxmpp/plugins/xep_0045.py | 22 +++++--------------- sleekxmpp/plugins/xep_0050.py | 22 +++++--------------- sleekxmpp/plugins/xep_0078.py | 22 +++++--------------- sleekxmpp/plugins/xep_0085.py | 2 +- sleekxmpp/plugins/xep_0092.py | 22 +++++--------------- sleekxmpp/plugins/xep_0128.py | 2 +- sleekxmpp/plugins/xep_0199.py | 23 +++++---------------- sleekxmpp/stanza/__init__.py | 2 +- sleekxmpp/stanza/error.py | 2 +- sleekxmpp/stanza/htmlim.py | 2 +- sleekxmpp/stanza/iq.py | 2 +- sleekxmpp/stanza/message.py | 2 +- sleekxmpp/stanza/nick.py | 2 +- sleekxmpp/stanza/presence.py | 2 +- sleekxmpp/stanza/rootstanza.py | 2 +- sleekxmpp/stanza/roster.py | 2 +- sleekxmpp/tests/testpubsub.py | 18 ++++------------ sleekxmpp/xmlstream/filesocket.py | 2 +- sleekxmpp/xmlstream/handler/base.py | 2 +- sleekxmpp/xmlstream/handler/callback.py | 2 +- sleekxmpp/xmlstream/handler/waiter.py | 2 +- sleekxmpp/xmlstream/handler/xmlcallback.py | 2 +- sleekxmpp/xmlstream/handler/xmlwaiter.py | 2 +- sleekxmpp/xmlstream/matcher/base.py | 2 +- sleekxmpp/xmlstream/matcher/id.py | 2 +- sleekxmpp/xmlstream/matcher/many.py | 2 +- sleekxmpp/xmlstream/matcher/stanzapath.py | 2 +- sleekxmpp/xmlstream/matcher/xmlmask.py | 2 +- sleekxmpp/xmlstream/matcher/xpath.py | 2 +- sleekxmpp/xmlstream/stanzabase.py | 2 +- sleekxmpp/xmlstream/statemachine.py | 2 +- sleekxmpp/xmlstream/xmlstream.py | 2 +- tests/sleektest.py | 7 ++++++- 44 files changed, 84 insertions(+), 184 deletions(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 86b74fc..b680dda 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -5,7 +5,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import absolute_import, unicode_literals from . basexmpp import basexmpp diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 8489b24..bfbfd59 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import with_statement, unicode_literals diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index de12581..4d39ce8 100755 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -5,7 +5,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import absolute_import from . basexmpp import basexmpp diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index 5b761cf..bbbd69d 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. -See the file license.txt for copying permission. +See the file LICENSE for copying permission. """ class XMPPError(Exception): diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index fbc5e01..b51977b 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -1,20 +1,8 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz + Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060'] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 4223646..a5260b0 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -1,22 +1,12 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ + + class base_plugin(object): def __init__(self, xmpp, config): diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index fb1ecb3..7e44234 100644 --- a/sleekxmpp/plugins/gmail_notify.py +++ b/sleekxmpp/plugins/gmail_notify.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py index 2d4203a..651408a 100644 --- a/sleekxmpp/plugins/old_0004.py +++ b/sleekxmpp/plugins/old_0004.py @@ -1,21 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from . import base import logging diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py index 712e84a..50f2b5e 100644 --- a/sleekxmpp/plugins/xep_0004.py +++ b/sleekxmpp/plugins/xep_0004.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 1e04fe4..a9d8d6a 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index 9af27e3..ea0b10b 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index cc676a6..cd1a9a0 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -1,21 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from __future__ import with_statement from . import base diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py index 11a1772..319401b 100644 --- a/sleekxmpp/plugins/xep_0050.py +++ b/sleekxmpp/plugins/xep_0050.py @@ -1,21 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from __future__ import with_statement from . import base diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py index f873290..4b3ab82 100644 --- a/sleekxmpp/plugins/xep_0078.py +++ b/sleekxmpp/plugins/xep_0078.py @@ -1,21 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from __future__ import with_statement from xml.etree import cElementTree as ET diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py index 66940af..b7b5d6d 100644 --- a/sleekxmpp/plugins/xep_0085.py +++ b/sleekxmpp/plugins/xep_0085.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permissio + See the file LICENSE for copying permissio """ import logging diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py index 77a6d9d..ca02c4a 100644 --- a/sleekxmpp/plugins/xep_0092.py +++ b/sleekxmpp/plugins/xep_0092.py @@ -1,21 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2007 Nathanael C. Fritz - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from xml.etree import cElementTree as ET from . import base diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py index dfe5829..824977b 100644 --- a/sleekxmpp/plugins/xep_0128.py +++ b/sleekxmpp/plugins/xep_0128.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py index 1da57fe..3fc62f5 100644 --- a/sleekxmpp/plugins/xep_0199.py +++ b/sleekxmpp/plugins/xep_0199.py @@ -1,22 +1,9 @@ """ - SleekXMPP: The Sleek XMPP Library - XEP-0199 (Ping) support - Copyright (C) 2007 Kevin Smith - This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. """ from xml.etree import cElementTree as ET from . import base diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index c3d8a31..b8fca22 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -3,6 +3,6 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ __all__ = ['presence'] diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index b9ab267..7771c87 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 14595e2..50195b1 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index ded7515..daf05b4 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import StanzaBase from xml.etree import cElementTree as ET diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 3834180..75ecc23 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import StanzaBase from xml.etree import cElementTree as ET diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index ec29070..47d4620 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index 0da7ffc..ec68176 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . error import Error diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 3b4822d..e568b62 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import StanzaBase from xml.etree import cElementTree as ET diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 708b8d4..eda65ec 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID import logging diff --git a/sleekxmpp/tests/testpubsub.py b/sleekxmpp/tests/testpubsub.py index d8f3ff4..24855c9 100755 --- a/sleekxmpp/tests/testpubsub.py +++ b/sleekxmpp/tests/testpubsub.py @@ -1,19 +1,9 @@ """ + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + See the file LICENSE for copying permission. """ import logging diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index f60c5b8..07b395d 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from socket import _fileobject import socket diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 5d55f4e..720846d 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ class BaseHandler(object): diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 49cfa14..889b0aa 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base import logging diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index c85a0c4..7c4330a 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base try: diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py index 632c142..67879df 100644 --- a/sleekxmpp/xmlstream/handler/xmlcallback.py +++ b/sleekxmpp/xmlstream/handler/xmlcallback.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import threading from . callback import Callback diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py index 2344403..cf90751 100644 --- a/sleekxmpp/xmlstream/handler/xmlwaiter.py +++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . waiter import Waiter diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py index 8185bdc..51da094 100644 --- a/sleekxmpp/xmlstream/matcher/base.py +++ b/sleekxmpp/xmlstream/matcher/base.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ class MatcherBase(object): diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py index bb858fc..43972c2 100644 --- a/sleekxmpp/xmlstream/matcher/id.py +++ b/sleekxmpp/xmlstream/matcher/id.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py index cf860e6..ff0c4e4 100644 --- a/sleekxmpp/xmlstream/matcher/many.py +++ b/sleekxmpp/xmlstream/matcher/many.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py index bd091c0..e315445 100644 --- a/sleekxmpp/xmlstream/matcher/stanzapath.py +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index eba3e95..89fd642 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index f6d0424..7f3d20b 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from . import base from xml.etree import cElementTree diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 4e6afee..6436fc5 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from xml.etree import cElementTree as ET import logging diff --git a/sleekxmpp/xmlstream/statemachine.py b/sleekxmpp/xmlstream/statemachine.py index fb7d150..8a1aa22 100644 --- a/sleekxmpp/xmlstream/statemachine.py +++ b/sleekxmpp/xmlstream/statemachine.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import with_statement import threading diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 003ead1..cea204b 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ from __future__ import with_statement, unicode_literals diff --git a/tests/sleektest.py b/tests/sleektest.py index 9f4198e..f89966e 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -3,7 +3,7 @@ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. - See the file license.txt for copying permission. + See the file LICENSE for copying permission. """ import unittest @@ -127,6 +127,11 @@ class SleekTest(unittest.TestCase): msg['type'] = msg['type'] if xml.attrib.get('type', None) is None: xml.attrib['type'] = 'normal' + msg2['type'] = msg2['type'] + debug += ">>>>Given Stanza:\n%s\n" % ET.tostring(msg.xml) + debug += "XML String:\n%s\n" % ET.tostring(xml) + debug += ">>>>Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) + values = msg2.getStanzaValues() msg3 = self.Message() From 9724efa123c2727e7617236a0c55238e286f6b00 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 12:16:06 -0400 Subject: [PATCH 40/99] Please tab nanny. --- sleekxmpp/basexmpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index bfbfd59..2c2bb91 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -122,7 +122,7 @@ class basexmpp(object): # threaded is no longer needed, but leaving it for backwards compatibility for now if name is None: name = 'add_handler_%s' % self.getNewId() - self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream)) + self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream)) def getId(self): return "%x".upper() % self.id From de24e9ed458cea4bccb9962b69e5fb4271841b3d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 12:16:57 -0400 Subject: [PATCH 41/99] Lots of XEP-0004 bug fixes. Forms have default type of 'form' setFields now uses a list of tuples instead of a dictionary because ordering is important. getFields defaults to returning a list of tuples, but the use_dict parameter can change that --- sleekxmpp/plugins/xep_0004.py | 80 +++++++++++++++++++---------------- tests/test_forms.py | 71 +++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 59 deletions(-) diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py index 50f2b5e..037fc09 100644 --- a/sleekxmpp/plugins/xep_0004.py +++ b/sleekxmpp/plugins/xep_0004.py @@ -23,10 +23,18 @@ class Form(ElementBase): sub_interfaces = set(('title',)) form_types = set(('cancel', 'form', 'result', 'submit')) - def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): + def setup(self, xml=None): + if ElementBase.setup(self, xml): #if we had to generate xml + self['type'] = 'form' + + def addField(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs): + kwtype = kwargs.get('type', None) + if kwtype is None: + kwtype = ftype + field = FormField(parent=self) field['var'] = var - field['type'] = ftype + field['type'] = kwtype field['label'] = label field['desc'] = desc field['required'] = required @@ -55,7 +63,10 @@ class Form(ElementBase): field['var'] = var field['value'] = values.get(var, None) - def addReported(self, var, ftype='text-single', label='', desc=''): + def addReported(self, var, ftype=None, label='', desc='', **kwargs): + kwtype = kwargs.get('type', None) + if kwtype is None: + kwtype = ftype reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) @@ -64,7 +75,7 @@ class Form(ElementBase): reported.append(fieldXML) field = FormField(xml=fieldXML) field['var'] = var - field['type'] = ftype + field['type'] = kwtype field['label'] = label field['desc'] = desc return field @@ -92,19 +103,21 @@ class Form(ElementBase): if reportedXML is not None: self.xml.remove(reportedXML) - def getFields(self): - fields = {} - fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + def getFields(self, use_dict=False): + fields = {} if use_dict else [] + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) - fields[field['var']] = field + if use_dict: + fields[field['var']] = field + else: + fields.append((field['var'], field)) return fields def getInstructions(self): instructions = '' - instsXML = self.xml.findall('{%s}instructions') - for instXML in instsXML: - instructions += instXML.text + instsXML = self.xml.findall('{%s}instructions' % self.namespace) + return "\n".join([instXML.text for instXML in instsXML]) def getItems(self): items = [] @@ -129,7 +142,7 @@ class Form(ElementBase): def getValues(self): values = {} - fields = self.getFields() + fields = self.getFields(use_dict=True) for var in fields: values[var] = fields[var]['value'] return values @@ -140,20 +153,19 @@ class Form(ElementBase): elif self['type'] == 'submit': self['type'] = 'result' - def setFields(self, fields): + def setFields(self, fields, default=None): del self['fields'] - for var in fields: - field = fields[var] + for field_data in fields: + var = field_data[0] + field = field_data[1] + field['var'] = var - # Remap 'type' to 'ftype' to match the addField method - ftype = field.get('type', 'text-single') - field['type'] = ftype - del field['type'] - field['ftype'] = ftype - - self.addField(var, **field) + self.addField(**field) def setInstructions(self, instructions): + del self['instructions'] + if instructions in [None, '']: + return instructions = instructions.split('\n') for instruction in instructions: inst = ET.Element('{%s}instructions' % self.namespace) @@ -164,20 +176,14 @@ class Form(ElementBase): for item in items: self.addItem(item) - def setReported(self, reported): + def setReported(self, reported, default=None): for var in reported: field = reported[var] - - # Remap 'type' to 'ftype' to match the addReported method - ftype = field.get('type', 'text-single') - field['type'] = ftype - del field['type'] - field['ftype'] = ftype - + field['var'] = var self.addReported(var, **field) def setValues(self, values): - fields = self.getFields() + fields = self.getFields(use_dict=True) for field in values: fields[field]['value'] = values[field] @@ -226,7 +232,7 @@ class FormField(ElementBase): optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: opt = FieldOption(xml=optXML) - options.append({'label': opt['label'], 'value':opt['value']}) + options.append({'label': opt['label'], 'value':opt['value']}) return options def getRequired(self): @@ -277,22 +283,24 @@ class FormField(ElementBase): def setValue(self, value): self.delValue() valXMLName = '{%s}value' % self.namespace - + if self['type'] == 'boolean': if value in self.true_values: valXML = ET.Element(valXMLName) - valXML.text = 'true' + valXML.text = '1' self.xml.append(valXML) else: valXML = ET.Element(valXMLName) - valXML.text = 'true' + valXML.text = '0' self.xml.append(valXML) - if self['type'] in self.multi_value_types: + elif self['type'] in self.multi_value_types or self['type'] in ['', None]: if self['type'] in self.multi_line_types and isinstance(value, str): value = value.split('\n') if not isinstance(value, list): value = [value] for val in value: + if self['type'] in ['', None] and val in self.true_values: + val = '1' valXML = ET.Element(valXMLName) valXML.text = val self.xml.append(valXML) diff --git a/tests/test_forms.py b/tests/test_forms.py index 1616024..7d37506 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,5 +1,5 @@ from sleektest import * -import sleekxmpp.plugins.alt_0004 as xep_0004 +import sleekxmpp.plugins.xep_0004 as xep_0004 class TestDataForms(SleekTest): @@ -16,19 +16,19 @@ class TestDataForms(SleekTest): self.checkMessage(msg, """ - + Instructions Second batch - """, use_values=False) + """) def testAddField(self): """Testing adding fields to a data form.""" msg = self.Message() form = msg['form'] - form.addField('f1', + form.addField(var='f1', ftype='text-single', label='Text', desc='A text field', @@ -37,7 +37,7 @@ class TestDataForms(SleekTest): self.checkMessage(msg, """ - + A text field @@ -45,26 +45,26 @@ class TestDataForms(SleekTest): - """, use_values=False) + """) - form['fields'] = {'f1': {'type': 'text-single', - 'label': 'Username', - 'required': True}, - 'f2': {'type': 'text-private', - 'label': 'Password', - 'required': True}, - 'f3': {'type': 'text-multi', - 'label': 'Message', - 'value': 'Enter message.\nA long one even.'}, - 'f4': {'type': 'list-single', - 'label': 'Message Type', - 'options': [{'label': 'Cool!', - 'value': 'cool'}, - {'label': 'Urgh!', - 'value': 'urgh'}]}} + form['fields'] = [('f1', {'type': 'text-single', + 'label': 'Username', + 'required': True}), + ('f2', {'type': 'text-private', + 'label': 'Password', + 'required': True}), + ('f3', {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'}), + ('f4', {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]})] self.checkMessage(msg, """ - + @@ -85,6 +85,31 @@ class TestDataForms(SleekTest): - """, use_values=False) + """) + + def testSetValues(self): + """Testing setting form values""" + + msg = self.Message() + form = msg['form'] + form.setFields([ + ('foo', {'type': 'text-single'}), + ('bar', {'type': 'list-multi'})]) + + form.setValues({'foo': 'Foo!', + 'bar': ['a', 'b']}) + + self.checkMessage(msg, """ + + + + Foo! + + + a + b + + + """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) From 7ad014368709873ed8ab760acc62264278f7723f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 12:18:38 -0400 Subject: [PATCH 42/99] Updated pubsub stanzas to use xep_0004 stanza objects, and updated tests to match. --- sleekxmpp/plugins/stanza_pubsub.py | 50 +- tests/test_pubsubstanzas.py | 802 ++++++++++++++++++----------- 2 files changed, 503 insertions(+), 349 deletions(-) diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py index e04f1a7..96d02f9 100644 --- a/sleekxmpp/plugins/stanza_pubsub.py +++ b/sleekxmpp/plugins/stanza_pubsub.py @@ -257,7 +257,7 @@ class Configure(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'configure' plugin_attrib = name - interfaces = set(('node', 'type', 'config')) + interfaces = set(('node', 'type')) plugin_attrib_map = {} plugin_tag_map = {} @@ -266,22 +266,8 @@ class Configure(ElementBase): if not t: t == 'leaf' return t - def getConfig(self): - config = self.xml.find('{jabber:x:data}x') - form = xep_0004.Form() - if config is not None: - form.fromXML(config) - return form - - def setConfig(self, value): - self.xml.append(value.getXML()) - return self - - def delConfig(self): - config = self.xml.find('{jabber:x:data}x') - self.xml.remove(config) - registerStanzaPlugin(Pubsub, Configure) +registerStanzaPlugin(Configure, xep_0004.Form) class DefaultConfig(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' @@ -293,21 +279,6 @@ class DefaultConfig(ElementBase): def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) - - def getConfig(self): - config = self.xml.find('{jabber:x:data}x') - form = xep_0004.Form() - if config is not None: - form.fromXML(config) - return form - - def setConfig(self, value): - self.xml.append(value.getXML()) - return self - - def delConfig(self): - config = self.xml.find('{jabber:x:data}x') - self.xml.remove(config) def getType(self): t = self._getAttr('type') @@ -315,6 +286,7 @@ class DefaultConfig(ElementBase): return t registerStanzaPlugin(PubsubOwner, DefaultConfig) +registerStanzaPlugin(DefaultConfig, xep_0004.Form) class Options(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' @@ -538,22 +510,8 @@ class EventConfiguration(ElementBase): plugin_attrib_map = {} plugin_tag_map = {} - def getConfig(self): - config = self.xml.find('{jabber:x:data}x') - form = xep_0004.Form() - if config is not None: - form.fromXML(config) - return form - - def setConfig(self, value): - self.xml.append(value.getXML()) - return self - - def delConfig(self): - config = self.xml.find('{jabber:x:data}x') - self.xml.remove(config) - registerStanzaPlugin(Event, EventConfiguration) +registerStanzaPlugin(EventConfiguration, xep_0004.Form) class EventPurge(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' diff --git a/tests/test_pubsubstanzas.py b/tests/test_pubsubstanzas.py index 089ee18..794fa03 100644 --- a/tests/test_pubsubstanzas.py +++ b/tests/test_pubsubstanzas.py @@ -1,315 +1,511 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from sleektest import * +import sleekxmpp.plugins.xep_0004 as xep_0004 +import sleekxmpp.plugins.stanza_pubsub as pubsub -class testpubsubstanzas(unittest.TestCase): - def setUp(self): - import sleekxmpp.plugins.stanza_pubsub as ps - self.ps = ps +class TestPubsubStanzas(SleekTest): - def testAffiliations(self): - "Testing iq/pubsub/affiliations/affiliation stanzas" - iq = self.ps.Iq() - aff1 = self.ps.Affiliation() - aff1['node'] = 'testnode' - aff1['affiliation'] = 'owner' - aff2 = self.ps.Affiliation() - aff2['node'] = 'testnode2' - aff2['affiliation'] = 'publisher' - iq['pubsub']['affiliations'].append(aff1) - iq['pubsub']['affiliations'].append(aff2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), "3 methods for creating stanza don't match") - self.failUnless(iq.match('iq@id=0/pubsub/affiliations/affiliation@node=testnode2@affiliation=publisher'), 'Match path failed') + def testAffiliations(self): + "Testing iq/pubsub/affiliations/affiliation stanzas" + iq = self.Iq() + aff1 = pubsub.Affiliation() + aff1['node'] = 'testnode' + aff1['affiliation'] = 'owner' + aff2 = pubsub.Affiliation() + aff2['node'] = 'testnode2' + aff2['affiliation'] = 'publisher' + iq['pubsub']['affiliations'].append(aff1) + iq['pubsub']['affiliations'].append(aff2) + self.checkIq(iq, """ + + + + + + + + """) - def testSubscriptions(self): - "Testing iq/pubsub/subscriptions/subscription stanzas" - iq = self.ps.Iq() - sub1 = self.ps.Subscription() - sub1['node'] = 'testnode' - sub1['jid'] = 'steve@myserver.tld/someresource' - sub2 = self.ps.Subscription() - sub2['node'] = 'testnode2' - sub2['jid'] = 'boogers@bork.top/bill' - sub2['subscription'] = 'subscribed' - iq['pubsub']['subscriptions'].append(sub1) - iq['pubsub']['subscriptions'].append(sub2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testOptionalSettings(self): - "Testing iq/pubsub/subscription/subscribe-options stanzas" - iq = self.ps.Iq() - iq['pubsub']['subscription']['suboptions']['required'] = True - iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' - iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" - iq['pubsub']['subscription']['subscription'] = 'unconfigured' - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testItems(self): - "Testing iq/pubsub/items stanzas" - iq = self.ps.Iq() - iq['pubsub']['items'] - payload = ET.fromstring("""""") - payload2 = ET.fromstring("""""") - item = self.ps.Item() - item['id'] = 'asdf' - item['payload'] = payload - item2 = self.ps.Item() - item2['id'] = 'asdf2' - item2['payload'] = payload2 - iq['pubsub']['items'].append(item) - iq['pubsub']['items'].append(item2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testCreate(self): - "Testing iq/pubsub/create&configure stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub']['create']['node'] = 'mynode' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub']['configure']['config'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testState(self): - "Testing iq/psstate stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['psstate']['node']= 'mynode' - iq['psstate']['item']= 'myitem' - pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') - iq['psstate']['payload'] = pl - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testDefault(self): - "Testing iq/pubsub_owner/default stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub_owner']['default'] - iq['pubsub_owner']['default']['node'] = 'mynode' - iq['pubsub_owner']['default']['type'] = 'leaf' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub_owner']['default']['config'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testSubscribe(self): - "Testing iq/pubsub/subscribe stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub']['subscribe']['options'] - iq['pubsub']['subscribe']['node'] = 'cheese' - iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' - iq['pubsub']['subscribe']['options']['node'] = 'cheese' - iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub']['subscribe']['options']['options'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testPublish(self): - "Testing iq/pubsub/publish stanzas" - iq = self.ps.Iq() - iq['pubsub']['publish']['node'] = 'thingers' - payload = ET.fromstring("""""") - payload2 = ET.fromstring("""""") - item = self.ps.Item() - item['id'] = 'asdf' - item['payload'] = payload - item2 = self.ps.Item() - item2['id'] = 'asdf2' - item2['payload'] = payload2 - iq['pubsub']['publish'].append(item) - iq['pubsub']['publish'].append(item2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) + def testSubscriptions(self): + "Testing iq/pubsub/subscriptions/subscription stanzas" + iq = self.Iq() + sub1 = pubsub.Subscription() + sub1['node'] = 'testnode' + sub1['jid'] = 'steve@myserver.tld/someresource' + sub2 = pubsub.Subscription() + sub2['node'] = 'testnode2' + sub2['jid'] = 'boogers@bork.top/bill' + sub2['subscription'] = 'subscribed' + iq['pubsub']['subscriptions'].append(sub1) + iq['pubsub']['subscriptions'].append(sub2) + self.checkIq(iq, """ + + + + + + + + """) - def testDelete(self): - "Testing iq/pubsub_owner/delete stanzas" - iq = self.ps.Iq() - iq['pubsub_owner']['delete']['node'] = 'thingers' - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - iq3.setValues(iq2.getValues()) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) + def testOptionalSettings(self): + "Testing iq/pubsub/subscription/subscribe-options stanzas" + iq = self.Iq() + iq['pubsub']['subscription']['suboptions']['required'] = True + iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' + iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" + iq['pubsub']['subscription']['subscription'] = 'unconfigured' + self.checkIq(iq, """ + + + + + + + + + """) + + def testItems(self): + "Testing iq/pubsub/items stanzas" + iq = self.Iq() + iq['pubsub']['items'] + payload = ET.fromstring(""" + + + + """) + payload2 = ET.fromstring(""" + + + + """) + item = pubsub.Item() + item['id'] = 'asdf' + item['payload'] = payload + item2 = pubsub.Item() + item2['id'] = 'asdf2' + item2['payload'] = payload2 + iq['pubsub']['items'].append(item) + iq['pubsub']['items'].append(item2) + self.checkIq(iq, """ + + + + + + + + + + + + + + + + + + """) + + def testCreate(self): + "Testing iq/pubsub/create&configure stanzas" + iq = self.Iq() + iq['pubsub']['create']['node'] = 'mynode' + iq['pubsub']['configure']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + self.checkIq(iq, """ + + + + + + + This thing is awesome + + + + + """) + + def testState(self): + "Testing iq/psstate stanzas" + iq = self.Iq() + iq['psstate']['node']= 'mynode' + iq['psstate']['item']= 'myitem' + pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') + iq['psstate']['payload'] = pl + self.checkIq(iq, """ + + + + + """) + + def testDefault(self): + "Testing iq/pubsub_owner/default stanzas" + iq = self.Iq() + iq['pubsub_owner']['default'] + iq['pubsub_owner']['default']['node'] = 'mynode' + iq['pubsub_owner']['default']['type'] = 'leaf' + iq['pubsub_owner']['default']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + self.checkIq(iq, """ + + + + + + This thing is awesome + + + + + """, use_values=False) + + def testSubscribe(self): + "Testing iq/pubsub/subscribe stanzas" + iq = self.Iq() + iq['pubsub']['subscribe']['options'] + iq['pubsub']['subscribe']['node'] = 'cheese' + iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' + iq['pubsub']['subscribe']['options']['node'] = 'cheese' + iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' + form = xep_0004.Form() + form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') + iq['pubsub']['subscribe']['options']['options'] = form + self.checkIq(iq, """ + + + + + + + This thing is awesome + + + + + + """, use_values=False) + + def testPublish(self): + "Testing iq/pubsub/publish stanzas" + iq = self.Iq() + iq['pubsub']['publish']['node'] = 'thingers' + payload = ET.fromstring(""" + + + + """) + payload2 = ET.fromstring(""" + + + + """) + item = pubsub.Item() + item['id'] = 'asdf' + item['payload'] = payload + item2 = pubsub.Item() + item2['id'] = 'asdf2' + item2['payload'] = payload2 + iq['pubsub']['publish'].append(item) + iq['pubsub']['publish'].append(item2) - def testCreateConfigGet(self): - """Testing getting config from full create""" - xml = """http://jabber.org/protocol/pubsub#node_configleaf111101openpublishersnever""" - iq = self.ps.Iq(None, self.ps.ET.fromstring(xml)) - config = iq['pubsub']['configure']['config'] - self.failUnless(config.getValues() != {}) + self.checkIq(iq, """ + + + + + + + + + + + + + + + + + + """) - def testItemEvent(self): - """Testing message/pubsub_event/items/item""" - msg = self.ps.Message() - item = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - item['payload'] = pl - item['id'] = 'abc123' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testDelete(self): + "Testing iq/pubsub_owner/delete stanzas" + iq = self.Iq() + iq['pubsub_owner']['delete']['node'] = 'thingers' + self.checkIq(iq, """ + + + + + """) - def testItemsEvent(self): - """Testing multiple message/pubsub_event/items/item""" - msg = self.ps.Message() - item = self.ps.EventItem() - item2 = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testCreateConfigGet(self): + """Testing getting config from full create""" + iq = self.Iq() + iq['to'] = 'pubsub.asdf' + iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7' + iq['type'] = 'set' + iq['id'] = 'E' - def testItemsEvent(self): - """Testing message/pubsub_event/items/item & retract mix""" - msg = self.ps.Message() - item = self.ps.EventItem() - item2 = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - retract = self.ps.EventRetract() - retract['id'] = 'aabbcc' - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(retract) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testCollectionAssociate(self): - """Testing message/pubsub_event/collection/associate""" - msg = self.ps.Message() - msg['pubsub_event']['collection']['associate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + pub = iq['pubsub'] + pub['create']['node'] = 'testnode2' + pub['configure']['form']['type'] = 'submit' + pub['configure']['form'].setFields([ + ('FORM_TYPE', {'type': 'hidden', + 'value': 'http://jabber.org/protocol/pubsub#node_config'}), + ('pubsub#node_type', {'type': 'list-single', + 'label': 'Select the node type', + 'value': 'leaf'}), + ('pubsub#title', {'type': 'text-single', + 'label': 'A friendly name for the node'}), + ('pubsub#deliver_notifications', {'type': 'boolean', + 'label': 'Deliver event notifications', + 'value': True}), + ('pubsub#deliver_payloads', {'type': 'boolean', + 'label': 'Deliver payloads with event notifications', + 'value': True}), + ('pubsub#notify_config', {'type': 'boolean', + 'label': 'Notify subscribers when the node configuration changes'}), + ('pubsub#notify_delete', {'type': 'boolean', + 'label': 'Notify subscribers when the node is deleted'}), + ('pubsub#notify_retract', {'type': 'boolean', + 'label': 'Notify subscribers when items are removed from the node', + 'value': True}), + ('pubsub#notify_sub', {'type': 'boolean', + 'label': 'Notify owners about new subscribers and unsubscribes'}), + ('pubsub#persist_items', {'type': 'boolean', + 'label': 'Persist items in storage'}), + ('pubsub#max_items', {'type': 'text-single', + 'label': 'Max # of items to persist', + 'value': '10'}), + ('pubsub#subscribe', {'type': 'boolean', + 'label': 'Whether to allow subscriptions', + 'value': True}), + ('pubsub#access_model', {'type': 'list-single', + 'label': 'Specify the subscriber model', + 'value': 'open'}), + ('pubsub#publish_model', {'type': 'list-single', + 'label': 'Specify the publisher model', + 'value': 'publishers'}), + ('pubsub#send_last_published_item', {'type': 'list-single', + 'label': 'Send last published item', + 'value': 'never'}), + ('pubsub#presence_based_delivery', {'type': 'boolean', + 'label': 'Deliver notification only to available users'}), + ]) - def testCollectionDisassociate(self): - """Testing message/pubsub_event/collection/disassociate""" - msg = self.ps.Message() - msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + self.checkIq(iq, """ + + + + + + + http://jabber.org/protocol/pubsub#node_config + + + leaf + + + + 1 + + + 1 + + + + + 1 + + + + + 10 + + + 1 + + + open + + + publishers + + + never + + + + + + """) - def testEventConfiguration(self): - """Testing message/pubsub_event/configuration/config""" - msg = self.ps.Message() - from sleekxmpp.plugins import xep_0004 - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - msg['pubsub_event']['configuration']['node'] = 'cheese' - msg['pubsub_event']['configuration']['config'] = form - msg['type'] = 'headline' - xmlstring = """This thing is awesome""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testEventPurge(self): - """Testing message/pubsub_event/purge""" - msg = self.ps.Message() - msg['pubsub_event']['purge']['node'] = 'pickles' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testEventSubscription(self): - """Testing message/pubsub_event/subscription""" - msg = self.ps.Message() - msg['pubsub_event']['subscription']['node'] = 'pickles' - msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' - msg['pubsub_event']['subscription']['subid'] = 'aabb1122' - msg['pubsub_event']['subscription']['subscription'] = 'subscribed' - msg['pubsub_event']['subscription']['expiry'] = 'presence' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlcompare.comparemany([xmlstring, str(msg), str(msg2), str(msg3)])) + def testItemEvent(self): + """Testing message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + item['payload'] = pl + item['id'] = 'abc123' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + """) -suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas) + def testItemsEvent(self): + """Testing multiple message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + + + + """) + + def testItemsEvent(self): + """Testing message/pubsub_event/items/item & retract mix""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + retract = pubsub.EventRetract() + retract['id'] = 'aabbcc' + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(retract) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + + + + """) + + def testCollectionAssociate(self): + """Testing message/pubsub_event/collection/associate""" + msg = self.Message() + msg['pubsub_event']['collection']['associate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + + """) + + def testCollectionDisassociate(self): + """Testing message/pubsub_event/collection/disassociate""" + msg = self.Message() + msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + + """) + + def testEventConfiguration(self): + """Testing message/pubsub_event/configuration/config""" + msg = self.Message() + msg['pubsub_event']['configuration']['node'] = 'cheese' + msg['pubsub_event']['configuration']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + This thing is awesome + + + + + """) + + def testEventPurge(self): + """Testing message/pubsub_event/purge""" + msg = self.Message() + msg['pubsub_event']['purge']['node'] = 'pickles' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + """) + + def testEventSubscription(self): + """Testing message/pubsub_event/subscription""" + msg = self.Message() + msg['pubsub_event']['subscription']['node'] = 'pickles' + msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' + msg['pubsub_event']['subscription']['subid'] = 'aabb1122' + msg['pubsub_event']['subscription']['subscription'] = 'subscribed' + msg['pubsub_event']['subscription']['expiry'] = 'presence' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas) From 5c9b47afbdb79229d66378f9b9e1a601a3d58327 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 12:22:25 -0400 Subject: [PATCH 43/99] Update test_events to use SleekTest to make everything consistent. --- tests/test_events.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_events.py b/tests/test_events.py index 11821db..8f391e1 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,14 +1,11 @@ -import unittest +import sleekxmpp +from sleektest import * -class testevents(unittest.TestCase): - def setUp(self): - import sleekxmpp.stanza.presence as p - self.p = p +class TestEvents(SleekTest): def testEventHappening(self): "Test handler working" - import sleekxmpp c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handletestevent(event): @@ -20,7 +17,6 @@ class testevents(unittest.TestCase): def testDelEvent(self): "Test handler working, then deleted and not triggered" - import sleekxmpp c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handletestevent(event): @@ -32,4 +28,4 @@ class testevents(unittest.TestCase): self.failUnless(happened == [True], "event did not get triggered the correct number of times") -suite = unittest.TestLoader().loadTestsFromTestCase(testevents) +suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents) From b67b930596bfb0748b67bde02d37f2277d8c9576 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 12:27:22 -0400 Subject: [PATCH 44/99] Updated xep_0050 to use old_0004 for now. --- sleekxmpp/plugins/xep_0050.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py index 319401b..4ff4285 100644 --- a/sleekxmpp/plugins/xep_0050.py +++ b/sleekxmpp/plugins/xep_0050.py @@ -71,7 +71,7 @@ class xep_0050(base.base_plugin): in_command = xml.find('{http://jabber.org/protocol/commands}command') sessionid = in_command.get('sessionid', None) pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['xep_0004'].makeForm('result') + results = self.xmpp.plugin['old_0004'].makeForm('result') results.fromXML(in_command.find('{jabber:x:data}x')) pointer(results,sessionid) self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[])) @@ -82,7 +82,7 @@ class xep_0050(base.base_plugin): in_command = xml.find('{http://jabber.org/protocol/commands}command') sessionid = in_command.get('sessionid', None) pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['xep_0004'].makeForm('result') + results = self.xmpp.plugin['old_0004'].makeForm('result') results.fromXML(in_command.find('{jabber:x:data}x')) form, npointer, next = pointer(results,sessionid) self.sessions[sessionid]['next'] = npointer From 75afefb5c66c7d8a5ba7fa7bdcc3f87e69936ec5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 20 Jul 2010 13:23:35 -0400 Subject: [PATCH 45/99] Upated xep_0045 to use old_0004 for now. --- sleekxmpp/plugins/xep_0045.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index cd1a9a0..1892eea 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -154,13 +154,13 @@ class xep_0045(base.base_plugin): return False xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') if xform is None: return False - form = self.xmpp.plugin['xep_0004'].buildForm(xform) + form = self.xmpp.plugin['old_0004'].buildForm(xform) return form def configureRoom(self, room, form=None, ifrom=None): if form is None: form = self.getRoomForm(room, ifrom=ifrom) - #form = self.xmpp.plugin['xep_0004'].makeForm(ftype='submit') + #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig') iq = self.xmpp.makeIqSet() iq['to'] = room @@ -262,7 +262,7 @@ class xep_0045(base.base_plugin): form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') if form is None: raise ValueError - return self.xmpp.plugin['xep_0004'].buildForm(form) + return self.xmpp.plugin['old_0004'].buildForm(form) def cancelConfig(self, room): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') From 9fcd2e93a360939b5a55af93239b467a7ac32028 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 20 Jul 2010 11:15:59 -0700 Subject: [PATCH 46/99] don't send resource in bind request if you don't have one --- LICENSE | 2 +- sleekxmpp/__init__.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 59c501b..fb9f977 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 ICRL +Copyright (c) 2010 Nathanael C. Fritz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index b680dda..16fd105 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -222,9 +222,10 @@ class ClientXMPP(basexmpp, XMLStream): def handler_bind_resource(self, xml): logging.debug("Requesting resource: %s" % self.resource) iq = self.Iq(stype='set') - res = ET.Element('resource') - res.text = self.resource - xml.append(res) + if self.resource: + res = ET.Element('resource') + res.text = self.resource + xml.append(res) iq.append(xml) response = iq.send() #response = self.send(iq, self.Iq(sid=iq['id'])) From ca2c421e6c5de3dc97c8555301592d64e591db47 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 20 Jul 2010 11:20:47 -0700 Subject: [PATCH 47/99] fixed resource binding element to conform to spec --- sleekxmpp/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 16fd105..a9bfc95 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -221,6 +221,7 @@ class ClientXMPP(basexmpp, XMLStream): def handler_bind_resource(self, xml): logging.debug("Requesting resource: %s" % self.resource) + xml.clear() iq = self.Iq(stype='set') if self.resource: res = ET.Element('resource') From 66e92c6c9fece2ce5a2b62230a003c09f3ec0a7f Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Tue, 20 Jul 2010 11:33:43 -0700 Subject: [PATCH 48/99] Modified example to take JID and password on command line --- INSTALL | 3 +++ example.py | 69 +++++++++++++++++++++++++++++------------------------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/INSTALL b/INSTALL index b73b5e5..f081a35 100644 --- a/INSTALL +++ b/INSTALL @@ -6,3 +6,6 @@ python3 setup.py install Root install: sudo python3 setup.py install + +To test: +python example.py -v -j [USER@example.com] -p [PASSWORD] diff --git a/example.py b/example.py index c9b6559..972b6d0 100644 --- a/example.py +++ b/example.py @@ -8,41 +8,46 @@ import time import sys if sys.version_info < (3,0): - reload(sys) - sys.setdefaultencoding('utf8') + reload(sys) + sys.setdefaultencoding('utf8') class Example(sleekxmpp.ClientXMPP): - - def __init__(self, jid, password): - sleekxmpp.ClientXMPP.__init__(self, jid, password) - self.add_event_handler("session_start", self.start) - self.add_event_handler("message", self.message) - - def start(self, event): - self.getRoster() - self.sendPresence() + + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + self.add_event_handler("session_start", self.start) + self.add_event_handler("message", self.message) + + def start(self, event): + self.getRoster() + self.sendPresence() - def message(self, msg): - msg.reply("Thanks for sending\n%(body)s" % msg).send() + def message(self, msg): + msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': - #parse command line arguements - optp = OptionParser() - optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO) - optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) - optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) - optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use") - opts,args = optp.parse_args() - - logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') - xmpp = Example('user@gmail.com/sleekxmpp', 'password') - xmpp.registerPlugin('xep_0030') - xmpp.registerPlugin('xep_0004') - xmpp.registerPlugin('xep_0060') - xmpp.registerPlugin('xep_0199') - if xmpp.connect(('talk.google.com', 5222)): - xmpp.process(threaded=False) - print("done") - else: - print("Unable to connect.") + #parse command line arguements + optp = OptionParser() + optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO) + optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) + optp.add_option("-j","--jid", dest="jid", help="JID to use") + optp.add_option("-p","--password", dest="password", help="password to use") + opts,args = optp.parse_args() + + logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') + xmpp = Example(opts.jid, opts.password) + xmpp.registerPlugin('xep_0030') + xmpp.registerPlugin('xep_0004') + xmpp.registerPlugin('xep_0060') + xmpp.registerPlugin('xep_0199') + + # use this if you don't have pydns, and want to + # talk to GoogleTalk (e.g.) +# if xmpp.connect(('talk.google.com', 5222)): + if xmpp.connect(): + xmpp.process(threaded=False) + print("done") + else: + print("Unable to connect.") From d70a6e6f32d5d5f9bd4d4d97f987f1667f6eab40 Mon Sep 17 00:00:00 2001 From: Joe Hildebrand Date: Tue, 20 Jul 2010 13:55:48 -0700 Subject: [PATCH 49/99] Issue 26. Only set from address in reply() for components --- sleekxmpp/__init__.py | 429 ++++++++--------- sleekxmpp/componentxmpp.py | 101 ++-- sleekxmpp/xmlstream/stanzabase.py | 733 +++++++++++++++--------------- 3 files changed, 635 insertions(+), 628 deletions(-) diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index a9bfc95..3f0a1e8 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -30,231 +30,232 @@ from . import plugins #from . import stanza srvsupport = True try: - import dns.resolver + import dns.resolver except ImportError: - srvsupport = False + srvsupport = False #class PresenceStanzaType(object): -# -# def fromXML(self, xml): -# self.ptype = xml.get('type') +# +# def fromXML(self, xml): +# self.ptype = xml.get('type') class ClientXMPP(basexmpp, XMLStream): - """SleekXMPP's client class. Use only for good, not evil.""" + """SleekXMPP's client class. Use only for good, not evil.""" - def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True): - global srvsupport - XMLStream.__init__(self) - self.default_ns = 'jabber:client' - basexmpp.__init__(self) - self.plugin_config = plugin_config - self.escape_quotes = escape_quotes - self.set_jid(jid) - self.plugin_whitelist = plugin_whitelist - self.auto_reconnect = True - self.srvsupport = srvsupport - self.password = password - self.registered_features = [] - self.stream_header = """""" % (self.server,self.default_ns) - self.stream_footer = "" - #self.map_namespace('http://etherx.jabber.org/streams', 'stream') - #self.map_namespace('jabber:client', '') - self.features = [] - #TODO: Use stream state here - self.authenticated = False - self.sessionstarted = False - self.bound = False - self.bindfail = False - self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True)) - self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True)) - #self.registerHandler(Callback('Roster Update', MatchXMLMask("" % self.default_ns), self._handlePresenceSubscribe, thread=True)) - self.registerFeature("", self.handler_starttls, True) - self.registerFeature("", self.handler_sasl_auth, True) - self.registerFeature("", self.handler_bind_resource) - self.registerFeature("", self.handler_start_session) - - #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType) - #self.register_plugins() - - def __getitem__(self, key): - if key in self.plugin: - return self.plugin[key] - else: - logging.warning("""Plugin "%s" is not loaded.""" % key) - return False - - def get(self, key, default): - return self.plugin.get(key, default) + def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True): + global srvsupport + XMLStream.__init__(self) + self.default_ns = 'jabber:client' + basexmpp.__init__(self) + self.plugin_config = plugin_config + self.escape_quotes = escape_quotes + self.set_jid(jid) + self.plugin_whitelist = plugin_whitelist + self.auto_reconnect = True + self.srvsupport = srvsupport + self.password = password + self.registered_features = [] + self.stream_header = """""" % (self.server,self.default_ns) + self.stream_footer = "" + #self.map_namespace('http://etherx.jabber.org/streams', 'stream') + #self.map_namespace('jabber:client', '') + self.features = [] + #TODO: Use stream state here + self.authenticated = False + self.sessionstarted = False + self.bound = False + self.bindfail = False + self.is_component = False + self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True)) + self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True)) + #self.registerHandler(Callback('Roster Update', MatchXMLMask("" % self.default_ns), self._handlePresenceSubscribe, thread=True)) + self.registerFeature("", self.handler_starttls, True) + self.registerFeature("", self.handler_sasl_auth, True) + self.registerFeature("", self.handler_bind_resource) + self.registerFeature("", self.handler_start_session) + + #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType) + #self.register_plugins() + + def __getitem__(self, key): + if key in self.plugin: + return self.plugin[key] + else: + logging.warning("""Plugin "%s" is not loaded.""" % key) + return False + + def get(self, key, default): + return self.plugin.get(key, default) - def connect(self, address=tuple()): - """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses - the JID server.""" - if not address or len(address) < 2: - if not self.srvsupport: - logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.") - else: - logging.debug("Since no address is supplied, attempting SRV lookup.") - try: - answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, dns.rdatatype.SRV) - except dns.resolver.NXDOMAIN: - logging.debug("No appropriate SRV record found. Using JID server name.") - else: - # pick a random answer, weighted by priority - # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway - # suggestions are welcome - addresses = {} - intmax = 0 - priorities = [] - for answer in answers: - intmax += answer.priority - addresses[intmax] = (answer.target.to_text()[:-1], answer.port) - priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort() - picked = random.randint(0, intmax) - for priority in priorities: - if picked <= priority: - address = addresses[priority] - break - if not address: - # if all else fails take server from JID. - address = (self.server, 5222) - result = XMLStream.connect(self, address[0], address[1], use_tls=True) - if result: - self.event("connected") - else: - logging.warning("Failed to connect") - self.event("disconnected") - return result - - # overriding reconnect and disconnect so that we can get some events - # should events be part of or required by xmlstream? Maybe that would be cleaner - def reconnect(self): - logging.info("Reconnecting") - self.event("disconnected") - XMLStream.reconnect(self) - - def disconnect(self, init=True, close=False, reconnect=False): - self.event("disconnected") - XMLStream.disconnect(self, reconnect) - - def registerFeature(self, mask, pointer, breaker = False): - """Register a stream feature.""" - self.registered_features.append((MatchXMLMask(mask), pointer, breaker)) + def connect(self, address=tuple()): + """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses + the JID server.""" + if not address or len(address) < 2: + if not self.srvsupport: + logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.") + else: + logging.debug("Since no address is supplied, attempting SRV lookup.") + try: + answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, dns.rdatatype.SRV) + except dns.resolver.NXDOMAIN: + logging.debug("No appropriate SRV record found. Using JID server name.") + else: + # pick a random answer, weighted by priority + # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway + # suggestions are welcome + addresses = {} + intmax = 0 + priorities = [] + for answer in answers: + intmax += answer.priority + addresses[intmax] = (answer.target.to_text()[:-1], answer.port) + priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort() + picked = random.randint(0, intmax) + for priority in priorities: + if picked <= priority: + address = addresses[priority] + break + if not address: + # if all else fails take server from JID. + address = (self.server, 5222) + result = XMLStream.connect(self, address[0], address[1], use_tls=True) + if result: + self.event("connected") + else: + logging.warning("Failed to connect") + self.event("disconnected") + return result + + # overriding reconnect and disconnect so that we can get some events + # should events be part of or required by xmlstream? Maybe that would be cleaner + def reconnect(self): + logging.info("Reconnecting") + self.event("disconnected") + XMLStream.reconnect(self) + + def disconnect(self, init=True, close=False, reconnect=False): + self.event("disconnected") + XMLStream.disconnect(self, reconnect) + + def registerFeature(self, mask, pointer, breaker = False): + """Register a stream feature.""" + self.registered_features.append((MatchXMLMask(mask), pointer, breaker)) - def updateRoster(self, jid, name=None, subscription=None, groups=[]): - """Add or change a roster item.""" - iq = self.Iq().setStanzaValues({'type': 'set'}) - iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} - #self.send(iq, self.Iq().setValues({'id': iq['id']})) - r = iq.send() - return r['type'] == 'result' + def updateRoster(self, jid, name=None, subscription=None, groups=[]): + """Add or change a roster item.""" + iq = self.Iq().setStanzaValues({'type': 'set'}) + iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} + #self.send(iq, self.Iq().setValues({'id': iq['id']})) + r = iq.send() + return r['type'] == 'result' - def delRosterItem(self, jid): - iq = self.Iq() - iq['type'] = 'set' - iq['roster']['items'] = {jid: {'subscription': 'remove'}} - return iq.send()['type'] == 'result' - - def getRoster(self): - """Request the roster be sent.""" - iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send() - self._handleRoster(iq, request=True) - - def _handleStreamFeatures(self, features): - self.features = [] - for sub in features.xml: - self.features.append(sub.tag) - for subelement in features.xml: - for feature in self.registered_features: - if feature[0].match(subelement): - #if self.maskcmp(subelement, feature[0], True): - if feature[1](subelement) and feature[2]: #if breaker, don't continue - return True - - def handler_starttls(self, xml): - if not self.authenticated and self.ssl_support: - self.add_handler("", self.handler_tls_start, name='TLS Proceed', instream=True) - self.sendXML(xml) - return True - else: - logging.warning("The module tlslite is required in to some servers, and has not been found.") - return False + def delRosterItem(self, jid): + iq = self.Iq() + iq['type'] = 'set' + iq['roster']['items'] = {jid: {'subscription': 'remove'}} + return iq.send()['type'] == 'result' + + def getRoster(self): + """Request the roster be sent.""" + iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send() + self._handleRoster(iq, request=True) + + def _handleStreamFeatures(self, features): + self.features = [] + for sub in features.xml: + self.features.append(sub.tag) + for subelement in features.xml: + for feature in self.registered_features: + if feature[0].match(subelement): + #if self.maskcmp(subelement, feature[0], True): + if feature[1](subelement) and feature[2]: #if breaker, don't continue + return True + + def handler_starttls(self, xml): + if not self.authenticated and self.ssl_support: + self.add_handler("", self.handler_tls_start, name='TLS Proceed', instream=True) + self.sendXML(xml) + return True + else: + logging.warning("The module tlslite is required in to some servers, and has not been found.") + return False - def handler_tls_start(self, xml): - logging.debug("Starting TLS") - if self.startTLS(): - raise RestartStream() - - def handler_sasl_auth(self, xml): - if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: - return False - logging.debug("Starting SASL Auth") - self.add_handler("", self.handler_auth_success, name='SASL Sucess', instream=True) - self.add_handler("", self.handler_auth_fail, name='SASL Failure', instream=True) - sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism') - if len(sasl_mechs): - for sasl_mech in sasl_mechs: - self.features.append("sasl:%s" % sasl_mech.text) - if 'sasl:PLAIN' in self.features: - if sys.version_info < (3,0): - self.send("""%s""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8')) - else: - self.send("""%s""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8')) - else: - logging.error("No appropriate login method.") - self.disconnect() - #if 'sasl:DIGEST-MD5' in self.features: - # self._auth_digestmd5() - return True - - def handler_auth_success(self, xml): - self.authenticated = True - self.features = [] - raise RestartStream() + def handler_tls_start(self, xml): + logging.debug("Starting TLS") + if self.startTLS(): + raise RestartStream() + + def handler_sasl_auth(self, xml): + if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: + return False + logging.debug("Starting SASL Auth") + self.add_handler("", self.handler_auth_success, name='SASL Sucess', instream=True) + self.add_handler("", self.handler_auth_fail, name='SASL Failure', instream=True) + sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism') + if len(sasl_mechs): + for sasl_mech in sasl_mechs: + self.features.append("sasl:%s" % sasl_mech.text) + if 'sasl:PLAIN' in self.features: + if sys.version_info < (3,0): + self.send("""%s""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8')) + else: + self.send("""%s""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8')) + else: + logging.error("No appropriate login method.") + self.disconnect() + #if 'sasl:DIGEST-MD5' in self.features: + # self._auth_digestmd5() + return True + + def handler_auth_success(self, xml): + self.authenticated = True + self.features = [] + raise RestartStream() - def handler_auth_fail(self, xml): - logging.info("Authentication failed.") - self.disconnect() - self.event("failed_auth") - - def handler_bind_resource(self, xml): - logging.debug("Requesting resource: %s" % self.resource) - xml.clear() - iq = self.Iq(stype='set') - if self.resource: - res = ET.Element('resource') - res.text = self.resource - xml.append(res) - iq.append(xml) - response = iq.send() - #response = self.send(iq, self.Iq(sid=iq['id'])) - self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text) - self.bound = True - logging.info("Node set to: %s" % self.fulljid) - if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail: - logging.debug("Established Session") - self.sessionstarted = True - self.event("session_start") - - def handler_start_session(self, xml): - if self.authenticated and self.bound: - iq = self.makeIqSet(xml) - response = iq.send() - logging.debug("Established Session") - self.sessionstarted = True - self.event("session_start") - else: - #bind probably hasn't happened yet - self.bindfail = True - - def _handleRoster(self, iq, request=False): - if iq['type'] == 'set' or (iq['type'] == 'result' and request): - for jid in iq['roster']['items']: - if not jid in self.roster: - self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True} - self.roster[jid].update(iq['roster']['items'][jid]) - if iq['type'] == 'set': - self.send(self.Iq().setStanzaValues({'type': 'result', 'id': iq['id']}).enable('roster')) - self.event("roster_update", iq) + def handler_auth_fail(self, xml): + logging.info("Authentication failed.") + self.disconnect() + self.event("failed_auth") + + def handler_bind_resource(self, xml): + logging.debug("Requesting resource: %s" % self.resource) + xml.clear() + iq = self.Iq(stype='set') + if self.resource: + res = ET.Element('resource') + res.text = self.resource + xml.append(res) + iq.append(xml) + response = iq.send() + #response = self.send(iq, self.Iq(sid=iq['id'])) + self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text) + self.bound = True + logging.info("Node set to: %s" % self.fulljid) + if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail: + logging.debug("Established Session") + self.sessionstarted = True + self.event("session_start") + + def handler_start_session(self, xml): + if self.authenticated and self.bound: + iq = self.makeIqSet(xml) + response = iq.send() + logging.debug("Established Session") + self.sessionstarted = True + self.event("session_start") + else: + #bind probably hasn't happened yet + self.bindfail = True + + def _handleRoster(self, iq, request=False): + if iq['type'] == 'set' or (iq['type'] == 'result' and request): + for jid in iq['roster']['items']: + if not jid in self.roster: + self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True} + self.roster[jid].update(iq['roster']['items'][jid]) + if iq['type'] == 'set': + self.send(self.Iq().setStanzaValues({'type': 'result', 'id': iq['id']}).enable('roster')) + self.event("roster_update", iq) diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 4d39ce8..818d800 100755 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -30,59 +30,60 @@ from . import stanza import hashlib srvsupport = True try: - import dns.resolver + import dns.resolver except ImportError: - srvsupport = False + srvsupport = False class ComponentXMPP(basexmpp, XMLStream): - """SleekXMPP's client class. Use only for good, not evil.""" + """SleekXMPP's client class. Use only for good, not evil.""" - def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False): - XMLStream.__init__(self) - if use_jc_ns: - self.default_ns = 'jabber:client' - else: - self.default_ns = 'jabber:component:accept' - basexmpp.__init__(self) - self.auto_authorize = None - self.stream_header = "" % jid - self.stream_footer = "" - self.server_host = host - self.server_port = port - self.set_jid(jid) - self.secret = secret - self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake)) - - def __getitem__(self, key): - if key in self.plugin: - return self.plugin[key] - else: - logging.warning("""Plugin "%s" is not loaded.""" % key) - return False - - def get(self, key, default): - return self.plugin.get(key, default) - - def incoming_filter(self, xmlobj): - if xmlobj.tag.startswith('{jabber:client}'): - xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns) - for sub in xmlobj: - self.incoming_filter(sub) - return xmlobj + def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False): + XMLStream.__init__(self) + if use_jc_ns: + self.default_ns = 'jabber:client' + else: + self.default_ns = 'jabber:component:accept' + basexmpp.__init__(self) + self.auto_authorize = None + self.stream_header = "" % jid + self.stream_footer = "" + self.server_host = host + self.server_port = port + self.set_jid(jid) + self.secret = secret + self.is_component = True + self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake)) + + def __getitem__(self, key): + if key in self.plugin: + return self.plugin[key] + else: + logging.warning("""Plugin "%s" is not loaded.""" % key) + return False + + def get(self, key, default): + return self.plugin.get(key, default) + + def incoming_filter(self, xmlobj): + if xmlobj.tag.startswith('{jabber:client}'): + xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns) + for sub in xmlobj: + self.incoming_filter(sub) + return xmlobj - def start_stream_handler(self, xml): - sid = xml.get('id', '') - handshake = ET.Element('{jabber:component:accept}handshake') - if sys.version_info < (3,0): - handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower() - else: - handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower() - self.sendXML(handshake) - - def _handleHandshake(self, xml): - self.event("session_start") - - def connect(self): - logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port)) - return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port) + def start_stream_handler(self, xml): + sid = xml.get('id', '') + handshake = ET.Element('{jabber:component:accept}handshake') + if sys.version_info < (3,0): + handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower() + else: + handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower() + self.sendXML(handshake) + + def _handleHandshake(self, xml): + self.event("session_start") + + def connect(self): + logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port)) + return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 6436fc5..66a08e4 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -13,393 +13,398 @@ import weakref import copy if sys.version_info < (3,0): - from . import tostring26 as tostring + from . import tostring26 as tostring else: - from . import tostring + from . import tostring xmltester = type(ET.Element('xml')) def registerStanzaPlugin(stanza, plugin): - """ - Associate a stanza object as a plugin for another stanza. - """ - tag = "{%s}%s" % (plugin.namespace, plugin.name) - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map[tag] = plugin + """ + Associate a stanza object as a plugin for another stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin class JID(object): - def __init__(self, jid): - self.jid = jid - - def __getattr__(self, name): - if name == 'resource': - return self.jid.split('/', 1)[-1] - elif name == 'user': - if '@' in self.jid: - return self.jid.split('@', 1)[0] - else: - return '' - elif name == 'server': - return self.jid.split('@', 1)[-1].split('/', 1)[0] - elif name == 'full': - return self.jid - elif name == 'bare': - return self.jid.split('/', 1)[0] - - def __str__(self): - return self.jid + def __init__(self, jid): + self.jid = jid + + def __getattr__(self, name): + if name == 'resource': + return self.jid.split('/', 1)[-1] + elif name == 'user': + if '@' in self.jid: + return self.jid.split('@', 1)[0] + else: + return '' + elif name == 'server': + return self.jid.split('@', 1)[-1].split('/', 1)[0] + elif name == 'full': + return self.jid + elif name == 'bare': + return self.jid.split('/', 1)[0] + + def __str__(self): + return self.jid class ElementBase(tostring.ToString): - name = 'stanza' - plugin_attrib = 'plugin' - namespace = 'jabber:client' - interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = None + name = 'stanza' + plugin_attrib = 'plugin' + namespace = 'jabber:client' + interfaces = set(('type', 'to', 'from', 'id', 'payload')) + types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + sub_interfaces = tuple() + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = None - def __init__(self, xml=None, parent=None): - if parent is None: - self.parent = None - else: - self.parent = weakref.ref(parent) - self.xml = xml - self.plugins = {} - self.iterables = [] - self.idx = 0 - if not self.setup(xml): - for child in self.xml.getchildren(): - if child.tag in self.plugin_tag_map: - self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) - if self.subitem is not None: - for sub in self.subitem: - if child.tag == "{%s}%s" % (sub.namespace, sub.name): - self.iterables.append(sub(xml=child, parent=self)) - break + def __init__(self, xml=None, parent=None): + if parent is None: + self.parent = None + else: + self.parent = weakref.ref(parent) + self.xml = xml + self.plugins = {} + self.iterables = [] + self.idx = 0 + if not self.setup(xml): + for child in self.xml.getchildren(): + if child.tag in self.plugin_tag_map: + self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) + if self.subitem is not None: + for sub in self.subitem: + if child.tag == "{%s}%s" % (sub.namespace, sub.name): + self.iterables.append(sub(xml=child, parent=self)) + break - @property - def attrib(self): #backwards compatibility - return self + @property + def attrib(self): #backwards compatibility + return self - def __iter__(self): - self.idx = 0 - return self + def __iter__(self): + self.idx = 0 + return self - def __bool__(self): - return True - - def __next__(self): - self.idx += 1 - if self.idx > len(self.iterables): - self.idx = 0 - raise StopIteration - return self.iterables[self.idx - 1] - - def next(self): - return self.__next__() + def __bool__(self): + return True + + def __next__(self): + self.idx += 1 + if self.idx > len(self.iterables): + self.idx = 0 + raise StopIteration + return self.iterables[self.idx - 1] + + def next(self): + return self.__next__() - def __len__(self): - return len(self.iterables) - - def append(self, item): - if not isinstance(item, ElementBase): - if type(item) == xmltester: - return self.appendxml(item) - else: - raise TypeError - self.xml.append(item.xml) - self.iterables.append(item) - return self - - def pop(self, idx=0): - aff = self.iterables.pop(idx) - self.xml.remove(aff.xml) - return aff - - def get(self, key, defaultvalue=None): - value = self[key] - if value is None or value == '': - return defaultvalue - return value - - def keys(self): - out = [] - out += [x for x in self.interfaces] - out += [x for x in self.plugins] - if self.iterables: - out.append('substanzas') - return tuple(out) - - def match(self, matchstring): - if isinstance(matchstring, str): - nodes = matchstring.split('/') - else: - nodes = matchstring - tagargs = nodes[0].split('@') - if tagargs[0] not in (self.plugins, self.plugin_attrib): return False - founditerable = False - for iterable in self.iterables: - if nodes[1:] == []: - break - founditerable = iterable.match(nodes[1:]) - if founditerable: break; - for evals in tagargs[1:]: - x,y = evals.split('=') - if self[x] != y: return False - if not founditerable and len(nodes) > 1: - next = nodes[1].split('@')[0] - if next in self.plugins: - return self.plugins[next].match(nodes[1:]) - else: - return False - return True - - def find(self, xpath): # for backwards compatiblity, expose elementtree interface - return self.xml.find(xpath) + def __len__(self): + return len(self.iterables) + + def append(self, item): + if not isinstance(item, ElementBase): + if type(item) == xmltester: + return self.appendxml(item) + else: + raise TypeError + self.xml.append(item.xml) + self.iterables.append(item) + return self + + def pop(self, idx=0): + aff = self.iterables.pop(idx) + self.xml.remove(aff.xml) + return aff + + def get(self, key, defaultvalue=None): + value = self[key] + if value is None or value == '': + return defaultvalue + return value + + def keys(self): + out = [] + out += [x for x in self.interfaces] + out += [x for x in self.plugins] + if self.iterables: + out.append('substanzas') + return tuple(out) + + def match(self, matchstring): + if isinstance(matchstring, str): + nodes = matchstring.split('/') + else: + nodes = matchstring + tagargs = nodes[0].split('@') + if tagargs[0] not in (self.plugins, self.plugin_attrib): return False + founditerable = False + for iterable in self.iterables: + if nodes[1:] == []: + break + founditerable = iterable.match(nodes[1:]) + if founditerable: break; + for evals in tagargs[1:]: + x,y = evals.split('=') + if self[x] != y: return False + if not founditerable and len(nodes) > 1: + next = nodes[1].split('@')[0] + if next in self.plugins: + return self.plugins[next].match(nodes[1:]) + else: + return False + return True + + def find(self, xpath): # for backwards compatiblity, expose elementtree interface + return self.xml.find(xpath) - def findall(self, xpath): - return self.xml.findall(xpath) - - def setup(self, xml=None): - if self.xml is None: - self.xml = xml - if self.xml is None: - for ename in self.name.split('/'): - new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) - if self.xml is None: - self.xml = new - else: - self.xml.append(new) - if self.parent is not None: - self.parent().xml.append(self.xml) - return True #had to generate XML - else: - return False + def findall(self, xpath): + return self.xml.findall(xpath) + + def setup(self, xml=None): + if self.xml is None: + self.xml = xml + if self.xml is None: + for ename in self.name.split('/'): + new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) + if self.xml is None: + self.xml = new + else: + self.xml.append(new) + if self.parent is not None: + self.parent().xml.append(self.xml) + return True #had to generate XML + else: + return False - def enable(self, attrib): - self.initPlugin(attrib) - return self - - def initPlugin(self, attrib): - if attrib not in self.plugins: - self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) - - def __getitem__(self, attrib): - if attrib == 'substanzas': - return self.iterables - elif attrib in self.interfaces: - if hasattr(self, "get%s" % attrib.title()): - return getattr(self, "get%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._getSubText(attrib) - else: - return self._getAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - return self.plugins[attrib] - else: - return '' - - def __setitem__(self, attrib, value): - if attrib in self.interfaces: - if value is not None: - if hasattr(self, "set%s" % attrib.title()): - getattr(self, "set%s" % attrib.title())(value,) - else: - if attrib in self.sub_interfaces: - return self._setSubText(attrib, text=value) - else: - self._setAttr(attrib, value) - else: - self.__delitem__(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - self.initPlugin(attrib) - self.plugins[attrib][attrib] = value - return self - - def __delitem__(self, attrib): - if attrib.lower() in self.interfaces: - if hasattr(self, "del%s" % attrib.title()): - getattr(self, "del%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._delSub(attrib) - else: - self._delAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib in self.plugins: - del self.plugins[attrib] - return self - - def __eq__(self, other): - if not isinstance(other, ElementBase): - return False - values = self.getStanzaValues() - for key in other: - if key not in values or values[key] != other[key]: - return False - return True - - def _setAttr(self, name, value): - if value is None or value == '': - self.__delitem__(name) - else: - self.xml.attrib[name] = value - - def _delAttr(self, name): - if name in self.xml.attrib: - del self.xml.attrib[name] - - def _getAttr(self, name): - return self.xml.attrib.get(name, '') - - def _getSubText(self, name): - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) - if stanza is None or stanza.text is None: - return '' - else: - return stanza.text - - def _setSubText(self, name, attrib={}, text=None): - if text is None or text == '': - return self.__delitem__(name) - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) - if stanza is None: - #self.xml.append(ET.Element("{%s}%s" % (self.namespace, name), attrib)) - stanza = ET.Element("{%s}%s" % (self.namespace, name)) - self.xml.append(stanza) - stanza.text = text - return stanza - - def _delSub(self, name): - for child in self.xml.getchildren(): - if child.tag == "{%s}%s" % (self.namespace, name): - self.xml.remove(child) - - def getStanzaValues(self): - out = {} - for interface in self.interfaces: - out[interface] = self[interface] - for pluginkey in self.plugins: - out[pluginkey] = self.plugins[pluginkey].getStanzaValues() - if self.iterables: - iterables = [] - for stanza in self.iterables: - iterables.append(stanza.getStanzaValues()) - iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) - out['substanzas'] = iterables - return out - - def setStanzaValues(self, attrib): - for interface in attrib: - if interface == 'substanzas': - for subdict in attrib['substanzas']: - if '__childtag__' in subdict: - for subclass in self.subitem: - if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): - sub = subclass(parent=self) - sub.setStanzaValues(subdict) - self.iterables.append(sub) - break - elif interface in self.interfaces: - self[interface] = attrib[interface] - elif interface in self.plugin_attrib_map and interface not in self.plugins: - self.initPlugin(interface) - if interface in self.plugins: - self.plugins[interface].setStanzaValues(attrib[interface]) - return self - - def appendxml(self, xml): - self.xml.append(xml) - return self + def enable(self, attrib): + self.initPlugin(attrib) + return self + + def initPlugin(self, attrib): + if attrib not in self.plugins: + self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) + + def __getitem__(self, attrib): + if attrib == 'substanzas': + return self.iterables + elif attrib in self.interfaces: + if hasattr(self, "get%s" % attrib.title()): + return getattr(self, "get%s" % attrib.title())() + else: + if attrib in self.sub_interfaces: + return self._getSubText(attrib) + else: + return self._getAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: self.initPlugin(attrib) + return self.plugins[attrib] + else: + return '' + + def __setitem__(self, attrib, value): + if attrib in self.interfaces: + if value is not None: + if hasattr(self, "set%s" % attrib.title()): + getattr(self, "set%s" % attrib.title())(value,) + else: + if attrib in self.sub_interfaces: + return self._setSubText(attrib, text=value) + else: + self._setAttr(attrib, value) + else: + self.__delitem__(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: self.initPlugin(attrib) + self.initPlugin(attrib) + self.plugins[attrib][attrib] = value + return self + + def __delitem__(self, attrib): + if attrib.lower() in self.interfaces: + if hasattr(self, "del%s" % attrib.title()): + getattr(self, "del%s" % attrib.title())() + else: + if attrib in self.sub_interfaces: + return self._delSub(attrib) + else: + self._delAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib in self.plugins: + del self.plugins[attrib] + return self + + def __eq__(self, other): + if not isinstance(other, ElementBase): + return False + values = self.getStanzaValues() + for key in other: + if key not in values or values[key] != other[key]: + return False + return True + + def _setAttr(self, name, value): + if value is None or value == '': + self.__delitem__(name) + else: + self.xml.attrib[name] = value + + def _delAttr(self, name): + if name in self.xml.attrib: + del self.xml.attrib[name] + + def _getAttr(self, name): + return self.xml.attrib.get(name, '') + + def _getSubText(self, name): + stanza = self.xml.find("{%s}%s" % (self.namespace, name)) + if stanza is None or stanza.text is None: + return '' + else: + return stanza.text + + def _setSubText(self, name, attrib={}, text=None): + if text is None or text == '': + return self.__delitem__(name) + stanza = self.xml.find("{%s}%s" % (self.namespace, name)) + if stanza is None: + #self.xml.append(ET.Element("{%s}%s" % (self.namespace, name), attrib)) + stanza = ET.Element("{%s}%s" % (self.namespace, name)) + self.xml.append(stanza) + stanza.text = text + return stanza + + def _delSub(self, name): + for child in self.xml.getchildren(): + if child.tag == "{%s}%s" % (self.namespace, name): + self.xml.remove(child) + + def getStanzaValues(self): + out = {} + for interface in self.interfaces: + out[interface] = self[interface] + for pluginkey in self.plugins: + out[pluginkey] = self.plugins[pluginkey].getStanzaValues() + if self.iterables: + iterables = [] + for stanza in self.iterables: + iterables.append(stanza.getStanzaValues()) + iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) + out['substanzas'] = iterables + return out + + def setStanzaValues(self, attrib): + for interface in attrib: + if interface == 'substanzas': + for subdict in attrib['substanzas']: + if '__childtag__' in subdict: + for subclass in self.subitem: + if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): + sub = subclass(parent=self) + sub.setStanzaValues(subdict) + self.iterables.append(sub) + break + elif interface in self.interfaces: + self[interface] = attrib[interface] + elif interface in self.plugin_attrib_map and interface not in self.plugins: + self.initPlugin(interface) + if interface in self.plugins: + self.plugins[interface].setStanzaValues(attrib[interface]) + return self + + def appendxml(self, xml): + self.xml.append(xml) + return self - def __copy__(self): - return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) - - #def __del__(self): #prevents garbage collection of reference cycle - # if self.parent is not None: - # self.parent.xml.remove(self.xml) + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) + + #def __del__(self): #prevents garbage collection of reference cycle + # if self.parent is not None: + # self.parent.xml.remove(self.xml) class StanzaBase(ElementBase): - name = 'stanza' - namespace = 'jabber:client' - interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() + name = 'stanza' + namespace = 'jabber:client' + interfaces = set(('type', 'to', 'from', 'id', 'payload')) + types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + sub_interfaces = tuple() - def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): - self.stream = stream - if stream is not None: - self.namespace = stream.default_ns - ElementBase.__init__(self, xml) - if stype is not None: - self['type'] = stype - if sto is not None: - self['to'] = sto - if sfrom is not None: - self['from'] = sfrom - self.tag = "{%s}%s" % (self.namespace, self.name) - - def setType(self, value): - if value in self.types: - self.xml.attrib['type'] = value - return self + def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): + self.stream = stream + if stream is not None: + self.namespace = stream.default_ns + ElementBase.__init__(self, xml) + if stype is not None: + self['type'] = stype + if sto is not None: + self['to'] = sto + if sfrom is not None: + self['from'] = sfrom + self.tag = "{%s}%s" % (self.namespace, self.name) + + def setType(self, value): + if value in self.types: + self.xml.attrib['type'] = value + return self - def getPayload(self): - return self.xml.getchildren() - - def setPayload(self, value): - self.xml.append(value) - return self - - def delPayload(self): - self.clear() - return self - - def clear(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - for plugin in list(self.plugins.keys()): - del self.plugins[plugin] - return self - - def reply(self): - self['from'], self['to'] = self['to'], self['from'] - self.clear() - return self - - def error(self): - self['type'] = 'error' - return self - - def getTo(self): - return JID(self._getAttr('to')) - - def setTo(self, value): - return self._setAttr('to', str(value)) - - def getFrom(self): - return JID(self._getAttr('from')) - - def setFrom(self, value): - return self._setAttr('from', str(value)) - - def unhandled(self): - pass - - def exception(self, e): - logging.error(traceback.format_tb(e)) - - def send(self): - self.stream.sendRaw(self.__str__()) + def getPayload(self): + return self.xml.getchildren() + + def setPayload(self, value): + self.xml.append(value) + return self + + def delPayload(self): + self.clear() + return self + + def clear(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + for plugin in list(self.plugins.keys()): + del self.plugins[plugin] + return self + + def reply(self): + # if it's a component, use from + if self.stream and hasattr(self.stream, "is_component") and self.stream.is_component: + self['from'], self['to'] = self['to'], self['from'] + else: + self['to'] = self['from'] + del self['from'] + self.clear() + return self + + def error(self): + self['type'] = 'error' + return self + + def getTo(self): + return JID(self._getAttr('to')) + + def setTo(self, value): + return self._setAttr('to', str(value)) + + def getFrom(self): + return JID(self._getAttr('from')) + + def setFrom(self, value): + return self._setAttr('from', str(value)) + + def unhandled(self): + pass + + def exception(self, e): + logging.error(traceback.format_tb(e)) + + def send(self): + self.stream.sendRaw(self.__str__()) - def __copy__(self): - return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) From d0a5c539d8f7cacbcf10730e65695a9dee89fc70 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 23 Jul 2010 19:41:11 -0400 Subject: [PATCH 50/99] Fix shebang lines to use #!/usr/bin/env python instead of hard coding a python version. --- example.py | 1 + sleekxmpp/__init__.py | 2 +- sleekxmpp/componentxmpp.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 972b6d0..4eb88b3 100644 --- a/example.py +++ b/example.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # coding=utf8 import sleekxmpp diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index 3f0a1e8..d2f5765 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/env python """ SleekXMPP: The Sleek XMPP Library diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 818d800..5534a45 100755 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/env python """ SleekXMPP: The Sleek XMPP Library From 07208a3eaf8f9c337971000fa3ac7bcbb6bef5e2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 23 Jul 2010 19:51:41 -0400 Subject: [PATCH 51/99] Fix shebang line for testall.py --- testall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testall.py b/testall.py index bf0b4c7..bb36883 100644 --- a/testall.py +++ b/testall.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/env python import unittest import logging import sys From 73a3d07ad9b9154e6f712012bad489207c9f8e85 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 23 Jul 2010 19:51:41 -0400 Subject: [PATCH 52/99] Fix shebang line for testall.py --- testall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testall.py b/testall.py index bf0b4c7..bb36883 100644 --- a/testall.py +++ b/testall.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/env python import unittest import logging import sys From ec860bf9e29f4a88fc1797faa1dbcf0c0fc3889c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 26 Jul 2010 19:44:42 -0400 Subject: [PATCH 53/99] Add StateManager as replacement for StateMachine. --- sleekxmpp/xmlstream/statemanager.py | 139 ++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 sleekxmpp/xmlstream/statemanager.py diff --git a/sleekxmpp/xmlstream/statemanager.py b/sleekxmpp/xmlstream/statemanager.py new file mode 100644 index 0000000..c7f76e7 --- /dev/null +++ b/sleekxmpp/xmlstream/statemanager.py @@ -0,0 +1,139 @@ +""" + 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 __future__ import with_statement +import threading + + +class StateError(Exception): + """Raised whenever a state transition was attempted but failed.""" + + +class StateManager(object): + """ + At the very core of SleekXMPP there is a need to track various + library configuration settings, XML stream features, and the + network connection status. The state manager is responsible for + tracking this information in a thread-safe manner. + + State 'variables' store the current state of these items as simple + string values or booleans. Changing those values must be done + according to transitions defined when creating the state variable. + + If a state variable is given a value that is not allowed according + to the transition definitions, a StateError is raised. When a + valid value is assigned an event is raised named: + + _state_changed_nameofthestatevariable + + The event carries a dictionary containing the previous and the new + state values. + """ + + def __init__(self, event_func=None): + """ + Initialize the state manager. The parameter event_func should be + the event() method of a SleekXMPP object in order to enable + _state_changed_* events. + """ + self.main_lock = threading.Lock() + self.locks = {} + self.state_variables = {} + + if event_func is not None: + self.event = event_func + else: + self.event = lambda name, data: None + + def add(self, name, default=False, values=None, transitions=None): + """ + Create a new state variable. + + When transitions is specified, only those defined state change + transitions will be allowed. + + When values is specified (and not transitions), any state changes + between those values are allowed. + + If neither values nor transitions are defined, then the state variable + will be a binary switch between True and False. + """ + if name in self.state_variables: + raise IndexError("State variable %s already exists" % name) + + self.locks[name] = threading.Lock() + with self.locks[name]: + var = {'value': default, + 'default': default, + 'transitions': {}} + + if transitions is not None: + for start in transitions: + var['transitions'][start] = set(transitions[start]) + elif values is not None: + values = set(values) + for value in values: + var['transitions'][value] = values + elif values is None: + var['transitions'] = {True: [False], + False: [True]} + + self.state_variables[name] = var + + def addStates(self, var_defs): + """ + Create multiple state variables at once. + """ + for var, data in var_defs: + self.add(var, + default=data.get('default', False), + values=data.get('values', None), + transitions=data.get('transitions', None)) + + def force_set(self, name, val): + """ + Force setting a state variable's value by overriding transition checks. + """ + with self.locks[name]: + self.state_variables[name]['value'] = val + + def reset(self, name): + """ + Reset a state variable to its default value. + """ + with self.locks[name]: + default = self.state_variables[name]['default'] + self.state_variables[name]['value'] = default + + def __getitem__(self, name): + """ + Get the value of a state variable if it exists. + """ + with self.locks[name]: + if name not in self.state_variables: + raise IndexError("State variable %s does not exist" % name) + return self.state_variables[name]['value'] + + def __setitem__(self, name, val): + """ + Attempt to set the value of a state variable, but raise StateError + if the transition is undefined. + + A _state_changed_* event is triggered after a successful transition. + """ + with self.locks[name]: + if name not in self.state_variables: + raise IndexError("State variable %s does not exist" % name) + current = self.state_variables[name]['value'] + if current == val: + return + if val in self.state_variables[name]['transitions'][current]: + self.state_variables[name]['value'] = val + self.event('_state_changed_%s' % name, {'from': current, 'to': val}) + else: + raise StateError("Can not transition from '%s' to '%s'" % (str(current), str(val))) From c8989c04f3675235e3ae730cb240e2154b5d9e76 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 26 Jul 2010 21:02:25 -0400 Subject: [PATCH 54/99] Replaced traceback calls to use logging.exception where applicable. --- sleekxmpp/plugins/xep_0050.py | 1 - sleekxmpp/stanza/rootstanza.py | 2 ++ sleekxmpp/xmlstream/stanzabase.py | 3 +-- sleekxmpp/xmlstream/xmlstream.py | 12 +++++------- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py index 4ff4285..5efb911 100644 --- a/sleekxmpp/plugins/xep_0050.py +++ b/sleekxmpp/plugins/xep_0050.py @@ -9,7 +9,6 @@ from __future__ import with_statement from . import base import logging from xml.etree import cElementTree as ET -import traceback import time class xep_0050(base.base_plugin): diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index e568b62..3bae1f0 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -9,6 +9,7 @@ from .. xmlstream.stanzabase import StanzaBase from xml.etree import cElementTree as ET from . error import Error from .. exceptions import XMPPError +import logging import traceback import sys @@ -29,6 +30,7 @@ class RootStanza(StanzaBase): self['error']['text'] = "SleekXMPP got into trouble." else: self['error']['text'] = traceback.format_tb(e.__traceback__) + logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) self.send() # all jabber:client root stanzas should have the error plugin diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 66a08e4..ab05407 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -7,7 +7,6 @@ """ from xml.etree import cElementTree as ET import logging -import traceback import sys import weakref import copy @@ -400,7 +399,7 @@ class StanzaBase(ElementBase): pass def exception(self, e): - logging.error(traceback.format_tb(e)) + logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) def send(self): self.stream.sendRaw(self.__str__()) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index cea204b..6ce89c2 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -19,7 +19,6 @@ import logging import socket import threading import time -import traceback import types import copy import xml.sax.saxutils @@ -195,14 +194,14 @@ class XMLStream(object): return else: self.state.set('processing', False) - traceback.print_exc() + logging.exception('Socket Error') self.disconnect(reconnect=True) except: if not self.state.reconnect: return else: self.state.set('processing', False) - traceback.print_exc() + logging.exception('Connection error. Reconnecting.') self.disconnect(reconnect=True) if self.state['reconnect']: self.reconnect() @@ -258,8 +257,7 @@ class XMLStream(object): logging.warning("Failed to send %s" % data) self.state.set('connected', False) if self.state.reconnect: - logging.error("Disconnected. Socket Error.") - traceback.print_exc() + logging.exception("Disconnected. Socket Error.") self.disconnect(reconnect=True) def sendRaw(self, data): @@ -344,14 +342,14 @@ class XMLStream(object): try: handler.run(args[0]) except Exception as e: - traceback.print_exc() + logging.exception('Error processing event handler: %s' % handler.name) args[0].exception(e) elif etype == 'schedule': try: logging.debug(args) handler(*args[0]) except: - logging.error(traceback.format_exc()) + logging.exception('Error processing scheduled task') elif etype == 'quit': logging.debug("Quitting eventRunner thread") return False From 2cb82afc2cd35051a89c5d843f13bbf0132e2003 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 26 Jul 2010 18:13:09 -0700 Subject: [PATCH 55/99] updated and moved jid class -- jids now have setters --- sleekxmpp/xmlstream/jid.py | 70 ++++++++++++++++++++++++++++++++++++++ tests/test_jid.py | 26 ++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 sleekxmpp/xmlstream/jid.py create mode 100644 tests/test_jid.py diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py new file mode 100644 index 0000000..2839ba5 --- /dev/null +++ b/sleekxmpp/xmlstream/jid.py @@ -0,0 +1,70 @@ +class JID(object): + def __init__(self, jid): + """Initialize a new jid""" + self.reset(jid) + + def reset(self, jid): + """Start fresh from a new jid string""" + self._full = self._jid = str(jid) + self._domain = None + self._resource = None + self._user = None + self._domain = None + self._bare = None + + def __getattr__(self, name): + """Handle getting the jid values, using cache if available""" + if name == 'resource': + if self._resource is not None: return self._resource + self._resource = self._jid.split('/', 1)[-1] + return self._resource + elif name == 'user': + if self._user is not None: return self._user + if '@' in self._jid: + self._user = self._jid.split('@', 1)[0] + else: + self._user = self._user + return self._user + elif name in ('server', 'domain'): + if self._domain is not None: return self._domain + self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] + return self._domain + elif name == 'full': + return self._jid + elif name == 'bare': + if self._bare is not None: return self._bare + self._bare = self._jid.split('/', 1)[0] + return self._bare + + def __setattr__(self, name, value): + """Edit a jid by updating it's individual values, resetting by a generated jid in the end""" + if name in ('resource', 'user', 'domain'): + object.__setattr__(self, "_%s" % name, value) + self.regenerate() + elif name == 'server': + self.domain = value + elif name in ('full', 'jid'): + self.reset(value) + elif name == 'bare': + if '@' in value: + u, d = value.split('@', 1) + object.__setattr__(self, "_user", u) + object.__setattr__(self, "_domain", d) + else: + object.__setattr__(self, "_domain", value) + self.regenerate() + else: + object.__setattr__(self, name, value) + + + def regenerate(self): + """Generate a new jid based on current values, useful after editing""" + jid = "" + if self.user: jid = "%s@" % self.user + jid += self.domain + if self.resource: jid += "/%s" % self.resource + self.reset(jid) + + def __str__(self): + return self.full + diff --git a/tests/test_jid.py b/tests/test_jid.py new file mode 100644 index 0000000..8b4c976 --- /dev/null +++ b/tests/test_jid.py @@ -0,0 +1,26 @@ +from sleektest import * +from sleekxmpp.xmlstream.jid import JID + +class TestJIDClass(SleekTest): + def testJIDfromfull(self): + j = JID('user@someserver/some/resource') + self.assertEqual(j.user, 'user', "User does not match") + self.assertEqual(j.domain, 'someserver', "Domain does not match") + self.assertEqual(j.resource, 'some/resource', "Resource does not match") + self.assertEqual(j.bare, 'user@someserver', "Bare does not match") + self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match") + self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match") + + def testJIDchange(self): + j = JID('user1@someserver1/some1/resource1') + j.user = 'user' + j.domain = 'someserver' + j.resource = 'some/resource' + self.assertEqual(j.user, 'user', "User does not match") + self.assertEqual(j.domain, 'someserver', "Domain does not match") + self.assertEqual(j.resource, 'some/resource', "Resource does not match") + self.assertEqual(j.bare, 'user@someserver', "Bare does not match") + self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match") + self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass) From a349a2a317a6ca7152e0adb21d5fabbfd632ebaf Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Mon, 26 Jul 2010 18:13:34 -0700 Subject: [PATCH 56/99] removed jid from stanzabase to external file --- sleekxmpp/xmlstream/stanzabase.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 66a08e4..8af90a0 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -11,6 +11,7 @@ import traceback import sys import weakref import copy +from . jid import JID if sys.version_info < (3,0): from . import tostring26 as tostring @@ -29,28 +30,6 @@ def registerStanzaPlugin(stanza, plugin): stanza.plugin_tag_map[tag] = plugin -class JID(object): - def __init__(self, jid): - self.jid = jid - - def __getattr__(self, name): - if name == 'resource': - return self.jid.split('/', 1)[-1] - elif name == 'user': - if '@' in self.jid: - return self.jid.split('@', 1)[0] - else: - return '' - elif name == 'server': - return self.jid.split('@', 1)[-1].split('/', 1)[0] - elif name == 'full': - return self.jid - elif name == 'bare': - return self.jid.split('/', 1)[0] - - def __str__(self): - return self.jid - class ElementBase(tostring.ToString): name = 'stanza' plugin_attrib = 'plugin' From aad185fe29f0dd92388875fa2b64120057de2ea5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 26 Jul 2010 21:38:23 -0400 Subject: [PATCH 57/99] Update test to reflect change in reply() method that removes the from attribute. --- tests/test_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_stream.py b/tests/test_stream.py index eb4aaa5..c2f2667 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -26,7 +26,7 @@ class TestStreamTester(SleekTest): """) self.streamSendMessage(""" - + Thanks for sending: Hi! """) From aa02ecd1544c5fe7f5b3b82aa3b79da0e30da6e3 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 27 Jul 2010 02:07:22 -0400 Subject: [PATCH 58/99] Added notes/ideas/comments on things that can be cleaned/simplified or needs to be expanded before the 1.0 release. --- todo1.0 | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 todo1.0 diff --git a/todo1.0 b/todo1.0 new file mode 100644 index 0000000..5e9a43e --- /dev/null +++ b/todo1.0 @@ -0,0 +1,153 @@ +Atom stanza? Should stanzas for plugins go in sleekxmpp.stanza or sleekxmpp.plugins.stanza? + +sleekxmpp.stanza __all__ out of date? + +stanza.error could be cleaned up, use _delSubText, etc + +stanza.htmlim could use _delSubText, etc. Why not msg['html']['body'] instead of msg['html']['html']? + +Unify timeout values - maybe xmlstream.stanzabase.TIMEOUT? + +Need to use registerStanzaPlugin in rootstanza instead of the manual steps + +Is exceptions.XMPPError raised anywhere? grep says no, is it still needed? + +Extra tostring method in xmlstream. Maybe modify the tostring/ packages to accept a stream and top namespace instead of assuming a stanza object? Change ElementBase __str__ to pass its stream object to new function instead of extending ToString. + +Move tostring and tostring26 to be a single package with import switch in a __init__.py that selects appropriate file to import. + +Remove thread remains from handler classes. + +Clarify what is meant by 'instream' for a handler. + +Allow for XPath matching to use basic [@att="value"] filter. <- eek? Alternative is to use xml mask, but those are longer. + +ElementBase sub_items not subitem? + +Only import cElementTree in stanzabase, everything else reference that. Would make using lxml instead of cElementTree easier if needed. + +*XMPP needs to use JID class instead of lots of fields. + +BaseXMPP set_jid, makeIqQuery, getjidresource, getjidbare not needed + +Is BaseXMPP.send used anywhere? + +Make top level examples folder, put example.py, component_example.py there. Maybe even the disco browser too. + +__getitem__ should be in BaseXMPP. get too, or remove it? + +xmlstreammod in ComponentXMPP can be removed, just use XMLStream. + +Why CamelCase and underscore_names? Document semantics. + +Make the common classes available using __init__.py + +Need to unit test stanzabase/elementbase. Heavily. + +conn_tests and sleekxmpp/tests and sleekxmpp/xmlstresm/test.* -> convert to either unit tests, or at least put in same place + +Update setup.py - github url, version # + + + +-- PEP8 - all files + +Need to use spaces + +Docstrings are lacking. Need to document attributes and return values. + +Organize imports + +Use absolute, not relative imports + +Fix one-liner if statements + +Line length limit of 79 characters + + + +-- Plugins + +--- xep_0004 + +Need more unit tests + +--- xep_0009 + +Need stanza objects + +Need unit tests + +--- xep_0045 + +Need to use stanza objects + +A few TODO comments for checking roles and using defaults + +Need unit tests + +--- xep_0050 + +Need unit tests + +Need stanza objects - use new xep_0004 + +--- xep_0060 + +Need unit tests + +Need to use existing stanza objects + +--- xep_0078 + +Is it useful still? + +Need stanza objects/unit tests + +--- xep_0086 + +Is there a way to automate setting error codes? + +Seems like this should be part of the error stanza by default + +Use stanza objects + +--- xep_0092 + +Stanza objects + +Unit tests + +--- xep_0199 + +Stanza objects + +Unit tests + +Clean commented code + + + +-- Documentation + +Document the Zen/Tao/Whatever of SleekXMPP to explain design goals and decisions + +Write architecture description + +XMPP:TDG needs to be rewritten. + +Need to update docs that reference old JID attributes of sleekxmpp objects + +Page describing new JID class + +Message page needs updating + +Iq page needs to be written + +Make guides to go with example.py and component_example.py + +Page on xmlstream.matchers + +Page on xmlstream.handlers, especially waiters + +Page on using xmlstream.scheduler From bd92ef6acfd9b1ecd390f08b28f1a3f4e7ec69a9 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 28 Jul 2010 13:14:41 -0400 Subject: [PATCH 59/99] Updated RootStanza to use registerStanzaPlugin, and be PEP8 compliant. --- sleekxmpp/stanza/__init__.py | 7 +++- sleekxmpp/stanza/rootstanza.py | 74 +++++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 25 deletions(-) diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index b8fca22..8302c43 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -5,4 +5,9 @@ See the file LICENSE for copying permission. """ -__all__ = ['presence'] + + +from sleekxmpp.stanza.error import Error +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.presence import Presence diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 3bae1f0..fb5498f 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -5,34 +5,60 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from .. exceptions import XMPPError + import logging import traceback import sys +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.stanza import Error +from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase, registerStanzaPlugin + + class RootStanza(StanzaBase): - def exception(self, e): #called when a handler raises an exception - self.reply() - if isinstance(e, XMPPError): # we raised this deliberately - self['error']['condition'] = e.condition - self['error']['text'] = e.text - if e.extension is not None: # extended error tag - extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args) - self['error'].xml.append(extxml) - self['error']['type'] = e.etype - else: # we probably didn't raise this on purpose, so send back a traceback - self['error']['condition'] = 'undefined-condition' - if sys.version_info < (3,0): - self['error']['text'] = "SleekXMPP got into trouble." - else: - self['error']['text'] = traceback.format_tb(e.__traceback__) - logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) - self.send() + """ + A top-level XMPP stanza in an XMLStream. -# all jabber:client root stanzas should have the error plugin -RootStanza.plugin_attrib_map['error'] = Error -RootStanza.plugin_tag_map["{%s}%s" % (Error.namespace, Error.name)] = Error + The RootStanza class provides a more XMPP specific exception + handler than provided by the generic StanzaBase class. + + Methods: + exception -- Overrides StanzaBase.exception + """ + + def exception(self, e): + """ + Create and send an error reply. + + Typically called when an event handler raises an exception. + The error's type and text content are based on the exception + object's type and content. + + Overrides StanzaBase.exception. + + Arguments: + e -- Exception object + """ + self.reply() + if isinstance(e, XMPPError): + # We raised this deliberately + self['error']['condition'] = e.condition + self['error']['text'] = e.text + if e.extension is not None: + # Extended error tag + extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args) + self['error'].append(extxml) + self['error']['type'] = e.etype + else: + # We probably didn't raise this on purpose, so send back a traceback + self['error']['condition'] = 'undefined-condition' + if sys.version_info < (3,0): + self['error']['text'] = "SleekXMPP got into trouble." + else: + self['error']['text'] = traceback.format_tb(e.__traceback__) + logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) + self.send() + + +registerStanzaPlugin(RootStanza, Error) From e8e934fa95ea776d067d875fa67e89cc9e273e90 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 11:02:42 -0400 Subject: [PATCH 60/99] Fixed some PEP8 errors in RootStanza (trailing whitespace and line length) --- sleekxmpp/stanza/rootstanza.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index fb5498f..eafc79a 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -41,23 +41,25 @@ class RootStanza(StanzaBase): e -- Exception object """ self.reply() - if isinstance(e, XMPPError): + if isinstance(e, XMPPError): # We raised this deliberately self['error']['condition'] = e.condition self['error']['text'] = e.text - if e.extension is not None: + if e.extension is not None: # Extended error tag - extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args) + extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), + e.extension_args) self['error'].append(extxml) self['error']['type'] = e.etype - else: - # We probably didn't raise this on purpose, so send back a traceback + else: + # We probably didn't raise this on purpose, so send a traceback self['error']['condition'] = 'undefined-condition' - if sys.version_info < (3,0): + if sys.version_info < (3, 0): self['error']['text'] = "SleekXMPP got into trouble." else: self['error']['text'] = traceback.format_tb(e.__traceback__) - logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) + logging.exception('Error handling {%s}%s stanza' % + (self.namespace, self.name)) self.send() From d148f633f3771c43c94229bfcd2104b65396f5e1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 11:04:21 -0400 Subject: [PATCH 61/99] Modified ElementBase _getSubText, _setSubText, and _delSubText to use the namespace in a tag name if one is given and to use self.namespace otherwise. --- sleekxmpp/xmlstream/stanzabase.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index b8c80ff..5b41a40 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -239,26 +239,31 @@ class ElementBase(tostring.ToString): return self.xml.attrib.get(name, '') def _getSubText(self, name): - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + stanza = self.xml.find(name) if stanza is None or stanza.text is None: return '' else: return stanza.text def _setSubText(self, name, attrib={}, text=None): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) if text is None or text == '': return self.__delitem__(name) - stanza = self.xml.find("{%s}%s" % (self.namespace, name)) + stanza = self.xml.find(name) if stanza is None: - #self.xml.append(ET.Element("{%s}%s" % (self.namespace, name), attrib)) - stanza = ET.Element("{%s}%s" % (self.namespace, name)) + stanza = ET.Element(name) self.xml.append(stanza) stanza.text = text return stanza def _delSub(self, name): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) for child in self.xml.getchildren(): - if child.tag == "{%s}%s" % (self.namespace, name): + if child.tag == name: self.xml.remove(child) def getStanzaValues(self): From 25f43bd21906cb13df137b926dc62ca23bd97df0 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 11:06:10 -0400 Subject: [PATCH 62/99] Updated error stanza to be PEP8 compliant and include documentation. --- sleekxmpp/stanza/error.py | 175 ++++++++++++++++++++++++++------------ 1 file changed, 122 insertions(+), 53 deletions(-) diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 7771c87..3253d9c 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -5,58 +5,127 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET + +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class Error(ElementBase): - namespace = 'jabber:client' - name = 'error' - plugin_attrib = 'error' - conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'internal-server-error', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'resource-constraint', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request')) - interfaces = set(('code', 'condition', 'text', 'type')) - types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) - sub_interfaces = set(('text',)) - condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' - - def setup(self, xml=None): - if ElementBase.setup(self, xml): #if we had to generate xml - self['type'] = 'cancel' - self['condition'] = 'feature-not-implemented' - if self.parent is not None: - self.parent()['type'] = 'error' - - def getCondition(self): - for child in self.xml.getchildren(): - if "{%s}" % self.condition_ns in child.tag: - return child.tag.split('}', 1)[-1] - return '' - - def setCondition(self, value): - if value in self.conditions: - for child in self.xml.getchildren(): - if "{%s}" % self.condition_ns in child.tag: - self.xml.remove(child) - condition = ET.Element("{%s}%s" % (self.condition_ns, value)) - self.xml.append(condition) - return self - - def delCondition(self): - return self - - def getText(self): - text = '' - textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text") - if textxml is not None: - text = textxml.text - return text - - def setText(self, value): - self.delText() - textxml = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}text') - textxml.text = value - self.xml.append(textxml) - return self - - def delText(self): - textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text") - if textxml is not None: - self.xml.remove(textxml) + + """ + XMPP stanzas of type 'error' should include an stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + Example error stanza: + + + + The item was not found. + + + + Stanza Interface: + code -- The error code used in older XMPP versions. + condition -- The name of the condition element. + text -- Human readable description of the error. + type -- Error type indicating how the error should be handled. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + types -- A set of values indicating how the error + should be treated. + + Methods: + setup -- Overrides ElementBase.setup. + getCondition -- Retrieve the name of the condition element. + setCondition -- Add a condition element. + delCondition -- Remove the condition element. + getText -- Retrieve the contents of the element. + setText -- Set the contents of the element. + delText -- Remove the element. + """ + + namespace = 'jabber:client' + name = 'error' + plugin_attrib = 'error' + interfaces = set(('code', 'condition', 'text', 'type')) + sub_interfaces = set(('text',)) + conditions = set(('bad-request', 'conflict', 'feature-not-implemented', + 'forbidden', 'gone', 'internal-server-error', + 'item-not-found', 'jid-malformed', 'not-acceptable', + 'not-allowed', 'not-authorized', 'payment-required', + 'recipient-unavailable', 'redirect', + 'registration-required', 'remote-server-not-found', + 'remote-server-timeout', 'resource-constraint', + 'service-unavailable', 'subscription-required', + 'undefined-condition', 'unexpected-request')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' + types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup. + + Sets a default error type and condition, and changes the + parent stanza's type to 'error'. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + if ElementBase.setup(self, xml): + #If we had to generate XML then set default values. + self['type'] = 'cancel' + self['condition'] = 'feature-not-implemented' + if self.parent is not None: + self.parent()['type'] = 'error' + + def getCondition(self): + """Return the condition element's name.""" + for child in self.xml.getchildren(): + if "{%s}" % self.condition_ns in child.tag: + return child.tag.split('}', 1)[-1] + return '' + + def setCondition(self, value): + """ + Set the tag name of the condition element. + + Arguments: + value -- The tag name of the condition element. + """ + if value in self.conditions: + del self['condition'] + self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value))) + return self + + def delCondition(self): + """Remove the condition element.""" + for child in self.xml.getchildren(): + if "{%s}" % self.condition_ns in child.tag: + self.xml.remove(child) + return self + + def getText(self): + """Retrieve the contents of the element.""" + return self._getSubText('{%s}text' % self.condition_ns) + + def setText(self, value): + """ + Set the contents of the element. + + Arguments: + value -- The new contents for the element. + """ + self._setSubText('{%s}text' % self.condition_ns, text=value) + return self + + def delText(self): + """Remove the element.""" + self._delSub('{%s}text' % self.condition_ns) + return self From a49f511a2f844bbab261b5ce84257f026e1eb37c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 20:16:57 -0400 Subject: [PATCH 63/99] Added RESPONSE_TIMEOUT constant to sleekxmpp.xmlstream to serve as a single place to specify a default timeout value when waiting for a stanza response. --- sleekxmpp/xmlstream/xmlstream.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6ce89c2..94fed64 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -24,6 +24,7 @@ import copy import xml.sax.saxutils from . import scheduler +RESPONSE_TIMEOUT = 10 HANDLER_THREADS = 1 ssl_support = True From 60a183b011f8c0ee5a3c1840075a43addf33fc4f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 20:18:04 -0400 Subject: [PATCH 64/99] Added useful imports to the xmlstream, xmlstream.handler, and xmlstream.matcher __init__.py files to make it simpler to import common classes. --- sleekxmpp/xmlstream/__init__.py | 10 ++++++++++ sleekxmpp/xmlstream/handler/__init__.py | 12 ++++++++++++ sleekxmpp/xmlstream/matcher/__init__.py | 13 +++++++++++++ 3 files changed, 35 insertions(+) diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py index e69de29..2611896 100644 --- a/sleekxmpp/xmlstream/__init__.py +++ b/sleekxmpp/xmlstream/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase +from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py index e69de29..50e286a 100644 --- a/sleekxmpp/xmlstream/handler/__init__.py +++ b/sleekxmpp/xmlstream/handler/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.handler.waiter import Waiter +from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback +from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py index e69de29..91cb8d6 100644 --- a/sleekxmpp/xmlstream/matcher/__init__.py +++ b/sleekxmpp/xmlstream/matcher/__init__.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.matcher.id import MatcherId +from sleekxmpp.xmlstream.matcher.many import MatchMany +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath +from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath From a96a046e27289d733eabe0cb1b902e679da5d4ca Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 23:15:49 -0400 Subject: [PATCH 65/99] Remove extra debugging lines and speed up stream testing in SleekTest. --- tests/sleektest.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index f89966e..71ff105 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -128,10 +128,7 @@ class SleekTest(unittest.TestCase): if xml.attrib.get('type', None) is None: xml.attrib['type'] = 'normal' msg2['type'] = msg2['type'] - debug += ">>>>Given Stanza:\n%s\n" % ET.tostring(msg.xml) debug += "XML String:\n%s\n" % ET.tostring(xml) - debug += ">>>>Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) - values = msg2.getStanzaValues() msg3 = self.Message() @@ -234,25 +231,25 @@ class SleekTest(unittest.TestCase): self.xmpp.process(threaded=True) if skip: # Clear startup stanzas - self.xmpp.socket.nextSent(timeout=0.1) + self.xmpp.socket.nextSent(timeout=0.01) def streamRecv(self, data): data = str(data) self.xmpp.socket.recvData(data) - def streamSendMessage(self, data, use_values=True, timeout=.5): + def streamSendMessage(self, data, use_values=True, timeout=.1): if isinstance(data, str): data = self.Message(xml=ET.fromstring(data)) - sent = self.xmpp.socket.nextSent(timeout=1) + sent = self.xmpp.socket.nextSent(timeout) self.checkMessage(data, sent, use_values) - def streamSendIq(self, data, use_values=True, timeout=.5): + def streamSendIq(self, data, use_values=True, timeout=.1): if isinstance(data, str): data = self.Iq(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) self.checkIq(data, sent, use_values) - def streamSendPresence(self, data, use_values=True, timeout=.5): + def streamSendPresence(self, data, use_values=True, timeout=.1): if isinstance(data, str): data = self.Presence(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) From 1da3e5b35eb59909d4d6903b1c0190a7aad98a30 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 23:55:13 -0400 Subject: [PATCH 66/99] Added unit tests for error stanzas. Corrected error in deleting conditions. --- sleekxmpp/stanza/error.py | 4 ++- tests/test_errorstanzas.py | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/test_errorstanzas.py diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 3253d9c..6d18c29 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -108,7 +108,9 @@ class Error(ElementBase): """Remove the condition element.""" for child in self.xml.getchildren(): if "{%s}" % self.condition_ns in child.tag: - self.xml.remove(child) + tag = child.tag.split('}', 1)[-1] + if tag in self.conditions: + self.xml.remove(child) return self def getText(self): diff --git a/tests/test_errorstanzas.py b/tests/test_errorstanzas.py new file mode 100644 index 0000000..788d6c1 --- /dev/null +++ b/tests/test_errorstanzas.py @@ -0,0 +1,56 @@ +from sleektest import * + +class TestErrorStanzas(SleekTest): + + def testSetup(self): + """Test setting initial values in error stanza.""" + msg = self.Message() + msg.enable('error') + self.checkMessage(msg, """ + + + + + + """) + + def testCondition(self): + """Test modifying the error condition.""" + msg = self.Message() + msg['error']['condition'] = 'item-not-found' + + self.checkMessage(msg, """ + + + + + + """) + + self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.") + + del msg['error']['condition'] + + self.checkMessage(msg, """ + + + + """) + + def testDelCondition(self): + """Test that deleting error conditions doesn't remove extra elements.""" + msg = self.Message() + msg['error']['text'] = 'Error!' + msg['error']['condition'] = 'internal-server-error' + + del msg['error']['condition'] + + self.checkMessage(msg, """ + + + Error! + + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas) From cbed8029bad691f8353854dc264f83303a196a09 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 29 Jul 2010 23:58:25 -0400 Subject: [PATCH 67/99] Updated, cleaned, and documented Iq stanza class. Also added unit tests. --- sleekxmpp/stanza/iq.py | 231 ++++++++++++++++++++++++++++------------ tests/test_iqstanzas.py | 93 ++++++++++++++++ 2 files changed, 257 insertions(+), 67 deletions(-) create mode 100644 tests/test_iqstanzas.py diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index daf05b4..c735ae6 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -5,73 +5,170 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from .. xmlstream.handler.waiter import Waiter -from .. xmlstream.matcher.id import MatcherId -from . rootstanza import RootStanza + +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream import RESPONSE_TIMEOUT +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.xmlstream.handler import Waiter +from sleekxmpp.xmlstream.matcher import MatcherId + class Iq(RootStanza): - interfaces = set(('type', 'to', 'from', 'id','query')) - types = set(('get', 'result', 'set', 'error')) - name = 'iq' - plugin_attrib = name - namespace = 'jabber:client' - def __init__(self, *args, **kwargs): - StanzaBase.__init__(self, *args, **kwargs) - if self['id'] == '': - if self.stream is not None: - self['id'] = self.stream.getNewId() - else: - self['id'] = '0' - - def unhandled(self): - if self['type'] in ('get', 'set'): - self.reply() - self['error']['condition'] = 'feature-not-implemented' - self['error']['text'] = 'No handlers registered for this request.' - self.send() - - def setPayload(self, value): - self.clear() - StanzaBase.setPayload(self, value) - return self - - def setQuery(self, value): - query = self.xml.find("{%s}query" % value) - if query is None and value: - self.clear() - query = ET.Element("{%s}query" % value) - self.xml.append(query) - return self - - def getQuery(self): - for child in self.xml.getchildren(): - if child.tag.endswith('query'): - ns =child.tag.split('}')[0] - if '{' in ns: - ns = ns[1:] - return ns - return '' - - def reply(self): - self['type'] = 'result' - StanzaBase.reply(self) - return self - - def delQuery(self): - for child in self.getchildren(): - if child.tag.endswith('query'): - self.xml.remove(child) - return self - - def send(self, block=True, timeout=10): - if block and self['type'] in ('get', 'set'): - waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) - self.stream.registerHandler(waitfor) - StanzaBase.send(self) - return waitfor.wait(timeout) - else: - return StanzaBase.send(self) + """ + XMPP stanzas, or info/query stanzas, are XMPP's method of + requesting and modifying information, similar to HTTP's GET and + POST methods. + + Each stanza must have an 'id' value which associates the + stanza with the response stanza. XMPP entities must always + be given a response stanza with a type of 'result' after + sending a stanza of type 'get' or 'set'. + + Most uses cases for stanzas will involve adding a + element whose namespace indicates the type of information + desired. However, some custom XMPP applications use stanzas + as a carrier stanza for an application-specific protocol instead. + + Example Stanzas: + + + + + + + + Friends + + + + + Stanza Interface: + query -- The namespace of the element if one exists. + + Methods: + __init__ -- Overrides StanzaBase.__init__. + unhandled -- Send error if there are no handlers. + setPayload -- Overrides StanzaBase.setPayload. + setQuery -- Add or modify a element. + getQuery -- Return the namespace of the element. + delQuery -- Remove the element. + reply -- Overrides StanzaBase.reply + send -- Overrides StanzaBase.send + """ + + namespace = 'jabber:client' + name = 'iq' + interfaces = set(('type', 'to', 'from', 'id', 'query')) + types = set(('get', 'result', 'set', 'error')) + plugin_attrib = name + + def __init__(self, *args, **kwargs): + """ + Initialize a new stanza with an 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None: + self['id'] = self.stream.getNewId() + else: + self['id'] = '0' + + def unhandled(self): + """ + Send a feature-not-implemented error if the stanza is not handled. + + Overrides StanzaBase.unhandled. + """ + if self['type'] in ('get', 'set'): + self.reply() + self['error']['condition'] = 'feature-not-implemented' + self['error']['text'] = 'No handlers registered for this request.' + self.send() + + def setPayload(self, value): + """ + Set the XML contents of the stanza. + + Arguments: + value -- An XML object to use as the stanza's contents + """ + self.clear() + StanzaBase.setPayload(self, value) + return self + + def setQuery(self, value): + """ + Add or modify a element. + + Query elements are differentiated by their namespace. + + Arguments: + value -- The namespace of the element. + """ + query = self.xml.find("{%s}query" % value) + if query is None and value: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) + return self + + def getQuery(self): + """Return the namespace of the element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + ns = child.tag.split('}')[0] + if '{' in ns: + ns = ns[1:] + return ns + return '' + + def delQuery(self): + """Remove the element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + self.xml.remove(child) + return self + + def reply(self): + """ + Send a reply stanza. + + Overrides StanzaBase.reply + + Sets the 'type' to 'result' in addition to the default + StanzaBase.reply behavior. + """ + self['type'] = 'result' + StanzaBase.reply(self) + return self + + def send(self, block=True, timeout=RESPONSE_TIMEOUT): + """ + Send an 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. + + 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 + """ + if block and self['type'] in ('get', 'set'): + waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) + self.stream.registerHandler(waitfor) + StanzaBase.send(self) + return waitfor.wait(timeout) + else: + return StanzaBase.send(self) diff --git a/tests/test_iqstanzas.py b/tests/test_iqstanzas.py new file mode 100644 index 0000000..9ae5929 --- /dev/null +++ b/tests/test_iqstanzas.py @@ -0,0 +1,93 @@ +from sleektest import * +from sleekxmpp.xmlstream.stanzabase import ET + + +class TestIqStanzas(SleekTest): + + def setUp(self): + """Start XML stream for testing.""" + self.streamStart() + + def tearDown(self): + """Shutdown the XML stream after testing.""" + self.streamClose() + + def testSetup(self): + """Test initializing default Iq values.""" + iq = self.Iq() + self.checkIq(iq, """ + + """) + + def testPayload(self): + """Test setting Iq stanza payload.""" + iq = self.Iq() + iq.setPayload(ET.Element('{test}tester')) + self.checkIq(iq, """ + + + + """, use_values=False) + + + def testUnhandled(self): + """Test behavior for Iq.unhandled.""" + self.streamRecv(""" + + + + """) + + iq = self.Iq() + iq['id'] = 'test' + iq['error']['condition'] = 'feature-not-implemented' + iq['error']['text'] = 'No handlers registered for this request.' + + self.streamSendIq(iq, """ + + + + + No handlers registered for this request. + + + + """) + + def testQuery(self): + """Test modifying query element of Iq stanzas.""" + iq = self.Iq() + + iq['query'] = 'query_ns' + self.checkIq(iq, """ + + + + """) + + iq['query'] = 'query_ns2' + self.checkIq(iq, """ + + + + """) + + self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") + + del iq['query'] + self.checkIq(iq, """ + + """) + + def testReply(self): + """Test setting proper result type in Iq replies.""" + iq = self.Iq() + iq['to'] = 'user@localhost' + iq['type'] = 'get' + iq.reply() + + self.checkIq(iq, """ + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas) From 1cedea280499c0b7f7debb6c95442038d8f2d89c Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Jul 2010 14:11:24 -0400 Subject: [PATCH 68/99] Added optional default value to _getAttr. --- sleekxmpp/stanza/iq.py | 3 +++ sleekxmpp/xmlstream/stanzabase.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index c735ae6..c5ef8bb 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -49,6 +49,9 @@ class Iq(RootStanza): Stanza Interface: query -- The namespace of the element if one exists. + Attributes: + types -- May be one of: get, set, result, or error. + Methods: __init__ -- Overrides StanzaBase.__init__. unhandled -- Send error if there are no handlers. diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 5b41a40..94ff958 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -235,8 +235,8 @@ class ElementBase(tostring.ToString): if name in self.xml.attrib: del self.xml.attrib[name] - def _getAttr(self, name): - return self.xml.attrib.get(name, '') + def _getAttr(self, name, default=''): + return self.xml.attrib.get(name, default) def _getSubText(self, name): if '}' not in name: From ecde696468cb3b661dfdf5226c186403321d905f Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 3 Aug 2010 07:37:58 +0000 Subject: [PATCH 69/99] temporary disabled testall methodlength until pep-8 conversion is done --- testall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testall.py b/testall.py index bb36883..d3d049e 100644 --- a/testall.py +++ b/testall.py @@ -21,7 +21,7 @@ class testoverall(unittest.TestCase): self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp')) #raise "Help!" - def testMethodLength(self): + def disabled_testMethodLength(self): """Testing for excessive method lengths""" import re dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp') From 851e90c572d9c2a5dd72a8643920c59d4d1493ae Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Tue, 3 Aug 2010 07:51:52 +0000 Subject: [PATCH 70/99] added dnspython.org to requirements in README --- README | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README b/README index abc2d09..cc783b6 100644 --- a/README +++ b/README @@ -4,6 +4,11 @@ Hosted at http://wiki.github.com/fritzy/SleekXMPP/ Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre If you're coming here from The Definitive Guide, please read http://wiki.github.com/fritzy/SleekXMPP/xmpp-the-definitive-guide +Requirements: +We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required. +If you do not install this library, you main need to specify server/port for services that use SRV records (like GTalk). +"sudo pip install dnspython" on a *nix system with pip installed. + SleekXMPP has several design goals/philosophies: - Low number of dependencies. - Every XEP as a plugin. From 939ae298c2856f095526a9e0f52216e9dc4e7db1 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 12:19:45 -0400 Subject: [PATCH 71/99] Updated message stanzas and tests with documentation and PEP8 style. --- sleekxmpp/stanza/message.py | 186 +++++++++++++++++++++++++---------- tests/test_messagestanzas.py | 73 +++++++------- 2 files changed, 170 insertions(+), 89 deletions(-) diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 75ecc23..560e1d4 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -5,59 +5,139 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import StanzaBase -from xml.etree import cElementTree as ET -from . error import Error -from . rootstanza import RootStanza + +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET + class Message(RootStanza): - interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick')) - types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) - sub_interfaces = set(('body', 'subject')) - name = 'message' - plugin_attrib = name - namespace = 'jabber:client' - def getType(self): - return self.xml.attrib.get('type', 'normal') - - def chat(self): - self['type'] = 'chat' - return self - - def normal(self): - self['type'] = 'normal' - return self - - def reply(self, body=None): - StanzaBase.reply(self) - if self['type'] == 'groupchat': - self['to'] = self['to'].bare - del self['id'] - if body is not None: - self['body'] = body - return self - - def getMucroom(self): - if self['type'] == 'groupchat': - return self['from'].bare - else: - return '' - - def setMucroom(self, value): - pass - - def delMucroom(self): - pass - - def getMucnick(self): - if self['type'] == 'groupchat': - return self['from'].resource - else: - return '' - - def setMucnick(self, value): - pass - - def delMucnick(self): - pass + """ + XMPP's stanzas are a "push" mechanism to send information + to other XMPP entities without requiring a response. + + Chat clients will typically use stanzas that have a type + of either "chat" or "groupchat". + + When handling a message event, be sure to check if the message is + an error response. + + Example stanzas: + + Hi! + + + + Hi everyone! + + + Stanza Interface: + body -- The main contents of the message. + subject -- An optional description of the message's contents. + mucroom -- (Read-only) The name of the MUC room that sent the message. + mucnick -- (Read-only) The MUC nickname of message's sender. + + Attributes: + types -- May be one of: normal, chat, headline, groupchat, or error. + + Methods: + chat -- Set the message type to 'chat'. + normal -- Set the message type to 'normal'. + reply -- Overrides StanzaBase.reply + getType -- Overrides StanzaBase interface + getMucroom -- Return the name of the MUC room of the message. + setMucroom -- Dummy method to prevent assignment. + delMucroom -- Dummy method to prevent deletion. + getMucnick -- Return the MUC nickname of the message's sender. + setMucnick -- Dummy method to prevent assignment. + delMucnick -- Dummy method to prevent deletion. + """ + + namespace = 'jabber:client' + name = 'message' + interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', + 'mucroom', 'mucnick')) + sub_interfaces = set(('body', 'subject')) + plugin_attrib = name + types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) + + def getType(self): + """ + Return the message type. + + Overrides default stanza interface behavior. + + Returns 'normal' if no type attribute is present. + """ + return self._getAttr('type', 'normal') + + def chat(self): + """Set the message type to 'chat'.""" + self['type'] = 'chat' + return self + + def normal(self): + """Set the message type to 'chat'.""" + self['type'] = 'normal' + return self + + def reply(self, body=None): + """ + Create a message reply. + + Overrides StanzaBase.reply. + + Sets proper 'to' attribute if the message is from a MUC, and + adds a message body if one is given. + + Arguments: + body -- Optional text content for the message. + """ + StanzaBase.reply(self) + if self['type'] == 'groupchat': + self['to'] = self['to'].bare + + del self['id'] + + if body is not None: + self['body'] = body + return self + + def getMucroom(self): + """ + Return the name of the MUC room where the message originated. + + Read-only stanza interface. + """ + if self['type'] == 'groupchat': + return self['from'].bare + else: + return '' + + def getMucnick(self): + """ + Return the nickname of the MUC user that sent the message. + + Read-only stanza interface. + """ + if self['type'] == 'groupchat': + return self['from'].resource + else: + return '' + + def setMucroom(self, value): + """Dummy method to prevent modification.""" + pass + + def delMucroom(self): + """Dummy method to prevent deletion.""" + pass + + def setMucnick(self, value): + """Dummy method to prevent modification.""" + pass + + def delMucnick(self): + """Dummy method to prevent deletion.""" + pass diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index c83b59a..f55211d 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -2,44 +2,45 @@ from sleektest import * from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.htmlim import HTMLIM + class TestMessageStanzas(SleekTest): - def setUp(self): - registerStanzaPlugin(Message, HTMLIM) - - def testGroupchatReplyRegression(self): - "Regression groupchat reply should be to barejid" - msg = self.Message() - msg['to'] = 'me@myserver.tld' - msg['from'] = 'room@someservice.someserver.tld/somenick' - msg['type'] = 'groupchat' - msg['body'] = "this is a message" - msg.reply() - self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') + def setUp(self): + registerStanzaPlugin(Message, HTMLIM) - def testAttribProperty(self): - "Test attrib property returning self" - msg = self.Message() - msg.attrib.attrib.attrib['to'] = 'usr@server.tld' - self.failUnless(str(msg['to']) == 'usr@server.tld') - - def testHTMLPlugin(self): - "Test message/html/html stanza" - msg = self.Message() - msg['to'] = "fritzy@netflint.net/sleekxmpp" - msg['body'] = "this is the plaintext message" - msg['type'] = 'chat' - p = ET.Element('{http://www.w3.org/1999/xhtml}p') - p.text = "This is the htmlim message" - msg['html']['html'] = p - self.checkMessage(msg, """ - - this is the plaintext message - - -

This is the htmlim message

- - -
""") + def testGroupchatReplyRegression(self): + "Regression groupchat reply should be to barejid" + msg = self.Message() + msg['to'] = 'me@myserver.tld' + msg['from'] = 'room@someservice.someserver.tld/somenick' + msg['type'] = 'groupchat' + msg['body'] = "this is a message" + msg.reply() + self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') + + def testAttribProperty(self): + "Test attrib property returning self" + msg = self.Message() + msg.attrib.attrib.attrib['to'] = 'usr@server.tld' + self.failUnless(str(msg['to']) == 'usr@server.tld') + + def testHTMLPlugin(self): + "Test message/html/html stanza" + msg = self.Message() + msg['to'] = "fritzy@netflint.net/sleekxmpp" + msg['body'] = "this is the plaintext message" + msg['type'] = 'chat' + p = ET.Element('{http://www.w3.org/1999/xhtml}p') + p.text = "This is the htmlim message" + msg['html']['html'] = p + self.checkMessage(msg, """ + + this is the plaintext message + + +

This is the htmlim message

+ + +
""") suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) From 41ab2b84604849d0e650ecd833554b3488733785 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 17:30:34 -0400 Subject: [PATCH 72/99] Updated presence stanza with documentation and PEP8 style. --- sleekxmpp/stanza/presence.py | 177 +++++++++++++++++++++++++--------- tests/test_presencestanzas.py | 88 +++++++++-------- 2 files changed, 178 insertions(+), 87 deletions(-) diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index ec68176..651bf34 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -6,54 +6,141 @@ See the file LICENSE for copying permission. """ -from . error import Error -from . rootstanza import RootStanza -from .. xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.stanza import Error +from sleekxmpp.stanza.rootstanza import RootStanza +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET class Presence(RootStanza): - interfaces = set(('type', 'to', 'from', 'id', 'show', 'status', 'priority')) - types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed')) - showtypes = set(('dnd', 'chat', 'xa', 'away')) - sub_interfaces = set(('show', 'status', 'priority')) - name = 'presence' - plugin_attrib = name - namespace = 'jabber:client' - def setShow(self, show): - if show in self.showtypes: - self._setSubText('show', text=show) - return self + """ + XMPP's stanza allows entities to know the status of other + clients and components. Since it is currently the only multi-cast + stanza in XMPP, many extensions add more information to + stanzas to broadcast to every entry in the roster, such as + capabilities, music choices, or locations (XEP-0115: Entity Capabilities + and XEP-0163: Personal Eventing Protocol). - def setType(self, value): - if value in self.types: - self['show'] = None - if value == 'available': - value = '' - self._setAttr('type', value) - elif value in self.showtypes: - self['show'] = value - return self + Since stanzas are broadcast when an XMPP entity changes + its status, the bulk of the traffic in an XMPP network will be from + stanzas. Therefore, do not include more information than + necessary in a status message or within a stanza in order + to help keep the network running smoothly. - def setPriority(self, value): - self._setSubText('priority', text = str(value)) - - def getPriority(self): - p = self._getSubText('priority') - if not p: p = 0 - return int(p) - - def getType(self): - out = self._getAttr('type') - if not out: - out = self['show'] - if not out or out is None: - out = 'available' - return out - - def reply(self): - if self['type'] == 'unsubscribe': - self['type'] = 'unsubscribed' - elif self['type'] == 'subscribe': - self['type'] = 'subscribed' - return StanzaBase.reply(self) + Example stanzas: + + + + away + Getting lunch. + 5 + + + + + + + Stanza Interface: + priority -- A value used by servers to determine message routing. + show -- The type of status, such as away or available for chat. + status -- Custom, human readable status message. + + Attributes: + types -- One of: available, unavailable, error, probe, + subscribe, subscribed, unsubscribe, + and unsubscribed. + showtypes -- One of: away, chat, dnd, and xa. + + Methods: + reply -- Overrides StanzaBase.reply + setShow -- Set the value of the element. + getType -- Get the value of the type attribute or element. + setType -- Set the value of the type attribute or element. + getPriority -- Get the value of the element. + setPriority -- Set the value of the element. + """ + + namespace = 'jabber:client' + name = 'presence' + interfaces = set(('type', 'to', 'from', 'id', 'show', + 'status', 'priority')) + sub_interfaces = set(('show', 'status', 'priority')) + plugin_attrib = name + + types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', + 'subscribed', 'unsubscribe', 'unsubscribed')) + showtypes = set(('dnd', 'chat', 'xa', 'away')) + + def setShow(self, show): + """ + Set the value of the element. + + Arguments: + show -- Must be one of: away, chat, dnd, or xa. + """ + if show in self.showtypes: + self._setSubText('show', text=show) + return self + + def setType(self, value): + """ + Set the type attribute's value, and the element + if applicable. + + Arguments: + value -- Must be in either self.types or self.showtypes. + """ + if value in self.types: + self['show'] = None + if value == 'available': + value = '' + self._setAttr('type', value) + elif value in self.showtypes: + self['show'] = value + return self + + def setPriority(self, value): + """ + Set the entity's priority value. Some server use priority to + determine message routing behavior. + + Bot clients should typically use a priority of 0 if the same + JID is used elsewhere by a human-interacting client. + + Arguments: + value -- An integer value greater than or equal to 0. + """ + self._setSubText('priority', text=str(value)) + + def getPriority(self): + """ + Return the value of the element as an integer. + """ + p = self._getSubText('priority') + if not p: + p = 0 + return int(p) + + def getType(self): + """ + Return the value of the stanza's type attribute, or + the value of the element. + """ + out = self._getAttr('type') + if not out: + out = self['show'] + if not out or out is None: + out = 'available' + return out + + def reply(self): + """ + Set the appropriate presence reply type. + + Overrides StanzaBase.reply. + """ + if self['type'] == 'unsubscribe': + self['type'] = 'unsubscribed' + elif self['type'] == 'subscribe': + self['type'] = 'subscribed' + return StanzaBase.reply(self) diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index 02799c8..2cab3af 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -2,52 +2,56 @@ import sleekxmpp from sleektest import * from sleekxmpp.stanza.presence import Presence + class TestPresenceStanzas(SleekTest): - - def testPresenceShowRegression(self): - """Regression check presence['type'] = 'dnd' show value working""" - p = self.Presence() - p['type'] = 'dnd' - self.checkPresence(p, """ - dnd - """) - def testPresenceType(self): - """Test manipulating presence['type']""" - p = self.Presence() - p['type'] = 'available' - self.checkPresence(p, """ - - """) - self.failUnless(p['type'] == 'available', "Incorrect presence['type'] for type 'available'") + def testPresenceShowRegression(self): + """Regression check presence['type'] = 'dnd' show value working""" + p = self.Presence() + p['type'] = 'dnd' + self.checkPresence(p, "dnd") - for showtype in ['away', 'chat', 'dnd', 'xa']: - p['type'] = showtype - self.checkPresence(p, """ - %s - """ % showtype) - self.failUnless(p['type'] == showtype, "Incorrect presence['type'] for type '%s'" % showtype) + def testPresenceType(self): + """Test manipulating presence['type']""" + p = self.Presence() + p['type'] = 'available' + self.checkPresence(p, "") + self.failUnless(p['type'] == 'available', + "Incorrect presence['type'] for type 'available'") - p['type'] = None - self.checkPresence(p, """ - - """) + for showtype in ['away', 'chat', 'dnd', 'xa']: + p['type'] = showtype + self.checkPresence(p, """ + %s + """ % showtype) + self.failUnless(p['type'] == showtype, + "Incorrect presence['type'] for type '%s'" % showtype) + + p['type'] = None + self.checkPresence(p, "") + + def testPresenceUnsolicitedOffline(self): + """ + Unsolicted offline presence does not spawn changed_status + or update the roster. + """ + p = self.Presence() + p['type'] = 'unavailable' + p['from'] = 'bill@chadmore.com/gmail15af' + + c = sleekxmpp.ClientXMPP('crap@wherever', 'password') + happened = [] + + def handlechangedpresence(event): + happened.append(True) + + c.add_event_handler("changed_status", handlechangedpresence) + c._handlePresence(p) + + self.failUnless(happened == [], + "changed_status event triggered for extra unavailable presence") + self.failUnless(c.roster == {}, + "Roster updated for superfulous unavailable presence") - def testPresenceUnsolicitedOffline(self): - """Unsolicted offline presence does not spawn changed_status or update roster""" - p = self.Presence() - p['type'] = 'unavailable' - p['from'] = 'bill@chadmore.com/gmail15af' - - c = sleekxmpp.ClientXMPP('crap@wherever', 'password') - happened = [] - def handlechangedpresence(event): - happened.append(True) - c.add_event_handler("changed_status", handlechangedpresence) - c._handlePresence(p) - - self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence") - self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence") - suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) From 18683d2b7559cafc0d102f1facfc05aebfabcdcd Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 17:32:12 -0400 Subject: [PATCH 73/99] Fixed typo in README --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index cc783b6..da9fe8c 100644 --- a/README +++ b/README @@ -6,7 +6,7 @@ If you're coming here from The Definitive Guide, please read http://wiki.github. Requirements: We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required. -If you do not install this library, you main need to specify server/port for services that use SRV records (like GTalk). +If you do not install this library, you may need to specify the server/port for services that use SRV records (like GTalk). "sudo pip install dnspython" on a *nix system with pip installed. SleekXMPP has several design goals/philosophies: From 183a3f1b87a8f5a6c4e174dec6de513a0bb5cb83 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 17:58:18 -0400 Subject: [PATCH 74/99] Updated XHTML-IM stanza with documentation and PEP8 style. --- sleekxmpp/stanza/htmlim.py | 95 ++++++++++++++++++++++++++---------- tests/test_messagestanzas.py | 2 +- 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 50195b1..c2f2f0c 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -5,31 +5,76 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET + +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class HTMLIM(ElementBase): - namespace = 'http://jabber.org/protocol/xhtml-im' - name = 'html' - plugin_attrib = 'html' - interfaces = set(('html',)) - plugin_attrib_map = set() - plugin_xml_map = set() - def setHtml(self, html): - if isinstance(html, str): - html = ET.XML(html) - if html.tag != '{http://www.w3.org/1999/xhtml}body': - body = ET.Element('{http://www.w3.org/1999/xhtml}body') - body.append(html) - self.xml.append(body) - else: - self.xml.append(html) - - def getHtml(self): - html = self.xml.find('{http://www.w3.org/1999/xhtml}body') - if html is None: return '' - return html - - def delHtml(self): - if self.parent is not None: - self.parent().xml.remove(self.xml) + """ + XEP-0071: XHTML-IM defines a method for embedding XHTML content + within a stanza so that lightweight markup can be used + to format the message contents and to create links. + + Only a subset of XHTML is recommended for use with XHTML-IM. + See the full spec at 'http://xmpp.org/extensions/xep-0071.html' + for more information. + + Example stanza: + + Non-html message content. + + +

HTML!

+ + +
+ + Stanza Interface: + body -- The contents of the HTML body tag. + + Methods: + getBody -- Return the HTML body contents. + setBody -- Set the HTML body contents. + delBody -- Remove the HTML body contents. + """ + + namespace = 'http://jabber.org/protocol/xhtml-im' + name = 'html' + interfaces = set(('body',)) + plugin_attrib = name + + def setBody(self, html): + """ + Set the contents of the HTML body. + + Arguments: + html -- Either a string or XML object. If the top level + element is not with a namespace of + 'http://www.w3.org/1999/xhtml', it will be wrapped. + """ + if isinstance(html, str): + html = ET.XML(html) + if html.tag != '{http://www.w3.org/1999/xhtml}body': + body = ET.Element('{http://www.w3.org/1999/xhtml}body') + body.append(html) + self.xml.append(body) + else: + self.xml.append(html) + + def getBody(self): + """Return the contents of the HTML body.""" + html = self.xml.find('{http://www.w3.org/1999/xhtml}body') + if html is None: + return '' + return html + + def delBody(self): + """Remove the HTML body contents.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) + + +registerStanzaPlugin(Message, HTMLIM) diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index f55211d..5e2a667 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -32,7 +32,7 @@ class TestMessageStanzas(SleekTest): msg['type'] = 'chat' p = ET.Element('{http://www.w3.org/1999/xhtml}p') p.text = "This is the htmlim message" - msg['html']['html'] = p + msg['html']['body'] = p self.checkMessage(msg, """ this is the plaintext message From 956fdf69709af8f59b92c0fe5e77490f2c670648 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 18:31:22 -0400 Subject: [PATCH 75/99] Fix whitespace issues, and make some debugging statements clearer. --- tests/sleektest.py | 51 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index 71ff105..cea8df6 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -21,7 +21,7 @@ from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin class TestSocket(object): - + def __init__(self, *args, **kwargs): self.socket = socket.socket(*args, **kwargs) self.recv_queue = queue.Queue() @@ -104,10 +104,10 @@ class SleekTest(unittest.TestCase): def checkMessage(self, msg, xml_string, use_values=True): """ - Create and compare several message stanza objects to a - correct XML string. + Create and compare several message stanza objects to a + correct XML string. - If use_values is False, the test using getValues() and + If use_values is False, the test using getValues() and setValues() will not be used. """ @@ -118,10 +118,10 @@ class SleekTest(unittest.TestCase): self.fix_namespaces(xml, 'jabber:client') debug += "XML String:\n%s\n" % ET.tostring(xml) - + msg2 = self.Message(xml) debug += "Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) - + if use_values: # Ugly, but need to make sure the type attribute is set. msg['type'] = msg['type'] @@ -133,10 +133,10 @@ class SleekTest(unittest.TestCase): values = msg2.getStanzaValues() msg3 = self.Message() msg3.setStanzaValues(values) - + debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), + self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug @@ -144,10 +144,10 @@ class SleekTest(unittest.TestCase): def checkIq(self, iq, xml_string, use_values=True): """ - Create and compare several iq stanza objects to a - correct XML string. + Create and compare several iq stanza objects to a + correct XML string. - If use_values is False, the test using getValues() and + If use_values is False, the test using getValues() and setValues() will not be used. """ @@ -157,10 +157,10 @@ class SleekTest(unittest.TestCase): xml = ET.fromstring(xml_string) self.fix_namespaces(xml, 'jabber:client') debug += "XML String:\n%s\n" % ET.tostring(xml) - + iq2 = self.Iq(xml) debug += "Constructed Stanza:\n%s\n" % ET.tostring(iq2.xml) - + if use_values: values = iq.getStanzaValues() iq3 = self.Iq() @@ -168,7 +168,7 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(iq3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), + self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug @@ -176,27 +176,28 @@ class SleekTest(unittest.TestCase): def checkPresence(self, pres, xml_string, use_values=True): """ - Create and compare several presence stanza objects to a - correct XML string. + Create and compare several presence stanza objects to a + correct XML string. - If use_values is False, the test using getValues() and + If use_values is False, the test using getValues() and setValues() will not be used. """ self.fix_namespaces(pres.xml, 'jabber:client') - debug = "Given Stanza:\n%s\n" % ET.tostring(pres.xml) xml = ET.fromstring(xml_string) self.fix_namespaces(xml, 'jabber:client') - debug += "XML String:\n%s\n" % ET.tostring(xml) - + pres2 = self.Presence(xml) - debug += "Constructed Stanza:\n%s\n" % ET.tostring(pres2.xml) # Ugly, but 'priority' has a default value and need to make # sure it is set pres['priority'] = pres['priority'] pres2['priority'] = pres2['priority'] + debug = "Given Stanza:\n%s\n" % ET.tostring(pres.xml) + debug += "XML String:\n%s\n" % ET.tostring(xml) + debug += "Constructed Stanza:\n%s\n" % ET.tostring(pres2.xml) + if use_values: values = pres.getStanzaValues() pres3 = self.Presence() @@ -204,7 +205,7 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(pres3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, pres.xml, pres2.xml, pres3.xml]), + self.failUnless(self.compare([xml, pres.xml, pres2.xml, pres3.xml]), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug @@ -229,7 +230,7 @@ class SleekTest(unittest.TestCase): self.xmpp.connectTCP = lambda a, b, c, d: True self.xmpp.startTLS = lambda: True self.xmpp.process(threaded=True) - if skip: + if skip: # Clear startup stanzas self.xmpp.socket.nextSent(timeout=0.01) @@ -242,7 +243,7 @@ class SleekTest(unittest.TestCase): data = self.Message(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) self.checkMessage(data, sent, use_values) - + def streamSendIq(self, data, use_values=True, timeout=.1): if isinstance(data, str): data = self.Iq(xml=ET.fromstring(data)) @@ -265,7 +266,7 @@ class SleekTest(unittest.TestCase): def fix_namespaces(self, xml, ns): """ - Assign a namespace to an element and any children that + Assign a namespace to an element and any children that don't have a namespace. """ if xml.tag.startswith('{'): From fec69be7318d9ad2a8e4ac128ac57f1969a6b852 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 3 Aug 2010 18:32:53 -0400 Subject: [PATCH 76/99] Update nick stanza with documentation and PEP8 style. --- sleekxmpp/stanza/nick.py | 78 ++++++++++++++++++++++++++++------- tests/test_messagestanzas.py | 13 +++++- tests/test_presencestanzas.py | 10 +++++ 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index 47d4620..de54b30 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -5,22 +5,68 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + class Nick(ElementBase): - namespace = 'http://jabber.org/nick/nick' - name = 'nick' - plugin_attrib = 'nick' - interfaces = set(('nick')) - plugin_attrib_map = set() - plugin_xml_map = set() - def setNick(self, nick): - self.xml.text = nick - - def getNick(self): - return self.xml.text - - def delNick(self): - if self.parent is not None: - self.parent().xml.remove(self.xml) + """ + XEP-0172: User Nickname allows the addition of a element + in several stanza types, including and stanzas. + + The nickname contained in a should be the global, friendly or + informal name chosen by the owner of a bare JID. The element + may be included when establishing communications with new entities, + such as normal XMPP users or MUC services. + + The nickname contained in a element will not necessarily be + the same as the nickname used in a MUC. + + Example stanzas: + + The User + ... + + + + The User + + + Stanza Interface: + nick -- A global, friendly or informal name chosen by a user. + + Methods: + getNick -- Return the nickname in the element. + setNick -- Add a element with the given nickname. + delNick -- Remove the element. + """ + + namespace = 'http://jabber.org/nick/nick' + name = 'nick' + plugin_attrib = name + interfaces = set(('nick',)) + + def setNick(self, nick): + """ + Add a element with the given nickname. + + Arguments: + nick -- A human readable, informal name. + """ + self.xml.text = nick + + def getNick(self): + """Return the nickname in the element.""" + return self.xml.text + + def delNick(self): + """Remove the element.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) + + +registerStanzaPlugin(Message, Nick) +registerStanzaPlugin(Presence, Nick) diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index 5e2a667..9b7b0a3 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -25,7 +25,7 @@ class TestMessageStanzas(SleekTest): self.failUnless(str(msg['to']) == 'usr@server.tld') def testHTMLPlugin(self): - "Test message/html/html stanza" + "Test message/html/body stanza" msg = self.Message() msg['to'] = "fritzy@netflint.net/sleekxmpp" msg['body'] = "this is the plaintext message" @@ -43,4 +43,15 @@ class TestMessageStanzas(SleekTest): """) + def testNickPlugin(self): + "Test message/nick/nick stanza." + msg = self.Message() + msg['nick']['nick'] = 'A nickname!' + self.checkMessage(msg, """ + + A nickname! + + """) + + suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index 2cab3af..6c11263 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -53,5 +53,15 @@ class TestPresenceStanzas(SleekTest): self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence") + def testNickPlugin(self): + """Test presence/nick/nick stanza.""" + p = self.Presence() + p['nick']['nick'] = 'A nickname!' + self.checkPresence(p, """ + + A nickname! + + """) + suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) From aa1dbe97e0df2437a63a0b16566b75b8a874f065 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 4 Aug 2010 00:33:28 -0400 Subject: [PATCH 77/99] Updated and simplified new JID class to have more documentation and use PEP8 style. --- sleekxmpp/xmlstream/__init__.py | 1 + sleekxmpp/xmlstream/jid.py | 179 ++++++++++++++++++++------------ 2 files changed, 116 insertions(+), 64 deletions(-) diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py index 2611896..c82ab34 100644 --- a/sleekxmpp/xmlstream/__init__.py +++ b/sleekxmpp/xmlstream/__init__.py @@ -6,5 +6,6 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.xmlstream.jid import JID from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index 2839ba5..292abd9 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -1,70 +1,121 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + class JID(object): - def __init__(self, jid): - """Initialize a new jid""" - self.reset(jid) + """ + A representation of a Jabber ID, or JID. - def reset(self, jid): - """Start fresh from a new jid string""" - self._full = self._jid = str(jid) - self._domain = None - self._resource = None - self._user = None - self._domain = None - self._bare = None - - def __getattr__(self, name): - """Handle getting the jid values, using cache if available""" - if name == 'resource': - if self._resource is not None: return self._resource - self._resource = self._jid.split('/', 1)[-1] - return self._resource - elif name == 'user': - if self._user is not None: return self._user - if '@' in self._jid: - self._user = self._jid.split('@', 1)[0] - else: - self._user = self._user - return self._user - elif name in ('server', 'domain'): - if self._domain is not None: return self._domain - self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] - return self._domain - elif name == 'full': - return self._jid - elif name == 'bare': - if self._bare is not None: return self._bare - self._bare = self._jid.split('/', 1)[0] - return self._bare + Each JID may have three components: a user, a domain, and an optional + resource. For example: user@domain/resource - def __setattr__(self, name, value): - """Edit a jid by updating it's individual values, resetting by a generated jid in the end""" - if name in ('resource', 'user', 'domain'): - object.__setattr__(self, "_%s" % name, value) - self.regenerate() - elif name == 'server': - self.domain = value - elif name in ('full', 'jid'): - self.reset(value) - elif name == 'bare': - if '@' in value: - u, d = value.split('@', 1) - object.__setattr__(self, "_user", u) - object.__setattr__(self, "_domain", d) - else: - object.__setattr__(self, "_domain", value) - self.regenerate() - else: - object.__setattr__(self, name, value) + When a resource is not used, the JID is called a bare JID. + The JID is a full JID otherwise. + Attributes: + jid -- Alias for 'full'. + full -- The value of the full JID. + bare -- The value of the bare JID. + user -- The username portion of the JID. + domain -- The domain name portion of the JID. + server -- Alias for 'domain'. + resource -- The resource portion of the JID. - def regenerate(self): - """Generate a new jid based on current values, useful after editing""" - jid = "" - if self.user: jid = "%s@" % self.user - jid += self.domain - if self.resource: jid += "/%s" % self.resource - self.reset(jid) - - def __str__(self): - return self.full + Methods: + reset -- Use a new JID value. + regenerate -- Recreate the JID from its components. + """ + def __init__(self, jid): + """Initialize a new JID""" + self.reset(jid) + + def reset(self, jid): + """ + Start fresh from a new JID string. + + Arguments: + jid - The new JID value. + """ + self._full = self._jid = str(jid) + self._domain = None + self._resource = None + self._user = None + self._bare = None + + def __getattr__(self, name): + """ + Handle getting the JID values, using cache if available. + + Arguments: + name -- One of: user, server, domain, resource, + full, or bare. + """ + if name == 'resource': + if self._resource is None: + self._resource = self._jid.split('/', 1)[-1] + return self._resource + elif name == 'user': + if self._user is None: + if '@' in self._jid: + self._user = self._jid.split('@', 1)[0] + else: + self._user = self._user + return self._user + elif name in ('server', 'domain'): + if self._domain is None: + self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] + return self._domain + elif name == 'full': + return self._jid + elif name == 'bare': + if self._bare is None: + self._bare = self._jid.split('/', 1)[0] + return self._bare + + def __setattr__(self, name, value): + """ + Edit a JID by updating it's individual values, resetting the + generated JID in the end. + + Arguments: + name -- The name of the JID part. One of: user, domain, + server, resource, full, jid, or bare. + value -- The new value for the JID part. + """ + if name in ('resource', 'user', 'domain'): + object.__setattr__(self, "_%s" % name, value) + self.regenerate() + elif name == 'server': + self.domain = value + elif name in ('full', 'jid'): + self.reset(value) + elif name == 'bare': + if '@' in value: + u, d = value.split('@', 1) + object.__setattr__(self, "_user", u) + object.__setattr__(self, "_domain", d) + else: + object.__setattr__(self, "_domain", value) + self.regenerate() + else: + object.__setattr__(self, name, value) + + def regenerate(self): + """Generate a new JID based on current values, useful after editing.""" + jid = "" + if self.user: + jid = "%s@" % self.user + jid += self.domain + if self.resource: + jid += "/%s" % self.resource + self.reset(jid) + + def __str__(self): + """Use the full JID as the string value.""" + return self.full From c54466596f3dcc7a35a41c49fff0d057d4a8ed8f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 4 Aug 2010 14:41:37 -0400 Subject: [PATCH 78/99] Modified sleekxmpp.xmlstream.tostring to import ToString class based on Python version. The package sleekxmpp.xmlstream.tostring26 remains for now until stanzabase is updated, but is no longer needed. --- sleekxmpp/xmlstream/tostring/__init__.py | 70 ++++------------------ sleekxmpp/xmlstream/tostring/tostring.py | 60 +++++++++++++++++++ sleekxmpp/xmlstream/tostring/tostring26.py | 65 ++++++++++++++++++++ 3 files changed, 137 insertions(+), 58 deletions(-) create mode 100644 sleekxmpp/xmlstream/tostring/tostring.py create mode 100644 sleekxmpp/xmlstream/tostring/tostring26.py diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py index 6603cbb..d93fe4e 100644 --- a/sleekxmpp/xmlstream/tostring/__init__.py +++ b/sleekxmpp/xmlstream/tostring/__init__.py @@ -1,60 +1,14 @@ +""" -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != '': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append("" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return ''.join(newoutput) +""" - def xmlesc(self, text): - text = list(text) - cc = 0 - matches = ('&', '<', '"', '>', "'") - for c in text: - if c in matches: - if c == '&': - text[cc] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - else: - text[cc] = '"' - cc += 1 - return ''.join(text) +import sys + + +# Import the correct ToString class based on the Python version. +if sys.version_info < (3, 0): + from sleekxmpp.xmlstream.tostring.tostring26 import ToString +else: + from sleekxmpp.xmlstream.tostring.tostring import ToString + +__all__ = ['ToString'] diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py new file mode 100644 index 0000000..6603cbb --- /dev/null +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -0,0 +1,60 @@ + +class ToString(object): + def __str__(self, xml=None, xmlns='', stringbuffer=''): + if xml is None: + xml = self.xml + newoutput = [stringbuffer] + #TODO respect ET mapped namespaces + itag = xml.tag.split('}', 1)[-1] + if '}' in xml.tag: + ixmlns = xml.tag.split('}', 1)[0][1:] + else: + ixmlns = '' + nsbuffer = '' + if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace: + if self.stream is not None and ixmlns in self.stream.namespace_map: + if self.stream.namespace_map[ixmlns] != '': + itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) + else: + nsbuffer = """ xmlns="%s\"""" % ixmlns + if ixmlns not in ('', xmlns, self.namespace): + nsbuffer = """ xmlns="%s\"""" % ixmlns + newoutput.append("<%s" % itag) + newoutput.append(nsbuffer) + for attrib in xml.attrib: + if '{' not in attrib: + newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) + if len(xml) or xml.text or xml.tail: + newoutput.append(">") + if xml.text: + newoutput.append(self.xmlesc(xml.text)) + if len(xml): + for child in xml.getchildren(): + newoutput.append(self.__str__(child, ixmlns)) + newoutput.append("" % (itag, )) + if xml.tail: + newoutput.append(self.xmlesc(xml.tail)) + elif xml.text: + newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) + else: + newoutput.append(" />") + return ''.join(newoutput) + + def xmlesc(self, text): + text = list(text) + cc = 0 + matches = ('&', '<', '"', '>', "'") + for c in text: + if c in matches: + if c == '&': + text[cc] = '&' + elif c == '<': + text[cc] = '<' + elif c == '>': + text[cc] = '>' + elif c == "'": + text[cc] = ''' + else: + text[cc] = '"' + cc += 1 + return ''.join(text) diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py new file mode 100644 index 0000000..9711c30 --- /dev/null +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -0,0 +1,65 @@ +import types + +class ToString(object): + def __str__(self, xml=None, xmlns='', stringbuffer=''): + if xml is None: + xml = self.xml + newoutput = [stringbuffer] + #TODO respect ET mapped namespaces + itag = xml.tag.split('}', 1)[-1] + if '}' in xml.tag: + ixmlns = xml.tag.split('}', 1)[0][1:] + else: + ixmlns = '' + nsbuffer = '' + if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace: + if self.stream is not None and ixmlns in self.stream.namespace_map: + if self.stream.namespace_map[ixmlns] != u'': + itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) + else: + nsbuffer = """ xmlns="%s\"""" % ixmlns + if ixmlns not in ('', xmlns, self.namespace): + nsbuffer = """ xmlns="%s\"""" % ixmlns + newoutput.append("<%s" % itag) + newoutput.append(nsbuffer) + for attrib in xml.attrib: + if '{' not in attrib: + newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) + if len(xml) or xml.text or xml.tail: + newoutput.append(u">") + if xml.text: + newoutput.append(self.xmlesc(xml.text)) + if len(xml): + for child in xml.getchildren(): + newoutput.append(self.__str__(child, ixmlns)) + newoutput.append(u"" % (itag, )) + if xml.tail: + newoutput.append(self.xmlesc(xml.tail)) + elif xml.text: + newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) + else: + newoutput.append(" />") + return u''.join(newoutput) + + def xmlesc(self, text): + if type(text) != types.UnicodeType: + text = list(unicode(text, 'utf-8', 'ignore')) + else: + text = list(text) + + cc = 0 + matches = (u'&', u'<', u'"', u'>', u"'") + for c in text: + if c in matches: + if c == u'&': + text[cc] = u'&' + elif c == u'<': + text[cc] = u'<' + elif c == u'>': + text[cc] = u'>' + elif c == u"'": + text[cc] = u''' + else: + text[cc] = u'"' + cc += 1 + return ''.join(text) From 58f77d898f82ab108fa17d562a32c68d3ea35306 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 5 Aug 2010 20:23:07 -0400 Subject: [PATCH 79/99] Updated tests to use a relative import for SleekTest to please Python3. Fixed some tabs/spaces issues. --- tests/test_addresses.py | 175 +++++++++++----------- tests/test_chatstates.py | 64 ++++---- tests/test_disco.py | 98 ++++++------ tests/test_errorstanzas.py | 8 +- tests/test_events.py | 2 +- tests/test_forms.py | 20 +-- tests/test_gmail.py | 2 +- tests/test_iqstanzas.py | 22 +-- tests/test_jid.py | 2 +- tests/test_messagestanzas.py | 2 +- tests/test_presencestanzas.py | 2 +- tests/test_pubsubstanzas.py | 274 +++++++++++++++++----------------- tests/test_stream.py | 2 +- 13 files changed, 337 insertions(+), 336 deletions(-) diff --git a/tests/test_addresses.py b/tests/test_addresses.py index 63d1100..450e136 100644 --- a/tests/test_addresses.py +++ b/tests/test_addresses.py @@ -1,110 +1,111 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0033 as xep_0033 class TestAddresses(SleekTest): - def setUp(self): - registerStanzaPlugin(Message, xep_0033.Addresses) + def setUp(self): + registerStanzaPlugin(Message, xep_0033.Addresses) - def testAddAddress(self): - """Testing adding extended stanza address.""" - msg = self.Message() - msg['addresses'].addAddress(atype='to', jid='to@header1.org') - self.checkMessage(msg, """ - - -
- - - """) + def testAddAddress(self): + """Testing adding extended stanza address.""" + msg = self.Message() + msg['addresses'].addAddress(atype='to', jid='to@header1.org') + self.checkMessage(msg, """ + + +
+ + + """) - msg = self.Message() - msg['addresses'].addAddress(atype='replyto', - jid='replyto@header1.org', - desc='Reply address') - self.checkMessage(msg, """ - - -
- - - """) + msg = self.Message() + msg['addresses'].addAddress(atype='replyto', + jid='replyto@header1.org', + desc='Reply address') + self.checkMessage(msg, """ + + +
+ + + """) - def testAddAddresses(self): - """Testing adding multiple extended stanza addresses.""" + def testAddAddresses(self): + """Testing adding multiple extended stanza addresses.""" - xmlstring = """ - - -
-
-
- - - """ + xmlstring = """ + + +
+
+
+ + + """ - msg = self.Message() - msg['addresses'].setAddresses([{'type':'replyto', - 'jid':'replyto@header1.org', - 'desc':'Reply address'}, - {'type':'cc', - 'jid':'cc@header2.org'}, - {'type':'bcc', - 'jid':'bcc@header2.org'}]) - self.checkMessage(msg, xmlstring) + msg = self.Message() + msg['addresses'].setAddresses([ + {'type':'replyto', + 'jid':'replyto@header1.org', + 'desc':'Reply address'}, + {'type':'cc', + 'jid':'cc@header2.org'}, + {'type':'bcc', + 'jid':'bcc@header2.org'}]) + self.checkMessage(msg, xmlstring) - msg = self.Message() - msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', + msg = self.Message() + msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', 'desc':'Reply address'}] - msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] - msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] - self.checkMessage(msg, xmlstring) + msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] + msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] + self.checkMessage(msg, xmlstring) - def testAddURI(self): - """Testing adding URI attribute to extended stanza address.""" + def testAddURI(self): + """Testing adding URI attribute to extended stanza address.""" - msg = self.Message() - addr = msg['addresses'].addAddress(atype='to', - jid='to@header1.org', - node='foo') - self.checkMessage(msg, """ - - -
- - - """) + msg = self.Message() + addr = msg['addresses'].addAddress(atype='to', + jid='to@header1.org', + node='foo') + self.checkMessage(msg, """ + + +
+ + + """) - addr['uri'] = 'mailto:to@header2.org' - self.checkMessage(msg, """ - - -
- - - """) + addr['uri'] = 'mailto:to@header2.org' + self.checkMessage(msg, """ + + +
+ + + """) - def testDelivered(self): - """Testing delivered attribute of extended stanza addresses.""" + def testDelivered(self): + """Testing delivered attribute of extended stanza addresses.""" - xmlstring = """ - - -
- - - """ + xmlstring = """ + + +
+ + + """ - msg = self.Message() - addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') - self.checkMessage(msg, xmlstring % '') + msg = self.Message() + addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') + self.checkMessage(msg, xmlstring % '') - addr['delivered'] = True - self.checkMessage(msg, xmlstring % 'delivered="true"') + addr['delivered'] = True + self.checkMessage(msg, xmlstring % 'delivered="true"') - addr['delivered'] = False - self.checkMessage(msg, xmlstring % '') + addr['delivered'] = False + self.checkMessage(msg, xmlstring % '') suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py index bcacb9e..74359df 100644 --- a/tests/test_chatstates.py +++ b/tests/test_chatstates.py @@ -1,44 +1,44 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0085 as xep_0085 class TestChatStates(SleekTest): - def setUp(self): - registerStanzaPlugin(Message, xep_0085.Active) - registerStanzaPlugin(Message, xep_0085.Composing) - registerStanzaPlugin(Message, xep_0085.Gone) - registerStanzaPlugin(Message, xep_0085.Inactive) - registerStanzaPlugin(Message, xep_0085.Paused) - - def testCreateChatState(self): - """Testing creating chat states.""" - - xmlstring = """ - - <%s xmlns="http://jabber.org/protocol/chatstates" /> - - """ + def setUp(self): + registerStanzaPlugin(Message, xep_0085.Active) + registerStanzaPlugin(Message, xep_0085.Composing) + registerStanzaPlugin(Message, xep_0085.Gone) + registerStanzaPlugin(Message, xep_0085.Inactive) + registerStanzaPlugin(Message, xep_0085.Paused) - msg = self.Message() - msg['chat_state'].active() - self.checkMessage(msg, xmlstring % 'active', - use_values=False) + def testCreateChatState(self): + """Testing creating chat states.""" - msg['chat_state'].composing() - self.checkMessage(msg, xmlstring % 'composing', - use_values=False) + xmlstring = """ + + <%s xmlns="http://jabber.org/protocol/chatstates" /> + + """ + + msg = self.Message() + msg['chat_state'].active() + self.checkMessage(msg, xmlstring % 'active', + use_values=False) + + msg['chat_state'].composing() + self.checkMessage(msg, xmlstring % 'composing', + use_values=False) - msg['chat_state'].gone() - self.checkMessage(msg, xmlstring % 'gone', - use_values=False) + msg['chat_state'].gone() + self.checkMessage(msg, xmlstring % 'gone', + use_values=False) - msg['chat_state'].inactive() - self.checkMessage(msg, xmlstring % 'inactive', - use_values=False) + msg['chat_state'].inactive() + self.checkMessage(msg, xmlstring % 'inactive', + use_values=False) - msg['chat_state'].paused() - self.checkMessage(msg, xmlstring % 'paused', - use_values=False) + msg['chat_state'].paused() + self.checkMessage(msg, xmlstring % 'paused', + use_values=False) suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) diff --git a/tests/test_disco.py b/tests/test_disco.py index 96a12e2..2cc50ee 100644 --- a/tests/test_disco.py +++ b/tests/test_disco.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0030 as xep_0030 @@ -7,7 +7,7 @@ class TestDisco(SleekTest): def setUp(self): registerStanzaPlugin(Iq, xep_0030.DiscoInfo) registerStanzaPlugin(Iq, xep_0030.DiscoItems) - + def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" iq = self.Iq() @@ -61,7 +61,7 @@ class TestDisco(SleekTest): iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') + iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') self.checkIq(iq, """ @@ -76,8 +76,8 @@ class TestDisco(SleekTest): iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - iq['disco_info'].addFeature('foo') - iq['disco_info'].addFeature('bar') + iq['disco_info'].addFeature('foo') + iq['disco_info'].addFeature('bar') self.checkIq(iq, """ @@ -93,9 +93,9 @@ class TestDisco(SleekTest): 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'].addItem('user@localhost') + iq['disco_items'].addItem('user@localhost', 'foo') + iq['disco_items'].addItem('user@localhost', 'bar', 'Testing') self.checkIq(iq, """ @@ -109,68 +109,68 @@ class TestDisco(SleekTest): def testAddRemoveIdentities(self): """Test adding and removing identities to disco#info stanza""" - ids = [('automation', 'commands', 'AdHoc'), - ('conference', 'text', 'ChatRoom')] + ids = [('automation', 'commands', 'AdHoc'), + ('conference', 'text', 'ChatRoom')] - info = xep_0030.DiscoInfo() - info.addIdentity(*ids[0]) - self.failUnless(info.getIdentities() == [ids[0]]) + info = xep_0030.DiscoInfo() + info.addIdentity(*ids[0]) + self.failUnless(info.getIdentities() == [ids[0]]) - info.delIdentity('automation', 'commands') - self.failUnless(info.getIdentities() == []) + info.delIdentity('automation', 'commands') + self.failUnless(info.getIdentities() == []) - info.setIdentities(ids) - self.failUnless(info.getIdentities() == ids) + info.setIdentities(ids) + self.failUnless(info.getIdentities() == ids) - info.delIdentity('automation', 'commands') - self.failUnless(info.getIdentities() == [ids[1]]) + info.delIdentity('automation', 'commands') + self.failUnless(info.getIdentities() == [ids[1]]) - info.delIdentities() - self.failUnless(info.getIdentities() == []) + info.delIdentities() + self.failUnless(info.getIdentities() == []) def testAddRemoveFeatures(self): """Test adding and removing features to disco#info stanza""" - features = ['foo', 'bar', 'baz'] + features = ['foo', 'bar', 'baz'] - info = xep_0030.DiscoInfo() - info.addFeature(features[0]) - self.failUnless(info.getFeatures() == [features[0]]) + info = xep_0030.DiscoInfo() + info.addFeature(features[0]) + self.failUnless(info.getFeatures() == [features[0]]) - info.delFeature('foo') - self.failUnless(info.getFeatures() == []) + info.delFeature('foo') + self.failUnless(info.getFeatures() == []) - info.setFeatures(features) - self.failUnless(info.getFeatures() == features) + info.setFeatures(features) + self.failUnless(info.getFeatures() == features) - info.delFeature('bar') - self.failUnless(info.getFeatures() == ['foo', 'baz']) + info.delFeature('bar') + self.failUnless(info.getFeatures() == ['foo', 'baz']) - info.delFeatures() - self.failUnless(info.getFeatures() == []) + info.delFeatures() + self.failUnless(info.getFeatures() == []) 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')] + items = [('user@localhost', None, None), + ('user@localhost', 'foo', None), + ('user@localhost', 'bar', 'Test')] - info = xep_0030.DiscoItems() - self.failUnless(True, ""+str(items[0])) + info = xep_0030.DiscoItems() + self.failUnless(True, ""+str(items[0])) - info.addItem(*(items[0])) - self.failUnless(info.getItems() == [items[0]], info.getItems()) + info.addItem(*(items[0])) + self.failUnless(info.getItems() == [items[0]], info.getItems()) - info.delItem('user@localhost') - self.failUnless(info.getItems() == []) + info.delItem('user@localhost') + self.failUnless(info.getItems() == []) - info.setItems(items) - self.failUnless(info.getItems() == items) + info.setItems(items) + self.failUnless(info.getItems() == items) - info.delItem('user@localhost', 'foo') - self.failUnless(info.getItems() == [items[0], items[2]]) + info.delItem('user@localhost', 'foo') + self.failUnless(info.getItems() == [items[0], items[2]]) + + info.delItems() + self.failUnless(info.getItems() == []) - info.delItems() - self.failUnless(info.getItems() == []) - suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) diff --git a/tests/test_errorstanzas.py b/tests/test_errorstanzas.py index 788d6c1..d6fafc5 100644 --- a/tests/test_errorstanzas.py +++ b/tests/test_errorstanzas.py @@ -1,7 +1,7 @@ -from sleektest import * +from . sleektest import * class TestErrorStanzas(SleekTest): - + def testSetup(self): """Test setting initial values in error stanza.""" msg = self.Message() @@ -16,7 +16,7 @@ class TestErrorStanzas(SleekTest): def testCondition(self): """Test modifying the error condition.""" - msg = self.Message() + msg = self.Message() msg['error']['condition'] = 'item-not-found' self.checkMessage(msg, """ @@ -42,7 +42,7 @@ class TestErrorStanzas(SleekTest): msg = self.Message() msg['error']['text'] = 'Error!' msg['error']['condition'] = 'internal-server-error' - + del msg['error']['condition'] self.checkMessage(msg, """ diff --git a/tests/test_events.py b/tests/test_events.py index 8f391e1..bbc5832 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,5 +1,5 @@ import sleekxmpp -from sleektest import * +from . sleektest import * class TestEvents(SleekTest): diff --git a/tests/test_forms.py b/tests/test_forms.py index 7d37506..d571063 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0004 as xep_0004 @@ -28,11 +28,11 @@ class TestDataForms(SleekTest): msg = self.Message() form = msg['form'] - form.addField(var='f1', - ftype='text-single', + form.addField(var='f1', + ftype='text-single', label='Text', - desc='A text field', - required=True, + desc='A text field', + required=True, value='Some text!') self.checkMessage(msg, """ @@ -47,8 +47,8 @@ class TestDataForms(SleekTest): """) - form['fields'] = [('f1', {'type': 'text-single', - 'label': 'Username', + form['fields'] = [('f1', {'type': 'text-single', + 'label': 'Username', 'required': True}), ('f2', {'type': 'text-private', 'label': 'Password', @@ -58,7 +58,7 @@ class TestDataForms(SleekTest): 'value': 'Enter message.\nA long one even.'}), ('f4', {'type': 'list-single', 'label': 'Message Type', - 'options': [{'label': 'Cool!', + 'options': [{'label': 'Cool!', 'value': 'cool'}, {'label': 'Urgh!', 'value': 'urgh'}]})] @@ -89,13 +89,13 @@ class TestDataForms(SleekTest): def testSetValues(self): """Testing setting form values""" - + msg = self.Message() form = msg['form'] form.setFields([ ('foo', {'type': 'text-single'}), ('bar', {'type': 'list-multi'})]) - + form.setValues({'foo': 'Foo!', 'bar': ['a', 'b']}) diff --git a/tests/test_gmail.py b/tests/test_gmail.py index b2e70d2..dd256e2 100644 --- a/tests/test_gmail.py +++ b/tests/test_gmail.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.gmail_notify as gmail diff --git a/tests/test_iqstanzas.py b/tests/test_iqstanzas.py index 9ae5929..98a01a2 100644 --- a/tests/test_iqstanzas.py +++ b/tests/test_iqstanzas.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * from sleekxmpp.xmlstream.stanzabase import ET @@ -18,7 +18,7 @@ class TestIqStanzas(SleekTest): self.checkIq(iq, """ """) - + def testPayload(self): """Test setting Iq stanza payload.""" iq = self.Iq() @@ -38,10 +38,10 @@ class TestIqStanzas(SleekTest): """) - iq = self.Iq() + iq = self.Iq() iq['id'] = 'test' - iq['error']['condition'] = 'feature-not-implemented' - iq['error']['text'] = 'No handlers registered for this request.' + iq['error']['condition'] = 'feature-not-implemented' + iq['error']['text'] = 'No handlers registered for this request.' self.streamSendIq(iq, """ @@ -72,21 +72,21 @@ class TestIqStanzas(SleekTest): """) - self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") + self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") - del iq['query'] - self.checkIq(iq, """ + del iq['query'] + self.checkIq(iq, """ """) def testReply(self): """Test setting proper result type in Iq replies.""" iq = self.Iq() - iq['to'] = 'user@localhost' - iq['type'] = 'get' + iq['to'] = 'user@localhost' + iq['type'] = 'get' iq.reply() - self.checkIq(iq, """ + self.checkIq(iq, """ """) diff --git a/tests/test_jid.py b/tests/test_jid.py index 8b4c976..cddac42 100644 --- a/tests/test_jid.py +++ b/tests/test_jid.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * from sleekxmpp.xmlstream.jid import JID class TestJIDClass(SleekTest): diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index 9b7b0a3..2a1567d 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.htmlim import HTMLIM diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index 6c11263..d6a5a38 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -1,5 +1,5 @@ import sleekxmpp -from sleektest import * +from . sleektest import * from sleekxmpp.stanza.presence import Presence diff --git a/tests/test_pubsubstanzas.py b/tests/test_pubsubstanzas.py index 794fa03..cddfd12 100644 --- a/tests/test_pubsubstanzas.py +++ b/tests/test_pubsubstanzas.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0004 as xep_0004 import sleekxmpp.plugins.stanza_pubsub as pubsub @@ -25,7 +25,7 @@ class TestPubsubStanzas(SleekTest): """) - + def testSubscriptions(self): "Testing iq/pubsub/subscriptions/subscription stanzas" iq = self.Iq() @@ -38,7 +38,7 @@ class TestPubsubStanzas(SleekTest): sub2['subscription'] = 'subscribed' iq['pubsub']['subscriptions'].append(sub1) iq['pubsub']['subscriptions'].append(sub2) - self.checkIq(iq, """ + self.checkIq(iq, """ @@ -55,7 +55,7 @@ class TestPubsubStanzas(SleekTest): iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" iq['pubsub']['subscription']['subscription'] = 'unconfigured' - self.checkIq(iq, """ + self.checkIq(iq, """ @@ -88,7 +88,7 @@ class TestPubsubStanzas(SleekTest): item2['payload'] = payload2 iq['pubsub']['items'].append(item) iq['pubsub']['items'].append(item2) - self.checkIq(iq, """ + self.checkIq(iq, """ @@ -112,8 +112,8 @@ class TestPubsubStanzas(SleekTest): "Testing iq/pubsub/create&configure stanzas" iq = self.Iq() iq['pubsub']['create']['node'] = 'mynode' - iq['pubsub']['configure']['form'].addField('pubsub#title', - ftype='text-single', + iq['pubsub']['configure']['form'].addField('pubsub#title', + ftype='text-single', value='This thing is awesome') self.checkIq(iq, """ @@ -131,12 +131,12 @@ class TestPubsubStanzas(SleekTest): def testState(self): "Testing iq/psstate stanzas" - iq = self.Iq() - iq['psstate']['node']= 'mynode' + iq = self.Iq() + iq['psstate']['node']= 'mynode' iq['psstate']['item']= 'myitem' pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') iq['psstate']['payload'] = pl - self.checkIq(iq, """ + self.checkIq(iq, """ @@ -144,16 +144,16 @@ class TestPubsubStanzas(SleekTest): """) def testDefault(self): - "Testing iq/pubsub_owner/default stanzas" - iq = self.Iq() - iq['pubsub_owner']['default'] - iq['pubsub_owner']['default']['node'] = 'mynode' - iq['pubsub_owner']['default']['type'] = 'leaf' - iq['pubsub_owner']['default']['form'].addField('pubsub#title', - ftype='text-single', + "Testing iq/pubsub_owner/default stanzas" + iq = self.Iq() + iq['pubsub_owner']['default'] + iq['pubsub_owner']['default']['node'] = 'mynode' + iq['pubsub_owner']['default']['type'] = 'leaf' + iq['pubsub_owner']['default']['form'].addField('pubsub#title', + ftype='text-single', value='This thing is awesome') self.checkIq(iq, """ - + @@ -166,54 +166,54 @@ class TestPubsubStanzas(SleekTest): """, use_values=False) def testSubscribe(self): - "Testing iq/pubsub/subscribe stanzas" - iq = self.Iq() - iq['pubsub']['subscribe']['options'] - iq['pubsub']['subscribe']['node'] = 'cheese' - iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' - iq['pubsub']['subscribe']['options']['node'] = 'cheese' - iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub']['subscribe']['options']['options'] = form + "testing iq/pubsub/subscribe stanzas" + iq = self.Iq() + iq['pubsub']['subscribe']['options'] + iq['pubsub']['subscribe']['node'] = 'cheese' + iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' + iq['pubsub']['subscribe']['options']['node'] = 'cheese' + iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' + form = xep_0004.Form() + form.addField('pubsub#title', ftype='text-single', value='this thing is awesome') + iq['pubsub']['subscribe']['options']['options'] = form self.checkIq(iq, """ - - - - - - - This thing is awesome - - - - - - """, use_values=False) + + + + + + + this thing is awesome + + + + + + """, use_values=False) def testPublish(self): - "Testing iq/pubsub/publish stanzas" - iq = self.Iq() - iq['pubsub']['publish']['node'] = 'thingers' - payload = ET.fromstring(""" + "Testing iq/pubsub/publish stanzas" + iq = self.Iq() + iq['pubsub']['publish']['node'] = 'thingers' + payload = ET.fromstring(""" """) - payload2 = ET.fromstring(""" + payload2 = ET.fromstring(""" """) - item = pubsub.Item() - item['id'] = 'asdf' - item['payload'] = payload - item2 = pubsub.Item() - item2['id'] = 'asdf2' - item2['payload'] = payload2 - iq['pubsub']['publish'].append(item) - iq['pubsub']['publish'].append(item2) - + item = pubsub.Item() + item['id'] = 'asdf' + item['payload'] = payload + item2 = pubsub.Item() + item2['id'] = 'asdf2' + item2['payload'] = payload2 + iq['pubsub']['publish'].append(item) + iq['pubsub']['publish'].append(item2) + self.checkIq(iq, """ @@ -235,19 +235,19 @@ class TestPubsubStanzas(SleekTest): """) def testDelete(self): - "Testing iq/pubsub_owner/delete stanzas" - iq = self.Iq() - iq['pubsub_owner']['delete']['node'] = 'thingers' + "Testing iq/pubsub_owner/delete stanzas" + iq = self.Iq() + iq['pubsub_owner']['delete']['node'] = 'thingers' self.checkIq(iq, """ - + """) def testCreateConfigGet(self): - """Testing getting config from full create""" - iq = self.Iq() + """Testing getting config from full create""" + iq = self.Iq() iq['to'] = 'pubsub.asdf' iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7' iq['type'] = 'set' @@ -348,16 +348,16 @@ class TestPubsubStanzas(SleekTest): """) def testItemEvent(self): - """Testing message/pubsub_event/items/item""" - msg = self.Message() - item = pubsub.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - item['payload'] = pl - item['id'] = 'abc123' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - self.checkMessage(msg, """ + """Testing message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + item['payload'] = pl + item['id'] = 'abc123' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ @@ -369,21 +369,21 @@ class TestPubsubStanzas(SleekTest): """) def testItemsEvent(self): - """Testing multiple message/pubsub_event/items/item""" - msg = self.Message() - item = pubsub.EventItem() - item2 = pubsub.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - self.checkMessage(msg, """ + """Testing multiple message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ @@ -398,24 +398,24 @@ class TestPubsubStanzas(SleekTest): """) def testItemsEvent(self): - """Testing message/pubsub_event/items/item & retract mix""" - msg = self.Message() - item = pubsub.EventItem() - item2 = pubsub.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - retract = pubsub.EventRetract() - retract['id'] = 'aabbcc' - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(retract) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - self.checkMessage(msg, """ + """Testing message/pubsub_event/items/item & retract mix""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + retract = pubsub.EventRetract() + retract['id'] = 'aabbcc' + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(retract) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ @@ -430,12 +430,12 @@ class TestPubsubStanzas(SleekTest): """) def testCollectionAssociate(self): - """Testing message/pubsub_event/collection/associate""" - msg = self.Message() - msg['pubsub_event']['collection']['associate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - self.checkMessage(msg, """ + """Testing message/pubsub_event/collection/associate""" + msg = self.Message() + msg['pubsub_event']['collection']['associate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ @@ -445,12 +445,12 @@ class TestPubsubStanzas(SleekTest): """) def testCollectionDisassociate(self): - """Testing message/pubsub_event/collection/disassociate""" - msg = self.Message() - msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - self.checkMessage(msg, """ + """Testing message/pubsub_event/collection/disassociate""" + msg = self.Message() + msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ @@ -460,15 +460,15 @@ class TestPubsubStanzas(SleekTest): """) def testEventConfiguration(self): - """Testing message/pubsub_event/configuration/config""" - msg = self.Message() - msg['pubsub_event']['configuration']['node'] = 'cheese' - msg['pubsub_event']['configuration']['form'].addField('pubsub#title', - ftype='text-single', + """Testing message/pubsub_event/configuration/config""" + msg = self.Message() + msg['pubsub_event']['configuration']['node'] = 'cheese' + msg['pubsub_event']['configuration']['form'].addField('pubsub#title', + ftype='text-single', value='This thing is awesome') - msg['type'] = 'headline' + msg['type'] = 'headline' self.checkMessage(msg, """ - + @@ -481,11 +481,11 @@ class TestPubsubStanzas(SleekTest): """) def testEventPurge(self): - """Testing message/pubsub_event/purge""" - msg = self.Message() - msg['pubsub_event']['purge']['node'] = 'pickles' - msg['type'] = 'headline' - self.checkMessage(msg, """ + """Testing message/pubsub_event/purge""" + msg = self.Message() + msg['pubsub_event']['purge']['node'] = 'pickles' + msg['type'] = 'headline' + self.checkMessage(msg, """ @@ -493,15 +493,15 @@ class TestPubsubStanzas(SleekTest): """) def testEventSubscription(self): - """Testing message/pubsub_event/subscription""" - msg = self.Message() - msg['pubsub_event']['subscription']['node'] = 'pickles' - msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' - msg['pubsub_event']['subscription']['subid'] = 'aabb1122' - msg['pubsub_event']['subscription']['subscription'] = 'subscribed' - msg['pubsub_event']['subscription']['expiry'] = 'presence' - msg['type'] = 'headline' - self.checkMessage(msg, """ + """Testing message/pubsub_event/subscription""" + msg = self.Message() + msg['pubsub_event']['subscription']['node'] = 'pickles' + msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' + msg['pubsub_event']['subscription']['subid'] = 'aabb1122' + msg['pubsub_event']['subscription']['subscription'] = 'subscribed' + msg['pubsub_event']['subscription']['expiry'] = 'presence' + msg['type'] = 'headline' + self.checkMessage(msg, """ diff --git a/tests/test_stream.py b/tests/test_stream.py index c2f2667..6e24074 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -1,4 +1,4 @@ -from sleektest import * +from . sleektest import * import sleekxmpp.plugins.xep_0033 as xep_0033 From e077204a16c76df4af90ba067e94c31af3d9e372 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 5 Aug 2010 20:26:41 -0400 Subject: [PATCH 80/99] Replaced the ToString class with a tostring function. The sleekxmpp.xmlstream.tostring and sleekxmpp.xmlstream.tostring26 packages have been merged to sleekxmpp.xmlstream.tostring. The __init__.py file will import the appropriate tostring function depending on the Python version. The setup.py file has been updated with the package changes. ElementBase is now a direct descendent of object and does not subclass ToString. Stanza objects now return their XML contents for __repr__. --- setup.py | 14 +- sleekxmpp/basexmpp.py | 53 +++---- sleekxmpp/xmlstream/stanzabase.py | 91 ++++++------ sleekxmpp/xmlstream/tostring/__init__.py | 13 +- sleekxmpp/xmlstream/tostring/tostring.py | 147 +++++++++++-------- sleekxmpp/xmlstream/tostring/tostring26.py | 157 +++++++++++++-------- sleekxmpp/xmlstream/tostring26/__init__.py | 65 --------- sleekxmpp/xmlstream/xmlstream.py | 101 ++++--------- 8 files changed, 298 insertions(+), 343 deletions(-) delete mode 100644 sleekxmpp/xmlstream/tostring26/__init__.py diff --git a/setup.py b/setup.py index 280ec3c..e3acf18 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,13 @@ import sys # min_version = '0.6c6' # else: # min_version = '0.6a9' -# +# # try: # use_setuptools(min_version=min_version) # except TypeError: # # locally installed ez_setup won't have min_version # use_setuptools() -# +# # from setuptools import setup, find_packages, Extension, Feature VERSION = '0.2.3.1' @@ -37,17 +37,13 @@ CLASSIFIERS = [ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', ] -packages = [ 'sleekxmpp', +packages = [ 'sleekxmpp', 'sleekxmpp/plugins', 'sleekxmpp/stanza', 'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream/matcher', - 'sleekxmpp/xmlstream/handler' ] - -if sys.version_info < (3, 0): - packages.append('sleekxmpp/xmlstream/tostring26') -else: - packages.append('sleekxmpp/xmlstream/tostring') + 'sleekxmpp/xmlstream/handler', + 'sleekxmpp/xmlstream/tostring'] setup( name = "sleekxmpp", diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 2c2bb91..b7b605b 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -25,6 +25,7 @@ from . stanza.roster import Roster from . stanza.nick import Nick from . stanza.htmlim import HTMLIM from . stanza.error import Error +from sleekxmpp.xmlstream.tostring import tostring import logging import threading @@ -60,7 +61,7 @@ class basexmpp(object): registerStanzaPlugin(Iq, Roster) registerStanzaPlugin(Message, Nick) registerStanzaPlugin(Message, HTMLIM) - + def Message(self, *args, **kwargs): return Message(self, *args, **kwargs) @@ -69,7 +70,7 @@ class basexmpp(object): def Presence(self, *args, **kwargs): return Presence(self, *args, **kwargs) - + def set_jid(self, jid): """Rip a JID apart and claim it as our own.""" self.fulljid = jid @@ -77,12 +78,12 @@ class basexmpp(object): self.jid = self.getjidbare(jid) self.username = jid.split('@', 1)[0] self.server = jid.split('@',1)[-1].split('/', 1)[0] - + def process(self, *args, **kwargs): for idx in self.plugin: if not self.plugin[idx].post_inited: self.plugin[idx].post_init() return super(basexmpp, self).process(*args, **kwargs) - + def registerPlugin(self, plugin, pconfig = {}): """Register a plugin not in plugins.__init__.__all__ but in the plugins directory.""" @@ -97,7 +98,7 @@ class basexmpp(object): if hasattr(self.plugin[plugin], 'xep'): xep = "(XEP-%s) " % self.plugin[plugin].xep logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description)) - + def register_plugins(self): """Initiates all plugins in the plugins/__init__.__all__""" if self.plugin_whitelist: @@ -112,24 +113,24 @@ class basexmpp(object): # run post_init() for cross-plugin interaction for plugin in self.plugin: self.plugin[plugin].post_init() - + def getNewId(self): with self.id_lock: self.id += 1 return self.getId() - + def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): # threaded is no longer needed, but leaving it for backwards compatibility for now if name is None: name = 'add_handler_%s' % self.getNewId() self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream)) - + def getId(self): return "%x".upper() % self.id def sendXML(self, data, mask=None, timeout=10): - return self.send(self.tostring(data), mask, timeout) - + return self.send(tostring(data), mask, timeout) + def send(self, data, mask=None, timeout=10): #logging.warning("Deprecated send used for \"%s\"" % (data,)) #if not type(data) == type(''): @@ -144,19 +145,19 @@ class basexmpp(object): self.sendRaw(data) if mask is not None: return waitfor.wait(timeout) - + def makeIq(self, id=0, ifrom=None): return self.Iq().setStanzaValues({'id': str(id), 'from': ifrom}) - + def makeIqGet(self, queryxmlns = None): iq = self.Iq().setStanzaValues({'type': 'get'}) if queryxmlns: iq.append(ET.Element("{%s}query" % queryxmlns)) return iq - + def makeIqResult(self, id): return self.Iq().setStanzaValues({'id': id, 'type': 'result'}) - + def makeIqSet(self, sub=None): iq = self.Iq().setStanzaValues({'type': 'set'}) if sub != None: @@ -172,13 +173,13 @@ class basexmpp(object): query = ET.Element("{%s}query" % xmlns) iq.append(query) return iq - + def makeQueryRoster(self, iq=None): query = ET.Element("{jabber:iq:roster}query") if iq: iq.append(query) return query - + def add_event_handler(self, name, pointer, threaded=False, disposable=False): if not name in self.event_handlers: self.event_handlers[name] = [] @@ -188,13 +189,13 @@ class basexmpp(object): """Remove a handler for an event.""" if not name in self.event_handlers: return - + # Need to keep handlers that do not use # the given function pointer def filter_pointers(handler): return handler[0] != pointer - self.event_handlers[name] = filter(filter_pointers, + self.event_handlers[name] = filter(filter_pointers, self.event_handlers[name]) def event(self, name, eventdata = {}): # called on an event @@ -209,7 +210,7 @@ class basexmpp(object): if handler[2]: #disposable with self.lock: self.event_handlers[name].pop(self.event_handlers[name].index(handler)) - + def makeMessage(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) message['body'] = mbody @@ -217,7 +218,7 @@ class basexmpp(object): if mnick is not None: message['nick'] = mnick if mhtml is not None: message['html']['html'] = mhtml return message - + def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None): presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) if pshow is not None: presence['type'] = pshow @@ -226,10 +227,10 @@ class basexmpp(object): presence['priority'] = ppriority presence['status'] = pstatus return presence - + def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom,mnick)) - + def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None): self.send(self.makePresence(pshow,pstatus,ppriority,pto, ptype=ptype, pfrom=pfrom)) if not self.sentpresence: @@ -243,19 +244,19 @@ class basexmpp(object): nick.text = pnick presence.append(nick) self.send(presence) - + def getjidresource(self, fulljid): if '/' in fulljid: return fulljid.split('/', 1)[-1] else: return '' - + def getjidbare(self, fulljid): return fulljid.split('/', 1)[0] def _handleMessage(self, msg): self.event('message', msg) - + def _handlePresence(self, presence): """Update roster items based on presence""" self.event("presence_%s" % presence['type'], presence) @@ -296,7 +297,7 @@ class basexmpp(object): if name: name = "(%s) " % name logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status)) - + def _handlePresenceSubscribe(self, presence): """Handling subscriptions automatically.""" if self.auto_authorize == True: diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 94ff958..3b5f0bf 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -12,10 +12,7 @@ import weakref import copy from . jid import JID -if sys.version_info < (3,0): - from . import tostring26 as tostring -else: - from . import tostring +from sleekxmpp.xmlstream.tostring import tostring xmltester = type(ET.Element('xml')) @@ -29,7 +26,7 @@ def registerStanzaPlugin(stanza, plugin): stanza.plugin_tag_map[tag] = plugin -class ElementBase(tostring.ToString): +class ElementBase(object): name = 'stanza' plugin_attrib = 'plugin' namespace = 'jabber:client' @@ -70,20 +67,20 @@ class ElementBase(tostring.ToString): def __bool__(self): return True - + def __next__(self): self.idx += 1 if self.idx > len(self.iterables): self.idx = 0 raise StopIteration return self.iterables[self.idx - 1] - + def next(self): return self.__next__() def __len__(self): return len(self.iterables) - + def append(self, item): if not isinstance(item, ElementBase): if type(item) == xmltester: @@ -93,18 +90,18 @@ class ElementBase(tostring.ToString): self.xml.append(item.xml) self.iterables.append(item) return self - + def pop(self, idx=0): aff = self.iterables.pop(idx) self.xml.remove(aff.xml) return aff - + def get(self, key, defaultvalue=None): value = self[key] if value is None or value == '': return defaultvalue return value - + def keys(self): out = [] out += [x for x in self.interfaces] @@ -112,7 +109,7 @@ class ElementBase(tostring.ToString): if self.iterables: out.append('substanzas') return tuple(out) - + def match(self, matchstring): if isinstance(matchstring, str): nodes = matchstring.split('/') @@ -136,13 +133,13 @@ class ElementBase(tostring.ToString): else: return False return True - + def find(self, xpath): # for backwards compatiblity, expose elementtree interface return self.xml.find(xpath) def findall(self, xpath): return self.xml.findall(xpath) - + def setup(self, xml=None): if self.xml is None: self.xml = xml @@ -162,11 +159,11 @@ class ElementBase(tostring.ToString): def enable(self, attrib): self.initPlugin(attrib) return self - + def initPlugin(self, attrib): if attrib not in self.plugins: self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) - + def __getitem__(self, attrib): if attrib == 'substanzas': return self.iterables @@ -183,7 +180,7 @@ class ElementBase(tostring.ToString): return self.plugins[attrib] else: return '' - + def __setitem__(self, attrib, value): if attrib in self.interfaces: if value is not None: @@ -201,7 +198,7 @@ class ElementBase(tostring.ToString): self.initPlugin(attrib) self.plugins[attrib][attrib] = value return self - + def __delitem__(self, attrib): if attrib.lower() in self.interfaces: if hasattr(self, "del%s" % attrib.title()): @@ -215,7 +212,7 @@ class ElementBase(tostring.ToString): if attrib in self.plugins: del self.plugins[attrib] return self - + def __eq__(self, other): if not isinstance(other, ElementBase): return False @@ -224,20 +221,20 @@ class ElementBase(tostring.ToString): if key not in values or values[key] != other[key]: return False return True - + def _setAttr(self, name, value): if value is None or value == '': self.__delitem__(name) else: self.xml.attrib[name] = value - + def _delAttr(self, name): if name in self.xml.attrib: del self.xml.attrib[name] - + def _getAttr(self, name, default=''): return self.xml.attrib.get(name, default) - + def _getSubText(self, name): if '}' not in name: name = "{%s}%s" % (self.namespace, name) @@ -246,7 +243,7 @@ class ElementBase(tostring.ToString): return '' else: return stanza.text - + def _setSubText(self, name, attrib={}, text=None): if '}' not in name: name = "{%s}%s" % (self.namespace, name) @@ -258,14 +255,14 @@ class ElementBase(tostring.ToString): self.xml.append(stanza) stanza.text = text return stanza - + def _delSub(self, name): if '}' not in name: name = "{%s}%s" % (self.namespace, name) for child in self.xml.getchildren(): if child.tag == name: self.xml.remove(child) - + def getStanzaValues(self): out = {} for interface in self.interfaces: @@ -279,7 +276,7 @@ class ElementBase(tostring.ToString): iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) out['substanzas'] = iterables return out - + def setStanzaValues(self, attrib): for interface in attrib: if interface == 'substanzas': @@ -298,14 +295,20 @@ class ElementBase(tostring.ToString): if interface in self.plugins: self.plugins[interface].setStanzaValues(attrib[interface]) return self - + def appendxml(self, xml): self.xml.append(xml) return self def __copy__(self): return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) - + + def __str__(self): + return tostring(self.xml, xmlns='', stanza_ns=self.namespace) + + def __repr__(self): + return self.__str__() + #def __del__(self): #prevents garbage collection of reference cycle # if self.parent is not None: # self.parent.xml.remove(self.xml) @@ -329,7 +332,7 @@ class StanzaBase(ElementBase): if sfrom is not None: self['from'] = sfrom self.tag = "{%s}%s" % (self.namespace, self.name) - + def setType(self, value): if value in self.types: self.xml.attrib['type'] = value @@ -337,22 +340,22 @@ class StanzaBase(ElementBase): def getPayload(self): return self.xml.getchildren() - + def setPayload(self, value): self.xml.append(value) return self - + def delPayload(self): self.clear() return self - + def clear(self): for child in self.xml.getchildren(): self.xml.remove(child) for plugin in list(self.plugins.keys()): del self.plugins[plugin] return self - + def reply(self): # if it's a component, use from if self.stream and hasattr(self.stream, "is_component") and self.stream.is_component: @@ -362,32 +365,34 @@ class StanzaBase(ElementBase): del self['from'] self.clear() return self - + def error(self): self['type'] = 'error' return self - + def getTo(self): return JID(self._getAttr('to')) - + def setTo(self, value): return self._setAttr('to', str(value)) - + def getFrom(self): return JID(self._getAttr('from')) - + def setFrom(self, value): return self._setAttr('from', str(value)) - + def unhandled(self): pass - + def exception(self, e): logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) - + def send(self): self.stream.sendRaw(self.__str__()) def __copy__(self): return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) - + + def __str__(self): + return tostring(self.xml, xmlns='', stanza_ns=self.namespace, stream=self.stream) diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py index d93fe4e..5852cba 100644 --- a/sleekxmpp/xmlstream/tostring/__init__.py +++ b/sleekxmpp/xmlstream/tostring/__init__.py @@ -1,14 +1,19 @@ """ + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + See the file LICENSE for copying permission. """ import sys +# Import the correct tostring and xml_escape functions based on the Python +# version in order to properly handle Unicode. -# Import the correct ToString class based on the Python version. if sys.version_info < (3, 0): - from sleekxmpp.xmlstream.tostring.tostring26 import ToString + from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape else: - from sleekxmpp.xmlstream.tostring.tostring import ToString + from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape -__all__ = ['ToString'] +__all__ = ['tostring', 'xml_escape'] diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py index 6603cbb..62ff118 100644 --- a/sleekxmpp/xmlstream/tostring/tostring.py +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -1,60 +1,91 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != '': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append("" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return ''.join(newoutput) + See the file LICENSE for copying permission. +""" - def xmlesc(self, text): - text = list(text) - cc = 0 - matches = ('&', '<', '"', '>', "'") - for c in text: - if c in matches: - if c == '&': - text[cc] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - else: - text[cc] = '"' - cc += 1 - return ''.join(text) + +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): + """ + Serialize an XML object to a Unicode string. + + Arguments: + xml -- The XML object to serialize. If the value is None, + then the XML object contained in this stanza + object will be used. + xmlns -- Optional namespace of an element wrapping the XML + object. + stanza_ns -- The namespace of the stanza object that contains + the XML object. + stream -- The XML stream that generated the XML object. + outbuffer -- Optional buffer for storing serializations during + recursive calls. + """ + # Add previous results to the start of the output. + output = [outbuffer] + + # Extract the element's tag name. + tag_name = xml.tag.split('}', 1)[-1] + + # Extract the element's namespace if it is defined. + if '}' in xml.tag: + tag_xmlns = xml.tag.split('}', 1)[0][1:] + else: + tag_xmlns = '' + + # Output the tag name and derived namespace of the element. + namespace = '' + if tag_xmlns not in ['', xmlns, stanza_ns]: + namespace = ' xmlns="%s"' % tag_xmlns + if stream and tag_xmlns in stream.namespace_map: + mapped_namespace = stream.namespace_map[tag_xmlns] + if mapped_namespace: + tag = "%s:%s" % (mapped_namespace, tag_name) + output.append("<%s" % tag_name) + output.append(namespace) + + # Output escaped attribute values. + for attrib, value in xml.attrib.items(): + if '{' not in attrib: + value = xml_escape(value) + output.append(' %s="%s"' % (attrib, value)) + + if len(xml) or xml.text: + # If there are additional child elements to serialize. + output.append(">") + if xml.text: + output.append(xml_escape(xml.text)) + if len(xml): + for child in xml.getchildren(): + output.append(tostring(child, tag_xmlns, stanza_ns, stream)) + output.append("" % tag_name) + elif xml.text: + # If we only have text content. + output.append(">%s" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return ''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + text = list(text) + escapes = {'&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return ''.join(text) diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py index 9711c30..9dba271 100644 --- a/sleekxmpp/xmlstream/tostring/tostring26.py +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -1,65 +1,100 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals import types -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != u'': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(u">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append(u"" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return u''.join(newoutput) - def xmlesc(self, text): - if type(text) != types.UnicodeType: - text = list(unicode(text, 'utf-8', 'ignore')) - else: - text = list(text) +def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): + """ + Serialize an XML object to a Unicode string. - cc = 0 - matches = (u'&', u'<', u'"', u'>', u"'") - for c in text: - if c in matches: - if c == u'&': - text[cc] = u'&' - elif c == u'<': - text[cc] = u'<' - elif c == u'>': - text[cc] = u'>' - elif c == u"'": - text[cc] = u''' - else: - text[cc] = u'"' - cc += 1 - return ''.join(text) + Arguments: + xml -- The XML object to serialize. If the value is None, + then the XML object contained in this stanza + object will be used. + xmlns -- Optional namespace of an element wrapping the XML + object. + stanza_ns -- The namespace of the stanza object that contains + the XML object. + stream -- The XML stream that generated the XML object. + outbuffer -- Optional buffer for storing serializations during + recursive calls. + """ + # Add previous results to the start of the output. + output = [outbuffer] + + # Extract the element's tag name. + tag_name = xml.tag.split('}', 1)[-1] + + # Extract the element's namespace if it is defined. + if '}' in xml.tag: + tag_xmlns = xml.tag.split('}', 1)[0][1:] + else: + tag_xmlns = u'' + + # Output the tag name and derived namespace of the element. + namespace = u'' + if tag_xmlns not in ['', xmlns, stanza_ns]: + namespace = u' xmlns="%s"' % tag_xmlns + if stream and tag_xmlns in stream.namespace_map: + mapped_namespace = stream.namespace_map[tag_xmlns] + if mapped_namespace: + tag = u"%s:%s" % (mapped_namespace, tag_name) + output.append(u"<%s" % tag_name) + output.append(namespace) + + # 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)) + + if len(xml) or xml.text: + # If there are additional child elements to serialize. + output.append(u">") + if xml.text: + output.append(xml_escape(xml.text)) + if len(xml): + for child in xml.getchildren(): + output.append(tostring(child, tag_xmlns, stanza_ns, stream)) + output.append(u"" % tag_name) + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + elif xml.text: + # If we only have text content. + output.append(u">%s" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(u" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return u''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + if type(text) != types.UnicodeType: + text = list(unicode(text, 'utf-8', 'ignore')) + else: + text = list(text) + escapes = {u'&': u'&', + u'<': u'<', + u'>': u'>', + u"'": u''', + u'"': u'"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return u''.join(text) diff --git a/sleekxmpp/xmlstream/tostring26/__init__.py b/sleekxmpp/xmlstream/tostring26/__init__.py deleted file mode 100644 index 9711c30..0000000 --- a/sleekxmpp/xmlstream/tostring26/__init__.py +++ /dev/null @@ -1,65 +0,0 @@ -import types - -class ToString(object): - def __str__(self, xml=None, xmlns='', stringbuffer=''): - if xml is None: - xml = self.xml - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace: - if self.stream is not None and ixmlns in self.stream.namespace_map: - if self.stream.namespace_map[ixmlns] != u'': - itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - if ixmlns not in ('', xmlns, self.namespace): - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - if '{' not in attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(u">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.__str__(child, ixmlns)) - newoutput.append(u"" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return u''.join(newoutput) - - def xmlesc(self, text): - if type(text) != types.UnicodeType: - text = list(unicode(text, 'utf-8', 'ignore')) - else: - text = list(text) - - cc = 0 - matches = (u'&', u'<', u'"', u'>', u"'") - for c in text: - if c in matches: - if c == u'&': - text[cc] = u'&' - elif c == u'<': - text[cc] = u'<' - elif c == u'>': - text[cc] = u'>' - elif c == u"'": - text[cc] = u''' - else: - text[cc] = u'"' - cc += 1 - return ''.join(text) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 94fed64..ffaa651 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -23,6 +23,7 @@ import types import copy import xml.sax.saxutils from . import scheduler +from sleekxmpp.xmlstream.tostring import tostring RESPONSE_TIMEOUT = 10 HANDLER_THREADS = 1 @@ -37,7 +38,7 @@ if sys.version_info < (3, 0): #monkey patch broken filesocket object from . import filesocket #socket._fileobject = filesocket.filesocket - + class RestartStream(Exception): pass @@ -82,7 +83,7 @@ class XMLStream(object): self.namespace_map = {} self.run = True - + def setSocket(self, socket): "Set the socket" self.socket = socket @@ -90,10 +91,10 @@ class XMLStream(object): self.filesocket = socket.makefile('rb', 0) # ElementTree.iterparse requires a file. 0 buffer files have to be binary self.state.set('connected', True) - + def setFileSocket(self, filesocket): self.filesocket = filesocket - + def connect(self, host='', port=0, use_ssl=False, use_tls=True): "Link to connectTCP" return self.connectTCP(host, port, use_ssl, use_tls) @@ -125,7 +126,7 @@ class XMLStream(object): except socket.error as serr: logging.error("Could not connect. Socket Error #%s: %s" % (serr.errno, serr.strerror)) time.sleep(1) - + def connectUnix(self, filepath): "Connect to Unix file and create socket" @@ -146,7 +147,7 @@ class XMLStream(object): logging.warning("Tried to enable TLS, but ssl module not found.") return False raise RestartStream() - + def process(self, threaded=True): self.scheduler.process(threaded=True) for t in range(0, HANDLER_THREADS): @@ -160,10 +161,10 @@ class XMLStream(object): self.__thread['process'].start() else: self._process() - + def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False): self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.eventqueue) - + def _process(self): "Start processing the socket." firstrun = True @@ -212,7 +213,7 @@ class XMLStream(object): #self.__thread['readXML'].start() #self.__thread['spawnEvents'] = threading.Thread(name='spawnEvents', target=self.__spawnEvents) #self.__thread['spawnEvents'].start() - + def __readXML(self): "Parses the incoming stream, adding to xmlin queue as it goes" #build cElementTree object from expat was we go @@ -245,7 +246,7 @@ class XMLStream(object): if event == b'start': edepth += 1 logging.debug("Ending readXML loop") - + def _sendThread(self): while self.run: data = self.sendqueue.get(True) @@ -260,11 +261,11 @@ class XMLStream(object): if self.state.reconnect: logging.exception("Disconnected. Socket Error.") self.disconnect(reconnect=True) - + def sendRaw(self, data): self.sendqueue.put(data) return True - + def disconnect(self, reconnect=False): self.state.set('reconnect', reconnect) if self.state['disconnecting']: @@ -290,20 +291,20 @@ class XMLStream(object): if self.state['processing']: #raise CloseStream pass - + def reconnect(self): self.state.set('tls',False) self.state.set('ssl',False) time.sleep(1) self.connect() - + def incoming_filter(self, xmlobj): return xmlobj - + def __spawnEvent(self, xmlobj): "watching xmlOut and processes handlers" #convert XML into Stanza - logging.debug("RECV: %s" % cElementTree.tostring(xmlobj)) + logging.debug("RECV: %s" % tostring(xmlobj)) xmlobj = self.incoming_filter(xmlobj) stanza_type = StanzaBase for stanza_class in self.__root_stanza: @@ -323,7 +324,7 @@ class XMLStream(object): stanza.unhandled() #loop through handlers and test match #spawn threads as necessary, call handlers, sending Stanza - + def _eventRunner(self): logging.debug("Loading event runner") while self.run: @@ -354,11 +355,11 @@ class XMLStream(object): elif etype == 'quit': logging.debug("Quitting eventRunner thread") return False - + def registerHandler(self, handler, before=None, after=None): "Add handler with matcher class and parameters." self.__handlers.append(handler) - + def removeHandler(self, name): "Removes the handler." idx = 0 @@ -367,81 +368,27 @@ class XMLStream(object): self.__handlers.pop(idx) return idx += 1 - + def registerStanza(self, stanza_class): "Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects." self.__root_stanza.append(stanza_class) - + def registerStanzaExtension(self, stanza_class, stanza_extension): if stanza_class not in stanza_extensions: stanza_extensions[stanza_class] = [stanza_extension] else: stanza_extensions[stanza_class].append(stanza_extension) - + def removeStanza(self, stanza_class, root=False): "Removes the stanza's registration." if root: del self.__root_stanza[stanza_class] else: del self.__stanza[stanza_class] - + def removeStanzaExtension(self, stanza_class, stanza_extension): stanza_extension[stanza_class].pop(stanza_extension) - def tostring(self, xml, xmlns='', stringbuffer=''): - newoutput = [stringbuffer] - #TODO respect ET mapped namespaces - itag = xml.tag.split('}', 1)[-1] - if '}' in xml.tag: - ixmlns = xml.tag.split('}', 1)[0][1:] - else: - ixmlns = '' - nsbuffer = '' - if xmlns != ixmlns and ixmlns != '': - if ixmlns in self.namespace_map: - if self.namespace_map[ixmlns] != '': - itag = "%s:%s" % (self.namespace_map[ixmlns], itag) - else: - nsbuffer = """ xmlns="%s\"""" % ixmlns - newoutput.append("<%s" % itag) - newoutput.append(nsbuffer) - for attrib in xml.attrib: - newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib]))) - if len(xml) or xml.text or xml.tail: - newoutput.append(">") - if xml.text: - newoutput.append(self.xmlesc(xml.text)) - if len(xml): - for child in xml.getchildren(): - newoutput.append(self.tostring(child, ixmlns)) - newoutput.append("" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%s" % (self.xmlesc(xml.text), itag)) - else: - newoutput.append(" />") - return ''.join(newoutput) - - def xmlesc(self, text): - text = list(text) - cc = 0 - matches = ('&', '<', '"', '>', "'") - for c in text: - if c in matches: - if c == '&': - text[cc] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - elif self.escape_quotes: - text[cc] = '"' - cc += 1 - return ''.join(text) - def start_stream_handler(self, xml): """Meant to be overridden""" pass From 3c0dfb56e6dcd864a29523950fcc23e6c3761ff7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 5 Aug 2010 20:43:38 -0400 Subject: [PATCH 81/99] Update tostring docs to clarify what the xmlns and stanza_ns parameters do. --- sleekxmpp/xmlstream/tostring/tostring.py | 4 ++++ sleekxmpp/xmlstream/tostring/tostring26.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py index 62ff118..c269632 100644 --- a/sleekxmpp/xmlstream/tostring/tostring.py +++ b/sleekxmpp/xmlstream/tostring/tostring.py @@ -11,6 +11,10 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): """ Serialize an XML object to a Unicode string. + If namespaces are provided using xmlns or stanza_ns, then elements + that use those namespaces will not include the xmlns attribute in + the output. + Arguments: xml -- The XML object to serialize. If the value is None, then the XML object contained in this stanza diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py index 9dba271..7a37637 100644 --- a/sleekxmpp/xmlstream/tostring/tostring26.py +++ b/sleekxmpp/xmlstream/tostring/tostring26.py @@ -14,6 +14,10 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): """ Serialize an XML object to a Unicode string. + If namespaces are provided using xmlns or stanza_ns, then elements + that use those namespaces will not include the xmlns attribute in + the output. + Arguments: xml -- The XML object to serialize. If the value is None, then the XML object contained in this stanza From 0d0c044a688490eb295ddd305247f406eefc0855 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 5 Aug 2010 20:57:55 -0400 Subject: [PATCH 82/99] Add unit tests for the tostring function. --- tests/test_tostring.py | 104 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_tostring.py diff --git a/tests/test_tostring.py b/tests/test_tostring.py new file mode 100644 index 0000000..2999949 --- /dev/null +++ b/tests/test_tostring.py @@ -0,0 +1,104 @@ +from . sleektest import * +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.tostring import tostring, xml_escape + + +class TestToString(SleekTest): + + """ + Test the implementation of sleekxmpp.xmlstream.tostring + """ + + def tryTostring(self, original='', expected=None, message='', **kwargs): + """ + Compare the result of calling tostring against an + expected result. + """ + if not expected: + expected=original + if isinstance(original, str): + xml = ET.fromstring(original) + else: + xml=original + result = tostring(xml, **kwargs) + self.failUnless(result == expected, "%s: %s" % (message, result)) + + def testXMLEscape(self): + """Test escaping XML special characters.""" + original = """'Hi & welcome!'""" + escaped = xml_escape(original) + desired = """<foo bar="baz">'Hi""" + desired += """ & welcome!'</foo>""" + + self.failUnless(escaped == desired, + "XML escaping did not work: %s." % escaped) + + def testEmptyElement(self): + """Test converting an empty element to a string.""" + self.tryTostring( + original='', + message="Empty element not serialized correctly") + + def testEmptyElementWrapped(self): + """Test converting an empty element inside another element.""" + self.tryTostring( + original='', + message="Wrapped empty element not serialized correctly") + + def testEmptyElementWrappedText(self): + """ + Test converting an empty element wrapped with text + inside another element. + """ + self.tryTostring( + original='Some text. More text.', + message="Text wrapped empty element serialized incorrectly") + + def testMultipleChildren(self): + """Test converting multiple child elements to a Unicode string.""" + self.tryTostring( + original='', + message="Multiple child elements not serialized correctly") + + def testXMLNS(self): + """ + Test using xmlns tostring parameter, which will prevent adding + an xmlns attribute to the serialized element if the element's + namespace is the same. + """ + self.tryTostring( + original='', + expected='', + message="The xmlns parameter was not used properly.", + xmlns='foo') + + def testStanzaNs(self): + """ + Test using the stanza_ns tostring parameter, which will prevent + adding an xmlns attribute to the serialized element if the + element's namespace is the same. + """ + self.tryTostring( + original='', + expected='', + message="The stanza_ns parameter was not used properly.", + stanza_ns='foo') + + def testStanzaStr(self): + """ + Test that stanza objects are serialized properly. + """ + utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0' + if not hasattr(utf8_message, 'decode'): + # Python 3 + utf8_message = bytes(utf8_message, encoding='utf-8') + msg = Message() + msg['body'] = utf8_message.decode('utf-8') + expected = '\xe0\xb2\xa0_\xe0\xb2\xa0' + result = msg.__str__() + self.failUnless(result == expected, + "Stanza Unicode handling is incorrect: %s" % result) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestToString) From 4d1f071f831183568147870302722e492d71d051 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 5 Aug 2010 23:11:22 -0400 Subject: [PATCH 83/99] Updated the use of tostring in xmlstream.py Now uses the xmlns and stream parameters to reduce the number of extra xmlns attributes used in the logging output. Added self.default_ns to XMLStream just to be safe. --- sleekxmpp/xmlstream/xmlstream.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index ffaa651..bf39bb3 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -73,6 +73,7 @@ class XMLStream(object): self.use_ssl = False self.use_tls = False + self.default_ns = '' self.stream_header = "" self.stream_footer = "" @@ -304,7 +305,7 @@ class XMLStream(object): def __spawnEvent(self, xmlobj): "watching xmlOut and processes handlers" #convert XML into Stanza - logging.debug("RECV: %s" % tostring(xmlobj)) + logging.debug("RECV: %s" % tostring(xmlobj, xmlns=self.default_ns, stream=self)) xmlobj = self.incoming_filter(xmlobj) stanza_type = StanzaBase for stanza_class in self.__root_stanza: From 48ba7292bcbc5207f08766ed3a855e16e2bf11d7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 6 Aug 2010 12:04:52 -0400 Subject: [PATCH 84/99] Updated SleekTest to use the new tostring function instead of ET.tostring --- tests/sleektest.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index cea8df6..a711654 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -18,6 +18,7 @@ from sleekxmpp import Message, Iq from sleekxmpp.stanza.presence import Presence from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.tostring import tostring class TestSocket(object): @@ -112,15 +113,15 @@ class SleekTest(unittest.TestCase): """ self.fix_namespaces(msg.xml, 'jabber:client') - debug = "Given Stanza:\n%s\n" % ET.tostring(msg.xml) + debug = "Given Stanza:\n%s\n" % tostring(msg.xml) xml = ET.fromstring(xml_string) self.fix_namespaces(xml, 'jabber:client') - debug += "XML String:\n%s\n" % ET.tostring(xml) + debug += "XML String:\n%s\n" % tostring(xml) msg2 = self.Message(xml) - debug += "Constructed Stanza:\n%s\n" % ET.tostring(msg2.xml) + debug += "Constructed Stanza:\n%s\n" % tostring(msg2.xml) if use_values: # Ugly, but need to make sure the type attribute is set. @@ -128,13 +129,13 @@ class SleekTest(unittest.TestCase): if xml.attrib.get('type', None) is None: xml.attrib['type'] = 'normal' msg2['type'] = msg2['type'] - debug += "XML String:\n%s\n" % ET.tostring(xml) + debug += "XML String:\n%s\n" % tostring(xml) values = msg2.getStanzaValues() msg3 = self.Message() msg3.setStanzaValues(values) - debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(msg3.xml) + debug += "Second Constructed Stanza:\n%s\n" % tostring(msg3.xml) debug = "Three methods for creating stanza do not match:\n" + debug self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), debug) @@ -152,21 +153,21 @@ class SleekTest(unittest.TestCase): """ self.fix_namespaces(iq.xml, 'jabber:client') - debug = "Given Stanza:\n%s\n" % ET.tostring(iq.xml) + debug = "Given Stanza:\n%s\n" % tostring(iq.xml) xml = ET.fromstring(xml_string) self.fix_namespaces(xml, 'jabber:client') - debug += "XML String:\n%s\n" % ET.tostring(xml) + debug += "XML String:\n%s\n" % tostring(xml) iq2 = self.Iq(xml) - debug += "Constructed Stanza:\n%s\n" % ET.tostring(iq2.xml) + debug += "Constructed Stanza:\n%s\n" % tostring(iq2.xml) if use_values: values = iq.getStanzaValues() iq3 = self.Iq() iq3.setStanzaValues(values) - debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(iq3.xml) + debug += "Second Constructed Stanza:\n%s\n" % tostring(iq3.xml) debug = "Three methods for creating stanza do not match:\n" + debug self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), debug) @@ -194,16 +195,16 @@ class SleekTest(unittest.TestCase): pres['priority'] = pres['priority'] pres2['priority'] = pres2['priority'] - debug = "Given Stanza:\n%s\n" % ET.tostring(pres.xml) - debug += "XML String:\n%s\n" % ET.tostring(xml) - debug += "Constructed Stanza:\n%s\n" % ET.tostring(pres2.xml) + debug = "Given Stanza:\n%s\n" % tostring(pres.xml) + debug += "XML String:\n%s\n" % tostring(xml) + debug += "Constructed Stanza:\n%s\n" % tostring(pres2.xml) if use_values: values = pres.getStanzaValues() pres3 = self.Presence() pres3.setStanzaValues(values) - debug += "Second Constructed Stanza:\n%s\n" % ET.tostring(pres3.xml) + debug += "Second Constructed Stanza:\n%s\n" % tostring(pres3.xml) debug = "Three methods for creating stanza do not match:\n" + debug self.failUnless(self.compare([xml, pres.xml, pres2.xml, pres3.xml]), debug) From c09e9c702c114f76d6cbfd9d7fb1a19cefa5a1e5 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 11 Aug 2010 18:21:12 -0400 Subject: [PATCH 85/99] Updated sleekxmpp.exceptions with PEP8 style and docs. --- sleekxmpp/exceptions.py | 45 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index bbbd69d..40217ef 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -3,14 +3,43 @@ Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. -See the file LICENSE for copying permission. + See the file LICENSE for copying permission. """ class XMPPError(Exception): - def __init__(self, condition='undefined-condition', text=None, etype=None, extension=None, extension_ns=None, extension_args=None): - self.condition = condition - self.text = text - self.etype = etype - self.extension = extension - self.extension_ns = extension_ns - self.extension_args = extension_args + + """ + A generic exception that may be raised while processing an XMPP stanza + to indicate that an error response stanza should be sent. + + The exception method for stanza objects extending RootStanza will create + an error stanza and initialize any additional substanzas using the + extension information included in the exception. + + Meant for use in SleekXMPP plugins and applications using SleekXMPP. + """ + + def __init__(self, condition='undefined-condition', text=None, etype=None, + extension=None, extension_ns=None, extension_args=None): + """ + Create a new XMPPError exception. + + Extension information can be included to add additional XML elements + to the generated error stanza. + + Arguments: + condition -- The XMPP defined error condition. + text -- Human readable text describing the error. + etype -- The XMPP error type, such as cancel or modify. + extension -- Tag name of the extension's XML content. + extension_ns -- XML namespace of the extensions' XML content. + extension_args -- Content and attributes for the extension + element. Same as the additional arguments to + the ET.Element constructor. + """ + self.condition = condition + self.text = text + self.etype = etype + self.extension = extension + self.extension_ns = extension_ns + self.extension_args = extension_args From 7a5ef2849218e122b04e244aeedd67844a0690b2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 11 Aug 2010 18:41:57 -0400 Subject: [PATCH 86/99] Updated SleekTest.streamClose to check that the stream was actually started before closing it. Updated tests for Iq stanzas to not start a stream for every test; tests now run a lot faster. The call to streamClose must still be in the tearDown method to ensure it is called in the case of an error. --- tests/sleektest.py | 2 +- tests/test_iqstanzas.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index a711654..0adab2b 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -258,7 +258,7 @@ class SleekTest(unittest.TestCase): self.checkPresence(data, sent, use_values) def streamClose(self): - if self.xmpp is not None: + if hasattr(self, 'xmpp') and self.xmpp is not None: self.xmpp.disconnect() self.xmpp.socket.recvData(self.xmpp.stream_footer) diff --git a/tests/test_iqstanzas.py b/tests/test_iqstanzas.py index 98a01a2..2dabc5e 100644 --- a/tests/test_iqstanzas.py +++ b/tests/test_iqstanzas.py @@ -4,10 +4,6 @@ from sleekxmpp.xmlstream.stanzabase import ET class TestIqStanzas(SleekTest): - def setUp(self): - """Start XML stream for testing.""" - self.streamStart() - def tearDown(self): """Shutdown the XML stream after testing.""" self.streamClose() @@ -32,6 +28,7 @@ class TestIqStanzas(SleekTest): def testUnhandled(self): """Test behavior for Iq.unhandled.""" + self.streamStart() self.streamRecv(""" From b40a48979636ccb4055294427292b2decf095fea Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 11 Aug 2010 23:32:14 -0400 Subject: [PATCH 87/99] Updated roster stanza with docs and PEP8 style. --- sleekxmpp/stanza/roster.py | 144 +++++++++++++++++++++++++------------ tests/test_roster.py | 84 ++++++++++++++++++++++ 2 files changed, 183 insertions(+), 45 deletions(-) create mode 100644 tests/test_roster.py diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index eda65ec..292c895 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -5,51 +5,105 @@ See the file LICENSE for copying permission. """ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -import logging + +from sleekxmpp.stanza import Iq +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.xmlstream.stanzabase import ET, ElementBase + class Roster(ElementBase): - namespace = 'jabber:iq:roster' - name = 'query' - plugin_attrib = 'roster' - interfaces = set(('items',)) - sub_interfaces = set() - def setItems(self, items): - self.delItems() - for jid in items: - ijid = str(jid) - item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) - if 'subscription' in items[jid]: - item.attrib['subscription'] = items[jid]['subscription'] - if 'name' in items[jid]: - name = items[jid]['name'] - if name is not None: - item.attrib['name'] = name - if 'groups' in items[jid]: - for group in items[jid]['groups']: - groupxml = ET.Element('{jabber:iq:roster}group') - groupxml.text = group - item.append(groupxml) - self.xml.append(item) - return self - - def getItems(self): - items = {} - itemsxml = self.xml.findall('{jabber:iq:roster}item') - if itemsxml is not None: - for itemxml in itemsxml: - item = {} - item['name'] = itemxml.get('name', '') - item['subscription'] = itemxml.get('subscription', '') - item['groups'] = [] - groupsxml = itemxml.findall('{jabber:iq:roster}group') - if groupsxml is not None: - for groupxml in groupsxml: - item['groups'].append(groupxml.text) - items[itemxml.get('jid')] = item - return items - - def delItems(self): - for child in self.xml.getchildren(): - self.xml.remove(child) + """ + Example roster stanzas: + + + + Friends + + + + + Stanza Inteface: + items -- A dictionary of roster entries contained + in the stanza. + + Methods: + getItems -- Return a dictionary of roster entries. + setItems -- Add elements. + delItems -- Remove all elements. + """ + + namespace = 'jabber:iq:roster' + name = 'query' + plugin_attrib = 'roster' + interfaces = set(('items',)) + + def setItems(self, items): + """ + Set the roster entries in the stanza. + + Uses a dictionary using JIDs as keys, where each entry is itself + a dictionary that contains: + name -- An alias or nickname for the JID. + subscription -- The subscription type. Can be one of 'to', + 'from', 'both', 'none', or 'remove'. + groups -- A list of group names to which the JID + has been assigned. + + Arguments: + items -- A dictionary of roster entries. + """ + self.delItems() + for jid in items: + ijid = str(jid) + item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) + if 'subscription' in items[jid]: + item.attrib['subscription'] = items[jid]['subscription'] + if 'name' in items[jid]: + name = items[jid]['name'] + if name is not None: + item.attrib['name'] = name + if 'groups' in items[jid]: + for group in items[jid]['groups']: + groupxml = ET.Element('{jabber:iq:roster}group') + groupxml.text = group + item.append(groupxml) + self.xml.append(item) + return self + + def getItems(self): + """ + Return a dictionary of roster entries. + + Each item is keyed using its JID, and contains: + name -- An assigned alias or nickname for the JID. + subscription -- The subscription type. Can be one of 'to', + 'from', 'both', 'none', or 'remove'. + groups -- A list of group names to which the JID has + been assigned. + """ + items = {} + itemsxml = self.xml.findall('{jabber:iq:roster}item') + if itemsxml is not None: + for itemxml in itemsxml: + item = {} + item['name'] = itemxml.get('name', '') + item['subscription'] = itemxml.get('subscription', '') + item['groups'] = [] + groupsxml = itemxml.findall('{jabber:iq:roster}group') + if groupsxml is not None: + for groupxml in groupsxml: + item['groups'].append(groupxml.text) + items[itemxml.get('jid')] = item + return items + + def delItems(self): + """ + Remove all elements from the roster stanza. + """ + for child in self.xml.getchildren(): + self.xml.remove(child) + + +registerStanzaPlugin(Iq, Roster) diff --git a/tests/test_roster.py b/tests/test_roster.py new file mode 100644 index 0000000..6f9fa3d --- /dev/null +++ b/tests/test_roster.py @@ -0,0 +1,84 @@ +from . sleektest import * +from sleekxmpp.stanza.roster import Roster + + +class TestRosterStanzas(SleekTest): + + def testAddItems(self): + """Test adding items to a roster stanza.""" + iq = self.Iq() + iq['roster'].setItems({ + 'user@example.com': { + 'name': 'User', + 'subscription': 'both', + 'groups': ['Friends', 'Coworkers']}, + 'otheruser@example.com': { + 'name': 'Other User', + 'subscription': 'both', + 'groups': []}}) + self.checkIq(iq, """ + + + + Friends + Coworkers + + + + + """) + + def testGetItems(self): + """Test retrieving items from a roster stanza.""" + xml_string = """ + + + + Friends + Coworkers + + + + + """ + iq = self.Iq(ET.fromstring(xml_string)) + expected = { + 'user@example.com': { + 'name': 'User', + 'subscription': 'both', + 'groups': ['Friends', 'Coworkers']}, + 'otheruser@example.com': { + 'name': 'Other User', + 'subscription': 'both', + 'groups': []}} + debug = "Roster items don't match after retrieval." + debug += "\nReturned: %s" % str(iq['roster']['items']) + debug += "\nExpected: %s" % str(expected) + self.failUnless(iq['roster']['items'] == expected, debug) + + def testDelItems(self): + """Test clearing items from a roster stanza.""" + xml_string = """ + + + + Friends + Coworkers + + + + + """ + iq = self.Iq(ET.fromstring(xml_string)) + del iq['roster']['items'] + self.checkIq(iq, """ + + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas) From 22134c302b68f37ded406ea335aca4c9bdab3090 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 12 Aug 2010 01:25:42 -0400 Subject: [PATCH 88/99] Updated SleekTest with docs and PEP8 style. --- tests/sleektest.py | 293 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 245 insertions(+), 48 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index 0adab2b..c7c7241 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -9,38 +9,73 @@ import unittest import socket try: - import queue + import queue except ImportError: - import Queue as queue -from xml.etree import cElementTree as ET + import Queue as queue + from sleekxmpp import ClientXMPP -from sleekxmpp import Message, Iq -from sleekxmpp.stanza.presence import Presence -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin +from sleekxmpp.stanza import Message, Iq, Presence +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ET from sleekxmpp.xmlstream.tostring import tostring class TestSocket(object): + """ + A dummy socket that reads and writes to queues instead + of an actual networking socket. + + Methods: + nextSent -- Return the next sent stanza. + recvData -- Make a stanza available to read next. + recv -- Read the next stanza from the socket. + send -- Write a stanza to the socket. + makefile -- Dummy call, returns self. + read -- Read the next stanza from the socket. + """ + def __init__(self, *args, **kwargs): + """ + Create a new test socket. + + Arguments: + Same as arguments for socket.socket + """ self.socket = socket.socket(*args, **kwargs) self.recv_queue = queue.Queue() self.send_queue = queue.Queue() def __getattr__(self, name): - """Pass requests through to actual socket""" - # Override a few methods to prevent actual socket connections - overrides = {'connect': lambda *args: None, - 'close': lambda *args: None, - 'shutdown': lambda *args: None} + """ + Return attribute values of internal, dummy socket. + + Some attributes and methods are disabled to prevent the + socket from connecting to the network. + + Arguments: + name -- Name of the attribute requested. + """ + + def dummy(*args): + """Method to do nothing and prevent actual socket connections.""" + return None + + overrides = {'connect': dummy, + 'close': dummy, + 'shutdown': dummy} + return overrides.get(name, getattr(self.socket, name)) # ------------------------------------------------------------------ # Testing Interface def nextSent(self, timeout=None): - """Get the next stanza that has been 'sent'""" + """ + Get the next stanza that has been 'sent'. + + Arguments: + timeout -- Optional timeout for waiting for a new value. + """ args = {'block': False} if timeout is not None: args = {'block': True, 'timeout': timeout} @@ -50,27 +85,58 @@ class TestSocket(object): return None def recvData(self, data): - """Add data to the receiving queue""" + """ + Add data to the receiving queue. + + Arguments: + data -- String data to 'write' to the socket to be received + by the XMPP client. + """ self.recv_queue.put(data) # ------------------------------------------------------------------ # Socket Interface def recv(self, *args, **kwargs): + """ + Read a value from the received queue. + + Arguments: + Placeholders. Same as for socket.Socket.recv. + """ return self.read(block=True) def send(self, data): + """ + Send data by placing it in the send queue. + + Arguments: + data -- String value to write. + """ self.send_queue.put(data) # ------------------------------------------------------------------ # File Socket - def makefile(self, mode='r', bufsize=-1): - """File socket version to use with ElementTree""" + def makefile(self, *args, **kwargs): + """ + File socket version to use with ElementTree. + + Arguments: + Placeholders, same as socket.Socket.makefile() + """ return self - def read(self, size=4096, block=True, timeout=None): - """Implement the file socket interface""" + def read(self, block=True, timeout=None, **kwargs): + """ + Implement the file socket interface. + + Arguments: + block -- Indicate if the read should block until a + value is ready. + timeout -- Time in seconds a block should last before + returning None. + """ if timeout is not None: block = True try: @@ -80,24 +146,65 @@ class TestSocket(object): class SleekTest(unittest.TestCase): + """ A SleekXMPP specific TestCase class that provides methods for comparing message, iq, and presence stanzas. + + Methods: + Message -- Create a Message stanza object. + Iq -- Create an Iq stanza object. + Presence -- Create a Presence stanza object. + checkMessage -- Compare a Message stanza against an XML string. + checkIq -- Compare an Iq stanza against an XML string. + checkPresence -- Compare a Presence stanza against an XML string. + streamStart -- Initialize a dummy XMPP client. + streamRecv -- Queue data for XMPP client to receive. + streamSendMessage -- Check that the XMPP client sent the given + Message stanza. + streamSendIq -- Check that the XMPP client sent the given + Iq stanza. + streamSendPresence -- Check taht the XMPP client sent the given + Presence stanza. + streamClose -- Disconnect the XMPP client. + fix_namespaces -- Add top-level namespace to an XML object. + compare -- Compare XML objects against each other. """ # ------------------------------------------------------------------ # Shortcut methods for creating stanza objects def Message(self, *args, **kwargs): - """Create a message stanza.""" + """ + Create a Message stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Message's values. + """ return Message(None, *args, **kwargs) def Iq(self, *args, **kwargs): - """Create an iq stanza.""" + """ + Create an Iq stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Iq's values. + """ return Iq(None, *args, **kwargs) def Presence(self, *args, **kwargs): - """Create a presence stanza.""" + """ + Create a Presence stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Iq's values. + """ return Presence(None, *args, **kwargs) # ------------------------------------------------------------------ @@ -108,8 +215,15 @@ class SleekTest(unittest.TestCase): Create and compare several message stanza objects to a correct XML string. - If use_values is False, the test using getValues() and - setValues() will not be used. + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + msg -- The Message stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. """ self.fix_namespaces(msg.xml, 'jabber:client') @@ -137,19 +251,26 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % tostring(msg3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, msg.xml, msg2.xml, msg3.xml]), + self.failUnless(self.compare(xml, msg.xml, msg2.xml, msg3.xml), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, msg.xml, msg2.xml]), debug) + self.failUnless(self.compare(xml, msg.xml, msg2.xml), debug) def checkIq(self, iq, xml_string, use_values=True): """ Create and compare several iq stanza objects to a correct XML string. - If use_values is False, the test using getValues() and - setValues() will not be used. + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + iq -- The Iq stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. """ self.fix_namespaces(iq.xml, 'jabber:client') @@ -169,20 +290,28 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % tostring(iq3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, iq.xml, iq2.xml, iq3.xml]), + self.failUnless(self.compare(xml, iq.xml, iq2.xml, iq3.xml), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, iq.xml, iq2.xml]), debug) + self.failUnless(self.compare(xml, iq.xml, iq2.xml), debug) def checkPresence(self, pres, xml_string, use_values=True): """ Create and compare several presence stanza objects to a correct XML string. - If use_values is False, the test using getValues() and - setValues() will not be used. + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + iq -- The Iq stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. """ + self.fix_namespaces(pres.xml, 'jabber:client') xml = ET.fromstring(xml_string) @@ -206,17 +335,26 @@ class SleekTest(unittest.TestCase): debug += "Second Constructed Stanza:\n%s\n" % tostring(pres3.xml) debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, pres.xml, pres2.xml, pres3.xml]), + self.failUnless(self.compare(xml, pres.xml, pres2.xml, pres3.xml), debug) else: debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare([xml, pres.xml, pres2.xml]), debug) - + self.failUnless(self.compare(xml, pres.xml, pres2.xml), debug) # ------------------------------------------------------------------ # Methods for simulating stanza streams. def streamStart(self, mode='client', skip=True): + """ + Initialize an XMPP client or component using a dummy XML stream. + + Arguments: + mode -- Either 'client' or 'component'. Defaults to 'client'. + skip -- Indicates if the first item in the sent queue (the + stream header) should be removed. Tests that wish + to test initializing the stream should set this to + False. Otherwise, the default of True should be used. + """ if mode == 'client': self.xmpp = ClientXMPP('tester@localhost', 'test') self.xmpp.setSocket(TestSocket()) @@ -236,28 +374,82 @@ class SleekTest(unittest.TestCase): self.xmpp.socket.nextSent(timeout=0.01) def streamRecv(self, data): + """ + Pass data to the dummy XMPP client as if it came from an XMPP server. + + Arguments: + data -- String stanza XML to be received and processed by the + XMPP client or component. + """ data = str(data) self.xmpp.socket.recvData(data) def streamSendMessage(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkMessage. + + Arguments: + data -- The XML string of the expected Message stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkMessage. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ if isinstance(data, str): data = self.Message(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) self.checkMessage(data, sent, use_values) def streamSendIq(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkIq. + + Arguments: + data -- The XML string of the expected Iq stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkIq. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ if isinstance(data, str): data = self.Iq(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) self.checkIq(data, sent, use_values) def streamSendPresence(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkPresence. + + Arguments: + data -- The XML string of the expected Presence stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkPresence. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ if isinstance(data, str): data = self.Presence(xml=ET.fromstring(data)) sent = self.xmpp.socket.nextSent(timeout) self.checkPresence(data, sent, use_values) def streamClose(self): + """ + Disconnect the dummy XMPP client. + + Can be safely called even if streamStart has not been called. + + Must be placed in the tearDown method of a test class to ensure + that the XMPP client is disconnected after an error. + """ if hasattr(self, 'xmpp') and self.xmpp is not None: self.xmpp.disconnect() self.xmpp.socket.recvData(self.xmpp.stream_footer) @@ -269,6 +461,10 @@ class SleekTest(unittest.TestCase): """ Assign a namespace to an element and any children that don't have a namespace. + + Arguments: + xml -- The XML object to fix. + ns -- The namespace to add to the XML object. """ if xml.tag.startswith('{'): return @@ -276,36 +472,37 @@ class SleekTest(unittest.TestCase): for child in xml.getchildren(): self.fix_namespaces(child, ns) - def compare(self, xml1, xml2=None): + def compare(self, xml, *other): """ Compare XML objects. - If given a list of XML objects, then - all of the elements in the list will be - compared. + Arguments: + xml -- The XML object to compare against. + *other -- The list of XML objects to compare. """ + if not other: + return False # Compare multiple objects - if type(xml1) is list: - xmls = xml1 - xml1 = xmls[0] - for xml in xmls[1:]: - xml2 = xml - if not self.compare(xml1, xml2): + if len(other) > 1: + for xml2 in other: + if not self.compare(xml, xml2): return False return True + other = other[0] + # Step 1: Check tags - if xml1.tag != xml2.tag: + if xml.tag != other.tag: return False # Step 2: Check attributes - if xml1.attrib != xml2.attrib: + if xml.attrib != other.attrib: return False # Step 3: Recursively check children - for child in xml1: - child2s = xml2.findall("%s" % child.tag) + for child in xml: + child2s = other.findall("%s" % child.tag) if child2s is None: return False for child2 in child2s: From 5da7bd1866b6ea2c58055a3aac7d0ed273ed40c6 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 12 Aug 2010 01:26:01 -0400 Subject: [PATCH 89/99] Removed unused xmlcompare.py. --- tests/xmlcompare.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 tests/xmlcompare.py diff --git a/tests/xmlcompare.py b/tests/xmlcompare.py deleted file mode 100644 index d97af97..0000000 --- a/tests/xmlcompare.py +++ /dev/null @@ -1,28 +0,0 @@ -from xml.etree import cElementTree as ET - -def comparemany(xmls): - xml1 = xmls[0] - if type(xml1) == type(''): - xml1 = ET.fromstring(xml1) - for xml in xmls[1:]: - xml2 = xml - if type(xml2) == type(''): - xml2 = ET.fromstring(xml2) - if not compare(xml1, xml2): return False - return True - -def compare(xml1, xml2): - if xml1.tag != xml2.tag: - return False - if xml1.attrib != xml2.attrib: - return False - for child in xml1: - child2s = xml2.findall("%s" % child.tag) - if child2s is None: - return False - found = False - for child2 in child2s: - found = compare(child, child2) - if found: break - if not found: return False - return True From 4b52007e8c39566bb2083a2e4041de4b294c4948 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 12 Aug 2010 23:24:09 -0400 Subject: [PATCH 90/99] Cleaned stanzabase imports. --- sleekxmpp/xmlstream/stanzabase.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 3b5f0bf..687fc4f 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -5,13 +5,14 @@ See the file LICENSE for copying permission. """ -from xml.etree import cElementTree as ET + +import copy import logging import sys import weakref -import copy -from . jid import JID +from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring xmltester = type(ET.Element('xml')) From b0fb205c165311b3b66d36b87a2632dcd70d018a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 10:12:51 -0400 Subject: [PATCH 91/99] Updated registerStanzaPlugin and the XML test type. --- sleekxmpp/xmlstream/stanzabase.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 687fc4f..feadbd4 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -15,16 +15,22 @@ from xml.etree import cElementTree as ET from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring -xmltester = type(ET.Element('xml')) + +# Used to check if an argument is an XML object. +XML_TYPE = type(ET.Element('xml')) def registerStanzaPlugin(stanza, plugin): - """ - Associate a stanza object as a plugin for another stanza. - """ - tag = "{%s}%s" % (plugin.namespace, plugin.name) - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map[tag] = plugin + """ + Associate a stanza object as a plugin for another stanza. + + Arguments: + stanza -- The class of the parent stanza. + plugin -- The class of the plugin stanza. + """ + tag = "{%s}%s" % (plugin.namespace, plugin.name) + stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin + stanza.plugin_tag_map[tag] = plugin class ElementBase(object): @@ -84,7 +90,7 @@ class ElementBase(object): def append(self, item): if not isinstance(item, ElementBase): - if type(item) == xmltester: + if type(item) == XML_TYPE: return self.appendxml(item) else: raise TypeError From 747001d33c87dbd644b369f9e41da9ba16927561 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 10:15:52 -0400 Subject: [PATCH 92/99] Adjust first level indenting in ElementBase to prepare for cleanup. --- sleekxmpp/xmlstream/stanzabase.py | 500 +++++++++++++++--------------- 1 file changed, 250 insertions(+), 250 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index feadbd4..a6425b8 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -34,291 +34,291 @@ def registerStanzaPlugin(stanza, plugin): class ElementBase(object): - name = 'stanza' - plugin_attrib = 'plugin' - namespace = 'jabber:client' - interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = None + name = 'stanza' + plugin_attrib = 'plugin' + namespace = 'jabber:client' + interfaces = set(('type', 'to', 'from', 'id', 'payload')) + types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + sub_interfaces = tuple() + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = None - def __init__(self, xml=None, parent=None): - if parent is None: - self.parent = None - else: - self.parent = weakref.ref(parent) - self.xml = xml - self.plugins = {} - self.iterables = [] - self.idx = 0 - if not self.setup(xml): - for child in self.xml.getchildren(): - if child.tag in self.plugin_tag_map: - self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) - if self.subitem is not None: - for sub in self.subitem: - if child.tag == "{%s}%s" % (sub.namespace, sub.name): - self.iterables.append(sub(xml=child, parent=self)) - break + def __init__(self, xml=None, parent=None): + if parent is None: + self.parent = None + else: + self.parent = weakref.ref(parent) + self.xml = xml + self.plugins = {} + self.iterables = [] + self.idx = 0 + if not self.setup(xml): + for child in self.xml.getchildren(): + if child.tag in self.plugin_tag_map: + self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) + if self.subitem is not None: + for sub in self.subitem: + if child.tag == "{%s}%s" % (sub.namespace, sub.name): + self.iterables.append(sub(xml=child, parent=self)) + break - @property - def attrib(self): #backwards compatibility - return self + @property + def attrib(self): #backwards compatibility + return self - def __iter__(self): - self.idx = 0 - return self + def __iter__(self): + self.idx = 0 + return self - def __bool__(self): - return True + def __bool__(self): + return True - def __next__(self): - self.idx += 1 - if self.idx > len(self.iterables): - self.idx = 0 - raise StopIteration - return self.iterables[self.idx - 1] + def __next__(self): + self.idx += 1 + if self.idx > len(self.iterables): + self.idx = 0 + raise StopIteration + return self.iterables[self.idx - 1] - def next(self): - return self.__next__() + def next(self): + return self.__next__() - def __len__(self): - return len(self.iterables) + def __len__(self): + return len(self.iterables) - def append(self, item): - if not isinstance(item, ElementBase): - if type(item) == XML_TYPE: - return self.appendxml(item) - else: - raise TypeError - self.xml.append(item.xml) - self.iterables.append(item) - return self + def append(self, item): + if not isinstance(item, ElementBase): + if type(item) == XML_TYPE: + return self.appendxml(item) + else: + raise TypeError + self.xml.append(item.xml) + self.iterables.append(item) + return self - def pop(self, idx=0): - aff = self.iterables.pop(idx) - self.xml.remove(aff.xml) - return aff + def pop(self, idx=0): + aff = self.iterables.pop(idx) + self.xml.remove(aff.xml) + return aff - def get(self, key, defaultvalue=None): - value = self[key] - if value is None or value == '': - return defaultvalue - return value + def get(self, key, defaultvalue=None): + value = self[key] + if value is None or value == '': + return defaultvalue + return value - def keys(self): - out = [] - out += [x for x in self.interfaces] - out += [x for x in self.plugins] - if self.iterables: - out.append('substanzas') - return tuple(out) + def keys(self): + out = [] + out += [x for x in self.interfaces] + out += [x for x in self.plugins] + if self.iterables: + out.append('substanzas') + return tuple(out) - def match(self, matchstring): - if isinstance(matchstring, str): - nodes = matchstring.split('/') - else: - nodes = matchstring - tagargs = nodes[0].split('@') - if tagargs[0] not in (self.plugins, self.plugin_attrib): return False - founditerable = False - for iterable in self.iterables: - if nodes[1:] == []: - break - founditerable = iterable.match(nodes[1:]) - if founditerable: break; - for evals in tagargs[1:]: - x,y = evals.split('=') - if self[x] != y: return False - if not founditerable and len(nodes) > 1: - next = nodes[1].split('@')[0] - if next in self.plugins: - return self.plugins[next].match(nodes[1:]) - else: - return False - return True + def match(self, matchstring): + if isinstance(matchstring, str): + nodes = matchstring.split('/') + else: + nodes = matchstring + tagargs = nodes[0].split('@') + if tagargs[0] not in (self.plugins, self.plugin_attrib): return False + founditerable = False + for iterable in self.iterables: + if nodes[1:] == []: + break + founditerable = iterable.match(nodes[1:]) + if founditerable: break; + for evals in tagargs[1:]: + x,y = evals.split('=') + if self[x] != y: return False + if not founditerable and len(nodes) > 1: + next = nodes[1].split('@')[0] + if next in self.plugins: + return self.plugins[next].match(nodes[1:]) + else: + return False + return True - def find(self, xpath): # for backwards compatiblity, expose elementtree interface - return self.xml.find(xpath) + def find(self, xpath): # for backwards compatiblity, expose elementtree interface + return self.xml.find(xpath) - def findall(self, xpath): - return self.xml.findall(xpath) + def findall(self, xpath): + return self.xml.findall(xpath) - def setup(self, xml=None): - if self.xml is None: - self.xml = xml - if self.xml is None: - for ename in self.name.split('/'): - new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) - if self.xml is None: - self.xml = new - else: - self.xml.append(new) - if self.parent is not None: - self.parent().xml.append(self.xml) - return True #had to generate XML - else: - return False + def setup(self, xml=None): + if self.xml is None: + self.xml = xml + if self.xml is None: + for ename in self.name.split('/'): + new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) + if self.xml is None: + self.xml = new + else: + self.xml.append(new) + if self.parent is not None: + self.parent().xml.append(self.xml) + return True #had to generate XML + else: + return False - def enable(self, attrib): - self.initPlugin(attrib) - return self + def enable(self, attrib): + self.initPlugin(attrib) + return self - def initPlugin(self, attrib): - if attrib not in self.plugins: - self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) + def initPlugin(self, attrib): + if attrib not in self.plugins: + self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) - def __getitem__(self, attrib): - if attrib == 'substanzas': - return self.iterables - elif attrib in self.interfaces: - if hasattr(self, "get%s" % attrib.title()): - return getattr(self, "get%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._getSubText(attrib) - else: - return self._getAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - return self.plugins[attrib] - else: - return '' + def __getitem__(self, attrib): + if attrib == 'substanzas': + return self.iterables + elif attrib in self.interfaces: + if hasattr(self, "get%s" % attrib.title()): + return getattr(self, "get%s" % attrib.title())() + else: + if attrib in self.sub_interfaces: + return self._getSubText(attrib) + else: + return self._getAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: self.initPlugin(attrib) + return self.plugins[attrib] + else: + return '' - def __setitem__(self, attrib, value): - if attrib in self.interfaces: - if value is not None: - if hasattr(self, "set%s" % attrib.title()): - getattr(self, "set%s" % attrib.title())(value,) - else: - if attrib in self.sub_interfaces: - return self._setSubText(attrib, text=value) - else: - self._setAttr(attrib, value) - else: - self.__delitem__(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - self.initPlugin(attrib) - self.plugins[attrib][attrib] = value - return self + def __setitem__(self, attrib, value): + if attrib in self.interfaces: + if value is not None: + if hasattr(self, "set%s" % attrib.title()): + getattr(self, "set%s" % attrib.title())(value,) + else: + if attrib in self.sub_interfaces: + return self._setSubText(attrib, text=value) + else: + self._setAttr(attrib, value) + else: + self.__delitem__(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: self.initPlugin(attrib) + self.initPlugin(attrib) + self.plugins[attrib][attrib] = value + return self - def __delitem__(self, attrib): - if attrib.lower() in self.interfaces: - if hasattr(self, "del%s" % attrib.title()): - getattr(self, "del%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._delSub(attrib) - else: - self._delAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib in self.plugins: - del self.plugins[attrib] - return self + def __delitem__(self, attrib): + if attrib.lower() in self.interfaces: + if hasattr(self, "del%s" % attrib.title()): + getattr(self, "del%s" % attrib.title())() + else: + if attrib in self.sub_interfaces: + return self._delSub(attrib) + else: + self._delAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib in self.plugins: + del self.plugins[attrib] + return self - def __eq__(self, other): - if not isinstance(other, ElementBase): - return False - values = self.getStanzaValues() - for key in other: - if key not in values or values[key] != other[key]: - return False - return True + def __eq__(self, other): + if not isinstance(other, ElementBase): + return False + values = self.getStanzaValues() + for key in other: + if key not in values or values[key] != other[key]: + return False + return True - def _setAttr(self, name, value): - if value is None or value == '': - self.__delitem__(name) - else: - self.xml.attrib[name] = value + def _setAttr(self, name, value): + if value is None or value == '': + self.__delitem__(name) + else: + self.xml.attrib[name] = value - def _delAttr(self, name): - if name in self.xml.attrib: - del self.xml.attrib[name] + def _delAttr(self, name): + if name in self.xml.attrib: + del self.xml.attrib[name] - def _getAttr(self, name, default=''): - return self.xml.attrib.get(name, default) + def _getAttr(self, name, default=''): + return self.xml.attrib.get(name, default) - def _getSubText(self, name): - if '}' not in name: - name = "{%s}%s" % (self.namespace, name) - stanza = self.xml.find(name) - if stanza is None or stanza.text is None: - return '' - else: - return stanza.text + def _getSubText(self, name): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + stanza = self.xml.find(name) + if stanza is None or stanza.text is None: + return '' + else: + return stanza.text - def _setSubText(self, name, attrib={}, text=None): - if '}' not in name: - name = "{%s}%s" % (self.namespace, name) - if text is None or text == '': - return self.__delitem__(name) - stanza = self.xml.find(name) - if stanza is None: - stanza = ET.Element(name) - self.xml.append(stanza) - stanza.text = text - return stanza + def _setSubText(self, name, attrib={}, text=None): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + if text is None or text == '': + return self.__delitem__(name) + stanza = self.xml.find(name) + if stanza is None: + stanza = ET.Element(name) + self.xml.append(stanza) + stanza.text = text + return stanza - def _delSub(self, name): - if '}' not in name: - name = "{%s}%s" % (self.namespace, name) - for child in self.xml.getchildren(): - if child.tag == name: - self.xml.remove(child) + def _delSub(self, name): + if '}' not in name: + name = "{%s}%s" % (self.namespace, name) + for child in self.xml.getchildren(): + if child.tag == name: + self.xml.remove(child) - def getStanzaValues(self): - out = {} - for interface in self.interfaces: - out[interface] = self[interface] - for pluginkey in self.plugins: - out[pluginkey] = self.plugins[pluginkey].getStanzaValues() - if self.iterables: - iterables = [] - for stanza in self.iterables: - iterables.append(stanza.getStanzaValues()) - iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) - out['substanzas'] = iterables - return out + def getStanzaValues(self): + out = {} + for interface in self.interfaces: + out[interface] = self[interface] + for pluginkey in self.plugins: + out[pluginkey] = self.plugins[pluginkey].getStanzaValues() + if self.iterables: + iterables = [] + for stanza in self.iterables: + iterables.append(stanza.getStanzaValues()) + iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) + out['substanzas'] = iterables + return out - def setStanzaValues(self, attrib): - for interface in attrib: - if interface == 'substanzas': - for subdict in attrib['substanzas']: - if '__childtag__' in subdict: - for subclass in self.subitem: - if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): - sub = subclass(parent=self) - sub.setStanzaValues(subdict) - self.iterables.append(sub) - break - elif interface in self.interfaces: - self[interface] = attrib[interface] - elif interface in self.plugin_attrib_map and interface not in self.plugins: - self.initPlugin(interface) - if interface in self.plugins: - self.plugins[interface].setStanzaValues(attrib[interface]) - return self + def setStanzaValues(self, attrib): + for interface in attrib: + if interface == 'substanzas': + for subdict in attrib['substanzas']: + if '__childtag__' in subdict: + for subclass in self.subitem: + if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): + sub = subclass(parent=self) + sub.setStanzaValues(subdict) + self.iterables.append(sub) + break + elif interface in self.interfaces: + self[interface] = attrib[interface] + elif interface in self.plugin_attrib_map and interface not in self.plugins: + self.initPlugin(interface) + if interface in self.plugins: + self.plugins[interface].setStanzaValues(attrib[interface]) + return self - def appendxml(self, xml): - self.xml.append(xml) - return self + def appendxml(self, xml): + self.xml.append(xml) + return self - def __copy__(self): - return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) + def __copy__(self): + return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) - def __str__(self): - return tostring(self.xml, xmlns='', stanza_ns=self.namespace) + def __str__(self): + return tostring(self.xml, xmlns='', stanza_ns=self.namespace) - def __repr__(self): - return self.__str__() + def __repr__(self): + return self.__str__() - #def __del__(self): #prevents garbage collection of reference cycle - # if self.parent is not None: - # self.parent.xml.remove(self.xml) +#def __del__(self): #prevents garbage collection of reference cycle +# if self.parent is not None: +# self.parent.xml.remove(self.xml) class StanzaBase(ElementBase): name = 'stanza' From 415520200efbc6f8a93d1e667f2b7d43b7994667 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 10:26:33 -0400 Subject: [PATCH 93/99] Updated ElementBase.__init__ --- sleekxmpp/xmlstream/stanzabase.py | 46 +++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index a6425b8..6762792 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -45,24 +45,36 @@ class ElementBase(object): subitem = None def __init__(self, xml=None, parent=None): - if parent is None: - self.parent = None - else: - self.parent = weakref.ref(parent) - self.xml = xml - self.plugins = {} - self.iterables = [] - self.idx = 0 - if not self.setup(xml): - for child in self.xml.getchildren(): - if child.tag in self.plugin_tag_map: - self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self) - if self.subitem is not None: - for sub in self.subitem: - if child.tag == "{%s}%s" % (sub.namespace, sub.name): - self.iterables.append(sub(xml=child, parent=self)) - break + """ + Create a new stanza object. + Arguments: + xml -- Initialize the stanza with optional existing XML. + parent -- Optional stanza object that contains this stanza. + """ + self.xml = xml + self.plugins = {} + self.iterables = [] + self.idx = 0 + if parent is None: + self.parent = None + else: + self.parent = weakref.ref(parent) + + if self.setup(xml): + # If we generated our own XML, then everything is ready. + return + + # Initialize values using provided XML + for child in self.xml.getchildren(): + 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: + if child.tag == "{%s}%s" % (sub.namespace, sub.name): + self.iterables.append(sub(child, self)) + break @property def attrib(self): #backwards compatibility From c721fb412618662d33fa73fb9f9e1f0c4f045fef Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 12:23:34 -0400 Subject: [PATCH 94/99] Added a generic checkStanza method to SleekTest. Updated the other check methods to use it. --- tests/sleektest.py | 168 +++++++++++++++++++++++---------------------- 1 file changed, 86 insertions(+), 82 deletions(-) diff --git a/tests/sleektest.py b/tests/sleektest.py index c7c7241..801253d 100644 --- a/tests/sleektest.py +++ b/tests/sleektest.py @@ -13,6 +13,7 @@ try: except ImportError: import Queue as queue +import sleekxmpp from sleekxmpp import ClientXMPP from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ET @@ -210,6 +211,84 @@ class SleekTest(unittest.TestCase): # ------------------------------------------------------------------ # Methods for comparing stanza objects to XML strings + def checkStanza(self, stanza_class, stanza, xml_string, + defaults=None, use_values=True): + """ + Create and compare several stanza objects to a correct XML string. + + If use_values is False, test using getStanzaValues() and + setStanzaValues() will not be used. + + Some stanzas provide default values for some interfaces, but + these defaults can be problematic for testing since they can easily + be forgotten when supplying the XML string. A list of interfaces that + use defaults may be provided and the generated stanzas will use the + default values for those interfaces if needed. + + However, correcting the supplied XML is not possible for interfaces + that add or remove XML elements. Only interfaces that map to XML + attributes may be set using the defaults parameter. The supplied XML + must take into account any extra elements that are included by default. + + Arguments: + stanza_class -- The class of the stanza being tested. + stanza -- The stanza object to test. + xml_string -- A string version of the correct XML expected. + defaults -- A list of stanza interfaces that have default + values. These interfaces will be set to their + defaults for the given and generated stanzas to + prevent unexpected test failures. + use_values -- Indicates if testing using getStanzaValues() and + setStanzaValues() should be used. Defaults to + True. + """ + xml = ET.fromstring(xml_string) + + # Ensure that top level namespaces are used, even if they + # were not provided. + self.fix_namespaces(stanza.xml, 'jabber:client') + self.fix_namespaces(xml, 'jabber:client') + + stanza2 = stanza_class(xml=xml) + + if use_values: + # Using getStanzaValues() and setStanzaValues() will add + # XML for any interface that has a default value. We need + # to set those defaults on the existing stanzas and XML + # so that they will compare correctly. + default_stanza = stanza_class() + if defaults is None: + defaults = [] + for interface in defaults: + stanza[interface] = stanza[interface] + stanza2[interface] = stanza2[interface] + # Can really only automatically add defaults for top + # level attribute values. Anything else must be accounted + # for in the provided XML string. + if interface not in xml.attrib: + if interface in default_stanza.xml.attrib: + value = default_stanza.xml.attrib[interface] + xml.attrib[interface] = value + + values = stanza2.getStanzaValues() + stanza3 = stanza_class() + stanza3.setStanzaValues(values) + + debug = "Three methods for creating stanzas do not match.\n" + debug += "Given XML:\n%s\n" % tostring(xml) + debug += "Given stanza:\n%s\n" % tostring(stanza.xml) + debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) + debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml) + result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml) + else: + debug = "Two methods for creating stanzas do not match.\n" + debug += "Given XML:\n%s\n" % tostring(xml) + debug += "Given stanza:\n%s\n" % tostring(stanza.xml) + debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) + result = self.compare(xml, stanza.xml, stanza2.xml) + + self.failUnless(result, debug) + def checkMessage(self, msg, xml_string, use_values=True): """ Create and compare several message stanza objects to a @@ -226,36 +305,9 @@ class SleekTest(unittest.TestCase): to True. """ - self.fix_namespaces(msg.xml, 'jabber:client') - debug = "Given Stanza:\n%s\n" % tostring(msg.xml) - - xml = ET.fromstring(xml_string) - self.fix_namespaces(xml, 'jabber:client') - - debug += "XML String:\n%s\n" % tostring(xml) - - msg2 = self.Message(xml) - debug += "Constructed Stanza:\n%s\n" % tostring(msg2.xml) - - if use_values: - # Ugly, but need to make sure the type attribute is set. - msg['type'] = msg['type'] - if xml.attrib.get('type', None) is None: - xml.attrib['type'] = 'normal' - msg2['type'] = msg2['type'] - debug += "XML String:\n%s\n" % tostring(xml) - - values = msg2.getStanzaValues() - msg3 = self.Message() - msg3.setStanzaValues(values) - - debug += "Second Constructed Stanza:\n%s\n" % tostring(msg3.xml) - debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, msg.xml, msg2.xml, msg3.xml), - debug) - else: - debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, msg.xml, msg2.xml), debug) + return self.checkStanza(Message, msg, xml_string, + defaults=['type'], + use_values = use_values) def checkIq(self, iq, xml_string, use_values=True): """ @@ -272,29 +324,7 @@ class SleekTest(unittest.TestCase): and setStanzaValues should be used. Defaults to True. """ - - self.fix_namespaces(iq.xml, 'jabber:client') - debug = "Given Stanza:\n%s\n" % tostring(iq.xml) - - xml = ET.fromstring(xml_string) - self.fix_namespaces(xml, 'jabber:client') - debug += "XML String:\n%s\n" % tostring(xml) - - iq2 = self.Iq(xml) - debug += "Constructed Stanza:\n%s\n" % tostring(iq2.xml) - - if use_values: - values = iq.getStanzaValues() - iq3 = self.Iq() - iq3.setStanzaValues(values) - - debug += "Second Constructed Stanza:\n%s\n" % tostring(iq3.xml) - debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, iq.xml, iq2.xml, iq3.xml), - debug) - else: - debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, iq.xml, iq2.xml), debug) + return self.checkStanza(Iq, iq, xml_string, use_values=use_values) def checkPresence(self, pres, xml_string, use_values=True): """ @@ -311,35 +341,9 @@ class SleekTest(unittest.TestCase): and setStanzaValues should be used. Defaults to True. """ - - self.fix_namespaces(pres.xml, 'jabber:client') - - xml = ET.fromstring(xml_string) - self.fix_namespaces(xml, 'jabber:client') - - pres2 = self.Presence(xml) - - # Ugly, but 'priority' has a default value and need to make - # sure it is set - pres['priority'] = pres['priority'] - pres2['priority'] = pres2['priority'] - - debug = "Given Stanza:\n%s\n" % tostring(pres.xml) - debug += "XML String:\n%s\n" % tostring(xml) - debug += "Constructed Stanza:\n%s\n" % tostring(pres2.xml) - - if use_values: - values = pres.getStanzaValues() - pres3 = self.Presence() - pres3.setStanzaValues(values) - - debug += "Second Constructed Stanza:\n%s\n" % tostring(pres3.xml) - debug = "Three methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, pres.xml, pres2.xml, pres3.xml), - debug) - else: - debug = "Two methods for creating stanza do not match:\n" + debug - self.failUnless(self.compare(xml, pres.xml, pres2.xml), debug) + return self.checkStanza(Presence, pres, xml_string, + defaults=['priority'], + use_values=use_values) # ------------------------------------------------------------------ # Methods for simulating stanza streams. From c20fab0f6c28bcbfbe54db687be056a9b5088ad4 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 12:24:47 -0400 Subject: [PATCH 95/99] Updated ElementBase.setup, and added unit tests. --- sleekxmpp/xmlstream/stanzabase.py | 48 ++++++++++++++++++++----------- tests/test_elementbase.py | 23 +++++++++++++++ 2 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 tests/test_elementbase.py diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 6762792..b11d59e 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -76,6 +76,38 @@ class ElementBase(object): self.iterables.append(sub(child, self)) break + def setup(self, xml=None): + """ + Initialize the stanza's XML contents. + + Will return True if XML was generated according to the stanza's + definition. + + Arguments: + xml -- Optional XML object to use for the stanza's content + instead of generating XML. + """ + if self.xml is None: + self.xml = xml + + if self.xml is None: + # Generate XML from the stanza definition + for ename in self.name.split('/'): + new = ET.Element("{%s}%s" % (self.namespace, ename)) + if self.xml is None: + self.xml = new + else: + last_xml.append(new) + last_xml = new + if self.parent is not None: + self.parent().xml.append(self.xml) + + # We had to generate XML + return True + else: + # We did not generate XML + return False + @property def attrib(self): #backwards compatibility return self @@ -159,22 +191,6 @@ class ElementBase(object): def findall(self, xpath): return self.xml.findall(xpath) - def setup(self, xml=None): - if self.xml is None: - self.xml = xml - if self.xml is None: - for ename in self.name.split('/'): - new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace}) - if self.xml is None: - self.xml = new - else: - self.xml.append(new) - if self.parent is not None: - self.parent().xml.append(self.xml) - return True #had to generate XML - else: - return False - def enable(self, attrib): self.initPlugin(attrib) return self diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py new file mode 100644 index 0000000..1b018b4 --- /dev/null +++ b/tests/test_elementbase.py @@ -0,0 +1,23 @@ +from . sleektest import * +from sleekxmpp.xmlstream.stanzabase import ElementBase + +class TestElementBase(SleekTest): + + def testExtendedName(self): + """Test element names of the form tag1/tag2/tag3.""" + + class TestStanza(ElementBase): + name = "foo/bar/baz" + namespace = "test" + + stanza = TestStanza() + self.checkStanza(TestStanza, stanza, """ + + + + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) From b580a3138d46933331cd9829c7dcb3f4135a194f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 12:51:07 -0400 Subject: [PATCH 96/99] Updated ElementBase.enable and ElementBase.initPlugin --- sleekxmpp/xmlstream/stanzabase.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index b11d59e..aba6333 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -108,6 +108,29 @@ class ElementBase(object): # We did not generate XML return False + def enable(self, attrib): + """ + Enable and initialize a stanza plugin. + + Alias for initPlugin. + + Arguments: + attrib -- The stanza interface for the plugin. + """ + return self.initPlugin(attrib) + + def initPlugin(self, attrib): + """ + Enable and initialize a stanza plugin. + + Arguments: + attrib -- The stanza interface for the plugin. + """ + if attrib not in self.plugins: + plugin_class = self.plugin_attrib_map[attrib] + self.plugins[attrib] = plugin_class(parent=self) + return self + @property def attrib(self): #backwards compatibility return self @@ -191,14 +214,6 @@ class ElementBase(object): def findall(self, xpath): return self.xml.findall(xpath) - def enable(self, attrib): - self.initPlugin(attrib) - return self - - def initPlugin(self, attrib): - if attrib not in self.plugins: - self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self) - def __getitem__(self, attrib): if attrib == 'substanzas': return self.iterables From fe49b8c377b8491ec5fd5bc74d44834bd384f6a8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 20:05:24 -0400 Subject: [PATCH 97/99] Updated getStanzaValues and setStanzaValues with docs and unit tests. --- sleekxmpp/xmlstream/stanzabase.py | 85 +++++++++++++++++------------ tests/test_elementbase.py | 90 +++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 33 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index aba6333..3e280d6 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -131,6 +131,58 @@ class ElementBase(object): self.plugins[attrib] = plugin_class(parent=self) return self + def getStanzaValues(self): + """ + Return a dictionary of the stanza's interface values. + + Stanza plugin values are included as nested dictionaries. + """ + values = {} + for interface in self.interfaces: + values[interface] = self[interface] + for plugin, stanza in self.plugins.items(): + values[plugin] = stanza.getStanzaValues() + if self.iterables: + iterables = [] + for stanza in self.iterables: + iterables.append(stanza.getStanzaValues()) + iterables[-1].update({ + '__childtag__': "{%s}%s" % (stanza.namespace, stanza.name) + }) + values['substanzas'] = iterables + return values + + def setStanzaValues(self, values): + """ + Set multiple stanza interface values using a dictionary. + + Stanza plugin values may be set using nested dictionaries. + + Arguments: + values -- A dictionary mapping stanza interface with values. + Plugin interfaces may accept a nested dictionary that + will be used recursively. + """ + for interface, value in values.items(): + if interface == 'substanzas': + for subdict in value: + if '__childtag__' in subdict: + for subclass in self.subitem: + child_tag = "{%s}%s" % (subclass.namespace, + subclass.name) + if subdict['__childtag__'] == child_tag: + sub = subclass(parent=self) + sub.setStanzaValues(subdict) + self.iterables.append(sub) + break + elif interface in self.interfaces: + self[interface] = value + elif interface in self.plugin_attrib_map: + if interface not in self.plugins: + self.initPlugin(interface) + self.plugins[interface].setStanzaValues(value) + return self + @property def attrib(self): #backwards compatibility return self @@ -313,39 +365,6 @@ class ElementBase(object): if child.tag == name: self.xml.remove(child) - def getStanzaValues(self): - out = {} - for interface in self.interfaces: - out[interface] = self[interface] - for pluginkey in self.plugins: - out[pluginkey] = self.plugins[pluginkey].getStanzaValues() - if self.iterables: - iterables = [] - for stanza in self.iterables: - iterables.append(stanza.getStanzaValues()) - iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)}) - out['substanzas'] = iterables - return out - - def setStanzaValues(self, attrib): - for interface in attrib: - if interface == 'substanzas': - for subdict in attrib['substanzas']: - if '__childtag__' in subdict: - for subclass in self.subitem: - if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name): - sub = subclass(parent=self) - sub.setStanzaValues(subdict) - self.iterables.append(sub) - break - elif interface in self.interfaces: - self[interface] = attrib[interface] - elif interface in self.plugin_attrib_map and interface not in self.plugins: - self.initPlugin(interface) - if interface in self.plugins: - self.plugins[interface].setStanzaValues(attrib[interface]) - return self - def appendxml(self, xml): self.xml.append(xml) return self diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index 1b018b4..99c397a 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -19,5 +19,95 @@ class TestElementBase(SleekTest): """) + def testGetStanzaValues(self): + """Test getStanzaValues using plugins and substanzas.""" + + class TestStanzaPlugin(ElementBase): + name = "foo2" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "foo2" + + class TestSubStanza(ElementBase): + name = "subfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + subitem = set((TestSubStanza,)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + stanza['bar'] = 'a' + stanza['foo2']['baz'] = 'b' + substanza = TestSubStanza() + substanza['bar'] = 'c' + stanza.append(substanza) + + values = stanza.getStanzaValues() + expected = {'bar': 'a', + 'baz': '', + 'foo2': {'bar': '', + 'baz': 'b'}, + 'substanzas': [{'__childtag__': '{foo}subfoo', + 'bar': 'c', + 'baz': ''}]} + self.failUnless(values == expected, + "Unexpected stanza values:\n%s\n%s" % (str(expected), str(values))) + + + def testSetStanzaValues(self): + """Test using setStanzaValues with substanzas and plugins.""" + + class TestStanzaPlugin(ElementBase): + name = "pluginfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "plugin_foo" + + class TestStanzaPlugin2(ElementBase): + name = "pluginfoo2" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "plugin_foo2" + + class TestSubStanza(ElementBase): + name = "subfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + subitem = set((TestSubStanza,)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + registerStanzaPlugin(TestStanza, TestStanzaPlugin2) + + stanza = TestStanza() + values = {'bar': 'a', + 'baz': '', + 'plugin_foo': {'bar': '', + 'baz': 'b'}, + 'plugin_foo2': {'bar': 'd', + 'baz': 'e'}, + 'substanzas': [{'__childtag__': '{foo}subfoo', + 'bar': 'c', + 'baz': ''}]} + stanza.setStanzaValues(values) + + self.checkStanza(TestStanza, stanza, """ + + + + + + """) + suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) From 2f6f4fc16d81fd01bc5a4569e0a1bc4ede4a3af8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 13 Aug 2010 21:33:11 -0400 Subject: [PATCH 98/99] Updated ElementBase.__getitem__ with docs and unit tests. --- sleekxmpp/xmlstream/stanzabase.py | 59 ++++++++++++++++++++++--------- tests/test_elementbase.py | 41 +++++++++++++++++++++ 2 files changed, 83 insertions(+), 17 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 3e280d6..962cf8e 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -183,6 +183,48 @@ class ElementBase(object): self.plugins[interface].setStanzaValues(value) return self + def __getitem__(self, attrib): + """ + Return the value of a stanza interface using dictionary-like syntax. + + Example: + >>> msg['body'] + 'Message contents' + + Stanza interfaces are typically mapped directly to the underlying XML + object, but can be overridden by the presence of a getAttrib method + (or getFoo where the interface is named foo, etc). + + The search order for interface value retrieval for an interface + named 'foo' is: + 1. The list of substanzas. + 2. The result of calling getFoo. + 3. The contents of the foo subelement, if foo is a sub interface. + 4. The value of the foo attribute of the XML object. + 5. The plugin named 'foo' + 6. An empty string. + + Arguments: + attrib -- The name of the requested stanza interface. + """ + if attrib == 'substanzas': + return self.iterables + elif attrib in self.interfaces: + get_method = "get%s" % attrib.title() + if hasattr(self, get_method): + return getattr(self, get_method)() + else: + if attrib in self.sub_interfaces: + return self._getSubText(attrib) + else: + return self._getAttr(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: + self.initPlugin(attrib) + return self.plugins[attrib] + else: + return '' + @property def attrib(self): #backwards compatibility return self @@ -266,23 +308,6 @@ class ElementBase(object): def findall(self, xpath): return self.xml.findall(xpath) - def __getitem__(self, attrib): - if attrib == 'substanzas': - return self.iterables - elif attrib in self.interfaces: - if hasattr(self, "get%s" % attrib.title()): - return getattr(self, "get%s" % attrib.title())() - else: - if attrib in self.sub_interfaces: - return self._getSubText(attrib) - else: - return self._getAttr(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - return self.plugins[attrib] - else: - return '' - def __setitem__(self, attrib, value): if attrib in self.interfaces: if value is not None: diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index 99c397a..d6fd457 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -109,5 +109,46 @@ class TestElementBase(SleekTest): """) + def testGetItem(self): + """Test accessing stanza interfaces.""" + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + sub_interfaces = set(('baz',)) + + class TestStanzaPlugin(ElementBase): + name = "foobar" + namespace = "foo" + plugin_attrib = "foobar" + interfaces = set(('fizz',)) + + TestStanza.subitem = (TestStanza,) + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + substanza = TestStanza() + stanza.append(substanza) + stanza.setStanzaValues({'bar': 'a', + 'baz': 'b', + 'foobar': {'fizz': 'c'}}) + + # Test non-plugin interfaces + expected = {'substanzas': [substanza], + 'bar': 'a', + 'baz': 'b', + 'meh': ''} + for interface, value in expected.items(): + result = stanza[interface] + self.failUnless(result == value, + "Incorrect stanza interface access result: %s" % result) + + # Test plugin interfaces + self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin), + "Incorrect plugin object result.") + self.failUnless(stanza['foobar']['fizz'] == 'c', + "Incorrect plugin subvalue result.") + suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) From e4240dd593207a5912de996c42451b3946f113b2 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 19 Aug 2010 14:21:58 -0400 Subject: [PATCH 99/99] Updated ElementBase.__setitem__ and added unit tests. --- sleekxmpp/xmlstream/stanzabase.py | 62 ++++++++++++++++++++++--------- tests/test_elementbase.py | 41 +++++++++++++++++++- 2 files changed, 84 insertions(+), 19 deletions(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 962cf8e..83a8ddf 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -225,6 +225,50 @@ class ElementBase(object): else: return '' + def __setitem__(self, attrib, value): + """ + Set the value of a stanza interface using dictionary-like syntax. + + Example: + >>> msg['body'] = "Hi!" + >>> msg['body'] + 'Hi!' + + Stanza interfaces are typically mapped directly to the underlying XML + object, but can be overridden by the presence of a setAttrib method + (or setFoo where the interface is named foo, etc). + + The effect of interface value assignment for an interface + named 'foo' will be one of: + 1. Delete the interface's contents if the value is None. + 2. Call setFoo, if it exists. + 3. Set the text of a foo element, if foo is in sub_interfaces. + 4. Set the value of a top level XML attribute name foo. + 5. Attempt to pass value to a plugin named foo using the plugin's + foo interface. + 6. Do nothing. + + Arguments: + attrib -- The name of the stanza interface to modify. + value -- The new value of the stanza interface. + """ + if attrib in self.interfaces: + if value is not None: + if hasattr(self, "set%s" % attrib.title()): + getattr(self, "set%s" % attrib.title())(value,) + else: + if attrib in self.sub_interfaces: + return self._setSubText(attrib, text=value) + else: + self._setAttr(attrib, value) + else: + self.__delitem__(attrib) + elif attrib in self.plugin_attrib_map: + if attrib not in self.plugins: + self.initPlugin(attrib) + self.plugins[attrib][attrib] = value + return self + @property def attrib(self): #backwards compatibility return self @@ -308,24 +352,6 @@ class ElementBase(object): def findall(self, xpath): return self.xml.findall(xpath) - def __setitem__(self, attrib, value): - if attrib in self.interfaces: - if value is not None: - if hasattr(self, "set%s" % attrib.title()): - getattr(self, "set%s" % attrib.title())(value,) - else: - if attrib in self.sub_interfaces: - return self._setSubText(attrib, text=value) - else: - self._setAttr(attrib, value) - else: - self.__delitem__(attrib) - elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: self.initPlugin(attrib) - self.initPlugin(attrib) - self.plugins[attrib][attrib] = value - return self - def __delitem__(self, attrib): if attrib.lower() in self.interfaces: if hasattr(self, "del%s" % attrib.title()): diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py index d6fd457..95502f5 100644 --- a/tests/test_elementbase.py +++ b/tests/test_elementbase.py @@ -115,9 +115,12 @@ class TestElementBase(SleekTest): class TestStanza(ElementBase): name = "foo" namespace = "foo" - interfaces = set(('bar', 'baz')) + interfaces = set(('bar', 'baz', 'qux')) sub_interfaces = set(('baz',)) + def getQux(self): + return 'qux' + class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" @@ -132,12 +135,14 @@ class TestElementBase(SleekTest): stanza.append(substanza) stanza.setStanzaValues({'bar': 'a', 'baz': 'b', + 'qux': 42, 'foobar': {'fizz': 'c'}}) # Test non-plugin interfaces expected = {'substanzas': [substanza], 'bar': 'a', 'baz': 'b', + 'qux': 'qux', 'meh': ''} for interface, value in expected.items(): result = stanza[interface] @@ -150,5 +155,39 @@ class TestElementBase(SleekTest): self.failUnless(stanza['foobar']['fizz'] == 'c', "Incorrect plugin subvalue result.") + def testSetItem(self): + """Test assigning to stanza interfaces.""" + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz', 'qux')) + sub_interfaces = set(('baz',)) + + def setQux(self, value): + pass + + class TestStanzaPlugin(ElementBase): + name = "foobar" + namespace = "foo" + plugin_attrib = "foobar" + interfaces = set(('foobar',)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + + stanza['bar'] = 'attribute!' + stanza['baz'] = 'element!' + stanza['qux'] = 'overridden' + stanza['foobar'] = 'plugin' + + self.checkStanza(TestStanza, stanza, """ + + element! + + + """) + suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)